Add a fuzz test that exercises the public methods of the PrivateBroadcast class from src/private_broadcast.h and checks for correctness.
test: add fuzz test for private broadcast #35129
pull vasild wants to merge 1 commits into bitcoin:master from vasild:private_broadcast_fuzz changing 3 files +188 −5-
vasild commented at 12:57 PM on April 21, 2026: contributor
- DrahtBot added the label Tests on Apr 21, 2026
-
DrahtBot commented at 12:57 PM on April 21, 2026: contributor
<!--e57a25ab6845829454e8d69fc972939a-->
The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.
<!--021abf342d371248e50ceaed478a90ca-->
Reviews
See the guideline for information on the review process. A summary of reviews will appear here.
<!--5faf32d7da4f0f540f40219e4f7537a3-->
-
vasild commented at 1:02 PM on April 21, 2026: contributor
The test was originally provided by @kevkevinpal in #29415 (comment)
There is a complementary fuzz test in #35090 which exercises the peerman / connman call paths while this one here calls directly the
PrivateBroadcastmethods. -
in src/test/fuzz/private_broadcast.cpp:24 in d31c86f255
19 | + 20 | +FUZZ_TARGET(private_broadcast) 21 | +{ 22 | + SeedRandomStateForTest(SeedRand::ZEROS); 23 | + FuzzedDataProvider fdp(buffer.data(), buffer.size()); 24 | + SetMockTime(ConsumeTime(fdp));
maflcko commented at 1:17 PM on April 21, 2026:Please don't use the legacy
SetMockTime, which modifies the global and leaks the value between test cases. Probably fine here, but for consistency, I'd avoid it and useNodeClockContext.Also, it seems odd to hold the value constant, this will limit the possible coverage?
vasild commented at 8:58 AM on April 22, 2026:It seems unnecessary to mock the time, removed
SetMockTime()altogether.
maflcko commented at 11:41 AM on April 22, 2026:It seems unnecessary to mock the time, removed
SetMockTime()altogether.Huh? Running the fuzz target immediately prints:
The current fuzz target accessed system time. This is acceptable, but requires the fuzz target to use a NodeClockContext, SteadyClockContext or call SetMockTime() at the beginning of processing the fuzz input. Without setting mock time, time-dependent behavior can lead to non-reproducible bugs or inefficient fuzzing.Also, the private broadcast module certainly uses time, so I don't understand why it would be unnecessary.
brunoerg commented at 1:07 PM on April 22, 2026:For reference,
NodeConfirmedReception,GetStaleandPickTxForSendcallNodeClock::now().
vasild commented at 2:00 PM on April 22, 2026:Alright, 3rd attempt: added
NodeClockContext clock_ctx{ConsumeTime(fdp)};and then on every iteration change it withclock_ctx.set(ConsumeTime(fdp));. That would allow the time to go backwards, but should be ok to test as it might happen in the real world due to clock adjustment. Shouldn't cause misbehavior from thePrivateBroadcastclass.in src/test/fuzz/private_broadcast.cpp:46 in d31c86f255
41 | + return next_nodeid++; 42 | + } 43 | + return fdp.ConsumeIntegralInRange<NodeId>(0, next_nodeid - 1); 44 | + }; 45 | + 46 | + while (fdp.remaining_bytes() > 0) {
maflcko commented at 1:19 PM on April 21, 2026:Please use the
LIMITED_WHILEpattern withConsumeBool(). Otherwise, if something is added later, it will be silently dead code. Of course, it is fine right now, but just taking the risk in the future seems not worth it.
vasild commented at 9:04 AM on April 22, 2026:Changed to
LIMITED_WHILE(fdp.ConsumeBool(), 10000).I am just curious why that is preferred over
LIMITED_WHILE(fdp.remaining_bytes() > 0, 10000). To me the logic should be - as long as there is data, keep going. That is better described by the latter. TheConsumeBool()variant seems to create extra unnecessary work for the fuzzer to figure out thattrues should be planted in specific places in the data to avoid premature exit.DrahtBot added the label CI failed on Apr 21, 2026DrahtBot commented at 1:45 PM on April 21, 2026: contributor<!--85328a0da195eb286784d51f73fa0af9-->
🚧 At least one of the CI tasks failed. <sub>Task
tidy: https://github.com/bitcoin/bitcoin/actions/runs/24723618894/job/72318638206</sub> <sub>LLM reason (✨ experimental): CI failed because clang-tidy reported a warnings-as-errorsperformance-unnecessary-copy-initializationintest/fuzz/private_broadcast.cpp(copy-constructingtxfrom a const reference).</sub><details><summary>Hints</summary>
Try to run the tests locally, according to the documentation. However, a CI failure may still happen due to a number of reasons, for example:
Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.
A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.
An intermittent issue.
Leave a comment here, if you need help tracking down a confusing failure.
</details>
fanquake added the label Private Broadcast on Apr 21, 2026in src/test/fuzz/private_broadcast.cpp:40 in d31c86f255
35 | + std::unordered_set<NodeId> nodes_that_confirmed_reception; 36 | + 37 | + NodeId next_nodeid{0}; // Generate unique node ids. 38 | + 39 | + const auto ExistentOrNewNodeId = [&next_nodeid, &fdp](){ 40 | + if (fdp.ConsumeBool() || next_nodeid == 0) {
andrewtoth commented at 6:40 PM on April 21, 2026:No need to consume input if we are already going in this path.
if (next_nodeid == 0 || fdp.ConsumeBool()) {
vasild commented at 9:04 AM on April 22, 2026:Done.
in src/test/fuzz/private_broadcast.cpp:51 in d31c86f255
46 | + while (fdp.remaining_bytes() > 0) { 47 | + CallOneOf( 48 | + fdp, 49 | + [&] { // Add() 50 | + CTransactionRef tx; 51 | + if (fdp.ConsumeIntegralInRange<uint8_t>(0, 10) != 0 || transactions.empty()) {
andrewtoth commented at 6:48 PM on April 21, 2026:No need to consume input if we are already going in this path. Using
ConsumeIntegralInRange<uint8_t>(0, 10) != 0is an anti-pattern in fuzz targets. The fuzzer will explore all paths regardless. It doesn't respect the hand-crafted probability distribution, which just wastes time trying to find inputs that reach both sides.ConsumeBool()gives the fuzzer a clean branch to explore both. The comments// New transaction most of the time.and// A duplicate occasionally.should also be removed.if (transactions.empty() || fdp.ConsumeBool()) {
vasild commented at 9:04 AM on April 22, 2026:Done.
vasild force-pushed on Apr 22, 2026vasild commented at 8:57 AM on April 22, 2026: contributord31c86f255e12c94402c21b7e98f64eb6de93365...0f07f37c0e11b02f4ca2bfb421c95ce2ba922781: address suggestions and pet tidyin src/test/fuzz/private_broadcast.cpp:55 in 0f07f37c0e outdated
50 | + if (transactions.empty() || fdp.ConsumeBool()) { 51 | + tx = MakeTransactionRef(ConsumeTransaction(fdp, std::nullopt)); 52 | + } else { 53 | + tx = *PickIterator(fdp, transactions); 54 | + } 55 | + if (pb.Add(tx)) {
brunoerg commented at 1:19 PM on April 22, 2026:feel free to ignore but you might assert that a tx from transactions is not added, assuming you're using
PickIteratorhere to exercise adding a tx that has been already added, e.g.:@@ -47,12 +47,15 @@ FUZZ_TARGET(private_broadcast) fdp, [&] { // Add() CTransactionRef tx; + bool from_transactions{false}; if (transactions.empty() || fdp.ConsumeBool()) { tx = MakeTransactionRef(ConsumeTransaction(fdp, std::nullopt)); } else { tx = *PickIterator(fdp, transactions); + from_transactions = true; } if (pb.Add(tx)) { + Assert(!from_transactions); transactions.emplace(tx); } },
vasild commented at 2:27 PM on April 22, 2026:Done.
in src/test/fuzz/private_broadcast.cpp:146 in 0f07f37c0e outdated
141 | + Assert(!transactions.empty()); 142 | + } else { 143 | + Assert(transactions.empty()); 144 | + } 145 | + }, 146 | + [&] { // GetStale()
brunoerg commented at 1:22 PM on April 22, 2026:I think
GetStale()testing will be more effective if you put one more lambda that would advance mock time (assuming you must mock time in this harness btw). Otherwise, it looksAssert(stale.size() <= transactions.size())is trivially true.
vasild commented at 2:32 PM on April 22, 2026:Now the test changes the mock time. Not in one lambda but on every iteration, I guess it is fine either way. It could also make it go back, in addition to advancing it forward.
in src/test/fuzz/private_broadcast.cpp:78 in 0f07f37c0e
73 | + } else { 74 | + ++it; 75 | + } 76 | + } 77 | + 78 | + Assert(pb.Remove(tx).has_value());
brunoerg commented at 1:25 PM on April 22, 2026:Instead of just checking whether
Removehas or not value, you could for each tx, count how many of its sent-to nodes are innodes_that_confirmed_reception, and assert*pb.Remove(tx) == expected_confirmed.
vasild commented at 2:33 PM on April 22, 2026:Done, thanks!
6cca706a15test: add fuzz test for private broadcast
Co-authored-by: Vasil Dimov <vd@FreeBSD.org>
vasild force-pushed on Apr 22, 2026vasild commented at 2:26 PM on April 22, 2026: contributor0f07f37c0e11b02f4ca2bfb421c95ce2ba922781...6cca706a155246cf6ed6c3b166416b2f43cd0415: address suggestionsDrahtBot removed the label CI failed on Apr 23, 2026ContributorsLabels
github-metadata-mirror
This is a metadata mirror of the GitHub repository bitcoin/bitcoin. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2026-05-16 00:12 UTC
This site is hosted by @0xB10C
More mirrored repositories can be found on mirror.b10c.me