From 2ee4fafa3f4887e1f8cfc1a65a980f9c055549ca Mon Sep 17 00:00:00 2001 From: kevkevinpal Date: Sat, 14 Jun 2025 09:25:47 -0400 Subject: [PATCH] test: add fuzz test for private broadcast Co-authored-by: Greg Sanders Co-authored-by: Vasil Dimov --- src/test/fuzz/CMakeLists.txt | 1 + src/test/fuzz/private_broadcast.cpp | 202 ++++++++++++++++++++++++++++ src/test/fuzz/util.h | 14 +- 3 files changed, 212 insertions(+), 5 deletions(-) create mode 100644 src/test/fuzz/private_broadcast.cpp diff --git a/src/test/fuzz/CMakeLists.txt b/src/test/fuzz/CMakeLists.txt index a159ef7e11c..2d6e03ba803 100644 --- a/src/test/fuzz/CMakeLists.txt +++ b/src/test/fuzz/CMakeLists.txt @@ -94,6 +94,7 @@ add_executable(fuzz pow.cpp prevector.cpp primitives_transaction.cpp + private_broadcast.cpp process_message.cpp process_messages.cpp protocol.cpp diff --git a/src/test/fuzz/private_broadcast.cpp b/src/test/fuzz/private_broadcast.cpp new file mode 100644 index 00000000000..4db24f8e178 --- /dev/null +++ b/src/test/fuzz/private_broadcast.cpp @@ -0,0 +1,202 @@ +// Copyright (c) 2025-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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +struct CTransactionRefHash { + size_t operator()(const CTransactionRef& tx) const + { + return static_cast(tx->GetWitnessHash().ToUint256().GetUint64(0)); + } +}; + +struct CTransactionRefComp { + bool operator()(const CTransactionRef& a, const CTransactionRef& b) const + { + return a->GetWitnessHash() == b->GetWitnessHash(); + } +}; + +FUZZ_TARGET(private_broadcast) +{ + SeedRandomStateForTest(SeedRand::ZEROS); + FuzzedDataProvider fdp(buffer.data(), buffer.size()); + FakeNodeClock clock_ctx{ConsumeTime(fdp)}; + + PrivateBroadcast pb; + + // Random transaction that the test generated and passed to Add(). Trimmed when Remove() is called. + // The values are the number of times a transaction was picked for sending. + std::unordered_map transactions; + + // Ids of nodes that were passed to PickTxForSend(). Trimmed when Remove() is called. + std::unordered_set nodes_sent_to; + + // A subset of `nodes_sent_to`, node ids passed to NodeConfirmedReception(). Trimmed when Remove() is called. + std::unordered_set nodes_that_confirmed_reception; + + NodeId next_nodeid{0}; // Generate unique node ids. + + const auto ExistentOrNewNodeId = [&next_nodeid, &fdp](){ + if (next_nodeid == 0 || fdp.ConsumeBool()) { + return next_nodeid++; + } + return fdp.ConsumeIntegralInRange(0, next_nodeid - 1); + }; + + LIMITED_WHILE(fdp.ConsumeBool(), 10000) { + CallOneOf( + fdp, + [&] { // Add() + CTransactionRef tx; + bool from_transactions{false}; + if (transactions.empty() || fdp.ConsumeBool()) { + tx = MakeTransactionRef(ConsumeTransaction(fdp, std::nullopt)); + } else { + tx = PickIterator(fdp, transactions)->first; + from_transactions = true; + } + if (pb.Add(tx)) { + Assert(!from_transactions); + transactions.emplace(tx, 0); + } + }, + [&] { // Remove() + if (transactions.empty()) { + return; + } + const auto transactions_it{PickIterator(fdp, transactions)}; + const CTransactionRef& tx{transactions_it->first}; + + size_t num_nodes_that_confirmed_tx{0}; + + // Remove relevant entries from nodes_sent_to[] and nodes_that_confirmed_reception[] if any. + for (auto it = nodes_sent_to.begin(); it != nodes_sent_to.end();) { + const NodeId nodeid{*it}; + const auto opt_tx_for_node{pb.GetTxForNode(nodeid)}; + if (opt_tx_for_node.has_value() && opt_tx_for_node.value() == tx) { + it = nodes_sent_to.erase(it); + if (nodes_that_confirmed_reception.erase(nodeid) > 0) { + ++num_nodes_that_confirmed_tx; + } + } else { + ++it; + } + } + + const auto opt_num_confirmed{pb.Remove(tx)}; + + Assert(opt_num_confirmed.has_value()); + Assert(opt_num_confirmed.value() == num_nodes_that_confirmed_tx); + Assert(!pb.Remove(tx).has_value()); + transactions.erase(transactions_it); + }, + [&] { // PickTxForSend() + // Only give pristine node ids to PickTxForSend() as required. + const NodeId will_send_to_nodeid{next_nodeid++}; + const CService will_send_to_address{ConsumeService(fdp)}; + + const auto opt_tx{pb.PickTxForSend(will_send_to_nodeid, will_send_to_address)}; + + if (opt_tx.has_value()) { + Assert(transactions.contains(opt_tx.value())); + + // "Number of times picked for sending" is the primary key in Priority's comparison + // (fewest sends = highest priority), so PickTxForSend() must return a transaction + // with the minimum send count of any in the queue. Ties are broken by state we + // don't model, so only check this key. + const size_t min_picked{std::ranges::min_element( + transactions, {}, [](const auto& el) { return el.second; })->second}; + const auto picked_it{transactions.find(opt_tx.value())}; + Assert(picked_it != transactions.end()); + Assert(picked_it->second == min_picked); // picked the least-sent transaction + ++picked_it->second; // PickTxForSend() recorded exactly one send + + const auto& [_, inserted]{nodes_sent_to.emplace(will_send_to_nodeid)}; + Assert(inserted); + } else { + Assert(transactions.empty()); + } + }, + [&] { // GetTxForNode() + const NodeId nodeid{ExistentOrNewNodeId()}; + + const auto opt_tx{pb.GetTxForNode(nodeid)}; + + if (nodes_sent_to.contains(nodeid)) { + Assert(opt_tx.has_value()); + Assert(transactions.contains(opt_tx.value())); + } else { + Assert(!opt_tx.has_value()); + } + }, + [&] { // NodeConfirmedReception() + const NodeId nodeid{ExistentOrNewNodeId()}; + + pb.NodeConfirmedReception(nodeid); + + if (nodes_sent_to.contains(nodeid)) { + // nodeid was previously passed to PickTxForSend(), so NodeConfirmedReception() + // must have changed the internal state. Remember this to later check that + // DidNodeConfirmReception() works correctly. + nodes_that_confirmed_reception.emplace(nodeid); + } + }, + [&] { // DidNodeConfirmReception() + const NodeId nodeid{ExistentOrNewNodeId()}; + + const bool confirmed{pb.DidNodeConfirmReception(nodeid)}; + + if (nodes_that_confirmed_reception.contains(nodeid)) { + Assert(confirmed); + } else { + Assert(!confirmed); + } + }, + [&] { // HavePendingTransactions() + if (pb.HavePendingTransactions()) { + Assert(!transactions.empty()); + } else { + Assert(transactions.empty()); + } + }, + [&] { // GetStale() + const auto stale{pb.GetStale()}; + + Assert(stale.size() <= transactions.size()); + + for (const auto& stale_tx : stale) { + Assert(transactions.contains(stale_tx)); + } + }, + [&] { // GetBroadcastInfo() + const auto all_broadcast_info{pb.GetBroadcastInfo()}; + + Assert(all_broadcast_info.size() == transactions.size()); + + for (const auto& info : all_broadcast_info) { + const auto it{transactions.find(info.tx)}; + Assert(it != transactions.end()); + Assert(info.peers.size() == it->second); // exactly the sends we recorded + } + }, + [&] { + clock_ctx.set(ConsumeTime(fdp)); + }); + } +} diff --git a/src/test/fuzz/util.h b/src/test/fuzz/util.h index 335cefb710f..04edef0b7b1 100644 --- a/src/test/fuzz/util.h +++ b/src/test/fuzz/util.h @@ -45,14 +45,18 @@ size_t CallOneOf(FuzzedDataProvider& fuzzed_data_provider, Callables... callable return call_size; } +template +auto PickIterator(FuzzedDataProvider& fuzzed_data_provider, Collection& col) +{ + const auto sz{col.size()}; + assert(sz >= 1); + return std::next(col.begin(), fuzzed_data_provider.ConsumeIntegralInRange(0, sz - 1)); +} + template auto& PickValue(FuzzedDataProvider& fuzzed_data_provider, Collection& col) { - auto sz{col.size()}; - assert(sz >= 1); - auto it = col.begin(); - std::advance(it, fuzzed_data_provider.ConsumeIntegralInRange(0, sz - 1)); - return *it; + return *PickIterator(fuzzed_data_provider, col); } template