diff --git a/src/Makefile.am b/src/Makefile.am index 22f28dbbd102..af4f5b81dff3 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -252,6 +252,7 @@ BITCOIN_CORE_H = \ governance/votedb.h \ gsl/assert.h \ gsl/pointers.h \ + headerssync.h \ httprpc.h \ httpserver.h \ i2p.h \ @@ -422,6 +423,7 @@ BITCOIN_CORE_H = \ unordered_lru_cache.h \ util/asmap.h \ util/bip32.h \ + util/bitdeque.h \ util/bytevectorhash.h \ util/check.h \ util/edge.h \ @@ -562,6 +564,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 a34ea4b120a3..e8907f8a06dd 100644 --- a/src/Makefile.test.include +++ b/src/Makefile.test.include @@ -133,6 +133,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 \ @@ -302,6 +303,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/bench/blockfilter_index.cpp b/src/bench/blockfilter_index.cpp index c38a34011a23..237a39cc2c16 100644 --- a/src/bench/blockfilter_index.cpp +++ b/src/bench/blockfilter_index.cpp @@ -54,7 +54,7 @@ class BlockFilterIndexBenchSetup ++block.nNonce; } - if (!m_testing_setup->m_node.chainman->ProcessNewBlock(std::make_shared(block), true, nullptr)) { + if (!m_testing_setup->m_node.chainman->ProcessNewBlock(std::make_shared(block), true, true, nullptr)) { throw std::runtime_error("failed to create block filter benchmark chain"); } } diff --git a/src/bitcoin-chainstate.cpp b/src/bitcoin-chainstate.cpp index 204c4701f08d..a42e03d2448f 100644 --- a/src/bitcoin-chainstate.cpp +++ b/src/bitcoin-chainstate.cpp @@ -213,7 +213,7 @@ int main(int argc, char* argv[]) 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) { std::cerr << "duplicate" << std::endl; @@ -228,6 +228,9 @@ int main(int argc, char* argv[]) case BlockValidationResult::BLOCK_RESULT_UNSET: std::cerr << "initial value. Block has not yet been rejected" << std::endl; break; + case BlockValidationResult::BLOCK_HEADER_LOW_WORK: + std::cerr << "the block header may be on a too-little-work chain" << std::endl; + break; case BlockValidationResult::BLOCK_CONSENSUS: std::cerr << "invalid by consensus rules (excluding any below reasons)" << std::endl; break; 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..1b34b23db679 --- /dev/null +++ b/src/headerssync.cpp @@ -0,0 +1,320 @@ +// 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::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 + * 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 std::vector& hashes, const bool full_headers_message) +{ + ProcessingResult ret; + + Assume(received_headers.size() == hashes.size()); + 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, hashes); + 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::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) { + // 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 (size_t i = 0; i < received_headers.size(); ++i) { + if (!ValidateAndStoreRedownloadedHeader(received_headers[i], hashes[i])) { + // 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::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; + } 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::NET, "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, const std::vector& hashes) +{ + // The caller should not give us an empty set of headers. + Assume(headers.size() > 0); + if (headers.size() == 0) return true; + + Assume(headers.size() == hashes.size()); + + 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::NET, "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 (size_t i = 0; i < headers.size(); ++i) { + if (!ValidateAndProcessSingleHeader(headers[i], hashes[i])) { + 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::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; +} + +bool HeadersSyncState::ValidateAndProcessSingleHeader(const CBlockHeader& current, const uint256& hash) +{ + 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::NET, "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(hash) & 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::NET, "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, const uint256& hash) +{ + 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::NET, "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::NET, "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::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; + } + bool commitment = m_hasher(hash) & 1; + bool expected_commitment = m_header_commitments.front(); + m_header_commitments.pop_front(); + if (commitment != expected_commitment) { + LogPrint(BCLog::NET, "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 = hash; + + 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..4f7c381d6260 --- /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, const std::vector& hashes, 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, const std::vector& hashes); + + /** In PRESYNC, process and update state for a single header */ + bool ValidateAndProcessSingleHeader(const CBlockHeader& current, const uint256& hash); + + /** In REDOWNLOAD, check a header's commitment (if applicable) and add to + * buffer for later processing */ + bool ValidateAndStoreRedownloadedHeader(const CBlockHeader& header, const uint256& hash); + + /** 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/interfaces/node.h b/src/interfaces/node.h index 22cda4e8fa51..22adcbe36697 100644 --- a/src/interfaces/node.h +++ b/src/interfaces/node.h @@ -556,7 +556,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/net_processing.cpp b/src/net_processing.cpp index 740a4e966d27..bb2de56d329a 100644 --- a/src/net_processing.cpp +++ b/src/net_processing.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -432,6 +433,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} @@ -594,9 +604,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); @@ -618,7 +628,7 @@ class PeerManagerImpl final : public PeerManager void UnitTestMisbehaving(NodeId peer_id, int howmuch) override EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex) { Misbehaving(*Assert(GetPeerRef(peer_id)), howmuch, ""); }; 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); @@ -746,22 +756,75 @@ 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, + std::vector&& hashes, + 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 std::vector& hashes, const Consensus::Params& consensusParams, Peer& peer); + /** 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). */ void HandleFewUnconnectingHeaders(CNode& pfrom, Peer& peer, const std::vector& headers) 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; + bool CheckHeadersAreContinuous(const std::vector& headers, const std::vector& hashes) 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. + * @param[in,out] hashes The hashes of those headers; kept index-aligned with headers, including when headers are replaced. + * @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, std::vector& hashes, 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 (headers will be empty after + * calling); false otherwise. + */ + bool TryLowWorkHeadersSync(Peer& peer, CNode& pfrom, + const CBlockIndex* chain_start_header, + std::vector& headers, std::vector& hashes, 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. @@ -788,6 +851,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. @@ -1018,6 +1084,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}; @@ -1063,7 +1147,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); /** Process compact block txns */ void ProcessCompactBlockTxns(CNode& pfrom, Peer& peer, const BlockTransactions& block_transactions) @@ -1796,7 +1880,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); } @@ -1860,6 +1947,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; } @@ -1918,6 +2011,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: @@ -3041,6 +3138,35 @@ void PeerManagerImpl::SendBlockTransactions(CNode& pfrom, Peer& peer, const CBlo m_connman.PushMessage(&pfrom, msgMaker.Make(NetMsgType::BLOCKTXN, resp)); } +bool PeerManagerImpl::CheckHeadersPoW(const std::vector& headers, const std::vector& hashes, const Consensus::Params& consensusParams, Peer& peer) +{ + // Do these headers have proof-of-work matching what's claimed? + if (!HasValidProofOfWork(headers, hashes, consensusParams)) { + Misbehaving(peer, 100, "header with invalid proof of work"); + return false; + } + + // Are these headers connected to each other? + if (!CheckHeadersAreContinuous(headers, hashes)) { + Misbehaving(peer, 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, m_chainman.MinimumChainWork()); +} + /** * Special handling for unconnecting headers that might be part of a block * announcement. @@ -3082,18 +3208,167 @@ void PeerManagerImpl::HandleFewUnconnectingHeaders(CNode& pfrom, Peer& peer, } } -bool PeerManagerImpl::CheckHeadersAreContinuous(const std::vector& headers) const +bool PeerManagerImpl::CheckHeadersAreContinuous(const std::vector& headers, const std::vector& hashes) const { - uint256 hashLastBlock; - for (const CBlockHeader& header : headers) { - if (!hashLastBlock.IsNull() && header.hashPrevBlock != hashLastBlock) { + assert(headers.size() == hashes.size()); + for (size_t i = 1; i < headers.size(); ++i) { + if (headers[i].hashPrevBlock != hashes[i - 1]) { return false; } - hashLastBlock = header.GetHash(); } return true; } +bool PeerManagerImpl::IsContinuationOfLowWorkHeadersSync(Peer& peer, CNode& pfrom, std::vector& headers, std::vector& hashes, bool uses_compressed) +{ + if (peer.m_headers_sync) { + auto result = peer.m_headers_sync->ProcessNextHeaders(headers, hashes, 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); + // The swapped-in headers are not the ones the passed-in hashes + // were computed from; recompute so both stay index-aligned. + hashes.clear(); + hashes.reserve(headers.size()); + for (const CBlockHeader& header : headers) hashes.push_back(header.GetHash()); + } + + 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, std::vector& hashes, 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. Failures are + // handled inside of IsContinuationOfLowWorkHeadersSync. + (void)IsContinuationOfLowWorkHeadersSync(peer, pfrom, headers, hashes, uses_compressed); + } else { + LogPrint(BCLog::NET, "Ignoring low-work chain (height=%u) from peer=%d\n", chain_start_header->nHeight + headers.size(), pfrom.GetId()); + } + + // 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; +} + +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); @@ -3238,21 +3513,75 @@ void PeerManagerImpl::UpdatePeerStateForReceivedHeaders(CNode& pfrom, } void PeerManagerImpl::ProcessHeadersMessage(CNode& pfrom, Peer& peer, - const std::vector& headers, - bool via_compact_block) + std::vector&& headers, + std::vector&& hashes, + bool via_compact_block, bool uses_compressed) { - const CNetMsgMaker msgMaker(pfrom.GetCommonVersion()); + Assume(headers.size() == hashes.size()); 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, hashes, m_chainparams.GetConsensus(), peer)) { + // 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, hashes, 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) { @@ -3266,28 +3595,57 @@ 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(hashes.back()); + if (IsAncestorOfBestHeaderOrTip(last_received_header)) { + already_validated_work = true; + } + } + + // 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. - if (!CheckHeadersAreContinuous(headers)) { - Misbehaving(peer, 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, hashes, 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, GetLocator(pindexLast), peer)) { LogPrint(BCLog::NET, "more %s (%d) to end to peer=%d (startheight:%d)\n", @@ -3622,10 +3980,10 @@ static DSTXValidationResult ValidateDSTX(CDeterministicMNManager& dmnman, CDSTXM return {DSTXValidationScore::NONE, 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 @@ -3719,7 +4077,7 @@ void PeerManagerImpl::ProcessCompactBlockTxns(CNode& pfrom, Peer& peer, const Bl // 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; } @@ -4112,12 +4470,6 @@ void PeerManagerImpl::ProcessMessage( CMNAuth::PushMNAUTH(pfrom, m_connman, *Assert(m_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 @@ -4943,13 +5295,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, 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)) { @@ -4959,7 +5316,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; @@ -5137,7 +5494,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}, {cmpctblock.header.GetHash()}, /*via_compact_block=*/true, UsesCompressedHeaders(*peer)); } if (fBlockReconstructed) { @@ -5156,7 +5513,7 @@ void PeerManagerImpl::ProcessMessage( // we have a chain with at least the minimum chain work), 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 @@ -5219,7 +5576,30 @@ void PeerManagerImpl::ProcessMessage( } } - return ProcessHeadersMessage(pfrom, *peer, headers, /*via_compact_block=*/false); + // Pre-compute each header's hash once so downstream PRESYNC/REDOWNLOAD/ + // CheckHeadersPoW/CheckHeadersAreContinuous reuse it instead of + // recomputing X11 multiple times per header. + std::vector hashes; + hashes.reserve(headers.size()); + for (const CBlockHeader& h : headers) hashes.push_back(h.GetHash()); + + ProcessHeadersMessage(pfrom, *peer, std::move(headers), std::move(hashes), /*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) @@ -5248,6 +5628,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 @@ -5258,8 +5639,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; } @@ -6117,6 +6504,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 > m_chainman.MinimumChainWork()) { + // 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 { @@ -6199,6 +6608,8 @@ bool PeerManagerImpl::SendMessages(CNode* pto) MaybeSendAddr(*pto, *peer, current_time); + MaybeSendSendHeaders(*pto, *peer); + { LOCK(cs_main); diff --git a/src/net_processing.h b/src/net_processing.h index 8fceb9bcd295..563a5d48629a 100644 --- a/src/net_processing.h +++ b/src/net_processing.h @@ -78,6 +78,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 61a5b3bb756e..6ebd1f7b7aa3 100644 --- a/src/node/interface_ui.cpp +++ b/src/node/interface_ui.cpp @@ -65,7 +65,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, int64_t height, int64_t timestamp) { return g_ui_signals.NotifyHeaderTip(s, height, timestamp); } +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 49f0469918ee..75615d0b3066 100644 --- a/src/node/interface_ui.h +++ b/src/node/interface_ui.h @@ -111,7 +111,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, int64_t height, int64_t timestamp); + 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 4900dcf785af..b5ec8063638a 100644 --- a/src/node/interfaces.cpp +++ b/src/node/interfaces.cpp @@ -1156,8 +1156,8 @@ class NodeImpl : public Node std::unique_ptr handleNotifyHeaderTip(NotifyHeaderTipFn fn) override { return MakeHandler( - ::uiInterface.NotifyHeaderTip_connect([fn](SynchronizationState sync_state, int64_t height, int64_t timestamp) { - fn(sync_state, BlockTip{(int)height, timestamp, uint256{}}); + ::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 diff --git a/src/pow.cpp b/src/pow.cpp index a6a79fca4331..f43f926f7c9e 100644 --- a/src/pow.cpp +++ b/src/pow.cpp @@ -236,6 +236,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..88a5deea91a4 100644 --- a/src/pow.h +++ b/src/pow.h @@ -21,4 +21,21 @@ 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 is a non-consensus anti-DoS heuristic used by headers sync. It only + * enforces a bound below nPowKGWHeight, where Dash uses Bitcoin-style + * retargeting: there, new_nbits must be within a factor of 4 of old_nbits. + * From nPowKGWHeight onwards (KGW/DGW per-block retargeting) there is no + * useful per-pair bound, so it always returns true. See the comment above the + * definition in pow.cpp for the full rationale. + * + * Always returns true on networks where min difficulty blocks are allowed, + * such as regtest/testnet/devnet. + */ +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/bitcoingui.cpp b/src/qt/bitcoingui.cpp index 9858dd9f818c..21b2948c8ae4 100644 --- a/src/qt/bitcoingui.cpp +++ b/src/qt/bitcoingui.cpp @@ -891,7 +891,7 @@ 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)); + 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); @@ -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()) @@ -1622,7 +1629,7 @@ void BitcoinGUI::setNumBlocks(int count, const QDateTime& blockDate, const QStri if (modalOverlay) { if (synctype != SyncType::BLOCK_SYNC) - modalOverlay->setKnownBestHeight(count, blockDate); + 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 (synctype == SyncType::HEADER_SYNC) { + if (synctype == SyncType::HEADER_PRESYNC) { + updateHeadersPresyncProgressLabel(count, blockDate); + return; + } else if (synctype == SyncType::HEADER_SYNC) { updateHeadersSyncProgressLabel(); return; } diff --git a/src/qt/bitcoingui.h b/src/qt/bitcoingui.h index 2bcba1234fb4..51adb3b5a672 100644 --- a/src/qt/bitcoingui.h +++ b/src/qt/bitcoingui.h @@ -275,6 +275,7 @@ class BitcoinGUI : public QMainWindow void stopGovernanceSyncAnimation(); void updateHeadersSyncProgressLabel(); + void updateHeadersPresyncProgressLabel(int64_t height, const QDateTime& blockDate); void updateProgressBarVisibility(); diff --git a/src/qt/clientmodel.cpp b/src/qt/clientmodel.cpp index 426f21f545d3..eb009a0aff7a 100644 --- a/src/qt/clientmodel.cpp +++ b/src/qt/clientmodel.cpp @@ -314,8 +314,8 @@ void ClientModel::subscribeToCoreSignals() TipChanged(sync_state, tip, verification_progress, SyncType::BLOCK_SYNC); })); m_event_handlers.emplace_back(m_node.handleNotifyHeaderTip( - [this](SynchronizationState sync_state, interfaces::BlockTip tip) { - TipChanged(sync_state, tip, /*verification_progress=*/0.0, SyncType::HEADER_SYNC); + [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 883ff7b78e74..0d5cfee5b532 100644 --- a/src/qt/clientmodel.h +++ b/src/qt/clientmodel.h @@ -41,6 +41,7 @@ enum class BlockSource { }; enum class SyncType { + HEADER_PRESYNC, HEADER_SYNC, BLOCK_SYNC }; diff --git a/src/qt/informationwidget.cpp b/src/qt/informationwidget.cpp index 710121822b58..3102722c00f4 100644 --- a/src/qt/informationwidget.cpp +++ b/src/qt/informationwidget.cpp @@ -106,7 +106,7 @@ void InformationWidget::setNetworkActive(bool networkActive) updateNetworkState(); } -void InformationWidget::setNumBlocks(int count, const QDateTime& blockDate, const QString& blockHash, double nVerificationProgress, SyncType synctype) +void InformationWidget::setNumBlocks(int count, const QDateTime& blockDate, const QString& blockHash, double nVerificationProgress, SyncType synctype, SynchronizationState sync_state) { if (synctype == SyncType::BLOCK_SYNC) { ui->numberOfBlocks->setText(QString::number(count)); diff --git a/src/qt/informationwidget.h b/src/qt/informationwidget.h index 3eb82cc91665..d35524d9d222 100644 --- a/src/qt/informationwidget.h +++ b/src/qt/informationwidget.h @@ -14,6 +14,7 @@ class InformationWidget; } // namespace Ui enum class SyncType; +enum class SynchronizationState; class InformationWidget : public QWidget { @@ -31,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, SyncType synctype); + 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 dc4c6a1a0bcb..4119db4a0a13 100644 --- a/src/qt/modaloverlay.cpp +++ b/src/qt/modaloverlay.cpp @@ -93,13 +93,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) @@ -173,6 +176,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 ae1138f59434..45b31d11836d 100644 --- a/src/qt/rpcconsole.cpp +++ b/src/qt/rpcconsole.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -778,7 +779,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, SyncType::BLOCK_SYNC); + verification_progress, SyncType::BLOCK_SYNC, SynchronizationState::INIT_DOWNLOAD); //Setup autocomplete and attach it QStringList wordList; diff --git a/src/rpc/mining.cpp b/src/rpc/mining.cpp index 0886aaceab92..8dff0a38a031 100644 --- a/src/rpc/mining.cpp +++ b/src/rpc/mining.cpp @@ -152,7 +152,7 @@ static bool GenerateBlock(ChainstateManager& chainman, CBlock& block, uint64_t& if (!process_new_block) return true; - if (!chainman.ProcessNewBlock(block_out, /*force_processing=*/true, nullptr)) { + if (!chainman.ProcessNewBlock(block_out, /*force_processing=*/true, /*min_pow_checked=*/true, nullptr)) { throw JSONRPCError(RPC_INTERNAL_ERROR, "ProcessNewBlock, block not accepted"); } @@ -1048,7 +1048,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"; @@ -1090,7 +1090,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 7457eae7b1b5..06778a21536a 100644 --- a/src/rpc/net.cpp +++ b/src/rpc/net.cpp @@ -158,6 +158,7 @@ static RPCHelpMan getpeerinfo() {RPCResult::Type::BOOL, "masternode", "Whether connection was due to masternode connection attempt"}, {RPCResult::Type::NUM, "banscore", /*optional=*/true, "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", "", @@ -273,6 +274,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 6cb70d3e7f1e..5bc11b32686c 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/bls_tests.cpp b/src/test/bls_tests.cpp index 5696a34273ff..f2bc547206a0 100644 --- a/src/test/bls_tests.cpp +++ b/src/test/bls_tests.cpp @@ -798,7 +798,7 @@ BOOST_AUTO_TEST_CASE(v19_boundary_validation_failure_restores_bls_scheme) CBlock connect_block = setup.CreateBlock({bad_tx}, coinbase_pk, chainman.ActiveChainstate()); const int height_before_invalid_block{WITH_LOCK(::cs_main, return chainman.ActiveChain().Height())}; - (void)chainman.ProcessNewBlock(std::make_shared(connect_block), /*force_processing=*/true, /*new_block=*/nullptr); + (void)chainman.ProcessNewBlock(std::make_shared(connect_block), /*force_processing=*/true, /*min_pow_checked=*/true, /*new_block=*/nullptr); BOOST_CHECK_EQUAL(WITH_LOCK(::cs_main, return chainman.ActiveChain().Height()), height_before_invalid_block); BOOST_CHECK(bls::bls_legacy_scheme.load()); } diff --git a/src/test/coinstatsindex_tests.cpp b/src/test/coinstatsindex_tests.cpp index de9a01bb1a91..3020de4b881f 100644 --- a/src/test/coinstatsindex_tests.cpp +++ b/src/test/coinstatsindex_tests.cpp @@ -97,7 +97,7 @@ BOOST_FIXTURE_TEST_CASE(coinstatsindex_unclean_shutdown, TestChain100Setup) LOCK(cs_main); BlockValidationState state; BOOST_CHECK(CheckBlock(block, state, params.GetConsensus())); - BOOST_CHECK(m_node.chainman->AcceptBlock(new_block, state, &new_block_index, true, nullptr, nullptr)); + BOOST_CHECK(m_node.chainman->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 e76b07e6059c..698b51116ff6 100644 --- a/src/test/evo_deterministicmns_tests.cpp +++ b/src/test/evo_deterministicmns_tests.cpp @@ -220,7 +220,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(tip_height(), nHeight); BOOST_REQUIRE(block->GetHash() != tip_hash()); BOOST_REQUIRE(!dmnman.GetListAtChainTip().HasMN(tx.GetHash())); @@ -230,7 +230,7 @@ void FuncDIP3Activation(TestChainSetup& setup) BOOST_CHECK_EQUAL(tip_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)); sync_dmn_tip(); BOOST_CHECK_EQUAL(tip_height(), nHeight + 2); BOOST_CHECK_EQUAL(block->GetHash(), tip_hash()); @@ -261,7 +261,7 @@ void FuncV19Activation(TestChainSetup& setup) int nHeight = tip_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(tip_index(), chainman.GetConsensus(), Consensus::DEPLOYMENT_V19)); ++nHeight; BOOST_CHECK_EQUAL(tip_height(), nHeight); @@ -279,7 +279,7 @@ void FuncV19Activation(TestChainSetup& setup) auto tx_upreg = CreateProUpRegTx(chainman, 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(tip_index(), chainman.GetConsensus(), Consensus::DEPLOYMENT_V19)); ++nHeight; BOOST_CHECK_EQUAL(tip_height(), nHeight); @@ -298,7 +298,7 @@ void FuncV19Activation(TestChainSetup& setup) const auto spend_coins = BuildSimpleUtxoMap({MakeTransactionRef(tx_reg)}); SignTransaction(tx_spend, spend_coins, collateral_key); 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(tip_index(), chainman.GetConsensus(), Consensus::DEPLOYMENT_V19)); ++nHeight; BOOST_CHECK_EQUAL(tip_height(), nHeight); @@ -744,7 +744,7 @@ void FuncMNPaymentMultiplicityV24Boundary(TestChainSetup& setup) for (int i = 0; i < 2000; ++i) { CBlock good = setup.CreateBlock({}, coinbase_pk, chainman.ActiveChainstate()); if (auto dup = find_duplicate(good)) return {good, *dup}; - BOOST_REQUIRE(chainman.ProcessNewBlock(std::make_shared(good), /*force_processing=*/true, nullptr)); + BOOST_REQUIRE(chainman.ProcessNewBlock(std::make_shared(good), /*force_processing=*/true, true, nullptr)); sync_dmn_tip(); } BOOST_REQUIRE_MESSAGE(false, "expected owner/operator collision to yield a duplicate coinbase output"); @@ -789,7 +789,7 @@ void FuncMNPaymentMultiplicityV24Boundary(TestChainSetup& setup) // testing post-v24 behaviour by accident. BOOST_REQUIRE(!DeploymentActiveAfter(tip_index(), chainman, Consensus::DEPLOYMENT_V24)); const CBlock merged = build_merge_cheat(good, dup); - BOOST_REQUIRE(chainman.ProcessNewBlock(std::make_shared(merged), /*force_processing=*/true, nullptr)); + BOOST_REQUIRE(chainman.ProcessNewBlock(std::make_shared(merged), /*force_processing=*/true, true, nullptr)); BOOST_CHECK_EQUAL(tip_hash(), merged.GetHash()); sync_dmn_tip(); } @@ -808,12 +808,12 @@ void FuncMNPaymentMultiplicityV24Boundary(TestChainSetup& setup) const auto [good, dup] = mine_until_duplicate(); const CBlock merged = build_merge_cheat(good, dup); const uint256 tip_before = tip_hash(); - chainman.ProcessNewBlock(std::make_shared(merged), /*force_processing=*/true, nullptr); + chainman.ProcessNewBlock(std::make_shared(merged), /*force_processing=*/true, true, nullptr); BOOST_CHECK(tip_hash() == tip_before); BOOST_CHECK(tip_hash() != merged.GetHash()); // The faithful block (both identical outputs present) connects. - BOOST_REQUIRE(chainman.ProcessNewBlock(std::make_shared(good), /*force_processing=*/true, nullptr)); + BOOST_REQUIRE(chainman.ProcessNewBlock(std::make_shared(good), /*force_processing=*/true, true, nullptr)); BOOST_CHECK_EQUAL(tip_hash(), good.GetHash()); } } @@ -1276,7 +1276,7 @@ void FuncTestMempoolReorg(TestChainSetup& setup) auto tx_collateral = CreateSpendTx(chainman, utxos, scriptCollateral, dmn_types::Regular.collat_amount, 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(tip_index()); BOOST_CHECK_EQUAL(tip_height(), nHeight + 1); BOOST_CHECK_EQUAL(block->GetHash(), tip_hash()); @@ -1450,7 +1450,7 @@ void FuncTestMempoolProRegReplacementUpdateConflict(TestChainSetup& setup) // Fund and mine an external collateral, then register MN X against it. auto tx_collateral = CreateSpendTx(chainman, utxos, scriptCollateral, dmn_types::Regular.collat_amount, 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)); sync_dmn_tip(); BOOST_CHECK_EQUAL(tip_height(), nHeight + 1); @@ -1459,7 +1459,7 @@ void FuncTestMempoolProRegReplacementUpdateConflict(TestChainSetup& setup) ownerKey, operatorKey, collateralKey, setup.coinbaseKey); const uint256 proTxHash = 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)); sync_dmn_tip(); BOOST_CHECK_EQUAL(tip_height(), nHeight + 2); BOOST_REQUIRE(dmnman.GetListAtChainTip().HasMN(proTxHash)); @@ -1554,7 +1554,7 @@ void FuncVerifyDB(TestChainSetup& setup) auto tx_collateral = CreateSpendTx(chainman, utxos, scriptCollateral, dmn_types::Regular.collat_amount, 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)); sync_dmn_tip(); BOOST_CHECK_EQUAL(tip_height(), nHeight + 1); BOOST_CHECK_EQUAL(block->GetHash(), tip_hash()); @@ -1565,7 +1565,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)); sync_dmn_tip(); BOOST_CHECK_EQUAL(tip_height(), nHeight + 2); BOOST_CHECK_EQUAL(block->GetHash(), tip_hash()); @@ -1577,7 +1577,7 @@ void FuncVerifyDB(TestChainSetup& setup) auto proUpRevTx = CreateProUpRevTx(chainman, 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)); sync_dmn_tip(); BOOST_CHECK_EQUAL(tip_height(), nHeight + 3); BOOST_CHECK_EQUAL(block->GetHash(), tip_hash()); diff --git a/src/test/fuzz/bitdeque.cpp b/src/test/fuzz/bitdeque.cpp new file mode 100644 index 000000000000..65f5cb3fd047 --- /dev/null +++ b/src/test/fuzz/bitdeque.cpp @@ -0,0 +1,541 @@ +// 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; + } + + LIMITED_WHILE(provider.remaining_bytes() > 0, 900) + { + { + 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..68358f7022af 100644 --- a/src/test/fuzz/pow.cpp +++ b/src/test/fuzz/pow.cpp @@ -83,3 +83,44 @@ 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)); + } + auto last_block{blocks.back().get()}; + CBlockHeader next_header; + next_header.nVersion = version; + next_header.nTime = new_time; + next_header.nBits = nbits; + unsigned int new_nbits{GetNextWorkRequired(last_block, &next_header, 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 4e832f0ac885..5f891ea7218f 100644 --- a/src/test/fuzz/utxo_snapshot.cpp +++ b/src/test/fuzz/utxo_snapshot.cpp @@ -58,7 +58,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..b3ded9fb8fb9 --- /dev/null +++ b/src/test/headers_sync_chainwork_tests.cpp @@ -0,0 +1,153 @@ +// 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; + + auto hashes_of = [](const std::vector& chain) { + std::vector hashes; + hashes.reserve(chain.size()); + for (const auto& h : chain) hashes.push_back(h.GetHash()); + return hashes; + }; + + // 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()}, {first_chain.front().GetHash()}, true); + // Pretend the first header is still "full", so we don't abort. + auto result = hss->ProcessNextHeaders(headers_batch, hashes_of(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, hashes_of(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, hashes_of(first_chain), true); + BOOST_CHECK(hss->GetState() == HeadersSyncState::State::REDOWNLOAD); + + result = hss->ProcessNextHeaders(first_chain, hashes_of(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()}, {second_chain.front().GetHash()}, 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, hashes_of(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 46005a2df037..9b4675331147 100644 --- a/src/test/miner_tests.cpp +++ b/src/test/miner_tests.cpp @@ -712,7 +712,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 b5853f54de03..ea96459ab268 100644 --- a/src/test/pow_tests.cpp +++ b/src/test/pow_tests.cpp @@ -2,6 +2,7 @@ // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. +#include #include #include #include @@ -65,6 +66,57 @@ BOOST_AUTO_TEST_CASE(get_next_work) BOOST_CHECK_EQUAL(GetNextWorkRequired(blockIndexLast, &blockHeader, chainParamsDev->GetConsensus()), 0x207fffffU); // Block #123457 has 0x207fffff } +/* Test PermittedDifficultyTransition, the (non-consensus) anti-DoS heuristic + * used by headers sync. It enforces a +/-4x bound on the per-pair difficulty + * only below nPowKGWHeight (Bitcoin-style retargeting); from nPowKGWHeight + * onwards (KGW/DGW) it has no useful bound and always returns true, and it + * also always returns true on networks that allow min-difficulty blocks. */ +BOOST_AUTO_TEST_CASE(permitted_difficulty_transition) +{ + const auto chainParams = CreateChainParams(*m_node.args, CBaseChainParams::MAIN); + const Consensus::Params& params = chainParams->GetConsensus(); + + // The 4x check is only reached on a chain without min-difficulty blocks, + // and only below nPowKGWHeight; pick a height in that range. + BOOST_REQUIRE(!params.fPowAllowMinDifficultyBlocks); + const int64_t height{params.nPowKGWHeight / 2}; + + // Base difficulty with a small mantissa so that the +/-4x boundary targets + // are exactly representable in the compact nBits format; the >4x rejection + // cases need only fall clearly outside the window. + const uint32_t base_nbits{0x1a010000U}; + arith_uint256 base_target; + base_target.SetCompact(base_nbits); + + arith_uint256 t; + t = base_target; t *= 4; const uint32_t easier_4x{t.GetCompact()}; + t = base_target; t *= 5; const uint32_t easier_5x{t.GetCompact()}; + t = base_target; t /= arith_uint256(4); const uint32_t harder_4x{t.GetCompact()}; + t = base_target; t /= arith_uint256(5); const uint32_t harder_5x{t.GetCompact()}; + + // No change, and exactly +/-4x, are permitted. + BOOST_CHECK(PermittedDifficultyTransition(params, height, base_nbits, base_nbits)); + BOOST_CHECK(PermittedDifficultyTransition(params, height, base_nbits, easier_4x)); + BOOST_CHECK(PermittedDifficultyTransition(params, height, base_nbits, harder_4x)); + + // More than 4x easier or harder is rejected (exercises both return-false paths). + BOOST_CHECK(!PermittedDifficultyTransition(params, height, base_nbits, easier_5x)); + BOOST_CHECK(!PermittedDifficultyTransition(params, height, base_nbits, harder_5x)); + + // From nPowKGWHeight onwards the bound is not enforced: an arbitrarily + // large difficulty jump is permitted. + BOOST_CHECK(PermittedDifficultyTransition(params, params.nPowKGWHeight, base_nbits, easier_5x)); + BOOST_CHECK(PermittedDifficultyTransition(params, params.nPowKGWHeight, base_nbits, harder_5x)); + + // Networks that allow min-difficulty blocks (testnet/devnet) skip the + // check entirely, even below nPowKGWHeight. + for (const auto& chain : {CBaseChainParams::TESTNET, CBaseChainParams::DEVNET}) { + const auto cp = CreateChainParams(*m_node.args, chain); + BOOST_REQUIRE(cp->GetConsensus().fPowAllowMinDifficultyBlocks); + BOOST_CHECK(PermittedDifficultyTransition(cp->GetConsensus(), /*height=*/1, base_nbits, easier_5x)); + } +} + /* Test the constraint on the upper bound for next work */ // BOOST_AUTO_TEST_CASE(get_next_work_pow_limit) // { diff --git a/src/test/util/mining.cpp b/src/test/util/mining.cpp index 31f89130c34a..ca65f046e719 100644 --- a/src/test/util/mining.cpp +++ b/src/test/util/mining.cpp @@ -71,7 +71,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}; @@ -127,7 +127,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 6355676e9c88..3c387f08d364 100644 --- a/src/test/util/setup_common.cpp +++ b/src/test/util/setup_common.cpp @@ -568,7 +568,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 2939888582ac..396b7fb67b3a 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 555c87c09b96..b91d223fe4d4 100644 --- a/src/test/validation_block_tests.cpp +++ b/src/test/validation_block_tests.cpp @@ -100,7 +100,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; } @@ -157,7 +157,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) @@ -180,13 +180,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); } } @@ -213,7 +213,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()}; @@ -229,7 +229,7 @@ BOOST_AUTO_TEST_CASE(checkblock_accept_known_hash) bool newblock = false; BOOST_REQUIRE(m_node.chainman->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); @@ -258,7 +258,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 fcbdd14b7ab3..b6a0e64a3b42 100644 --- a/src/test/validation_chainstate_tests.cpp +++ b/src/test/validation_chainstate_tests.cpp @@ -149,7 +149,7 @@ BOOST_FIXTURE_TEST_CASE(chainstate_update_tip, TestChain100Setup) bool checked = CheckBlock(*pblockone, state, chainparams.GetConsensus()); BOOST_CHECK(checked); bool accepted = chainman.AcceptBlock( - pblockone, state, &pindex, true, nullptr, &newblock); + pblockone, state, &pindex, true, nullptr, &newblock, true); BOOST_CHECK(accepted); } @@ -242,7 +242,7 @@ BOOST_FIXTURE_TEST_CASE(chainstate_connectblock_bls_scheme, V19AboveSnapshotSetu CBlockIndex* pindex = nullptr; bool newblock = false; BOOST_REQUIRE(CheckBlock(*pblock, state, Params().GetConsensus())); - BOOST_REQUIRE(m_node.chainman->AcceptBlock(pblock, state, &pindex, true, nullptr, &newblock)); + BOOST_REQUIRE(m_node.chainman->AcceptBlock(pblock, state, &pindex, true, nullptr, &newblock, true)); } BOOST_REQUIRE(background_cs.ActivateBestChain(state, pblock)); diff --git a/src/test/validation_chainstatemanager_tests.cpp b/src/test/validation_chainstatemanager_tests.cpp index 50a8f8dc1145..0a4a0324c529 100644 --- a/src/test/validation_chainstatemanager_tests.cpp +++ b/src/test/validation_chainstatemanager_tests.cpp @@ -486,7 +486,7 @@ BOOST_FIXTURE_TEST_CASE(chainstatemanager_reconsider_block_candidates, SnapshotT CScript script_pub_key = CScript() << ToByteVector(coinbaseKey.GetPubKey()) << OP_CHECKSIG; CBlock fork_block = CreateBlock(/*txns=*/{}, script_pub_key, *background_chainstate); BOOST_REQUIRE_EQUAL(fork_block.hashPrevBlock, WITH_LOCK(::cs_main, return background_chainstate->m_chain.Tip()->GetBlockHash())); - BOOST_REQUIRE(chainman.ProcessNewBlock(std::make_shared(fork_block), /*force_processing=*/true, /*new_block=*/nullptr)); + BOOST_REQUIRE(chainman.ProcessNewBlock(std::make_shared(fork_block), /*force_processing=*/true, /*min_pow_checked=*/true, /*new_block=*/nullptr)); CBlockIndex* fork_index{WITH_LOCK(::cs_main, return chainman.m_blockman.LookupBlockIndex(fork_block.GetHash()))}; BOOST_REQUIRE(fork_index); 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 9fbd0725b67c..cc665e21ce95 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -3407,7 +3407,7 @@ static bool NotifyHeaderTip(Chainstate& chainstate) LOCKS_EXCLUDED(cs_main) { } // Send block tip changed notifications without cs_main if (fNotify) { - uiInterface.NotifyHeaderTip(GetSynchronizationState(fInitialBlockDownload), pindexHeader->nHeight, pindexHeader->nTime); + uiInterface.NotifyHeaderTip(GetSynchronizationState(fInitialBlockDownload), pindexHeader->nHeight, pindexHeader->nTime, false); GetMainSignals().NotifyHeaderTip(pindexHeader, fInitialBlockDownload); } return fNotify; @@ -4116,6 +4116,15 @@ bool CheckBlock(const CBlock& block, BlockValidationState& state, const Consensu return true; } +bool HasValidProofOfWork(const std::vector& headers, const std::vector& hashes, const Consensus::Params& consensusParams) +{ + assert(headers.size() == hashes.size()); + for (size_t i = 0; i < headers.size(); ++i) { + if (!CheckProofOfWork(hashes[i], headers[i].nBits, consensusParams)) return false; + } + return true; +} + bool IsBlockMutated(const CBlock& block) { BlockValidationState state; @@ -4142,6 +4151,16 @@ bool IsBlockMutated(const CBlock& block) return false; } +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(). @@ -4280,7 +4299,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); @@ -4383,6 +4402,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) @@ -4411,14 +4434,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())}; CheckBlockIndex(); if (!accepted) { @@ -4440,8 +4463,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 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 + // 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 ChainstateManager::AcceptBlock(const std::shared_ptr& pblock, BlockValidationState& state, CBlockIndex** ppindex, bool fRequested, const FlatFilePos* dbp, bool* fNewBlock, const uint256* known_hash) +bool ChainstateManager::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(); @@ -4459,7 +4507,7 @@ bool ChainstateManager::AcceptBlock(const std::shared_ptr& pblock, ASSERT_IF_DEBUG(!known_hash || *known_hash == block.GetHash()); const uint256 hash{known_hash ? *known_hash : block.GetHash()}; - bool accepted_header{AcceptBlockHeader(block, state, &pindex, hash)}; + bool accepted_header{AcceptBlockHeader(block, state, &pindex, min_pow_checked, hash)}; CheckBlockIndex(); if (!accepted_header) @@ -4548,7 +4596,7 @@ bool ChainstateManager::AcceptBlock(const std::shared_ptr& pblock, 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); @@ -4569,7 +4617,7 @@ bool ChainstateManager::ProcessNewBlock(const std::shared_ptr& blo bool ret = CheckBlock(*block, state, GetConsensus()); if (ret) { // Store to disk - ret = AcceptBlock(block, state, &pindex, force_processing, nullptr, new_block); + ret = AcceptBlock(block, state, &pindex, force_processing, nullptr, new_block, min_pow_checked); } if (!ret) { GetMainSignals().BlockChecked(*block, state); @@ -5049,7 +5097,7 @@ bool Chainstate::LoadGenesisBlock() params.DevNetGenesisBlock()); bool fCheckBlock = CheckBlock(*shared_pblock, state, params.GetConsensus()); assert(fCheckBlock); - if (!m_chainman.AcceptBlock(shared_pblock, state, nullptr, true, nullptr, nullptr)) + if (!m_chainman.AcceptBlock(shared_pblock, state, nullptr, true, nullptr, nullptr, true)) return false; } } catch (const std::runtime_error &e) { @@ -5141,7 +5189,7 @@ void ChainstateManager::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()) { @@ -5209,7 +5257,7 @@ void ChainstateManager::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 4d88869e9805..2f19b9700cc7 100644 --- a/src/validation.h +++ b/src/validation.h @@ -364,6 +364,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 std::vector& hashes, 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: @@ -901,14 +907,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 Chainstate; + /** Most recent headers presync progress update, for rate-limiting. */ + std::chrono::time_point m_last_presync_update GUARDED_BY(::cs_main) {}; + std::array m_warningcache GUARDED_BY(::cs_main); //! Returns nullptr if no snapshot has been loaded. @@ -1141,10 +1154,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. @@ -1153,10 +1171,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); /** * Sufficiently validate a block for disk storage (and store on disk). @@ -1166,6 +1185,8 @@ class ChainstateManager * peer. * @param[in] dbp The location on disk, if we are importing * this block from prior storage. + * @param[in] min_pow_checked True if proof-of-work anti-DoS checks have + * been done by caller for headers chain * @param[in] known_hash Optional precomputed block hash. * * @param[out] state The state of the block validation. @@ -1176,7 +1197,7 @@ class ChainstateManager * * @returns False if the block or header is invalid, or if saving to disk fails (likely a fatal error); true otherwise. */ - 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); void ReceivedBlockTransactions(const CBlock& block, CBlockIndex* pindexNew, const FlatFilePos& pos) EXCLUSIVE_LOCKS_REQUIRED(cs_main); @@ -1206,6 +1227,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); + //! When starting up, search the datadir for a chainstate based on a UTXO //! snapshot that is in the process of being validated. bool DetectSnapshotChainstate(CTxMemPool* mempool, bilingual_str& error) EXCLUSIVE_LOCKS_REQUIRED(::cs_main); 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 f90a264a05c3..029762fe96b8 100755 --- a/test/functional/p2p_compactblocks.py +++ b/test/functional/p2p_compactblocks.py @@ -646,6 +646,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. @@ -936,6 +957,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..e9e99b96d366 --- /dev/null +++ b/test/functional/p2p_headers_sync_with_minchainwork.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python3 +# 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""" + +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 + +import time + +NODE1_BLOCKS_REQUIRED = 15 +NODE2_BLOCKS_REQUIRED = 2047 + + +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 + 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() + self.reconnect_all() + self.sync_all() + + 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 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" + # 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.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 + 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 { + '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.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) + + 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("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 and node3 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) + + # Hold time constant before reconnecting so that no in-flight request + # predates the mocktime change, which would trigger download timeouts + # and disconnections once the sync is under way. + self.mocktime_all(int(time.time())) + self.reconnect_all() + + self.sync_blocks(timeout=300) # Ensure tips eventually agree + self.mocktime_all(0) + + + 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_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) 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 e7be82be6430..89a1e320cbfd 100755 --- a/test/functional/rpc_blockchain.py +++ b/test/functional/rpc_blockchain.py @@ -532,8 +532,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) @@ -541,10 +542,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 2cf351a52ae0..01bb6af02182 100755 --- a/test/functional/rpc_net.py +++ b/test/functional/rpc_net.py @@ -154,6 +154,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 56acde042f86..87c70b6bb350 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -235,6 +235,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',