Bitcoin Core 31.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{
41 FuzzedDataProvider fdp(buffer.data(), buffer.size());
42 FakeNodeClock clock_ctx{ConsumeTime(fdp)};
43
44 const size_t cap{fdp.ConsumeIntegralInRange<size_t>(1, 12)};
45 const size_t max_send_attempts{fdp.ConsumeIntegralInRange<size_t>(1, 12)};
46 PrivateBroadcast pb{cap, max_send_attempts};
47
48 // Random transaction that the test generated and passed to Add(). Trimmed when Remove() is called.
49 // The values are the number of times a transaction was picked for sending.
50 std::unordered_map<CTransactionRef, size_t, CTransactionRefHash, CTransactionRefComp> transactions;
51
52 // Transactions passed to PickTxForSend(), indexed by node id. Trimmed when
53 // Remove() is called or a transaction is reset by Add().
54 std::unordered_map<NodeId, CTransactionRef> nodes_sent_to;
55
56 // A subset of `nodes_sent_to`, node ids passed to NodeConfirmedReception().
57 // Trimmed when Remove() is called or a transaction is reset by Add().
58 std::unordered_set<NodeId> nodes_that_confirmed_reception;
59
60 NodeId next_nodeid{0}; // Generate unique node ids.
61
62 const auto is_pending{[max_send_attempts](const auto& entry) {
63 return entry.second < max_send_attempts;
64 }};
65
66 const auto ExistentOrNewNodeId = [&next_nodeid, &fdp](){
67 if (next_nodeid == 0 || fdp.ConsumeBool()) {
68 return next_nodeid++;
69 }
70 return fdp.ConsumeIntegralInRange<NodeId>(0, next_nodeid - 1);
71 };
72
73 LIMITED_WHILE (fdp.ConsumeBool(), 10000) {
75 fdp,
76 [&] { // Add()
78 if (transactions.empty() || fdp.ConsumeBool()) {
79 tx = MakeTransactionRef(ConsumeTransaction(fdp, std::nullopt));
80 } else {
81 tx = PickIterator(fdp, transactions)->first;
82 }
83
84 const bool present_before{transactions.contains(tx)};
85 const auto res{pb.Add(tx)};
86 if (present_before) {
87 auto tx_it{transactions.find(tx)};
88 Assert(tx_it != transactions.end());
89 if (is_pending(*tx_it)) {
91 } else {
93 tx_it->second = 0;
94 for (auto it = nodes_sent_to.begin(); it != nodes_sent_to.end();) {
95 if (CTransactionRefComp{}(it->second, tx)) {
96 nodes_that_confirmed_reception.erase(it->first);
97 it = nodes_sent_to.erase(it);
98 } else {
99 ++it;
100 }
101 }
102 }
103 } else if (transactions.size() >= cap) {
105 } else {
107 transactions.emplace(tx, 0);
108 }
109 },
110 [&] { // Remove()
111 if (transactions.empty()) {
112 return;
113 }
114 const auto transactions_it{PickIterator(fdp, transactions)};
115 const CTransactionRef& tx{transactions_it->first};
116
117 size_t num_nodes_that_confirmed_tx{0};
118
119 // Remove relevant entries from nodes_sent_to[] and nodes_that_confirmed_reception[] if any.
120 for (auto it = nodes_sent_to.begin(); it != nodes_sent_to.end();) {
121 const NodeId nodeid{it->first};
122 if (CTransactionRefComp{}(it->second, tx)) {
123 it = nodes_sent_to.erase(it);
124 if (nodes_that_confirmed_reception.erase(nodeid) > 0) {
125 ++num_nodes_that_confirmed_tx;
126 }
127 } else {
128 ++it;
129 }
130 }
131
132 const auto opt_num_confirmed{pb.Remove(tx)};
133
134 Assert(opt_num_confirmed.has_value());
135 Assert(opt_num_confirmed.value() == num_nodes_that_confirmed_tx);
136 Assert(!pb.Remove(tx).has_value());
137 transactions.erase(transactions_it);
138 },
139 [&] { // PickTxForSend()
140 // Only give pristine node ids to PickTxForSend() as required.
141 const NodeId will_send_to_nodeid{next_nodeid++};
142 const CService will_send_to_address{ConsumeService(fdp)};
143
144 const auto opt_tx{pb.PickTxForSend(will_send_to_nodeid, will_send_to_address)};
145
146 if (opt_tx.has_value()) {
147 Assert(transactions.contains(opt_tx.value()));
148
149 // "Number of times picked for sending" is the primary key in Priority's comparison
150 // (fewest sends = highest priority), so PickTxForSend() must return a transaction
151 // with the minimum send count of any in the queue. Ties are broken by state we
152 // don't model, so only check this key.
153 auto pending_transactions{transactions | std::views::filter(is_pending)};
154 const size_t min_picked{std::ranges::min_element(
155 pending_transactions, {}, [](const auto& el) { return el.second; })->second};
156 const auto picked_it{transactions.find(opt_tx.value())};
157 Assert(picked_it != transactions.end());
158 Assert(picked_it->second == min_picked); // picked the least-sent transaction
159 ++picked_it->second; // PickTxForSend() recorded exactly one send
160
161 const auto& [_, inserted]{nodes_sent_to.emplace(will_send_to_nodeid, opt_tx.value())};
162 Assert(inserted);
163 } else {
164 Assert(std::ranges::none_of(transactions, is_pending));
165 }
166 },
167 [&] { // GetTxForNode()
168 const NodeId nodeid{ExistentOrNewNodeId()};
169
170 const auto opt_tx{pb.GetTxForNode(nodeid)};
171
172 if (nodes_sent_to.contains(nodeid)) {
173 Assert(opt_tx.has_value());
174 Assert(transactions.contains(opt_tx.value()));
175 Assert(opt_tx.value() == nodes_sent_to.at(nodeid));
176 } else {
177 Assert(!opt_tx.has_value());
178 }
179 },
180 [&] { // NodeConfirmedReception()
181 const NodeId nodeid{ExistentOrNewNodeId()};
182
183 pb.NodeConfirmedReception(nodeid);
184
185 if (nodes_sent_to.contains(nodeid)) {
186 // nodeid was previously passed to PickTxForSend(), so NodeConfirmedReception()
187 // must have changed the internal state. Remember this to later check that
188 // DidNodeConfirmReception() works correctly.
189 nodes_that_confirmed_reception.emplace(nodeid);
190 }
191 },
192 [&] { // DidNodeConfirmReception()
193 const NodeId nodeid{ExistentOrNewNodeId()};
194
195 const bool confirmed{pb.DidNodeConfirmReception(nodeid)};
196
197 if (nodes_that_confirmed_reception.contains(nodeid)) {
198 Assert(confirmed);
199 } else {
200 Assert(!confirmed);
201 }
202 },
203 [&] { // HavePendingTransactions()
204 if (std::ranges::any_of(transactions, is_pending)) {
205 Assert(pb.HavePendingTransactions());
206 } else {
207 Assert(!pb.HavePendingTransactions());
208 }
209 },
210 [&] { // GetStale()
211 const auto stale{pb.GetStale()};
212
213 Assert(stale.size() <= transactions.size());
214
215 for (const auto& stale_tx : stale) {
216 const auto it{transactions.find(stale_tx)};
217 Assert(it != transactions.end());
218 Assert(is_pending(*it));
219 }
220 },
221 [&] { // GetBroadcastInfo()
222 const auto all_broadcast_info{pb.GetBroadcastInfo()};
223
224 Assert(all_broadcast_info.size() == transactions.size());
225
226 for (const auto& info : all_broadcast_info) {
227 const auto it{transactions.find(info.tx)};
228 Assert(it != transactions.end());
229 Assert(info.peers.size() == it->second); // exactly the sends we recorded
230 Assert(info.attempts_remaining == max_send_attempts - it->second);
231 }
232 },
233 [&] {
234 clock_ctx.set(ConsumeTime(fdp));
235 });
236 }
237}
if(!SetupNetworking())
#define Assert(val)
Identity function.
Definition: check.h:116
A combination of a network address (CNetAddr) and a (TCP) port.
Definition: netaddress.h:530
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.
LIMITED_WHILE(provider.remaining_bytes(), 10000)
int64_t NodeId
Definition: net.h:105
std::shared_ptr< const CTransaction > CTransactionRef
Definition: transaction.h:403
bool operator()(const CTransactionRef &a, const CTransactionRef &b) const
size_t operator()(const CTransactionRef &tx) const
SeedRandomStateForTest(SeedRand::ZEROS)
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
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
@ ZEROS
Seed with a compile time constant of zeros.
consteval auto _(util::TranslatedLiteral str)
Definition: translation.h:79