private_broadcast_tests.cpp raw
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
5 #include <primitives/transaction.h>
6 #include <private_broadcast.h>
7 #include <test/util/setup_common.h>
8 #include <test/util/time.h>
9 #include <util/time.h>
10
11 #include <algorithm>
12 #include <ostream>
13 #include <boost/test/unit_test.hpp>
14
15 std::ostream& operator<<(std::ostream& os, PrivateBroadcast::AddResult r)
16 {
17 switch (r) {
18 case PrivateBroadcast::AddResult::Added: return os << "Added";
19 case PrivateBroadcast::AddResult::AlreadyPresent: return os << "AlreadyPresent";
20 case PrivateBroadcast::AddResult::QueueFull: return os << "QueueFull";
21 } // no default case, so the compiler can warn about missing cases
22 assert(false);
23 }
24
25 BOOST_FIXTURE_TEST_SUITE(private_broadcast_tests, BasicTestingSetup)
26
27 static CTransactionRef MakeDummyTx(uint32_t id, size_t num_witness)
28 {
29 CMutableTransaction mtx;
30 mtx.vin.resize(1);
31 mtx.vin[0].nSequence = id;
32 if (num_witness > 0) {
33 mtx.vin[0].scriptWitness = CScriptWitness{};
34 mtx.vin[0].scriptWitness.stack.resize(num_witness);
35 }
36 return MakeTransactionRef(mtx);
37 }
38
39 BOOST_AUTO_TEST_CASE(basic)
40 {
41 FakeNodeClock clock{};
42
43 PrivateBroadcast pb;
44 const NodeId recipient1{1};
45 in_addr ipv4Addr;
46 ipv4Addr.s_addr = 0xa0b0c001;
47 const CService addr1{ipv4Addr, 1111};
48
49 // No transactions initially.
50 BOOST_CHECK(!pb.PickTxForSend(/*will_send_to_nodeid=*/recipient1, /*will_send_to_address=*/addr1).has_value());
51 BOOST_CHECK_EQUAL(pb.GetStale().size(), 0);
52 BOOST_CHECK(!pb.HavePendingTransactions());
53 BOOST_CHECK_EQUAL(pb.GetBroadcastInfo().size(), 0);
54
55 // Make a transaction and add it.
56 const auto tx1{MakeDummyTx(/*id=*/1, /*num_witness=*/0)};
57
58 BOOST_CHECK_EQUAL(pb.Add(tx1), PrivateBroadcast::AddResult::Added);
59 BOOST_CHECK_EQUAL(pb.Add(tx1), PrivateBroadcast::AddResult::AlreadyPresent);
60
61 // Make another transaction with same txid, different wtxid and add it.
62 const auto tx2{MakeDummyTx(/*id=*/1, /*num_witness=*/1)};
63 BOOST_REQUIRE(tx1->GetHash() == tx2->GetHash());
64 BOOST_REQUIRE(tx1->GetWitnessHash() != tx2->GetWitnessHash());
65
66 BOOST_CHECK_EQUAL(pb.Add(tx2), PrivateBroadcast::AddResult::Added);
67 const auto find_tx_info{[](auto& infos, const CTransactionRef& tx) -> const PrivateBroadcast::TxBroadcastInfo& {
68 const auto it{std::ranges::find(infos, tx->GetWitnessHash(), [](const auto& info) { return info.tx->GetWitnessHash(); })};
69 BOOST_REQUIRE(it != infos.end());
70 return *it;
71 }};
72 const auto check_peer_counts{[&](size_t tx1_peer_count, size_t tx2_peer_count) {
73 const auto infos{pb.GetBroadcastInfo()};
74 BOOST_CHECK_EQUAL(infos.size(), 2);
75 BOOST_CHECK_EQUAL(find_tx_info(infos, tx1).peers.size(), tx1_peer_count);
76 BOOST_CHECK_EQUAL(find_tx_info(infos, tx2).peers.size(), tx2_peer_count);
77 }};
78
79 check_peer_counts(/*tx1_peer_count=*/0, /*tx2_peer_count=*/0);
80
81 const auto tx_for_recipient1{pb.PickTxForSend(/*will_send_to_nodeid=*/recipient1, /*will_send_to_address=*/addr1).value()};
82 BOOST_CHECK(tx_for_recipient1 == tx1 || tx_for_recipient1 == tx2);
83
84 // A second pick must return the other transaction.
85 const NodeId recipient2{2};
86 const CService addr2{ipv4Addr, 2222};
87 const auto tx_for_recipient2{pb.PickTxForSend(/*will_send_to_nodeid=*/recipient2, /*will_send_to_address=*/addr2).value()};
88 BOOST_CHECK(tx_for_recipient2 == tx1 || tx_for_recipient2 == tx2);
89 BOOST_CHECK_NE(tx_for_recipient1, tx_for_recipient2);
90
91 check_peer_counts(/*tx1_peer_count=*/1, /*tx2_peer_count=*/1);
92
93 const NodeId nonexistent_recipient{0};
94
95 // Confirm transactions <-> recipients mapping is correct.
96 BOOST_CHECK(!pb.GetTxForNode(nonexistent_recipient).has_value());
97 BOOST_CHECK_EQUAL(pb.GetTxForNode(recipient1).value(), tx_for_recipient1);
98 BOOST_CHECK_EQUAL(pb.GetTxForNode(recipient2).value(), tx_for_recipient2);
99
100 // Confirm none of the transactions' reception have been confirmed.
101 BOOST_CHECK(!pb.DidNodeConfirmReception(recipient1));
102 BOOST_CHECK(!pb.DidNodeConfirmReception(recipient2));
103 BOOST_CHECK(!pb.DidNodeConfirmReception(nonexistent_recipient));
104
105 // 1. Freshly added transactions should NOT be stale yet.
106 BOOST_CHECK_EQUAL(pb.GetStale().size(), 0);
107
108 // 2. Fast-forward the mock clock past the INITIAL_STALE_DURATION.
109 clock += PrivateBroadcast::INITIAL_STALE_DURATION + 1min;
110
111 // 3. Now that the initial duration has passed, both unconfirmed transactions should be stale.
112 BOOST_CHECK_EQUAL(pb.GetStale().size(), 2);
113
114 // Confirm reception by recipient1.
115 pb.NodeConfirmedReception(nonexistent_recipient); // Dummy call.
116 pb.NodeConfirmedReception(recipient1);
117
118 BOOST_CHECK(pb.DidNodeConfirmReception(recipient1));
119 BOOST_CHECK(!pb.DidNodeConfirmReception(recipient2));
120
121 const auto infos{pb.GetBroadcastInfo()};
122 BOOST_CHECK_EQUAL(infos.size(), 2);
123 {
124 const auto& peers{find_tx_info(infos, tx_for_recipient1).peers};
125 BOOST_CHECK_EQUAL(peers.size(), 1);
126 BOOST_CHECK_EQUAL(peers[0].address.ToStringAddrPort(), addr1.ToStringAddrPort());
127 BOOST_CHECK(peers[0].received.has_value());
128 }
129 {
130 const auto& peers{find_tx_info(infos, tx_for_recipient2).peers};
131 BOOST_CHECK_EQUAL(peers.size(), 1);
132 BOOST_CHECK_EQUAL(peers[0].address.ToStringAddrPort(), addr2.ToStringAddrPort());
133 BOOST_CHECK(!peers[0].received.has_value());
134 }
135
136 const auto stale_state{pb.GetStale()};
137 BOOST_CHECK_EQUAL(stale_state.size(), 1);
138 BOOST_CHECK_EQUAL(stale_state[0], tx_for_recipient2);
139
140 clock += 10h;
141
142 BOOST_CHECK_EQUAL(pb.GetStale().size(), 2);
143
144 BOOST_CHECK_EQUAL(pb.Remove(tx_for_recipient1).value(), 1);
145 BOOST_CHECK(!pb.Remove(tx_for_recipient1).has_value());
146 BOOST_CHECK_EQUAL(pb.Remove(tx_for_recipient2).value(), 0);
147 BOOST_CHECK(!pb.Remove(tx_for_recipient2).has_value());
148
149 BOOST_CHECK_EQUAL(pb.GetBroadcastInfo().size(), 0);
150 const CService addr_nonexistent{ipv4Addr, 3333};
151 BOOST_CHECK(!pb.PickTxForSend(/*will_send_to_nodeid=*/nonexistent_recipient, /*will_send_to_address=*/addr_nonexistent).has_value());
152 }
153
154 BOOST_AUTO_TEST_CASE(stale_unpicked_tx)
155 {
156 FakeNodeClock clock{};
157
158 PrivateBroadcast pb;
159 const auto tx{MakeDummyTx(/*id=*/42, /*num_witness=*/0)};
160 BOOST_REQUIRE_EQUAL(pb.Add(tx), PrivateBroadcast::AddResult::Added);
161
162 // Unpicked transactions use the longer INITIAL_STALE_DURATION.
163 BOOST_CHECK_EQUAL(pb.GetStale().size(), 0);
164 clock += PrivateBroadcast::INITIAL_STALE_DURATION - 1min;
165 BOOST_CHECK_EQUAL(pb.GetStale().size(), 0);
166 clock += 2min;
167 const auto stale_state{pb.GetStale()};
168 BOOST_REQUIRE_EQUAL(stale_state.size(), 1);
169 BOOST_CHECK_EQUAL(stale_state[0], tx);
170 }
171
172 BOOST_AUTO_TEST_CASE(rejection_at_cap)
173 {
174 PrivateBroadcast pb;
175 constexpr size_t num_cap{PrivateBroadcast::MAX_TRANSACTIONS};
176 constexpr size_t num_over{5};
177
178 // Fill the queue exactly to the cap; every distinct Add() succeeds.
179 std::vector<CTransactionRef> txs;
180 txs.reserve(num_cap);
181 for (size_t i{0}; i < num_cap; ++i) {
182 auto tx{MakeDummyTx(/*id=*/static_cast<uint32_t>(i), /*num_witness=*/0)};
183 BOOST_REQUIRE_EQUAL(pb.Add(tx), PrivateBroadcast::AddResult::Added);
184 txs.push_back(std::move(tx));
185 }
186 BOOST_CHECK_EQUAL(pb.GetBroadcastInfo().size(), num_cap);
187
188 // Further distinct transactions are rejected, and the queue is unchanged.
189 for (size_t i{0}; i < num_over; ++i) {
190 const auto tx{MakeDummyTx(/*id=*/static_cast<uint32_t>(num_cap + i), /*num_witness=*/0)};
191 BOOST_CHECK_EQUAL(pb.Add(tx), PrivateBroadcast::AddResult::QueueFull);
192 }
193 BOOST_CHECK_EQUAL(pb.GetBroadcastInfo().size(), num_cap);
194
195 // Nothing was evicted: all originally-added transactions are still present.
196 const auto infos{pb.GetBroadcastInfo()};
197 std::set<uint256> present_wtxids;
198 for (const auto& info : infos) {
199 present_wtxids.insert(info.tx->GetWitnessHash().ToUint256());
200 }
201 BOOST_CHECK_EQUAL(present_wtxids.size(), infos.size());
202 for (size_t i{0}; i < num_cap; ++i) {
203 BOOST_CHECK_MESSAGE(present_wtxids.contains(txs[i]->GetWitnessHash().ToUint256()),
204 "tx index " << i << " should still be present");
205 }
206
207 // Re-adding an already-present tx is AlreadyPresent even at the cap (not QueueFull).
208 BOOST_CHECK_EQUAL(pb.Add(txs[0]), PrivateBroadcast::AddResult::AlreadyPresent);
209 BOOST_CHECK_EQUAL(pb.GetBroadcastInfo().size(), num_cap);
210
211 // Removing one frees exactly one slot for a new transaction.
212 BOOST_REQUIRE(pb.Remove(txs[0]).has_value());
213 BOOST_CHECK_EQUAL(pb.GetBroadcastInfo().size(), num_cap - 1);
214 const auto fresh{MakeDummyTx(/*id=*/0xffffffff, /*num_witness=*/0)};
215 BOOST_CHECK_EQUAL(pb.Add(fresh), PrivateBroadcast::AddResult::Added);
216 BOOST_CHECK_EQUAL(pb.GetBroadcastInfo().size(), num_cap);
217
218 // A previously-removed tx can be added again as a brand-new entry.
219 BOOST_REQUIRE(pb.Remove(fresh).has_value());
220 BOOST_CHECK_EQUAL(pb.Add(fresh), PrivateBroadcast::AddResult::Added);
221 BOOST_CHECK_EQUAL(pb.GetBroadcastInfo().size(), num_cap);
222 }
223
224 BOOST_AUTO_TEST_SUITE_END()
225