Inside Bitcoin
Opening the selected file…
Inside Bitcoin
Opening the selected file…
The code behind the network, with room to understand it.
Release published 2026-10-28 · Snapshot fetched 2026-11-01. Latest stable release at fetch time; development branches and Git history are excluded. Only the selected code window and visible folders load here.
1// Copyright (c) 2025-present The Bitcoin Core developers2// Distributed under the MIT software license, see the accompanying3// file COPYING or http://www.opensource.org/licenses/mit-license.php.5#include <consensus/tx_check.h>6#include <consensus/validation.h>7#include <net.h>8#include <primitives/transaction.h>9#include <private_broadcast.h>10#include <test/fuzz/FuzzedDataProvider.h>11#include <test/fuzz/fuzz.h>12#include <test/fuzz/util.h>13#include <test/fuzz/util/net.h>14#include <test/util/setup_common.h>15#include <test/util/time.h>16#include <util/overflow.h>17#include <util/time.h>19#include <algorithm>20#include <ranges>21#include <unordered_map>22#include <unordered_set>24struct CTransactionRefHash {25size_t operator()(const CTransactionRef& tx) const26{27return static_cast<size_t>(tx->GetWitnessHash().ToUint256().GetUint64(0));28}29};31struct CTransactionRefComp {32bool operator()(const CTransactionRef& a, const CTransactionRef& b) const33{34return a->GetWitnessHash() == b->GetWitnessHash();35}36};38FUZZ_TARGET(private_broadcast)39{40FuzzedDataProvider fdp(buffer.data(), buffer.size());41FakeNodeClock clock_ctx{ConsumeTime(fdp)};43const size_t cap{fdp.ConsumeIntegralInRange<size_t>(1, 12)};44const size_t max_send_attempts{fdp.ConsumeIntegralInRange<size_t>(1, 12)};45PrivateBroadcast pb{cap, max_send_attempts};47// Random transactions that the test generated and passed to Add(). Trimmed by Remove() and Add() cleanup.48// The values are the number of times a transaction was picked for sending.49std::unordered_map<CTransactionRef, size_t, CTransactionRefHash, CTransactionRefComp> transactions;50// The values are the number of times a transaction is planned to be sent.51std::unordered_map<CTransactionRef, size_t, CTransactionRefHash, CTransactionRefComp> planned_sends;52std::unordered_set<CTransactionRef, CTransactionRefHash, CTransactionRefComp> resolved_transactions;54// Transactions passed to PickTxForSend(), indexed by node id. Trimmed when55// Remove() is called or a transaction is reset or cleaned up by Add().56std::unordered_map<NodeId, CTransactionRef> nodes_sent_to;57std::unordered_set<NodeId> disconnected_nodes;59NodeId next_nodeid{0}; // Generate unique node ids.61const auto is_pending{[&](const auto& entry) {62return entry.second < std::min(planned_sends.at(entry.first), max_send_attempts);63}};65const auto ExistentOrNewNodeId = [&next_nodeid, &fdp](){66if (next_nodeid == 0 || fdp.ConsumeBool()) {67return next_nodeid++;68}69return fdp.ConsumeIntegralInRange<NodeId>(0, next_nodeid - 1);70};72LIMITED_WHILE (fdp.ConsumeBool(), 10000) {73CallOneOf(74fdp,75[&] { // Add()76CTransactionRef tx;77if (transactions.empty() || fdp.ConsumeBool()) {78tx = MakeTransactionRef(ConsumeTransaction(fdp, std::nullopt));79} else {80tx = PickIterator(fdp, transactions)->first;81}83auto cleanup_candidates{resolved_transactions};84for (const auto& [nodeid, sent_tx] : nodes_sent_to) {85if (!disconnected_nodes.contains(nodeid)) cleanup_candidates.erase(sent_tx);86}87std::erase_if(transactions, [&](const auto& entry) {88if (!cleanup_candidates.contains(entry.first) || is_pending(entry)) return false;89planned_sends.erase(entry.first);90resolved_transactions.erase(entry.first);91return true;92});93std::erase_if(nodes_sent_to, [&](const auto& entry) { return !transactions.contains(entry.second); });95const bool present_before{transactions.contains(tx)};96const auto res{pb.Add(tx)};97if (present_before) {98auto tx_it{transactions.find(tx)};99Assert(tx_it != transactions.end());100if (tx_it->second < max_send_attempts) {101Assert(res == PrivateBroadcast::AddResult::AlreadyPresent);102} else {103Assert(res == PrivateBroadcast::AddResult::Added);104tx_it->second = 0;105planned_sends[tx] = PrivateBroadcast::INITIAL_CONNECTION_COUNT;106resolved_transactions.erase(tx);107for (auto it = nodes_sent_to.begin(); it != nodes_sent_to.end();) {108if (CTransactionRefComp{}(it->second, tx)) {109it = nodes_sent_to.erase(it);110} else {111++it;112}113}114}115} else if (transactions.size() >= cap) {116Assert(res == PrivateBroadcast::AddResult::QueueFull);117} else {118Assert(res == PrivateBroadcast::AddResult::Added);119transactions.emplace(tx, 0);120planned_sends.emplace(tx, PrivateBroadcast::INITIAL_CONNECTION_COUNT);121}122},123[&] { // Remove()124if (transactions.empty()) {125return;126}127const auto transactions_it{PickIterator(fdp, transactions)};128const CTransactionRef& tx{transactions_it->first};130// Remove relevant entries from nodes_sent_to[] if any.131for (auto it = nodes_sent_to.begin(); it != nodes_sent_to.end();) {132if (CTransactionRefComp{}(it->second, tx)) {133it = nodes_sent_to.erase(it);134} else {135++it;136}137}139const auto remaining_sends{pb.Remove(tx)};141Assert(remaining_sends.has_value());142Assert(remaining_sends.value() == std::min(planned_sends.at(tx), max_send_attempts) - transactions_it->second);143Assert(!pb.Remove(tx).has_value());144planned_sends.erase(tx);145resolved_transactions.erase(tx);146transactions.erase(transactions_it);147},148[&] { // MarkResolved()149const CTransactionRef tx{transactions.empty() || fdp.ConsumeBool() ?150MakeTransactionRef(ConsumeTransaction(fdp, std::nullopt)) :151PickIterator(fdp, transactions)->first};152Assert(pb.MarkResolved(tx) == transactions.contains(tx));153if (transactions.contains(tx)) resolved_transactions.insert(tx);154},155[&] { // PickTxForSend()156// Only give pristine node ids to PickTxForSend() as required.157const NodeId will_send_to_nodeid{next_nodeid++};158const CService will_send_to_address{ConsumeService(fdp)};160const auto opt_tx{pb.PickTxForSend(will_send_to_nodeid, will_send_to_address)};162if (opt_tx.has_value()) {163Assert(transactions.contains(opt_tx.value()));165// "Number of times picked for sending" is the primary key in Priority's comparison166// (fewest sends = highest priority), so PickTxForSend() must return a transaction167// with the minimum send count of any in the queue. Ties are broken by state we168// don't model, so only check this key.169auto pending_transactions{transactions | std::views::filter(is_pending)};170const size_t min_picked{std::ranges::min_element(171pending_transactions, {}, [](const auto& el) { return el.second; })->second};172const auto picked_it{transactions.find(opt_tx.value())};173Assert(picked_it != transactions.end());174Assert(picked_it->second == min_picked); // picked the least-sent transaction175++picked_it->second; // PickTxForSend() recorded exactly one send177const auto& [_, inserted]{nodes_sent_to.emplace(will_send_to_nodeid, opt_tx.value())};178Assert(inserted);179} else {180Assert(std::ranges::none_of(transactions, is_pending));181}182},183[&] { // GetTxForNode()184const NodeId nodeid{ExistentOrNewNodeId()};186const auto opt_tx{pb.GetTxForNode(nodeid)};188if (nodes_sent_to.contains(nodeid)) {189Assert(opt_tx.has_value());190Assert(transactions.contains(opt_tx.value()));191Assert(opt_tx.value() == nodes_sent_to.at(nodeid));192} else {193Assert(!opt_tx.has_value());194}195},196[&] { // NodeConfirmedReception()197const NodeId nodeid{ExistentOrNewNodeId()};199pb.NodeConfirmedReception(nodeid);200},201[&] { // NodeDisconnected()202const NodeId nodeid{ExistentOrNewNodeId()};203pb.NodeDisconnected(nodeid);204if (nodes_sent_to.contains(nodeid)) disconnected_nodes.insert(nodeid);205},206[&] { // HavePendingTransactions()207if (std::ranges::any_of(transactions, is_pending)) {208Assert(pb.HavePendingTransactions());209} else {210Assert(!pb.HavePendingTransactions());211}212},213[&] { // TryGrantRetry()214if (transactions.empty()) return;215const auto tx_it{PickIterator(fdp, transactions)};216auto& count{planned_sends.at(tx_it->first)};217const bool can_retry{!resolved_transactions.contains(tx_it->first) && !is_pending(*tx_it) && count < max_send_attempts};218Assert(pb.TryGrantRetry(tx_it->first) == can_retry);219if (can_retry) ++count;220},221[&] { // GetStale()222const auto stale{pb.GetStale()};224Assert(stale.size() <= transactions.size());226for (const auto& stale_tx : stale) {227const auto it{transactions.find(stale_tx)};228Assert(it != transactions.end());229Assert(!resolved_transactions.contains(stale_tx));230Assert(planned_sends.at(stale_tx) < max_send_attempts);231}232},233[&] { // GetBroadcastInfo()234const auto all_broadcast_info{pb.GetBroadcastInfo()};236Assert(all_broadcast_info.size() == transactions.size() - resolved_transactions.size());238for (const auto& info : all_broadcast_info) {239const auto it{transactions.find(info.tx)};240Assert(it != transactions.end());241Assert(!resolved_transactions.contains(info.tx));242Assert(info.peers.size() == it->second); // exactly the sends we recorded243Assert(info.attempts_remaining == max_send_attempts - it->second);244}245},246[&] {247clock_ctx.set(ConsumeTime(fdp));248});249}250}
Independent explanations by MyBitcoinExplorer. Not affiliated with or endorsed by Bitcoin Core. Guides are educational and do not replace the source or its tests.
Bitcoin Core source © its contributors, distributed under the MIT license. Original notices and bundled third-party licenses are retained in the source tree.