headers_sync_chainwork_tests.cpp raw

   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()
 256