From a97f43d63a6e835bae20b0bc5d536df98f55d8a0 Mon Sep 17 00:00:00 2001 From: marcofleon Date: Tue, 13 Aug 2024 12:08:42 +0100 Subject: [PATCH] fuzz: Add harness for p2p headers sync --- src/test/fuzz/CMakeLists.txt | 1 + src/test/fuzz/p2p_headers_presync.cpp | 200 ++++++++++++++++++++++++++ 2 files changed, 201 insertions(+) create mode 100644 src/test/fuzz/p2p_headers_presync.cpp diff --git a/src/test/fuzz/CMakeLists.txt b/src/test/fuzz/CMakeLists.txt index 0bff9bf784a..be1d108c48d 100644 --- a/src/test/fuzz/CMakeLists.txt +++ b/src/test/fuzz/CMakeLists.txt @@ -72,6 +72,7 @@ add_executable(fuzz netbase_dns_lookup.cpp node_eviction.cpp p2p_handshake.cpp + p2p_headers_presync.cpp p2p_transport_serialization.cpp package_eval.cpp parse_hd_keypath.cpp diff --git a/src/test/fuzz/p2p_headers_presync.cpp b/src/test/fuzz/p2p_headers_presync.cpp new file mode 100644 index 00000000000..b86d03442dd --- /dev/null +++ b/src/test/fuzz/p2p_headers_presync.cpp @@ -0,0 +1,200 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { +constexpr uint32_t FUZZ_MAX_HEADERS_RESULTS{16}; + +class HeadersSyncSetup : public TestingSetup +{ + std::vector m_connections; + +public: + HeadersSyncSetup(const ChainType chain_type = ChainType::MAIN, + TestOpts opts = {}) + : TestingSetup(chain_type, opts) + { + PeerManager::Options peerman_opts; + node::ApplyArgsManOptions(*m_node.args, peerman_opts); + peerman_opts.max_headers_result = FUZZ_MAX_HEADERS_RESULTS; + m_node.peerman = PeerManager::make(*m_node.connman, *m_node.addrman, + m_node.banman.get(), *m_node.chainman, + *m_node.mempool, *m_node.warnings, peerman_opts); + + CConnman::Options options; + options.m_msgproc = m_node.peerman.get(); + m_node.connman->Init(options); + } + + void ResetAndInitialize() EXCLUSIVE_LOCKS_REQUIRED(NetEventsInterface::g_msgproc_mutex); + void SendMessage(FuzzedDataProvider& fuzzed_data_provider, CSerializedNetMsg&& msg) + EXCLUSIVE_LOCKS_REQUIRED(NetEventsInterface::g_msgproc_mutex); +}; + +void HeadersSyncSetup::ResetAndInitialize() +{ + m_connections.clear(); + auto& connman = static_cast(*m_node.connman); + connman.StopNodes(); + + NodeId id{0}; + std::vector conn_types = { + ConnectionType::OUTBOUND_FULL_RELAY, + ConnectionType::BLOCK_RELAY, + ConnectionType::INBOUND + }; + + for (auto conn_type : conn_types) { + CAddress addr{}; + m_connections.push_back(new CNode(id++, nullptr, addr, 0, 0, addr, "", conn_type, false)); + CNode& p2p_node = *m_connections.back(); + + connman.Handshake( + /*node=*/p2p_node, + /*successfully_connected=*/true, + /*remote_services=*/ServiceFlags(NODE_NETWORK | NODE_WITNESS), + /*local_services=*/ServiceFlags(NODE_NETWORK | NODE_WITNESS), + /*version=*/PROTOCOL_VERSION, + /*relay_txs=*/true); + + connman.AddTestNode(p2p_node); + } +} + +void HeadersSyncSetup::SendMessage(FuzzedDataProvider& fuzzed_data_provider, CSerializedNetMsg&& msg) +{ + auto& connman = static_cast(*m_node.connman); + CNode& connection = *PickValue(fuzzed_data_provider, m_connections); + + connman.FlushSendBuffer(connection); + (void)connman.ReceiveMsgFrom(connection, std::move(msg)); + connection.fPauseSend = false; + try { + connman.ProcessMessagesOnce(connection); + } catch (const std::ios_base::failure&) { + } + m_node.peerman->SendMessages(&connection); +} + +CBlockHeader ConsumeHeader(FuzzedDataProvider& fuzzed_data_provider, const uint256& prev_hash, uint32_t prev_nbits) +{ + CBlockHeader header; + header.nNonce = 0; + // Either use the previous difficulty or let the fuzzer choose + header.nBits = fuzzed_data_provider.ConsumeBool() ? + prev_nbits : + fuzzed_data_provider.ConsumeIntegralInRange(0x17058EBE, 0x1D00FFFF); + header.nTime = ConsumeTime(fuzzed_data_provider); + header.hashPrevBlock = prev_hash; + header.nVersion = fuzzed_data_provider.ConsumeIntegral(); + return header; +} + +CBlock ConsumeBlock(FuzzedDataProvider& fuzzed_data_provider, const uint256& prev_hash, uint32_t prev_nbits) +{ + auto header = ConsumeHeader(fuzzed_data_provider, prev_hash, prev_nbits); + // In order to reach the headers acceptance logic, the block is + // constructed in a way that will pass the mutation checks. + CBlock block{header}; + CMutableTransaction tx; + tx.vin.resize(1); + tx.vout.resize(1); + tx.vout[0].nValue = 0; + tx.vin[0].scriptSig.resize(2); + block.vtx.push_back(MakeTransactionRef(tx)); + block.hashMerkleRoot = block.vtx[0]->GetHash(); + return block; +} + +void FinalizeHeader(CBlockHeader& header) +{ + while (!CheckProofOfWork(header.GetHash(), header.nBits, Params().GetConsensus())) { + ++(header.nNonce); + } +} + +// Global setup works for this test as state modification (specifically in the +// block index) would indicate a bug. +HeadersSyncSetup* g_testing_setup; + +void initialize() +{ + static auto setup = MakeNoLogFileContext(ChainType::MAIN, {.extra_args = {"-checkpoints=0"}}); + g_testing_setup = setup.get(); +} +} // namespace + +FUZZ_TARGET(p2p_headers_presync, .init = initialize) +{ + ChainstateManager& chainman = *g_testing_setup->m_node.chainman; + + LOCK(NetEventsInterface::g_msgproc_mutex); + + g_testing_setup->ResetAndInitialize(); + + FuzzedDataProvider fuzzed_data_provider{buffer.data(), buffer.size()}; + + CBlockHeader base{Params().GenesisBlock()}; + SetMockTime(base.nTime); + + // The chain is just a single block, so this is equal to 1 + size_t original_index_size{WITH_LOCK(cs_main, return chainman.m_blockman.m_block_index.size())}; + + LIMITED_WHILE(fuzzed_data_provider.ConsumeBool(), 100) + { + auto finalized_block = [&]() { + CBlock block = ConsumeBlock(fuzzed_data_provider, base.GetHash(), base.nBits); + FinalizeHeader(block); + return block; + }; + + // Send low-work headers, compact blocks, and blocks + CallOneOf( + fuzzed_data_provider, + [&]() NO_THREAD_SAFETY_ANALYSIS { + // Send FUZZ_MAX_HEADERS_RESULTS headers + std::vector headers; + headers.resize(FUZZ_MAX_HEADERS_RESULTS); + for (CBlock& header : headers) { + header = ConsumeHeader(fuzzed_data_provider, base.GetHash(), base.nBits); + FinalizeHeader(header); + base = header; + } + + auto headers_msg = NetMsg::Make(NetMsgType::HEADERS, TX_WITH_WITNESS(headers)); + g_testing_setup->SendMessage(fuzzed_data_provider, std::move(headers_msg)); + }, + [&]() NO_THREAD_SAFETY_ANALYSIS { + // Send a compact block + auto block = finalized_block(); + CBlockHeaderAndShortTxIDs cmpct_block{block, fuzzed_data_provider.ConsumeIntegral()}; + + auto headers_msg = NetMsg::Make(NetMsgType::CMPCTBLOCK, TX_WITH_WITNESS(cmpct_block)); + g_testing_setup->SendMessage(fuzzed_data_provider, std::move(headers_msg)); + }, + [&]() NO_THREAD_SAFETY_ANALYSIS { + // Send a block + auto block = finalized_block(); + + auto headers_msg = NetMsg::Make(NetMsgType::BLOCK, TX_WITH_WITNESS(block)); + g_testing_setup->SendMessage(fuzzed_data_provider, std::move(headers_msg)); + }); + } + + // The headers/blocks sent in this test should never be stored, as the chains don't have the work required + // to meet the anti-DoS work threshold. So, if at any point the block index grew in size, then there's a bug + // in the headers pre-sync logic. + assert(WITH_LOCK(cs_main, return chainman.m_blockman.m_block_index.size()) == original_index_size); + + g_testing_setup->m_node.validation_signals->SyncWithValidationInterfaceQueue(); +}