Skip to content

Commit 699643f

Browse files
committed
[unit test] MempoolRejectedTx
1 parent fa584cb commit 699643f

File tree

2 files changed

+338
-0
lines changed

2 files changed

+338
-0
lines changed

src/test/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ add_executable(test_bitcoin
125125
torcontrol_tests.cpp
126126
transaction_tests.cpp
127127
translation_tests.cpp
128+
txdownload_tests.cpp
128129
txindex_tests.cpp
129130
txpackage_tests.cpp
130131
txreconciliation_tests.cpp

src/test/txdownload_tests.cpp

Lines changed: 337 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,337 @@
1+
// Copyright (c) 2011-2022 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 <addresstype.h>
6+
#include <consensus/validation.h>
7+
#include <net_processing.h>
8+
#include <node/txdownloadman_impl.h>
9+
#include <primitives/transaction.h>
10+
#include <script/script.h>
11+
#include <test/util/random.h>
12+
#include <test/util/setup_common.h>
13+
#include <validation.h>
14+
15+
#include <array>
16+
17+
#include <boost/test/unit_test.hpp>
18+
19+
BOOST_FIXTURE_TEST_SUITE(txdownload_tests, TestingSetup)
20+
21+
struct Behaviors {
22+
bool m_txid_in_rejects;
23+
bool m_wtxid_in_rejects;
24+
bool m_txid_in_rejects_recon;
25+
bool m_wtxid_in_rejects_recon;
26+
bool m_keep_for_compact;
27+
bool m_ignore_inv_txid;
28+
bool m_ignore_inv_wtxid;
29+
30+
// Constructor. We are passing and casting ints because they are more readable in a table (see all_expected_results).
31+
Behaviors(bool txid_rejects, bool wtxid_rejects, bool txid_recon, bool wtxid_recon, bool keep, bool txid_inv, bool wtxid_inv) :
32+
m_txid_in_rejects(txid_rejects),
33+
m_wtxid_in_rejects(wtxid_rejects),
34+
m_txid_in_rejects_recon(txid_recon),
35+
m_wtxid_in_rejects_recon(wtxid_recon),
36+
m_keep_for_compact(keep),
37+
m_ignore_inv_txid(txid_inv),
38+
m_ignore_inv_wtxid(wtxid_inv)
39+
{}
40+
41+
void CheckEqual(const Behaviors& other, bool segwit)
42+
{
43+
BOOST_CHECK_EQUAL(other.m_wtxid_in_rejects, m_wtxid_in_rejects);
44+
BOOST_CHECK_EQUAL(other.m_wtxid_in_rejects_recon, m_wtxid_in_rejects_recon);
45+
BOOST_CHECK_EQUAL(other.m_keep_for_compact, m_keep_for_compact);
46+
BOOST_CHECK_EQUAL(other.m_ignore_inv_wtxid, m_ignore_inv_wtxid);
47+
48+
// false negatives for nonsegwit transactions, since txid == wtxid.
49+
if (segwit) {
50+
BOOST_CHECK_EQUAL(other.m_txid_in_rejects, m_txid_in_rejects);
51+
BOOST_CHECK_EQUAL(other.m_txid_in_rejects_recon, m_txid_in_rejects_recon);
52+
BOOST_CHECK_EQUAL(other.m_ignore_inv_txid, m_ignore_inv_txid);
53+
}
54+
}
55+
};
56+
57+
// Map from failure reason to expected behavior for a segwit tx that fails
58+
// Txid and Wtxid are assumed to be different here. For a nonsegwit transaction, use the wtxid results.
59+
static std::map<TxValidationResult, Behaviors> expected_behaviors{
60+
{TxValidationResult::TX_CONSENSUS, {/*txid_rejects*/0,/*wtxid_rejects*/1,/*txid_recon*/0,/*wtxid_recon*/0,/*keep*/1,/*txid_inv*/0,/*wtxid_inv*/1}},
61+
{TxValidationResult::TX_RECENT_CONSENSUS_CHANGE, { 0, 1, 0, 0, 1, 0, 1}},
62+
{TxValidationResult::TX_INPUTS_NOT_STANDARD, { 1, 1, 0, 0, 1, 1, 1}},
63+
{TxValidationResult::TX_NOT_STANDARD, { 0, 1, 0, 0, 1, 0, 1}},
64+
{TxValidationResult::TX_MISSING_INPUTS, { 0, 0, 0, 0, 1, 0, 1}},
65+
{TxValidationResult::TX_PREMATURE_SPEND, { 0, 1, 0, 0, 1, 0, 1}},
66+
{TxValidationResult::TX_WITNESS_MUTATED, { 0, 1, 0, 0, 1, 0, 1}},
67+
{TxValidationResult::TX_WITNESS_STRIPPED, { 0, 0, 0, 0, 0, 0, 0}},
68+
{TxValidationResult::TX_CONFLICT, { 0, 1, 0, 0, 1, 0, 1}},
69+
{TxValidationResult::TX_MEMPOOL_POLICY, { 0, 1, 0, 0, 1, 0, 1}},
70+
{TxValidationResult::TX_NO_MEMPOOL, { 0, 1, 0, 0, 1, 0, 1}},
71+
{TxValidationResult::TX_RECONSIDERABLE, { 0, 0, 0, 1, 1, 0, 1}},
72+
{TxValidationResult::TX_UNKNOWN, { 0, 1, 0, 0, 1, 0, 1}},
73+
};
74+
75+
static bool CheckOrphanBehavior(node::TxDownloadManagerImpl& txdownload_impl, const CTransactionRef& tx, const node::RejectedTxTodo& ret, std::string& err_msg,
76+
bool expect_orphan, bool expect_keep, unsigned int expected_parents)
77+
{
78+
// Missing inputs can never result in a PackageToValidate.
79+
if (ret.m_package_to_validate.has_value()) {
80+
err_msg = strprintf("returned a PackageToValidate on missing inputs");
81+
return false;
82+
}
83+
84+
if (expect_orphan != txdownload_impl.m_orphanage.HaveTx(tx->GetWitnessHash())) {
85+
err_msg = strprintf("unexpectedly %s tx in orpanage", expect_orphan ? "did not find" : "found");
86+
return false;
87+
}
88+
if (expect_keep != ret.m_should_add_extra_compact_tx) {
89+
err_msg = strprintf("unexpectedly returned %s add to vExtraTxnForCompact", expect_keep ? "should not" : "should");
90+
return false;
91+
}
92+
if (expected_parents != ret.m_unique_parents.size()) {
93+
err_msg = strprintf("expected %u unique_parents, got %u", expected_parents, ret.m_unique_parents.size());
94+
return false;
95+
}
96+
return true;
97+
}
98+
99+
static CTransactionRef CreatePlaceholderTx(bool segwit)
100+
{
101+
// Each tx returned from here spends the previous one.
102+
static Txid prevout_hash{};
103+
104+
CMutableTransaction mtx;
105+
mtx.vin.emplace_back(prevout_hash, 0);
106+
// This makes txid != wtxid
107+
if (segwit) mtx.vin[0].scriptWitness.stack.push_back({1});
108+
mtx.vout.emplace_back(CENT, CScript());
109+
auto ptx = MakeTransactionRef(mtx);
110+
prevout_hash = ptx->GetHash();
111+
return ptx;
112+
}
113+
114+
BOOST_FIXTURE_TEST_CASE(tx_rejection_types, TestChain100Setup)
115+
{
116+
CTxMemPool& pool = *Assert(m_node.mempool);
117+
FastRandomContext det_rand{true};
118+
node::TxDownloadOptions DEFAULT_OPTS{pool, det_rand, DEFAULT_MAX_ORPHAN_TRANSACTIONS, true};
119+
120+
// A new TxDownloadManagerImpl is created for each tx so we can just reuse the same one.
121+
TxValidationState state;
122+
NodeId nodeid{0};
123+
std::chrono::microseconds now{GetTime()};
124+
node::TxDownloadConnectionInfo connection_info{/*m_preferred=*/false, /*m_relay_permissions=*/false, /*m_wtxid_relay=*/true};
125+
126+
for (const auto segwit_parent : {true, false}) {
127+
for (const auto segwit_child : {true, false}) {
128+
const auto ptx_parent = CreatePlaceholderTx(segwit_parent);
129+
const auto ptx_child = CreatePlaceholderTx(segwit_child);
130+
const auto& parent_txid = ptx_parent->GetHash().ToUint256();
131+
const auto& parent_wtxid = ptx_parent->GetWitnessHash().ToUint256();
132+
const auto& child_txid = ptx_child->GetHash().ToUint256();
133+
const auto& child_wtxid = ptx_child->GetWitnessHash().ToUint256();
134+
135+
for (const auto& [result, expected_behavior] : expected_behaviors) {
136+
node::TxDownloadManagerImpl txdownload_impl{DEFAULT_OPTS};
137+
txdownload_impl.ConnectedPeer(nodeid, connection_info);
138+
// Parent failure
139+
state.Invalid(result, "");
140+
const auto& [keep, unique_txids, package_to_validate] = txdownload_impl.MempoolRejectedTx(ptx_parent, state, nodeid, /*first_time_failure=*/true);
141+
142+
// No distinction between txid and wtxid caching for nonsegwit transactions, so only test these specific
143+
// behaviors for segwit transactions.
144+
Behaviors actual_behavior{
145+
/*txid_rejects=*/txdownload_impl.RecentRejectsFilter().contains(parent_txid),
146+
/*wtxid_rejects=*/txdownload_impl.RecentRejectsFilter().contains(parent_wtxid),
147+
/*txid_recon=*/txdownload_impl.RecentRejectsReconsiderableFilter().contains(parent_txid),
148+
/*wtxid_recon=*/txdownload_impl.RecentRejectsReconsiderableFilter().contains(parent_wtxid),
149+
/*keep=*/keep,
150+
/*txid_inv=*/txdownload_impl.AddTxAnnouncement(nodeid, GenTxid::Txid(parent_txid), now, /*p2p_inv=*/true),
151+
/*wtxid_inv=*/txdownload_impl.AddTxAnnouncement(nodeid, GenTxid::Wtxid(parent_wtxid), now, /*p2p_inv=*/true),
152+
};
153+
BOOST_TEST_MESSAGE("Testing behavior for " << result << (segwit_parent ? " segwit " : " nonsegwit"));
154+
actual_behavior.CheckEqual(expected_behavior, /*segwit=*/segwit_parent);
155+
156+
// Later, a child of this transaction fails for missing inputs
157+
state.Invalid(TxValidationResult::TX_MISSING_INPUTS, "");
158+
txdownload_impl.MempoolRejectedTx(ptx_child, state, nodeid, /*first_time_failure=*/true);
159+
160+
// If parent (by txid) was rejected, child is too.
161+
const bool parent_txid_rejected{segwit_parent ? expected_behavior.m_txid_in_rejects : expected_behavior.m_wtxid_in_rejects};
162+
BOOST_CHECK_EQUAL(parent_txid_rejected, txdownload_impl.RecentRejectsFilter().contains(child_txid));
163+
BOOST_CHECK_EQUAL(parent_txid_rejected, txdownload_impl.RecentRejectsFilter().contains(child_wtxid));
164+
165+
// Unless rejected, the child should be in orphanage.
166+
BOOST_CHECK_EQUAL(!parent_txid_rejected, txdownload_impl.m_orphanage.HaveTx(ptx_child->GetWitnessHash()));
167+
}
168+
}
169+
}
170+
}
171+
172+
BOOST_FIXTURE_TEST_CASE(handle_missing_inputs, TestChain100Setup)
173+
{
174+
CTxMemPool& pool = *Assert(m_node.mempool);
175+
FastRandomContext det_rand{true};
176+
node::TxDownloadOptions DEFAULT_OPTS{pool, det_rand, DEFAULT_MAX_ORPHAN_TRANSACTIONS, true};
177+
NodeId nodeid{1};
178+
node::TxDownloadConnectionInfo DEFAULT_CONN{/*m_preferred=*/false, /*m_relay_permissions=*/false, /*m_wtxid_relay=*/true};
179+
180+
// We need mature coinbases
181+
mineBlocks(20);
182+
183+
// Transactions with missing inputs are treated differently depending on how much we know about
184+
// their parents.
185+
CKey wallet_key = GenerateRandomKey();
186+
CScript destination = GetScriptForDestination(PKHash(wallet_key.GetPubKey()));
187+
// Amount for spending coinbase in a 1-in-1-out tx, at depth n, each time deducting 1000 from the amount as fees.
188+
CAmount amount_depth_1{50 * COIN - 1000};
189+
CAmount amount_depth_2{amount_depth_1 - 1000};
190+
// Amount for spending coinbase in a 1-in-2-out tx, deducting 1000 in fees
191+
CAmount amount_split_half{25 * COIN - 500};
192+
int test_chain_height{100};
193+
194+
TxValidationState state_orphan;
195+
state_orphan.Invalid(TxValidationResult::TX_MISSING_INPUTS, "");
196+
197+
// Transactions are not all submitted to mempool. Conserve the number of m_coinbase_txns we
198+
// consume, and only increment this index number when we would conflict with an existing
199+
// mempool transaction.
200+
size_t coinbase_idx{0};
201+
202+
for (int decisions = 0; decisions < (1 << 4); ++decisions) {
203+
auto mtx_single_parent = CreateValidMempoolTransaction(m_coinbase_txns[coinbase_idx], /*input_vout=*/0, test_chain_height, coinbaseKey, destination, amount_depth_1, /*submit=*/false);
204+
auto single_parent = MakeTransactionRef(mtx_single_parent);
205+
206+
auto mtx_orphan = CreateValidMempoolTransaction(single_parent, /*input_vout=*/0, test_chain_height, wallet_key, destination, amount_depth_2, /*submit=*/false);
207+
auto orphan = MakeTransactionRef(mtx_orphan);
208+
209+
node::TxDownloadManagerImpl txdownload_impl{DEFAULT_OPTS};
210+
txdownload_impl.ConnectedPeer(nodeid, DEFAULT_CONN);
211+
212+
// Each bit of decisions tells us whether the parent is in a particular cache.
213+
// It is definitely possible for a transaction to be in multiple caches. For example, it
214+
// may have both a low feerate and found to violate some mempool policy when validated
215+
// in a 1p1c.
216+
const bool parent_recent_rej(decisions & 1);
217+
const bool parent_recent_rej_recon((decisions >> 1) & 1);
218+
const bool parent_recent_conf((decisions >> 2) & 1);
219+
const bool parent_in_mempool((decisions >> 3) & 1);
220+
221+
if (parent_recent_rej) txdownload_impl.RecentRejectsFilter().insert(single_parent->GetHash().ToUint256());
222+
if (parent_recent_rej_recon) txdownload_impl.RecentRejectsReconsiderableFilter().insert(single_parent->GetHash().ToUint256());
223+
if (parent_recent_conf) txdownload_impl.RecentConfirmedTransactionsFilter().insert(single_parent->GetHash().ToUint256());
224+
if (parent_in_mempool) {
225+
const auto mempool_result = WITH_LOCK(::cs_main, return m_node.chainman->ProcessTransaction(single_parent));
226+
BOOST_CHECK(mempool_result.m_result_type == MempoolAcceptResult::ResultType::VALID);
227+
coinbase_idx += 1;
228+
assert(coinbase_idx < m_coinbase_txns.size());
229+
}
230+
231+
// Whether or not the transaction is added as an orphan depends solely on whether or not
232+
// it's in RecentRejectsFilter. Specifically, the parent is allowed to be in
233+
// RecentRejectsReconsiderableFilter, but it cannot be in RecentRejectsFilter.
234+
const bool expect_keep_orphan = !parent_recent_rej;
235+
const auto ret_1p1c = txdownload_impl.MempoolRejectedTx(orphan, state_orphan, nodeid, /*first_time_failure=*/true);
236+
std::string err_msg;
237+
const bool ok = CheckOrphanBehavior(txdownload_impl, orphan, ret_1p1c, err_msg,
238+
/*expect_orphan=*/expect_keep_orphan, /*expect_keep=*/true, /*expected_parents=*/expect_keep_orphan ? 1 : 0);
239+
BOOST_CHECK_MESSAGE(ok, err_msg);
240+
}
241+
242+
// Orphan with multiple parents
243+
{
244+
std::vector<CTransactionRef> parents;
245+
std::vector<COutPoint> outpoints;
246+
int32_t num_parents{24};
247+
for (int32_t i = 0; i < num_parents; ++i) {
248+
assert(coinbase_idx < m_coinbase_txns.size());
249+
auto mtx_parent = CreateValidMempoolTransaction(m_coinbase_txns[coinbase_idx++], /*input_vout=*/0, test_chain_height,
250+
coinbaseKey, destination, amount_depth_1 + i, /*submit=*/false);
251+
auto ptx_parent = MakeTransactionRef(mtx_parent);
252+
parents.emplace_back(ptx_parent);
253+
outpoints.emplace_back(ptx_parent->GetHash(), 0);
254+
}
255+
256+
// Send all coins to 1 output.
257+
auto mtx_orphan = CreateValidMempoolTransaction(parents, outpoints, test_chain_height, {wallet_key}, {{amount_depth_2 * num_parents, destination}}, /*submit=*/false);
258+
auto orphan = MakeTransactionRef(mtx_orphan);
259+
260+
// 1 parent in RecentRejectsReconsiderableFilter, the rest are unknown
261+
{
262+
node::TxDownloadManagerImpl txdownload_impl{DEFAULT_OPTS};
263+
txdownload_impl.ConnectedPeer(nodeid, DEFAULT_CONN);
264+
265+
txdownload_impl.RecentRejectsReconsiderableFilter().insert(parents[0]->GetHash().ToUint256());
266+
const auto ret_1p1c_parent_reconsiderable = txdownload_impl.MempoolRejectedTx(orphan, state_orphan, nodeid, /*first_time_failure=*/true);
267+
std::string err_msg;
268+
const bool ok = CheckOrphanBehavior(txdownload_impl, orphan, ret_1p1c_parent_reconsiderable, err_msg,
269+
/*expect_orphan=*/true, /*expect_keep=*/true, /*expected_parents=*/num_parents);
270+
BOOST_CHECK_MESSAGE(ok, err_msg);
271+
}
272+
273+
// 1 parent in RecentRejectsReconsiderableFilter, the rest are confirmed
274+
{
275+
node::TxDownloadManagerImpl txdownload_impl{DEFAULT_OPTS};
276+
txdownload_impl.ConnectedPeer(nodeid, DEFAULT_CONN);
277+
278+
txdownload_impl.RecentRejectsReconsiderableFilter().insert(parents[0]->GetHash().ToUint256());
279+
for (int32_t i = 1; i < num_parents; ++i) {
280+
txdownload_impl.RecentConfirmedTransactionsFilter().insert(parents[i]->GetHash().ToUint256());
281+
}
282+
283+
const auto ret_1recon_conf = txdownload_impl.MempoolRejectedTx(orphan, state_orphan, nodeid, /*first_time_failure=*/true);
284+
std::string err_msg;
285+
const bool ok = CheckOrphanBehavior(txdownload_impl, orphan, ret_1recon_conf, err_msg,
286+
/*expect_orphan=*/true, /*expect_keep=*/true, /*expected_parents=*/num_parents);
287+
BOOST_CHECK_MESSAGE(ok, err_msg);
288+
}
289+
290+
// 1 parent in RecentRejectsReconsiderableFilter, 1 other in {RecentRejectsReconsiderableFilter, RecentRejectsFilter}
291+
for (int i = 0; i < 2; ++i) {
292+
node::TxDownloadManagerImpl txdownload_impl{DEFAULT_OPTS};
293+
txdownload_impl.ConnectedPeer(nodeid, DEFAULT_CONN);
294+
295+
txdownload_impl.RecentRejectsReconsiderableFilter().insert(parents[1]->GetHash().ToUint256());
296+
297+
// Doesn't really matter which parent
298+
auto& alreadyhave_parent = parents[0];
299+
if (i == 0) {
300+
txdownload_impl.RecentRejectsReconsiderableFilter().insert(alreadyhave_parent->GetHash().ToUint256());
301+
} else if (i == 1) {
302+
txdownload_impl.RecentRejectsFilter().insert(alreadyhave_parent->GetHash().ToUint256());
303+
}
304+
305+
const auto ret_2_problems = txdownload_impl.MempoolRejectedTx(orphan, state_orphan, nodeid, /*first_time_failure=*/true);
306+
std::string err_msg;
307+
const bool ok = CheckOrphanBehavior(txdownload_impl, orphan, ret_2_problems, err_msg,
308+
/*expect_orphan=*/false, /*expect_keep=*/true, /*expected_parents=*/0);
309+
BOOST_CHECK_MESSAGE(ok, err_msg);
310+
}
311+
}
312+
313+
// Orphan with multiple inputs spending from a single parent
314+
{
315+
assert(coinbase_idx < m_coinbase_txns.size());
316+
auto parent_2outputs = MakeTransactionRef(CreateValidMempoolTransaction({m_coinbase_txns[coinbase_idx]}, {{m_coinbase_txns[coinbase_idx]->GetHash(), 0}}, test_chain_height, {coinbaseKey},
317+
{{amount_split_half, destination}, {amount_split_half, destination}}, /*submit=*/false));
318+
319+
auto orphan = MakeTransactionRef(CreateValidMempoolTransaction({parent_2outputs}, {{parent_2outputs->GetHash(), 0}, {parent_2outputs->GetHash(), 1}},
320+
test_chain_height, {wallet_key}, {{amount_depth_2, destination}}, /*submit=*/false));
321+
// Parent is in RecentRejectsReconsiderableFilter. Inputs will find it twice, but this
322+
// should only counts as 1 parent in the filter.
323+
{
324+
node::TxDownloadManagerImpl txdownload_impl{DEFAULT_OPTS};
325+
txdownload_impl.ConnectedPeer(nodeid, DEFAULT_CONN);
326+
327+
txdownload_impl.RecentRejectsReconsiderableFilter().insert(parent_2outputs->GetHash().ToUint256());
328+
const auto ret_1p1c_2reconsiderable = txdownload_impl.MempoolRejectedTx(orphan, state_orphan, nodeid, /*first_time_failure=*/true);
329+
std::string err_msg;
330+
const bool ok = CheckOrphanBehavior(txdownload_impl, orphan, ret_1p1c_2reconsiderable, err_msg,
331+
/*expect_orphan=*/true, /*expect_keep=*/true, /*expected_parents=*/1);
332+
BOOST_CHECK_MESSAGE(ok, err_msg);
333+
}
334+
}
335+
}
336+
337+
BOOST_AUTO_TEST_SUITE_END()

0 commit comments

Comments
 (0)