Files
bitcoin/src/test/headers_sync_chainwork_tests.cpp
Hodlinator cc5dda1de3 headerssync: Make HeadersSyncState more flexible and move constants
Move calculated constants from the top of src/headerssync.cpp into src/kernel/chainparams.cpp.

Instead of being hardcoded to mainnet parameters, HeadersSyncState can now vary depending on chain or test. (This means we can reset TARGET_BLOCKS back to the nice round number of 15'000).

Signet and testnets got new HeadersSyncParams constants through temporarily altering headerssync-params.py with corresponding GENESIS_TIME and MINCHAINWORK_HEADERS (based off defaultAssumeValid block height comments, corresponding to nMinimumChainWork). Regtest doesn't have a default assume valid block height, so the values are copied from Testnet 4. Since the constants only affect memory usage, and have very low impact unless dealing with a largely malicious chain, it's not that critical to keep updating them for non-mainnet chains.

GENESIS_TIMEs (UTC):
Testnet3: 1296688602 = datetime(2011, 2, 2)
Testnet4: 1714777860 = datetime(2024, 5, 3)
Signet: 1598918400 = datetime(2020, 9, 1)
2025-09-12 22:28:41 +02:00

255 lines
12 KiB
C++

// Copyright (c) 2022-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.
#include <chain.h>
#include <chainparams.h>
#include <consensus/params.h>
#include <headerssync.h>
#include <net_processing.h>
#include <pow.h>
#include <test/util/setup_common.h>
#include <validation.h>
#include <cstddef>
#include <vector>
#include <boost/test/unit_test.hpp>
using State = HeadersSyncState::State;
// Standard set of checks common to all scenarios. Macro keeps failure lines at the call-site.
#define CHECK_RESULT(result_expression, hss, exp_state, exp_success, exp_request_more, \
exp_headers_size, exp_pow_validated_prev, exp_locator_hash) \
do { \
const auto result{result_expression}; \
BOOST_REQUIRE_EQUAL(hss.GetState(), exp_state); \
BOOST_CHECK_EQUAL(result.success, exp_success); \
BOOST_CHECK_EQUAL(result.request_more, exp_request_more); \
BOOST_CHECK_EQUAL(result.pow_validated_headers.size(), exp_headers_size); \
const std::optional<uint256> pow_validated_prev_opt{exp_pow_validated_prev}; \
if (pow_validated_prev_opt) { \
BOOST_CHECK_EQUAL(result.pow_validated_headers.at(0).hashPrevBlock, pow_validated_prev_opt); \
} else { \
BOOST_CHECK_EQUAL(exp_headers_size, 0); \
} \
const std::optional<uint256> locator_hash_opt{exp_locator_hash}; \
if (locator_hash_opt) { \
BOOST_CHECK_EQUAL(hss.NextHeadersRequestLocator().vHave.at(0), locator_hash_opt); \
} else { \
BOOST_CHECK_EQUAL(exp_state, State::FINAL); \
} \
} while (false)
constexpr size_t TARGET_BLOCKS{15'000};
constexpr arith_uint256 CHAIN_WORK{TARGET_BLOCKS * 2};
// Subtract MAX_HEADERS_RESULTS (2000 headers/message) + an arbitrary smaller
// value (123) so our redownload buffer is well below the number of blocks
// required to reach the CHAIN_WORK threshold, to behave similarly to mainnet.
constexpr size_t REDOWNLOAD_BUFFER_SIZE{TARGET_BLOCKS - (MAX_HEADERS_RESULTS + 123)};
constexpr size_t COMMITMENT_PERIOD{600}; // Somewhat close to mainnet.
struct HeadersGeneratorSetup : public RegTestingSetup {
const CBlock& genesis{Params().GenesisBlock()};
const CBlockIndex* chain_start{WITH_LOCK(::cs_main, return m_node.chainman->m_blockman.LookupBlockIndex(genesis.GetHash()))};
// Generate headers for two different chains (using differing merkle roots
// to ensure the headers are different).
const std::vector<CBlockHeader>& FirstChain()
{
// Block header hash target is half of max uint256 (2**256 / 2), expressible
// roughly as the coefficient 0x7fffff with the exponent 0x20 (32 bytes).
// This implies around every 2nd hash attempt should succeed, which
// is why CHAIN_WORK == TARGET_BLOCKS * 2.
assert(genesis.nBits == 0x207fffff);
// Subtract 1 since the genesis block also contributes work so we reach
// the CHAIN_WORK target.
static const auto first_chain{GenerateHeaders(/*count=*/TARGET_BLOCKS - 1, genesis.GetHash(),
genesis.nVersion, genesis.nTime, /*merkle_root=*/uint256::ZERO, genesis.nBits)};
return first_chain;
}
const std::vector<CBlockHeader>& SecondChain()
{
// Subtract 2 to keep total work below the target.
static const auto second_chain{GenerateHeaders(/*count=*/TARGET_BLOCKS - 2, genesis.GetHash(),
genesis.nVersion, genesis.nTime, /*merkle_root=*/uint256::ONE, genesis.nBits)};
return second_chain;
}
HeadersSyncState CreateState()
{
return {/*id=*/0,
Params().GetConsensus(),
HeadersSyncParams{
.commitment_period = COMMITMENT_PERIOD,
.redownload_buffer_size = REDOWNLOAD_BUFFER_SIZE,
},
chain_start,
/*minimum_required_work=*/CHAIN_WORK};
}
private:
/** 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.
*/
std::vector<CBlockHeader> GenerateHeaders(size_t count,
uint256 prev_hash, int32_t nVersion, uint32_t prev_time,
const uint256& merkle_root, uint32_t nBits);
};
void HeadersGeneratorSetup::FindProofOfWork(CBlockHeader& starting_header)
{
while (!CheckProofOfWork(starting_header.GetHash(), starting_header.nBits, Params().GetConsensus())) {
++starting_header.nNonce;
}
}
std::vector<CBlockHeader> HeadersGeneratorSetup::GenerateHeaders(
const size_t count, uint256 prev_hash, const int32_t nVersion,
uint32_t prev_time, const uint256& merkle_root, const uint32_t nBits)
{
std::vector<CBlockHeader> headers(count);
for (auto& next_header : headers) {
next_header.nVersion = nVersion;
next_header.hashPrevBlock = prev_hash;
next_header.hashMerkleRoot = merkle_root;
next_header.nTime = ++prev_time;
next_header.nBits = nBits;
FindProofOfWork(next_header);
prev_hash = next_header.GetHash();
}
return headers;
}
// 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.
// Then we deliver the second set of headers and verify that they fail
// processing (presumably due to commitments not matching).
// 2. Verify that repeating with the first set of headers in both phases is
// successful.
// 3. Repeat the second set of headers in both phases to demonstrate behavior
// when the chain a peer provides has too little work.
BOOST_FIXTURE_TEST_SUITE(headers_sync_chainwork_tests, HeadersGeneratorSetup)
BOOST_AUTO_TEST_CASE(sneaky_redownload)
{
const auto& first_chain{FirstChain()};
const auto& second_chain{SecondChain()};
// Feed the first chain to HeadersSyncState, by delivering 1 header
// initially and then the rest.
HeadersSyncState hss{CreateState()};
// Just feed one header and check state.
// Pretend the message is still "full", so we don't abort.
CHECK_RESULT(hss.ProcessNextHeaders({{first_chain.front()}}, /*full_headers_message=*/true),
hss, /*exp_state=*/State::PRESYNC,
/*exp_success*/true, /*exp_request_more=*/true,
/*exp_headers_size=*/0, /*exp_pow_validated_prev=*/std::nullopt,
/*exp_locator_hash=*/first_chain.front().GetHash());
// This chain should look valid, and we should have met the proof-of-work
// requirement during PRESYNC and transitioned to REDOWNLOAD.
CHECK_RESULT(hss.ProcessNextHeaders(std::span{first_chain}.subspan(1), true),
hss, /*exp_state=*/State::REDOWNLOAD,
/*exp_success*/true, /*exp_request_more=*/true,
/*exp_headers_size=*/0, /*exp_pow_validated_prev=*/std::nullopt,
/*exp_locator_hash=*/genesis.GetHash());
// Below is the number of commitment bits that must randomly match between
// the two chains for this test to spuriously fail. 1 / 2^25 =
// 1 in 33'554'432 (somewhat less due to HeadersSyncState::m_commit_offset).
static_assert(TARGET_BLOCKS / COMMITMENT_PERIOD == 25);
// Try to sneakily feed back the second chain during REDOWNLOAD.
CHECK_RESULT(hss.ProcessNextHeaders(second_chain, true),
hss, /*exp_state=*/State::FINAL,
/*exp_success*/false, // Foiled! We detected mismatching headers.
/*exp_request_more=*/false,
/*exp_headers_size=*/0, /*exp_pow_validated_prev=*/std::nullopt,
/*exp_locator_hash=*/std::nullopt);
}
BOOST_AUTO_TEST_CASE(happy_path)
{
const auto& first_chain{FirstChain()};
// Headers message that moves us to the next state doesn't need to be full.
for (const bool full_headers_message : {false, true}) {
// This time we feed the first chain twice.
HeadersSyncState hss{CreateState()};
// Sufficient work transitions us from PRESYNC to REDOWNLOAD:
const auto genesis_hash{genesis.GetHash()};
CHECK_RESULT(hss.ProcessNextHeaders(first_chain, full_headers_message),
hss, /*exp_state=*/State::REDOWNLOAD,
/*exp_success*/true, /*exp_request_more=*/true,
/*exp_headers_size=*/0, /*exp_pow_validated_prev=*/std::nullopt,
/*exp_locator_hash=*/genesis_hash);
// Process only so that the internal threshold isn't exceeded, meaning
// validated headers shouldn't be returned yet:
CHECK_RESULT(hss.ProcessNextHeaders({first_chain.begin(), REDOWNLOAD_BUFFER_SIZE}, true),
hss, /*exp_state=*/State::REDOWNLOAD,
/*exp_success*/true, /*exp_request_more=*/true,
/*exp_headers_size=*/0, /*exp_pow_validated_prev=*/std::nullopt,
/*exp_locator_hash=*/first_chain[REDOWNLOAD_BUFFER_SIZE - 1].GetHash());
// We start receiving headers for permanent storage before completing:
CHECK_RESULT(hss.ProcessNextHeaders({{first_chain[REDOWNLOAD_BUFFER_SIZE]}}, true),
hss, /*exp_state=*/State::REDOWNLOAD,
/*exp_success*/true, /*exp_request_more=*/true,
/*exp_headers_size=*/1, /*exp_pow_validated_prev=*/genesis_hash,
/*exp_locator_hash=*/first_chain[REDOWNLOAD_BUFFER_SIZE].GetHash());
// Feed in remaining headers, meeting the work threshold again and
// completing the REDOWNLOAD phase:
CHECK_RESULT(hss.ProcessNextHeaders({first_chain.begin() + REDOWNLOAD_BUFFER_SIZE + 1, first_chain.end()}, full_headers_message),
hss, /*exp_state=*/State::FINAL,
/*exp_success*/true, /*exp_request_more=*/false,
// All headers except the one already returned above:
/*exp_headers_size=*/first_chain.size() - 1, /*exp_pow_validated_prev=*/first_chain.front().GetHash(),
/*exp_locator_hash=*/std::nullopt);
}
}
BOOST_AUTO_TEST_CASE(too_little_work)
{
const auto& second_chain{SecondChain()};
// Verify that just trying to process the second chain would not succeed
// (too little work).
HeadersSyncState hss{CreateState()};
BOOST_REQUIRE_EQUAL(hss.GetState(), State::PRESYNC);
// Pretend just the first message is "full", so we don't abort.
CHECK_RESULT(hss.ProcessNextHeaders({{second_chain.front()}}, true),
hss, /*exp_state=*/State::PRESYNC,
/*exp_success*/true, /*exp_request_more=*/true,
/*exp_headers_size=*/0, /*exp_pow_validated_prev=*/std::nullopt,
/*exp_locator_hash=*/second_chain.front().GetHash());
// 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 cause
// the sync to end with no headers for acceptance.
CHECK_RESULT(hss.ProcessNextHeaders(std::span{second_chain}.subspan(1), false),
hss, /*exp_state=*/State::FINAL,
// Nevertheless, no validation errors should have been detected with the
// chain:
/*exp_success*/true,
/*exp_request_more=*/false,
/*exp_headers_size=*/0, /*exp_pow_validated_prev=*/std::nullopt,
/*exp_locator_hash=*/std::nullopt);
}
BOOST_AUTO_TEST_SUITE_END()