headers_sync_chainwork_tests.cpp
1 // Copyright (c) 2022-present The Bitcoin Core developers 2 // Distributed under the MIT software license, see the accompanying 3 // file COPYING or http://www.opensource.org/licenses/mit-license.php. 4 5 #include <chain.h> 6 #include <chainparams.h> 7 #include <consensus/params.h> 8 #include <headerssync.h> 9 #include <net_processing.h> 10 #include <pow.h> 11 #include <test/util/common.h> 12 #include <test/util/setup_common.h> 13 #include <validation.h> 14 15 #include <cstddef> 16 #include <vector> 17 18 #include <boost/test/unit_test.hpp> 19 20 using State = HeadersSyncState::State; 21 22 // Standard set of checks common to all scenarios. Macro keeps failure lines at the call-site. 23 #define CHECK_RESULT(result_expression, hss, exp_state, exp_success, exp_request_more, \ 24 exp_headers_size, exp_pow_validated_prev, exp_locator_hash) \ 25 do { \ 26 const auto result{result_expression}; \ 27 BOOST_REQUIRE_EQUAL(hss.GetState(), exp_state); \ 28 BOOST_CHECK_EQUAL(result.success, exp_success); \ 29 BOOST_CHECK_EQUAL(result.request_more, exp_request_more); \ 30 BOOST_CHECK_EQUAL(result.pow_validated_headers.size(), exp_headers_size); \ 31 const std::optional<uint256> pow_validated_prev_opt{exp_pow_validated_prev}; \ 32 if (pow_validated_prev_opt) { \ 33 BOOST_CHECK_EQUAL(result.pow_validated_headers.at(0).hashPrevBlock, pow_validated_prev_opt); \ 34 } else { \ 35 BOOST_CHECK_EQUAL(exp_headers_size, 0); \ 36 } \ 37 const std::optional<uint256> locator_hash_opt{exp_locator_hash}; \ 38 if (locator_hash_opt) { \ 39 BOOST_CHECK_EQUAL(hss.NextHeadersRequestLocator().vHave.at(0), locator_hash_opt); \ 40 } else { \ 41 BOOST_CHECK_EQUAL(exp_state, State::FINAL); \ 42 } \ 43 } while (false) 44 45 constexpr size_t TARGET_BLOCKS{15'000}; 46 constexpr arith_uint256 CHAIN_WORK{TARGET_BLOCKS * 2}; 47 48 // Subtract MAX_HEADERS_RESULTS (2000 headers/message) + an arbitrary smaller 49 // value (123) so our redownload buffer is well below the number of blocks 50 // required to reach the CHAIN_WORK threshold, to behave similarly to mainnet. 51 constexpr size_t REDOWNLOAD_BUFFER_SIZE{TARGET_BLOCKS - (MAX_HEADERS_RESULTS + 123)}; 52 constexpr size_t COMMITMENT_PERIOD{600}; // Somewhat close to mainnet. 53 54 struct HeadersGeneratorSetup : public RegTestingSetup { 55 const CBlock& genesis{Params().GenesisBlock()}; 56 CBlockIndex& chain_start{WITH_LOCK(::cs_main, return *Assert(m_node.chainman->m_blockman.LookupBlockIndex(genesis.GetHash())))}; 57 58 // Generate headers for two different chains (using differing merkle roots 59 // to ensure the headers are different). 60 const std::vector<CBlockHeader>& FirstChain() 61 { 62 // Block header hash target is half of max uint256 (2**256 / 2), expressible 63 // roughly as the coefficient 0x7fffff with the exponent 0x20 (32 bytes). 64 // This implies around every 2nd hash attempt should succeed, which 65 // is why CHAIN_WORK == TARGET_BLOCKS * 2. 66 assert(genesis.nBits == 0x207fffff); 67 68 // Subtract 1 since the genesis block also contributes work so we reach 69 // the CHAIN_WORK target. 70 static const auto first_chain{GenerateHeaders(/*count=*/TARGET_BLOCKS - 1, genesis.GetHash(), 71 genesis.nVersion, genesis.nTime, /*merkle_root=*/uint256::ZERO, genesis.nBits)}; 72 return first_chain; 73 } 74 const std::vector<CBlockHeader>& SecondChain() 75 { 76 // Subtract 2 to keep total work below the target. 77 static const auto second_chain{GenerateHeaders(/*count=*/TARGET_BLOCKS - 2, genesis.GetHash(), 78 genesis.nVersion, genesis.nTime, /*merkle_root=*/uint256::ONE, genesis.nBits)}; 79 return second_chain; 80 } 81 82 HeadersSyncState CreateState() 83 { 84 return {/*id=*/0, 85 Params().GetConsensus(), 86 HeadersSyncParams{ 87 .commitment_period = COMMITMENT_PERIOD, 88 .redownload_buffer_size = REDOWNLOAD_BUFFER_SIZE, 89 }, 90 chain_start, 91 /*minimum_required_work=*/CHAIN_WORK}; 92 } 93 94 private: 95 /** Search for a nonce to meet (regtest) proof of work */ 96 void FindProofOfWork(CBlockHeader& starting_header); 97 /** 98 * Generate headers in a chain that build off a given starting hash, using 99 * the given nVersion, advancing time by 1 second from the starting 100 * prev_time, and with a fixed merkle root hash. 101 */ 102 std::vector<CBlockHeader> GenerateHeaders(size_t count, 103 uint256 prev_hash, int32_t nVersion, uint32_t prev_time, 104 const uint256& merkle_root, uint32_t nBits); 105 }; 106 107 void HeadersGeneratorSetup::FindProofOfWork(CBlockHeader& starting_header) 108 { 109 while (!CheckProofOfWork(starting_header.GetHash(), starting_header.nBits, Params().GetConsensus())) { 110 ++starting_header.nNonce; 111 } 112 } 113 114 std::vector<CBlockHeader> HeadersGeneratorSetup::GenerateHeaders( 115 const size_t count, uint256 prev_hash, const int32_t nVersion, 116 uint32_t prev_time, const uint256& merkle_root, const uint32_t nBits) 117 { 118 std::vector<CBlockHeader> headers(count); 119 for (auto& next_header : headers) { 120 next_header.nVersion = nVersion; 121 next_header.hashPrevBlock = prev_hash; 122 next_header.hashMerkleRoot = merkle_root; 123 next_header.nTime = ++prev_time; 124 next_header.nBits = nBits; 125 126 FindProofOfWork(next_header); 127 prev_hash = next_header.GetHash(); 128 } 129 return headers; 130 } 131 132 // In this test, we construct two sets of headers from genesis, one with 133 // sufficient proof of work and one without. 134 // 1. We deliver the first set of headers and verify that the headers sync state 135 // updates to the REDOWNLOAD phase successfully. 136 // Then we deliver the second set of headers and verify that they fail 137 // processing (presumably due to commitments not matching). 138 // 2. Verify that repeating with the first set of headers in both phases is 139 // successful. 140 // 3. Repeat the second set of headers in both phases to demonstrate behavior 141 // when the chain a peer provides has too little work. 142 BOOST_FIXTURE_TEST_SUITE(headers_sync_chainwork_tests, HeadersGeneratorSetup) 143 144 BOOST_AUTO_TEST_CASE(sneaky_redownload) 145 { 146 const auto& first_chain{FirstChain()}; 147 const auto& second_chain{SecondChain()}; 148 149 // Feed the first chain to HeadersSyncState, by delivering 1 header 150 // initially and then the rest. 151 HeadersSyncState hss{CreateState()}; 152 153 // Just feed one header and check state. 154 // Pretend the message is still "full", so we don't abort. 155 CHECK_RESULT(hss.ProcessNextHeaders({{first_chain.front()}}, /*full_headers_message=*/true), 156 hss, /*exp_state=*/State::PRESYNC, 157 /*exp_success=*/true, /*exp_request_more=*/true, 158 /*exp_headers_size=*/0, /*exp_pow_validated_prev=*/std::nullopt, 159 /*exp_locator_hash=*/first_chain.front().GetHash()); 160 161 // This chain should look valid, and we should have met the proof-of-work 162 // requirement during PRESYNC and transitioned to REDOWNLOAD. 163 CHECK_RESULT(hss.ProcessNextHeaders(std::span{first_chain}.subspan(1), true), 164 hss, /*exp_state=*/State::REDOWNLOAD, 165 /*exp_success=*/true, /*exp_request_more=*/true, 166 /*exp_headers_size=*/0, /*exp_pow_validated_prev=*/std::nullopt, 167 /*exp_locator_hash=*/genesis.GetHash()); 168 169 // Below is the number of commitment bits that must randomly match between 170 // the two chains for this test to spuriously fail. 1 / 2^25 = 171 // 1 in 33'554'432 (somewhat less due to HeadersSyncState::m_commit_offset). 172 static_assert(TARGET_BLOCKS / COMMITMENT_PERIOD == 25); 173 174 // Try to sneakily feed back the second chain during REDOWNLOAD. 175 CHECK_RESULT(hss.ProcessNextHeaders(second_chain, true), 176 hss, /*exp_state=*/State::FINAL, 177 /*exp_success=*/false, // Foiled! We detected mismatching headers. 178 /*exp_request_more=*/false, 179 /*exp_headers_size=*/0, /*exp_pow_validated_prev=*/std::nullopt, 180 /*exp_locator_hash=*/std::nullopt); 181 } 182 183 BOOST_AUTO_TEST_CASE(happy_path) 184 { 185 const auto& first_chain{FirstChain()}; 186 187 // Headers message that moves us to the next state doesn't need to be full. 188 for (const bool full_headers_message : {false, true}) { 189 // This time we feed the first chain twice. 190 HeadersSyncState hss{CreateState()}; 191 192 // Sufficient work transitions us from PRESYNC to REDOWNLOAD: 193 const auto genesis_hash{genesis.GetHash()}; 194 CHECK_RESULT(hss.ProcessNextHeaders(first_chain, full_headers_message), 195 hss, /*exp_state=*/State::REDOWNLOAD, 196 /*exp_success=*/true, /*exp_request_more=*/true, 197 /*exp_headers_size=*/0, /*exp_pow_validated_prev=*/std::nullopt, 198 /*exp_locator_hash=*/genesis_hash); 199 200 // Process only so that the internal threshold isn't exceeded, meaning 201 // validated headers shouldn't be returned yet: 202 CHECK_RESULT(hss.ProcessNextHeaders({first_chain.begin(), REDOWNLOAD_BUFFER_SIZE}, true), 203 hss, /*exp_state=*/State::REDOWNLOAD, 204 /*exp_success=*/true, /*exp_request_more=*/true, 205 /*exp_headers_size=*/0, /*exp_pow_validated_prev=*/std::nullopt, 206 /*exp_locator_hash=*/first_chain[REDOWNLOAD_BUFFER_SIZE - 1].GetHash()); 207 208 // We start receiving headers for permanent storage before completing: 209 CHECK_RESULT(hss.ProcessNextHeaders({{first_chain[REDOWNLOAD_BUFFER_SIZE]}}, true), 210 hss, /*exp_state=*/State::REDOWNLOAD, 211 /*exp_success=*/true, /*exp_request_more=*/true, 212 /*exp_headers_size=*/1, /*exp_pow_validated_prev=*/genesis_hash, 213 /*exp_locator_hash=*/first_chain[REDOWNLOAD_BUFFER_SIZE].GetHash()); 214 215 // Feed in remaining headers, meeting the work threshold again and 216 // completing the REDOWNLOAD phase: 217 CHECK_RESULT(hss.ProcessNextHeaders({first_chain.begin() + REDOWNLOAD_BUFFER_SIZE + 1, first_chain.end()}, full_headers_message), 218 hss, /*exp_state=*/State::FINAL, 219 /*exp_success=*/true, /*exp_request_more=*/false, 220 // All headers except the one already returned above: 221 /*exp_headers_size=*/first_chain.size() - 1, /*exp_pow_validated_prev=*/first_chain.front().GetHash(), 222 /*exp_locator_hash=*/std::nullopt); 223 } 224 } 225 226 BOOST_AUTO_TEST_CASE(too_little_work) 227 { 228 const auto& second_chain{SecondChain()}; 229 230 // Verify that just trying to process the second chain would not succeed 231 // (too little work). 232 HeadersSyncState hss{CreateState()}; 233 BOOST_REQUIRE_EQUAL(hss.GetState(), State::PRESYNC); 234 235 // Pretend just the first message is "full", so we don't abort. 236 CHECK_RESULT(hss.ProcessNextHeaders({{second_chain.front()}}, true), 237 hss, /*exp_state=*/State::PRESYNC, 238 /*exp_success=*/true, /*exp_request_more=*/true, 239 /*exp_headers_size=*/0, /*exp_pow_validated_prev=*/std::nullopt, 240 /*exp_locator_hash=*/second_chain.front().GetHash()); 241 242 // Tell the sync logic that the headers message was not full, implying no 243 // more headers can be requested. For a low-work-chain, this should cause 244 // the sync to end with no headers for acceptance. 245 CHECK_RESULT(hss.ProcessNextHeaders(std::span{second_chain}.subspan(1), false), 246 hss, /*exp_state=*/State::FINAL, 247 // Nevertheless, no validation errors should have been detected with the 248 // chain: 249 /*exp_success=*/true, 250 /*exp_request_more=*/false, 251 /*exp_headers_size=*/0, /*exp_pow_validated_prev=*/std::nullopt, 252 /*exp_locator_hash=*/std::nullopt); 253 } 254 255 BOOST_AUTO_TEST_SUITE_END()