From 3c58efe2acf0e7fbf1daaadd8bff2e0cfd3761e1 Mon Sep 17 00:00:00 2001 From: Eugene Siegel Date: Tue, 3 Feb 2026 17:04:02 -0500 Subject: [PATCH] fuzz: mine blocks and send headers for them in cmpctblock harness The blocks may include mempool and non-mempool transactions. If the block index was added to, reset the rng, mempool, and chainman. Also move FinalizeHeader from p2p_headers_presync.cpp to util.h so that the mining function can use it to create valid headers. --- src/test/fuzz/cmpctblock.cpp | 123 +++++++++++++++++++++++++- src/test/fuzz/p2p_headers_presync.cpp | 8 -- src/test/fuzz/util.h | 9 ++ 3 files changed, 130 insertions(+), 10 deletions(-) diff --git a/src/test/fuzz/cmpctblock.cpp b/src/test/fuzz/cmpctblock.cpp index a4ad037d374..de8cec7bca2 100644 --- a/src/test/fuzz/cmpctblock.cpp +++ b/src/test/fuzz/cmpctblock.cpp @@ -8,9 +8,11 @@ #include #include #include +#include #include #include #include +#include #include #include #include @@ -60,6 +62,14 @@ TestingSetup* g_setup; const CAmount AMOUNT_FEE{1000}; //! Cached coinbases that each iteration can copy and use. std::vector> g_mature_coinbase; +//! Constant value used to create valid headers. +uint32_t g_nBits; +//! One for each block the fuzzer generates. +struct BlockInfo { + std::shared_ptr block; + uint256 hash; + uint32_t height; +}; void ResetChainmanAndMempool(TestingSetup& setup) { @@ -98,6 +108,7 @@ void initialize_cmpctblock() { static const auto testing_setup = MakeNoLogFileContext(); g_setup = testing_setup.get(); + g_nBits = Params().GenesisBlock().nBits; ResetChainmanAndMempool(*g_setup); } @@ -113,6 +124,7 @@ FUZZ_TARGET(cmpctblock, .init = initialize_cmpctblock) auto& chainman = static_cast(*setup->m_node.chainman); chainman.ResetIbd(); chainman.DisableNextWrite(); + const size_t initial_index_size{WITH_LOCK(chainman.GetMutex(), return chainman.BlockIndex().size())}; AddrMan addrman{*setup->m_node.netgroupman, /*deterministic=*/true, /*consistency_check_ratio=*/0}; auto& connman = *static_cast(setup->m_node.connman.get()); @@ -137,6 +149,9 @@ FUZZ_TARGET(cmpctblock, .init = initialize_cmpctblock) connman.AddTestNode(p2p_node); } + // Stores blocks generated this iteration. + std::vector info; + // Coinbase UTXOs for this iteration. std::vector> mature_coinbase = g_mature_coinbase; @@ -147,7 +162,7 @@ FUZZ_TARGET(cmpctblock, .init = initialize_cmpctblock) tx_mut.version = fuzzed_data_provider.ConsumeBool() ? CTransaction::CURRENT_VERSION : TRUC_VERSION; tx_mut.nLockTime = fuzzed_data_provider.ConsumeBool() ? 0 : fuzzed_data_provider.ConsumeIntegral(); - // If the mempool is non-empty, sometimes choose a mempool outpoint. Otherwise, choose a coinbase. + // Choose an outpoint from the mempool, created blocks, or coinbases. CAmount amount_in; COutPoint outpoint; unsigned long mempool_size = mempool.size(); @@ -156,6 +171,14 @@ FUZZ_TARGET(cmpctblock, .init = initialize_cmpctblock) CTransactionRef tx = WITH_LOCK(mempool.cs, return mempool.txns_randomized[random_idx].second->GetSharedTx();); outpoint = COutPoint(tx->GetHash(), 0); amount_in = tx->vout[0].nValue; + } else if (info.size() != 0 && fuzzed_data_provider.ConsumeBool()) { + // These blocks (and txs) may be invalid, use a spent output, or not be in the main chain. + auto info_it = info.begin(); + std::advance(info_it, fuzzed_data_provider.ConsumeIntegralInRange(0, info.size() - 1)); + auto tx_it = info_it->block->vtx.begin(); + std::advance(tx_it, fuzzed_data_provider.ConsumeIntegralInRange(0, info_it->block->vtx.size() - 1)); + outpoint = COutPoint(tx_it->get()->GetHash(), 0); + amount_in = tx_it->get()->vout[0].nValue; } else { auto coinbase_it = mature_coinbase.begin(); std::advance(coinbase_it, fuzzed_data_provider.ConsumeIntegralInRange(0, mature_coinbase.size() - 1)); @@ -181,6 +204,78 @@ FUZZ_TARGET(cmpctblock, .init = initialize_cmpctblock) return tx; }; + auto create_block = [&]() { + uint256 prev; + uint32_t height; + + if (info.size() == 0 || fuzzed_data_provider.ConsumeBool()) { + LOCK(cs_main); + prev = chainman.ActiveChain().Tip()->GetBlockHash(); + height = chainman.ActiveChain().Height() + 1; + } else { + size_t index = fuzzed_data_provider.ConsumeIntegralInRange(0, info.size() - 1); + prev = info[index].hash; + height = info[index].height + 1; + } + + const auto new_time = WITH_LOCK(::cs_main, return chainman.ActiveChain().Tip()->GetMedianTimePast() + 1); + + CBlockHeader header; + header.nNonce = 0; + header.hashPrevBlock = prev; + header.nBits = g_nBits; + header.nTime = new_time; + header.nVersion = fuzzed_data_provider.ConsumeIntegral(); + + std::shared_ptr block = std::make_shared(); + *block = header; + + CMutableTransaction coinbase_tx; + coinbase_tx.vin.resize(1); + coinbase_tx.vin[0].prevout.SetNull(); + coinbase_tx.vin[0].scriptSig = CScript() << height << OP_0; + coinbase_tx.vout.resize(1); + coinbase_tx.vout[0].scriptPubKey = CScript() << OP_TRUE; + coinbase_tx.vout[0].nValue = COIN; + block->vtx.push_back(MakeTransactionRef(coinbase_tx)); + + const auto mempool_size = mempool.size(); + if (fuzzed_data_provider.ConsumeBool() && mempool_size != 0) { + // Add txns from the mempool. Since we do not include parents, it may be an invalid block. + size_t num_txns = fuzzed_data_provider.ConsumeIntegralInRange(1, mempool_size); + size_t random_idx = fuzzed_data_provider.ConsumeIntegralInRange(0, mempool_size - 1); + + LOCK(mempool.cs); + for (size_t i = random_idx; i < random_idx + num_txns; ++i) { + CTransactionRef mempool_tx = mempool.txns_randomized[i % mempool_size].second->GetSharedTx(); + block->vtx.push_back(mempool_tx); + } + } + + // Create and add (possibly invalid) txns that are not in the mempool. + if (fuzzed_data_provider.ConsumeBool()) { + size_t new_txns = fuzzed_data_provider.ConsumeIntegralInRange(1, 10); + for (size_t i = 0; i < new_txns; ++i) { + CTransactionRef non_mempool_tx = create_tx(); + block->vtx.push_back(non_mempool_tx); + } + } + + CBlockIndex* pindexPrev{WITH_LOCK(::cs_main, return chainman.m_blockman.LookupBlockIndex(prev))}; + chainman.GenerateCoinbaseCommitment(*block, pindexPrev); + + bool mutated; + block->hashMerkleRoot = BlockMerkleRoot(*block, &mutated); + FinalizeHeader(*block, chainman); + + BlockInfo block_info; + block_info.block = block; + block_info.hash = block->GetHash(); + block_info.height = height; + + return block_info; + }; + LIMITED_WHILE(fuzzed_data_provider.ConsumeBool(), 1000) { CSerializedNetMsg net_msg; @@ -191,6 +286,23 @@ FUZZ_TARGET(cmpctblock, .init = initialize_cmpctblock) CallOneOf( fuzzed_data_provider, + [&]() { + // Send a headers message for an existing block (if one exists). + size_t num_blocks = info.size(); + if (num_blocks == 0) { + sent_net_msg = false; + return; + } + + // Choose an existing block and send a HEADERS message for it. + size_t index = fuzzed_data_provider.ConsumeIntegralInRange(0, num_blocks - 1); + CBlock block = *info[index].block; + block.vtx.clear(); // No tx in HEADERS. + std::vector headers; + headers.emplace_back(block); + + net_msg = NetMsg::Make(NetMsgType::HEADERS, TX_WITH_WITNESS(headers)); + }, [&]() { // Send a sendcmpct message, optionally setting hb mode. bool hb = fuzzed_data_provider.ConsumeBool(); @@ -200,6 +312,12 @@ FUZZ_TARGET(cmpctblock, .init = initialize_cmpctblock) sent_sendcmpct = true; valid_sendcmpct = version == CMPCTBLOCKS_VERSION; }, + [&]() { + // Mine a block, but don't send it. + BlockInfo block_info = create_block(); + info.push_back(block_info); + sent_net_msg = false; + }, [&]() { // Send a transaction. CTransactionRef tx = create_tx(); @@ -247,9 +365,10 @@ FUZZ_TARGET(cmpctblock, .init = initialize_cmpctblock) setup->m_node.validation_signals->UnregisterAllValidationInterfaces(); connman.StopNodes(); + const size_t end_index_size{WITH_LOCK(chainman.GetMutex(), return chainman.BlockIndex().size())}; const uint64_t end_sequence{WITH_LOCK(mempool.cs, return mempool.GetSequence())}; - if (initial_sequence != end_sequence) { + if (initial_index_size != end_index_size || initial_sequence != end_sequence) { MakeRandDeterministicDANGEROUS(uint256::ZERO); ResetChainmanAndMempool(*g_setup); } diff --git a/src/test/fuzz/p2p_headers_presync.cpp b/src/test/fuzz/p2p_headers_presync.cpp index 704e447fe69..0e60cb2aded 100644 --- a/src/test/fuzz/p2p_headers_presync.cpp +++ b/src/test/fuzz/p2p_headers_presync.cpp @@ -8,7 +8,6 @@ #include #include #include -#include #include #include #include @@ -143,13 +142,6 @@ CBlock ConsumeBlock(FuzzedDataProvider& fuzzed_data_provider, const uint256& pre return block; } -void FinalizeHeader(CBlockHeader& header, const ChainstateManager& chainman) -{ - while (!CheckProofOfWork(header.GetHash(), header.nBits, chainman.GetParams().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; diff --git a/src/test/fuzz/util.h b/src/test/fuzz/util.h index aa1eed8badf..6fb8267b5c5 100644 --- a/src/test/fuzz/util.h +++ b/src/test/fuzz/util.h @@ -13,6 +13,7 @@ #include #include #include +#include #include #include