From 2745fe85040355628b7499e16788ac28ace6aeb5 Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Mon, 22 Jun 2026 15:27:02 +0200 Subject: [PATCH 01/10] ci: disable ancestor commit job --- .github/workflows/ci.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bf8da3b07617..7967c7dcbc43 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,7 +32,10 @@ jobs: runs-on: ${{ github.repository == 'bitcoin/bitcoin' && 'warp-ubuntu-latest-x64-8x' || 'ubuntu-latest' }} env: TEST_RUNNER_PORT_MIN: "14000" # Use a larger port range to avoid colliding with other CI services. - if: github.event_name == 'pull_request' && github.event.pull_request.commits != 1 + # Disabled on this branch while intermediate consensus-draft commits are + # allowed to be non-buildable. + # Restore: github.event_name == 'pull_request' && github.event.pull_request.commits != 1 + if: ${{ false }} timeout-minutes: 360 # Use maximum time, see https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idtimeout-minutes. steps: - name: Determine fetch depth From b374a062aeeeba9453c91720b66b1a4d85fd8499 Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Wed, 27 May 2026 10:51:05 +0200 Subject: [PATCH 02/10] test: add v31.0 previous release hashes --- test/get_previous_releases.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/get_previous_releases.py b/test/get_previous_releases.py index cd77c55e4612..8f191784a3a6 100755 --- a/test/get_previous_releases.py +++ b/test/get_previous_releases.py @@ -91,6 +91,15 @@ "866a4b703a2095301151c17dcc753e19e4dba61ec68d19709ec4f81ff4320103": {"tag": "v28.2", "archive": "bitcoin-28.2-x86_64-apple-darwin.tar.gz"}, "98add5f220c01b387343b70edeb6273403fe081e22cd85fda132704cdcaa98aa": {"tag": "v28.2", "archive": "bitcoin-28.2-x86_64-linux-gnu.tar.gz"}, "da0869639c323bbf6f264f1829083b9514e10179b90c34b09d8cbcab8a1897e3": {"tag": "v28.2", "archive": "bitcoin-28.2-win64.zip"}, + + "4de1d568dedd48604f75132421bc0abeca432639589b49a3909c81db3a813112": {"tag": "v31.0", "archive": "bitcoin-31.0-aarch64-linux-gnu.tar.gz"}, + "8c19d007bfc73502625095ea4073af3a98ceb722d500556ab173bac5bcadd0d6": {"tag": "v31.0", "archive": "bitcoin-31.0-arm-linux-gnueabihf.tar.gz"}, + "a2d7a13b4da53d4a3e4c517f3a0269e2429813417bb320d3b268993cfdc545d0": {"tag": "v31.0", "archive": "bitcoin-31.0-arm64-apple-darwin.tar.gz"}, + "1d9c865aa0ccf675fc068e79d9fa57a5a70b59132fca38bb322a7d44ce2f0ff2": {"tag": "v31.0", "archive": "bitcoin-31.0-powerpc64-linux-gnu.tar.gz"}, + "7ece4ea365bba9b2008b27f0717ef6a518598a572edaa2815e775faadc53c136": {"tag": "v31.0", "archive": "bitcoin-31.0-riscv64-linux-gnu.tar.gz"}, + "56824dd705bc2a3b22d42e8aa02ed53498d491ff7c2c8aa96831333871887ead": {"tag": "v31.0", "archive": "bitcoin-31.0-x86_64-apple-darwin.tar.gz"}, + "d3e4c58a35b1d0a97a457462c94f55501ad167c660c245cb1ffa565641c65074": {"tag": "v31.0", "archive": "bitcoin-31.0-x86_64-linux-gnu.tar.gz"}, + "82fd2c504a0f20a31d4d13bd407783d6fc7bf17622d0ce85228a9b92694e03f0": {"tag": "v31.0", "archive": "bitcoin-31.0-win64.zip"}, } From 4a61ac1d5325240f27123cfbf217d55794213f4a Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Mon, 22 Jun 2026 13:11:54 +0200 Subject: [PATCH 03/10] consensus: extend block timestamps past 2106 Consensus changes in this draft: - The first block with nTime >= 2^32 must have a negative nVersion and use the new 84-byte header format. - Every subsequent block must continue to use the new header format. - Blocks using the new header format must have nTime >= 2^32. - Block timestamps, MTP, and timestamp comparisons used by consensus validation are represented as uint64_t, allowing the full uint64_t timestamp range. Tests added/updated: - Add feature_y2106_extended_time.py to exercise the post-2106 hardfork behavior against current and previous-release nodes, including the max uint64 timestamp reaching contextual validation. - Extend interface_ipc_mining.py to cover 64-bit submitSolution timestamps. --- .github/ci-windows-cross.py | 5 +- src/bench/wallet_create_tx.cpp | 2 +- src/chain.cpp | 5 +- src/chain.h | 26 +++-- src/consensus/tx_verify.cpp | 13 ++- src/headerssync.cpp | 6 +- src/headerssync.h | 7 +- src/interfaces/mining.h | 2 +- src/ipc/capnp/mining.capnp | 2 +- src/kernel/chain.h | 3 +- src/node/chainstate.cpp | 2 +- src/node/interfaces.cpp | 18 ++-- src/node/miner.cpp | 40 +++++--- src/node/miner.h | 4 +- src/pow.cpp | 14 +-- src/pow.h | 2 +- src/primitives/block.cpp | 4 +- src/primitives/block.h | 54 +++++++++- src/rpc/blockchain.cpp | 8 +- src/rpc/node.cpp | 7 +- src/signet.cpp | 5 +- src/test/fuzz/package_eval.cpp | 9 +- src/test/fuzz/pow.cpp | 2 +- src/test/fuzz/tx_pool.cpp | 9 +- src/test/fuzz/versionbits.cpp | 18 +++- src/test/pow_tests.cpp | 2 +- src/test/skiplist_tests.cpp | 4 +- src/validation.cpp | 65 ++++++++---- src/versionbits.cpp | 18 +++- .../feature_coinstatsindex_compatibility.py | 3 +- .../functional/feature_unsupported_utxo_db.py | 3 +- .../functional/feature_y2106_extended_time.py | 99 +++++++++++++++++++ test/functional/interface_ipc_mining.py | 6 ++ test/functional/rpc_blockchain.py | 7 +- test/functional/test_framework/blocktools.py | 3 + test/functional/test_framework/messages.py | 25 ++++- test/functional/test_runner.py | 1 + 37 files changed, 398 insertions(+), 105 deletions(-) create mode 100755 test/functional/feature_y2106_extended_time.py diff --git a/.github/ci-windows-cross.py b/.github/ci-windows-cross.py index bf13f81ac75c..c5d2dca609c4 100755 --- a/.github/ci-windows-cross.py +++ b/.github/ci-windows-cross.py @@ -115,7 +115,10 @@ def run_functional_tests(): "--tmpdir", str(Path(workspace) / "test_feature_unsupported_utxo_db"), ] - run(cmd_feature_unsupported_db) + # Exit code 77 is the functional test framework's skip code. + result = run(cmd_feature_unsupported_db, check=False) + if result.returncode not in (0, 77): + sys.exit(result.returncode) def run_unit_tests(): diff --git a/src/bench/wallet_create_tx.cpp b/src/bench/wallet_create_tx.cpp index 394f21ce187d..22fff5abd9da 100644 --- a/src/bench/wallet_create_tx.cpp +++ b/src/bench/wallet_create_tx.cpp @@ -50,7 +50,7 @@ using wallet::WALLET_FLAG_DESCRIPTORS; struct TipBlock { uint256 prev_block_hash; - int64_t prev_block_time; + uint64_t prev_block_time; int tip_height; }; diff --git a/src/chain.cpp b/src/chain.cpp index b066f0f7c689..9d5b9f6095f3 100644 --- a/src/chain.cpp +++ b/src/chain.cpp @@ -59,9 +59,10 @@ const CBlockIndex* CChain::FindFork(const CBlockIndex& index) const CBlockIndex* CChain::FindEarliestAtLeast(int64_t nTime, int height) const { - std::pair blockparams = std::make_pair(nTime, height); + const uint64_t min_time{nTime < 0 ? 0 : static_cast(nTime)}; + std::pair blockparams = std::make_pair(min_time, height); std::vector::const_iterator lower = std::lower_bound(vChain.begin(), vChain.end(), blockparams, - [](CBlockIndex* pBlock, const std::pair& blockparams) -> bool { return pBlock->GetBlockTimeMax() < blockparams.first || pBlock->nHeight < blockparams.second; }); + [](CBlockIndex* pBlock, const std::pair& blockparams) -> bool { return pBlock->GetBlockTimeMax() < blockparams.first || pBlock->nHeight < blockparams.second; }); return (lower == vChain.end() ? nullptr : *lower); } diff --git a/src/chain.h b/src/chain.h index 7701e9262a42..d9fc9d73beba 100644 --- a/src/chain.h +++ b/src/chain.h @@ -139,7 +139,7 @@ class CBlockIndex //! block header int32_t nVersion{0}; uint256 hashMerkleRoot{}; - uint32_t nTime{0}; + uint64_t nTime{0}; uint32_t nBits{0}; uint32_t nNonce{0}; @@ -149,7 +149,7 @@ class CBlockIndex int32_t nSequenceId{SEQ_ID_INIT_FROM_DISK}; //! (memory only) Maximum nTime in the chain up to and including this block. - unsigned int nTimeMax{0}; + uint64_t nTimeMax{0}; explicit CBlockIndex(const CBlockHeader& block) : nVersion{block.nVersion}, @@ -190,6 +190,9 @@ class CBlockIndex block.hashPrevBlock = pprev->GetBlockHash(); block.hashMerkleRoot = hashMerkleRoot; block.nTime = nTime; + if (block.nTime >= CBlockHeader::EXTENDED_TIME_THRESHOLD) { + block.SetExtendedTimeEncoding(); + } block.nBits = nBits; block.nNonce = nNonce; return block; @@ -218,23 +221,23 @@ class CBlockIndex return NodeSeconds{std::chrono::seconds{nTime}}; } - int64_t GetBlockTime() const + uint64_t GetBlockTime() const { - return (int64_t)nTime; + return nTime; } - int64_t GetBlockTimeMax() const + uint64_t GetBlockTimeMax() const { - return (int64_t)nTimeMax; + return nTimeMax; } static constexpr int nMedianTimeSpan = 11; - int64_t GetMedianTimePast() const + uint64_t GetMedianTimePast() const { - int64_t pmedian[nMedianTimeSpan]; - int64_t* pbegin = &pmedian[nMedianTimeSpan]; - int64_t* pend = &pmedian[nMedianTimeSpan]; + uint64_t pmedian[nMedianTimeSpan]; + uint64_t* pbegin = &pmedian[nMedianTimeSpan]; + uint64_t* pend = &pmedian[nMedianTimeSpan]; const CBlockIndex* pindex = this; for (int i = 0; i < nMedianTimeSpan && pindex; i++, pindex = pindex->pprev) @@ -366,6 +369,9 @@ class CDiskBlockIndex : public CBlockIndex block.hashPrevBlock = hashPrev; block.hashMerkleRoot = hashMerkleRoot; block.nTime = nTime; + if (block.nTime >= CBlockHeader::EXTENDED_TIME_THRESHOLD) { + block.SetExtendedTimeEncoding(); + } block.nBits = nBits; block.nNonce = nNonce; return block.GetHash(); diff --git a/src/consensus/tx_verify.cpp b/src/consensus/tx_verify.cpp index 4efed70fd411..9057251f2a9a 100644 --- a/src/consensus/tx_verify.cpp +++ b/src/consensus/tx_verify.cpp @@ -14,6 +14,15 @@ #include #include +#include +#include +#include + +static int64_t SaturatingBlockTime(uint64_t nTime) +{ + return nTime > static_cast(std::numeric_limits::max()) ? std::numeric_limits::max() : static_cast(nTime); +} + bool IsFinalTx(const CTransaction &tx, int nBlockHeight, int64_t nBlockTime) { if (tx.nLockTime == 0) @@ -71,7 +80,7 @@ std::pair CalculateSequenceLocks(const CTransaction &tx, int flags int nCoinHeight = prevHeights[txinIndex]; if (txin.nSequence & CTxIn::SEQUENCE_LOCKTIME_TYPE_FLAG) { - const int64_t nCoinTime{Assert(block.GetAncestor(std::max(nCoinHeight - 1, 0)))->GetMedianTimePast()}; + const int64_t nCoinTime{SaturatingBlockTime(Assert(block.GetAncestor(std::max(nCoinHeight - 1, 0)))->GetMedianTimePast())}; // NOTE: Subtract 1 to maintain nLockTime semantics // BIP 68 relative lock times have the semantics of calculating // the first block or time at which the transaction would be @@ -97,7 +106,7 @@ std::pair CalculateSequenceLocks(const CTransaction &tx, int flags bool EvaluateSequenceLocks(const CBlockIndex& block, std::pair lockPair) { assert(block.pprev); - int64_t nBlockTime = block.pprev->GetMedianTimePast(); + int64_t nBlockTime = SaturatingBlockTime(block.pprev->GetMedianTimePast()); if (lockPair.first >= block.nHeight || lockPair.second >= nBlockTime) return false; diff --git a/src/headerssync.cpp b/src/headerssync.cpp index 633ffef53adb..60e0ff01832b 100644 --- a/src/headerssync.cpp +++ b/src/headerssync.cpp @@ -10,9 +10,9 @@ #include #include -// Our memory analysis in headerssync-params.py assumes this many bytes for a -// CompressedHeader (we should re-calculate parameters if we compress further). -static_assert(sizeof(CompressedHeader) == 48); +// Our memory analysis in headerssync-params.py assumes at most this many bytes +// for a CompressedHeader (we should re-calculate parameters if we compress further). +static_assert(sizeof(CompressedHeader) <= 56); HeadersSyncState::HeadersSyncState(NodeId id, const Consensus::Params& consensus_params, diff --git a/src/headerssync.h b/src/headerssync.h index 6d720874411d..5e391ca1c2c5 100644 --- a/src/headerssync.h +++ b/src/headerssync.h @@ -22,7 +22,7 @@ struct CompressedHeader { // header int32_t nVersion{0}; uint256 hashMerkleRoot; - uint32_t nTime{0}; + uint64_t nTime{0}; uint32_t nBits{0}; uint32_t nNonce{0}; @@ -47,6 +47,9 @@ struct CompressedHeader { ret.hashPrevBlock = hash_prev_block; ret.hashMerkleRoot = hashMerkleRoot; ret.nTime = nTime; + if (ret.nTime >= CBlockHeader::EXTENDED_TIME_THRESHOLD) { + ret.SetExtendedTimeEncoding(); + } ret.nBits = nBits; ret.nNonce = nNonce; return ret; @@ -123,7 +126,7 @@ class HeadersSyncState { int64_t GetPresyncHeight() const { return m_current_height; } /** Return the block timestamp of the last header received during the PRESYNC phase. */ - uint32_t GetPresyncTime() const { return m_last_header_received.nTime; } + uint64_t GetPresyncTime() const { return m_last_header_received.nTime; } /** Return the amount of work in the chain received during the PRESYNC phase. */ arith_uint256 GetPresyncWork() const { return m_current_chain_work; } diff --git a/src/interfaces/mining.h b/src/interfaces/mining.h index ff4f87109f92..e4ef993100b7 100644 --- a/src/interfaces/mining.h +++ b/src/interfaces/mining.h @@ -75,7 +75,7 @@ class BlockTemplate * the solved block is constructed and broadcast by multiple nodes * (e.g. both the miner who constructed the template and the pool). */ - virtual bool submitSolution(uint32_t version, uint32_t timestamp, uint32_t nonce, CTransactionRef coinbase) = 0; + virtual bool submitSolution(uint32_t version, uint64_t timestamp, uint32_t nonce, CTransactionRef coinbase) = 0; /** * Waits for fees in the next block to rise, a new tip or the timeout. diff --git a/src/ipc/capnp/mining.capnp b/src/ipc/capnp/mining.capnp index a6dd8d71f315..0776ded59a02 100644 --- a/src/ipc/capnp/mining.capnp +++ b/src/ipc/capnp/mining.capnp @@ -36,7 +36,7 @@ interface BlockTemplate $Proxy.wrap("interfaces::BlockTemplate") { getTxSigops @4 (context: Proxy.Context) -> (result: List(Int64)); getCoinbaseTx @5 (context: Proxy.Context) -> (result: CoinbaseTx); getCoinbaseMerklePath @6 (context: Proxy.Context) -> (result: List(Data)); - submitSolution @7 (context: Proxy.Context, version: UInt32, timestamp: UInt32, nonce: UInt32, coinbase :Data) -> (result: Bool); + submitSolution @7 (context: Proxy.Context, version: UInt32, timestamp: UInt64, nonce: UInt32, coinbase :Data) -> (result: Bool); waitNext @8 (context: Proxy.Context, options: BlockWaitOptions) -> (result: BlockTemplate); interruptWait @9() -> (); } diff --git a/src/kernel/chain.h b/src/kernel/chain.h index 5afa51f4dda0..144081c7b821 100644 --- a/src/kernel/chain.h +++ b/src/kernel/chain.h @@ -7,6 +7,7 @@ #include +#include #include class CBlock; @@ -26,7 +27,7 @@ struct BlockInfo { const CBlockUndo* undo_data = nullptr; // The maximum time in the chain up to and including this block. // A timestamp that can only move forward. - unsigned int chain_time_max{0}; + int64_t chain_time_max{0}; BlockInfo(const uint256& hash LIFETIMEBOUND) : hash(hash) {} }; diff --git a/src/node/chainstate.cpp b/src/node/chainstate.cpp index 1725fe70bd93..e352f67e90af 100644 --- a/src/node/chainstate.cpp +++ b/src/node/chainstate.cpp @@ -248,7 +248,7 @@ ChainstateLoadResult VerifyLoadedChainstate(ChainstateManager& chainman, const C for (auto& chainstate : chainman.m_chainstates) { if (!is_coinsview_empty(*chainstate)) { const CBlockIndex* tip = chainstate->m_chain.Tip(); - if (tip && tip->nTime > GetTime() + MAX_FUTURE_BLOCK_TIME) { + if (tip && tip->nTime > static_cast(std::max(GetTime(), 0)) + MAX_FUTURE_BLOCK_TIME) { return {ChainstateLoadStatus::FAILURE, _("The block database contains a block which appears to be from the future. " "This may be due to your computer's date and time being set incorrectly. " "Only rebuild the block database if you are sure that your computer's date and time are correct")}; diff --git a/src/node/interfaces.cpp b/src/node/interfaces.cpp index 3e34c3abf398..b7f0abc49401 100644 --- a/src/node/interfaces.cpp +++ b/src/node/interfaces.cpp @@ -75,6 +75,7 @@ #include #include #include +#include #include #include #include @@ -101,6 +102,11 @@ using node::BlockWaitOptions; using node::CoinbaseTx; using util::Join; +static int64_t SaturatingBlockTime(uint64_t nTime) +{ + return nTime > static_cast(std::numeric_limits::max()) ? std::numeric_limits::max() : static_cast(nTime); +} + namespace node { // All members of the classes in this namespace are intentionally public, as the // classes themselves are private. @@ -304,7 +310,7 @@ class NodeImpl : public Node auto best_header = chainman().m_best_header; if (best_header) { height = best_header->nHeight; - block_time = best_header->GetBlockTime(); + block_time = SaturatingBlockTime(best_header->GetBlockTime()); return true; } return false; @@ -423,7 +429,7 @@ class NodeImpl : public Node std::unique_ptr handleNotifyBlockTip(NotifyBlockTipFn fn) override { return MakeSignalHandler(::uiInterface.NotifyBlockTip.connect([fn](SynchronizationState sync_state, const CBlockIndex& block, double verification_progress) { - fn(sync_state, BlockTip{block.nHeight, block.GetBlockTime(), block.GetBlockHash()}, verification_progress); + fn(sync_state, BlockTip{block.nHeight, SaturatingBlockTime(block.GetBlockTime()), block.GetBlockHash()}, verification_progress); })); } std::unique_ptr handleNotifyHeaderTip(NotifyHeaderTipFn fn) override @@ -449,9 +455,9 @@ bool FillBlock(const CBlockIndex* index, const FoundBlock& block, UniqueLockGetBlockHash(); if (block.m_height) *block.m_height = index->nHeight; - if (block.m_time) *block.m_time = index->GetBlockTime(); - if (block.m_max_time) *block.m_max_time = index->GetBlockTimeMax(); - if (block.m_mtp_time) *block.m_mtp_time = index->GetMedianTimePast(); + if (block.m_time) *block.m_time = SaturatingBlockTime(index->GetBlockTime()); + if (block.m_max_time) *block.m_max_time = SaturatingBlockTime(index->GetBlockTimeMax()); + if (block.m_mtp_time) *block.m_mtp_time = SaturatingBlockTime(index->GetMedianTimePast()); if (block.m_in_active_chain) *block.m_in_active_chain = active[index->nHeight] == index; if (block.m_locator) { *block.m_locator = GetLocator(index); } if (block.m_next_block) FillBlock(active[index->nHeight] == index ? active[index->nHeight + 1] : nullptr, *block.m_next_block, lock, active, blockman); @@ -917,7 +923,7 @@ class BlockTemplateImpl : public BlockTemplate return TransactionMerklePath(m_block_template->block, 0); } - bool submitSolution(uint32_t version, uint32_t timestamp, uint32_t nonce, CTransactionRef coinbase) override + bool submitSolution(uint32_t version, uint64_t timestamp, uint32_t nonce, CTransactionRef coinbase) override { AddMerkleRootAndCoinbase(m_block_template->block, std::move(coinbase), version, timestamp, nonce); std::string reason; diff --git a/src/node/miner.cpp b/src/node/miner.cpp index ccd9cc7c57ac..f920efb51b7e 100644 --- a/src/node/miner.cpp +++ b/src/node/miner.cpp @@ -46,6 +46,7 @@ #include #include #include +#include #include #include #include @@ -54,35 +55,44 @@ namespace node { -int64_t GetMinimumTime(const CBlockIndex* pindexPrev, const int64_t difficulty_adjustment_interval) +static int64_t SaturatingBlockTime(uint64_t nTime) { - int64_t min_time{pindexPrev->GetMedianTimePast() + 1}; + return nTime > static_cast(std::numeric_limits::max()) ? std::numeric_limits::max() : static_cast(nTime); +} + +uint64_t GetMinimumTime(const CBlockIndex* pindexPrev, const int64_t difficulty_adjustment_interval) +{ + uint64_t min_time{pindexPrev->GetMedianTimePast() + 1}; // Height of block to be mined. const int height{pindexPrev->nHeight + 1}; // Account for BIP94 timewarp rule on all networks. This makes future // activation safer. if (height % difficulty_adjustment_interval == 0) { - min_time = std::max(min_time, pindexPrev->GetBlockTime() - MAX_TIMEWARP); + min_time = std::max(min_time, pindexPrev->nTime > MAX_TIMEWARP ? pindexPrev->nTime - MAX_TIMEWARP : 0); } return min_time; } int64_t UpdateTime(CBlockHeader* pblock, const Consensus::Params& consensusParams, const CBlockIndex* pindexPrev) { - int64_t nOldTime = pblock->nTime; - int64_t nNewTime{std::max(GetMinimumTime(pindexPrev, consensusParams.DifficultyAdjustmentInterval()), - TicksSinceEpoch(NodeClock::now()))}; + const uint64_t nOldTime{pblock->nTime}; + const int64_t now{TicksSinceEpoch(NodeClock::now())}; + uint64_t nNewTime{std::max(GetMinimumTime(pindexPrev, consensusParams.DifficultyAdjustmentInterval()), + static_cast(std::max(now, 0)))}; if (nOldTime < nNewTime) { pblock->nTime = nNewTime; } + if (pblock->nTime >= CBlockHeader::EXTENDED_TIME_THRESHOLD) { + pblock->SetExtendedTimeEncoding(); + } // Updating time can change work required on testnet: if (consensusParams.fPowAllowMinDifficultyBlocks) { pblock->nBits = GetNextWorkRequired(pindexPrev, pblock, consensusParams); } - return nNewTime - nOldTime; + return SaturatingBlockTime(pblock->nTime - nOldTime); } void RegenerateCommitments(CBlock& block, ChainstateManager& chainman) @@ -149,7 +159,10 @@ std::unique_ptr BlockAssembler::CreateNewBlock() } pblock->nTime = TicksSinceEpoch(NodeClock::now()); - m_lock_time_cutoff = pindexPrev->GetMedianTimePast(); + if (pblock->nTime >= CBlockHeader::EXTENDED_TIME_THRESHOLD) { + pblock->SetExtendedTimeEncoding(); + } + m_lock_time_cutoff = SaturatingBlockTime(pindexPrev->GetMedianTimePast()); if (m_mempool) { LOCK(m_mempool->cs); @@ -340,7 +353,7 @@ void BlockAssembler::addChunks() } } -void AddMerkleRootAndCoinbase(CBlock& block, CTransactionRef coinbase, uint32_t version, uint32_t timestamp, uint32_t nonce) +void AddMerkleRootAndCoinbase(CBlock& block, CTransactionRef coinbase, uint32_t version, uint64_t timestamp, uint32_t nonce) { if (block.vtx.size() == 0) { block.vtx.emplace_back(coinbase); @@ -349,6 +362,10 @@ void AddMerkleRootAndCoinbase(CBlock& block, CTransactionRef coinbase, uint32_t } block.nVersion = version; block.nTime = timestamp; + block.SetLegacyTimeEncoding(); + if (block.nTime >= CBlockHeader::EXTENDED_TIME_THRESHOLD) { + block.SetExtendedTimeEncoding(); + } block.nNonce = nonce; block.hashMerkleRoot = BlockMerkleRoot(block); @@ -469,8 +486,9 @@ std::unique_ptr WaitAndCreateNewBlock(ChainstateManager& chainma // On test networks return a minimum difficulty block after 20 minutes if (!tip_changed && allow_min_difficulty) { - const NodeClock::time_point tip_time{std::chrono::seconds{chainman.ActiveChain().Tip()->GetBlockTime()}}; - if (now > tip_time + 20min) { + const CBlockIndex* tip{chainman.ActiveChain().Tip()}; + if (tip->nTime <= static_cast(std::numeric_limits::max()) && + now > NodeClock::time_point{std::chrono::seconds{static_cast(tip->nTime)}} + 20min) { tip_changed = true; } } diff --git a/src/node/miner.h b/src/node/miner.h index af3273073295..fe6d52a321bc 100644 --- a/src/node/miner.h +++ b/src/node/miner.h @@ -120,7 +120,7 @@ class BlockAssembler * accounts for the BIP94 timewarp rule, so does not necessarily reflect the * consensus limit. */ -int64_t GetMinimumTime(const CBlockIndex* pindexPrev, int64_t difficulty_adjustment_interval); +uint64_t GetMinimumTime(const CBlockIndex* pindexPrev, int64_t difficulty_adjustment_interval); int64_t UpdateTime(CBlockHeader* pblock, const Consensus::Params& consensusParams, const CBlockIndex* pindexPrev); @@ -128,7 +128,7 @@ int64_t UpdateTime(CBlockHeader* pblock, const Consensus::Params& consensusParam void RegenerateCommitments(CBlock& block, ChainstateManager& chainman); /* Compute the block's merkle root, insert or replace the coinbase transaction and the merkle root into the block */ -void AddMerkleRootAndCoinbase(CBlock& block, CTransactionRef coinbase, uint32_t version, uint32_t timestamp, uint32_t nonce); +void AddMerkleRootAndCoinbase(CBlock& block, CTransactionRef coinbase, uint32_t version, uint64_t timestamp, uint32_t nonce); //! Submit a block and capture the validation state via the BlockChecked callback. //! Returns whether ProcessNewBlock accepted the block. diff --git a/src/pow.cpp b/src/pow.cpp index 9a9f4e58178b..02bd89f1779c 100644 --- a/src/pow.cpp +++ b/src/pow.cpp @@ -47,17 +47,19 @@ unsigned int GetNextWorkRequired(const CBlockIndex* pindexLast, const CBlockHead return CalculateNextWorkRequired(pindexLast, pindexFirst->GetBlockTime(), params); } -unsigned int CalculateNextWorkRequired(const CBlockIndex* pindexLast, int64_t nFirstBlockTime, const Consensus::Params& params) +unsigned int CalculateNextWorkRequired(const CBlockIndex* pindexLast, uint64_t nFirstBlockTime, const Consensus::Params& params) { if (params.fPowNoRetargeting) return pindexLast->nBits; // Limit adjustment step - int64_t nActualTimespan = pindexLast->GetBlockTime() - nFirstBlockTime; - if (nActualTimespan < params.nPowTargetTimespan/4) - nActualTimespan = params.nPowTargetTimespan/4; - if (nActualTimespan > params.nPowTargetTimespan*4) - nActualTimespan = params.nPowTargetTimespan*4; + const uint64_t nLastBlockTime{pindexLast->GetBlockTime()}; + const uint64_t nPowTargetTimespan{static_cast(params.nPowTargetTimespan)}; + uint64_t nActualTimespan{nLastBlockTime > nFirstBlockTime ? nLastBlockTime - nFirstBlockTime : 0}; + if (nActualTimespan < nPowTargetTimespan / 4) + nActualTimespan = nPowTargetTimespan / 4; + if (nActualTimespan > nPowTargetTimespan * 4) + nActualTimespan = nPowTargetTimespan * 4; // Retarget const arith_uint256 bnPowLimit = UintToArith256(params.powLimit); diff --git a/src/pow.h b/src/pow.h index 26004c46187c..ee2d9779a086 100644 --- a/src/pow.h +++ b/src/pow.h @@ -27,7 +27,7 @@ class arith_uint256; std::optional DeriveTarget(unsigned int nBits, uint256 pow_limit); unsigned int GetNextWorkRequired(const CBlockIndex* pindexLast, const CBlockHeader *pblock, const Consensus::Params&); -unsigned int CalculateNextWorkRequired(const CBlockIndex* pindexLast, int64_t nFirstBlockTime, const Consensus::Params&); +unsigned int CalculateNextWorkRequired(const CBlockIndex* pindexLast, uint64_t nFirstBlockTime, const Consensus::Params&); /** Check whether a block hash satisfies the proof-of-work requirement specified by nBits */ bool CheckProofOfWork(uint256 hash, unsigned int nBits, const Consensus::Params&); diff --git a/src/primitives/block.cpp b/src/primitives/block.cpp index 0e81b74d375b..0dd3ac4d9eed 100644 --- a/src/primitives/block.cpp +++ b/src/primitives/block.cpp @@ -19,12 +19,12 @@ uint256 CBlockHeader::GetHash() const std::string CBlock::ToString() const { std::stringstream s; - s << strprintf("CBlock(hash=%s, ver=0x%08x, hashPrevBlock=%s, hashMerkleRoot=%s, nTime=%u, nBits=%08x, nNonce=%u, vtx=%u)\n", + s << strprintf("CBlock(hash=%s, ver=0x%08x, hashPrevBlock=%s, hashMerkleRoot=%s, nTime=%d, nBits=%08x, nNonce=%u, vtx=%u)\n", GetHash().ToString(), nVersion, hashPrevBlock.ToString(), hashMerkleRoot.ToString(), - nTime, nBits, nNonce, + GetBlockTime(), nBits, nNonce, vtx.size()); for (const auto& tx : vtx) { s << " " << tx->ToString() << "\n"; diff --git a/src/primitives/block.h b/src/primitives/block.h index 8ca4fb4800ee..1e2ce55cb65e 100644 --- a/src/primitives/block.h +++ b/src/primitives/block.h @@ -30,16 +30,48 @@ class CBlockHeader int32_t nVersion; uint256 hashPrevBlock; uint256 hashMerkleRoot; - uint32_t nTime; + uint64_t nTime; uint32_t nBits; uint32_t nNonce; + //! Memory-only marker for the extended header encoding. This draft derives + //! the value from a negative nVersion when parsing from the wire or disk. + bool m_extended; + + static constexpr uint64_t EXTENDED_TIME_THRESHOLD{uint64_t{1} << 32}; CBlockHeader() { SetNull(); } - SERIALIZE_METHODS(CBlockHeader, obj) { READWRITE(obj.nVersion, obj.hashPrevBlock, obj.hashMerkleRoot, obj.nTime, obj.nBits, obj.nNonce); } + template + void Serialize(Stream& s) const + { + const uint32_t time_low{static_cast(nTime)}; + s << nVersion << hashPrevBlock << hashMerkleRoot; + s << time_low; + if (m_extended) { + const uint32_t time_high{static_cast(nTime >> 32)}; + s << time_high; + } + s << nBits << nNonce; + } + + template + void Unserialize(Stream& s) + { + uint32_t time_low; + s >> nVersion >> hashPrevBlock >> hashMerkleRoot; + m_extended = nVersion < 0; + s >> time_low; + nTime = time_low; + if (m_extended) { + uint32_t time_high; + s >> time_high; + nTime |= uint64_t{time_high} << 32; + } + s >> nBits >> nNonce; + } void SetNull() { @@ -49,6 +81,7 @@ class CBlockHeader nTime = 0; nBits = 0; nNonce = 0; + m_extended = false; } bool IsNull() const @@ -58,14 +91,27 @@ class CBlockHeader uint256 GetHash() const; + void SetExtendedTimeEncoding() + { + m_extended = true; + if (nVersion >= 0) { + nVersion = nVersion == 0 ? -1 : -nVersion; + } + } + + void SetLegacyTimeEncoding() + { + m_extended = false; + } + NodeSeconds Time() const { return NodeSeconds{std::chrono::seconds{nTime}}; } - int64_t GetBlockTime() const + uint64_t GetBlockTime() const { - return (int64_t)nTime; + return nTime; } }; diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp index d78b82bce3e5..9b71e30f660a 100644 --- a/src/rpc/blockchain.cpp +++ b/src/rpc/blockchain.cpp @@ -59,6 +59,7 @@ #include #include +#include #include #include #include @@ -69,6 +70,11 @@ using kernel::CCoinsStats; using kernel::CoinStatsHashType; +static int64_t SaturatingBlockTime(uint64_t nTime) +{ + return nTime > static_cast(std::numeric_limits::max()) ? std::numeric_limits::max() : static_cast(nTime); +} + using interfaces::BlockRef; using interfaces::Mining; using node::BlockManager; @@ -1882,7 +1888,7 @@ static RPCMethod getchaintxstats() } const CBlockIndex& past_block{*CHECK_NONFATAL(pindex->GetAncestor(pindex->nHeight - blockcount))}; - const int64_t nTimeDiff{pindex->GetMedianTimePast() - past_block.GetMedianTimePast()}; + const int64_t nTimeDiff{SaturatingBlockTime(pindex->GetMedianTimePast() - past_block.GetMedianTimePast())}; UniValue ret(UniValue::VOBJ); ret.pushKV("time", pindex->nTime); diff --git a/src/rpc/node.cpp b/src/rpc/node.cpp index 6d0d5511e334..3d29024a65a7 100644 --- a/src/rpc/node.cpp +++ b/src/rpc/node.cpp @@ -5,6 +5,7 @@ #include // IWYU pragma: keep +#include #include #include #include @@ -62,9 +63,9 @@ static RPCMethod setmocktime() LOCK(cs_main); const int64_t time{request.params[0].getInt()}; - // block timestamps are uint32_t, so mocking time beyond that is meaningless for anything - // consensus-related and can cause integer overflow/truncation issues in time arithmetic. - constexpr int64_t max_time{std::numeric_limits::max()}; + // Block timestamps can use the extended 64-bit encoding after the 32-bit + // timestamp range is exhausted. + constexpr int64_t max_time{std::numeric_limits::max() - MAX_FUTURE_BLOCK_TIME}; if (time < 0 || time > max_time) { throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Mocktime must be in the range [0, %s], not %s.", max_time, time)); } diff --git a/src/signet.cpp b/src/signet.cpp index 9206f47752a4..89d6389fccdc 100644 --- a/src/signet.cpp +++ b/src/signet.cpp @@ -115,7 +115,10 @@ std::optional SignetTxs::Create(const CBlock& block, const CScript& c writer << block.nVersion; writer << block.hashPrevBlock; writer << signet_merkle; - writer << block.nTime; + writer << static_cast(block.nTime); + if (block.m_extended) { + writer << static_cast(block.nTime >> 32); + } tx_to_spend.vin[0].scriptSig << block_data; tx_spending.vin[0].prevout = COutPoint(tx_to_spend.GetHash(), 0); diff --git a/src/test/fuzz/package_eval.cpp b/src/test/fuzz/package_eval.cpp index bcf76cf35251..6623aa4a2f35 100644 --- a/src/test/fuzz/package_eval.cpp +++ b/src/test/fuzz/package_eval.cpp @@ -131,9 +131,12 @@ struct TransactionsDelta final : public CValidationInterface { void MockTime(FuzzedDataProvider& fuzzed_data_provider, const Chainstate& chainstate) { - const auto time = ConsumeTime(fuzzed_data_provider, - chainstate.m_chain.Tip()->GetMedianTimePast() + 1, - std::numeric_limitsnTime)>::max()); + static constexpr int64_t MAX_FUZZ_MOCK_TIME{std::numeric_limits::max()}; + const uint64_t tip_mtp{chainstate.m_chain.Tip()->GetMedianTimePast()}; + const int64_t min_time{tip_mtp >= static_cast(MAX_FUZZ_MOCK_TIME) ? + MAX_FUZZ_MOCK_TIME : + static_cast(tip_mtp + 1)}; + const auto time = ConsumeTime(fuzzed_data_provider, min_time, MAX_FUZZ_MOCK_TIME); SetMockTime(time); } diff --git a/src/test/fuzz/pow.cpp b/src/test/fuzz/pow.cpp index a9151ca500c0..05daab351dff 100644 --- a/src/test/fuzz/pow.cpp +++ b/src/test/fuzz/pow.cpp @@ -63,7 +63,7 @@ FUZZ_TARGET(pow, .init = initialize_pow) } { (void)GetBlockProof(current_block); - (void)CalculateNextWorkRequired(¤t_block, fuzzed_data_provider.ConsumeIntegralInRange(0, std::numeric_limits::max()), consensus_params); + (void)CalculateNextWorkRequired(¤t_block, fuzzed_data_provider.ConsumeIntegral(), consensus_params); if (current_block.nHeight != std::numeric_limits::max() && current_block.nHeight - (consensus_params.DifficultyAdjustmentInterval() - 1) >= 0) { (void)GetNextWorkRequired(¤t_block, &(*block_header), consensus_params); } diff --git a/src/test/fuzz/tx_pool.cpp b/src/test/fuzz/tx_pool.cpp index 23c40c9fc440..55ada0ec35a9 100644 --- a/src/test/fuzz/tx_pool.cpp +++ b/src/test/fuzz/tx_pool.cpp @@ -167,9 +167,12 @@ void Finish(FuzzedDataProvider& fuzzed_data_provider, MockedTxPool& tx_pool, Cha void MockTime(FuzzedDataProvider& fuzzed_data_provider, const Chainstate& chainstate) { - const auto time = ConsumeTime(fuzzed_data_provider, - chainstate.m_chain.Tip()->GetMedianTimePast() + 1, - std::numeric_limitsnTime)>::max()); + static constexpr int64_t MAX_FUZZ_MOCK_TIME{std::numeric_limits::max()}; + const uint64_t tip_mtp{chainstate.m_chain.Tip()->GetMedianTimePast()}; + const int64_t min_time{tip_mtp >= static_cast(MAX_FUZZ_MOCK_TIME) ? + MAX_FUZZ_MOCK_TIME : + static_cast(tip_mtp + 1)}; + const auto time = ConsumeTime(fuzzed_data_provider, min_time, MAX_FUZZ_MOCK_TIME); SetMockTime(time); } diff --git a/src/test/fuzz/versionbits.cpp b/src/test/fuzz/versionbits.cpp index a161f5b40bf1..447d41a528c3 100644 --- a/src/test/fuzz/versionbits.cpp +++ b/src/test/fuzz/versionbits.cpp @@ -22,6 +22,16 @@ #include namespace { +bool MedianTimePastBefore(uint64_t median_time_past, int64_t time) +{ + return time > 0 && median_time_past < static_cast(time); +} + +bool MedianTimePastAtOrAfter(uint64_t median_time_past, int64_t time) +{ + return time <= 0 || median_time_past >= static_cast(time); +} + class TestConditionChecker : public VersionBitsConditionChecker { private: @@ -285,13 +295,13 @@ FUZZ_TARGET(versionbits, .init = initialize) case ThresholdState::DEFINED: assert(since == 0); assert(exp_state == ThresholdState::DEFINED); - assert(current_block->GetMedianTimePast() < dep.nStartTime); + assert(MedianTimePastBefore(current_block->GetMedianTimePast(), dep.nStartTime)); return; case ThresholdState::STARTED: - assert(current_block->GetMedianTimePast() >= dep.nStartTime); + assert(MedianTimePastAtOrAfter(current_block->GetMedianTimePast(), dep.nStartTime)); if (exp_state == ThresholdState::STARTED) { assert(blocks_sig < dep.threshold); - assert(current_block->GetMedianTimePast() < dep.nTimeout); + assert(MedianTimePastBefore(current_block->GetMedianTimePast(), dep.nTimeout)); } else { assert(exp_state == ThresholdState::DEFINED); } @@ -309,7 +319,7 @@ FUZZ_TARGET(versionbits, .init = initialize) assert(exp_state == ThresholdState::ACTIVE || exp_state == ThresholdState::LOCKED_IN); return; case ThresholdState::FAILED: - assert(never_active_test || current_block->GetMedianTimePast() >= dep.nTimeout); + assert(never_active_test || MedianTimePastAtOrAfter(current_block->GetMedianTimePast(), dep.nTimeout)); if (exp_state == ThresholdState::STARTED) { assert(blocks_sig < dep.threshold); } else { diff --git a/src/test/pow_tests.cpp b/src/test/pow_tests.cpp index f6123401ad0e..1ee1730e8569 100644 --- a/src/test/pow_tests.cpp +++ b/src/test/pow_tests.cpp @@ -153,7 +153,7 @@ BOOST_AUTO_TEST_CASE(GetBlockProofEquivalentTime_test) CBlockIndex *p3 = &blocks[m_rng.randrange(10000)]; int64_t tdiff = GetBlockProofEquivalentTime(*p1, *p2, *p3, chainParams->GetConsensus()); - BOOST_CHECK_EQUAL(tdiff, p1->GetBlockTime() - p2->GetBlockTime()); + BOOST_CHECK_EQUAL(tdiff, static_cast(p1->GetBlockTime()) - static_cast(p2->GetBlockTime())); } } diff --git a/src/test/skiplist_tests.cpp b/src/test/skiplist_tests.cpp index 4fc08b0cadce..253d129c799b 100644 --- a/src/test/skiplist_tests.cpp +++ b/src/test/skiplist_tests.cpp @@ -123,7 +123,7 @@ BOOST_AUTO_TEST_CASE(findearliestatleast_test) } } // Check that we set nTimeMax up correctly. - unsigned int curTimeMax = 0; + uint64_t curTimeMax = 0; for (unsigned int i=0; inTimeMax >= test_time); BOOST_CHECK((ret->pprev==nullptr) || ret->pprev->nTimeMax < test_time); diff --git a/src/validation.cpp b/src/validation.cpp index d3f832f41e9a..5dc10d36a375 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -69,6 +69,7 @@ #include #include #include +#include #include #include #include @@ -113,6 +114,8 @@ const std::vector CHECKLEVEL_DOC { * */ static constexpr int PRUNE_LOCK_BUFFER{10}; +static int64_t SaturatingBlockTime(uint64_t nTime); + // Return whether the completed full flush should compact chainstate static bool ShouldCompactChainstate(bool in_ibd) { @@ -169,7 +172,7 @@ bool CheckFinalTxAtTip(const CBlockIndex& active_chain_tip, const CTransaction& // When the next block is created its previous block will be the current // chain tip, so we use that to calculate the median time passed to // IsFinalTx(). - const int64_t nBlockTime{active_chain_tip.GetMedianTimePast()}; + const int64_t nBlockTime{SaturatingBlockTime(active_chain_tip.GetMedianTimePast())}; return IsFinalTx(tx, nBlockHeight, nBlockTime); } @@ -291,7 +294,7 @@ static bool IsCurrentForFeeEstimation(Chainstate& active_chainstate) EXCLUSIVE_L if (active_chainstate.m_chainman.IsInitialBlockDownload()) { return false; } - if (active_chainstate.m_chain.Tip()->GetBlockTime() < count_seconds(GetTime() - MAX_FEE_ESTIMATION_TIP_AGE)) + if (active_chainstate.m_chain.Tip()->GetBlockTime() < static_cast(std::max(count_seconds(GetTime() - MAX_FEE_ESTIMATION_TIP_AGE), 0))) return false; if (active_chainstate.m_chain.Height() < active_chainstate.m_chainman.m_best_header->nHeight - 1) { return false; @@ -1980,12 +1983,12 @@ void Chainstate::InvalidChainFound(CBlockIndex* pindexNew) LogInfo("%s: invalid block=%s height=%d log2_work=%f date=%s", __func__, pindexNew->GetBlockHash().ToString(), pindexNew->nHeight, - log(pindexNew->nChainWork.getdouble())/log(2.0), FormatISO8601DateTime(pindexNew->GetBlockTime())); + log(pindexNew->nChainWork.getdouble())/log(2.0), FormatISO8601DateTime(SaturatingBlockTime(pindexNew->GetBlockTime()))); CBlockIndex *tip = m_chain.Tip(); assert (tip); LogInfo("%s: current best=%s height=%d log2_work=%f date=%s", __func__, tip->GetBlockHash().ToString(), m_chain.Height(), log(tip->nChainWork.getdouble())/log(2.0), - FormatISO8601DateTime(tip->GetBlockTime())); + FormatISO8601DateTime(SaturatingBlockTime(tip->GetBlockTime()))); CheckForkWarningConditions(); } @@ -2886,7 +2889,7 @@ static void UpdateTipLog( prefix, func_name, tip->GetBlockHash().ToString(), tip->nHeight, tip->nVersion, log(tip->nChainWork.getdouble()) / log(2.0), tip->m_chain_tx_count, - FormatISO8601DateTime(tip->GetBlockTime()), + FormatISO8601DateTime(SaturatingBlockTime(tip->GetBlockTime())), background_validation ? chainman.GetBackgroundVerificationProgress(*tip) : chainman.GuessVerificationProgress(tip), coins_tip.DynamicMemoryUsage() / double(1_MiB), coins_tip.GetCacheSize(), @@ -3837,6 +3840,10 @@ void ChainstateManager::ReceivedBlockTransactions(const CBlock& block, CBlockInd static bool CheckBlockHeader(const CBlockHeader& block, BlockValidationState& state, const Consensus::Params& consensusParams, bool fCheckPOW = true) { + if (block.m_extended != (block.nVersion < 0) || + block.m_extended != (block.nTime >= CBlockHeader::EXTENDED_TIME_THRESHOLD)) { + return state.Invalid(BlockValidationResult::BLOCK_INVALID_HEADER, "bad-time-encoding", "block timestamp encoding does not match timestamp range"); + } // Check proof of work matches claimed amount if (fCheckPOW && !CheckProofOfWork(block.GetHash(), block.nBits, consensusParams)) return state.Invalid(BlockValidationResult::BLOCK_INVALID_HEADER, "high-hash", "proof of work failed"); @@ -3844,6 +3851,18 @@ static bool CheckBlockHeader(const CBlockHeader& block, BlockValidationState& st return true; } +static int64_t SaturatingBlockTime(uint64_t nTime) +{ + return nTime > static_cast(std::numeric_limits::max()) ? std::numeric_limits::max() : static_cast(nTime); +} + +static bool BlockTimeTooNew(uint64_t nTime) +{ + const int64_t now{TicksSinceEpoch(NodeClock::now())}; + const uint64_t max_block_time{static_cast(std::max(now, 0)) + MAX_FUTURE_BLOCK_TIME}; + return nTime > max_block_time; +} + static bool CheckMerkleRoot(const CBlock& block, BlockValidationState& state) { if (block.m_checked_merkle_root) return true; @@ -4098,8 +4117,14 @@ static bool ContextualCheckBlockHeader(const CBlockHeader& block, BlockValidatio if (block.nBits != GetNextWorkRequired(pindexPrev, &block, consensusParams)) return state.Invalid(BlockValidationResult::BLOCK_INVALID_HEADER, "bad-diffbits", "incorrect proof of work"); + if (pindexPrev->nTime >= CBlockHeader::EXTENDED_TIME_THRESHOLD) { + if (!block.m_extended || block.nTime < CBlockHeader::EXTENDED_TIME_THRESHOLD) { + return state.Invalid(BlockValidationResult::BLOCK_INVALID_HEADER, "bad-time-encoding", "post-threshold block must use extended timestamp encoding"); + } + } + // Check timestamp against prev - if (block.GetBlockTime() <= pindexPrev->GetMedianTimePast()) + if (block.nTime <= pindexPrev->GetMedianTimePast()) return state.Invalid(BlockValidationResult::BLOCK_INVALID_HEADER, "time-too-old", "block's timestamp is too early"); // Testnet4 and regtest only: Check timestamp against prev for difficulty-adjustment @@ -4108,23 +4133,24 @@ static bool ContextualCheckBlockHeader(const CBlockHeader& block, BlockValidatio // Check timestamp for the first block of each difficulty adjustment // interval, except the genesis block. if (nHeight % consensusParams.DifficultyAdjustmentInterval() == 0) { - if (block.GetBlockTime() < pindexPrev->GetBlockTime() - MAX_TIMEWARP) { + if (block.nTime < pindexPrev->nTime && pindexPrev->nTime - block.nTime > MAX_TIMEWARP) { return state.Invalid(BlockValidationResult::BLOCK_INVALID_HEADER, "time-timewarp-attack", "block's timestamp is too early on diff adjustment block"); } } } // Check timestamp - if (block.Time() > NodeClock::now() + std::chrono::seconds{MAX_FUTURE_BLOCK_TIME}) { + if (BlockTimeTooNew(block.nTime)) { return state.Invalid(BlockValidationResult::BLOCK_TIME_FUTURE, "time-too-new", "block timestamp too far in the future"); } // Reject blocks with outdated version - if ((block.nVersion < 2 && DeploymentActiveAfter(pindexPrev, chainman, Consensus::DEPLOYMENT_HEIGHTINCB)) || - (block.nVersion < 3 && DeploymentActiveAfter(pindexPrev, chainman, Consensus::DEPLOYMENT_DERSIG)) || - (block.nVersion < 4 && DeploymentActiveAfter(pindexPrev, chainman, Consensus::DEPLOYMENT_CLTV))) { - return state.Invalid(BlockValidationResult::BLOCK_INVALID_HEADER, strprintf("bad-version(0x%08x)", block.nVersion), - strprintf("rejected nVersion=0x%08x block", block.nVersion)); + if (block.nVersion >= 0 && + ((block.nVersion < 2 && DeploymentActiveAfter(pindexPrev, chainman, Consensus::DEPLOYMENT_HEIGHTINCB)) || + (block.nVersion < 3 && DeploymentActiveAfter(pindexPrev, chainman, Consensus::DEPLOYMENT_DERSIG)) || + (block.nVersion < 4 && DeploymentActiveAfter(pindexPrev, chainman, Consensus::DEPLOYMENT_CLTV)))) { + return state.Invalid(BlockValidationResult::BLOCK_INVALID_HEADER, strprintf("bad-version(0x%08x)", block.nVersion), + strprintf("rejected nVersion=0x%08x block", block.nVersion)); } return true; @@ -4147,9 +4173,9 @@ static bool ContextualCheckBlock(const CBlock& block, BlockValidationState& stat enforce_locktime_median_time_past = true; } - const int64_t nLockTimeCutoff{enforce_locktime_median_time_past ? - pindexPrev->GetMedianTimePast() : - block.GetBlockTime()}; + const int64_t nLockTimeCutoff{SaturatingBlockTime(enforce_locktime_median_time_past ? + pindexPrev->GetMedianTimePast() : + block.GetBlockTime())}; // Check that all transactions are finalized for (const auto& tx : block.vtx) { @@ -4590,7 +4616,7 @@ bool Chainstate::LoadChainTip() LogInfo("Loaded best chain: hashBestChain=%s height=%d date=%s progress=%f", tip->GetBlockHash().ToString(), m_chain.Height(), - FormatISO8601DateTime(tip->GetBlockTime()), + FormatISO8601DateTime(SaturatingBlockTime(tip->GetBlockTime())), m_chainman.GuessVerificationProgress(tip)); // Ensure KernelNotifications m_tip_block is set even if no new block arrives. @@ -5513,8 +5539,9 @@ double ChainstateManager::GuessVerificationProgress(const CBlockIndex* pindex) c } const int64_t nNow{TicksSinceEpoch(NodeClock::now())}; + const int64_t pindex_time{SaturatingBlockTime(pindex->GetBlockTime())}; const auto block_time{ - (Assume(m_best_header) && std::abs(nNow - pindex->GetBlockTime()) <= Ticks(2h) && + (Assume(m_best_header) && std::abs(nNow - pindex_time) <= Ticks(2h) && Assume(m_best_header->nHeight >= pindex->nHeight)) ? // When the header is known to be recent, switch to a height-based // approach. This ensures the returned value is quantized when @@ -5522,7 +5549,7 @@ double ChainstateManager::GuessVerificationProgress(const CBlockIndex* pindex) c // avoids relying too much on the exact miner-set timestamp, which // may be off. nNow - (m_best_header->nHeight - pindex->nHeight) * GetConsensus().nPowTargetSpacing : - pindex->GetBlockTime(), + pindex_time, }; double fTxTotal; diff --git a/src/versionbits.cpp b/src/versionbits.cpp index e282d21d2e5f..f1bc5240ed93 100644 --- a/src/versionbits.cpp +++ b/src/versionbits.cpp @@ -9,8 +9,20 @@ #include #include +#include + using enum ThresholdState; +static bool MedianTimePastBefore(uint64_t median_time_past, int64_t time) +{ + return time > 0 && median_time_past < static_cast(time); +} + +static bool MedianTimePastAtOrAfter(uint64_t median_time_past, int64_t time) +{ + return time <= 0 || median_time_past >= static_cast(time); +} + std::string StateName(ThresholdState state) { switch (state) { @@ -54,7 +66,7 @@ ThresholdState AbstractThresholdConditionChecker::GetStateFor(const CBlockIndex* cache[pindexPrev] = ThresholdState::DEFINED; break; } - if (pindexPrev->GetMedianTimePast() < nTimeStart) { + if (MedianTimePastBefore(pindexPrev->GetMedianTimePast(), nTimeStart)) { // Optimization: don't recompute down further, as we know every earlier block will be before the start time cache[pindexPrev] = ThresholdState::DEFINED; break; @@ -75,7 +87,7 @@ ThresholdState AbstractThresholdConditionChecker::GetStateFor(const CBlockIndex* switch (state) { case ThresholdState::DEFINED: { - if (pindexPrev->GetMedianTimePast() >= nTimeStart) { + if (MedianTimePastAtOrAfter(pindexPrev->GetMedianTimePast(), nTimeStart)) { stateNext = ThresholdState::STARTED; } break; @@ -92,7 +104,7 @@ ThresholdState AbstractThresholdConditionChecker::GetStateFor(const CBlockIndex* } if (count >= nThreshold) { stateNext = ThresholdState::LOCKED_IN; - } else if (pindexPrev->GetMedianTimePast() >= nTimeTimeout) { + } else if (MedianTimePastAtOrAfter(pindexPrev->GetMedianTimePast(), nTimeTimeout)) { stateNext = ThresholdState::FAILED; } break; diff --git a/test/functional/feature_coinstatsindex_compatibility.py b/test/functional/feature_coinstatsindex_compatibility.py index 21496a9cbd17..aad3a2504163 100755 --- a/test/functional/feature_coinstatsindex_compatibility.py +++ b/test/functional/feature_coinstatsindex_compatibility.py @@ -9,7 +9,7 @@ import shutil -from test_framework.test_framework import BitcoinTestFramework +from test_framework.test_framework import BitcoinTestFramework, SkipTest from test_framework.util import assert_equal @@ -20,6 +20,7 @@ def set_test_params(self): def skip_test_if_missing_module(self): self.skip_if_no_previous_releases() + raise SkipTest("old block index databases are not supported by the 2106 timestamp draft") def setup_nodes(self): self.add_nodes( diff --git a/test/functional/feature_unsupported_utxo_db.py b/test/functional/feature_unsupported_utxo_db.py index a71ae223766c..87a16e10864e 100755 --- a/test/functional/feature_unsupported_utxo_db.py +++ b/test/functional/feature_unsupported_utxo_db.py @@ -9,7 +9,7 @@ import shutil -from test_framework.test_framework import BitcoinTestFramework +from test_framework.test_framework import BitcoinTestFramework, SkipTest from test_framework.util import assert_equal @@ -20,6 +20,7 @@ def set_test_params(self): def skip_test_if_missing_module(self): self.skip_if_no_previous_releases() + raise SkipTest("old block index databases are not supported by the 2106 timestamp draft") def setup_network(self): self.add_nodes( diff --git a/test/functional/feature_y2106_extended_time.py b/test/functional/feature_y2106_extended_time.py new file mode 100755 index 000000000000..de98d3fc30a5 --- /dev/null +++ b/test/functional/feature_y2106_extended_time.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +# Copyright (c) 2026-present The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Test the draft post-2106 extended block timestamp hard fork.""" + +from io import BytesIO + +from test_framework.blocktools import ( + NORMAL_GBT_REQUEST_PARAMS, + create_block, +) +from test_framework.messages import CBlockHeader, uint256_from_compact +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import assert_equal +from test_framework.wallet import MiniWallet + + +TIME_2106 = 2**32 - 1 +MAX_UINT64 = 2**64 - 1 + + +class Y2106ExtendedTimeTest(BitcoinTestFramework): + def set_test_params(self): + self.setup_clean_chain = True + self.num_nodes = 2 + + def skip_test_if_missing_module(self): + self.skip_if_no_previous_releases() + + def setup_network(self): + self.add_nodes( + self.num_nodes, + versions=[ + None, + 310000, + ], + ) + + def submit_to_previous_release(self, block_hex): + assert_equal(self.nodes[1].submitblock(block_hex), "high-hash") + + def solve_with_zero_low_nonce_byte(self, block): + target = uint256_from_compact(block.nBits) + block.nNonce = 0 + while block.hash_int > target: + block.nNonce += 256 + + def run_test(self): + self.start_nodes() + wallet = MiniWallet(self.nodes[0]) + + self.log.info("Reject extended timestamp encoding below 2**32") + tmpl = self.nodes[0].getblocktemplate(NORMAL_GBT_REQUEST_PARAMS) + bad_low_time_block = create_block(tmpl=tmpl) + bad_low_time_block.nTime = 0 + bad_low_time_block.set_extended_time_encoding() + bad_low_time_block.solve() + assert_equal(self.nodes[0].submitblock(bad_low_time_block.serialize().hex()), "bad-time-encoding") + + self.log.info("Allow the maximum uint64 timestamp past header validation") + max_time_block = create_block(tmpl=tmpl) + max_time_block.nTime = MAX_UINT64 + max_time_block.set_extended_time_encoding() + max_time_block.hashMerkleRoot = max_time_block.calc_merkle_root() + max_time_block.solve() + assert_equal(self.nodes[0].submitblock(max_time_block.serialize().hex()), "time-too-new") + + self.log.info("Build a shared chain whose MTP reaches the last 32-bit timestamp") + block_hashes = self.generate(wallet, 8, sync_fun=self.no_op) + self.nodes[1].setmocktime(TIME_2106) + self.nodes[0].setmocktime(TIME_2106) + block_hashes += self.generate(wallet, 6, sync_fun=self.no_op) + assert_equal(self.nodes[0].getblockheader(block_hashes[-1])["mediantime"], TIME_2106) + + for block_hash in block_hashes: + assert_equal(self.nodes[1].submitblock(self.nodes[0].getblock(block_hash, 0)), None) + assert_equal(self.nodes[1].getbestblockhash(), self.nodes[0].getblockhash(14)) + + self.log.info("Continue the chain with an extended 64-bit timestamp") + self.nodes[0].setmocktime(TIME_2106 + 1) + extended_block = create_block(tmpl=self.nodes[0].getblocktemplate(NORMAL_GBT_REQUEST_PARAMS)) + assert_equal(extended_block.nTime, TIME_2106 + 1) + self.solve_with_zero_low_nonce_byte(extended_block) + extended_block_hex = extended_block.serialize().hex() + assert_equal(self.nodes[0].submitblock(extended_block_hex), None) + extended_header = CBlockHeader() + extended_header.deserialize(BytesIO(bytes.fromhex(extended_block_hex))) + assert_equal(extended_header.nTime, TIME_2106 + 1) + assert extended_header.uses_extended_time_encoding() + assert extended_header.nVersion < 0 + assert_equal(self.nodes[0].getblockheader(extended_block.hash_hex)["time"], TIME_2106 + 1) + + self.log.info("Previous release rejects the extended-time block") + self.submit_to_previous_release(extended_block_hex) + + +if __name__ == "__main__": + Y2106ExtendedTimeTest(__file__).main() diff --git a/test/functional/interface_ipc_mining.py b/test/functional/interface_ipc_mining.py index 4cd9c17c99f6..e7bcaa47584a 100755 --- a/test/functional/interface_ipc_mining.py +++ b/test/functional/interface_ipc_mining.py @@ -518,6 +518,12 @@ async def async_routine(): except capnp.lib.capnp.KjException as e: assert_capnp_failed(e, "remote exception: std::exception: SpanReader::read(): end of data:") + self.log.debug("submitSolution should carry a 64-bit timestamp") + submitted = (await template.submitSolution(ctx, block.nVersion, CBlockHeader.EXTENDED_TIME_THRESHOLD, block.nNonce, coinbase.serialize())).result + assert_equal(submitted, False) + remote_high_time_block = await mining_get_block(template, ctx) + assert_equal(remote_high_time_block.nTime, CBlockHeader.EXTENDED_TIME_THRESHOLD) + self.log.debug("Submit a block with a bad version") block.nVersion = 0 block.solve() diff --git a/test/functional/rpc_blockchain.py b/test/functional/rpc_blockchain.py index d731907fd8ca..8b2a9799b5a9 100755 --- a/test/functional/rpc_blockchain.py +++ b/test/functional/rpc_blockchain.py @@ -290,11 +290,14 @@ def _test_y2106(self): self.log.info("Check that block timestamps work until year 2106") self.generate(self.nodes[0], 8)[-1] time_2106 = 2**32 - 1 - assert_raises_rpc_error(-8, f"Mocktime must be in the range [0, {time_2106}], not -1.", self.nodes[0].setmocktime, -1) - assert_raises_rpc_error(-8, f"Mocktime must be in the range [0, {time_2106}], not {time_2106 + 1}.", self.nodes[0].setmocktime, time_2106 + 1) + max_mocktime = 2**63 - 1 - 2 * 60 * 60 + assert_raises_rpc_error(-8, f"Mocktime must be in the range [0, {max_mocktime}], not -1.", self.nodes[0].setmocktime, -1) self.nodes[0].setmocktime(time_2106) last = self.generate(self.nodes[0], 6)[-1] assert_equal(self.nodes[0].getblockheader(last)["mediantime"], time_2106) + self.nodes[0].setmocktime(time_2106 + 1) + last = self.generate(self.nodes[0], 1)[-1] + assert_equal(self.nodes[0].getblockheader(last)["time"], time_2106 + 1) def _test_getchaintxstats(self): self.log.info("Test getchaintxstats") diff --git a/test/functional/test_framework/blocktools.py b/test/functional/test_framework/blocktools.py index 529f07cd3c7f..44c7fda81c54 100644 --- a/test/functional/test_framework/blocktools.py +++ b/test/functional/test_framework/blocktools.py @@ -17,6 +17,7 @@ ) from .messages import ( CBlock, + CBlockHeader, COIN, COutPoint, CTransaction, @@ -102,6 +103,8 @@ def create_block(hashprev=None, coinbase=None, *, ntime=None, height=None, versi tmpl = {} block.nVersion = version or tmpl.get('version') or VERSIONBITS_LAST_OLD_BLOCK_VERSION block.nTime = ntime or tmpl.get('curtime') or int(time.time() + 600) + if block.nTime >= CBlockHeader.EXTENDED_TIME_THRESHOLD: + block.set_extended_time_encoding() block.hashPrevBlock = hashprev or int(tmpl['previousblockhash'], 0x10) if tmpl and tmpl.get('bits') is not None: block.nBits = struct.unpack('>I', bytes.fromhex(tmpl['bits']))[0] diff --git a/test/functional/test_framework/messages.py b/test/functional/test_framework/messages.py index 07abd5b6eb09..23e15d7d4590 100755 --- a/test/functional/test_framework/messages.py +++ b/test/functional/test_framework/messages.py @@ -730,7 +730,9 @@ def __repr__(self): class CBlockHeader: __slots__ = ("hashMerkleRoot", "hashPrevBlock", "nBits", "nNonce", - "nTime", "nVersion") + "nTime", "nVersion", "m_extended") + + EXTENDED_TIME_THRESHOLD = 2**32 def __init__(self, header=None): if header is None: @@ -742,6 +744,7 @@ def __init__(self, header=None): self.nTime = header.nTime self.nBits = header.nBits self.nNonce = header.nNonce + self.m_extended = header.m_extended def set_null(self): self.nVersion = 4 @@ -750,24 +753,40 @@ def set_null(self): self.nTime = 0 self.nBits = 0 self.nNonce = 0 + self.m_extended = False def deserialize(self, f): self.nVersion = int.from_bytes(f.read(4), "little", signed=True) + self.m_extended = self.nVersion < 0 self.hashPrevBlock = deser_uint256(f) self.hashMerkleRoot = deser_uint256(f) - self.nTime = int.from_bytes(f.read(4), "little") + time_low = int.from_bytes(f.read(4), "little") + self.nTime = time_low + if self.m_extended: + time_high = int.from_bytes(f.read(4), "little") + self.nTime |= time_high << 32 self.nBits = int.from_bytes(f.read(4), "little") self.nNonce = int.from_bytes(f.read(4), "little") def serialize(self): return self._serialize_header() + def uses_extended_time_encoding(self): + return self.m_extended + + def set_extended_time_encoding(self): + self.m_extended = True + if self.nVersion >= 0: + self.nVersion = -self.nVersion if self.nVersion else -1 + def _serialize_header(self): r = b"" r += self.nVersion.to_bytes(4, "little", signed=True) r += ser_uint256(self.hashPrevBlock) r += ser_uint256(self.hashMerkleRoot) - r += self.nTime.to_bytes(4, "little") + r += (self.nTime & 0xffffffff).to_bytes(4, "little") + if self.m_extended: + r += (self.nTime >> 32).to_bytes(4, "little") r += self.nBits.to_bytes(4, "little") r += self.nNonce.to_bytes(4, "little") return r diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 5bfc7d86239b..ccea4ff19f9c 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -357,6 +357,7 @@ 'rpc_scantxoutset.py', 'feature_torcontrol.py', 'feature_unsupported_utxo_db.py', + 'feature_y2106_extended_time.py', 'mempool_cluster.py', 'feature_logging.py', 'interface_ipc.py', From 55d7ff8ea0b577e59e8124d3bc48963f91ebd7a7 Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Mon, 22 Jun 2026 14:20:33 +0200 Subject: [PATCH 04/10] rpc: disable txout proofs for extended blocks --- src/rpc/txoutproof.cpp | 3 +++ test/functional/feature_y2106_extended_time.py | 9 ++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/rpc/txoutproof.cpp b/src/rpc/txoutproof.cpp index 628825fc80e6..bbeb5fa8e886 100644 --- a/src/rpc/txoutproof.cpp +++ b/src/rpc/txoutproof.cpp @@ -106,6 +106,9 @@ static RPCMethod gettxoutproof() if (!chainman.m_blockman.ReadBlock(block, *pblockindex)) { throw JSONRPCError(RPC_INTERNAL_ERROR, "Can't read block from disk"); } + if (block.m_extended) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "gettxoutproof is not supported for extended-header blocks"); + } unsigned int ntxFound = 0; for (const auto& tx : block.vtx) { diff --git a/test/functional/feature_y2106_extended_time.py b/test/functional/feature_y2106_extended_time.py index de98d3fc30a5..941d4c2b9c3b 100755 --- a/test/functional/feature_y2106_extended_time.py +++ b/test/functional/feature_y2106_extended_time.py @@ -12,7 +12,7 @@ ) from test_framework.messages import CBlockHeader, uint256_from_compact from test_framework.test_framework import BitcoinTestFramework -from test_framework.util import assert_equal +from test_framework.util import assert_equal, assert_raises_rpc_error from test_framework.wallet import MiniWallet @@ -90,6 +90,13 @@ def run_test(self): assert extended_header.uses_extended_time_encoding() assert extended_header.nVersion < 0 assert_equal(self.nodes[0].getblockheader(extended_block.hash_hex)["time"], TIME_2106 + 1) + assert_raises_rpc_error( + -5, + "gettxoutproof is not supported for extended-header blocks", + self.nodes[0].gettxoutproof, + [extended_block.vtx[0].txid_hex], + extended_block.hash_hex, + ) self.log.info("Previous release rejects the extended-time block") self.submit_to_previous_release(extended_block_hex) From 7e29089aadc71e48216981cf3ef68ff1f6f64fb6 Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Mon, 22 Jun 2026 14:20:43 +0200 Subject: [PATCH 05/10] net: disable filtered blocks for extended blocks --- src/net_processing.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/net_processing.cpp b/src/net_processing.cpp index c01f93c21aed..a20710c519fc 100644 --- a/src/net_processing.cpp +++ b/src/net_processing.cpp @@ -2470,7 +2470,10 @@ void PeerManagerImpl::ProcessGetBlockData(CNode& pfrom, Peer& peer, const CInv& CMerkleBlock merkleBlock; if (auto tx_relay = peer.GetTxRelay(); tx_relay != nullptr) { LOCK(tx_relay->m_bloom_filter_mutex); - if (tx_relay->m_bloom_filter) { + if (tx_relay->m_bloom_filter && !pblock->m_extended) { + // Partial merkle proofs only carry txids; this draft + // disables them for extended-header blocks instead of + // defining a new proof format. sendMerkleBlock = true; merkleBlock = CMerkleBlock(*pblock, *tx_relay->m_bloom_filter); } From b002e341aca420094570502096047d7896db98cb Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Mon, 22 Jun 2026 13:39:48 +0200 Subject: [PATCH 06/10] refactor: add transaction merkle hash helpers --- src/consensus/merkle.cpp | 10 +++++++--- src/consensus/merkle.h | 2 ++ src/merkleblock.cpp | 5 +++-- src/merkleblock.h | 2 +- src/test/merkle_tests.cpp | 4 ++-- 5 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/consensus/merkle.cpp b/src/consensus/merkle.cpp index c6ae81c61420..3f58fe3a1c75 100644 --- a/src/consensus/merkle.cpp +++ b/src/consensus/merkle.cpp @@ -42,6 +42,10 @@ root. */ +uint256 TxMerkleNodeHash(const uint256& left, const uint256& right) +{ + return Hash(left, right); +} uint256 ComputeMerkleRoot(std::vector hashes, bool* mutated) { bool mutation = false; @@ -118,7 +122,7 @@ static void MerkleComputation(const std::vector& leaves, uint32_t leaf_ path.push_back(h); matchh = true; } - h = Hash(inner[level], h); + h = TxMerkleNodeHash(inner[level], h); } // Store the resulting hash at inner position level. inner[level] = h; @@ -144,7 +148,7 @@ static void MerkleComputation(const std::vector& leaves, uint32_t leaf_ if (matchh) { path.push_back(h); } - h = Hash(h, h); + h = TxMerkleNodeHash(h, h); // Increment count to the value it would have if two entries at this // level had existed. count += ((uint32_t{1}) << level); @@ -157,7 +161,7 @@ static void MerkleComputation(const std::vector& leaves, uint32_t leaf_ path.push_back(h); matchh = true; } - h = Hash(inner[level], h); + h = TxMerkleNodeHash(inner[level], h); level++; } } diff --git a/src/consensus/merkle.h b/src/consensus/merkle.h index 03b5a1b5aac7..7d4dd3f82ce8 100644 --- a/src/consensus/merkle.h +++ b/src/consensus/merkle.h @@ -10,6 +10,8 @@ #include #include +/** Calculate the hash of an internal transaction merkle tree node. */ +uint256 TxMerkleNodeHash(const uint256& left, const uint256& right); uint256 ComputeMerkleRoot(std::vector hashes, bool* mutated = nullptr); /* diff --git a/src/merkleblock.cpp b/src/merkleblock.cpp index 6bb05a5edd31..f6a0effe7b3a 100644 --- a/src/merkleblock.cpp +++ b/src/merkleblock.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #include @@ -72,7 +73,7 @@ uint256 CPartialMerkleTree::CalcHash(int height, unsigned int pos, const std::ve else right = left; // combine subhashes - return Hash(left, right); + return TxMerkleNodeHash(left, right); } } @@ -130,7 +131,7 @@ uint256 CPartialMerkleTree::TraverseAndExtract(int height, unsigned int pos, uns right = left; } // and combine them before returning - return Hash(left, right); + return TxMerkleNodeHash(left, right); } } diff --git a/src/merkleblock.h b/src/merkleblock.h index 79611af01b24..89ae7b9e235f 100644 --- a/src/merkleblock.h +++ b/src/merkleblock.h @@ -73,7 +73,7 @@ class CPartialMerkleTree return (nTransactions+(1 << height)-1) >> height; } - /** calculate the hash of a node in the merkle tree (at leaf level: the txid's themselves) */ + /** calculate the hash of a node in the merkle tree */ uint256 CalcHash(int height, unsigned int pos, const std::vector &vTxid); /** recursive function that traverses tree nodes, storing the data as bits and hashes */ diff --git a/src/test/merkle_tests.cpp b/src/test/merkle_tests.cpp index a6dd23adb17e..79b21fe8901c 100644 --- a/src/test/merkle_tests.cpp +++ b/src/test/merkle_tests.cpp @@ -15,9 +15,9 @@ static uint256 ComputeMerkleRootFromBranch(const uint256& leaf, const std::vecto uint256 hash = leaf; for (std::vector::const_iterator it = vMerkleBranch.begin(); it != vMerkleBranch.end(); ++it) { if (nIndex & 1) { - hash = Hash(*it, hash); + hash = TxMerkleNodeHash(*it, hash); } else { - hash = Hash(hash, *it); + hash = TxMerkleNodeHash(hash, *it); } nIndex >>= 1; } From 8c9fa699951decae2fb9cffc1bab4de510b7b3c5 Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Mon, 22 Jun 2026 14:16:54 +0200 Subject: [PATCH 07/10] consensus: tag transaction merkle leaves after 2106 --- src/consensus/merkle.cpp | 15 ++++++-- src/signet.cpp | 4 +-- src/test/fuzz/merkle.cpp | 19 ++++++++--- src/test/merkle_tests.cpp | 34 +++++++++++++++++++ .../functional/feature_y2106_extended_time.py | 9 ++++- test/functional/test_framework/messages.py | 3 +- 6 files changed, 74 insertions(+), 10 deletions(-) diff --git a/src/consensus/merkle.cpp b/src/consensus/merkle.cpp index 3f58fe3a1c75..9de8226f374b 100644 --- a/src/consensus/merkle.cpp +++ b/src/consensus/merkle.cpp @@ -72,7 +72,14 @@ uint256 BlockMerkleRoot(const CBlock& block, bool* mutated) std::vector leaves; leaves.reserve((block.vtx.size() + 1) & ~1ULL); // capacity rounded up to even for (size_t s = 0; s < block.vtx.size(); s++) { - leaves.push_back(block.vtx[s]->GetHash().ToUint256()); + if (block.m_extended) { + // This draft tags the no-witness transaction serialization before + // using it as a merkle leaf. A real hardfork would likely define a + // tagged wtxid and remove the separate witness merkle tree. + leaves.push_back((HashWriter{TaggedHash("TaggedTxid")} << TX_NO_WITNESS(*block.vtx[s])).GetSHA256()); + } else { + leaves.push_back(block.vtx[s]->GetHash().ToUint256()); + } } return ComputeMerkleRoot(std::move(leaves), mutated); } @@ -178,7 +185,11 @@ std::vector TransactionMerklePath(const CBlock& block, uint32_t positio std::vector leaves; leaves.resize(block.vtx.size()); for (size_t s = 0; s < block.vtx.size(); s++) { - leaves[s] = block.vtx[s]->GetHash().ToUint256(); + if (block.m_extended) { + leaves[s] = (HashWriter{TaggedHash("TaggedTxid")} << TX_NO_WITNESS(*block.vtx[s])).GetSHA256(); + } else { + leaves[s] = block.vtx[s]->GetHash().ToUint256(); + } } return ComputeMerklePath(leaves, position); } diff --git a/src/signet.cpp b/src/signet.cpp index 89d6389fccdc..bf85322250d9 100644 --- a/src/signet.cpp +++ b/src/signet.cpp @@ -60,9 +60,9 @@ static uint256 ComputeModifiedMerkleRoot(const CMutableTransaction& cb, const CB { std::vector leaves; leaves.reserve((block.vtx.size() + 1) & ~1ULL); // capacity rounded up to even - leaves.push_back(cb.GetHash().ToUint256()); + leaves.push_back(TxMerkleLeafHash(CTransaction{cb}, block.m_extended)); for (size_t s = 1; s < block.vtx.size(); ++s) { - leaves.push_back(block.vtx[s]->GetHash().ToUint256()); + leaves.push_back(TxMerkleLeafHash(*block.vtx[s], block.m_extended)); } return ComputeMerkleRoot(std::move(leaves)); } diff --git a/src/test/fuzz/merkle.cpp b/src/test/fuzz/merkle.cpp index 0de1c0279554..60d14ba04473 100644 --- a/src/test/fuzz/merkle.cpp +++ b/src/test/fuzz/merkle.cpp @@ -14,12 +14,21 @@ #include #include -uint256 ComputeMerkleRootFromPath(const CBlock& block, uint32_t position, const std::vector& merkle_path) { +uint256 TransactionMerkleLeaf(const CBlock& block, uint32_t position) { if (position >= block.vtx.size()) { throw std::out_of_range("Position out of range"); } + if (block.m_extended) { + return (HashWriter{TaggedHash("TaggedWtxid")} + << COMPACTSIZE(position) + << TX_WITH_WITNESS(*block.vtx[position])) + .GetSHA256(); + } + return block.vtx[position]->GetHash().ToUint256(); +} - uint256 current_hash = block.vtx[position]->GetHash().ToUint256(); +uint256 ComputeMerkleRootFromPath(const CBlock& block, uint32_t position, const std::vector& merkle_path) { + uint256 current_hash = TransactionMerkleLeaf(block, position); for (const uint256& sibling : merkle_path) { if (position % 2 == 0) { @@ -61,7 +70,7 @@ FUZZ_TARGET(merkle) const uint256 block_merkle_root = BlockMerkleRoot(*block, &mutated); - if (tx_hashes.size() == 1) { + if (!block->m_extended && tx_hashes.size() == 1) { assert(block_merkle_root == tx_hashes[0]); } @@ -79,7 +88,9 @@ FUZZ_TARGET(merkle) // Check that the root we compute from TransactionMerklePath equals the same merkle_root and block_merkle_root if (tx_hashes.size() > 1) { uint256 merkle_root_from_merkle_path = ComputeMerkleRootFromPath(*block, position, merkle_path); - assert(merkle_root_from_merkle_path == merkle_root); + if (!block->m_extended) { + assert(merkle_root_from_merkle_path == merkle_root); + } assert(merkle_root_from_merkle_path == block_merkle_root); } diff --git a/src/test/merkle_tests.cpp b/src/test/merkle_tests.cpp index 79b21fe8901c..a95831d011b2 100644 --- a/src/test/merkle_tests.cpp +++ b/src/test/merkle_tests.cpp @@ -3,6 +3,8 @@ // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include +#include +#include #include #include #include @@ -176,6 +178,38 @@ BOOST_AUTO_TEST_CASE(merkle_test_oneTx_block) BOOST_CHECK_EQUAL(mutated, false); } +BOOST_AUTO_TEST_CASE(merkle_test_extended_domain_separation) +{ + const uint256 left{1}; + const uint256 right{2}; + const uint256 legacy_inner{Hash(left, right)}; + + CMutableTransaction mtx; + const CTransaction tx{mtx}; + const uint256 legacy_leaf{tx.GetHash().ToUint256()}; + const uint256 tagged_leaf{(HashWriter{TaggedHash("TaggedTxid")} << TX_NO_WITNESS(tx)).GetSHA256()}; + + BOOST_CHECK_EQUAL(TxMerkleNodeHash(left, right), legacy_inner); + BOOST_CHECK_NE(tagged_leaf, legacy_leaf); + + CBlock block; + block.nTime = CBlockHeader::EXTENDED_TIME_THRESHOLD; + block.SetExtendedTimeEncoding(); + block.vtx.resize(2); + for (std::size_t pos = 0; pos < block.vtx.size(); pos++) { + CMutableTransaction tx_mut; + tx_mut.nLockTime = pos; + block.vtx[pos] = MakeTransactionRef(std::move(tx_mut)); + } + + const uint256 root{BlockMerkleRoot(block)}; + BOOST_CHECK_NE(root, ComputeMerkleRoot({block.vtx[0]->GetHash().ToUint256(), block.vtx[1]->GetHash().ToUint256()})); + for (uint32_t pos{0}; pos < block.vtx.size(); ++pos) { + const uint256 extended_leaf{(HashWriter{TaggedHash("TaggedTxid")} << TX_NO_WITNESS(*block.vtx[pos])).GetSHA256()}; + BOOST_CHECK_EQUAL(ComputeMerkleRootFromBranch(extended_leaf, TransactionMerklePath(block, pos), pos), root); + } +} + BOOST_AUTO_TEST_CASE(merkle_test_OddTxWithRepeatedLastTx_block) { bool mutated; diff --git a/test/functional/feature_y2106_extended_time.py b/test/functional/feature_y2106_extended_time.py index 941d4c2b9c3b..48d33799719b 100755 --- a/test/functional/feature_y2106_extended_time.py +++ b/test/functional/feature_y2106_extended_time.py @@ -10,7 +10,7 @@ NORMAL_GBT_REQUEST_PARAMS, create_block, ) -from test_framework.messages import CBlockHeader, uint256_from_compact +from test_framework.messages import CBlockHeader, ser_uint256, uint256_from_compact from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal, assert_raises_rpc_error from test_framework.wallet import MiniWallet @@ -79,6 +79,13 @@ def run_test(self): self.log.info("Continue the chain with an extended 64-bit timestamp") self.nodes[0].setmocktime(TIME_2106 + 1) + self.log.info("Reject a post-2106 block with the legacy merkle root") + legacy_merkle_block = create_block(tmpl=self.nodes[0].getblocktemplate(NORMAL_GBT_REQUEST_PARAMS)) + legacy_merkle_block.hashMerkleRoot = legacy_merkle_block.get_merkle_root([ser_uint256(tx.txid_int) for tx in legacy_merkle_block.vtx]) + assert legacy_merkle_block.hashMerkleRoot != legacy_merkle_block.calc_merkle_root() + legacy_merkle_block.solve() + assert_equal(self.nodes[0].submitblock(legacy_merkle_block.serialize().hex()), "bad-txnmrklroot") + extended_block = create_block(tmpl=self.nodes[0].getblocktemplate(NORMAL_GBT_REQUEST_PARAMS)) assert_equal(extended_block.nTime, TIME_2106 + 1) self.solve_with_zero_low_nonce_byte(extended_block) diff --git a/test/functional/test_framework/messages.py b/test/functional/test_framework/messages.py index 23e15d7d4590..7dfa9b4a6fcf 100755 --- a/test/functional/test_framework/messages.py +++ b/test/functional/test_framework/messages.py @@ -29,6 +29,7 @@ import unittest from test_framework.crypto.siphash import siphash256 +from test_framework.key import TaggedHash from test_framework.util import ( assert_equal, assert_not_equal, @@ -843,7 +844,7 @@ def get_merkle_root(cls, hashes): def calc_merkle_root(self): hashes = [] for tx in self.vtx: - hashes.append(ser_uint256(tx.txid_int)) + hashes.append(TaggedHash("TaggedTxid", tx.serialize_without_witness()) if self.m_extended else ser_uint256(tx.txid_int)) return self.get_merkle_root(hashes) def calc_witness_merkle_root(self): From b8e9c341bd7bdcf1ad59a49513bb29ed45408367 Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Mon, 22 Jun 2026 15:25:43 +0200 Subject: [PATCH 08/10] consensus: reserve extended timestamp nonce byte --- src/chain.h | 4 ++-- src/node/blockstorage.cpp | 6 +++-- src/node/miner.cpp | 11 ++++++---- src/primitives/block.h | 22 +++++++++++++++++-- src/test/fuzz/block_header.cpp | 2 +- src/validation.cpp | 13 ++++++----- .../functional/feature_y2106_extended_time.py | 9 +++++--- test/functional/test_framework/messages.py | 5 +++++ 8 files changed, 53 insertions(+), 19 deletions(-) diff --git a/src/chain.h b/src/chain.h index d9fc9d73beba..6f8533bda946 100644 --- a/src/chain.h +++ b/src/chain.h @@ -218,12 +218,12 @@ class CBlockIndex NodeSeconds Time() const { - return NodeSeconds{std::chrono::seconds{nTime}}; + return NodeSeconds{std::chrono::seconds{GetBlockTime()}}; } uint64_t GetBlockTime() const { - return nTime; + return CBlockHeader::MaskedBlockTime(nTime, nVersion < 0); } uint64_t GetBlockTimeMax() const diff --git a/src/node/blockstorage.cpp b/src/node/blockstorage.cpp index 8726c741fa5e..3d7520b1ce6e 100644 --- a/src/node/blockstorage.cpp +++ b/src/node/blockstorage.cpp @@ -243,7 +243,8 @@ CBlockIndex* BlockManager::AddToBlockIndex(const CBlockHeader& block, CBlockInde pindexNew->nHeight = pindexNew->pprev->nHeight + 1; pindexNew->BuildSkip(); } - pindexNew->nTimeMax = (pindexNew->pprev ? std::max(pindexNew->pprev->nTimeMax, pindexNew->nTime) : pindexNew->nTime); + pindexNew->nTimeMax = (pindexNew->pprev ? std::max(pindexNew->pprev->nTimeMax, pindexNew->GetBlockTime()) : + pindexNew->GetBlockTime()); pindexNew->nChainWork = (pindexNew->pprev ? pindexNew->pprev->nChainWork : 0) + GetBlockProof(*pindexNew); pindexNew->RaiseValidity(BLOCK_VALID_TREE); if (best_header == nullptr || best_header->nChainWork < pindexNew->nChainWork) { @@ -482,7 +483,8 @@ bool BlockManager::LoadBlockIndex(const std::optional& snapshot_blockha } previous_index = pindex; pindex->nChainWork = (pindex->pprev ? pindex->pprev->nChainWork : 0) + GetBlockProof(*pindex); - pindex->nTimeMax = (pindex->pprev ? std::max(pindex->pprev->nTimeMax, pindex->nTime) : pindex->nTime); + pindex->nTimeMax = (pindex->pprev ? std::max(pindex->pprev->nTimeMax, pindex->GetBlockTime()) : + pindex->GetBlockTime()); // We can link the chain of blocks for which we've received transactions at some point, or // blocks that are assumed-valid on the basis of snapshot load (see diff --git a/src/node/miner.cpp b/src/node/miner.cpp index f920efb51b7e..5f9b475c484d 100644 --- a/src/node/miner.cpp +++ b/src/node/miner.cpp @@ -68,7 +68,8 @@ uint64_t GetMinimumTime(const CBlockIndex* pindexPrev, const int64_t difficulty_ // Account for BIP94 timewarp rule on all networks. This makes future // activation safer. if (height % difficulty_adjustment_interval == 0) { - min_time = std::max(min_time, pindexPrev->nTime > MAX_TIMEWARP ? pindexPrev->nTime - MAX_TIMEWARP : 0); + const uint64_t prev_time{pindexPrev->GetBlockTime()}; + min_time = std::max(min_time, prev_time > MAX_TIMEWARP ? prev_time - MAX_TIMEWARP : 0); } return min_time; } @@ -79,6 +80,7 @@ int64_t UpdateTime(CBlockHeader* pblock, const Consensus::Params& consensusParam const int64_t now{TicksSinceEpoch(NodeClock::now())}; uint64_t nNewTime{std::max(GetMinimumTime(pindexPrev, consensusParams.DifficultyAdjustmentInterval()), static_cast(std::max(now, 0)))}; + nNewTime = CBlockHeader::MinRawTimeForMaskedTime(nNewTime); if (nOldTime < nNewTime) { pblock->nTime = nNewTime; @@ -158,7 +160,7 @@ std::unique_ptr BlockAssembler::CreateNewBlock() pblock->nVersion = gArgs.GetIntArg("-blockversion", pblock->nVersion); } - pblock->nTime = TicksSinceEpoch(NodeClock::now()); + pblock->nTime = CBlockHeader::MinRawTimeForMaskedTime(TicksSinceEpoch(NodeClock::now())); if (pblock->nTime >= CBlockHeader::EXTENDED_TIME_THRESHOLD) { pblock->SetExtendedTimeEncoding(); } @@ -487,8 +489,9 @@ std::unique_ptr WaitAndCreateNewBlock(ChainstateManager& chainma // On test networks return a minimum difficulty block after 20 minutes if (!tip_changed && allow_min_difficulty) { const CBlockIndex* tip{chainman.ActiveChain().Tip()}; - if (tip->nTime <= static_cast(std::numeric_limits::max()) && - now > NodeClock::time_point{std::chrono::seconds{static_cast(tip->nTime)}} + 20min) { + const uint64_t tip_time{tip->GetBlockTime()}; + if (tip_time <= static_cast(std::numeric_limits::max()) && + now > NodeClock::time_point{std::chrono::seconds{static_cast(tip_time)}} + 20min) { tip_changed = true; } } diff --git a/src/primitives/block.h b/src/primitives/block.h index 1e2ce55cb65e..4920c3c13ae5 100644 --- a/src/primitives/block.h +++ b/src/primitives/block.h @@ -12,6 +12,7 @@ #include #include +#include #include #include #include @@ -38,12 +39,29 @@ class CBlockHeader bool m_extended; static constexpr uint64_t EXTENDED_TIME_THRESHOLD{uint64_t{1} << 32}; + //! The low byte remains part of the serialized header and block hash, but + //! is ignored when interpreting extended-header consensus time. + static constexpr uint64_t EXTENDED_TIME_EXTRA_NONCE_MASK{0xff}; + static constexpr uint64_t EXTENDED_TIME_MASK{~EXTENDED_TIME_EXTRA_NONCE_MASK}; CBlockHeader() { SetNull(); } + static constexpr uint64_t MaskedBlockTime(uint64_t nTime, bool extended) + { + return extended ? nTime & EXTENDED_TIME_MASK : nTime; + } + + static constexpr uint64_t MinRawTimeForMaskedTime(uint64_t nTime) + { + if (nTime < EXTENDED_TIME_THRESHOLD || (nTime & EXTENDED_TIME_EXTRA_NONCE_MASK) == 0) return nTime; + const uint64_t increment{uint64_t{1} << 8}; + if (nTime > std::numeric_limits::max() - increment) return nTime & EXTENDED_TIME_MASK; + return (nTime + EXTENDED_TIME_EXTRA_NONCE_MASK) & EXTENDED_TIME_MASK; + } + template void Serialize(Stream& s) const { @@ -106,12 +124,12 @@ class CBlockHeader NodeSeconds Time() const { - return NodeSeconds{std::chrono::seconds{nTime}}; + return NodeSeconds{std::chrono::seconds{GetBlockTime()}}; } uint64_t GetBlockTime() const { - return nTime; + return MaskedBlockTime(nTime, m_extended); } }; diff --git a/src/test/fuzz/block_header.cpp b/src/test/fuzz/block_header.cpp index 2b3284b3518d..ca50de1add5c 100644 --- a/src/test/fuzz/block_header.cpp +++ b/src/test/fuzz/block_header.cpp @@ -25,7 +25,7 @@ FUZZ_TARGET(block_header) const uint256 hash = block_header->GetHash(); constexpr uint256 u256_max{"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"}; assert(hash != u256_max); - assert(block_header->GetBlockTime() == block_header->nTime); + assert(block_header->GetBlockTime() == CBlockHeader::MaskedBlockTime(block_header->nTime, block_header->m_extended)); assert(block_header->IsNull() == (block_header->nBits == 0)); } { diff --git a/src/validation.cpp b/src/validation.cpp index 5dc10d36a375..0bfe2b6098ba 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -4117,14 +4117,17 @@ static bool ContextualCheckBlockHeader(const CBlockHeader& block, BlockValidatio if (block.nBits != GetNextWorkRequired(pindexPrev, &block, consensusParams)) return state.Invalid(BlockValidationResult::BLOCK_INVALID_HEADER, "bad-diffbits", "incorrect proof of work"); - if (pindexPrev->nTime >= CBlockHeader::EXTENDED_TIME_THRESHOLD) { - if (!block.m_extended || block.nTime < CBlockHeader::EXTENDED_TIME_THRESHOLD) { + const uint64_t block_time{block.GetBlockTime()}; + const uint64_t prev_block_time{pindexPrev->GetBlockTime()}; + + if (prev_block_time >= CBlockHeader::EXTENDED_TIME_THRESHOLD) { + if (!block.m_extended || block_time < CBlockHeader::EXTENDED_TIME_THRESHOLD) { return state.Invalid(BlockValidationResult::BLOCK_INVALID_HEADER, "bad-time-encoding", "post-threshold block must use extended timestamp encoding"); } } // Check timestamp against prev - if (block.nTime <= pindexPrev->GetMedianTimePast()) + if (block_time <= pindexPrev->GetMedianTimePast()) return state.Invalid(BlockValidationResult::BLOCK_INVALID_HEADER, "time-too-old", "block's timestamp is too early"); // Testnet4 and regtest only: Check timestamp against prev for difficulty-adjustment @@ -4133,14 +4136,14 @@ static bool ContextualCheckBlockHeader(const CBlockHeader& block, BlockValidatio // Check timestamp for the first block of each difficulty adjustment // interval, except the genesis block. if (nHeight % consensusParams.DifficultyAdjustmentInterval() == 0) { - if (block.nTime < pindexPrev->nTime && pindexPrev->nTime - block.nTime > MAX_TIMEWARP) { + if (block_time < prev_block_time && prev_block_time - block_time > MAX_TIMEWARP) { return state.Invalid(BlockValidationResult::BLOCK_INVALID_HEADER, "time-timewarp-attack", "block's timestamp is too early on diff adjustment block"); } } } // Check timestamp - if (BlockTimeTooNew(block.nTime)) { + if (BlockTimeTooNew(block_time)) { return state.Invalid(BlockValidationResult::BLOCK_TIME_FUTURE, "time-too-new", "block timestamp too far in the future"); } diff --git a/test/functional/feature_y2106_extended_time.py b/test/functional/feature_y2106_extended_time.py index 48d33799719b..15b6d060f90c 100755 --- a/test/functional/feature_y2106_extended_time.py +++ b/test/functional/feature_y2106_extended_time.py @@ -18,6 +18,7 @@ TIME_2106 = 2**32 - 1 MAX_UINT64 = 2**64 - 1 +TIME_EXTRA_NONCE = 0x5a class Y2106ExtendedTimeTest(BitcoinTestFramework): @@ -87,16 +88,18 @@ def run_test(self): assert_equal(self.nodes[0].submitblock(legacy_merkle_block.serialize().hex()), "bad-txnmrklroot") extended_block = create_block(tmpl=self.nodes[0].getblocktemplate(NORMAL_GBT_REQUEST_PARAMS)) - assert_equal(extended_block.nTime, TIME_2106 + 1) + extended_time = TIME_2106 + 1 + assert_equal(extended_block.nTime, extended_time) + extended_block.nTime |= TIME_EXTRA_NONCE self.solve_with_zero_low_nonce_byte(extended_block) extended_block_hex = extended_block.serialize().hex() assert_equal(self.nodes[0].submitblock(extended_block_hex), None) extended_header = CBlockHeader() extended_header.deserialize(BytesIO(bytes.fromhex(extended_block_hex))) - assert_equal(extended_header.nTime, TIME_2106 + 1) + assert_equal(extended_header.nTime, extended_time | TIME_EXTRA_NONCE) + assert_equal(extended_header.masked_time(), extended_time) assert extended_header.uses_extended_time_encoding() assert extended_header.nVersion < 0 - assert_equal(self.nodes[0].getblockheader(extended_block.hash_hex)["time"], TIME_2106 + 1) assert_raises_rpc_error( -5, "gettxoutproof is not supported for extended-header blocks", diff --git a/test/functional/test_framework/messages.py b/test/functional/test_framework/messages.py index 7dfa9b4a6fcf..9678331305b4 100755 --- a/test/functional/test_framework/messages.py +++ b/test/functional/test_framework/messages.py @@ -734,6 +734,8 @@ class CBlockHeader: "nTime", "nVersion", "m_extended") EXTENDED_TIME_THRESHOLD = 2**32 + EXTENDED_TIME_EXTRA_NONCE_MASK = 0xff + EXTENDED_TIME_MASK = 0xffffffffffffffff ^ EXTENDED_TIME_EXTRA_NONCE_MASK def __init__(self, header=None): if header is None: @@ -775,6 +777,9 @@ def serialize(self): def uses_extended_time_encoding(self): return self.m_extended + def masked_time(self): + return self.nTime & self.EXTENDED_TIME_MASK if self.m_extended else self.nTime + def set_extended_time_encoding(self): self.m_extended = True if self.nVersion >= 0: From 681ce8ebef40af2f5608decf11f4de58830cb879 Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Mon, 22 Jun 2026 14:58:19 +0200 Subject: [PATCH 09/10] consensus: desegwit --- src/consensus/merkle.cpp | 10 +++--- src/node/mining_types.h | 6 ++-- src/signet.cpp | 14 ++++++-- src/test/merkle_tests.cpp | 7 ++-- src/validation.cpp | 18 ++++++++++ src/validation.h | 8 ++--- .../functional/feature_y2106_extended_time.py | 34 +++++++++++++++---- test/functional/test_framework/messages.py | 2 +- 8 files changed, 75 insertions(+), 24 deletions(-) diff --git a/src/consensus/merkle.cpp b/src/consensus/merkle.cpp index 9de8226f374b..49e97d782dd2 100644 --- a/src/consensus/merkle.cpp +++ b/src/consensus/merkle.cpp @@ -73,10 +73,10 @@ uint256 BlockMerkleRoot(const CBlock& block, bool* mutated) leaves.reserve((block.vtx.size() + 1) & ~1ULL); // capacity rounded up to even for (size_t s = 0; s < block.vtx.size(); s++) { if (block.m_extended) { - // This draft tags the no-witness transaction serialization before - // using it as a merkle leaf. A real hardfork would likely define a - // tagged wtxid and remove the separate witness merkle tree. - leaves.push_back((HashWriter{TaggedHash("TaggedTxid")} << TX_NO_WITNESS(*block.vtx[s])).GetSHA256()); + // Extended blocks commit to witness data directly in the transaction + // merkle tree, so they do not use the legacy separate witness + // merkle commitment. + leaves.push_back((HashWriter{TaggedHash("TaggedWtxid")} << TX_WITH_WITNESS(*block.vtx[s])).GetSHA256()); } else { leaves.push_back(block.vtx[s]->GetHash().ToUint256()); } @@ -186,7 +186,7 @@ std::vector TransactionMerklePath(const CBlock& block, uint32_t positio leaves.resize(block.vtx.size()); for (size_t s = 0; s < block.vtx.size(); s++) { if (block.m_extended) { - leaves[s] = (HashWriter{TaggedHash("TaggedTxid")} << TX_NO_WITNESS(*block.vtx[s])).GetSHA256(); + leaves[s] = (HashWriter{TaggedHash("TaggedWtxid")} << TX_WITH_WITNESS(*block.vtx[s])).GetSHA256(); } else { leaves[s] = block.vtx[s]->GetHash().ToUint256(); } diff --git a/src/node/mining_types.h b/src/node/mining_types.h index 9c4fbcbbafeb..01221dc018d7 100644 --- a/src/node/mining_types.h +++ b/src/node/mining_types.h @@ -76,9 +76,9 @@ struct BlockCreateOptions { * * Note that higher level code like the getblocktemplate RPC may omit the * coinbase transaction entirely. It's instead constructed by pool software - * using fields like coinbasevalue, coinbaseaux and default_witness_commitment. - * This software typically also controls the payout outputs, even for solo - * mining. + * using fields like coinbasevalue, coinbaseaux and, for legacy blocks, + * default_witness_commitment. This software typically also controls the + * payout outputs, even for solo mining. * * The size and sigops are not checked against * coinbase_max_additional_weight and coinbase_output_max_additional_sigops. diff --git a/src/signet.cpp b/src/signet.cpp index bf85322250d9..ebc4ade20762 100644 --- a/src/signet.cpp +++ b/src/signet.cpp @@ -7,11 +7,13 @@ #include #include #include +#include #include #include #include