Bitcoin Core 32.99.0
P2P Digital Currency
private_broadcast.cpp
Go to the documentation of this file.
1// Copyright (c) 2025-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
7#include <net.h>
9#include <private_broadcast.h>
11#include <test/fuzz/fuzz.h>
12#include <test/fuzz/util.h>
13#include <test/fuzz/util/net.h>
15#include <test/util/time.h>
16#include <util/overflow.h>
17#include <util/time.h>
18
19#include <algorithm>
20#include <ranges>
21#include <unordered_map>
22#include <unordered_set>
23
25 size_t operator()(const CTransactionRef& tx) const
26 {
27 return static_cast<size_t>(tx->GetWitnessHash().ToUint256().GetUint64(0));
28 }
29};
30
32 bool operator()(const CTransactionRef& a, const CTransactionRef& b) const
33 {
34 return a->GetWitnessHash() == b->GetWitnessHash();
35 }
36};
37
38FUZZ_TARGET(private_broadcast)
39{
40 FuzzedDataProvider fdp(buffer.data(), buffer.size());
41 FakeNodeClock clock_ctx{ConsumeTime(fdp)};
42
43 const size_t cap{fdp.ConsumeIntegralInRange<size_t>(1, 12)};
44 const size_t max_send_attempts{fdp.ConsumeIntegralInRange<size_t>(1, 12)};
45 PrivateBroadcast pb{cap, max_send_attempts};
46
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.
49 std::unordered_map<CTransactionRef, size_t, CTransactionRefHash, CTransactionRefComp> transactions;
50 // The values are the number of times a transaction is planned to be sent.
51 std::unordered_map<CTransactionRef, size_t, CTransactionRefHash, CTransactionRefComp> planned_sends;
52 std::unordered_set<CTransactionRef, CTransactionRefHash, CTransactionRefComp> resolved_transactions;
53
54 // Transactions passed to PickTxForSend(), indexed by node id. Trimmed when
55 // Remove() is called or a transaction is reset or cleaned up by Add().
56 std::unordered_map<NodeId, CTransactionRef> nodes_sent_to;
57 std::unordered_set<NodeId> disconnected_nodes;
58
59 NodeId next_nodeid{0}; // Generate unique node ids.
60
61 const auto is_pending{[&](const auto& entry) {
62 return entry.second < std::min(planned_sends.at(entry.first), max_send_attempts);
63 }};
64
65 const auto ExistentOrNewNodeId = [&next_nodeid, &fdp](){
66 if (next_nodeid == 0 || fdp.ConsumeBool()) {
67 return next_nodeid++;
68 }
69 return fdp.ConsumeIntegralInRange<NodeId>(0, next_nodeid - 1);
70 };
71
72 LIMITED_WHILE (fdp.ConsumeBool(), 10000) {
74 fdp,
75 [&] { // Add()
77 if (transactions.empty() || fdp.ConsumeBool()) {
78 tx = MakeTransactionRef(ConsumeTransaction(fdp, std::nullopt));
79 } else {
80 tx = PickIterator(fdp, transactions)->first;
81 }
82
83 auto cleanup_candidates{resolved_transactions};
84 for (const auto& [nodeid, sent_tx] : nodes_sent_to) {
85 if (!disconnected_nodes.contains(nodeid)) cleanup_candidates.erase(sent_tx);
86 }
87 std::erase_if(transactions, [&](const auto& entry) {
88 if (!cleanup_candidates.contains(entry.first) || is_pending(entry)) return false;
89 planned_sends.erase(entry.first);
90 resolved_transactions.erase(entry.first);
91 return true;
92 });
93 std::erase_if(nodes_sent_to, [&](const auto& entry) { return !transactions.contains(entry.second); });
94
95 const bool present_before{transactions.contains(tx)};
96 const auto res{pb.Add(tx)};
97 if (present_before) {
98 auto tx_it{transactions.find(tx)};
99 Assert(tx_it != transactions.end());
100 if (tx_it->second < max_send_attempts) {
102 } else {
104 tx_it->second = 0;
106 resolved_transactions.erase(tx);
107 for (auto it = nodes_sent_to.begin(); it != nodes_sent_to.end();) {
108 if (CTransactionRefComp{}(it->second, tx)) {
109 it = nodes_sent_to.erase(it);
110 } else {
111 ++it;
112 }
113 }
114 }
115 } else if (transactions.size() >= cap) {
117 } else {
119 transactions.emplace(tx, 0);
120 planned_sends.emplace(tx, PrivateBroadcast::INITIAL_CONNECTION_COUNT);
121 }
122 },
123 [&] { // Remove()
124 if (transactions.empty()) {
125 return;
126 }
127 const auto transactions_it{PickIterator(fdp, transactions)};
128 const CTransactionRef& tx{transactions_it->first};
129
130 // Remove relevant entries from nodes_sent_to[] if any.
131 for (auto it = nodes_sent_to.begin(); it != nodes_sent_to.end();) {
132 if (CTransactionRefComp{}(it->second, tx)) {
133 it = nodes_sent_to.erase(it);
134 } else {
135 ++it;
136 }
137 }
138
139 const auto remaining_sends{pb.Remove(tx)};
140
141 Assert(remaining_sends.has_value());
142 Assert(remaining_sends.value() == std::min(planned_sends.at(tx), max_send_attempts) - transactions_it->second);
143 Assert(!pb.Remove(tx).has_value());
144 planned_sends.erase(tx);
145 resolved_transactions.erase(tx);
146 transactions.erase(transactions_it);
147 },
148 [&] { // MarkResolved()
149 const CTransactionRef tx{transactions.empty() || fdp.ConsumeBool() ?
150 MakeTransactionRef(ConsumeTransaction(fdp, std::nullopt)) :
151 PickIterator(fdp, transactions)->first};
152 Assert(pb.MarkResolved(tx) == transactions.contains(tx));
153 if (transactions.contains(tx)) resolved_transactions.insert(tx);
154 },
155 [&] { // PickTxForSend()
156 // Only give pristine node ids to PickTxForSend() as required.
157 const NodeId will_send_to_nodeid{next_nodeid++};
158 const CService will_send_to_address{ConsumeService(fdp)};
159
160 const auto opt_tx{pb.PickTxForSend(will_send_to_nodeid, will_send_to_address)};
161
162 if (opt_tx.has_value()) {
163 Assert(transactions.contains(opt_tx.value()));
164
165 // "Number of times picked for sending" is the primary key in Priority's comparison
166 // (fewest sends = highest priority), so PickTxForSend() must return a transaction
167 // with the minimum send count of any in the queue. Ties are broken by state we
168 // don't model, so only check this key.
169 auto pending_transactions{transactions | std::views::filter(is_pending)};
170 const size_t min_picked{std::ranges::min_element(
171 pending_transactions, {}, [](const auto& el) { return el.second; })->second};
172 const auto picked_it{transactions.find(opt_tx.value())};
173 Assert(picked_it != transactions.end());
174 Assert(picked_it->second == min_picked); // picked the least-sent transaction
175 ++picked_it->second; // PickTxForSend() recorded exactly one send
176
177 const auto& [_, inserted]{nodes_sent_to.emplace(will_send_to_nodeid, opt_tx.value())};
178 Assert(inserted);
179 } else {
180 Assert(std::ranges::none_of(transactions, is_pending));
181 }
182 },
183 [&] { // GetTxForNode()
184 const NodeId nodeid{ExistentOrNewNodeId()};
185
186 const auto opt_tx{pb.GetTxForNode(nodeid)};
187
188 if (nodes_sent_to.contains(nodeid)) {
189 Assert(opt_tx.has_value());
190 Assert(transactions.contains(opt_tx.value()));
191 Assert(opt_tx.value() == nodes_sent_to.at(nodeid));
192 } else {
193 Assert(!opt_tx.has_value());
194 }
195 },
196 [&] { // NodeConfirmedReception()
197 const NodeId nodeid{ExistentOrNewNodeId()};
198
199 pb.NodeConfirmedReception(nodeid);
200 },
201 [&] { // NodeDisconnected()
202 const NodeId nodeid{ExistentOrNewNodeId()};
203 pb.NodeDisconnected(nodeid);
204 if (nodes_sent_to.contains(nodeid)) disconnected_nodes.insert(nodeid);
205 },
206 [&] { // HavePendingTransactions()
207 if (std::ranges::any_of(transactions, is_pending)) {
208 Assert(pb.HavePendingTransactions());
209 } else {
210 Assert(!pb.HavePendingTransactions());
211 }
212 },
213 [&] { // TryGrantRetry()
214 if (transactions.empty()) return;
215 const auto tx_it{PickIterator(fdp, transactions)};
216 auto& count{planned_sends.at(tx_it->first)};
217 const bool can_retry{!resolved_transactions.contains(tx_it->first) && !is_pending(*tx_it) && count < max_send_attempts};
218 Assert(pb.TryGrantRetry(tx_it->first) == can_retry);
219 if (can_retry) ++count;
220 },
221 [&] { // GetStale()
222 const auto stale{pb.GetStale()};
223
224 Assert(stale.size() <= transactions.size());
225
226 for (const auto& stale_tx : stale) {
227 const auto it{transactions.find(stale_tx)};
228 Assert(it != transactions.end());
229 Assert(!resolved_transactions.contains(stale_tx));
230 Assert(planned_sends.at(stale_tx) < max_send_attempts);
231 }
232 },
233 [&] { // GetBroadcastInfo()
234 const auto all_broadcast_info{pb.GetBroadcastInfo()};
235
236 Assert(all_broadcast_info.size() == transactions.size() - resolved_transactions.size());
237
238 for (const auto& info : all_broadcast_info) {
239 const auto it{transactions.find(info.tx)};
240 Assert(it != transactions.end());
241 Assert(!resolved_transactions.contains(info.tx));
242 Assert(info.peers.size() == it->second); // exactly the sends we recorded
243 Assert(info.attempts_remaining == max_send_attempts - it->second);
244 }
245 },
246 [&] {
247 clock_ctx.set(ConsumeTime(fdp));
248 });
249 }
250}
#define Assert(val)
Identity function.
Definition: check.h:116
A combination of a network address (CNetAddr) and a (TCP) port.
Definition: netaddress.h:531
Helper to initialize the global NodeClock, let a duration elapse, and reset it after use in a test.
Definition: time.h:54
T ConsumeIntegralInRange(T min, T max)
Store a list of transactions to be broadcast privately.
@ QueueFull
Rejected: the queue is already at MAX_TRANSACTIONS.
@ AlreadyPresent
The transaction was already present with send attempts remaining; no change.
@ Added
The transaction was newly added or reset after exhausting its send attempts.
static constexpr size_t INITIAL_CONNECTION_COUNT
Number of connections to make for initial broadcast.
for(const auto &cache :caches)
LIMITED_WHILE(provider.remaining_bytes(), 10000)
int64_t NodeId
Definition: net.h:106
static CTransactionRef MakeTransactionRef(Tx &&txIn)
Definition: transaction.h:418
std::shared_ptr< const CTransaction > CTransactionRef
Definition: transaction.h:417
bool operator()(const CTransactionRef &a, const CTransactionRef &b) const
size_t operator()(const CTransactionRef &tx) const
FUZZ_TARGET(private_broadcast)
CService ConsumeService(FuzzedDataProvider &fuzzed_data_provider) noexcept
Definition: net.h:250
NodeSeconds ConsumeTime(FuzzedDataProvider &fuzzed_data_provider, const std::optional< int64_t > &min, const std::optional< int64_t > &max) noexcept
Definition: util.cpp:34
CMutableTransaction ConsumeTransaction(FuzzedDataProvider &fuzzed_data_provider, const std::optional< std::vector< Txid > > &prevout_txids, const int max_num_in, const int max_num_out) noexcept
Definition: util.cpp:42
size_t CallOneOf(FuzzedDataProvider &fuzzed_data_provider, Callables... callables)
Definition: util.h:37
auto PickIterator(FuzzedDataProvider &fuzzed_data_provider, Collection &col)
Definition: util.h:49
static int count
consteval auto _(util::TranslatedLiteral str)
Definition: translation.h:79