From b73b201855e1c0e2f7e798bc558067466ebb32e5 Mon Sep 17 00:00:00 2001 From: Konstantin Akimov Date: Tue, 30 Aug 2022 15:34:10 +0100 Subject: [PATCH 01/11] partial Merge bitcoin/bitcoin#25717: p2p: Implement anti-DoS headers sync BACKPORT NOTE: partial due to missing changes in bitcoin-chainstate.cpp 3add23454624c4c79c9eebc060b6fbed4e3131a7 ui: show header pre-synchronization progress (Pieter Wuille) 738421c50f2dbd7395b50a5dbdf6168b07435e62 Emit NotifyHeaderTip signals for pre-synchronization progress (Pieter Wuille) 376086fc5a187f5b2ab3a0d1202ed4e6c22bdb50 Make validation interface capable of signalling header presync (Pieter Wuille) 93eae27031a65b4156df49015ae45b2b541b4e5a Test large reorgs with headerssync logic (Suhas Daftuar) 355547334f7d08640ee1fa291227356d61145d1a Track headers presync progress and log it (Pieter Wuille) 03712dddfbb9fe0dc7a2ead53c65106189f5c803 Expose HeadersSyncState::m_current_height in getpeerinfo() (Suhas Daftuar) 150a5486db50ff77c91765392149000029c8a309 Test headers sync using minchainwork threshold (Suhas Daftuar) 0b6aa826b53470c9cc8ef4a153fa710dce80882f Add unit test for HeadersSyncState (Suhas Daftuar) 83c6a0c5249c4ecbd11f7828c84a50fb473faba3 Reduce spurious messages during headers sync (Suhas Daftuar) ed6cddd98e32263fc116a4380af6d66da20da990 Require callers of AcceptBlockHeader() to perform anti-dos checks (Suhas Daftuar) 551a8d957c4c44afbd0d608fcdf7c6a4352babce Utilize anti-DoS headers download strategy (Suhas Daftuar) ed470940cddbeb40425960d51cefeec4948febe4 Add functions to construct locators without CChain (Pieter Wuille) 84852bb6bb3579e475ce78fe729fd125ddbc715f Add bitdeque, an std::deque analogue that does bit packing. (Pieter Wuille) 1d4cfa4272cf2c8b980cc8762c1ff2220d3e8d51 Add function to validate difficulty changes (Suhas Daftuar) Pull request description: New nodes starting up for the first time lack protection against DoS from low-difficulty headers. While checkpoints serve as our protection against headers that fork from the main chain below the known checkpointed values, this protection only applies to nodes that have been able to download the honest chain to the checkpointed heights. We can protect all nodes from DoS from low-difficulty headers by adopting a different strategy: before we commit to storing a header in permanent storage, first verify that the header is part of a chain that has sufficiently high work (either `nMinimumChainWork`, or something comparable to our tip). This means that we will download headers from a given peer twice: once to verify the work on the chain, and a second time when permanently storing the headers. The p2p protocol doesn't provide an easy way for us to ensure that we receive the same headers during the second download of peer's headers chain. To ensure that a peer doesn't (say) give us the main chain in phase 1 to trick us into permanently storing an alternate, low-work chain in phase 2, we store commitments to the headers during our first download, which we validate in the second download. Some parameters must be chosen for commitment size/frequency in phase 1, and validation of commitments in phase 2. In this PR, those parameters are chosen to both (a) minimize the per-peer memory usage that an attacker could utilize, and (b) bound the expected amount of permanent memory that an attacker could get us to use to be well-below the memory growth that we'd get from the honest chain (where we expect 1 new block header every 10 minutes). After this PR, we should be able to remove checkpoints from our code, which is a nice philosophical change for us to make as well, as there has been confusion over the years about the role checkpoints play in Bitcoin's consensus algorithm. Thanks to Pieter Wuille for collaborating on this design. ACKs for top commit: Sjors: re-tACK 3add23454624c4c79c9eebc060b6fbed4e3131a7 mzumsande: re-ACK 3add23454624c4c79c9eebc060b6fbed4e3131a7 sipa: re-ACK 3add23454624c4c79c9eebc060b6fbed4e3131a7 glozow: ACK 3add234546 Tree-SHA512: e7789d65f62f72141b8899eb4a2fb3d0621278394d2d7adaa004675250118f89a4e4cb42777fe56649d744ec445ad95141e10f6def65f0a58b7b35b2e654a875 Co-authored-by: fanquake --- src/Makefile.am | 3 + src/Makefile.test.include | 2 + src/chain.cpp | 47 +- src/chain.h | 10 +- src/consensus/validation.h | 1 + src/headerssync.cpp | 317 ++++++++++ src/headerssync.h | 277 +++++++++ src/index/base.cpp | 2 +- src/interfaces/node.h | 2 +- src/logging.cpp | 3 + src/logging.h | 1 + src/net_processing.cpp | 470 +++++++++++++-- src/net_processing.h | 1 + src/node/interface_ui.cpp | 2 +- src/node/interface_ui.h | 2 +- src/node/interfaces.cpp | 8 +- src/pow.cpp | 69 +++ src/pow.h | 14 + src/qt/bitcoin.cpp | 2 + src/qt/bitcoingui.cpp | 30 +- src/qt/bitcoingui.h | 5 +- src/qt/clientmodel.cpp | 22 +- src/qt/clientmodel.h | 10 +- src/qt/informationwidget.cpp | 4 +- src/qt/informationwidget.h | 5 +- src/qt/modaloverlay.cpp | 12 +- src/qt/modaloverlay.h | 3 +- src/qt/rpcconsole.cpp | 3 +- src/qt/rpcconsole.h | 2 +- src/qt/sendcoinsdialog.cpp | 2 +- src/qt/sendcoinsdialog.h | 4 +- src/rpc/mining.cpp | 6 +- src/rpc/net.cpp | 2 + src/test/blockfilter_index_tests.cpp | 8 +- src/test/coinstatsindex_tests.cpp | 2 +- src/test/evo_deterministicmns_tests.cpp | 18 +- src/test/fuzz/bitdeque.cpp | 542 ++++++++++++++++++ src/test/fuzz/pow.cpp | 37 ++ src/test/fuzz/utxo_snapshot.cpp | 2 +- src/test/headers_sync_chainwork_tests.cpp | 146 +++++ src/test/miner_tests.cpp | 2 +- src/test/pow_tests.cpp | 17 +- src/test/skiplist_tests.cpp | 2 +- src/test/util/mining.cpp | 4 +- src/test/util/setup_common.cpp | 2 +- src/test/util_tests.cpp | 1 + src/test/validation_block_tests.cpp | 14 +- src/test/validation_chainstate_tests.cpp | 2 +- src/util/bitdeque.h | 469 +++++++++++++++ src/validation.cpp | 67 ++- src/validation.h | 31 +- test/functional/feature_block.py | 2 +- test/functional/p2p_compactblocks.py | 24 + test/functional/p2p_dos_header_tree.py | 4 +- .../p2p_headers_sync_with_minchainwork.py | 152 +++++ test/functional/p2p_unrequested_blocks.py | 14 +- test/functional/rpc_blockchain.py | 11 +- test/functional/rpc_net.py | 1 + test/functional/test_runner.py | 1 + 59 files changed, 2751 insertions(+), 167 deletions(-) create mode 100644 src/headerssync.cpp create mode 100644 src/headerssync.h create mode 100644 src/test/fuzz/bitdeque.cpp create mode 100644 src/test/headers_sync_chainwork_tests.cpp create mode 100644 src/util/bitdeque.h create mode 100755 test/functional/p2p_headers_sync_with_minchainwork.py diff --git a/src/Makefile.am b/src/Makefile.am index a0eca537b48d..b8e78d548019 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -245,6 +245,7 @@ BITCOIN_CORE_H = \ flat-database.h \ flatfile.h \ fs.h \ + headerssync.h \ httprpc.h \ httpserver.h \ i2p.h \ @@ -391,6 +392,7 @@ BITCOIN_CORE_H = \ undo.h \ unordered_lru_cache.h \ util/bip32.h \ + util/bitdeque.h \ util/bytevectorhash.h \ util/check.h \ util/edge.h \ @@ -531,6 +533,7 @@ libbitcoin_node_a_SOURCES = \ governance/vote.cpp \ governance/votedb.cpp \ gsl/assert.cpp \ + headerssync.cpp \ httprpc.cpp \ httpserver.cpp \ i2p.cpp \ diff --git a/src/Makefile.test.include b/src/Makefile.test.include index 375c9940d059..24623b882863 100644 --- a/src/Makefile.test.include +++ b/src/Makefile.test.include @@ -124,6 +124,7 @@ BITCOIN_TESTS =\ test/coinjoin_dstxmanager_tests.cpp \ test/coinjoin_queue_tests.cpp \ test/hash_tests.cpp \ + test/headers_sync_chainwork_tests.cpp \ test/httpserver_tests.cpp \ test/i2p_tests.cpp \ test/interfaces_tests.cpp \ @@ -283,6 +284,7 @@ test_fuzz_fuzz_SOURCES = \ test/fuzz/base_encode_decode.cpp \ test/fuzz/bech32.cpp \ test/fuzz/bip324.cpp \ + test/fuzz/bitdeque.cpp \ test/fuzz/block.cpp \ test/fuzz/block_header.cpp \ test/fuzz/blockfilter.cpp \ diff --git a/src/chain.cpp b/src/chain.cpp index 765c4d55a6ba..fb6455cbf271 100644 --- a/src/chain.cpp +++ b/src/chain.cpp @@ -29,32 +29,33 @@ void CChain::SetTip(CBlockIndex& block) } } -CBlockLocator CChain::GetLocator(const CBlockIndex *pindex) const { - int nStep = 1; - std::vector vHave; - vHave.reserve(32); - - if (!pindex) - pindex = Tip(); - while (pindex) { - vHave.push_back(pindex->GetBlockHash()); - // Stop when we have added the genesis block. - if (pindex->nHeight == 0) - break; +std::vector LocatorEntries(const CBlockIndex* index) +{ + int step = 1; + std::vector have; + if (index == nullptr) return have; + + have.reserve(32); + while (index) { + have.emplace_back(index->GetBlockHash()); + if (index->nHeight == 0) break; // Exponentially larger steps back, plus the genesis block. - int nHeight = std::max(pindex->nHeight - nStep, 0); - if (Contains(pindex)) { - // Use O(1) CChain index if possible. - pindex = (*this)[nHeight]; - } else { - // Otherwise, use O(log n) skiplist. - pindex = pindex->GetAncestor(nHeight); - } - if (vHave.size() > 10) - nStep *= 2; + int height = std::max(index->nHeight - step, 0); + // Use skiplist. + index = index->GetAncestor(height); + if (have.size() > 10) step *= 2; } + return have; +} - return CBlockLocator(vHave); +CBlockLocator GetLocator(const CBlockIndex* index) +{ + return CBlockLocator{std::move(LocatorEntries(index))}; +} + +CBlockLocator CChain::GetLocator() const +{ + return ::GetLocator(Tip()); } const CBlockIndex *CChain::FindFork(const CBlockIndex *pindex) const { diff --git a/src/chain.h b/src/chain.h index 4236e5a7adff..07ddfa274b7e 100644 --- a/src/chain.h +++ b/src/chain.h @@ -472,8 +472,8 @@ class CChain /** Set/initialize a chain with a given tip. */ void SetTip(CBlockIndex& block); - /** Return a CBlockLocator that refers to a block in this chain (by default the tip). */ - CBlockLocator GetLocator(const CBlockIndex* pindex = nullptr) const; + /** Return a CBlockLocator that refers to the tip in of this chain. */ + CBlockLocator GetLocator() const; /** Find the last common block between this chain and a block index entry. */ const CBlockIndex* FindFork(const CBlockIndex* pindex) const; @@ -482,4 +482,10 @@ class CChain CBlockIndex* FindEarliestAtLeast(int64_t nTime, int height) const; }; +/** Get a locator for a block index entry. */ +CBlockLocator GetLocator(const CBlockIndex* index); + +/** Construct a list of hash entries to put in a locator. */ +std::vector LocatorEntries(const CBlockIndex* index); + #endif // BITCOIN_CHAIN_H diff --git a/src/consensus/validation.h b/src/consensus/validation.h index 6d0942c5379e..7f102e5607bd 100644 --- a/src/consensus/validation.h +++ b/src/consensus/validation.h @@ -57,6 +57,7 @@ enum class BlockValidationResult { BLOCK_INVALID_PREV, //!< A block this one builds on is invalid BLOCK_TIME_FUTURE, //!< block timestamp was > 2 hours in the future (or our clock is bad) BLOCK_CHECKPOINT, //!< the block failed to meet one of our checkpoints + BLOCK_HEADER_LOW_WORK, //!< the block header may be on a too-little-work chain BLOCK_CHAINLOCK, //!< the block conflicts with the ChainLock }; diff --git a/src/headerssync.cpp b/src/headerssync.cpp new file mode 100644 index 000000000000..f8a0538cbc2b --- /dev/null +++ b/src/headerssync.cpp @@ -0,0 +1,317 @@ +// Copyright (c) 2022 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include +#include +#include +#include +#include + +// The two constants below are computed using the simulation script on +// https://gist.github.com/sipa/016ae445c132cdf65a2791534dfb7ae1 + +//! Store a commitment to a header every HEADER_COMMITMENT_PERIOD blocks. +constexpr size_t HEADER_COMMITMENT_PERIOD{584}; + +//! Only feed headers to validation once this many headers on top have been +//! received and validated against commitments. +constexpr size_t REDOWNLOAD_BUFFER_SIZE{13959}; // 13959/584 = ~23.9 commitments + +// Our memory analysis assumes 48 bytes for a CompressedHeader (so we should +// re-calculate parameters if we compress further) +static_assert(sizeof(CompressedHeader) == 48); + +HeadersSyncState::HeadersSyncState(NodeId id, const Consensus::Params& consensus_params, + const CBlockIndex* chain_start, const arith_uint256& minimum_required_work) : + m_id(id), m_consensus_params(consensus_params), + m_chain_start(chain_start), + m_minimum_required_work(minimum_required_work), + m_current_chain_work(chain_start->nChainWork), + m_commit_offset(GetRand(HEADER_COMMITMENT_PERIOD)), + m_last_header_received(m_chain_start->GetBlockHeader()), + m_current_height(chain_start->nHeight) +{ + // Estimate the number of blocks that could possibly exist on the peer's + // chain *right now* using 6 blocks/second (fastest blockrate given the MTP + // rule) times the number of seconds from the last allowed block until + // today. This serves as a memory bound on how many commitments we might + // store from this peer, and we can safely give up syncing if the peer + // exceeds this bound, because it's not possible for a consensus-valid + // chain to be longer than this (at the current time -- in the future we + // could try again, if necessary, to sync a longer chain). + m_max_commitments = 6*(Ticks(NodeClock::now() - NodeSeconds{std::chrono::seconds{chain_start->GetMedianTimePast()}}) + MAX_FUTURE_BLOCK_TIME) / HEADER_COMMITMENT_PERIOD; + + LogPrint(BCLog::HEADERSSYNC, "Initial headers sync started with peer=%d: height=%i, max_commitments=%i, min_work=%s\n", m_id, m_current_height, m_max_commitments, m_minimum_required_work.ToString()); +} + +/** Free any memory in use, and mark this object as no longer usable. This is + * required to guarantee that we won't reuse this object with the same + * SaltedTxidHasher for another sync. */ +void HeadersSyncState::Finalize() +{ + Assume(m_download_state != State::FINAL); + m_header_commitments = {}; + m_last_header_received.SetNull(); + m_redownloaded_headers = {}; + m_redownload_buffer_last_hash.SetNull(); + m_redownload_buffer_first_prev_hash.SetNull(); + m_process_all_remaining_headers = false; + m_current_height = 0; + + m_download_state = State::FINAL; +} + +/** Process the next batch of headers received from our peer. + * Validate and store commitments, and compare total chainwork to our target to + * see if we can switch to REDOWNLOAD mode. */ +HeadersSyncState::ProcessingResult HeadersSyncState::ProcessNextHeaders(const + std::vector& received_headers, const bool full_headers_message) +{ + ProcessingResult ret; + + Assume(!received_headers.empty()); + if (received_headers.empty()) return ret; + + Assume(m_download_state != State::FINAL); + if (m_download_state == State::FINAL) return ret; + + if (m_download_state == State::PRESYNC) { + // During PRESYNC, we minimally validate block headers and + // occasionally add commitments to them, until we reach our work + // threshold (at which point m_download_state is updated to REDOWNLOAD). + ret.success = ValidateAndStoreHeadersCommitments(received_headers); + if (ret.success) { + if (full_headers_message || m_download_state == State::REDOWNLOAD) { + // A full headers message means the peer may have more to give us; + // also if we just switched to REDOWNLOAD then we need to re-request + // headers from the beginning. + ret.request_more = true; + } else { + Assume(m_download_state == State::PRESYNC); + // If we're in PRESYNC and we get a non-full headers + // message, then the peer's chain has ended and definitely doesn't + // have enough work, so we can stop our sync. + LogPrint(BCLog::HEADERSSYNC, "Initial headers sync aborted with peer=%d: incomplete headers message at height=%i (presync phase)\n", m_id, m_current_height); + } + } + } else if (m_download_state == State::REDOWNLOAD) { + // During REDOWNLOAD, we compare our stored commitments to what we + // receive, and add headers to our redownload buffer. When the buffer + // gets big enough (meaning that we've checked enough commitments), + // we'll return a batch of headers to the caller for processing. + ret.success = true; + for (const auto& hdr : received_headers) { + if (!ValidateAndStoreRedownloadedHeader(hdr)) { + // Something went wrong -- the peer gave us an unexpected chain. + // We could consider looking at the reason for failure and + // punishing the peer, but for now just give up on sync. + ret.success = false; + break; + } + } + + if (ret.success) { + // Return any headers that are ready for acceptance. + ret.pow_validated_headers = PopHeadersReadyForAcceptance(); + + // If we hit our target blockhash, then all remaining headers will be + // returned and we can clear any leftover internal state. + if (m_redownloaded_headers.empty() && m_process_all_remaining_headers) { + LogPrint(BCLog::HEADERSSYNC, "Initial headers sync complete with peer=%d: releasing all at height=%i (redownload phase)\n", m_id, m_redownload_buffer_last_height); + } else if (full_headers_message) { + // If the headers message is full, we need to request more. + ret.request_more = true; + } else { + // For some reason our peer gave us a high-work chain, but is now + // declining to serve us that full chain again. Give up. + // Note that there's no more processing to be done with these + // headers, so we can still return success. + LogPrint(BCLog::HEADERSSYNC, "Initial headers sync aborted with peer=%d: incomplete headers message at height=%i (redownload phase)\n", m_id, m_redownload_buffer_last_height); + } + } + } + + if (!(ret.success && ret.request_more)) Finalize(); + return ret; +} + +bool HeadersSyncState::ValidateAndStoreHeadersCommitments(const std::vector& headers) +{ + // The caller should not give us an empty set of headers. + Assume(headers.size() > 0); + if (headers.size() == 0) return true; + + Assume(m_download_state == State::PRESYNC); + if (m_download_state != State::PRESYNC) return false; + + if (headers[0].hashPrevBlock != m_last_header_received.GetHash()) { + // Somehow our peer gave us a header that doesn't connect. + // This might be benign -- perhaps our peer reorged away from the chain + // they were on. Give up on this sync for now (likely we will start a + // new sync with a new starting point). + LogPrint(BCLog::HEADERSSYNC, "Initial headers sync aborted with peer=%d: non-continuous headers at height=%i (presync phase)\n", m_id, m_current_height); + return false; + } + + // If it does connect, (minimally) validate and occasionally store + // commitments. + for (const auto& hdr : headers) { + if (!ValidateAndProcessSingleHeader(hdr)) { + return false; + } + } + + if (m_current_chain_work >= m_minimum_required_work) { + m_redownloaded_headers.clear(); + m_redownload_buffer_last_height = m_chain_start->nHeight; + m_redownload_buffer_first_prev_hash = m_chain_start->GetBlockHash(); + m_redownload_buffer_last_hash = m_chain_start->GetBlockHash(); + m_redownload_chain_work = m_chain_start->nChainWork; + m_download_state = State::REDOWNLOAD; + LogPrint(BCLog::HEADERSSYNC, "Initial headers sync transition with peer=%d: reached sufficient work at height=%i, redownloading from height=%i\n", m_id, m_current_height, m_redownload_buffer_last_height); + } + return true; +} + +bool HeadersSyncState::ValidateAndProcessSingleHeader(const CBlockHeader& current) +{ + Assume(m_download_state == State::PRESYNC); + if (m_download_state != State::PRESYNC) return false; + + int next_height = m_current_height + 1; + + // Verify that the difficulty isn't growing too fast; an adversary with + // limited hashing capability has a greater chance of producing a high + // work chain if they compress the work into as few blocks as possible, + // so don't let anyone give a chain that would violate the difficulty + // adjustment maximum. + if (!PermittedDifficultyTransition(m_consensus_params, next_height, + m_last_header_received.nBits, current.nBits)) { + LogPrint(BCLog::HEADERSSYNC, "Initial headers sync aborted with peer=%d: invalid difficulty transition at height=%i (presync phase)\n", m_id, next_height); + return false; + } + + if (next_height % HEADER_COMMITMENT_PERIOD == m_commit_offset) { + // Add a commitment. + m_header_commitments.push_back(m_hasher(current.GetHash()) & 1); + if (m_header_commitments.size() > m_max_commitments) { + // The peer's chain is too long; give up. + // It's possible the chain grew since we started the sync; so + // potentially we could succeed in syncing the peer's chain if we + // try again later. + LogPrint(BCLog::HEADERSSYNC, "Initial headers sync aborted with peer=%d: exceeded max commitments at height=%i (presync phase)\n", m_id, next_height); + return false; + } + } + + m_current_chain_work += GetBlockProof(CBlockIndex(current)); + m_last_header_received = current; + m_current_height = next_height; + + return true; +} + +bool HeadersSyncState::ValidateAndStoreRedownloadedHeader(const CBlockHeader& header) +{ + Assume(m_download_state == State::REDOWNLOAD); + if (m_download_state != State::REDOWNLOAD) return false; + + int64_t next_height = m_redownload_buffer_last_height + 1; + + // Ensure that we're working on a header that connects to the chain we're + // downloading. + if (header.hashPrevBlock != m_redownload_buffer_last_hash) { + LogPrint(BCLog::HEADERSSYNC, "Initial headers sync aborted with peer=%d: non-continuous headers at height=%i (redownload phase)\n", m_id, next_height); + return false; + } + + // Check that the difficulty adjustments are within our tolerance: + uint32_t previous_nBits{0}; + if (!m_redownloaded_headers.empty()) { + previous_nBits = m_redownloaded_headers.back().nBits; + } else { + previous_nBits = m_chain_start->nBits; + } + + if (!PermittedDifficultyTransition(m_consensus_params, next_height, + previous_nBits, header.nBits)) { + LogPrint(BCLog::HEADERSSYNC, "Initial headers sync aborted with peer=%d: invalid difficulty transition at height=%i (redownload phase)\n", m_id, next_height); + return false; + } + + // Track work on the redownloaded chain + m_redownload_chain_work += GetBlockProof(CBlockIndex(header)); + + if (m_redownload_chain_work >= m_minimum_required_work) { + m_process_all_remaining_headers = true; + } + + // If we're at a header for which we previously stored a commitment, verify + // it is correct. Failure will result in aborting download. + // Also, don't check commitments once we've gotten to our target blockhash; + // it's possible our peer has extended its chain between our first sync and + // our second, and we don't want to return failure after we've seen our + // target blockhash just because we ran out of commitments. + if (!m_process_all_remaining_headers && next_height % HEADER_COMMITMENT_PERIOD == m_commit_offset) { + if (m_header_commitments.size() == 0) { + LogPrint(BCLog::HEADERSSYNC, "Initial headers sync aborted with peer=%d: commitment overrun at height=%i (redownload phase)\n", m_id, next_height); + // Somehow our peer managed to feed us a different chain and + // we've run out of commitments. + return false; + } + bool commitment = m_hasher(header.GetHash()) & 1; + bool expected_commitment = m_header_commitments.front(); + m_header_commitments.pop_front(); + if (commitment != expected_commitment) { + LogPrint(BCLog::HEADERSSYNC, "Initial headers sync aborted with peer=%d: commitment mismatch at height=%i (redownload phase)\n", m_id, next_height); + return false; + } + } + + // Store this header for later processing. + m_redownloaded_headers.push_back(header); + m_redownload_buffer_last_height = next_height; + m_redownload_buffer_last_hash = header.GetHash(); + + return true; +} + +std::vector HeadersSyncState::PopHeadersReadyForAcceptance() +{ + std::vector ret; + + Assume(m_download_state == State::REDOWNLOAD); + if (m_download_state != State::REDOWNLOAD) return ret; + + while (m_redownloaded_headers.size() > REDOWNLOAD_BUFFER_SIZE || + (m_redownloaded_headers.size() > 0 && m_process_all_remaining_headers)) { + ret.emplace_back(m_redownloaded_headers.front().GetFullHeader(m_redownload_buffer_first_prev_hash)); + m_redownloaded_headers.pop_front(); + m_redownload_buffer_first_prev_hash = ret.back().GetHash(); + } + return ret; +} + +CBlockLocator HeadersSyncState::NextHeadersRequestLocator() const +{ + Assume(m_download_state != State::FINAL); + if (m_download_state == State::FINAL) return {}; + + auto chain_start_locator = LocatorEntries(m_chain_start); + std::vector locator; + + if (m_download_state == State::PRESYNC) { + // During pre-synchronization, we continue from the last header received. + locator.push_back(m_last_header_received.GetHash()); + } + + if (m_download_state == State::REDOWNLOAD) { + // During redownload, we will download from the last received header that we stored. + locator.push_back(m_redownload_buffer_last_hash); + } + + locator.insert(locator.end(), chain_start_locator.begin(), chain_start_locator.end()); + + return CBlockLocator{std::move(locator)}; +} diff --git a/src/headerssync.h b/src/headerssync.h new file mode 100644 index 000000000000..16da9642462d --- /dev/null +++ b/src/headerssync.h @@ -0,0 +1,277 @@ +// Copyright (c) 2022 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_HEADERSSYNC_H +#define BITCOIN_HEADERSSYNC_H + +#include +#include +#include +#include // For NodeId +#include +#include +#include +#include + +#include +#include + +// A compressed CBlockHeader, which leaves out the prevhash +struct CompressedHeader { + // header + int32_t nVersion{0}; + uint256 hashMerkleRoot; + uint32_t nTime{0}; + uint32_t nBits{0}; + uint32_t nNonce{0}; + + CompressedHeader() + { + hashMerkleRoot.SetNull(); + } + + CompressedHeader(const CBlockHeader& header) + { + nVersion = header.nVersion; + hashMerkleRoot = header.hashMerkleRoot; + nTime = header.nTime; + nBits = header.nBits; + nNonce = header.nNonce; + } + + CBlockHeader GetFullHeader(const uint256& hash_prev_block) { + CBlockHeader ret; + ret.nVersion = nVersion; + ret.hashPrevBlock = hash_prev_block; + ret.hashMerkleRoot = hashMerkleRoot; + ret.nTime = nTime; + ret.nBits = nBits; + ret.nNonce = nNonce; + return ret; + }; +}; + +/** HeadersSyncState: + * + * We wish to download a peer's headers chain in a DoS-resistant way. + * + * The Bitcoin protocol does not offer an easy way to determine the work on a + * peer's chain. Currently, we can query a peer's headers by using a GETHEADERS + * message, and our peer can return a set of up to 2000 headers that connect to + * something we know. If a peer's chain has more than 2000 blocks, then we need + * a way to verify that the chain actually has enough work on it to be useful to + * us -- by being above our anti-DoS minimum-chain-work threshold -- before we + * commit to storing those headers in memory. Otherwise, it would be cheap for + * an attacker to waste all our memory by serving us low-work headers + * (particularly for a new node coming online for the first time). + * + * To prevent memory-DoS with low-work headers, while still always being + * able to reorg to whatever the most-work chain is, we require that a chain + * meet a work threshold before committing it to memory. We can do this by + * downloading a peer's headers twice, whenever we are not sure that the chain + * has sufficient work: + * + * - In the first download phase, called pre-synchronization, we can calculate + * the work on the chain as we go (just by checking the nBits value on each + * header, and validating the proof-of-work). + * + * - Once we have reached a header where the cumulative chain work is + * sufficient, we switch to downloading the headers a second time, this time + * processing them fully, and possibly storing them in memory. + * + * To prevent an attacker from using (eg) the honest chain to convince us that + * they have a high-work chain, but then feeding us an alternate set of + * low-difficulty headers in the second phase, we store commitments to the + * chain we see in the first download phase that we check in the second phase, + * as follows: + * + * - In phase 1 (presync), store 1 bit (using a salted hash function) for every + * N headers that we see. With a reasonable choice of N, this uses relatively + * little memory even for a very long chain. + * + * - In phase 2 (redownload), keep a lookahead buffer and only accept headers + * from that buffer into the block index (permanent memory usage) once they + * have some target number of verified commitments on top of them. With this + * parametrization, we can achieve a given security target for potential + * permanent memory usage, while choosing N to minimize memory use during the + * sync (temporary, per-peer storage). + */ + +class HeadersSyncState { +public: + ~HeadersSyncState() {} + + enum class State { + /** PRESYNC means the peer has not yet demonstrated their chain has + * sufficient work and we're only building commitments to the chain they + * serve us. */ + PRESYNC, + /** REDOWNLOAD means the peer has given us a high-enough-work chain, + * and now we're redownloading the headers we saw before and trying to + * accept them */ + REDOWNLOAD, + /** We're done syncing with this peer and can discard any remaining state */ + FINAL + }; + + /** Return the current state of our download */ + State GetState() const { return m_download_state; } + + /** Return the height reached during the PRESYNC phase */ + 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; } + + /** Return the amount of work in the chain received during the PRESYNC phase. */ + arith_uint256 GetPresyncWork() const { return m_current_chain_work; } + + /** Construct a HeadersSyncState object representing a headers sync via this + * download-twice mechanism). + * + * id: node id (for logging) + * consensus_params: parameters needed for difficulty adjustment validation + * chain_start: best known fork point that the peer's headers branch from + * minimum_required_work: amount of chain work required to accept the chain + */ + HeadersSyncState(NodeId id, const Consensus::Params& consensus_params, + const CBlockIndex* chain_start, const arith_uint256& minimum_required_work); + + /** Result data structure for ProcessNextHeaders. */ + struct ProcessingResult { + std::vector pow_validated_headers; + bool success{false}; + bool request_more{false}; + }; + + /** Process a batch of headers, once a sync via this mechanism has started + * + * received_headers: headers that were received over the network for processing. + * Assumes the caller has already verified the headers + * are continuous, and has checked that each header + * satisfies the proof-of-work target included in the + * header (but not necessarily verified that the + * proof-of-work target is correct and passes consensus + * rules). + * full_headers_message: true if the message was at max capacity, + * indicating more headers may be available + * ProcessingResult.pow_validated_headers: will be filled in with any + * headers that the caller can fully process and + * validate now (because these returned headers are + * on a chain with sufficient work) + * ProcessingResult.success: set to false if an error is detected and the sync is + * aborted; true otherwise. + * ProcessingResult.request_more: if true, the caller is suggested to call + * NextHeadersRequestLocator and send a getheaders message using it. + */ + ProcessingResult ProcessNextHeaders(const std::vector& + received_headers, bool full_headers_message); + + /** Issue the next GETHEADERS message to our peer. + * + * This will return a locator appropriate for the current sync object, to continue the + * synchronization phase it is in. + */ + CBlockLocator NextHeadersRequestLocator() const; + +private: + /** Clear out all download state that might be in progress (freeing any used + * memory), and mark this object as no longer usable. + */ + void Finalize(); + + /** + * Only called in PRESYNC. + * Validate the work on the headers we received from the network, and + * store commitments for later. Update overall state with successfully + * processed headers. + * On failure, this invokes Finalize() and returns false. + */ + bool ValidateAndStoreHeadersCommitments(const std::vector& headers); + + /** In PRESYNC, process and update state for a single header */ + bool ValidateAndProcessSingleHeader(const CBlockHeader& current); + + /** In REDOWNLOAD, check a header's commitment (if applicable) and add to + * buffer for later processing */ + bool ValidateAndStoreRedownloadedHeader(const CBlockHeader& header); + + /** Return a set of headers that satisfy our proof-of-work threshold */ + std::vector PopHeadersReadyForAcceptance(); + +private: + /** NodeId of the peer (used for log messages) **/ + const NodeId m_id; + + /** We use the consensus params in our anti-DoS calculations */ + const Consensus::Params& m_consensus_params; + + /** Store the last block in our block index that the peer's chain builds from */ + const CBlockIndex* m_chain_start{nullptr}; + + /** Minimum work that we're looking for on this chain. */ + const arith_uint256 m_minimum_required_work; + + /** Work that we've seen so far on the peer's chain */ + arith_uint256 m_current_chain_work; + + /** m_hasher is a salted hasher for making our 1-bit commitments to headers we've seen. */ + const SaltedTxidHasher m_hasher; + + /** A queue of commitment bits, created during the 1st phase, and verified during the 2nd. */ + bitdeque<> m_header_commitments; + + /** The (secret) offset on the heights for which to create commitments. + * + * m_header_commitments entries are created at any height h for which + * (h % HEADER_COMMITMENT_PERIOD) == m_commit_offset. */ + const unsigned m_commit_offset; + + /** m_max_commitments is a bound we calculate on how long an honest peer's chain could be, + * given the MTP rule. + * + * Any peer giving us more headers than this will have its sync aborted. This serves as a + * memory bound on m_header_commitments. */ + uint64_t m_max_commitments{0}; + + /** Store the latest header received while in PRESYNC (initialized to m_chain_start) */ + CBlockHeader m_last_header_received; + + /** Height of m_last_header_received */ + int64_t m_current_height{0}; + + /** During phase 2 (REDOWNLOAD), we buffer redownloaded headers in memory + * until enough commitments have been verified; those are stored in + * m_redownloaded_headers */ + std::deque m_redownloaded_headers; + + /** Height of last header in m_redownloaded_headers */ + int64_t m_redownload_buffer_last_height{0}; + + /** Hash of last header in m_redownloaded_headers (initialized to + * m_chain_start). We have to cache it because we don't have hashPrevBlock + * available in a CompressedHeader. + */ + uint256 m_redownload_buffer_last_hash; + + /** The hashPrevBlock entry for the first header in m_redownloaded_headers + * We need this to reconstruct the full header when it's time for + * processing. + */ + uint256 m_redownload_buffer_first_prev_hash; + + /** The accumulated work on the redownloaded chain. */ + arith_uint256 m_redownload_chain_work; + + /** Set this to true once we encounter the target blockheader during phase + * 2 (REDOWNLOAD). At this point, we can process and store all remaining + * headers still in m_redownloaded_headers. + */ + bool m_process_all_remaining_headers{false}; + + /** Current state of our headers sync. */ + State m_download_state{State::PRESYNC}; +}; + +#endif // BITCOIN_HEADERSSYNC_H diff --git a/src/index/base.cpp b/src/index/base.cpp index d585cac6dd53..faa83a576026 100644 --- a/src/index/base.cpp +++ b/src/index/base.cpp @@ -217,7 +217,7 @@ bool BaseIndex::CommitInternal(CDBBatch& batch) if (m_best_block_index == nullptr) { return false; } - GetDB().WriteBestBlock(batch, m_chainstate->m_chain.GetLocator(m_best_block_index)); + GetDB().WriteBestBlock(batch, GetLocator(m_best_block_index)); return true; } diff --git a/src/interfaces/node.h b/src/interfaces/node.h index 52e241a24ee3..6e63a8e1c4db 100644 --- a/src/interfaces/node.h +++ b/src/interfaces/node.h @@ -512,7 +512,7 @@ class Node //! Register handler for header tip messages. using NotifyHeaderTipFn = - std::function; + std::function; virtual std::unique_ptr handleNotifyHeaderTip(NotifyHeaderTipFn fn) = 0; //! Register handler for InstantSend data messages. diff --git a/src/logging.cpp b/src/logging.cpp index 98dcf1e2c69d..1627be1971dc 100644 --- a/src/logging.cpp +++ b/src/logging.cpp @@ -180,6 +180,7 @@ const CLogCategoryDesc LogCategories[] = {BCLog::BLOCKSTORE, "blockstorage"}, {BCLog::TXRECONCILIATION, "txreconciliation"}, {BCLog::SCAN, "scan"}, + {BCLog::HEADERSSYNC, "headerssync"}, {BCLog::ALL, "1"}, {BCLog::ALL, "all"}, @@ -297,6 +298,8 @@ std::string LogCategoryToStr(BCLog::LogFlags category) return "txreconciliation"; case BCLog::LogFlags::SCAN: return "scan"; + case BCLog::LogFlags::HEADERSSYNC: + return "headerssync"; /* Start Dash */ case BCLog::LogFlags::CHAINLOCKS: return "chainlocks"; diff --git a/src/logging.h b/src/logging.h index d0b2069319f0..c35d40e263b8 100644 --- a/src/logging.h +++ b/src/logging.h @@ -69,6 +69,7 @@ namespace BCLog { BLOCKSTORE = (1 << 26), TXRECONCILIATION = (1 << 27), SCAN = (1 << 28), + HEADERSSYNC = (1 << 29), //Start Dash CHAINLOCKS = ((uint64_t)1 << 32), diff --git a/src/net_processing.cpp b/src/net_processing.cpp index 3f8ba2e11b48..63347705e818 100644 --- a/src/net_processing.cpp +++ b/src/net_processing.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -411,6 +412,15 @@ struct Peer { /** Time of the last getheaders message to this peer */ NodeClock::time_point m_last_getheaders_timestamp GUARDED_BY(NetEventsInterface::g_msgproc_mutex){}; + /** Protects m_headers_sync **/ + Mutex m_headers_sync_mutex; + /** Headers-sync state for this peer (eg for initial sync, or syncing large + * reorgs) **/ + std::unique_ptr m_headers_sync PT_GUARDED_BY(m_headers_sync_mutex) GUARDED_BY(m_headers_sync_mutex) {}; + + /** Whether we've sent our peer a sendheaders message. **/ + std::atomic m_sent_sendheaders{false}; + explicit Peer(NodeId id, ServiceFlags our_services) : m_id(id) , m_our_services{our_services} @@ -614,9 +624,9 @@ class PeerManagerImpl final : public PeerManager /** Implement NetEventsInterface */ void InitializeNode(CNode& node, ServiceFlags our_services) override EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex); - void FinalizeNode(const CNode& node) override EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex); + void FinalizeNode(const CNode& node) override EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex, !m_headers_presync_mutex); bool ProcessMessages(CNode* pfrom, std::atomic& interrupt) override - EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex, !m_recent_confirmed_transactions_mutex, !m_most_recent_block_mutex, g_msgproc_mutex); + EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex, !m_recent_confirmed_transactions_mutex, !m_most_recent_block_mutex, !m_headers_presync_mutex, g_msgproc_mutex); bool SendMessages(CNode* pto) override EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex, !m_recent_confirmed_transactions_mutex, !m_most_recent_block_mutex, g_msgproc_mutex); @@ -638,7 +648,7 @@ class PeerManagerImpl final : public PeerManager void Misbehaving(const NodeId pnode, const int howmuch, const std::string& message = "") override EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex); void ProcessMessage(CNode& pfrom, const std::string& msg_type, CDataStream& vRecv, const std::chrono::microseconds time_received, const std::atomic& interruptMsgProc) override - EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex, !m_recent_confirmed_transactions_mutex, !m_most_recent_block_mutex, g_msgproc_mutex); + EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex, !m_recent_confirmed_transactions_mutex, !m_most_recent_block_mutex, !m_headers_presync_mutex, g_msgproc_mutex); void UpdateLastBlockAnnounceTime(NodeId node, int64_t time_in_seconds) override; bool IsBanned(NodeId pnode) override EXCLUSIVE_LOCKS_REQUIRED(cs_main, !m_peer_mutex); size_t GetRequestedObjectCount(NodeId nodeid) const override EXCLUSIVE_LOCKS_REQUIRED(::cs_main); @@ -753,15 +763,25 @@ class PeerManagerImpl final : public PeerManager */ bool ProcessOrphanTx(NodeId node_id) EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex, cs_main); - /** Process a single headers message from a peer. */ + /** Process a single headers message from a peer. + * + * @param[in] pfrom CNode of the peer + * @param[in] peer The peer sending us the headers + * @param[in] headers The headers received. Note that this may be modified within ProcessHeadersMessage. + * @param[in] via_compact_block Whether this header came in via compact block handling. + */ void ProcessHeadersMessage(CNode& pfrom, Peer& peer, - const std::vector& headers, - bool via_compact_block) - EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex, g_msgproc_mutex); + std::vector&& headers, + bool via_compact_block, bool uses_compressed) + EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex, !m_headers_presync_mutex, g_msgproc_mutex); [[nodiscard]] MessageProcessingResult ProcessPlatformBanMessage(NodeId node, std::string_view msg_type, CDataStream& vRecv) - EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex, g_msgproc_mutex); + EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex, !m_headers_presync_mutex, g_msgproc_mutex); /** Various helpers for headers processing, invoked by ProcessHeadersMessage() */ + /** Return true if headers are continuous and have valid proof-of-work (DoS points assigned on failure) */ + bool CheckHeadersPoW(const std::vector& headers, const Consensus::Params& consensusParams, CNode& pfrom) EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex); + /** Calculate an anti-DoS work threshold for headers chains */ + arith_uint256 GetAntiDoSWorkThreshold(); /** Deal with state tracking and headers sync for peers that send the * occasional non-connecting header (this can happen due to BIP 130 headers * announcements for blocks interacting with the 2hr (MAX_FUTURE_BLOCK_TIME) rule). */ @@ -769,6 +789,48 @@ class PeerManagerImpl final : public PeerManager EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex, g_msgproc_mutex); /** Return true if the headers connect to each other, false otherwise */ bool CheckHeadersAreContinuous(const std::vector& headers) const; + /** Try to continue a low-work headers sync that has already begun. + * Assumes the caller has already verified the headers connect, and has + * checked that each header satisfies the proof-of-work target included in + * the header. + * @param[in] peer The peer we're syncing with. + * @param[in] pfrom CNode of the peer + * @param[in,out] headers The headers to be processed. + * @return True if the passed in headers were successfully processed + * as the continuation of a low-work headers sync in progress; + * false otherwise. + * If false, the passed in headers will be returned back to + * the caller. + * If true, the returned headers may be empty, indicating + * there is no more work for the caller to do; or the headers + * may be populated with entries that have passed anti-DoS + * checks (and therefore may be validated for block index + * acceptance by the caller). + */ + bool IsContinuationOfLowWorkHeadersSync(Peer& peer, CNode& pfrom, + std::vector& headers, bool uses_compressed) + EXCLUSIVE_LOCKS_REQUIRED(peer.m_headers_sync_mutex, !m_headers_presync_mutex, g_msgproc_mutex); + /** Check work on a headers chain to be processed, and if insufficient, + * initiate our anti-DoS headers sync mechanism. + * + * @param[in] peer The peer whose headers we're processing. + * @param[in] pfrom CNode of the peer + * @param[in] chain_start_header Where these headers connect in our index. + * @param[in,out] headers The headers to be processed. + * + * @return True if chain was low work and a headers sync was + * initiated (and headers will be empty after calling); false + * otherwise. + */ + bool TryLowWorkHeadersSync(Peer& peer, CNode& pfrom, + const CBlockIndex* chain_start_header, + std::vector& headers, bool uses_compressed) + EXCLUSIVE_LOCKS_REQUIRED(!peer.m_headers_sync_mutex, !m_peer_mutex, !m_headers_presync_mutex, g_msgproc_mutex); + + /** Return true if the given header is an ancestor of + * m_chainman.m_best_header or our current tip */ + bool IsAncestorOfBestHeaderOrTip(const CBlockIndex* header) EXCLUSIVE_LOCKS_REQUIRED(cs_main); + /** Request further headers from this peer with a given locator. * We don't issue a getheaders message if we have a recent one outstanding. * This returns true if a getheaders is actually sent, and false otherwise. @@ -796,6 +858,9 @@ class PeerManagerImpl final : public PeerManager void MaybeSendAddr(CNode& node, Peer& peer, std::chrono::microseconds current_time) EXCLUSIVE_LOCKS_REQUIRED(g_msgproc_mutex); + /** Send a single `sendheaders` message, after we have completed headers sync with a peer. */ + void MaybeSendSendHeaders(CNode& node, Peer& peer); + /** Relay (gossip) an address to a few randomly chosen nodes. * * @param[in] originator The id of the peer that sent us the address. We don't want to relay it back. @@ -1030,6 +1095,24 @@ class PeerManagerImpl final : public PeerManager std::shared_ptr m_most_recent_compact_block GUARDED_BY(m_most_recent_block_mutex); uint256 m_most_recent_block_hash GUARDED_BY(m_most_recent_block_mutex); + // Data about the low-work headers synchronization, aggregated from all peers' HeadersSyncStates. + /** Mutex guarding the other m_headers_presync_* variables. */ + Mutex m_headers_presync_mutex; + /** A type to represent statistics about a peer's low-work headers sync. + * + * - The first field is the total verified amount of work in that synchronization. + * - The second is: + * - nullopt: the sync is in REDOWNLOAD phase (phase 2). + * - {height, timestamp}: the sync has the specified tip height and block timestamp (phase 1). + */ + using HeadersPresyncStats = std::pair>>; + /** Statistics for all peers in low-work headers sync. */ + std::map m_headers_presync_stats GUARDED_BY(m_headers_presync_mutex) {}; + /** The peer with the most-work entry in m_headers_presync_stats. */ + NodeId m_headers_presync_bestpeer GUARDED_BY(m_headers_presync_mutex) {-1}; + /** The m_headers_presync_stats improved, and needs signalling. */ + std::atomic_bool m_headers_presync_should_signal{false}; + /** Height of the highest block announced using BIP 152 high-bandwidth mode. */ int m_highest_fast_announce GUARDED_BY(::cs_main){0}; @@ -1070,7 +1153,7 @@ class PeerManagerImpl final : public PeerManager EXCLUSIVE_LOCKS_REQUIRED(!m_most_recent_block_mutex, peer.m_getdata_requests_mutex) LOCKS_EXCLUDED(::cs_main); /** Process a new block. Perform any post-processing housekeeping */ - void ProcessBlock(CNode& from, const std::shared_ptr& pblock, bool force_processing); + void ProcessBlock(CNode& from, const std::shared_ptr& pblock, bool force_processing, bool min_pow_checked); /** Relay map (txid -> CTransactionRef) */ typedef std::map MapRelay; @@ -1820,7 +1903,10 @@ void PeerManagerImpl::FinalizeNode(const CNode& node) { // fSuccessfullyConnected set. m_addrman.Connected(node.addr); } - + { + LOCK(m_headers_presync_mutex); + m_headers_presync_stats.erase(nodeid); + } LogPrint(BCLog::NET, "Cleared nodestate for peer=%d\n", nodeid); } @@ -1884,6 +1970,12 @@ bool PeerManagerImpl::GetNodeStateStats(NodeId nodeid, CNodeStateStats& stats) c stats.m_addr_processed = peer->m_addr_processed.load(); stats.m_addr_rate_limited = peer->m_addr_rate_limited.load(); stats.m_addr_relay_enabled = peer->m_addr_relay_enabled.load(); + { + LOCK(peer->m_headers_sync_mutex); + if (peer->m_headers_sync) { + stats.presync_height = peer->m_headers_sync->GetPresyncHeight(); + } + } return true; } @@ -1944,6 +2036,10 @@ bool PeerManagerImpl::MaybePunishNodeForBlock(NodeId nodeid, const BlockValidati switch (state.GetResult()) { case BlockValidationResult::BLOCK_RESULT_UNSET: break; + case BlockValidationResult::BLOCK_HEADER_LOW_WORK: + // We didn't try to process the block because the header chain may have + // too little work. + break; // The node is providing invalid data: case BlockValidationResult::BLOCK_CONSENSUS: case BlockValidationResult::BLOCK_MUTATED: @@ -3101,6 +3197,35 @@ void PeerManagerImpl::SendBlockTransactions(CNode& pfrom, const CBlock& block, c m_connman.PushMessage(&pfrom, msgMaker.Make(NetMsgType::BLOCKTXN, resp)); } +bool PeerManagerImpl::CheckHeadersPoW(const std::vector& headers, const Consensus::Params& consensusParams, CNode& pfrom) +{ + // Do these headers have proof-of-work matching what's claimed? + if (!HasValidProofOfWork(headers, consensusParams)) { + Misbehaving(pfrom.GetId(), 100, "header with invalid proof of work"); + return false; + } + + // Are these headers connected to each other? + if (!CheckHeadersAreContinuous(headers)) { + Misbehaving(pfrom.GetId(), 20, "non-continuous headers sequence"); + return false; + } + return true; +} + +arith_uint256 PeerManagerImpl::GetAntiDoSWorkThreshold() +{ + arith_uint256 near_chaintip_work = 0; + LOCK(cs_main); + if (m_chainman.ActiveChain().Tip() != nullptr) { + const CBlockIndex *tip = m_chainman.ActiveChain().Tip(); + // Use a 144 block buffer, so that we'll accept headers that fork from + // near our tip. + near_chaintip_work = tip->nChainWork - std::min(144*GetBlockProof(*tip), tip->nChainWork); + } + return std::max(near_chaintip_work, arith_uint256(nMinimumChainWork)); +} + /** * Special handling for unconnecting headers that might be part of a block * announcement. @@ -3122,7 +3247,7 @@ void PeerManagerImpl::HandleFewUnconnectingHeaders(CNode& pfrom, Peer& peer, nodestate->nUnconnectingHeaders++; // Try to fill in the missing headers. std::string msg_type = UsesCompressedHeaders(peer) ? NetMsgType::GETHEADERS2 : NetMsgType::GETHEADERS; - if (MaybeSendGetHeaders(pfrom, msg_type, m_chainman.ActiveChain().GetLocator(m_chainman.m_best_header), peer)) { + if (MaybeSendGetHeaders(pfrom, msg_type, GetLocator(m_chainman.m_best_header), peer)) { LogPrint(BCLog::NET, "received header %s: missing prev block %s, sending %s (%d) to end (peer=%d, nUnconnectingHeaders=%d)\n", headers[0].GetHash().ToString(), headers[0].hashPrevBlock.ToString(), @@ -3154,6 +3279,147 @@ bool PeerManagerImpl::CheckHeadersAreContinuous(const std::vector& return true; } +bool PeerManagerImpl::IsContinuationOfLowWorkHeadersSync(Peer& peer, CNode& pfrom, std::vector& headers, bool uses_compressed) +{ + if (peer.m_headers_sync) { + auto result = peer.m_headers_sync->ProcessNextHeaders(headers, headers.size() == GetHeadersLimit(pfrom, uses_compressed)); + if (result.request_more) { + auto locator = peer.m_headers_sync->NextHeadersRequestLocator(); + // If we were instructed to ask for a locator, it should not be empty. + Assume(!locator.vHave.empty()); + if (!locator.vHave.empty()) { + // It should be impossible for the getheaders request to fail, + // because we should have cleared the last getheaders timestamp + // when processing the headers that triggered this call. But + // it may be possible to bypass this via compactblock + // processing, so check the result before logging just to be + // safe. + std::string msg_type = UsesCompressedHeaders(peer) ? NetMsgType::GETHEADERS2 : NetMsgType::GETHEADERS; + bool sent_getheaders = MaybeSendGetHeaders(pfrom, msg_type, locator, peer); + if (sent_getheaders) { + LogPrint(BCLog::NET, "more getheaders (from %s) to peer=%d\n", + locator.vHave.front().ToString(), pfrom.GetId()); + } else { + LogPrint(BCLog::NET, "error sending next getheaders (from %s) to continue sync with peer=%d\n", + locator.vHave.front().ToString(), pfrom.GetId()); + } + } + } + + if (peer.m_headers_sync->GetState() == HeadersSyncState::State::FINAL) { + peer.m_headers_sync.reset(nullptr); + + // Delete this peer's entry in m_headers_presync_stats. + // If this is m_headers_presync_bestpeer, it will be replaced later + // by the next peer that triggers the else{} branch below. + LOCK(m_headers_presync_mutex); + m_headers_presync_stats.erase(pfrom.GetId()); + } else { + // Build statistics for this peer's sync. + HeadersPresyncStats stats; + stats.first = peer.m_headers_sync->GetPresyncWork(); + if (peer.m_headers_sync->GetState() == HeadersSyncState::State::PRESYNC) { + stats.second = {peer.m_headers_sync->GetPresyncHeight(), + peer.m_headers_sync->GetPresyncTime()}; + } + + // Update statistics in stats. + LOCK(m_headers_presync_mutex); + m_headers_presync_stats[pfrom.GetId()] = stats; + auto best_it = m_headers_presync_stats.find(m_headers_presync_bestpeer); + bool best_updated = false; + if (best_it == m_headers_presync_stats.end()) { + // If the cached best peer is outdated, iterate over all remaining ones (including + // newly updated one) to find the best one. + NodeId peer_best{-1}; + const HeadersPresyncStats* stat_best{nullptr}; + for (const auto& [peer, stat] : m_headers_presync_stats) { + if (!stat_best || stat > *stat_best) { + peer_best = peer; + stat_best = &stat; + } + } + m_headers_presync_bestpeer = peer_best; + best_updated = (peer_best == pfrom.GetId()); + } else if (best_it->first == pfrom.GetId() || stats > best_it->second) { + // pfrom was and remains the best peer, or pfrom just became best. + m_headers_presync_bestpeer = pfrom.GetId(); + best_updated = true; + } + if (best_updated && stats.second.has_value()) { + // If the best peer updated, and it is in its first phase, signal. + m_headers_presync_should_signal = true; + } + } + + if (result.success) { + // We only overwrite the headers passed in if processing was + // successful. + headers.swap(result.pow_validated_headers); + } + + return result.success; + } + // Either we didn't have a sync in progress, or something went wrong + // processing these headers, or we are returning headers to the caller to + // process. + return false; +} + +bool PeerManagerImpl::TryLowWorkHeadersSync(Peer& peer, CNode& pfrom, const CBlockIndex* chain_start_header, std::vector& headers, bool uses_compressed) +{ + // Calculate the total work on this chain. + arith_uint256 total_work = chain_start_header->nChainWork + CalculateHeadersWork(headers); + + // Our dynamic anti-DoS threshold (minimum work required on a headers chain + // before we'll store it) + arith_uint256 minimum_chain_work = GetAntiDoSWorkThreshold(); + + // Avoid DoS via low-difficulty-headers by only processing if the headers + // are part of a chain with sufficient work. + if (total_work < minimum_chain_work) { + // Only try to sync with this peer if their headers message was full; + // otherwise they don't have more headers after this so no point in + // trying to sync their too-little-work chain. + if (headers.size() == GetHeadersLimit(pfrom, uses_compressed)) { + // Note: we could advance to the last header in this set that is + // known to us, rather than starting at the first header (which we + // may already have); however this is unlikely to matter much since + // ProcessHeadersMessage() already handles the case where all + // headers in a received message are already known and are + // ancestors of m_best_header or chainActive.Tip(), by skipping + // this logic in that case. So even if the first header in this set + // of headers is known, some header in this set must be new, so + // advancing to the first unknown header would be a small effect. + LOCK(peer.m_headers_sync_mutex); + peer.m_headers_sync.reset(new HeadersSyncState(peer.m_id, m_chainparams.GetConsensus(), + chain_start_header, minimum_chain_work)); + + // Now a HeadersSyncState object for tracking this synchronization is created, + // process the headers using it as normal. + return IsContinuationOfLowWorkHeadersSync(peer, pfrom, headers, uses_compressed); + } else { + LogPrint(BCLog::NET, "Ignoring low-work chain (height=%u) from peer=%d\n", chain_start_header->nHeight + headers.size(), pfrom.GetId()); + // Since this is a low-work headers chain, no further processing is required. + headers = {}; + return true; + } + } + return false; +} + +bool PeerManagerImpl::IsAncestorOfBestHeaderOrTip(const CBlockIndex* header) +{ + if (header == nullptr) { + return false; + } else if (m_chainman.m_best_header != nullptr && header == m_chainman.m_best_header->GetAncestor(header->nHeight)) { + return true; + } else if (m_chainman.ActiveChain().Contains(header)) { + return true; + } + return false; +} + bool PeerManagerImpl::MaybeSendGetHeaders(CNode& pfrom, const std::string& msg_type, const CBlockLocator& locator, Peer& peer) { assert(msg_type == NetMsgType::GETHEADERS || msg_type == NetMsgType::GETHEADERS2); @@ -3298,21 +3564,73 @@ void PeerManagerImpl::UpdatePeerStateForReceivedHeaders(CNode& pfrom, } void PeerManagerImpl::ProcessHeadersMessage(CNode& pfrom, Peer& peer, - const std::vector& headers, - bool via_compact_block) + std::vector&& headers, + bool via_compact_block, bool uses_compressed) { - const CNetMsgMaker msgMaker(pfrom.GetCommonVersion()); size_t nCount = headers.size(); if (nCount == 0) { // Nothing interesting. Stop asking this peers for more headers. + // If we were in the middle of headers sync, receiving an empty headers + // message suggests that the peer suddenly has nothing to give us + // (perhaps it reorged to our chain). Clear download state for this peer. + LOCK(peer.m_headers_sync_mutex); + if (peer.m_headers_sync) { + peer.m_headers_sync.reset(nullptr); + LOCK(m_headers_presync_mutex); + m_headers_presync_stats.erase(pfrom.GetId()); + } + return; + } + + // Before we do any processing, make sure these pass basic sanity checks. + // We'll rely on headers having valid proof-of-work further down, as an + // anti-DoS criteria (note: this check is required before passing any + // headers into HeadersSyncState). + if (!CheckHeadersPoW(headers, m_chainparams.GetConsensus(), pfrom)) { + // Misbehaving() calls are handled within CheckHeadersPoW(), so we can + // just return. (Note that even if a header is announced via compact + // block, the header itself should be valid, so this type of error can + // always be punished.) return; } const CBlockIndex *pindexLast = nullptr; + // We'll set already_validated_work to true if these headers are + // successfully processed as part of a low-work headers sync in progress + // (either in PRESYNC or REDOWNLOAD phase). + // If true, this will mean that any headers returned to us (ie during + // REDOWNLOAD) can be validated without further anti-DoS checks. + bool already_validated_work = false; + + // If we're in the middle of headers sync, let it do its magic. + bool have_headers_sync = false; + { + LOCK(peer.m_headers_sync_mutex); + + already_validated_work = IsContinuationOfLowWorkHeadersSync(peer, pfrom, headers, uses_compressed); + + // The headers we passed in may have been: + // - untouched, perhaps if no headers-sync was in progress, or some + // failure occurred + // - erased, such as if the headers were successfully processed and no + // additional headers processing needs to take place (such as if we + // are still in PRESYNC) + // - replaced with headers that are now ready for validation, such as + // during the REDOWNLOAD phase of a low-work headers sync. + // So just check whether we still have headers that we need to process, + // or not. + if (headers.empty()) { + return; + } + + have_headers_sync = !!peer.m_headers_sync; + } + // Do these headers connect to something in our block index? - bool headers_connect_blockindex{WITH_LOCK(::cs_main, return m_chainman.m_blockman.LookupBlockIndex(headers[0].hashPrevBlock) != nullptr)}; + const CBlockIndex *chain_start_header{WITH_LOCK(::cs_main, return m_chainman.m_blockman.LookupBlockIndex(headers[0].hashPrevBlock))}; + bool headers_connect_blockindex{chain_start_header != nullptr}; if (!headers_connect_blockindex) { if (nCount <= MAX_BLOCKS_TO_ANNOUNCE) { @@ -3326,30 +3644,52 @@ void PeerManagerImpl::ProcessHeadersMessage(CNode& pfrom, Peer& peer, return; } + // If the headers we received are already in memory and an ancestor of + // m_best_header or our tip, skip anti-DoS checks. These headers will not + // use any more memory (and we are not leaking information that could be + // used to fingerprint us). + const CBlockIndex *last_received_header{nullptr}; + { + LOCK(cs_main); + last_received_header = m_chainman.m_blockman.LookupBlockIndex(headers.back().GetHash()); + if (IsAncestorOfBestHeaderOrTip(last_received_header)) { + already_validated_work = true; + } + } + // At this point, the headers connect to something in our block index. - if (!CheckHeadersAreContinuous(headers)) { - Misbehaving(pfrom.GetId(), 20, "non-continuous headers sequence"); + // Do anti-DoS checks to determine if we should process or store for later + // processing. + if (!already_validated_work && TryLowWorkHeadersSync(peer, pfrom, + chain_start_header, headers, uses_compressed)) { + // If we successfully started a low-work headers sync, then there + // should be no headers to process any further. + Assume(headers.empty()); return; } + // At this point, we have a set of headers with sufficient work on them + // which can be processed. + // If we don't have the last header, then this peer will have given us // something new (if these headers are valid). - bool received_new_header{WITH_LOCK(::cs_main, return m_chainman.m_blockman.LookupBlockIndex(headers.back().GetHash()) == nullptr)}; + bool received_new_header{last_received_header != nullptr}; + // Now process all the headers. BlockValidationState state; - if (!m_chainman.ProcessNewBlockHeaders(headers, state, &pindexLast)) { + if (!m_chainman.ProcessNewBlockHeaders(headers, /*min_pow_checked=*/true, state, &pindexLast)) { if (state.IsInvalid()) { MaybePunishNodeForBlock(pfrom.GetId(), state, via_compact_block, "invalid header received"); return; } } + Assume(pindexLast); - // Consider fetching more headers. - const bool uses_compressed = UsesCompressedHeaders(peer); + // Consider fetching more headers if we are not using our headers-sync mechanism. const std::string msg_type = uses_compressed ? NetMsgType::GETHEADERS2 : NetMsgType::GETHEADERS; - if (nCount == GetHeadersLimit(pfrom, uses_compressed)) { + if (nCount == GetHeadersLimit(pfrom, uses_compressed) && !have_headers_sync) { // Headers message had its maximum size; the peer may have more headers. - if (MaybeSendGetHeaders(pfrom, msg_type, m_chainman.ActiveChain().GetLocator(pindexLast), peer)) { + if (MaybeSendGetHeaders(pfrom, msg_type, GetLocator(pindexLast), peer)) { LogPrint(BCLog::NET, "more %s (%d) to end to peer=%d (startheight:%d)\n", msg_type, pindexLast->nHeight, pfrom.GetId(), peer.m_starting_height); } @@ -3639,10 +3979,10 @@ std::pair static ValidateDSTX(CDeterministicMN return {true, false}; } -void PeerManagerImpl::ProcessBlock(CNode& node, const std::shared_ptr& block, bool force_processing) +void PeerManagerImpl::ProcessBlock(CNode& node, const std::shared_ptr& block, bool force_processing, bool min_pow_checked) { bool new_block{false}; - m_chainman.ProcessNewBlock(block, force_processing, &new_block); + m_chainman.ProcessNewBlock(block, force_processing, min_pow_checked, &new_block); if (new_block) { node.m_last_block_time = GetTime(); // In case this block came from a different peer than we requested @@ -4036,12 +4376,6 @@ void PeerManagerImpl::ProcessMessage( CMNAuth::PushMNAUTH(pfrom, m_connman, *m_active_ctx->nodeman); } - // Tell our peer we prefer to receive headers rather than inv's - // We send this to non-NODE NETWORK peers as well, because even - // non-NODE NETWORK peers can announce blocks (such as pruning - // nodes) - m_connman.PushMessage(&pfrom, msgMaker.Make(UsesCompressedHeaders(*peer) ? NetMsgType::SENDHEADERS2 : NetMsgType::SENDHEADERS)); - if (pfrom.CanRelay()) { // Tell our peer we are willing to provide version 1 cmpctblocks. // However, we do not request new block announcements using @@ -4408,7 +4742,7 @@ void PeerManagerImpl::ProcessMessage( CNodeState& state{*Assert(State(pfrom.GetId()))}; if (state.fSyncStarted || (!peer->m_inv_triggered_getheaders_before_sync && *best_block != m_last_block_inv_triggering_headers_sync)) { std::string msg_type = UsesCompressedHeaders(*peer) ? NetMsgType::GETHEADERS2 : NetMsgType::GETHEADERS; - if (MaybeSendGetHeaders(pfrom, msg_type, m_chainman.ActiveChain().GetLocator(m_chainman.m_best_header), *peer)) { + if (MaybeSendGetHeaders(pfrom, msg_type, GetLocator(m_chainman.m_best_header), *peer)) { LogPrint(BCLog::NET, "%s (%d) %s to peer=%d\n", msg_type, m_chainman.m_best_header->nHeight, best_block->ToString(), pfrom.GetId()); @@ -4848,13 +5182,18 @@ void PeerManagerImpl::ProcessMessage( { LOCK(cs_main); - if (!m_chainman.m_blockman.LookupBlockIndex(cmpctblock.header.hashPrevBlock)) { + const CBlockIndex* prev_block = m_chainman.m_blockman.LookupBlockIndex(cmpctblock.header.hashPrevBlock); + if (!prev_block) { // Doesn't connect (or is genesis), instead of DoSing in AcceptBlockHeader, request deeper headers if (!m_chainman.ActiveChainstate().IsInitialBlockDownload()) { std::string ret_val = UsesCompressedHeaders(*peer) ? NetMsgType::GETHEADERS2 : NetMsgType::GETHEADERS; - MaybeSendGetHeaders(pfrom, ret_val, m_chainman.ActiveChain().GetLocator(m_chainman.m_best_header), *peer); + MaybeSendGetHeaders(pfrom, ret_val, GetLocator(m_chainman.m_best_header), *peer); } return; + } else if (prev_block->nChainWork + CalculateHeadersWork({cmpctblock.header}) < GetAntiDoSWorkThreshold()) { + // If we get a low-work header in a compact block, we can ignore it. + LogPrint(BCLog::NET, "Ignoring low-work compact block from peer %d\n", pfrom.GetId()); + return; } if (!m_chainman.m_blockman.LookupBlockIndex(blockhash)) { @@ -4864,7 +5203,7 @@ void PeerManagerImpl::ProcessMessage( const CBlockIndex *pindex = nullptr; BlockValidationState state; - if (!m_chainman.ProcessNewBlockHeaders({cmpctblock.header}, state, &pindex)) { + if (!m_chainman.ProcessNewBlockHeaders({cmpctblock.header}, /*min_pow_checked=*/true, state, &pindex)) { if (state.IsInvalid()) { MaybePunishNodeForBlock(pfrom.GetId(), state, /*via_compact_block=*/true, "invalid header via cmpctblock"); return; @@ -5015,7 +5354,7 @@ void PeerManagerImpl::ProcessMessage( // the peer if the header turns out to be for an invalid block. // Note that if a peer tries to build on an invalid chain, that // will be detected and the peer will be disconnected/discouraged. - return ProcessHeadersMessage(pfrom, *peer, {cmpctblock.header}, /*via_compact_block=*/true); + return ProcessHeadersMessage(pfrom, *peer, {cmpctblock.header}, /*via_compact_block=*/true, UsesCompressedHeaders(*peer)); } if (fBlockReconstructed) { @@ -5034,7 +5373,7 @@ void PeerManagerImpl::ProcessMessage( // we have a chain with at least nMinimumChainWork), and we ignore // compact blocks with less work than our tip, it is safe to treat // reconstructed compact blocks as having been requested. - ProcessBlock(pfrom, pblock, /*force_processing=*/true); + ProcessBlock(pfrom, pblock, /*force_processing=*/true, /*min_pow_checked=*/true); LOCK(cs_main); // hold cs_main for CBlockIndex::IsValid() if (pindex->IsValid(BLOCK_VALID_TRANSACTIONS)) { // Clear download state for this block, which is in @@ -5117,7 +5456,7 @@ void PeerManagerImpl::ProcessMessage( // disk-space attacks), but this should be safe due to the // protections in the compact block handler -- see related comment // in compact block optimistic reconstruction handling. - ProcessBlock(pfrom, pblock, /*force_processing=*/true); + ProcessBlock(pfrom, pblock, /*force_processing=*/true, /*min_pow_checked=*/true); } return; } @@ -5158,7 +5497,23 @@ void PeerManagerImpl::ProcessMessage( } } - return ProcessHeadersMessage(pfrom, *peer, headers, /*via_compact_block=*/false); + ProcessHeadersMessage(pfrom, *peer, std::move(headers), /*via_compact_block=*/false, msg_type == NetMsgType::HEADERS2); + + // Check if the headers presync progress needs to be reported to validation. + // This needs to be done without holding the m_headers_presync_mutex lock. + if (m_headers_presync_should_signal.exchange(false)) { + HeadersPresyncStats stats; + { + LOCK(m_headers_presync_mutex); + auto it = m_headers_presync_stats.find(m_headers_presync_bestpeer); + if (it != m_headers_presync_stats.end()) stats = it->second; + } + if (stats.second) { + m_chainman.ReportHeadersPresync(stats.first, stats.second->first, stats.second->second); + } + } + + return; } if (msg_type == NetMsgType::BLOCK) @@ -5176,6 +5531,7 @@ void PeerManagerImpl::ProcessMessage( bool forceProcessing = false; const uint256 hash(pblock->GetHash()); + bool min_pow_checked = false; { LOCK(cs_main); // Always process the block if we requested it, since we may @@ -5186,8 +5542,14 @@ void PeerManagerImpl::ProcessMessage( // which peers send us compact blocks, so the race between here and // cs_main in ProcessNewBlock is fine. mapBlockSource.emplace(hash, std::make_pair(pfrom.GetId(), true)); + + // Check work on this block against our anti-dos thresholds. + const CBlockIndex* prev_block = m_chainman.m_blockman.LookupBlockIndex(pblock->hashPrevBlock); + if (prev_block && prev_block->nChainWork + CalculateHeadersWork({pblock->GetBlockHeader()}) >= GetAntiDoSWorkThreshold()) { + min_pow_checked = true; + } } - ProcessBlock(pfrom, pblock, forceProcessing); + ProcessBlock(pfrom, pblock, forceProcessing, min_pow_checked); return; } @@ -5713,7 +6075,7 @@ void PeerManagerImpl::ConsiderEviction(CNode& pto, Peer& peer, std::chrono::seco // still respond to us with a sufficiently high work chain tip. std::string msg_type = UsesCompressedHeaders(peer) ? NetMsgType::GETHEADERS2 : NetMsgType::GETHEADERS; MaybeSendGetHeaders(pto, - msg_type, m_chainman.ActiveChain().GetLocator(state.m_chain_sync.m_work_header->pprev), + msg_type, GetLocator(state.m_chain_sync.m_work_header->pprev), peer); LogPrint(BCLog::NET, "sending %s to outbound peer=%d to verify chain work (current best known block:%s, benchmark blockhash: %s)\n", msg_type, pto.GetId(), state.pindexBestKnownBlock != nullptr ? state.pindexBestKnownBlock->GetBlockHash().ToString() : "", state.m_chain_sync.m_work_header->GetBlockHash().ToString()); state.m_chain_sync.m_sent_getheaders = true; @@ -5975,6 +6337,28 @@ void PeerManagerImpl::MaybeSendAddr(CNode& node, Peer& peer, std::chrono::micros } } +void PeerManagerImpl::MaybeSendSendHeaders(CNode& node, Peer& peer) +{ + // Delay sending SENDHEADERS (BIP 130) until we're done with an + // initial-headers-sync with this peer. Receiving headers announcements for + // new blocks while trying to sync their headers chain is problematic, + // because of the state tracking done. + if (!peer.m_sent_sendheaders) { + LOCK(cs_main); + CNodeState &state = *State(node.GetId()); + if (state.pindexBestKnownBlock != nullptr && + state.pindexBestKnownBlock->nChainWork > nMinimumChainWork) { + // Tell our peer we prefer to receive headers rather than inv's + // We send this to non-NODE NETWORK peers as well, because even + // non-NODE NETWORK peers can announce blocks (such as pruning + // nodes) + m_connman.PushMessage(&node, CNetMsgMaker(node.GetCommonVersion()).Make(UsesCompressedHeaders(peer) ? NetMsgType::SENDHEADERS2 : NetMsgType::SENDHEADERS)); + + peer.m_sent_sendheaders = true; + } + } +} + namespace { class CompareInvMempoolOrder { @@ -6059,6 +6443,8 @@ bool PeerManagerImpl::SendMessages(CNode* pto) MaybeSendAddr(*pto, *peer, current_time); + MaybeSendSendHeaders(*pto, *peer); + { LOCK(cs_main); @@ -6104,7 +6490,7 @@ bool PeerManagerImpl::SendMessages(CNode* pto) if (pindexStart->pprev) pindexStart = pindexStart->pprev; std::string msg_type = UsesCompressedHeaders(*peer) ? NetMsgType::GETHEADERS2 : NetMsgType::GETHEADERS; - if (MaybeSendGetHeaders(*pto, msg_type, m_chainman.ActiveChain().GetLocator(pindexStart), *peer)) { + if (MaybeSendGetHeaders(*pto, msg_type, GetLocator(pindexStart), *peer)) { LogPrint(BCLog::NET, "initial %s (%d) to peer=%d (startheight:%d)\n", msg_type, pindexStart->nHeight, pto->GetId(), peer->m_starting_height); state.fSyncStarted = true; diff --git a/src/net_processing.h b/src/net_processing.h index 4967444fe346..f85d05332e89 100644 --- a/src/net_processing.h +++ b/src/net_processing.h @@ -60,6 +60,7 @@ struct CNodeStateStats { uint64_t m_addr_rate_limited = 0; bool m_addr_relay_enabled{false}; ServiceFlags their_services; + int64_t presync_height{-1}; }; class PeerManagerInternal diff --git a/src/node/interface_ui.cpp b/src/node/interface_ui.cpp index 6fa66ba5f770..4f475fc03169 100644 --- a/src/node/interface_ui.cpp +++ b/src/node/interface_ui.cpp @@ -64,7 +64,7 @@ void CClientUIInterface::NotifyAlertChanged() { return g_ui_signals.NotifyAlertC void CClientUIInterface::ShowProgress(const std::string& title, int nProgress, bool resume_possible) { return g_ui_signals.ShowProgress(title, nProgress, resume_possible); } void CClientUIInterface::NotifyBlockTip(SynchronizationState s, const CBlockIndex* i) { return g_ui_signals.NotifyBlockTip(s, i); } void CClientUIInterface::NotifyChainLock(const std::string& bestChainLockHash, int bestChainLockHeight) { return g_ui_signals.NotifyChainLock(bestChainLockHash, bestChainLockHeight); } -void CClientUIInterface::NotifyHeaderTip(SynchronizationState s, const CBlockIndex* i) { return g_ui_signals.NotifyHeaderTip(s, i); } +void CClientUIInterface::NotifyHeaderTip(SynchronizationState s, int64_t height, int64_t timestamp, bool presync) { return g_ui_signals.NotifyHeaderTip(s, height, timestamp, presync); } void CClientUIInterface::NotifyGovernanceChanged() { return g_ui_signals.NotifyGovernanceChanged(); } void CClientUIInterface::NotifyInstantSendChanged() { return g_ui_signals.NotifyInstantSendChanged(); } void CClientUIInterface::NotifyMasternodeListChanged(const CDeterministicMNList& list, const CBlockIndex* i) { return g_ui_signals.NotifyMasternodeListChanged(list, i); } diff --git a/src/node/interface_ui.h b/src/node/interface_ui.h index 67a6d0f5b1b0..7cc40a7b0255 100644 --- a/src/node/interface_ui.h +++ b/src/node/interface_ui.h @@ -110,7 +110,7 @@ class CClientUIInterface ADD_SIGNALS_DECL_WRAPPER(NotifyChainLock, void, const std::string& bestChainLockHash, int bestChainLockHeight); /** Best header has changed */ - ADD_SIGNALS_DECL_WRAPPER(NotifyHeaderTip, void, SynchronizationState, const CBlockIndex*); + ADD_SIGNALS_DECL_WRAPPER(NotifyHeaderTip, void, SynchronizationState, int64_t height, int64_t timestamp, bool presync); /** Masternode list has changed */ ADD_SIGNALS_DECL_WRAPPER(NotifyMasternodeListChanged, void, const CDeterministicMNList&, const CBlockIndex*); diff --git a/src/node/interfaces.cpp b/src/node/interfaces.cpp index f3eac13383ad..3e6bf2c1e3d3 100644 --- a/src/node/interfaces.cpp +++ b/src/node/interfaces.cpp @@ -1006,9 +1006,8 @@ class NodeImpl : public Node std::unique_ptr handleNotifyHeaderTip(NotifyHeaderTipFn fn) override { return MakeHandler( - ::uiInterface.NotifyHeaderTip_connect([fn](SynchronizationState sync_state, const CBlockIndex* block) { - fn(sync_state, BlockTip{block->nHeight, block->GetBlockTime(), block->GetBlockHash()}, - /* verification progress is unused when a header was received */ 0); + ::uiInterface.NotifyHeaderTip_connect([fn](SynchronizationState sync_state, int64_t height, int64_t timestamp, bool presync) { + fn(sync_state, BlockTip{(int)height, timestamp, uint256{}}, presync); })); } std::unique_ptr handleNotifyInstantSendChanged(NotifyInstantSendChangedFn fn) override @@ -1217,8 +1216,7 @@ class ChainImpl : public Chain { LOCK(::cs_main); const CBlockIndex* index = chainman().m_blockman.LookupBlockIndex(block_hash); - if (!index) return {}; - return chainman().ActiveChain().GetLocator(index); + return GetLocator(index); } std::optional findLocatorFork(const CBlockLocator& locator) override { diff --git a/src/pow.cpp b/src/pow.cpp index ed014aeba7ae..f446b1b4203e 100644 --- a/src/pow.cpp +++ b/src/pow.cpp @@ -233,6 +233,75 @@ unsigned int CalculateNextWorkRequired(const CBlockIndex* pindexLast, int64_t nF return bnNew.GetCompact(); } +// Anti-DoS heuristic for HeadersSyncState (not consensus): bound the per-pair +// nBits ratio so an attacker can't ramp claimed difficulty arbitrarily fast +// during PRESYNC/REDOWNLOAD. Only meaningful for fixed-interval retargeting, +// where consecutive blocks within an interval have unchanged nBits and only +// retarget boundaries can move; per-block retargeting algorithms (KGW, DGW) +// have no useful per-pair upper bound (~7x and ~12x respectively in the +// adversarial-but-still-MTP-legal worst case), so a tight K causes false- +// rejects on legitimate volatile-hashrate chains while a loose K provides no +// extra protection beyond the surrounding layered defenses: +// - m_minimum_required_work threshold gates PRESYNC -> REDOWNLOAD; +// - m_max_commitments / REDOWNLOAD_BUFFER_SIZE bound transient memory; +// - ContextualCheckBlockHeader's exact-match nBits == GetNextWorkRequired +// check (src/validation.cpp) gates AddToBlockIndex (= disk). +// The latter two are unconditional; this heuristic only sharpens attacker +// chain length where it can be tightly bounded. +// +// Dash regimes by height: +// * height < nPowKGWHeight : Bitcoin-style retargeting. Consensus clamps +// nActualTimespan to [target/4, target*4] in CalculateNextWorkRequired, +// so 4x is the provable per-retarget upper bound and never false-rejects. +// * height >= nPowKGWHeight : KGW, then DGW v3 from nPowDGWHeight. Skip. +bool PermittedDifficultyTransition(const Consensus::Params& params, int64_t height, uint32_t old_nbits, uint32_t new_nbits) +{ + if (params.fPowAllowMinDifficultyBlocks) return true; + + // Per-block retargeting (KGW + DGW): no useful per-pair bound. + if (height >= params.nPowKGWHeight) return true; + + int64_t smallest_timespan = params.nPowTargetTimespan/4; + int64_t largest_timespan = params.nPowTargetTimespan*4; + + const arith_uint256 pow_limit = UintToArith256(params.powLimit); + arith_uint256 observed_new_target; + observed_new_target.SetCompact(new_nbits); + + // Calculate the largest difficulty value possible: + arith_uint256 largest_difficulty_target; + largest_difficulty_target.SetCompact(old_nbits); + largest_difficulty_target *= largest_timespan; + largest_difficulty_target /= params.nPowTargetTimespan; + + if (largest_difficulty_target > pow_limit) { + largest_difficulty_target = pow_limit; + } + + // Round and then compare this new calculated value to what is + // observed. + arith_uint256 maximum_new_target; + maximum_new_target.SetCompact(largest_difficulty_target.GetCompact()); + if (maximum_new_target < observed_new_target) return false; + + // Calculate the smallest difficulty value possible: + arith_uint256 smallest_difficulty_target; + smallest_difficulty_target.SetCompact(old_nbits); + smallest_difficulty_target *= smallest_timespan; + smallest_difficulty_target /= params.nPowTargetTimespan; + + if (smallest_difficulty_target > pow_limit) { + smallest_difficulty_target = pow_limit; + } + + // Round and then compare this new calculated value to what is + // observed. + arith_uint256 minimum_new_target; + minimum_new_target.SetCompact(smallest_difficulty_target.GetCompact()); + if (minimum_new_target > observed_new_target) return false; + return true; +} + bool CheckProofOfWork(uint256 hash, unsigned int nBits, const Consensus::Params& params) { bool fNegative; diff --git a/src/pow.h b/src/pow.h index fe18ec8bb3fd..648a3f4bb8ae 100644 --- a/src/pow.h +++ b/src/pow.h @@ -21,4 +21,18 @@ unsigned int CalculateNextWorkRequired(const CBlockIndex* pindexLast, int64_t nF /** Check whether a block hash satisfies the proof-of-work requirement specified by nBits */ bool CheckProofOfWork(uint256 hash, unsigned int nBits, const Consensus::Params&); +/** + * Return false if the proof-of-work requirement specified by new_nbits at a + * given height is not possible, given the proof-of-work on the prior block as + * specified by old_nbits. + * + * This function only checks that the new value is within a factor of 4 of the + * old value for blocks at the difficulty adjustment interval, and otherwise + * requires the values to be the same. + * + * Always returns true on networks where min difficulty blocks are allowed, + * such as regtest/testnet. + */ +bool PermittedDifficultyTransition(const Consensus::Params& params, int64_t height, uint32_t old_nbits, uint32_t new_nbits); + #endif // BITCOIN_POW_H diff --git a/src/qt/bitcoin.cpp b/src/qt/bitcoin.cpp index 08132c0a3848..0b104a6d2661 100644 --- a/src/qt/bitcoin.cpp +++ b/src/qt/bitcoin.cpp @@ -79,6 +79,7 @@ Q_IMPORT_PLUGIN(QAndroidPlatformIntegrationPlugin) Q_DECLARE_METATYPE(bool*) Q_DECLARE_METATYPE(CAmount) Q_DECLARE_METATYPE(SynchronizationState) +Q_DECLARE_METATYPE(SyncType) Q_DECLARE_METATYPE(uint256) static void RegisterMetaTypes() @@ -86,6 +87,7 @@ static void RegisterMetaTypes() // Register meta types used for QMetaObject::invokeMethod and Qt::QueuedConnection qRegisterMetaType(); qRegisterMetaType(); + qRegisterMetaType(); #ifdef ENABLE_WALLET qRegisterMetaType(); #endif diff --git a/src/qt/bitcoingui.cpp b/src/qt/bitcoingui.cpp index 8f6800b482ec..0db7c7e392b3 100644 --- a/src/qt/bitcoingui.cpp +++ b/src/qt/bitcoingui.cpp @@ -893,8 +893,8 @@ void BitcoinGUI::setClientModel(ClientModel *_clientModel, interfaces::BlockAndH connect(_clientModel, &ClientModel::numConnectionsChanged, this, &BitcoinGUI::setNumConnections); connect(_clientModel, &ClientModel::networkActiveChanged, this, &BitcoinGUI::setNetworkActive); - modalOverlay->setKnownBestHeight(tip_info->header_height, QDateTime::fromSecsSinceEpoch(tip_info->header_time)); - setNumBlocks(tip_info->block_height, QDateTime::fromSecsSinceEpoch(tip_info->block_time), QString::fromStdString(tip_info->block_hash.ToString()), tip_info->verification_progress, false, SynchronizationState::INIT_DOWNLOAD); + modalOverlay->setKnownBestHeight(tip_info->header_height, QDateTime::fromSecsSinceEpoch(tip_info->header_time), /*presync=*/false); + setNumBlocks(tip_info->block_height, QDateTime::fromSecsSinceEpoch(tip_info->block_time), QString::fromStdString(tip_info->block_hash.ToString()), tip_info->verification_progress, SyncType::BLOCK_SYNC, SynchronizationState::INIT_DOWNLOAD); connect(_clientModel, &ClientModel::numBlocksChanged, this, &BitcoinGUI::setNumBlocks); connect(_clientModel, &ClientModel::additionalDataSyncProgressChanged, this, &BitcoinGUI::setAdditionalDataSyncProgress); @@ -1410,7 +1410,7 @@ void BitcoinGUI::updateNetworkState() } if (fNetworkBecameActive || fNetworkBecameInactive) { - setNumBlocks(m_node.getNumBlocks(), QDateTime::fromSecsSinceEpoch(m_node.getLastBlockTime()), QString::fromStdString(m_node.getLastBlockHash()), m_node.getVerificationProgress(), false, SynchronizationState::INIT_DOWNLOAD); + setNumBlocks(m_node.getNumBlocks(), QDateTime::fromSecsSinceEpoch(m_node.getLastBlockTime()), QString::fromStdString(m_node.getLastBlockHash()), m_node.getVerificationProgress(), SyncType::BLOCK_SYNC, SynchronizationState::INIT_DOWNLOAD); } nCountPrev = count; @@ -1476,6 +1476,13 @@ void BitcoinGUI::updateHeadersSyncProgressLabel() progressBarLabel->setText(tr("Syncing Headers (%1%)…").arg(QString::number(100.0 / (headersTipHeight+estHeadersLeft)*headersTipHeight, 'f', 1))); } +void BitcoinGUI::updateHeadersPresyncProgressLabel(int64_t height, const QDateTime& blockDate) +{ + int estHeadersLeft = blockDate.secsTo(QDateTime::currentDateTime()) / Params().GetConsensus().nPowTargetSpacing; + if (estHeadersLeft > HEADER_HEIGHT_DELTA_SYNC) + progressBarLabel->setText(tr("Pre-syncing Headers (%1%)…").arg(QString::number(100.0 / (height+estHeadersLeft)*height, 'f', 1))); +} + void BitcoinGUI::openOptionsDialogWithTab(OptionsDialog::Tab tab) { if (!clientModel || !clientModel->getOptionsModel()) @@ -1600,7 +1607,7 @@ void BitcoinGUI::updateWidth() resize(std::max(width(), nWidth), height()); } -void BitcoinGUI::setNumBlocks(int count, const QDateTime& blockDate, const QString& blockHash, double nVerificationProgress, bool header, SynchronizationState sync_state) +void BitcoinGUI::setNumBlocks(int count, const QDateTime& blockDate, const QString& blockHash, double nVerificationProgress, SyncType synctype, SynchronizationState sync_state) { #ifdef Q_OS_MACOS // Disabling macOS App Nap on initial sync, disk, reindex operations and mixing. @@ -1621,8 +1628,8 @@ void BitcoinGUI::setNumBlocks(int count, const QDateTime& blockDate, const QStri if (modalOverlay) { - if (header) - modalOverlay->setKnownBestHeight(count, blockDate); + if (synctype != SyncType::BLOCK_SYNC) + modalOverlay->setKnownBestHeight(count, blockDate, synctype == SyncType::HEADER_PRESYNC); else modalOverlay->tipUpdate(count, blockDate, nVerificationProgress); } @@ -1639,7 +1646,10 @@ void BitcoinGUI::setNumBlocks(int count, const QDateTime& blockDate, const QStri BlockSource blockSource{clientModel->getBlockSource()}; switch (blockSource) { case BlockSource::NETWORK: - if (header) { + if (synctype == SyncType::HEADER_PRESYNC) { + updateHeadersPresyncProgressLabel(count, blockDate); + return; + } else if (synctype == SyncType::HEADER_SYNC) { updateHeadersSyncProgressLabel(); return; } @@ -1647,14 +1657,14 @@ void BitcoinGUI::setNumBlocks(int count, const QDateTime& blockDate, const QStri updateHeadersSyncProgressLabel(); break; case BlockSource::DISK: - if (header) { + if (synctype != SyncType::BLOCK_SYNC) { progressBarLabel->setText(tr("Indexing blocks on disk…")); } else { progressBarLabel->setText(tr("Processing blocks on disk…")); } break; case BlockSource::NONE: - if (header) { + if (synctype != SyncType::BLOCK_SYNC) { return; } progressBarLabel->setText(tr("Connecting to peers…")); @@ -1721,7 +1731,7 @@ void BitcoinGUI::setAdditionalDataSyncProgress(double nSyncProgress) // If masternodeSync->Reset() has been called make sure status bar shows the correct information. if (nSyncProgress == -1) { - setNumBlocks(m_node.getNumBlocks(), QDateTime::fromSecsSinceEpoch(m_node.getLastBlockTime()), QString::fromStdString(m_node.getLastBlockHash()), m_node.getVerificationProgress(), false, SynchronizationState::INIT_DOWNLOAD); + setNumBlocks(m_node.getNumBlocks(), QDateTime::fromSecsSinceEpoch(m_node.getLastBlockTime()), QString::fromStdString(m_node.getLastBlockHash()), m_node.getVerificationProgress(), SyncType::BLOCK_SYNC, SynchronizationState::INIT_DOWNLOAD); if (clientModel->getNumConnections()) { labelBlocksIcon->show(); startSpinner(); diff --git a/src/qt/bitcoingui.h b/src/qt/bitcoingui.h index 670ce48f084b..01bdd0f5caa6 100644 --- a/src/qt/bitcoingui.h +++ b/src/qt/bitcoingui.h @@ -10,6 +10,7 @@ #endif #include +#include #include #include @@ -30,7 +31,6 @@ #include #endif -class ClientModel; class NetworkStyle; class Notificator; class OptionsModel; @@ -275,6 +275,7 @@ class BitcoinGUI : public QMainWindow void stopGovernanceSyncAnimation(); void updateHeadersSyncProgressLabel(); + void updateHeadersPresyncProgressLabel(int64_t height, const QDateTime& blockDate); void updateProgressBarVisibility(); @@ -299,7 +300,7 @@ public Q_SLOTS: /** Get restart command-line parameters and request restart */ void handleRestart(QStringList args); /** Set number of blocks and last block date shown in the UI */ - void setNumBlocks(int count, const QDateTime& blockDate, const QString& blockHash, double nVerificationProgress, bool headers, SynchronizationState sync_state); + void setNumBlocks(int count, const QDateTime& blockDate, const QString& blockHash, double nVerificationProgress, SyncType synctype, SynchronizationState sync_state); /** Set additional data sync status shown in the UI */ void setAdditionalDataSyncProgress(double nSyncProgress); diff --git a/src/qt/clientmodel.cpp b/src/qt/clientmodel.cpp index 8398921dde33..eb009a0aff7a 100644 --- a/src/qt/clientmodel.cpp +++ b/src/qt/clientmodel.cpp @@ -89,8 +89,8 @@ ClientModel::ClientModel(interfaces::Node& node, OptionsModel *_optionsModel, QO // Update sync state to decide delay param, trigger refreshes connect(this, &ClientModel::numBlocksChanged, this, - [this](int, const QDateTime&, const QString&, double, bool header, SynchronizationState sync_state) { - if (header) return; + [this](int, const QDateTime&, const QString&, double, SyncType synctype, SynchronizationState sync_state) { + if (synctype != SyncType::BLOCK_SYNC) return; m_feeds->setSyncing(sync_state != SynchronizationState::POST_INIT); if (m_feed_creditpool) m_feed_creditpool->requestRefresh(); if (m_feed_masternode) m_feed_masternode->requestRefresh(); @@ -262,26 +262,26 @@ QString ClientModel::blocksDir() const return GUIUtil::PathToQString(gArgs.GetBlocksDirPath()); } -void ClientModel::TipChanged(SynchronizationState sync_state, interfaces::BlockTip tip, double verification_progress, bool header) +void ClientModel::TipChanged(SynchronizationState sync_state, interfaces::BlockTip tip, double verification_progress, SyncType synctype) { - if (header) { + if (synctype == SyncType::HEADER_SYNC) { // cache best headers time and height to reduce future cs_main locks cachedBestHeaderHeight = tip.block_height; cachedBestHeaderTime = tip.block_time; - } else { + } else if (synctype == SyncType::BLOCK_SYNC) { m_cached_num_blocks = tip.block_height; WITH_LOCK(m_cached_tip_mutex, m_cached_tip_blocks = tip.block_hash;); } // Throttle GUI notifications about (a) blocks during initial sync, and (b) both blocks and headers during reindex. - const bool throttle = (sync_state != SynchronizationState::POST_INIT && !header) || sync_state == SynchronizationState::INIT_REINDEX; + const bool throttle = (sync_state != SynchronizationState::POST_INIT && synctype == SyncType::BLOCK_SYNC) || sync_state == SynchronizationState::INIT_REINDEX; const auto now{throttle ? SteadyClock::now() : SteadyClock::time_point{}}; - auto& nLastUpdateNotification = header ? g_last_header_tip_update_notification : g_last_block_tip_update_notification; + auto& nLastUpdateNotification = synctype != SyncType::BLOCK_SYNC ? g_last_header_tip_update_notification : g_last_block_tip_update_notification; if (throttle && now < nLastUpdateNotification + MODEL_UPDATE_DELAY) { return; } - Q_EMIT numBlocksChanged(tip.block_height, QDateTime::fromSecsSinceEpoch(tip.block_time), QString::fromStdString(tip.block_hash.ToString()), verification_progress, header, sync_state); + Q_EMIT numBlocksChanged(tip.block_height, QDateTime::fromSecsSinceEpoch(tip.block_time), QString::fromStdString(tip.block_hash.ToString()), verification_progress, synctype, sync_state); nLastUpdateNotification = now; } @@ -311,11 +311,11 @@ void ClientModel::subscribeToCoreSignals() })); m_event_handlers.emplace_back(m_node.handleNotifyBlockTip( [this](SynchronizationState sync_state, interfaces::BlockTip tip, double verification_progress) { - TipChanged(sync_state, tip, verification_progress, /*header=*/false); + TipChanged(sync_state, tip, verification_progress, SyncType::BLOCK_SYNC); })); m_event_handlers.emplace_back(m_node.handleNotifyHeaderTip( - [this](SynchronizationState sync_state, interfaces::BlockTip tip, double verification_progress) { - TipChanged(sync_state, tip, verification_progress, /*header=*/true); + [this](SynchronizationState sync_state, interfaces::BlockTip tip, bool presync) { + TipChanged(sync_state, tip, /*verification_progress=*/0.0, presync ? SyncType::HEADER_PRESYNC : SyncType::HEADER_SYNC); })); m_event_handlers.emplace_back(m_node.handleNotifyAdditionalDataSyncProgressChanged( [this](double nSyncProgress) { diff --git a/src/qt/clientmodel.h b/src/qt/clientmodel.h index 72ff9a7b3d80..0d5cfee5b532 100644 --- a/src/qt/clientmodel.h +++ b/src/qt/clientmodel.h @@ -40,6 +40,12 @@ enum class BlockSource { NETWORK, }; +enum class SyncType { + HEADER_PRESYNC, + HEADER_SYNC, + BLOCK_SYNC +}; + enum NumConnections { CONNECTIONS_NONE = 0, CONNECTIONS_IN = (1U << 0), @@ -127,7 +133,7 @@ class ClientModel : public QObject QuorumFeed* m_feed_quorum{nullptr}; std::unique_ptr m_feeds{nullptr}; - void TipChanged(SynchronizationState sync_state, interfaces::BlockTip tip, double verification_progress, bool header) EXCLUSIVE_LOCKS_REQUIRED(!m_cached_tip_mutex); + void TipChanged(SynchronizationState sync_state, interfaces::BlockTip tip, double verification_progress, SyncType synctype) EXCLUSIVE_LOCKS_REQUIRED(!m_cached_tip_mutex); void subscribeToCoreSignals(); void unsubscribeFromCoreSignals(); @@ -136,7 +142,7 @@ class ClientModel : public QObject void governanceChanged(); void masternodeListChanged() const; void chainLockChanged(); - void numBlocksChanged(int count, const QDateTime& blockDate, const QString& blockHash, double nVerificationProgress, bool header, SynchronizationState sync_state); + void numBlocksChanged(int count, const QDateTime& blockDate, const QString& blockHash, double nVerificationProgress, SyncType header, SynchronizationState sync_state); void additionalDataSyncProgressChanged(double nSyncProgress); void mempoolSizeChanged(long count, size_t mempoolSizeInBytes, size_t mempoolMaxSizeInBytes); void instantSendChanged(); diff --git a/src/qt/informationwidget.cpp b/src/qt/informationwidget.cpp index d0c9e9bc9ece..c15fe03c097f 100644 --- a/src/qt/informationwidget.cpp +++ b/src/qt/informationwidget.cpp @@ -107,9 +107,9 @@ void InformationWidget::setNetworkActive(bool networkActive) updateNetworkState(); } -void InformationWidget::setNumBlocks(int count, const QDateTime& blockDate, const QString& blockHash, double nVerificationProgress, bool headers) +void InformationWidget::setNumBlocks(int count, const QDateTime& blockDate, const QString& blockHash, double nVerificationProgress, SyncType synctype, SynchronizationState sync_state) { - if (!headers) { + if (synctype == SyncType::BLOCK_SYNC) { ui->numberOfBlocks->setText(QString::number(count)); ui->lastBlockTime->setText(blockDate.toString()); ui->lastBlockHash->setText(blockHash); diff --git a/src/qt/informationwidget.h b/src/qt/informationwidget.h index 88eae235a9fd..d35524d9d222 100644 --- a/src/qt/informationwidget.h +++ b/src/qt/informationwidget.h @@ -13,6 +13,9 @@ namespace Ui { class InformationWidget; } // namespace Ui +enum class SyncType; +enum class SynchronizationState; + class InformationWidget : public QWidget { Q_OBJECT @@ -29,7 +32,7 @@ public Q_SLOTS: /** Set network state shown in the UI */ void setNetworkActive(bool networkActive); /** Set number of blocks, last block date and last block hash shown in the UI */ - void setNumBlocks(int count, const QDateTime& blockDate, const QString& blockHash, double nVerificationProgress, bool headers); + void setNumBlocks(int count, const QDateTime& blockDate, const QString& blockHash, double nVerificationProgress, SyncType synctype, SynchronizationState sync_state); /** Set size (number of transactions and memory usage) of the mempool in the UI */ void setMempoolSize(long numberOfTxs, size_t dynUsage, size_t maxUsage); diff --git a/src/qt/modaloverlay.cpp b/src/qt/modaloverlay.cpp index 154cb60f4cee..1844629a0555 100644 --- a/src/qt/modaloverlay.cpp +++ b/src/qt/modaloverlay.cpp @@ -94,13 +94,16 @@ bool ModalOverlay::event(QEvent* ev) { return QWidget::event(ev); } -void ModalOverlay::setKnownBestHeight(int count, const QDateTime& blockDate) +void ModalOverlay::setKnownBestHeight(int count, const QDateTime& blockDate, bool presync) { - if (count > bestHeaderHeight) { + if (!presync && count > bestHeaderHeight) { bestHeaderHeight = count; bestHeaderDate = blockDate; UpdateHeaderSyncLabel(); } + if (presync) { + UpdateHeaderPresyncLabel(count, blockDate); + } } void ModalOverlay::tipUpdate(int count, const QDateTime& blockDate, double nVerificationProgress) @@ -174,6 +177,11 @@ void ModalOverlay::UpdateHeaderSyncLabel() { ui->numberOfBlocksLeft->setText(tr("Unknown. Syncing Headers (%1, %2%)…").arg(bestHeaderHeight).arg(QString::number(100.0 / (bestHeaderHeight + est_headers_left) * bestHeaderHeight, 'f', 1))); } +void ModalOverlay::UpdateHeaderPresyncLabel(int height, const QDateTime& blockDate) { + int est_headers_left = blockDate.secsTo(QDateTime::currentDateTime()) / Params().GetConsensus().nPowTargetSpacing; + ui->numberOfBlocksLeft->setText(tr("Unknown. Pre-syncing Headers (%1, %2%)…").arg(height).arg(QString::number(100.0 / (height + est_headers_left) * height, 'f', 1))); +} + void ModalOverlay::toggleVisibility() { showHide(layerIsVisible, true); diff --git a/src/qt/modaloverlay.h b/src/qt/modaloverlay.h index aed94f140cc8..7107a3eebdd5 100644 --- a/src/qt/modaloverlay.h +++ b/src/qt/modaloverlay.h @@ -26,7 +26,7 @@ class ModalOverlay : public QWidget ~ModalOverlay(); void tipUpdate(int count, const QDateTime& blockDate, double nVerificationProgress); - void setKnownBestHeight(int count, const QDateTime& blockDate); + void setKnownBestHeight(int count, const QDateTime& blockDate, bool presync); // will show or hide the modal layer void showHide(bool hide = false, bool userRequested = false); @@ -51,6 +51,7 @@ public Q_SLOTS: bool foreverHidden{false}; QPropertyAnimation m_animation; void UpdateHeaderSyncLabel(); + void UpdateHeaderPresyncLabel(int height, const QDateTime& blockDate); }; #endif // BITCOIN_QT_MODALOVERLAY_H diff --git a/src/qt/rpcconsole.cpp b/src/qt/rpcconsole.cpp index 1c5596baafb7..17975b20a986 100644 --- a/src/qt/rpcconsole.cpp +++ b/src/qt/rpcconsole.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -783,7 +784,7 @@ void RPCConsole::setClientModel(ClientModel *model, int bestblock_height, int64_ // Provide initial values ui->informationWidget->setNumBlocks(/*count=*/bestblock_height, QDateTime::fromSecsSinceEpoch(bestblock_date), QString::fromStdString(bestblock_hash.ToString()), - verification_progress, /*headers=*/false); + verification_progress, SyncType::BLOCK_SYNC, SynchronizationState::INIT_DOWNLOAD); //Setup autocomplete and attach it QStringList wordList; diff --git a/src/qt/rpcconsole.h b/src/qt/rpcconsole.h index c978d1af14c6..770707e21636 100644 --- a/src/qt/rpcconsole.h +++ b/src/qt/rpcconsole.h @@ -9,6 +9,7 @@ #include #endif +#include #include #include #include @@ -21,7 +22,6 @@ #include #include -class ClientModel; class MasternodeFeed; class RPCExecutor; class RPCTimerInterface; diff --git a/src/qt/sendcoinsdialog.cpp b/src/qt/sendcoinsdialog.cpp index c09783e2ab08..3d71085fdeea 100644 --- a/src/qt/sendcoinsdialog.cpp +++ b/src/qt/sendcoinsdialog.cpp @@ -951,7 +951,7 @@ void SendCoinsDialog::updateCoinControlState() m_coin_control->fAllowWatchOnly = model->wallet().privateKeysDisabled() && !model->wallet().hasExternalSigner(); } -void SendCoinsDialog::updateNumberOfBlocks(int count, const QDateTime& blockDate, const QString& blockHash, double nVerificationProgress, bool header, SynchronizationState sync_state) { +void SendCoinsDialog::updateNumberOfBlocks(int count, const QDateTime& blockDate, const QString& blockHash, double nVerificationProgress, SyncType synctype, SynchronizationState sync_state) { if (sync_state == SynchronizationState::POST_INIT) { updateSmartFeeLabel(); } diff --git a/src/qt/sendcoinsdialog.h b/src/qt/sendcoinsdialog.h index 90e1eff6ce6e..d691f1555195 100644 --- a/src/qt/sendcoinsdialog.h +++ b/src/qt/sendcoinsdialog.h @@ -5,6 +5,7 @@ #ifndef BITCOIN_QT_SENDCOINSDIALOG_H #define BITCOIN_QT_SENDCOINSDIALOG_H +#include #include #include @@ -14,7 +15,6 @@ static const int MAX_SEND_POPUP_ENTRIES = 10; -class ClientModel; class SendCoinsEntry; class SendCoinsRecipient; enum class SynchronizationState; @@ -112,7 +112,7 @@ private Q_SLOTS: void coinControlClipboardBytes(); void coinControlClipboardChange(); void updateFeeSectionControls(); - void updateNumberOfBlocks(int count, const QDateTime& blockDate, const QString& blockHash, double nVerificationProgress, bool header, SynchronizationState sync_state); + void updateNumberOfBlocks(int count, const QDateTime& blockDate, const QString& blockHash, double nVerificationProgress, SyncType synctype, SynchronizationState sync_state); void updateSmartFeeLabel(); void keepChangeAddressChanged(bool); diff --git a/src/rpc/mining.cpp b/src/rpc/mining.cpp index 385240ee1e93..b18613b670c5 100644 --- a/src/rpc/mining.cpp +++ b/src/rpc/mining.cpp @@ -141,7 +141,7 @@ static bool GenerateBlock(ChainstateManager& chainman, CBlock& block, uint64_t& } std::shared_ptr shared_pblock = std::make_shared(block); - if (!chainman.ProcessNewBlock(shared_pblock, true, nullptr)) { + if (!chainman.ProcessNewBlock(shared_pblock, /*force_processing=*/true, /*min_pow_checked=*/true, nullptr)) { throw JSONRPCError(RPC_INTERNAL_ERROR, "ProcessNewBlock, block not accepted"); } @@ -1033,7 +1033,7 @@ static RPCHelpMan submitblock() bool new_block; auto sc = std::make_shared(block.GetHash()); RegisterSharedValidationInterface(sc); - bool accepted = chainman.ProcessNewBlock(blockptr, /*force_processing=*/true, /*new_block=*/&new_block); + bool accepted = chainman.ProcessNewBlock(blockptr, /*force_processing=*/true, /*min_pow_checked=*/true, /*new_block=*/&new_block); UnregisterSharedValidationInterface(sc); if (!new_block && accepted) { return "duplicate"; @@ -1075,7 +1075,7 @@ static RPCHelpMan submitheader() } BlockValidationState state; - chainman.ProcessNewBlockHeaders({h}, state); + chainman.ProcessNewBlockHeaders({h}, /*min_pow_checked=*/true, state); if (state.IsValid()) return UniValue::VNULL; if (state.IsError()) { throw JSONRPCError(RPC_VERIFY_ERROR, state.ToString()); diff --git a/src/rpc/net.cpp b/src/rpc/net.cpp index 2e75a19f3bcc..ca1e80c61b4e 100644 --- a/src/rpc/net.cpp +++ b/src/rpc/net.cpp @@ -157,6 +157,7 @@ static RPCHelpMan getpeerinfo() {RPCResult::Type::BOOL, "masternode", "Whether connection was due to masternode connection attempt"}, {RPCResult::Type::NUM, "banscore", "The ban score (DEPRECATED, returned only if config option -deprecatedrpc=banscore is passed)"}, {RPCResult::Type::NUM, "startingheight", "The starting height (block) of the peer"}, + {RPCResult::Type::NUM, "presynced_headers", /*optional=*/true, "The current height of header pre-synchronization with this peer, or -1 if no low-work sync is in progress"}, {RPCResult::Type::NUM, "synced_headers", "The last header we have in common with this peer"}, {RPCResult::Type::NUM, "synced_blocks", "The last block we have in common with this peer"}, {RPCResult::Type::ARR, "inflight", "", @@ -272,6 +273,7 @@ static RPCHelpMan getpeerinfo() obj.pushKV("banscore", statestats.m_misbehavior_score); } obj.pushKV("startingheight", statestats.m_starting_height); + obj.pushKV("presynced_headers", statestats.presync_height); obj.pushKV("synced_headers", statestats.nSyncHeight); obj.pushKV("synced_blocks", statestats.nCommonHeight); UniValue heights(UniValue::VARR); diff --git a/src/test/blockfilter_index_tests.cpp b/src/test/blockfilter_index_tests.cpp index 4a38ec79d8ad..feae22408d0c 100644 --- a/src/test/blockfilter_index_tests.cpp +++ b/src/test/blockfilter_index_tests.cpp @@ -101,7 +101,7 @@ bool BuildChainTestingSetup::BuildChain(const CBlockIndex* pindex, CBlockHeader header = block->GetBlockHeader(); BlockValidationState state; - if (!Assert(m_node.chainman)->ProcessNewBlockHeaders({header}, state, &pindex)) { + if (!Assert(m_node.chainman)->ProcessNewBlockHeaders({header}, true, state, &pindex)) { return false; } } @@ -172,7 +172,7 @@ BOOST_FIXTURE_TEST_CASE(blockfilter_index_initial_sync, BuildChainTestingSetup) uint256 chainA_last_header = last_header; for (size_t i = 0; i < 2; i++) { const auto& block = chainA[i]; - BOOST_REQUIRE(Assert(m_node.chainman)->ProcessNewBlock(block, true, nullptr)); + BOOST_REQUIRE(Assert(m_node.chainman)->ProcessNewBlock(block, true, true, nullptr)); } for (size_t i = 0; i < 2; i++) { const auto& block = chainA[i]; @@ -190,7 +190,7 @@ BOOST_FIXTURE_TEST_CASE(blockfilter_index_initial_sync, BuildChainTestingSetup) uint256 chainB_last_header = last_header; for (size_t i = 0; i < 3; i++) { const auto& block = chainB[i]; - BOOST_REQUIRE(Assert(m_node.chainman)->ProcessNewBlock(block, true, nullptr)); + BOOST_REQUIRE(Assert(m_node.chainman)->ProcessNewBlock(block, true, true, nullptr)); } for (size_t i = 0; i < 3; i++) { const auto& block = chainB[i]; @@ -221,7 +221,7 @@ BOOST_FIXTURE_TEST_CASE(blockfilter_index_initial_sync, BuildChainTestingSetup) // Reorg back to chain A. for (size_t i = 2; i < 4; i++) { const auto& block = chainA[i]; - BOOST_REQUIRE(Assert(m_node.chainman)->ProcessNewBlock(block, true, nullptr)); + BOOST_REQUIRE(Assert(m_node.chainman)->ProcessNewBlock(block, true, true, nullptr)); } // Check that chain A and B blocks can be retrieved. diff --git a/src/test/coinstatsindex_tests.cpp b/src/test/coinstatsindex_tests.cpp index b8f5a2b4b9f0..b7271a7d3c1d 100644 --- a/src/test/coinstatsindex_tests.cpp +++ b/src/test/coinstatsindex_tests.cpp @@ -95,7 +95,7 @@ BOOST_FIXTURE_TEST_CASE(coinstatsindex_unclean_shutdown, TestChain100Setup) LOCK(cs_main); BlockValidationState state; BOOST_CHECK(CheckBlock(block, state, params.GetConsensus())); - BOOST_CHECK(chainstate.AcceptBlock(new_block, state, &new_block_index, true, nullptr, nullptr)); + BOOST_CHECK(chainstate.AcceptBlock(new_block, state, &new_block_index, true, nullptr, nullptr, true)); CCoinsViewCache view(&chainstate.CoinsTip()); BOOST_CHECK(chainstate.ConnectBlock(block, state, new_block_index, view)); } diff --git a/src/test/evo_deterministicmns_tests.cpp b/src/test/evo_deterministicmns_tests.cpp index af286a51f148..61667d3b8b2c 100644 --- a/src/test/evo_deterministicmns_tests.cpp +++ b/src/test/evo_deterministicmns_tests.cpp @@ -264,7 +264,7 @@ void FuncDIP3Activation(TestChainSetup& setup) // We start one block before DIP3 activation, so mining a block with a DIP3 transaction should fail auto block = std::make_shared(setup.CreateBlock(txns, coinbase_pk, chainman.ActiveChainstate())); - chainman.ProcessNewBlock(block, true, nullptr); + chainman.ProcessNewBlock(block, true, true, nullptr); BOOST_CHECK_EQUAL(chainman.ActiveChain().Height(), nHeight); BOOST_REQUIRE(block->GetHash() != chainman.ActiveChain().Tip()->GetBlockHash()); BOOST_REQUIRE(!dmnman.GetListAtChainTip().HasMN(tx.GetHash())); @@ -274,7 +274,7 @@ void FuncDIP3Activation(TestChainSetup& setup) BOOST_CHECK_EQUAL(chainman.ActiveChain().Height(), nHeight + 1); // Mining a block with a DIP3 transaction should succeed now block = std::make_shared(setup.CreateBlock(txns, coinbase_pk, chainman.ActiveChainstate())); - BOOST_REQUIRE(chainman.ProcessNewBlock(block, true, nullptr)); + BOOST_REQUIRE(chainman.ProcessNewBlock(block, true, true, nullptr)); dmnman.UpdatedBlockTip(chainman.ActiveChain().Tip()); BOOST_CHECK_EQUAL(chainman.ActiveChain().Height(), nHeight + 2); BOOST_CHECK_EQUAL(block->GetHash(), chainman.ActiveChain().Tip()->GetBlockHash()); @@ -302,7 +302,7 @@ void FuncV19Activation(TestChainSetup& setup) int nHeight = chainman.ActiveChain().Height(); auto block = std::make_shared(setup.CreateBlock({tx_reg}, coinbase_pk, chainman.ActiveChainstate())); - BOOST_REQUIRE(chainman.ProcessNewBlock(block, true, nullptr)); + BOOST_REQUIRE(chainman.ProcessNewBlock(block, true, true, nullptr)); BOOST_REQUIRE(!DeploymentActiveAfter(chainman.ActiveChain().Tip(), chainman.GetConsensus(), Consensus::DEPLOYMENT_V19)); ++nHeight; BOOST_CHECK_EQUAL(chainman.ActiveChain().Height(), nHeight); @@ -320,7 +320,7 @@ void FuncV19Activation(TestChainSetup& setup) auto tx_upreg = CreateProUpRegTx(chainman.ActiveChain(), *(setup.m_node.mempool), utxos, tx_reg_hash, owner_key, operator_key_new.GetPublicKey(), owner_key.GetPubKey().GetID(), collateralScript, setup.coinbaseKey); block = std::make_shared(setup.CreateBlock({tx_upreg}, coinbase_pk, chainman.ActiveChainstate())); - BOOST_REQUIRE(chainman.ProcessNewBlock(block, true, nullptr)); + BOOST_REQUIRE(chainman.ProcessNewBlock(block, true, true, nullptr)); BOOST_REQUIRE(!DeploymentActiveAfter(chainman.ActiveChain().Tip(), chainman.GetConsensus(), Consensus::DEPLOYMENT_V19)); ++nHeight; BOOST_CHECK_EQUAL(chainman.ActiveChain().Height(), nHeight); @@ -340,7 +340,7 @@ void FuncV19Activation(TestChainSetup& setup) signing_provider.AddKeyPubKey(collateral_key, collateral_key.GetPubKey()); BOOST_REQUIRE(SignSignature(signing_provider, CTransaction(tx_reg), tx_spend, 0, SIGHASH_ALL)); block = std::make_shared(setup.CreateBlock({tx_spend}, coinbase_pk, chainman.ActiveChainstate())); - BOOST_REQUIRE(chainman.ProcessNewBlock(block, true, nullptr)); + BOOST_REQUIRE(chainman.ProcessNewBlock(block, true, true, nullptr)); BOOST_REQUIRE(!DeploymentActiveAfter(chainman.ActiveChain().Tip(), chainman.GetConsensus(), Consensus::DEPLOYMENT_V19)); ++nHeight; BOOST_CHECK_EQUAL(chainman.ActiveChain().Height(), nHeight); @@ -635,7 +635,7 @@ void FuncTestMempoolReorg(TestChainSetup& setup) SignTransaction(*(setup.m_node.mempool), tx_collateral, setup.coinbaseKey); auto block = std::make_shared(setup.CreateBlock({tx_collateral}, coinbase_pk, chainman.ActiveChainstate())); - BOOST_REQUIRE(chainman.ProcessNewBlock(block, true, nullptr)); + BOOST_REQUIRE(chainman.ProcessNewBlock(block, true, true, nullptr)); setup.m_node.dmnman->UpdatedBlockTip(chainman.ActiveChain().Tip()); BOOST_CHECK_EQUAL(chainman.ActiveChain().Height(), nHeight + 1); BOOST_CHECK_EQUAL(block->GetHash(), chainman.ActiveChain().Tip()->GetBlockHash()); @@ -781,7 +781,7 @@ void FuncVerifyDB(TestChainSetup& setup) SignTransaction(*(setup.m_node.mempool), tx_collateral, setup.coinbaseKey); auto block = std::make_shared(setup.CreateBlock({tx_collateral}, coinbase_pk, chainman.ActiveChainstate())); - BOOST_REQUIRE(chainman.ProcessNewBlock(block, true, nullptr)); + BOOST_REQUIRE(chainman.ProcessNewBlock(block, true, true, nullptr)); dmnman.UpdatedBlockTip(chainman.ActiveChain().Tip()); BOOST_CHECK_EQUAL(chainman.ActiveChain().Height(), nHeight + 1); BOOST_CHECK_EQUAL(block->GetHash(), chainman.ActiveChain().Tip()->GetBlockHash()); @@ -814,7 +814,7 @@ void FuncVerifyDB(TestChainSetup& setup) auto tx_reg_hash = tx_reg.GetHash(); block = std::make_shared(setup.CreateBlock({tx_reg}, coinbase_pk, chainman.ActiveChainstate())); - BOOST_REQUIRE(chainman.ProcessNewBlock(block, true, nullptr)); + BOOST_REQUIRE(chainman.ProcessNewBlock(block, true, true, nullptr)); dmnman.UpdatedBlockTip(chainman.ActiveChain().Tip()); BOOST_CHECK_EQUAL(chainman.ActiveChain().Height(), nHeight + 2); BOOST_CHECK_EQUAL(block->GetHash(), chainman.ActiveChain().Tip()->GetBlockHash()); @@ -826,7 +826,7 @@ void FuncVerifyDB(TestChainSetup& setup) auto proUpRevTx = CreateProUpRevTx(chainman.ActiveChain(), *(setup.m_node.mempool), collateral_utxos, tx_reg_hash, operatorKey, collateralKey); block = std::make_shared(setup.CreateBlock({proUpRevTx}, coinbase_pk, chainman.ActiveChainstate())); - BOOST_REQUIRE(chainman.ProcessNewBlock(block, true, nullptr)); + BOOST_REQUIRE(chainman.ProcessNewBlock(block, true, true, nullptr)); dmnman.UpdatedBlockTip(chainman.ActiveChain().Tip()); BOOST_CHECK_EQUAL(chainman.ActiveChain().Height(), nHeight + 3); BOOST_CHECK_EQUAL(block->GetHash(), chainman.ActiveChain().Tip()->GetBlockHash()); diff --git a/src/test/fuzz/bitdeque.cpp b/src/test/fuzz/bitdeque.cpp new file mode 100644 index 000000000000..3b24d0d12967 --- /dev/null +++ b/src/test/fuzz/bitdeque.cpp @@ -0,0 +1,542 @@ +// Copyright (c) 2022 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include + +#include +#include +#include + +#include +#include + +namespace { + +constexpr int LEN_BITS = 16; +constexpr int RANDDATA_BITS = 20; + +using bitdeque_type = bitdeque<128>; + +//! Deterministic random vector of bools, for begin/end insertions to draw from. +std::vector RANDDATA; + +void InitRandData() +{ + FastRandomContext ctx(true); + RANDDATA.clear(); + for (size_t i = 0; i < (1U << RANDDATA_BITS) + (1U << LEN_BITS); ++i) { + RANDDATA.push_back(ctx.randbool()); + } +} + +} // namespace + +FUZZ_TARGET(bitdeque, .init = InitRandData) +{ + FuzzedDataProvider provider(buffer.data(), buffer.size()); + FastRandomContext ctx(true); + + size_t maxlen = (1U << provider.ConsumeIntegralInRange(0, LEN_BITS)) - 1; + size_t limitlen = 4 * maxlen; + + std::deque deq; + bitdeque_type bitdeq; + + const auto& cdeq = deq; + const auto& cbitdeq = bitdeq; + + size_t initlen = provider.ConsumeIntegralInRange(0, maxlen); + while (initlen) { + bool val = ctx.randbool(); + deq.push_back(val); + bitdeq.push_back(val); + --initlen; + } + + while (provider.remaining_bytes()) { + { + assert(deq.size() == bitdeq.size()); + auto it = deq.begin(); + auto bitit = bitdeq.begin(); + auto itend = deq.end(); + while (it != itend) { + assert(*it == *bitit); + ++it; + ++bitit; + } + } + + CallOneOf(provider, + [&] { + // constructor() + deq = std::deque{}; + bitdeq = bitdeque_type{}; + }, + [&] { + // clear() + deq.clear(); + bitdeq.clear(); + }, + [&] { + // resize() + auto count = provider.ConsumeIntegralInRange(0, maxlen); + deq.resize(count); + bitdeq.resize(count); + }, + [&] { + // assign(count, val) + auto count = provider.ConsumeIntegralInRange(0, maxlen); + bool val = ctx.randbool(); + deq.assign(count, val); + bitdeq.assign(count, val); + }, + [&] { + // constructor(count, val) + auto count = provider.ConsumeIntegralInRange(0, maxlen); + bool val = ctx.randbool(); + deq = std::deque(count, val); + bitdeq = bitdeque_type(count, val); + }, + [&] { + // constructor(count) + auto count = provider.ConsumeIntegralInRange(0, maxlen); + deq = std::deque(count); + bitdeq = bitdeque_type(count); + }, + [&] { + // construct(begin, end) + auto count = provider.ConsumeIntegralInRange(0, maxlen); + auto rand_begin = RANDDATA.begin() + ctx.randbits(RANDDATA_BITS); + auto rand_end = rand_begin + count; + deq = std::deque(rand_begin, rand_end); + bitdeq = bitdeque_type(rand_begin, rand_end); + }, + [&] { + // assign(begin, end) + auto count = provider.ConsumeIntegralInRange(0, maxlen); + auto rand_begin = RANDDATA.begin() + ctx.randbits(RANDDATA_BITS); + auto rand_end = rand_begin + count; + deq.assign(rand_begin, rand_end); + bitdeq.assign(rand_begin, rand_end); + }, + [&] { + // construct(initializer_list) + std::initializer_list ilist{ctx.randbool(), ctx.randbool(), ctx.randbool(), ctx.randbool(), ctx.randbool()}; + deq = std::deque(ilist); + bitdeq = bitdeque_type(ilist); + }, + [&] { + // assign(initializer_list) + std::initializer_list ilist{ctx.randbool(), ctx.randbool(), ctx.randbool()}; + deq.assign(ilist); + bitdeq.assign(ilist); + }, + [&] { + // operator=(const&) + auto count = provider.ConsumeIntegralInRange(0, maxlen); + bool val = ctx.randbool(); + const std::deque deq2(count, val); + deq = deq2; + const bitdeque_type bitdeq2(count, val); + bitdeq = bitdeq2; + }, + [&] { + // operator=(&&) + auto count = provider.ConsumeIntegralInRange(0, maxlen); + bool val = ctx.randbool(); + std::deque deq2(count, val); + deq = std::move(deq2); + bitdeque_type bitdeq2(count, val); + bitdeq = std::move(bitdeq2); + }, + [&] { + // deque swap + auto count = provider.ConsumeIntegralInRange(0, maxlen); + auto rand_begin = RANDDATA.begin() + ctx.randbits(RANDDATA_BITS); + auto rand_end = rand_begin + count; + std::deque deq2(rand_begin, rand_end); + bitdeque_type bitdeq2(rand_begin, rand_end); + using std::swap; + assert(deq.size() == bitdeq.size()); + assert(deq2.size() == bitdeq2.size()); + swap(deq, deq2); + swap(bitdeq, bitdeq2); + assert(deq.size() == bitdeq.size()); + assert(deq2.size() == bitdeq2.size()); + }, + [&] { + // deque.swap + auto count = provider.ConsumeIntegralInRange(0, maxlen); + auto rand_begin = RANDDATA.begin() + ctx.randbits(RANDDATA_BITS); + auto rand_end = rand_begin + count; + std::deque deq2(rand_begin, rand_end); + bitdeque_type bitdeq2(rand_begin, rand_end); + assert(deq.size() == bitdeq.size()); + assert(deq2.size() == bitdeq2.size()); + deq.swap(deq2); + bitdeq.swap(bitdeq2); + assert(deq.size() == bitdeq.size()); + assert(deq2.size() == bitdeq2.size()); + }, + [&] { + // operator=(initializer_list) + std::initializer_list ilist{ctx.randbool(), ctx.randbool(), ctx.randbool()}; + deq = ilist; + bitdeq = ilist; + }, + [&] { + // iterator arithmetic + auto pos1 = provider.ConsumeIntegralInRange(0, cdeq.size()); + auto pos2 = provider.ConsumeIntegralInRange(0, cdeq.size()); + auto it = deq.begin() + pos1; + auto bitit = bitdeq.begin() + pos1; + if ((size_t)pos1 != cdeq.size()) assert(*it == *bitit); + assert(it - deq.begin() == pos1); + assert(bitit - bitdeq.begin() == pos1); + if (provider.ConsumeBool()) { + it += pos2 - pos1; + bitit += pos2 - pos1; + } else { + it -= pos1 - pos2; + bitit -= pos1 - pos2; + } + if ((size_t)pos2 != cdeq.size()) assert(*it == *bitit); + assert(deq.end() - it == bitdeq.end() - bitit); + if (provider.ConsumeBool()) { + if ((size_t)pos2 != cdeq.size()) { + ++it; + ++bitit; + } + } else { + if (pos2 != 0) { + --it; + --bitit; + } + } + assert(deq.end() - it == bitdeq.end() - bitit); + }, + [&] { + // begin() and end() + assert(deq.end() - deq.begin() == bitdeq.end() - bitdeq.begin()); + }, + [&] { + // begin() and end() (const) + assert(cdeq.end() - cdeq.begin() == cbitdeq.end() - cbitdeq.begin()); + }, + [&] { + // rbegin() and rend() + assert(deq.rend() - deq.rbegin() == bitdeq.rend() - bitdeq.rbegin()); + }, + [&] { + // rbegin() and rend() (const) + assert(cdeq.rend() - cdeq.rbegin() == cbitdeq.rend() - cbitdeq.rbegin()); + }, + [&] { + // cbegin() and cend() + assert(cdeq.cend() - cdeq.cbegin() == cbitdeq.cend() - cbitdeq.cbegin()); + }, + [&] { + // crbegin() and crend() + assert(cdeq.crend() - cdeq.crbegin() == cbitdeq.crend() - cbitdeq.crbegin()); + }, + [&] { + // size() and maxsize() + assert(cdeq.size() == cbitdeq.size()); + assert(cbitdeq.size() <= cbitdeq.max_size()); + }, + [&] { + // empty + assert(cdeq.empty() == cbitdeq.empty()); + }, + [&] { + // at (in range) and flip + if (!cdeq.empty()) { + size_t pos = provider.ConsumeIntegralInRange(0, cdeq.size() - 1); + auto& ref = deq.at(pos); + auto bitref = bitdeq.at(pos); + assert(ref == bitref); + if (ctx.randbool()) { + ref = !ref; + bitref.flip(); + } + } + }, + [&] { + // at (maybe out of range) and bit assign + size_t pos = provider.ConsumeIntegralInRange(0, cdeq.size() + maxlen); + bool newval = ctx.randbool(); + bool throw_deq{false}, throw_bitdeq{false}; + bool val_deq{false}, val_bitdeq{false}; + try { + auto& ref = deq.at(pos); + val_deq = ref; + ref = newval; + } catch (const std::out_of_range&) { + throw_deq = true; + } + try { + auto ref = bitdeq.at(pos); + val_bitdeq = ref; + ref = newval; + } catch (const std::out_of_range&) { + throw_bitdeq = true; + } + assert(throw_deq == throw_bitdeq); + assert(throw_bitdeq == (pos >= cdeq.size())); + if (!throw_deq) assert(val_deq == val_bitdeq); + }, + [&] { + // at (maybe out of range) (const) + size_t pos = provider.ConsumeIntegralInRange(0, cdeq.size() + maxlen); + bool throw_deq{false}, throw_bitdeq{false}; + bool val_deq{false}, val_bitdeq{false}; + try { + auto& ref = cdeq.at(pos); + val_deq = ref; + } catch (const std::out_of_range&) { + throw_deq = true; + } + try { + auto ref = cbitdeq.at(pos); + val_bitdeq = ref; + } catch (const std::out_of_range&) { + throw_bitdeq = true; + } + assert(throw_deq == throw_bitdeq); + assert(throw_bitdeq == (pos >= cdeq.size())); + if (!throw_deq) assert(val_deq == val_bitdeq); + }, + [&] { + // operator[] + if (!cdeq.empty()) { + size_t pos = provider.ConsumeIntegralInRange(0, cdeq.size() - 1); + assert(deq[pos] == bitdeq[pos]); + if (ctx.randbool()) { + deq[pos] = !deq[pos]; + bitdeq[pos].flip(); + } + } + }, + [&] { + // operator[] const + if (!cdeq.empty()) { + size_t pos = provider.ConsumeIntegralInRange(0, cdeq.size() - 1); + assert(deq[pos] == bitdeq[pos]); + } + }, + [&] { + // front() + if (!cdeq.empty()) { + auto& ref = deq.front(); + auto bitref = bitdeq.front(); + assert(ref == bitref); + if (ctx.randbool()) { + ref = !ref; + bitref = !bitref; + } + } + }, + [&] { + // front() const + if (!cdeq.empty()) { + auto& ref = cdeq.front(); + auto bitref = cbitdeq.front(); + assert(ref == bitref); + } + }, + [&] { + // back() and swap(bool, ref) + if (!cdeq.empty()) { + auto& ref = deq.back(); + auto bitref = bitdeq.back(); + assert(ref == bitref); + if (ctx.randbool()) { + ref = !ref; + bitref.flip(); + } + } + }, + [&] { + // back() const + if (!cdeq.empty()) { + const auto& cdeq = deq; + const auto& cbitdeq = bitdeq; + auto& ref = cdeq.back(); + auto bitref = cbitdeq.back(); + assert(ref == bitref); + } + }, + [&] { + // push_back() + if (cdeq.size() < limitlen) { + bool val = ctx.randbool(); + if (cdeq.empty()) { + deq.push_back(val); + bitdeq.push_back(val); + } else { + size_t pos = provider.ConsumeIntegralInRange(0, cdeq.size() - 1); + auto& ref = deq[pos]; + auto bitref = bitdeq[pos]; + assert(ref == bitref); + deq.push_back(val); + bitdeq.push_back(val); + assert(ref == bitref); // references are not invalidated + } + } + }, + [&] { + // push_front() + if (cdeq.size() < limitlen) { + bool val = ctx.randbool(); + if (cdeq.empty()) { + deq.push_front(val); + bitdeq.push_front(val); + } else { + size_t pos = provider.ConsumeIntegralInRange(0, cdeq.size() - 1); + auto& ref = deq[pos]; + auto bitref = bitdeq[pos]; + assert(ref == bitref); + deq.push_front(val); + bitdeq.push_front(val); + assert(ref == bitref); // references are not invalidated + } + } + }, + [&] { + // pop_back() + if (!cdeq.empty()) { + if (cdeq.size() == 1) { + deq.pop_back(); + bitdeq.pop_back(); + } else { + size_t pos = provider.ConsumeIntegralInRange(0, cdeq.size() - 2); + auto& ref = deq[pos]; + auto bitref = bitdeq[pos]; + assert(ref == bitref); + deq.pop_back(); + bitdeq.pop_back(); + assert(ref == bitref); // references to other elements are not invalidated + } + } + }, + [&] { + // pop_front() + if (!cdeq.empty()) { + if (cdeq.size() == 1) { + deq.pop_front(); + bitdeq.pop_front(); + } else { + size_t pos = provider.ConsumeIntegralInRange(1, cdeq.size() - 1); + auto& ref = deq[pos]; + auto bitref = bitdeq[pos]; + assert(ref == bitref); + deq.pop_front(); + bitdeq.pop_front(); + assert(ref == bitref); // references to other elements are not invalidated + } + } + }, + [&] { + // erase (in middle, single) + if (!cdeq.empty()) { + size_t before = provider.ConsumeIntegralInRange(0, cdeq.size() - 1); + size_t after = cdeq.size() - 1 - before; + auto it = deq.erase(cdeq.begin() + before); + auto bitit = bitdeq.erase(cbitdeq.begin() + before); + assert(it == cdeq.begin() + before && it == cdeq.end() - after); + assert(bitit == cbitdeq.begin() + before && bitit == cbitdeq.end() - after); + } + }, + [&] { + // erase (at front, range) + size_t count = provider.ConsumeIntegralInRange(0, cdeq.size()); + auto it = deq.erase(cdeq.begin(), cdeq.begin() + count); + auto bitit = bitdeq.erase(cbitdeq.begin(), cbitdeq.begin() + count); + assert(it == deq.begin()); + assert(bitit == bitdeq.begin()); + }, + [&] { + // erase (at back, range) + size_t count = provider.ConsumeIntegralInRange(0, cdeq.size()); + auto it = deq.erase(cdeq.end() - count, cdeq.end()); + auto bitit = bitdeq.erase(cbitdeq.end() - count, cbitdeq.end()); + assert(it == deq.end()); + assert(bitit == bitdeq.end()); + }, + [&] { + // erase (in middle, range) + size_t count = provider.ConsumeIntegralInRange(0, cdeq.size()); + size_t before = provider.ConsumeIntegralInRange(0, cdeq.size() - count); + size_t after = cdeq.size() - count - before; + auto it = deq.erase(cdeq.begin() + before, cdeq.end() - after); + auto bitit = bitdeq.erase(cbitdeq.begin() + before, cbitdeq.end() - after); + assert(it == cdeq.begin() + before && it == cdeq.end() - after); + assert(bitit == cbitdeq.begin() + before && bitit == cbitdeq.end() - after); + }, + [&] { + // insert/emplace (in middle, single) + if (cdeq.size() < limitlen) { + size_t before = provider.ConsumeIntegralInRange(0, cdeq.size()); + bool val = ctx.randbool(); + bool do_emplace = provider.ConsumeBool(); + auto it = deq.insert(cdeq.begin() + before, val); + auto bitit = do_emplace ? bitdeq.emplace(cbitdeq.begin() + before, val) + : bitdeq.insert(cbitdeq.begin() + before, val); + assert(it == deq.begin() + before); + assert(bitit == bitdeq.begin() + before); + } + }, + [&] { + // insert (at front, begin/end) + if (cdeq.size() < limitlen) { + size_t count = provider.ConsumeIntegralInRange(0, maxlen); + auto rand_begin = RANDDATA.begin() + ctx.randbits(RANDDATA_BITS); + auto rand_end = rand_begin + count; + auto it = deq.insert(cdeq.begin(), rand_begin, rand_end); + auto bitit = bitdeq.insert(cbitdeq.begin(), rand_begin, rand_end); + assert(it == cdeq.begin()); + assert(bitit == cbitdeq.begin()); + } + }, + [&] { + // insert (at back, begin/end) + if (cdeq.size() < limitlen) { + size_t count = provider.ConsumeIntegralInRange(0, maxlen); + auto rand_begin = RANDDATA.begin() + ctx.randbits(RANDDATA_BITS); + auto rand_end = rand_begin + count; + auto it = deq.insert(cdeq.end(), rand_begin, rand_end); + auto bitit = bitdeq.insert(cbitdeq.end(), rand_begin, rand_end); + assert(it == cdeq.end() - count); + assert(bitit == cbitdeq.end() - count); + } + }, + [&] { + // insert (in middle, range) + if (cdeq.size() < limitlen) { + size_t count = provider.ConsumeIntegralInRange(0, maxlen); + size_t before = provider.ConsumeIntegralInRange(0, cdeq.size()); + bool val = ctx.randbool(); + auto it = deq.insert(cdeq.begin() + before, count, val); + auto bitit = bitdeq.insert(cbitdeq.begin() + before, count, val); + assert(it == deq.begin() + before); + assert(bitit == bitdeq.begin() + before); + } + }, + [&] { + // insert (in middle, begin/end) + if (cdeq.size() < limitlen) { + size_t count = provider.ConsumeIntegralInRange(0, maxlen); + size_t before = provider.ConsumeIntegralInRange(0, cdeq.size()); + auto rand_begin = RANDDATA.begin() + ctx.randbits(RANDDATA_BITS); + auto rand_end = rand_begin + count; + auto it = deq.insert(cdeq.begin() + before, rand_begin, rand_end); + auto bitit = bitdeq.insert(cbitdeq.begin() + before, rand_begin, rand_end); + assert(it == deq.begin() + before); + assert(bitit == bitdeq.begin() + before); + } + } + ); + } + +} diff --git a/src/test/fuzz/pow.cpp b/src/test/fuzz/pow.cpp index eadf9328f127..d80ab6dbd83d 100644 --- a/src/test/fuzz/pow.cpp +++ b/src/test/fuzz/pow.cpp @@ -83,3 +83,40 @@ FUZZ_TARGET(pow, .init = initialize_pow) } } } + + +FUZZ_TARGET(pow_transition, .init = initialize_pow) +{ + FuzzedDataProvider fuzzed_data_provider(buffer.data(), buffer.size()); + const Consensus::Params& consensus_params{Params().GetConsensus()}; + std::vector> blocks; + + const uint32_t old_time{fuzzed_data_provider.ConsumeIntegral()}; + const uint32_t new_time{fuzzed_data_provider.ConsumeIntegral()}; + const int32_t version{fuzzed_data_provider.ConsumeIntegral()}; + uint32_t nbits{fuzzed_data_provider.ConsumeIntegral()}; + + const arith_uint256 pow_limit = UintToArith256(consensus_params.powLimit); + arith_uint256 old_target; + old_target.SetCompact(nbits); + if (old_target > pow_limit) { + nbits = pow_limit.GetCompact(); + } + // Create one difficulty adjustment period worth of headers + for (int height = 0; height < consensus_params.DifficultyAdjustmentInterval(); ++height) { + CBlockHeader header; + header.nVersion = version; + header.nTime = old_time; + header.nBits = nbits; + if (height == consensus_params.DifficultyAdjustmentInterval() - 1) { + header.nTime = new_time; + } + auto current_block{std::make_unique(header)}; + current_block->pprev = blocks.empty() ? nullptr : blocks.back().get(); + current_block->nHeight = height; + blocks.emplace_back(std::move(current_block)).get(); + } + auto last_block{blocks.back().get()}; + unsigned int new_nbits{GetNextWorkRequired(last_block, nullptr, consensus_params)}; + Assert(PermittedDifficultyTransition(consensus_params, last_block->nHeight + 1, last_block->nBits, new_nbits)); +} diff --git a/src/test/fuzz/utxo_snapshot.cpp b/src/test/fuzz/utxo_snapshot.cpp index b4a5de6371b1..deab0120d630 100644 --- a/src/test/fuzz/utxo_snapshot.cpp +++ b/src/test/fuzz/utxo_snapshot.cpp @@ -57,7 +57,7 @@ FUZZ_TARGET(utxo_snapshot, .init = initialize_chain) if (fuzzed_data_provider.ConsumeBool()) { for (const auto& block : *g_chain) { BlockValidationState dummy; - bool processed{chainman.ProcessNewBlockHeaders({*block}, dummy)}; + bool processed{chainman.ProcessNewBlockHeaders({*block}, true, dummy)}; Assert(processed); const auto* index{WITH_LOCK(::cs_main, return chainman.m_blockman.LookupBlockIndex(block->GetHash()))}; Assert(index); diff --git a/src/test/headers_sync_chainwork_tests.cpp b/src/test/headers_sync_chainwork_tests.cpp new file mode 100644 index 000000000000..41241ebee289 --- /dev/null +++ b/src/test/headers_sync_chainwork_tests.cpp @@ -0,0 +1,146 @@ +// Copyright (c) 2022 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +struct HeadersGeneratorSetup : public RegTestingSetup { + /** Search for a nonce to meet (regtest) proof of work */ + void FindProofOfWork(CBlockHeader& starting_header); + /** + * Generate headers in a chain that build off a given starting hash, using + * the given nVersion, advancing time by 1 second from the starting + * prev_time, and with a fixed merkle root hash. + */ + void GenerateHeaders(std::vector& headers, size_t count, + const uint256& starting_hash, const int nVersion, int prev_time, + const uint256& merkle_root, const uint32_t nBits); +}; + +void HeadersGeneratorSetup::FindProofOfWork(CBlockHeader& starting_header) +{ + while (!CheckProofOfWork(starting_header.GetHash(), starting_header.nBits, Params().GetConsensus())) { + ++(starting_header.nNonce); + } +} + +void HeadersGeneratorSetup::GenerateHeaders(std::vector& headers, + size_t count, const uint256& starting_hash, const int nVersion, int prev_time, + const uint256& merkle_root, const uint32_t nBits) +{ + uint256 prev_hash = starting_hash; + + while (headers.size() < count) { + headers.push_back(CBlockHeader()); + CBlockHeader& next_header = headers.back();; + next_header.nVersion = nVersion; + next_header.hashPrevBlock = prev_hash; + next_header.hashMerkleRoot = merkle_root; + next_header.nTime = prev_time+1; + next_header.nBits = nBits; + + FindProofOfWork(next_header); + prev_hash = next_header.GetHash(); + prev_time = next_header.nTime; + } + return; +} + +BOOST_FIXTURE_TEST_SUITE(headers_sync_chainwork_tests, HeadersGeneratorSetup) + +// In this test, we construct two sets of headers from genesis, one with +// sufficient proof of work and one without. +// 1. We deliver the first set of headers and verify that the headers sync state +// updates to the REDOWNLOAD phase successfully. +// 2. Then we deliver the second set of headers and verify that they fail +// processing (presumably due to commitments not matching). +// 3. Finally, we verify that repeating with the first set of headers in both +// phases is successful. +BOOST_AUTO_TEST_CASE(headers_sync_state) +{ + std::vector first_chain; + std::vector second_chain; + + std::unique_ptr hss; + + const int target_blocks = 15000; + arith_uint256 chain_work = target_blocks*2; + + // Generate headers for two different chains (using differing merkle roots + // to ensure the headers are different). + GenerateHeaders(first_chain, target_blocks-1, Params().GenesisBlock().GetHash(), + Params().GenesisBlock().nVersion, Params().GenesisBlock().nTime, + ArithToUint256(0), Params().GenesisBlock().nBits); + + GenerateHeaders(second_chain, target_blocks-2, Params().GenesisBlock().GetHash(), + Params().GenesisBlock().nVersion, Params().GenesisBlock().nTime, + ArithToUint256(1), Params().GenesisBlock().nBits); + + const CBlockIndex* chain_start = WITH_LOCK(::cs_main, return m_node.chainman->m_blockman.LookupBlockIndex(Params().GenesisBlock().GetHash())); + std::vector headers_batch; + + // Feed the first chain to HeadersSyncState, by delivering 1 header + // initially and then the rest. + headers_batch.insert(headers_batch.end(), std::next(first_chain.begin()), first_chain.end()); + + hss.reset(new HeadersSyncState(0, Params().GetConsensus(), chain_start, chain_work)); + (void)hss->ProcessNextHeaders({first_chain.front()}, true); + // Pretend the first header is still "full", so we don't abort. + auto result = hss->ProcessNextHeaders(headers_batch, true); + + // This chain should look valid, and we should have met the proof-of-work + // requirement. + BOOST_CHECK(result.success); + BOOST_CHECK(result.request_more); + BOOST_CHECK(hss->GetState() == HeadersSyncState::State::REDOWNLOAD); + + // Try to sneakily feed back the second chain. + result = hss->ProcessNextHeaders(second_chain, true); + BOOST_CHECK(!result.success); // foiled! + BOOST_CHECK(hss->GetState() == HeadersSyncState::State::FINAL); + + // Now try again, this time feeding the first chain twice. + hss.reset(new HeadersSyncState(0, Params().GetConsensus(), chain_start, chain_work)); + (void)hss->ProcessNextHeaders(first_chain, true); + BOOST_CHECK(hss->GetState() == HeadersSyncState::State::REDOWNLOAD); + + result = hss->ProcessNextHeaders(first_chain, true); + BOOST_CHECK(result.success); + BOOST_CHECK(!result.request_more); + // All headers should be ready for acceptance: + BOOST_CHECK(result.pow_validated_headers.size() == first_chain.size()); + // Nothing left for the sync logic to do: + BOOST_CHECK(hss->GetState() == HeadersSyncState::State::FINAL); + + // Finally, verify that just trying to process the second chain would not + // succeed (too little work) + hss.reset(new HeadersSyncState(0, Params().GetConsensus(), chain_start, chain_work)); + BOOST_CHECK(hss->GetState() == HeadersSyncState::State::PRESYNC); + // Pretend just the first message is "full", so we don't abort. + (void)hss->ProcessNextHeaders({second_chain.front()}, true); + BOOST_CHECK(hss->GetState() == HeadersSyncState::State::PRESYNC); + + headers_batch.clear(); + headers_batch.insert(headers_batch.end(), std::next(second_chain.begin(), 1), second_chain.end()); + // Tell the sync logic that the headers message was not full, implying no + // more headers can be requested. For a low-work-chain, this should causes + // the sync to end with no headers for acceptance. + result = hss->ProcessNextHeaders(headers_batch, false); + BOOST_CHECK(hss->GetState() == HeadersSyncState::State::FINAL); + BOOST_CHECK(result.pow_validated_headers.empty()); + BOOST_CHECK(!result.request_more); + // Nevertheless, no validation errors should have been detected with the + // chain: + BOOST_CHECK(result.success); +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/src/test/miner_tests.cpp b/src/test/miner_tests.cpp index 2ac2bc6d6698..a19716a8330d 100644 --- a/src/test/miner_tests.cpp +++ b/src/test/miner_tests.cpp @@ -599,7 +599,7 @@ BOOST_AUTO_TEST_CASE(CreateNewBlock_validity) } } std::shared_ptr shared_pblock = std::make_shared(*pblock); - BOOST_CHECK(Assert(m_node.chainman)->ProcessNewBlock(shared_pblock, true, nullptr)); + BOOST_CHECK(Assert(m_node.chainman)->ProcessNewBlock(shared_pblock, true, true, nullptr)); pblock->hashPrevBlock = pblock->GetHash(); }; diff --git a/src/test/pow_tests.cpp b/src/test/pow_tests.cpp index f1e32cc90425..90d9be58c43c 100644 --- a/src/test/pow_tests.cpp +++ b/src/test/pow_tests.cpp @@ -43,7 +43,13 @@ BOOST_AUTO_TEST_CASE(get_next_work) CBlockHeader blockHeader; blockHeader.nTime = 1408732505; // Block #123457 - BOOST_CHECK_EQUAL(GetNextWorkRequired(blockIndexLast, &blockHeader, chainParams->GetConsensus()), 0x1b1441deU); // Block #123457 has 0x1b1441de + // Here (and below): expected_nbits is calculated in + // CalculateNextWorkRequired(); redoing the calculation here would be just + // reimplementing the same code that is written in pow.cpp. Rather than + // copy that code, we just hardcode the expected result. + unsigned int expected_nbits = 0x1b1441deU; // Block #123457 has 0x1b1441deU + BOOST_CHECK_EQUAL(GetNextWorkRequired(blockIndexLast, &blockHeader, chainParams->GetConsensus()), expected_nbits); + BOOST_CHECK(PermittedDifficultyTransition(chainParams->GetConsensus(), blockIndexLast->nHeight+1, blockIndexLast->nBits, expected_nbits)); // test special rules for slow blocks on devnet/testnet const auto chainParamsDev = CreateChainParams(*m_node.args, CBaseChainParams::DEVNET); @@ -51,17 +57,26 @@ BOOST_AUTO_TEST_CASE(get_next_work) // make sure normal rules apply blockHeader.nTime = 1408732505; // Block #123457 BOOST_CHECK_EQUAL(GetNextWorkRequired(blockIndexLast, &blockHeader, chainParamsDev->GetConsensus()), 0x1b1441deU); // Block #123457 has 0x1b1441de + BOOST_CHECK(PermittedDifficultyTransition(chainParams->GetConsensus(), blockIndexLast->nHeight+1, blockIndexLast->nBits, expected_nbits)); // 10x higher target blockHeader.nTime = 1408733090; // Block #123457 (10m+1sec) BOOST_CHECK_EQUAL(GetNextWorkRequired(blockIndexLast, &blockHeader, chainParamsDev->GetConsensus()), 0x1c00c8f8U); // Block #123457 has 0x1c00c8f8 + BOOST_CHECK(PermittedDifficultyTransition(chainParams->GetConsensus(), blockIndexLast->nHeight+1, blockIndexLast->nBits, expected_nbits)); blockHeader.nTime = 1408733689; // Block #123457 (20m) BOOST_CHECK_EQUAL(GetNextWorkRequired(blockIndexLast, &blockHeader, chainParamsDev->GetConsensus()), 0x1c00c8f8U); // Block #123457 has 0x1c00c8f8 + BOOST_CHECK(PermittedDifficultyTransition(chainParams->GetConsensus(), blockIndexLast->nHeight+1, blockIndexLast->nBits, expected_nbits)); // lowest diff possible blockHeader.nTime = 1408739690; // Block #123457 (2h+1sec) BOOST_CHECK_EQUAL(GetNextWorkRequired(blockIndexLast, &blockHeader, chainParamsDev->GetConsensus()), 0x207fffffU); // Block #123457 has 0x207fffff + BOOST_CHECK(PermittedDifficultyTransition(chainParams->GetConsensus(), blockIndexLast->nHeight+1, blockIndexLast->nBits, expected_nbits)); blockHeader.nTime = 1408743289; // Block #123457 (3h) BOOST_CHECK_EQUAL(GetNextWorkRequired(blockIndexLast, &blockHeader, chainParamsDev->GetConsensus()), 0x207fffffU); // Block #123457 has 0x207fffff + BOOST_CHECK(PermittedDifficultyTransition(chainParams->GetConsensus(), blockIndexLast->nHeight+1, blockIndexLast->nBits, expected_nbits)); + + // Test that increasing nbits further would not be a PermittedDifficultyTransition. + unsigned int invalid_nbits = expected_nbits+1; + BOOST_CHECK(PermittedDifficultyTransition(chainParams->GetConsensus(), blockIndexLast->nHeight+1, blockIndexLast->nBits, invalid_nbits)); } /* Test the constraint on the upper bound for next work */ diff --git a/src/test/skiplist_tests.cpp b/src/test/skiplist_tests.cpp index 07724a97d06d..77d257ac4632 100644 --- a/src/test/skiplist_tests.cpp +++ b/src/test/skiplist_tests.cpp @@ -78,7 +78,7 @@ BOOST_AUTO_TEST_CASE(getlocator_test) for (int n=0; n<100; n++) { int r = InsecureRandRange(150000); CBlockIndex* tip = (r < 100000) ? &vBlocksMain[r] : &vBlocksSide[r - 100000]; - CBlockLocator locator = chain.GetLocator(tip); + CBlockLocator locator = GetLocator(tip); // The first result must be the block itself, the last one must be genesis. BOOST_CHECK(locator.vHave.front() == tip->GetBlockHash()); diff --git a/src/test/util/mining.cpp b/src/test/util/mining.cpp index adeabd74937a..364364c55259 100644 --- a/src/test/util/mining.cpp +++ b/src/test/util/mining.cpp @@ -72,7 +72,7 @@ COutPoint MineBlock(const NodeContext& node, const CScript& coinbase_scriptPubKe assert(block->nNonce); } - bool processed{Assert(node.chainman)->ProcessNewBlock(block, true, nullptr)}; + bool processed{Assert(node.chainman)->ProcessNewBlock(block, true, true, nullptr)}; assert(processed); return {block->vtx[0]->GetHash(), 0}; @@ -120,7 +120,7 @@ COutPoint MineBlock(const NodeContext& node, std::shared_ptr& block) bool new_block; BlockValidationStateCatcher bvsc{block->GetHash()}; RegisterValidationInterface(&bvsc); - const bool processed{chainman.ProcessNewBlock(block, true, &new_block)}; + const bool processed{chainman.ProcessNewBlock(block, true, true, &new_block)}; const bool duplicate{!new_block && processed}; assert(!duplicate); UnregisterValidationInterface(&bvsc); diff --git a/src/test/util/setup_common.cpp b/src/test/util/setup_common.cpp index 8a96cb0c9ba4..6018a7710fc2 100644 --- a/src/test/util/setup_common.cpp +++ b/src/test/util/setup_common.cpp @@ -512,7 +512,7 @@ CBlock TestChainSetup::CreateAndProcessBlock( CBlock block = this->CreateBlock(txns, scriptPubKey, *chainstate); std::shared_ptr shared_pblock = std::make_shared(block); - Assert(m_node.chainman)->ProcessNewBlock(shared_pblock, true, nullptr); + Assert(m_node.chainman)->ProcessNewBlock(shared_pblock, true, true, nullptr); return block; } diff --git a/src/test/util_tests.cpp b/src/test/util_tests.cpp index 2da64caf6576..d52c93c34549 100644 --- a/src/test/util_tests.cpp +++ b/src/test/util_tests.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include diff --git a/src/test/validation_block_tests.cpp b/src/test/validation_block_tests.cpp index 88867be2e75d..3b1c6dad2422 100644 --- a/src/test/validation_block_tests.cpp +++ b/src/test/validation_block_tests.cpp @@ -99,7 +99,7 @@ std::shared_ptr MinerTestingSetup::FinalizeBlock(std::shared_ptr // submit block header, so that miner can get the block height from the // global state and the node has the topology of the chain BlockValidationState ignored; - BOOST_CHECK(Assert(m_node.chainman)->ProcessNewBlockHeaders({pblock->GetBlockHeader()}, ignored)); + BOOST_CHECK(Assert(m_node.chainman)->ProcessNewBlockHeaders({pblock->GetBlockHeader()}, true, ignored)); return pblock; } @@ -156,7 +156,7 @@ BOOST_AUTO_TEST_CASE(processnewblock_signals_ordering) bool ignored; // Connect the genesis block and drain any outstanding events - BOOST_CHECK(Assert(m_node.chainman)->ProcessNewBlock(std::make_shared(Params().GenesisBlock()), true, &ignored)); + BOOST_CHECK(Assert(m_node.chainman)->ProcessNewBlock(std::make_shared(Params().GenesisBlock()), true, true, &ignored)); SyncWithValidationInterfaceQueue(); // subscribe to events (this subscriber will validate event ordering) @@ -178,13 +178,13 @@ BOOST_AUTO_TEST_CASE(processnewblock_signals_ordering) FastRandomContext insecure; for (int i = 0; i < 1000; i++) { const auto& block = blocks[insecure.randrange(blocks.size() - 1)]; - Assert(m_node.chainman)->ProcessNewBlock(block, true, &ignored); + Assert(m_node.chainman)->ProcessNewBlock(block, true, true, &ignored); } // to make sure that eventually we process the full chain - do it here for (const auto& block : blocks) { if (block->vtx.size() == 1) { - bool processed = Assert(m_node.chainman)->ProcessNewBlock(block, true, &ignored); + bool processed = Assert(m_node.chainman)->ProcessNewBlock(block, true, true, &ignored); assert(processed); } } @@ -211,7 +211,7 @@ BOOST_AUTO_TEST_CASE(checkblock_accept_known_hash) { bool ignored; BOOST_REQUIRE(Assert(m_node.chainman)->ProcessNewBlock( - std::make_shared(Params().GenesisBlock()), true, &ignored)); + std::make_shared(Params().GenesisBlock()), true, true, &ignored)); auto good = GoodBlock(Params().GenesisBlock().GetHash()); const uint256 hash{good->GetHash()}; @@ -227,7 +227,7 @@ BOOST_AUTO_TEST_CASE(checkblock_accept_known_hash) bool newblock = false; BOOST_REQUIRE(m_node.chainman->ActiveChainstate().AcceptBlock( good, state, &pindex, /*fRequested=*/true, - /*dbp=*/nullptr, &newblock, &hash)); + /*dbp=*/nullptr, &newblock, true, &hash)); BOOST_REQUIRE(state.IsValid()); BOOST_REQUIRE(newblock); BOOST_REQUIRE(pindex != nullptr); @@ -256,7 +256,7 @@ BOOST_AUTO_TEST_CASE(mempool_locks_reorg) { bool ignored; auto ProcessBlock = [&](std::shared_ptr block) -> bool { - return Assert(m_node.chainman)->ProcessNewBlock(block, /*force_processing=*/true, /*new_block=*/&ignored); + return Assert(m_node.chainman)->ProcessNewBlock(block, /*force_processing=*/true, /*min_pow_checked=*/true, /*new_block=*/&ignored); }; // Process all mined blocks diff --git a/src/test/validation_chainstate_tests.cpp b/src/test/validation_chainstate_tests.cpp index d1a3fda4539f..4a3410ddb688 100644 --- a/src/test/validation_chainstate_tests.cpp +++ b/src/test/validation_chainstate_tests.cpp @@ -134,7 +134,7 @@ BOOST_FIXTURE_TEST_CASE(chainstate_update_tip, TestChain100Setup) bool checked = CheckBlock(*pblock, state, chainparams.GetConsensus()); BOOST_CHECK(checked); bool accepted = background_cs.AcceptBlock( - pblock, state, &pindex, true, nullptr, &newblock); + pblock, state, &pindex, true, nullptr, &newblock, true); BOOST_CHECK(accepted); } // UpdateTip is called here diff --git a/src/util/bitdeque.h b/src/util/bitdeque.h new file mode 100644 index 000000000000..1e34b7247563 --- /dev/null +++ b/src/util/bitdeque.h @@ -0,0 +1,469 @@ +// Copyright (c) 2022 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_UTIL_BITDEQUE_H +#define BITCOIN_UTIL_BITDEQUE_H + +#include +#include +#include +#include +#include +#include + +/** Class that mimics std::deque, but with std::vector's bit packing. + * + * BlobSize selects the (minimum) number of bits that are allocated at once. + * Larger values reduce the asymptotic memory usage overhead, at the cost of + * needing larger up-front allocations. The default is 4096 bytes. + */ +template +class bitdeque +{ + // Internal definitions + using word_type = std::bitset; + using deque_type = std::deque; + static_assert(BlobSize > 0); + static constexpr int BITS_PER_WORD = BlobSize; + + // Forward and friend declarations of iterator types. + template class Iterator; + template friend class Iterator; + + /** Iterator to a bitdeque element, const or not. */ + template + class Iterator + { + using deque_iterator = std::conditional_t; + + deque_iterator m_it; + int m_bitpos{0}; + Iterator(const deque_iterator& it, int bitpos) : m_it(it), m_bitpos(bitpos) {} + friend class bitdeque; + + public: + using iterator_category = std::random_access_iterator_tag; + using value_type = bool; + using pointer = void; + using const_pointer = void; + using reference = std::conditional_t; + using const_reference = bool; + using difference_type = std::ptrdiff_t; + + /** Default constructor. */ + Iterator() = default; + + /** Default copy constructor. */ + Iterator(const Iterator&) = default; + + /** Conversion from non-const to const iterator. */ + template> + Iterator(const Iterator& x) : m_it(x.m_it), m_bitpos(x.m_bitpos) {} + + Iterator& operator+=(difference_type dist) + { + if (dist > 0) { + if (dist + m_bitpos >= BITS_PER_WORD) { + ++m_it; + dist -= BITS_PER_WORD - m_bitpos; + m_bitpos = 0; + } + auto jump = dist / BITS_PER_WORD; + m_it += jump; + m_bitpos += dist - jump * BITS_PER_WORD; + } else if (dist < 0) { + dist = -dist; + if (dist > m_bitpos) { + --m_it; + dist -= m_bitpos + 1; + m_bitpos = BITS_PER_WORD - 1; + } + auto jump = dist / BITS_PER_WORD; + m_it -= jump; + m_bitpos -= dist - jump * BITS_PER_WORD; + } + return *this; + } + + friend difference_type operator-(const Iterator& x, const Iterator& y) + { + return BITS_PER_WORD * (x.m_it - y.m_it) + x.m_bitpos - y.m_bitpos; + } + + Iterator& operator=(const Iterator&) = default; + Iterator& operator-=(difference_type dist) { return operator+=(-dist); } + Iterator& operator++() { ++m_bitpos; if (m_bitpos == BITS_PER_WORD) { m_bitpos = 0; ++m_it; }; return *this; } + Iterator operator++(int) { auto ret{*this}; operator++(); return ret; } + Iterator& operator--() { if (m_bitpos == 0) { m_bitpos = BITS_PER_WORD; --m_it; }; --m_bitpos; return *this; } + Iterator operator--(int) { auto ret{*this}; operator--(); return ret; } + friend Iterator operator+(Iterator x, difference_type dist) { x += dist; return x; } + friend Iterator operator+(difference_type dist, Iterator x) { x += dist; return x; } + friend Iterator operator-(Iterator x, difference_type dist) { x -= dist; return x; } + friend bool operator<(const Iterator& x, const Iterator& y) { return std::tie(x.m_it, x.m_bitpos) < std::tie(y.m_it, y.m_bitpos); } + friend bool operator>(const Iterator& x, const Iterator& y) { return std::tie(x.m_it, x.m_bitpos) > std::tie(y.m_it, y.m_bitpos); } + friend bool operator<=(const Iterator& x, const Iterator& y) { return std::tie(x.m_it, x.m_bitpos) <= std::tie(y.m_it, y.m_bitpos); } + friend bool operator>=(const Iterator& x, const Iterator& y) { return std::tie(x.m_it, x.m_bitpos) >= std::tie(y.m_it, y.m_bitpos); } + friend bool operator==(const Iterator& x, const Iterator& y) { return x.m_it == y.m_it && x.m_bitpos == y.m_bitpos; } + friend bool operator!=(const Iterator& x, const Iterator& y) { return x.m_it != y.m_it || x.m_bitpos != y.m_bitpos; } + reference operator*() const { return (*m_it)[m_bitpos]; } + reference operator[](difference_type pos) const { return *(*this + pos); } + }; + +public: + using value_type = bool; + using size_type = std::size_t; + using difference_type = typename deque_type::difference_type; + using reference = typename word_type::reference; + using const_reference = bool; + using iterator = Iterator; + using const_iterator = Iterator; + using pointer = void; + using const_pointer = void; + using reverse_iterator = std::reverse_iterator; + using const_reverse_iterator = std::reverse_iterator; + +private: + /** Deque of bitsets storing the actual bit data. */ + deque_type m_deque; + + /** Number of unused bits at the front of m_deque.front(). */ + int m_pad_begin; + + /** Number of unused bits at the back of m_deque.back(). */ + int m_pad_end; + + /** Shrink the container by n bits, removing from the end. */ + void erase_back(size_type n) + { + if (n >= static_cast(BITS_PER_WORD - m_pad_end)) { + n -= BITS_PER_WORD - m_pad_end; + m_pad_end = 0; + m_deque.erase(m_deque.end() - 1 - (n / BITS_PER_WORD), m_deque.end()); + n %= BITS_PER_WORD; + } + if (n) { + auto& last = m_deque.back(); + while (n) { + last.reset(BITS_PER_WORD - 1 - m_pad_end); + ++m_pad_end; + --n; + } + } + } + + /** Extend the container by n bits, adding at the end. */ + void extend_back(size_type n) + { + if (n > static_cast(m_pad_end)) { + n -= m_pad_end + 1; + m_pad_end = BITS_PER_WORD - 1; + m_deque.insert(m_deque.end(), 1 + (n / BITS_PER_WORD), {}); + n %= BITS_PER_WORD; + } + m_pad_end -= n; + } + + /** Shrink the container by n bits, removing from the beginning. */ + void erase_front(size_type n) + { + if (n >= static_cast(BITS_PER_WORD - m_pad_begin)) { + n -= BITS_PER_WORD - m_pad_begin; + m_pad_begin = 0; + m_deque.erase(m_deque.begin(), m_deque.begin() + 1 + (n / BITS_PER_WORD)); + n %= BITS_PER_WORD; + } + if (n) { + auto& first = m_deque.front(); + while (n) { + first.reset(m_pad_begin); + ++m_pad_begin; + --n; + } + } + } + + /** Extend the container by n bits, adding at the beginning. */ + void extend_front(size_type n) + { + if (n > static_cast(m_pad_begin)) { + n -= m_pad_begin + 1; + m_pad_begin = BITS_PER_WORD - 1; + m_deque.insert(m_deque.begin(), 1 + (n / BITS_PER_WORD), {}); + n %= BITS_PER_WORD; + } + m_pad_begin -= n; + } + + /** Insert a sequence of falses anywhere in the container. */ + void insert_zeroes(size_type before, size_type count) + { + size_type after = size() - before; + if (before < after) { + extend_front(count); + std::move(begin() + count, begin() + count + before, begin()); + } else { + extend_back(count); + std::move_backward(begin() + before, begin() + before + after, end()); + } + } + +public: + /** Construct an empty container. */ + explicit bitdeque() : m_pad_begin{0}, m_pad_end{0} {} + + /** Set the container equal to count times the value of val. */ + void assign(size_type count, bool val) + { + m_deque.clear(); + m_deque.resize((count + BITS_PER_WORD - 1) / BITS_PER_WORD); + m_pad_begin = 0; + m_pad_end = 0; + if (val) { + for (auto& elem : m_deque) elem.flip(); + } + if (count % BITS_PER_WORD) { + erase_back(BITS_PER_WORD - (count % BITS_PER_WORD)); + } + } + + /** Construct a container containing count times the value of val. */ + bitdeque(size_type count, bool val) { assign(count, val); } + + /** Construct a container containing count false values. */ + explicit bitdeque(size_t count) { assign(count, false); } + + /** Copy constructor. */ + bitdeque(const bitdeque&) = default; + + /** Move constructor. */ + bitdeque(bitdeque&&) noexcept = default; + + /** Copy assignment operator. */ + bitdeque& operator=(const bitdeque& other) = default; + + /** Move assignment operator. */ + bitdeque& operator=(bitdeque&& other) noexcept = default; + + // Iterator functions. + iterator begin() noexcept { return {m_deque.begin(), m_pad_begin}; } + iterator end() noexcept { return iterator{m_deque.end(), 0} - m_pad_end; } + const_iterator begin() const noexcept { return const_iterator{m_deque.cbegin(), m_pad_begin}; } + const_iterator cbegin() const noexcept { return const_iterator{m_deque.cbegin(), m_pad_begin}; } + const_iterator end() const noexcept { return const_iterator{m_deque.cend(), 0} - m_pad_end; } + const_iterator cend() const noexcept { return const_iterator{m_deque.cend(), 0} - m_pad_end; } + reverse_iterator rbegin() noexcept { return reverse_iterator{end()}; } + reverse_iterator rend() noexcept { return reverse_iterator{begin()}; } + const_reverse_iterator rbegin() const noexcept { return const_reverse_iterator{cend()}; } + const_reverse_iterator crbegin() const noexcept { return const_reverse_iterator{cend()}; } + const_reverse_iterator rend() const noexcept { return const_reverse_iterator{cbegin()}; } + const_reverse_iterator crend() const noexcept { return const_reverse_iterator{cbegin()}; } + + /** Count the number of bits in the container. */ + size_type size() const noexcept { return m_deque.size() * BITS_PER_WORD - m_pad_begin - m_pad_end; } + + /** Determine whether the container is empty. */ + bool empty() const noexcept + { + return m_deque.size() == 0 || (m_deque.size() == 1 && (m_pad_begin + m_pad_end == BITS_PER_WORD)); + } + + /** Return the maximum size of the container. */ + size_type max_size() const noexcept + { + if (m_deque.max_size() < std::numeric_limits::max() / BITS_PER_WORD) { + return m_deque.max_size() * BITS_PER_WORD; + } else { + return std::numeric_limits::max(); + } + } + + /** Set the container equal to the bits in [first,last). */ + template + void assign(It first, It last) + { + size_type count = std::distance(first, last); + assign(count, false); + auto it = begin(); + while (first != last) { + *(it++) = *(first++); + } + } + + /** Set the container equal to the bits in ilist. */ + void assign(std::initializer_list ilist) + { + assign(ilist.size(), false); + auto it = begin(); + auto init = ilist.begin(); + while (init != ilist.end()) { + *(it++) = *(init++); + } + } + + /** Set the container equal to the bits in ilist. */ + bitdeque& operator=(std::initializer_list ilist) + { + assign(ilist); + return *this; + } + + /** Construct a container containing the bits in [first,last). */ + template + bitdeque(It first, It last) { assign(first, last); } + + /** Construct a container containing the bits in ilist. */ + bitdeque(std::initializer_list ilist) { assign(ilist); } + + // Access an element of the container, with bounds checking. + reference at(size_type position) + { + if (position >= size()) throw std::out_of_range("bitdeque::at() out of range"); + return begin()[position]; + } + const_reference at(size_type position) const + { + if (position >= size()) throw std::out_of_range("bitdeque::at() out of range"); + return cbegin()[position]; + } + + // Access elements of the container without bounds checking. + reference operator[](size_type position) { return begin()[position]; } + const_reference operator[](size_type position) const { return cbegin()[position]; } + reference front() { return *begin(); } + const_reference front() const { return *cbegin(); } + reference back() { return end()[-1]; } + const_reference back() const { return cend()[-1]; } + + /** Release unused memory. */ + void shrink_to_fit() + { + m_deque.shrink_to_fit(); + } + + /** Empty the container. */ + void clear() noexcept + { + m_deque.clear(); + m_pad_begin = m_pad_end = 0; + } + + // Append an element to the container. + void push_back(bool val) + { + extend_back(1); + back() = val; + } + reference emplace_back(bool val) + { + extend_back(1); + auto ref = back(); + ref = val; + return ref; + } + + // Prepend an element to the container. + void push_front(bool val) + { + extend_front(1); + front() = val; + } + reference emplace_front(bool val) + { + extend_front(1); + auto ref = front(); + ref = val; + return ref; + } + + // Remove the last element from the container. + void pop_back() + { + erase_back(1); + } + + // Remove the first element from the container. + void pop_front() + { + erase_front(1); + } + + /** Resize the container. */ + void resize(size_type n) + { + if (n < size()) { + erase_back(size() - n); + } else { + extend_back(n - size()); + } + } + + // Swap two containers. + void swap(bitdeque& other) noexcept + { + std::swap(m_deque, other.m_deque); + std::swap(m_pad_begin, other.m_pad_begin); + std::swap(m_pad_end, other.m_pad_end); + } + friend void swap(bitdeque& b1, bitdeque& b2) noexcept { b1.swap(b2); } + + // Erase elements from the container. + iterator erase(const_iterator first, const_iterator last) + { + size_type before = std::distance(cbegin(), first); + size_type dist = std::distance(first, last); + size_type after = std::distance(last, cend()); + if (before < after) { + std::move_backward(begin(), begin() + before, end() - after); + erase_front(dist); + return begin() + before; + } else { + std::move(end() - after, end(), begin() + before); + erase_back(dist); + return end() - after; + } + } + + iterator erase(iterator first, iterator last) { return erase(const_iterator{first}, const_iterator{last}); } + iterator erase(const_iterator pos) { return erase(pos, pos + 1); } + iterator erase(iterator pos) { return erase(const_iterator{pos}, const_iterator{pos} + 1); } + + // Insert elements into the container. + iterator insert(const_iterator pos, bool val) + { + size_type before = pos - cbegin(); + insert_zeroes(before, 1); + auto it = begin() + before; + *it = val; + return it; + } + + iterator emplace(const_iterator pos, bool val) { return insert(pos, val); } + + iterator insert(const_iterator pos, size_type count, bool val) + { + size_type before = pos - cbegin(); + insert_zeroes(before, count); + auto it_begin = begin() + before; + auto it = it_begin; + auto it_end = it + count; + while (it != it_end) *(it++) = val; + return it_begin; + } + + template + iterator insert(const_iterator pos, It first, It last) + { + size_type before = pos - cbegin(); + size_type count = std::distance(first, last); + insert_zeroes(before, count); + auto it_begin = begin() + before; + auto it = it_begin; + while (first != last) { + *(it++) = *(first++); + } + return it_begin; + } +}; + +#endif // BITCOIN_UTIL_BITDEQUE_H diff --git a/src/validation.cpp b/src/validation.cpp index 2a16f339b631..ff5d17404644 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -3439,7 +3439,7 @@ static bool NotifyHeaderTip(CChainState& chainstate) LOCKS_EXCLUDED(cs_main) { } // Send block tip changed notifications without cs_main if (fNotify) { - uiInterface.NotifyHeaderTip(GetSynchronizationState(fInitialBlockDownload), pindexHeader); + uiInterface.NotifyHeaderTip(GetSynchronizationState(fInitialBlockDownload), pindexHeader->nHeight, pindexHeader->nTime, false); GetMainSignals().NotifyHeaderTip(pindexHeader, fInitialBlockDownload); } return fNotify; @@ -4058,6 +4058,22 @@ bool CheckBlock(const CBlock& block, BlockValidationState& state, const Consensu return true; } +bool HasValidProofOfWork(const std::vector& headers, const Consensus::Params& consensusParams) +{ + return std::all_of(headers.cbegin(), headers.cend(), + [&](const auto& header) { return CheckProofOfWork(header.GetHash(), header.nBits, consensusParams);}); +} + +arith_uint256 CalculateHeadersWork(const std::vector& headers) +{ + arith_uint256 total_work{0}; + for (const CBlockHeader& header : headers) { + CBlockIndex dummy(header); + total_work += GetBlockProof(dummy); + } + return total_work; +} + /** Context-dependent validity checks. * By "context", we mean only the previous block headers, but not the UTXO * set; UTXO-related validity checks are done in ConnectBlock(). @@ -4195,7 +4211,7 @@ static bool ContextualCheckBlock(const CBlock& block, BlockValidationState& stat return true; } -bool ChainstateManager::AcceptBlockHeader(const CBlockHeader& block, BlockValidationState& state, CBlockIndex** ppindex, const uint256& hash) +bool ChainstateManager::AcceptBlockHeader(const CBlockHeader& block, BlockValidationState& state, CBlockIndex** ppindex, bool min_pow_checked, const uint256& hash) { AssertLockHeld(cs_main); @@ -4298,6 +4314,10 @@ bool ChainstateManager::AcceptBlockHeader(const CBlockHeader& block, BlockValida return state.Invalid(BlockValidationResult::BLOCK_CHAINLOCK, "bad-chainlock"); } } + if (!min_pow_checked) { + LogPrint(BCLog::VALIDATION, "%s: not adding new block header %s, missing anti-dos proof-of-work validation\n", __func__, hash.ToString()); + return state.Invalid(BlockValidationResult::BLOCK_HEADER_LOW_WORK, "too-little-chainwork"); + } CBlockIndex* pindex{m_blockman.AddToBlockIndex(block, hash, m_best_header)}; if (ppindex) @@ -4326,14 +4346,14 @@ bool ChainstateManager::AcceptBlockHeader(const CBlockHeader& block, BlockValida } // Exposed wrapper for AcceptBlockHeader -bool ChainstateManager::ProcessNewBlockHeaders(const std::vector& headers, BlockValidationState& state, const CBlockIndex** ppindex) +bool ChainstateManager::ProcessNewBlockHeaders(const std::vector& headers, bool min_pow_checked, BlockValidationState& state, const CBlockIndex** ppindex) { AssertLockNotHeld(cs_main); { LOCK(cs_main); for (const CBlockHeader& header : headers) { CBlockIndex *pindex = nullptr; // Use a temp pindex instead of ppindex to avoid a const_cast - bool accepted{AcceptBlockHeader(header, state, &pindex, header.GetHash())}; + bool accepted{AcceptBlockHeader(header, state, &pindex, min_pow_checked, header.GetHash())}; ActiveChainstate().CheckBlockIndex(); if (!accepted) { @@ -4355,8 +4375,33 @@ bool ChainstateManager::ProcessNewBlockHeaders(const std::vector& return true; } +void ChainstateManager::ReportHeadersPresync(const arith_uint256& work, int64_t height, int64_t timestamp) +{ + AssertLockNotHeld(cs_main); + const auto& chainstate = ActiveChainstate(); + { + LOCK(cs_main); + // Don't report headers presync progress if we already have a post-minchainwork header chain. + // This means we lose reporting for potentially legimate, but unlikely, deep reorgs, but + // prevent attackers that spam low-work headers from filling our logs. + if (m_best_header->nChainWork >= UintToArith256(GetConsensus().nMinimumChainWork)) return; + // Rate limit headers presync updates to 4 per second, as these are not subject to DoS + // protection. + auto now = std::chrono::steady_clock::now(); + if (now < m_last_presync_update + std::chrono::milliseconds{250}) return; + m_last_presync_update = now; + } + bool initial_download = chainstate.IsInitialBlockDownload(); + uiInterface.NotifyHeaderTip(GetSynchronizationState(initial_download), height, timestamp, /*presync=*/true); + if (initial_download) { + const int64_t blocks_left{(GetTime() - timestamp) / GetConsensus().nPowTargetSpacing}; + const double progress{100.0 * height / (height + blocks_left)}; + LogPrintf("Pre-synchronizing blockheaders, height: %d (~%.2f%%)\n", height, progress); + } +} + /** Store block on disk. If dbp is non-nullptr, the file is known to already reside on disk */ -bool CChainState::AcceptBlock(const std::shared_ptr& pblock, BlockValidationState& state, CBlockIndex** ppindex, bool fRequested, const FlatFilePos* dbp, bool* fNewBlock, const uint256* known_hash) +bool CChainState::AcceptBlock(const std::shared_ptr& pblock, BlockValidationState& state, CBlockIndex** ppindex, bool fRequested, const FlatFilePos* dbp, bool* fNewBlock, bool min_pow_checked, const uint256* known_hash) { auto start = Now(); @@ -4374,7 +4419,7 @@ bool CChainState::AcceptBlock(const std::shared_ptr& pblock, Block ASSERT_IF_DEBUG(!known_hash || *known_hash == block.GetHash()); const uint256 hash{known_hash ? *known_hash : block.GetHash()}; - bool accepted_header{m_chainman.AcceptBlockHeader(block, state, &pindex, hash)}; + bool accepted_header{m_chainman.AcceptBlockHeader(block, state, &pindex, min_pow_checked, hash)}; CheckBlockIndex(); if (!accepted_header) @@ -4453,7 +4498,7 @@ bool CChainState::AcceptBlock(const std::shared_ptr& pblock, Block return true; } -bool ChainstateManager::ProcessNewBlock(const std::shared_ptr& block, bool force_processing, bool* new_block) +bool ChainstateManager::ProcessNewBlock(const std::shared_ptr& block, bool force_processing, bool min_pow_checked, bool* new_block) { AssertLockNotHeld(cs_main); @@ -4474,7 +4519,7 @@ bool ChainstateManager::ProcessNewBlock(const std::shared_ptr& blo bool ret = CheckBlock(*block, state, GetConsensus()); if (ret) { // Store to disk - ret = ActiveChainstate().AcceptBlock(block, state, &pindex, force_processing, nullptr, new_block); + ret = ActiveChainstate().AcceptBlock(block, state, &pindex, force_processing, nullptr, new_block, min_pow_checked); } if (!ret) { GetMainSignals().BlockChecked(*block, state); @@ -5075,7 +5120,7 @@ bool CChainState::LoadGenesisBlock() m_params.DevNetGenesisBlock()); bool fCheckBlock = CheckBlock(*shared_pblock, state, m_params.GetConsensus()); assert(fCheckBlock); - if (!AcceptBlock(shared_pblock, state, nullptr, true, nullptr, nullptr)) + if (!AcceptBlock(shared_pblock, state, nullptr, true, nullptr, nullptr, true)) return false; } } catch (const std::runtime_error &e) { @@ -5168,7 +5213,7 @@ void CChainState::LoadExternalBlockFile( nRewind = blkdat.GetPos(); BlockValidationState state; - if (AcceptBlock(pblock, state, nullptr, true, dbp, nullptr, &hash)) { + if (AcceptBlock(pblock, state, nullptr, true, dbp, nullptr, true, &hash)) { nLoaded++; } if (state.IsError()) { @@ -5222,7 +5267,7 @@ void CChainState::LoadExternalBlockFile( head.ToString()); LOCK(cs_main); BlockValidationState dummy; - if (AcceptBlock(pblockrecursive, dummy, nullptr, true, &it->second, nullptr, &blockhash)) { + if (AcceptBlock(pblockrecursive, dummy, nullptr, true, &it->second, nullptr, true, &blockhash)) { nLoaded++; queue.push_back(blockhash); } diff --git a/src/validation.h b/src/validation.h index 666773994265..61002eb70f90 100644 --- a/src/validation.h +++ b/src/validation.h @@ -384,6 +384,12 @@ bool TestBlockValidity(BlockValidationState& state, bool fCheckPOW = true, bool fCheckMerkleRoot = true) EXCLUSIVE_LOCKS_REQUIRED(cs_main); +/** Check with the proof of work on each blockheader matches the value in nBits */ +bool HasValidProofOfWork(const std::vector& headers, const Consensus::Params& consensusParams); + +/** Return the sum of the work on a given set of headers */ +arith_uint256 CalculateHeadersWork(const std::vector& headers); + /** RAII wrapper for VerifyDB: Verify consistency of the block and coin databases */ class CVerifyDB { public: @@ -705,7 +711,7 @@ class CChainState EXCLUSIVE_LOCKS_REQUIRED(!m_chainstate_mutex) LOCKS_EXCLUDED(::cs_main); - bool AcceptBlock(const std::shared_ptr& pblock, BlockValidationState& state, CBlockIndex** ppindex, bool fRequested, const FlatFilePos* dbp, bool* fNewBlock, const uint256* known_hash = nullptr) EXCLUSIVE_LOCKS_REQUIRED(cs_main); + bool AcceptBlock(const std::shared_ptr& pblock, BlockValidationState& state, CBlockIndex** ppindex, bool fRequested, const FlatFilePos* dbp, bool* fNewBlock, bool min_pow_checked, const uint256* known_hash = nullptr) EXCLUSIVE_LOCKS_REQUIRED(cs_main); // Block (dis)connection on a given view: DisconnectResult DisconnectBlock(const CBlock& block, const CBlockIndex* pindex, CCoinsViewCache& view) EXCLUSIVE_LOCKS_REQUIRED(cs_main); @@ -911,14 +917,21 @@ class ChainstateManager /** * If a block header hasn't already been seen, call CheckBlockHeader on it, ensure * that it doesn't descend from an invalid block, and then add it to m_block_index. + * Caller must set min_pow_checked=true in order to add a new header to the + * block index (permanent memory storage), indicating that the header is + * known to be part of a sufficiently high-work chain (anti-dos check). */ bool AcceptBlockHeader( const CBlockHeader& block, BlockValidationState& state, CBlockIndex** ppindex, + bool min_pow_checked, const uint256& hash) EXCLUSIVE_LOCKS_REQUIRED(cs_main); friend CChainState; + /** Most recent headers presync progress update, for rate-limiting. */ + std::chrono::time_point m_last_presync_update GUARDED_BY(::cs_main) {}; + public: explicit ChainstateManager(const CChainParams& chainparams) : m_chainparams{chainparams}, m_blockman{{chainparams}} { } @@ -1041,10 +1054,15 @@ class ChainstateManager * * @param[in] block The block we want to process. * @param[in] force_processing Process this block even if unrequested; used for non-network block sources. + * @param[in] min_pow_checked True if proof-of-work anti-DoS checks have + * been done by caller for headers chain + * (note: only affects headers acceptance; if + * block header is already present in block + * index then this parameter has no effect) * @param[out] new_block A boolean which is set to indicate if the block was first received via this call * @returns If the block was processed, independently of block validity */ - bool ProcessNewBlock(const std::shared_ptr& block, bool force_processing, bool* new_block) LOCKS_EXCLUDED(cs_main); + bool ProcessNewBlock(const std::shared_ptr& block, bool force_processing, bool min_pow_checked, bool* new_block) LOCKS_EXCLUDED(cs_main); /** * Process incoming block headers. @@ -1053,10 +1071,11 @@ class ChainstateManager * validationinterface callback. * * @param[in] block The block headers themselves + * @param[in] min_pow_checked True if proof-of-work anti-DoS checks have been done by caller for headers chain * @param[out] state This may be set to an Error state if any error occurred processing them * @param[out] ppindex If set, the pointer will be set to point to the last new block index object for the given headers */ - bool ProcessNewBlockHeaders(const std::vector& block, BlockValidationState& state, const CBlockIndex** ppindex = nullptr) LOCKS_EXCLUDED(cs_main); + bool ProcessNewBlockHeaders(const std::vector& block, bool min_pow_checked, BlockValidationState& state, const CBlockIndex** ppindex = nullptr) LOCKS_EXCLUDED(cs_main); /** * Try to add a transaction to the memory pool. @@ -1081,6 +1100,12 @@ class ChainstateManager std::optional optDIP0024IsActive = std::nullopt, std::optional optHaveDIP0024Quorums = std::nullopt) const; + /** This is used by net_processing to report pre-synchronization progress of headers, as + * headers are not yet fed to validation during that time, but validation is (for now) + * responsible for logging and signalling through NotifyHeaderTip, so it needs this + * information. */ + void ReportHeadersPresync(const arith_uint256& work, int64_t height, int64_t timestamp); + ~ChainstateManager(); }; diff --git a/test/functional/feature_block.py b/test/functional/feature_block.py index 12caa6340de0..f830cc9383e0 100755 --- a/test/functional/feature_block.py +++ b/test/functional/feature_block.py @@ -1306,7 +1306,7 @@ def run_test(self): blocks2 = [] for i in range(89, LARGE_REORG_SIZE + 89): blocks2.append(self.next_block("alt" + str(i))) - self.send_blocks(blocks2, False, force_send=True) + self.send_blocks(blocks2, False, force_send=False) # extend alt chain to trigger re-org block = self.next_block("alt" + str(chain1_tip + 1)) diff --git a/test/functional/p2p_compactblocks.py b/test/functional/p2p_compactblocks.py index b0ee1e2a09d6..28af468cf648 100755 --- a/test/functional/p2p_compactblocks.py +++ b/test/functional/p2p_compactblocks.py @@ -603,6 +603,27 @@ def test_getblocktxn_handler(self, test_node): bad_peer.send_message(msg) bad_peer.wait_for_disconnect() + def test_low_work_compactblocks(self, test_node): + # A compactblock with insufficient work won't get its header included + node = self.nodes[0] + hashPrevBlock = int(node.getblockhash(node.getblockcount() - 150), 16) + block = self.build_block_on_tip(node) + block.hashPrevBlock = hashPrevBlock + block.solve() + + comp_block = HeaderAndShortIDs() + comp_block.initialize_from_block(block) + with self.nodes[0].assert_debug_log(['[net] Ignoring low-work compact block from peer 0']): + test_node.send_and_ping(msg_cmpctblock(comp_block.to_p2p())) + + tips = node.getchaintips() + found = False + for x in tips: + if x["hash"] == block.hash: + found = True + break + assert not found + def test_compactblocks_not_at_tip(self, test_node): node = self.nodes[0] # Test that requesting old compactblocks doesn't work. @@ -810,6 +831,9 @@ def run_test(self): self.log.info("Testing compactblock requests/announcements not at chain tip...") self.test_compactblocks_not_at_tip(self.test_node) + self.log.info("Testing handling of low-work compact blocks...") + self.test_low_work_compactblocks(self.test_node) + self.log.info("Testing handling of incorrect blocktxn responses...") self.test_incorrect_blocktxn_response(self.test_node) diff --git a/test/functional/p2p_dos_header_tree.py b/test/functional/p2p_dos_header_tree.py index f3e855b24531..dfae96e517ca 100755 --- a/test/functional/p2p_dos_header_tree.py +++ b/test/functional/p2p_dos_header_tree.py @@ -24,7 +24,7 @@ def set_test_params(self): self.setup_clean_chain = True self.chain = 'testnet3' # Use testnet chain because it has an early checkpoint self.num_nodes = 2 - self.extra_args = [['-prune=945']] * self.num_nodes + self.extra_args = [['-prune=945', "-minimumchainwork=0x0"]] * self.num_nodes def add_options(self, parser): parser.add_argument( @@ -66,7 +66,7 @@ def run_test(self): self.log.info("Feed all fork headers (succeeds without checkpoint)") # On node 0 it succeeds because checkpoints are disabled - self.restart_node(0, extra_args=['-nocheckpoints', '-prune=945'], expected_stderr=EXPECTED_STDERR_NO_GOV_PRUNE) + self.restart_node(0, extra_args=['-nocheckpoints', '-prune=945', "-minimumchainwork=0x0"], expected_stderr=EXPECTED_STDERR_NO_GOV_PRUNE) peer_no_checkpoint = self.nodes[0].add_p2p_connection(P2PInterface()) peer_no_checkpoint.send_and_ping(msg_headers(self.headers_fork)) assert { diff --git a/test/functional/p2p_headers_sync_with_minchainwork.py b/test/functional/p2p_headers_sync_with_minchainwork.py new file mode 100755 index 000000000000..5c5a9d271680 --- /dev/null +++ b/test/functional/p2p_headers_sync_with_minchainwork.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python3 +# Copyright (c) 2019-2021 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 that we reject low difficulty headers to prevent our block tree from filling up with useless bloat""" + +from decimal import Decimal + +from test_framework.test_framework import BitcoinTestFramework + +from test_framework.p2p import ( + P2PInterface, +) + +from test_framework.messages import ( + msg_headers, +) + +from test_framework.blocktools import ( + NORMAL_GBT_REQUEST_PARAMS, + create_block, +) + +from test_framework.util import assert_equal + +NODE1_BLOCKS_REQUIRED = 15 +NODE2_BLOCKS_REQUIRED = 2047 + + +class RejectLowDifficultyHeadersTest(BitcoinTestFramework): + def set_test_params(self): + self.setup_clean_chain = True + self.num_nodes = 3 + # Node0 has no required chainwork; node1 requires 15 blocks on top of the genesis block; node2 requires 2047 + self.extra_args = [["-minimumchainwork=0x0", "-checkblockindex=0"], ["-minimumchainwork=0x1f", "-checkblockindex=0"], ["-minimumchainwork=0x1000", "-checkblockindex=0"]] + + def setup_network(self): + self.setup_nodes() + self.reconnect_all() + self.sync_all() + + def disconnect_all(self): + self.disconnect_nodes(0, 1) + self.disconnect_nodes(0, 2) + + def reconnect_all(self): + self.connect_nodes(0, 1) + self.connect_nodes(0, 2) + + def test_chains_sync_when_long_enough(self): + self.log.info("Generate blocks on the node with no required chainwork, and verify nodes 1 and 2 have no new headers in their headers tree") + with self.nodes[1].assert_debug_log(expected_msgs=["[net] Ignoring low-work chain (height=14)"]), self.nodes[2].assert_debug_log(expected_msgs=["[net] Ignoring low-work chain (height=14)"]): + self.generate(self.nodes[0], NODE1_BLOCKS_REQUIRED-1, sync_fun=self.no_op) + + for node in self.nodes[1:]: + chaintips = node.getchaintips() + assert(len(chaintips) == 1) + assert { + 'height': 0, + 'hash': '000008ca1832a4baf228eb1553c03d3a2c8e02399550dd6ea8d65cec3ef23d2e', + 'forkpoint': '000008ca1832a4baf228eb1553c03d3a2c8e02399550dd6ea8d65cec3ef23d2e', + 'difficulty': Decimal('4.656542373906925E-10'), + 'chainwork': '0000000000000000000000000000000000000000000000000000000000000002', + 'branchlen': 0, + 'status': 'active', + } in chaintips + + self.log.info("Generate more blocks to satisfy node1's minchainwork requirement, and verify node2 still has no new headers in headers tree") + with self.nodes[2].assert_debug_log(expected_msgs=["[net] Ignoring low-work chain (height=15)"]): + self.generate(self.nodes[0], NODE1_BLOCKS_REQUIRED - self.nodes[0].getblockcount(), sync_fun=self.no_op) + self.sync_blocks(self.nodes[0:2]) + + assert { + 'height': 0, + 'hash': '000008ca1832a4baf228eb1553c03d3a2c8e02399550dd6ea8d65cec3ef23d2e', + 'forkpoint': '000008ca1832a4baf228eb1553c03d3a2c8e02399550dd6ea8d65cec3ef23d2e', + 'difficulty': Decimal('4.656542373906925E-10'), + 'chainwork': '0000000000000000000000000000000000000000000000000000000000000002', + 'branchlen': 0, + 'status': 'active', + } in self.nodes[2].getchaintips() + + assert(len(self.nodes[2].getchaintips()) == 1) + + self.log.info("Generate long chain for node0/node1") + self.generate(self.nodes[0], NODE2_BLOCKS_REQUIRED-self.nodes[0].getblockcount(), sync_fun=self.no_op) + + self.log.info("Verify that node2 will sync the chain when it gets long enough") + self.sync_blocks() + + def test_peerinfo_includes_headers_presync_height(self): + self.log.info("Test that getpeerinfo() includes headers presync height") + + # Disconnect network, so that we can find our own peer connection more + # easily + self.disconnect_all() + + p2p = self.nodes[0].add_p2p_connection(P2PInterface()) + node = self.nodes[0] + + # Ensure we have a long chain already + current_height = self.nodes[0].getblockcount() + if (current_height < 3000): + self.generate(node, 3000-current_height, sync_fun=self.no_op) + + # Send a group of 2000 headers, forking from genesis. + new_blocks = [] + hashPrevBlock = int(node.getblockhash(0), 16) + for i in range(2000): + block = create_block(hashprev = hashPrevBlock, tmpl=node.getblocktemplate(NORMAL_GBT_REQUEST_PARAMS)) + block.solve() + new_blocks.append(block) + hashPrevBlock = block.sha256 + + headers_message = msg_headers(headers=new_blocks) + p2p.send_and_ping(headers_message) + + # getpeerinfo should show a sync in progress + assert_equal(node.getpeerinfo()[0]['presynced_headers'], 2000) + + def test_large_reorgs_can_succeed(self): + self.log.info("Test that a 2000+ block reorg, starting from a point that is more than 2000 blocks before a locator entry, can succeed") + + self.sync_all() # Ensure all nodes are synced. + self.disconnect_all() + + # locator(block at height T) will have heights: + # [T, T-1, ..., T-10, T-12, T-16, T-24, T-40, T-72, T-136, T-264, + # T-520, T-1032, T-2056, T-4104, ...] + # So mine a number of blocks > 4104 to ensure that the first window of + # received headers during a sync are fully between locator entries. + BLOCKS_TO_MINE = 4110 + + self.generate(self.nodes[0], BLOCKS_TO_MINE, sync_fun=self.no_op) + self.generate(self.nodes[1], BLOCKS_TO_MINE+2, sync_fun=self.no_op) + + self.reconnect_all() + + self.sync_blocks(timeout=300) # Ensure tips eventually agree + + + def run_test(self): + self.test_chains_sync_when_long_enough() + + self.test_large_reorgs_can_succeed() + + self.test_peerinfo_includes_headers_presync_height() + + + +if __name__ == '__main__': + RejectLowDifficultyHeadersTest().main() diff --git a/test/functional/p2p_unrequested_blocks.py b/test/functional/p2p_unrequested_blocks.py index 7bfc3f539a93..3d5f2b2e5070 100755 --- a/test/functional/p2p_unrequested_blocks.py +++ b/test/functional/p2p_unrequested_blocks.py @@ -68,6 +68,13 @@ def set_test_params(self): def setup_network(self): self.setup_nodes() + def check_hash_in_chaintips(self, node, blockhash): + tips = node.getchaintips() + for x in tips: + if x["hash"] == blockhash: + return True + return False + def run_test(self): test_node = self.nodes[0].add_p2p_connection(P2PInterface()) min_work_node = self.nodes[1].add_p2p_connection(P2PInterface()) @@ -85,10 +92,15 @@ def run_test(self): blocks_h2[i].solve() block_time += 1 test_node.send_and_ping(msg_block(blocks_h2[0])) - min_work_node.send_and_ping(msg_block(blocks_h2[1])) + + with self.nodes[1].assert_debug_log(expected_msgs=[f"AcceptBlockHeader: not adding new block header {blocks_h2[1].hash}, missing anti-dos proof-of-work validation"]): + min_work_node.send_and_ping(msg_block(blocks_h2[1])) assert_equal(self.nodes[0].getblockcount(), 2) assert_equal(self.nodes[1].getblockcount(), 1) + + # Ensure that the header of the second block was also not accepted by node1 + assert_equal(self.check_hash_in_chaintips(self.nodes[1], blocks_h2[1].hash), False) self.log.info("First height 2 block accepted by node0; correctly rejected by node1") # 3. Send another block that builds on genesis. diff --git a/test/functional/rpc_blockchain.py b/test/functional/rpc_blockchain.py index 2f74a60003ba..3dee277ab3d7 100755 --- a/test/functional/rpc_blockchain.py +++ b/test/functional/rpc_blockchain.py @@ -470,8 +470,9 @@ def _test_waitforblockheight(self): # (Previously this was broken based on setting # `rpc/blockchain.cpp:latestblock` incorrectly.) # - b20hash = node.getblockhash(20) - b20 = node.getblock(b20hash) + fork_height = current_height - 100 # choose something vaguely near our tip + fork_hash = node.getblockhash(fork_height) + fork_block = node.getblock(fork_hash) def solve_and_send_block(prevhash, height, time): b = create_block(prevhash, create_coinbase(height), time) @@ -479,10 +480,10 @@ def solve_and_send_block(prevhash, height, time): peer.send_and_ping(msg_block(b)) return b - b21f = solve_and_send_block(int(b20hash, 16), 21, b20['time'] + 1) - b22f = solve_and_send_block(b21f.sha256, 22, b21f.nTime + 1) + b1 = solve_and_send_block(int(fork_hash, 16), fork_height+1, fork_block['time'] + 1) + b2 = solve_and_send_block(b1.sha256, fork_height+2, b1.nTime + 1) - node.invalidateblock(b22f.hash) + node.invalidateblock(b2.hash) def assert_waitforheight(height, timeout=2): assert_equal( diff --git a/test/functional/rpc_net.py b/test/functional/rpc_net.py index 5813ebc3dab5..f7d32bc7248a 100755 --- a/test/functional/rpc_net.py +++ b/test/functional/rpc_net.py @@ -150,6 +150,7 @@ def test_getpeerinfo(self): "masternode": False, "network": "not_publicly_routable", "permissions": [], + 'presynced_headers': -1, "relaytxes": False, "services": "0000000000000000", "servicesnames": [], diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 026ed289447c..8537b854663d 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -221,6 +221,7 @@ 'wallet_signrawtransactionwithwallet.py --legacy-wallet', 'wallet_signrawtransactionwithwallet.py --descriptors', 'rpc_signrawtransactionwithkey.py', + 'p2p_headers_sync_with_minchainwork.py', 'rpc_rawtransaction.py --legacy-wallet', 'wallet_transactiontime_rescan.py --descriptors', 'wallet_transactiontime_rescan.py --legacy-wallet', From 5acd7f39dbefc94a5d684d64b2e069d79a18edc1 Mon Sep 17 00:00:00 2001 From: Konstantin Akimov Date: Wed, 31 Aug 2022 15:59:52 +0200 Subject: [PATCH 02/11] Merge bitcoin/bitcoin#25963: CBlockLocator: performance-move-const-arg Clang tidy fixup 6b24dfe24d2ed50ea0b06ce1919db3dc0e871a03 CBlockLocator: performance-move-const-arg Clang tidy fixups (Jon Atack) Pull request description: Fix Clang-tidy CI errors on master. See https://cirrus-ci.com/task/4806752200818688?logs=ci#L4696 for an example. ACKs for top commit: MarcoFalke: review ACK 6b24dfe24d2ed50ea0b06ce1919db3dc0e871a03 vasild: ACK 6b24dfe24d2ed50ea0b06ce1919db3dc0e871a03 Tree-SHA512: 7a67acf7b42da07b63fbb392236e9a7be8cf35c36e37ca980c4467fe8295c2eda8aef10f41a1e3036cd9ebece47fa957fc3256033f853bd6a97ce2ca42799a0a Co-authored-by: MacroFake --- src/chain.cpp | 2 +- src/primitives/block.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/chain.cpp b/src/chain.cpp index fb6455cbf271..d0982742fd1a 100644 --- a/src/chain.cpp +++ b/src/chain.cpp @@ -50,7 +50,7 @@ std::vector LocatorEntries(const CBlockIndex* index) CBlockLocator GetLocator(const CBlockIndex* index) { - return CBlockLocator{std::move(LocatorEntries(index))}; + return CBlockLocator{LocatorEntries(index)}; } CBlockLocator CChain::GetLocator() const diff --git a/src/primitives/block.h b/src/primitives/block.h index f33e44ad3904..bdeff68d9444 100644 --- a/src/primitives/block.h +++ b/src/primitives/block.h @@ -242,7 +242,7 @@ struct CBlockLocator CBlockLocator() {} - explicit CBlockLocator(const std::vector& vHaveIn) : vHave(vHaveIn) {} + explicit CBlockLocator(std::vector&& have) : vHave(std::move(have)) {} SERIALIZE_METHODS(CBlockLocator, obj) { From de6d652812bbddc5fb870879ff15e3fda5038c97 Mon Sep 17 00:00:00 2001 From: Konstantin Akimov Date: Thu, 1 Sep 2022 07:40:28 +0100 Subject: [PATCH 03/11] Merge bitcoin/bitcoin#25960: p2p: Headers-sync followups 94af3e43e20aa00b18e7a3f6d0f5fe3ad9494d97 Fix typo from PR25717 (Suhas Daftuar) e5982ecdc4650e9b6de38f190f3a97d792499e2a Bypass headers anti-DoS checks for NoBan peers (Suhas Daftuar) 132ed7eaaa4a47ab94db72ebfab0ef0e03caa488 Move headerssync logging to BCLog::NET (Suhas Daftuar) Pull request description: Remove BCLog::HEADERSSYNC and move all headerssync logging to BCLog::NET. Bypass headers anti-DoS checks for NoBan peers Also fix a typo that was introduced in PR25717. ACKs for top commit: Sjors: tACK 94af3e43e20aa00b18e7a3f6d0f5fe3ad9494d97 ajtowns: ACK 94af3e43e20aa00b18e7a3f6d0f5fe3ad9494d97 sipa: ACK 94af3e43e20aa00b18e7a3f6d0f5fe3ad9494d97 naumenkogs: ACK 94af3e43e20aa00b18e7a3f6d0f5fe3ad9494d97 w0xlt: ACK https://github.com/bitcoin/bitcoin/pull/25960/commits/94af3e43e20aa00b18e7a3f6d0f5fe3ad9494d97 Tree-SHA512: 612d594eddace977359bcc8234b2093d273fd50662f4ac70cb90903d28fb831f6e1aecff51a4ef6c0bb0f6fb5d1aa7ff1eb8798fac5ac142783788f3080717dc Co-authored-by: fanquake --- src/headerssync.cpp | 24 +++++----- src/logging.cpp | 3 -- src/logging.h | 1 - src/net_processing.cpp | 7 +++ src/validation.cpp | 2 +- .../p2p_headers_sync_with_minchainwork.py | 45 ++++++++++++++++--- 6 files changed, 59 insertions(+), 23 deletions(-) diff --git a/src/headerssync.cpp b/src/headerssync.cpp index f8a0538cbc2b..dc59f3b0a445 100644 --- a/src/headerssync.cpp +++ b/src/headerssync.cpp @@ -42,7 +42,7 @@ HeadersSyncState::HeadersSyncState(NodeId id, const Consensus::Params& consensus // could try again, if necessary, to sync a longer chain). m_max_commitments = 6*(Ticks(NodeClock::now() - NodeSeconds{std::chrono::seconds{chain_start->GetMedianTimePast()}}) + MAX_FUTURE_BLOCK_TIME) / HEADER_COMMITMENT_PERIOD; - LogPrint(BCLog::HEADERSSYNC, "Initial headers sync started with peer=%d: height=%i, max_commitments=%i, min_work=%s\n", m_id, m_current_height, m_max_commitments, m_minimum_required_work.ToString()); + LogPrint(BCLog::NET, "Initial headers sync started with peer=%d: height=%i, max_commitments=%i, min_work=%s\n", m_id, m_current_height, m_max_commitments, m_minimum_required_work.ToString()); } /** Free any memory in use, and mark this object as no longer usable. This is @@ -92,7 +92,7 @@ HeadersSyncState::ProcessingResult HeadersSyncState::ProcessNextHeaders(const // If we're in PRESYNC and we get a non-full headers // message, then the peer's chain has ended and definitely doesn't // have enough work, so we can stop our sync. - LogPrint(BCLog::HEADERSSYNC, "Initial headers sync aborted with peer=%d: incomplete headers message at height=%i (presync phase)\n", m_id, m_current_height); + LogPrint(BCLog::NET, "Initial headers sync aborted with peer=%d: incomplete headers message at height=%i (presync phase)\n", m_id, m_current_height); } } } else if (m_download_state == State::REDOWNLOAD) { @@ -118,7 +118,7 @@ HeadersSyncState::ProcessingResult HeadersSyncState::ProcessNextHeaders(const // If we hit our target blockhash, then all remaining headers will be // returned and we can clear any leftover internal state. if (m_redownloaded_headers.empty() && m_process_all_remaining_headers) { - LogPrint(BCLog::HEADERSSYNC, "Initial headers sync complete with peer=%d: releasing all at height=%i (redownload phase)\n", m_id, m_redownload_buffer_last_height); + LogPrint(BCLog::NET, "Initial headers sync complete with peer=%d: releasing all at height=%i (redownload phase)\n", m_id, m_redownload_buffer_last_height); } else if (full_headers_message) { // If the headers message is full, we need to request more. ret.request_more = true; @@ -127,7 +127,7 @@ HeadersSyncState::ProcessingResult HeadersSyncState::ProcessNextHeaders(const // declining to serve us that full chain again. Give up. // Note that there's no more processing to be done with these // headers, so we can still return success. - LogPrint(BCLog::HEADERSSYNC, "Initial headers sync aborted with peer=%d: incomplete headers message at height=%i (redownload phase)\n", m_id, m_redownload_buffer_last_height); + LogPrint(BCLog::NET, "Initial headers sync aborted with peer=%d: incomplete headers message at height=%i (redownload phase)\n", m_id, m_redownload_buffer_last_height); } } } @@ -150,7 +150,7 @@ bool HeadersSyncState::ValidateAndStoreHeadersCommitments(const std::vectorGetBlockHash(); m_redownload_chain_work = m_chain_start->nChainWork; m_download_state = State::REDOWNLOAD; - LogPrint(BCLog::HEADERSSYNC, "Initial headers sync transition with peer=%d: reached sufficient work at height=%i, redownloading from height=%i\n", m_id, m_current_height, m_redownload_buffer_last_height); + LogPrint(BCLog::NET, "Initial headers sync transition with peer=%d: reached sufficient work at height=%i, redownloading from height=%i\n", m_id, m_current_height, m_redownload_buffer_last_height); } return true; } @@ -188,7 +188,7 @@ bool HeadersSyncState::ValidateAndProcessSingleHeader(const CBlockHeader& curren // adjustment maximum. if (!PermittedDifficultyTransition(m_consensus_params, next_height, m_last_header_received.nBits, current.nBits)) { - LogPrint(BCLog::HEADERSSYNC, "Initial headers sync aborted with peer=%d: invalid difficulty transition at height=%i (presync phase)\n", m_id, next_height); + LogPrint(BCLog::NET, "Initial headers sync aborted with peer=%d: invalid difficulty transition at height=%i (presync phase)\n", m_id, next_height); return false; } @@ -200,7 +200,7 @@ bool HeadersSyncState::ValidateAndProcessSingleHeader(const CBlockHeader& curren // It's possible the chain grew since we started the sync; so // potentially we could succeed in syncing the peer's chain if we // try again later. - LogPrint(BCLog::HEADERSSYNC, "Initial headers sync aborted with peer=%d: exceeded max commitments at height=%i (presync phase)\n", m_id, next_height); + LogPrint(BCLog::NET, "Initial headers sync aborted with peer=%d: exceeded max commitments at height=%i (presync phase)\n", m_id, next_height); return false; } } @@ -222,7 +222,7 @@ bool HeadersSyncState::ValidateAndStoreRedownloadedHeader(const CBlockHeader& he // Ensure that we're working on a header that connects to the chain we're // downloading. if (header.hashPrevBlock != m_redownload_buffer_last_hash) { - LogPrint(BCLog::HEADERSSYNC, "Initial headers sync aborted with peer=%d: non-continuous headers at height=%i (redownload phase)\n", m_id, next_height); + LogPrint(BCLog::NET, "Initial headers sync aborted with peer=%d: non-continuous headers at height=%i (redownload phase)\n", m_id, next_height); return false; } @@ -236,7 +236,7 @@ bool HeadersSyncState::ValidateAndStoreRedownloadedHeader(const CBlockHeader& he if (!PermittedDifficultyTransition(m_consensus_params, next_height, previous_nBits, header.nBits)) { - LogPrint(BCLog::HEADERSSYNC, "Initial headers sync aborted with peer=%d: invalid difficulty transition at height=%i (redownload phase)\n", m_id, next_height); + LogPrint(BCLog::NET, "Initial headers sync aborted with peer=%d: invalid difficulty transition at height=%i (redownload phase)\n", m_id, next_height); return false; } @@ -255,7 +255,7 @@ bool HeadersSyncState::ValidateAndStoreRedownloadedHeader(const CBlockHeader& he // target blockhash just because we ran out of commitments. if (!m_process_all_remaining_headers && next_height % HEADER_COMMITMENT_PERIOD == m_commit_offset) { if (m_header_commitments.size() == 0) { - LogPrint(BCLog::HEADERSSYNC, "Initial headers sync aborted with peer=%d: commitment overrun at height=%i (redownload phase)\n", m_id, next_height); + LogPrint(BCLog::NET, "Initial headers sync aborted with peer=%d: commitment overrun at height=%i (redownload phase)\n", m_id, next_height); // Somehow our peer managed to feed us a different chain and // we've run out of commitments. return false; @@ -264,7 +264,7 @@ bool HeadersSyncState::ValidateAndStoreRedownloadedHeader(const CBlockHeader& he bool expected_commitment = m_header_commitments.front(); m_header_commitments.pop_front(); if (commitment != expected_commitment) { - LogPrint(BCLog::HEADERSSYNC, "Initial headers sync aborted with peer=%d: commitment mismatch at height=%i (redownload phase)\n", m_id, next_height); + LogPrint(BCLog::NET, "Initial headers sync aborted with peer=%d: commitment mismatch at height=%i (redownload phase)\n", m_id, next_height); return false; } } diff --git a/src/logging.cpp b/src/logging.cpp index 1627be1971dc..98dcf1e2c69d 100644 --- a/src/logging.cpp +++ b/src/logging.cpp @@ -180,7 +180,6 @@ const CLogCategoryDesc LogCategories[] = {BCLog::BLOCKSTORE, "blockstorage"}, {BCLog::TXRECONCILIATION, "txreconciliation"}, {BCLog::SCAN, "scan"}, - {BCLog::HEADERSSYNC, "headerssync"}, {BCLog::ALL, "1"}, {BCLog::ALL, "all"}, @@ -298,8 +297,6 @@ std::string LogCategoryToStr(BCLog::LogFlags category) return "txreconciliation"; case BCLog::LogFlags::SCAN: return "scan"; - case BCLog::LogFlags::HEADERSSYNC: - return "headerssync"; /* Start Dash */ case BCLog::LogFlags::CHAINLOCKS: return "chainlocks"; diff --git a/src/logging.h b/src/logging.h index c35d40e263b8..d0b2069319f0 100644 --- a/src/logging.h +++ b/src/logging.h @@ -69,7 +69,6 @@ namespace BCLog { BLOCKSTORE = (1 << 26), TXRECONCILIATION = (1 << 27), SCAN = (1 << 28), - HEADERSSYNC = (1 << 29), //Start Dash CHAINLOCKS = ((uint64_t)1 << 32), diff --git a/src/net_processing.cpp b/src/net_processing.cpp index 63347705e818..022edd1f93ab 100644 --- a/src/net_processing.cpp +++ b/src/net_processing.cpp @@ -3657,6 +3657,13 @@ void PeerManagerImpl::ProcessHeadersMessage(CNode& pfrom, Peer& peer, } } + // If our peer has NetPermissionFlags::NoBan privileges, then bypass our + // anti-DoS logic (this saves bandwidth when we connect to a trusted peer + // on startup). + if (pfrom.HasPermission(NetPermissionFlags::NoBan)) { + already_validated_work = true; + } + // At this point, the headers connect to something in our block index. // Do anti-DoS checks to determine if we should process or store for later // processing. diff --git a/src/validation.cpp b/src/validation.cpp index ff5d17404644..3cb010bde897 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -4382,7 +4382,7 @@ void ChainstateManager::ReportHeadersPresync(const arith_uint256& work, int64_t { LOCK(cs_main); // Don't report headers presync progress if we already have a post-minchainwork header chain. - // This means we lose reporting for potentially legimate, but unlikely, deep reorgs, but + // This means we lose reporting for potentially legitimate, but unlikely, deep reorgs, but // prevent attackers that spam low-work headers from filling our logs. if (m_best_header->nChainWork >= UintToArith256(GetConsensus().nMinimumChainWork)) return; // Rate limit headers presync updates to 4 per second, as these are not subject to DoS diff --git a/test/functional/p2p_headers_sync_with_minchainwork.py b/test/functional/p2p_headers_sync_with_minchainwork.py index 5c5a9d271680..c8d95a553654 100755 --- a/test/functional/p2p_headers_sync_with_minchainwork.py +++ b/test/functional/p2p_headers_sync_with_minchainwork.py @@ -30,9 +30,9 @@ class RejectLowDifficultyHeadersTest(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True - self.num_nodes = 3 + self.num_nodes = 4 # Node0 has no required chainwork; node1 requires 15 blocks on top of the genesis block; node2 requires 2047 - self.extra_args = [["-minimumchainwork=0x0", "-checkblockindex=0"], ["-minimumchainwork=0x1f", "-checkblockindex=0"], ["-minimumchainwork=0x1000", "-checkblockindex=0"]] + self.extra_args = [["-minimumchainwork=0x0", "-checkblockindex=0"], ["-minimumchainwork=0x1f", "-checkblockindex=0"], ["-minimumchainwork=0x1000", "-checkblockindex=0"], ["-minimumchainwork=0x1000", "-checkblockindex=0", "-whitelist=noban@127.0.0.1"]] def setup_network(self): self.setup_nodes() @@ -42,17 +42,47 @@ def setup_network(self): def disconnect_all(self): self.disconnect_nodes(0, 1) self.disconnect_nodes(0, 2) + self.disconnect_nodes(0, 3) def reconnect_all(self): self.connect_nodes(0, 1) self.connect_nodes(0, 2) + self.connect_nodes(0, 3) def test_chains_sync_when_long_enough(self): + # The Dash test framework pins mocktime to TIME_GENESIS_BLOCK when + # setup_clean_chain is set, which makes the genesis tip look "recent" + # from CanDirectFetch's perspective (window: 20 * nPowTargetSpacing + # = 3000s, see src/net_processing.cpp:1466). That would cause + # HeadersDirectFetchBlocks to fire as soon as node3 accepts headers + # via the NoBan bypass, pulling the full blocks too and leaving node3 + # in 'active' instead of the expected 'headers-only'. Push mocktime + # past the direct-fetch window before generating so genesis is "old". + # 20 * nPowTargetSpacing(150s) + 1 = 3001s; just past CanDirectFetch's window. + self.bump_mocktime(3001) self.log.info("Generate blocks on the node with no required chainwork, and verify nodes 1 and 2 have no new headers in their headers tree") with self.nodes[1].assert_debug_log(expected_msgs=["[net] Ignoring low-work chain (height=14)"]), self.nodes[2].assert_debug_log(expected_msgs=["[net] Ignoring low-work chain (height=14)"]): self.generate(self.nodes[0], NODE1_BLOCKS_REQUIRED-1, sync_fun=self.no_op) - for node in self.nodes[1:]: + # Node3 should always allow headers due to noban permissions + self.log.info("Check that node3 will sync headers (due to noban permissions)") + + def check_node3_chaintips(num_tips, tip_hash, height): + node3_chaintips = self.nodes[3].getchaintips() + assert(len(node3_chaintips) == num_tips) + assert { + 'height': height, + 'hash': tip_hash, + 'difficulty': Decimal('4.656542373906925E-10'), + 'chainwork': '%064x' % (2 * (height + 1)), + 'branchlen': height, + 'forkpoint': '000008ca1832a4baf228eb1553c03d3a2c8e02399550dd6ea8d65cec3ef23d2e', + 'status': 'headers-only', + } in node3_chaintips + + check_node3_chaintips(2, self.nodes[0].getbestblockhash(), NODE1_BLOCKS_REQUIRED-1) + + for node in self.nodes[1:3]: chaintips = node.getchaintips() assert(len(chaintips) == 1) assert { @@ -68,7 +98,7 @@ def test_chains_sync_when_long_enough(self): self.log.info("Generate more blocks to satisfy node1's minchainwork requirement, and verify node2 still has no new headers in headers tree") with self.nodes[2].assert_debug_log(expected_msgs=["[net] Ignoring low-work chain (height=15)"]): self.generate(self.nodes[0], NODE1_BLOCKS_REQUIRED - self.nodes[0].getblockcount(), sync_fun=self.no_op) - self.sync_blocks(self.nodes[0:2]) + self.sync_blocks(self.nodes[0:2]) # node3 will sync headers (noban permissions) but not blocks (due to minchainwork) assert { 'height': 0, @@ -82,10 +112,13 @@ def test_chains_sync_when_long_enough(self): assert(len(self.nodes[2].getchaintips()) == 1) - self.log.info("Generate long chain for node0/node1") + self.log.info("Check that node3 accepted these headers as well") + check_node3_chaintips(2, self.nodes[0].getbestblockhash(), NODE1_BLOCKS_REQUIRED) + + self.log.info("Generate long chain for node0/node1/node3") self.generate(self.nodes[0], NODE2_BLOCKS_REQUIRED-self.nodes[0].getblockcount(), sync_fun=self.no_op) - self.log.info("Verify that node2 will sync the chain when it gets long enough") + self.log.info("Verify that node2 and node3 will sync the chain when it gets long enough") self.sync_blocks() def test_peerinfo_includes_headers_presync_height(self): From 80f3dd2891f39f607d7c78e99fb7e6d1e670fdac Mon Sep 17 00:00:00 2001 From: Konstantin Akimov Date: Sun, 4 Sep 2022 22:27:50 +0100 Subject: [PATCH 04/11] Merge bitcoin/bitcoin#25978: test: fix non-determinism in p2p_headers_sync_with_minchainwork.py 88e7807e771a568ac34c320b4055d832990049df test: fix non-determinism in p2p_headers_sync_with_minchainwork.py (Suhas Daftuar) Pull request description: The test for node3's chaintips (added in PR25960) needs some sort of synchronization in order to be reliable. ACKs for top commit: mzumsande: Code Review ACK 88e7807e771a568ac34c320b4055d832990049df satsie: ACK 88e7807e771a568ac34c320b4055d832990049df Tree-SHA512: 5607c5b1a95d91e7cf81b695eb356b782cbb303bcc7fd9044e1058c0c0625c5f9e5fe4f4dde9d2bffa27a80d83fc060336720f7becbba505ccfb8a04fcc81705 Co-authored-by: fanquake --- test/functional/p2p_headers_sync_with_minchainwork.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/functional/p2p_headers_sync_with_minchainwork.py b/test/functional/p2p_headers_sync_with_minchainwork.py index c8d95a553654..e1017071d61c 100755 --- a/test/functional/p2p_headers_sync_with_minchainwork.py +++ b/test/functional/p2p_headers_sync_with_minchainwork.py @@ -61,7 +61,7 @@ def test_chains_sync_when_long_enough(self): # 20 * nPowTargetSpacing(150s) + 1 = 3001s; just past CanDirectFetch's window. self.bump_mocktime(3001) self.log.info("Generate blocks on the node with no required chainwork, and verify nodes 1 and 2 have no new headers in their headers tree") - with self.nodes[1].assert_debug_log(expected_msgs=["[net] Ignoring low-work chain (height=14)"]), self.nodes[2].assert_debug_log(expected_msgs=["[net] Ignoring low-work chain (height=14)"]): + with self.nodes[1].assert_debug_log(expected_msgs=["[net] Ignoring low-work chain (height=14)"]), self.nodes[2].assert_debug_log(expected_msgs=["[net] Ignoring low-work chain (height=14)"]), self.nodes[3].assert_debug_log(expected_msgs=["Synchronizing blockheaders, height: 14"]): self.generate(self.nodes[0], NODE1_BLOCKS_REQUIRED-1, sync_fun=self.no_op) # Node3 should always allow headers due to noban permissions @@ -96,7 +96,7 @@ def check_node3_chaintips(num_tips, tip_hash, height): } in chaintips self.log.info("Generate more blocks to satisfy node1's minchainwork requirement, and verify node2 still has no new headers in headers tree") - with self.nodes[2].assert_debug_log(expected_msgs=["[net] Ignoring low-work chain (height=15)"]): + with self.nodes[2].assert_debug_log(expected_msgs=["[net] Ignoring low-work chain (height=15)"]), self.nodes[3].assert_debug_log(expected_msgs=["Synchronizing blockheaders, height: 15"]): self.generate(self.nodes[0], NODE1_BLOCKS_REQUIRED - self.nodes[0].getblockcount(), sync_fun=self.no_op) self.sync_blocks(self.nodes[0:2]) # node3 will sync headers (noban permissions) but not blocks (due to minchainwork) From 676fddca2491cdeab79e9a831052327b6cc03a2a Mon Sep 17 00:00:00 2001 From: Konstantin Akimov Date: Thu, 22 Sep 2022 14:53:46 +0100 Subject: [PATCH 05/11] Merge bitcoin/bitcoin#26012: fuzz: Avoid timeout in bitdeque fuzz target fa4ba04c157b83b827f7541fa007710bd6211fe7 fuzz: Remove no-op call to get() (MacroFake) fa642286b83f29cb0ac0c8d4c7d8eba10600402c fuzz: Avoid timeout in bitdeque fuzz target (MacroFake) Pull request description: I'd guess that any bug should be discoverable within `10` ops. However, `900` seems also better than no limit at all, which causes timeouts such as https://bugs.chromium.org/p/oss-fuzz/issues/detail?id=50892 ACKs for top commit: sipa: ACK fa4ba04c157b83b827f7541fa007710bd6211fe7 Tree-SHA512: f6bd25e78d5f04c6f88e9300c2fa3d0993a0911cb0fd1b414077adc0edde1a06ad72af5e2f50f0ab1324f91999ae57d879686c545b2e6c19ae7f637a8804bd48 Co-authored-by: fanquake --- src/test/fuzz/bitdeque.cpp | 7 +++---- src/test/fuzz/pow.cpp | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/test/fuzz/bitdeque.cpp b/src/test/fuzz/bitdeque.cpp index 3b24d0d12967..65f5cb3fd047 100644 --- a/src/test/fuzz/bitdeque.cpp +++ b/src/test/fuzz/bitdeque.cpp @@ -2,11 +2,10 @@ // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. -#include - #include #include #include +#include #include #include @@ -54,7 +53,8 @@ FUZZ_TARGET(bitdeque, .init = InitRandData) --initlen; } - while (provider.remaining_bytes()) { + LIMITED_WHILE(provider.remaining_bytes() > 0, 900) + { { assert(deq.size() == bitdeq.size()); auto it = deq.begin(); @@ -538,5 +538,4 @@ FUZZ_TARGET(bitdeque, .init = InitRandData) } ); } - } diff --git a/src/test/fuzz/pow.cpp b/src/test/fuzz/pow.cpp index d80ab6dbd83d..e547f0d336eb 100644 --- a/src/test/fuzz/pow.cpp +++ b/src/test/fuzz/pow.cpp @@ -114,7 +114,7 @@ FUZZ_TARGET(pow_transition, .init = initialize_pow) auto current_block{std::make_unique(header)}; current_block->pprev = blocks.empty() ? nullptr : blocks.back().get(); current_block->nHeight = height; - blocks.emplace_back(std::move(current_block)).get(); + blocks.emplace_back(std::move(current_block)); } auto last_block{blocks.back().get()}; unsigned int new_nbits{GetNextWorkRequired(last_block, nullptr, consensus_params)}; From a179502aebd17d32ef016b978c96c3bd8f4ed43a Mon Sep 17 00:00:00 2001 From: Konstantin Akimov Date: Tue, 27 Sep 2022 10:54:16 +0100 Subject: [PATCH 06/11] Merge bitcoin/bitcoin#26172: p2p: ProcessHeadersMessage(): fix received_new_header bdcafb913398f0cdaff9c880618f9ebfc85c7693 p2p: ProcessHeadersMessage(): fix received_new_header (Larry Ruane) Pull request description: Follow-up to #25717. The commit "Utilize anti-DoS headers download strategy" changed how this bool variable is computed, so that its value is now the opposite of what it should be. Prior to #25717: ``` bool received_new_header{WITH_LOCK(::cs_main, return m_chainman.m_blockman.LookupBlockIndex(headers.back().GetHash()) == nullptr)}; ``` After #25717 (simplified): ``` { LOCK(cs_main); last_received_header = m_chainman.m_blockman.LookupBlockIndex(headers.back().GetHash()); } bool received_new_header{last_received_header != nullptr}; ``` ACKs for top commit: dergoegge: ACK bdcafb913398f0cdaff9c880618f9ebfc85c7693 glozow: ACK bdcafb913398f0cdaff9c880618f9ebfc85c7693, I believe this is correct and don't see anything to suggest the switch was intentional. stickies-v: ACK bdcafb913398f0cdaff9c880618f9ebfc85c7693 Tree-SHA512: 35c12762f1429585a0b1c15053e310e83efb28c3d8cbf4092fad9fe81c893f6d766df1f2b20624882acb9654d0539a0c871f587d7090dc2a198115adf59db3ec Co-authored-by: glozow --- src/net_processing.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/net_processing.cpp b/src/net_processing.cpp index 022edd1f93ab..c99a0b19b249 100644 --- a/src/net_processing.cpp +++ b/src/net_processing.cpp @@ -3680,7 +3680,7 @@ void PeerManagerImpl::ProcessHeadersMessage(CNode& pfrom, Peer& peer, // If we don't have the last header, then this peer will have given us // something new (if these headers are valid). - bool received_new_header{last_received_header != nullptr}; + bool received_new_header{last_received_header == nullptr}; // Now process all the headers. BlockValidationState state; From 6364abfdb929f78b9090c177955c477f0400098f Mon Sep 17 00:00:00 2001 From: Konstantin Akimov Date: Mon, 24 Oct 2022 15:34:27 +0100 Subject: [PATCH 07/11] Merge bitcoin/bitcoin#26355: p2p: Handle IsContinuationOfLowWorkHeadersSync return value correctly when new headers sync is started 7ad15d11005eac36421398530da127333d87ea80 [net processing] Handle IsContinuationOfLowWorkHeadersSync return value correctly when new headers sync is started (dergoegge) Pull request description: This PR fixes a bug in the headers sync logic that enables submitting headers to a nodes block index that don't lead to a chain that surpasses our DoS limit. The issue is that we ignore the return value on [the first `IsContinuationOfLowWorkHeadersSync` call after a new headers sync is started](https://github.com/bitcoin/bitcoin/blob/fabc0310480b49e159a15d494525c5aa15072cba/src/net_processing.cpp#L2553-L2568), which leads to us passing headers to [`ProcessNewBlockHeaders`](https://github.com/bitcoin/bitcoin/blob/fabc0310480b49e159a15d494525c5aa15072cba/src/net_processing.cpp#L2856) when that initial `IsContinuationOfLowWorkHeadersSync` call returns `false`. One easy way (maybe the only?) to trigger this is by sending 2000 headers where the last header has a different `nBits` value than the prior headers (which fails the pre-sync logic [here](https://github.com/bitcoin/bitcoin/blob/fabc0310480b49e159a15d494525c5aa15072cba/src/headerssync.cpp#L189)). Those 2000 headers will be passed to `ProcessNewBlockHeaders`. I haven't included a test here so far because we can't test this without changing the default value for `CRegTestParams::consensus.fPowAllowMinDifficultyBlocks` or doing some more involved refactoring. ACKs for top commit: sipa: ACK 7ad15d11005eac36421398530da127333d87ea80 glozow: ACK 7ad15d1100 Tree-SHA512: 9aabb8bf3700401e79863d0accda0befd2a83c4d469a53f97d827e51139e2f826aee08cdfbc8866b311b153f61fdac9b7aa515fcfa2a21c5e2812c2bf3c03664 Co-authored-by: glozow --- src/net_processing.cpp | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/net_processing.cpp b/src/net_processing.cpp index c99a0b19b249..caa6c39e3362 100644 --- a/src/net_processing.cpp +++ b/src/net_processing.cpp @@ -3397,14 +3397,22 @@ bool PeerManagerImpl::TryLowWorkHeadersSync(Peer& peer, CNode& pfrom, const CBlo // Now a HeadersSyncState object for tracking this synchronization is created, // process the headers using it as normal. - return IsContinuationOfLowWorkHeadersSync(peer, pfrom, headers, uses_compressed); + if (!IsContinuationOfLowWorkHeadersSync(peer, pfrom, headers, uses_compressed)) { + // Something went wrong, reset the headers sync. + peer.m_headers_sync.reset(nullptr); + LOCK(m_headers_presync_mutex); + m_headers_presync_stats.erase(peer.m_id); + } } else { LogPrint(BCLog::NET, "Ignoring low-work chain (height=%u) from peer=%d\n", chain_start_header->nHeight + headers.size(), pfrom.GetId()); - // Since this is a low-work headers chain, no further processing is required. - headers = {}; - return true; } + + // The peer has not yet given us a chain that meets our work threshold, + // so we want to prevent further processing of the headers in any case. + headers = {}; + return true; } + return false; } From 31f350b7b4157edf99da38148c0ba63b231c9a98 Mon Sep 17 00:00:00 2001 From: Konstantin Akimov Date: Mon, 31 Oct 2022 15:28:43 +0000 Subject: [PATCH 08/11] Merge bitcoin/bitcoin#26387: p2p: TryLowWorkHeadersSync follow-ups 784b02319128988038d4bd82f05736be22f14ee9 [net processing] Simplify use of IsContinuationOfLowWorkHeadersSync in TryLowWorkHeaderSync (dergoegge) e891aabf5a4992a65b9c5ae8606f8dd08515b310 [net processing] Fixup TryLowWorkHeadersSync comment (dergoegge) Pull request description: See https://github.com/bitcoin/bitcoin/pull/26355#discussion_r1003561481 and https://github.com/bitcoin/bitcoin/pull/26355#discussion_r1004554187 ACKs for top commit: hernanmarino: ACK 784b02319128988038d4bd82f05736be22f14ee9 brunoerg: crACK 784b02319128988038d4bd82f05736be22f14ee9 mzumsande: ACK 784b02319128988038d4bd82f05736be22f14ee9 Tree-SHA512: b47ac0d78a09ca3a1806e38c5d2e2fcf1e5f0668f202450b5079c5cb168e168ac6828c0948d23f3610696375134986d75ef3c6098858173023bcb743aec8004c Co-authored-by: fanquake --- src/net_processing.cpp | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/net_processing.cpp b/src/net_processing.cpp index caa6c39e3362..30787faf908f 100644 --- a/src/net_processing.cpp +++ b/src/net_processing.cpp @@ -818,9 +818,8 @@ class PeerManagerImpl final : public PeerManager * @param[in] chain_start_header Where these headers connect in our index. * @param[in,out] headers The headers to be processed. * - * @return True if chain was low work and a headers sync was - * initiated (and headers will be empty after calling); false - * otherwise. + * @return True if chain was low work (headers will be empty after + * calling); false otherwise. */ bool TryLowWorkHeadersSync(Peer& peer, CNode& pfrom, const CBlockIndex* chain_start_header, @@ -3395,14 +3394,10 @@ bool PeerManagerImpl::TryLowWorkHeadersSync(Peer& peer, CNode& pfrom, const CBlo peer.m_headers_sync.reset(new HeadersSyncState(peer.m_id, m_chainparams.GetConsensus(), chain_start_header, minimum_chain_work)); - // Now a HeadersSyncState object for tracking this synchronization is created, - // process the headers using it as normal. - if (!IsContinuationOfLowWorkHeadersSync(peer, pfrom, headers, uses_compressed)) { - // Something went wrong, reset the headers sync. - peer.m_headers_sync.reset(nullptr); - LOCK(m_headers_presync_mutex); - m_headers_presync_stats.erase(peer.m_id); - } + // Now a HeadersSyncState object for tracking this synchronization + // is created, process the headers using it as normal. Failures are + // handled inside of IsContinuationOfLowWorkHeadersSync. + (void)IsContinuationOfLowWorkHeadersSync(peer, pfrom, headers, uses_compressed); } else { LogPrint(BCLog::NET, "Ignoring low-work chain (height=%u) from peer=%d\n", chain_start_header->nHeight + headers.size(), pfrom.GetId()); } From c5594c1febf795fb1fc9d358a2d2df583d3936f0 Mon Sep 17 00:00:00 2001 From: Konstantin Akimov Date: Tue, 24 Jan 2023 12:24:04 +0100 Subject: [PATCH 09/11] Merge bitcoin/bitcoin#26954: test: Avoid rpc timeout in p2p_headers_sync_with_minchainwork fa952fad2fca833357e61a7bb8c76c2877b522f9 test: Avoid rpc timeout in p2p_headers_sync_with_minchainwork (MarcoFalke) Pull request description: When running a lot of tests in parallel, I get `JSONRPCException: 'generatetoaddress' RPC took longer than 30.000000 seconds.` The general recommendation, if running into timeouts, is to increase the `--timeout-factor`. However, I think that the default timeout values should be suitable to run the tests out of the box on reasonable hardware. ACKs for top commit: fanquake: ACK fa952fad2fca833357e61a7bb8c76c2877b522f9 Tree-SHA512: b7eeda54f8db900f077417c0431f659c67e686e2fc078f8c713e37ed75b8bc862814ce20e8400741638e35e224d7284ad16172bf5f82168f803376d0c9ec4524 Co-authored-by: MarcoFalke <*~=`'#}+{/-|&$^_@721217.xyz> --- test/functional/p2p_headers_sync_with_minchainwork.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/functional/p2p_headers_sync_with_minchainwork.py b/test/functional/p2p_headers_sync_with_minchainwork.py index e1017071d61c..8fee03f2abc1 100755 --- a/test/functional/p2p_headers_sync_with_minchainwork.py +++ b/test/functional/p2p_headers_sync_with_minchainwork.py @@ -29,6 +29,7 @@ class RejectLowDifficultyHeadersTest(BitcoinTestFramework): def set_test_params(self): + self.rpc_timeout *= 4 # To avoid timeout when generating BLOCKS_TO_MINE self.setup_clean_chain = True self.num_nodes = 4 # Node0 has no required chainwork; node1 requires 15 blocks on top of the genesis block; node2 requires 2047 From 28f32172c6032a3a5fd913304683de4dc28e2ca4 Mon Sep 17 00:00:00 2001 From: Konstantin Akimov Date: Tue, 14 Feb 2023 18:38:31 -0500 Subject: [PATCH 10/11] Merge bitcoin/bitcoin#26184: test: p2p: check that headers message with invalid proof-of-work disconnects peer 772671245d50d94fd5087deb2542854604eba174 test: p2p: check that headers message with invalid proof-of-work disconnects peer (Sebastian Falbesoner) Pull request description: One of the earliest anti-DoS checks done after receiving and deserializing a `headers` message from a peer is verifying whether the proof-of-work is valid (called in method `PeerManagerImpl::ProcessHeadersMessage`): https://github.com/bitcoin/bitcoin/blob/f227e153e80c8c50c30d76e1ac638d7206c7ff61/src/net_processing.cpp#L2752-L2762 The called method `PeerManagerImpl::CheckHeadersPoW` calls `Misbehaving` with a score of 100, i.e. leading to an immediate disconnect of the peer: https://github.com/bitcoin/bitcoin/blob/f227e153e80c8c50c30d76e1ac638d7206c7ff61/src/net_processing.cpp#L2368-L2372 This PR adds a simple test for both the misbehaving log and the resulting disconnect. For creating a block header with invalid proof-of-work, we first create one that is accepted by the node (the difficulty field `nBits` is copied from the genesis block) and based on that the nonce is modified until we have block header hash prefix that is too high to fulfill even the minimum difficulty. ACKs for top commit: Sjors: ACK 772671245d50d94fd5087deb2542854604eba174 achow101: ACK 772671245d50d94fd5087deb2542854604eba174 brunoerg: crACK 772671245d50d94fd5087deb2542854604eba174 furszy: Code review ACK 77267124 with a non-blocking speedup. Tree-SHA512: 680aa7939158d1dc672b90aa6554ba2b3a92584b6d3bcb0227776035858429feb8bc66eed18b47de0fe56df7d9b3ddaee231aaeaa360136603b9ad4b19e6ac11 Co-authored-by: Andrew Chow --- test/functional/p2p_invalid_messages.py | 33 ++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/test/functional/p2p_invalid_messages.py b/test/functional/p2p_invalid_messages.py index e8abe8537a96..c0d4c9d4185e 100755 --- a/test/functional/p2p_invalid_messages.py +++ b/test/functional/p2p_invalid_messages.py @@ -15,11 +15,11 @@ MAX_HEADERS_UNCOMPRESSED_RESULT, MAX_INV_SIZE, MAX_PROTOCOL_MESSAGE_LENGTH, + MSG_TX, msg_getdata, msg_headers, msg_headers2, msg_inv, - MSG_TX, msg_version, from_hex, ) @@ -63,6 +63,7 @@ def run_test(self): self.test_oversized_inv_msg() self.test_oversized_getdata_msg() self.test_oversized_headers_msg() + self.test_invalid_pow_headers_msg() self.test_noncontinuous_headers_msg() self.test_resource_exhaustion() @@ -189,6 +190,36 @@ def test_oversized_headers2_msg(self): size = MAX_HEADERS_COMPRESSED_RESULT + 1 self.test_oversized_msg(msg_headers2([CBlockHeader()] * size), size) + def test_invalid_pow_headers_msg(self): + self.log.info("Test headers message with invalid proof-of-work is logged as misbehaving and disconnects peer") + blockheader_tip_hash = self.nodes[0].getbestblockhash() + blockheader_tip = from_hex(CBlockHeader(), self.nodes[0].getblockheader(blockheader_tip_hash, False)) + + # send valid headers message first + assert_equal(self.nodes[0].getblockchaininfo()['headers'], 0) + blockheader = CBlockHeader() + blockheader.hashPrevBlock = int(blockheader_tip_hash, 16) + blockheader.nTime = blockheader_tip.nTime + 150 + blockheader.nBits = blockheader_tip.nBits + blockheader.rehash() + while not blockheader.hash.startswith('0'): + blockheader.nNonce += 1 + blockheader.rehash() + peer = self.nodes[0].add_p2p_connection(P2PInterface()) + peer.send_and_ping(msg_headers([blockheader])) + assert_equal(self.nodes[0].getblockchaininfo()['headers'], 1) + chaintips = self.nodes[0].getchaintips() + assert_equal(chaintips[0]['status'], 'headers-only') + assert_equal(chaintips[0]['hash'], blockheader.hash) + + # invalidate PoW + while not blockheader.hash.startswith('f'): + blockheader.nNonce += 1 + blockheader.rehash() + with self.nodes[0].assert_debug_log(['Misbehaving', 'header with invalid proof of work']): + peer.send_message(msg_headers([blockheader])) + peer.wait_for_disconnect() + def test_noncontinuous_headers_msg(self): self.log.info("Test headers message with non-continuous headers sequence is logged as misbehaving") block_hashes = self.generate(self.nodes[0], 10) From c4da8de05280cc8dcc4c13df1c212e3220953c77 Mon Sep 17 00:00:00 2001 From: merge-script Date: Mon, 2 Sep 2024 15:14:55 +0100 Subject: [PATCH 11/11] Merge bitcoin/bitcoin#30761: test: Avoid intermittent timeout in p2p_headers_sync_with_minchainwork.py fa247e6e8c7fddf9e3461c3e2e6f5fade0fe64cf test: Avoid intermittent timeout in p2p_headers_sync_with_minchainwork.py (MarcoFalke) Pull request description: Similar to https://github.com/bitcoin/bitcoin/pull/30705: The goal of this test case is to check that the sync works at all, not to check any timeout. On extremely slow hardware (for example qemu virtual hardware), downloading the 4110 BLOCKS_TO_MINE may take longer than the block download timeout. Fix it by pinning the time using mocktime temporarily, and advance it immediately after the sync. ACKs for top commit: stratospher: ACK fa247e6. Checked the timeout downloading block logs before/after using `setmocktime`. tdb3: ACK fa247e6e8c7fddf9e3461c3e2e6f5fade0fe64cf Tree-SHA512: f61632a8d9e484f1b888aafbf87f7adf71b8692387bd77f603cdbc0de49f30d42e654741d46ae1ff8b9706a5559ee0faabdb192ed0db7449010b68bfcdbaa42d --- test/functional/p2p_headers_sync_with_minchainwork.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/test/functional/p2p_headers_sync_with_minchainwork.py b/test/functional/p2p_headers_sync_with_minchainwork.py index 8fee03f2abc1..bc932ae4c7aa 100755 --- a/test/functional/p2p_headers_sync_with_minchainwork.py +++ b/test/functional/p2p_headers_sync_with_minchainwork.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2019-2021 The Bitcoin Core developers +# Copyright (c) 2019-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 that we reject low difficulty headers to prevent our block tree from filling up with useless bloat""" @@ -23,6 +23,8 @@ from test_framework.util import assert_equal +import time + NODE1_BLOCKS_REQUIRED = 15 NODE2_BLOCKS_REQUIRED = 2047 @@ -50,6 +52,10 @@ def reconnect_all(self): self.connect_nodes(0, 2) self.connect_nodes(0, 3) + def mocktime_all(self, time): + for n in self.nodes: + n.setmocktime(time) + def test_chains_sync_when_long_enough(self): # The Dash test framework pins mocktime to TIME_GENESIS_BLOCK when # setup_clean_chain is set, which makes the genesis tip look "recent" @@ -170,7 +176,9 @@ def test_large_reorgs_can_succeed(self): self.reconnect_all() + self.mocktime_all(int(time.time())) # Temporarily hold time to avoid internal timeouts self.sync_blocks(timeout=300) # Ensure tips eventually agree + self.mocktime_all(0) def run_test(self):