package_eval.cpp raw
1 // Copyright (c) 2023-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 <chain.h>
6 #include <consensus/amount.h>
7 #include <consensus/consensus.h>
8 #include <consensus/validation.h>
9 #include <node/mining_types.h>
10 #include <policy/feerate.h>
11 #include <policy/packages.h>
12 #include <policy/policy.h>
13 #include <policy/settings.h>
14 #include <policy/truc_policy.h>
15 #include <primitives/transaction.h>
16 #include <script/script.h>
17 #include <sync.h>
18 #include <test/fuzz/FuzzedDataProvider.h>
19 #include <test/fuzz/fuzz.h>
20 #include <test/fuzz/util.h>
21 #include <test/fuzz/util/mempool.h>
22 #include <test/util/mining.h>
23 #include <test/util/random.h>
24 #include <test/util/script.h>
25 #include <test/util/setup_common.h>
26 #include <test/util/txmempool.h>
27 #include <txmempool.h>
28 #include <util/check.h>
29 #include <util/hasher.h>
30 #include <util/time.h>
31 #include <util/translation.h>
32 #include <validation.h>
33 #include <validationinterface.h>
34
35 #include <cstddef>
36 #include <cstdint>
37 #include <functional>
38 #include <iterator>
39 #include <limits>
40 #include <map>
41 #include <memory>
42 #include <optional>
43 #include <set>
44 #include <span>
45 #include <string>
46 #include <unordered_map>
47 #include <utility>
48 #include <vector>
49 using node::NodeContext;
50
51 namespace {
52
53 const TestingSetup* g_setup;
54 std::vector<COutPoint> g_outpoints_coinbase_init_mature;
55
56 struct MockedTxPool : public CTxMemPool {
57 void RollingFeeUpdate() EXCLUSIVE_LOCKS_REQUIRED(!cs)
58 {
59 LOCK(cs);
60 lastRollingFeeUpdate = GetTime();
61 blockSinceLastRollingFeeBump = true;
62 }
63 };
64
65 void initialize_tx_pool()
66 {
67 static const auto testing_setup = MakeNoLogFileContext<const TestingSetup>();
68 g_setup = testing_setup.get();
69 SetMockTime(WITH_LOCK(g_setup->m_node.chainman->GetMutex(), return g_setup->m_node.chainman->ActiveTip()->Time()));
70
71 for (int i = 0; i < 2 * COINBASE_MATURITY; ++i) {
72 COutPoint prevout{MineBlock(g_setup->m_node, {
73 .coinbase_output_script = P2WSH_EMPTY,
74 })};
75 if (i < COINBASE_MATURITY) {
76 // Remember the txids to avoid expensive disk access later on
77 g_outpoints_coinbase_init_mature.push_back(prevout);
78 }
79 }
80 g_setup->m_node.validation_signals->SyncWithValidationInterfaceQueue();
81 }
82
83 struct OutpointsUpdater final : public CValidationInterface {
84 std::set<COutPoint>& m_mempool_outpoints;
85
86 explicit OutpointsUpdater(std::set<COutPoint>& r)
87 : m_mempool_outpoints{r} {}
88
89 void TransactionAddedToMempool(const NewMempoolTransactionInfo& tx, uint64_t /* mempool_sequence */) override
90 {
91 // for coins spent we always want to be able to rbf so they're not removed
92
93 // outputs from this tx can now be spent
94 for (uint32_t index{0}; index < tx.info.m_tx->vout.size(); ++index) {
95 m_mempool_outpoints.insert(COutPoint{tx.info.m_tx->GetHash(), index});
96 }
97 }
98
99 void TransactionRemovedFromMempool(const CTransactionRef& tx, MemPoolRemovalReason reason, uint64_t /* mempool_sequence */) override
100 {
101 // outpoints spent by this tx are now available
102 for (const auto& input : tx->vin) {
103 // Could already exist if this was a replacement
104 m_mempool_outpoints.insert(input.prevout);
105 }
106 // outpoints created by this tx no longer exist
107 for (uint32_t index{0}; index < tx->vout.size(); ++index) {
108 m_mempool_outpoints.erase(COutPoint{tx->GetHash(), index});
109 }
110 }
111 };
112
113 struct TransactionsDelta final : public CValidationInterface {
114 std::set<CTransactionRef>& m_added;
115
116 explicit TransactionsDelta(std::set<CTransactionRef>& a)
117 : m_added{a} {}
118
119 void TransactionAddedToMempool(const NewMempoolTransactionInfo& tx, uint64_t /* mempool_sequence */) override
120 {
121 // Transactions may be entered and booted any number of times
122 m_added.insert(tx.info.m_tx);
123 }
124
125 void TransactionRemovedFromMempool(const CTransactionRef& tx, MemPoolRemovalReason reason, uint64_t /* mempool_sequence */) override
126 {
127 // Transactions may be entered and booted any number of times
128 m_added.erase(tx);
129 }
130 };
131
132 void MockTime(FuzzedDataProvider& fuzzed_data_provider, const Chainstate& chainstate)
133 {
134 const auto time = ConsumeTime(fuzzed_data_provider,
135 chainstate.m_chain.Tip()->GetMedianTimePast() + 1,
136 std::numeric_limits<decltype(chainstate.m_chain.Tip()->nTime)>::max());
137 SetMockTime(time);
138 }
139
140 std::unique_ptr<CTxMemPool> MakeMempool(FuzzedDataProvider& fuzzed_data_provider, const NodeContext& node)
141 {
142 // Take the default options for tests...
143 CTxMemPool::Options mempool_opts{MemPoolOptionsForTest(node)};
144
145
146 // ...override specific options for this specific fuzz suite
147 mempool_opts.limits.ancestor_count = fuzzed_data_provider.ConsumeIntegralInRange<unsigned>(0, 50);
148 mempool_opts.limits.descendant_count = fuzzed_data_provider.ConsumeIntegralInRange<unsigned>(0, 50);
149 mempool_opts.max_size_bytes = fuzzed_data_provider.ConsumeIntegralInRange<unsigned>(0, 200) * 1'000'000;
150 mempool_opts.expiry = std::chrono::hours{fuzzed_data_provider.ConsumeIntegralInRange<unsigned>(0, 999)};
151 // Only interested in 2 cases: sigop cost 0 or when single legacy sigop cost is >> 1KvB
152 nBytesPerSigOp = fuzzed_data_provider.ConsumeIntegralInRange<unsigned>(0, 1) * 10'000;
153
154 mempool_opts.check_ratio = 1;
155 mempool_opts.require_standard = fuzzed_data_provider.ConsumeBool();
156
157 bilingual_str error;
158 // ...and construct a CTxMemPool from it
159 auto mempool{std::make_unique<CTxMemPool>(std::move(mempool_opts), error)};
160 // ... ignore the error since it might be beneficial to fuzz even when the
161 // mempool size is unreasonably small
162 Assert(error.empty() || error.original.starts_with("-maxmempool must be at least "));
163 return mempool;
164 }
165
166 std::unique_ptr<CTxMemPool> MakeEphemeralMempool(const NodeContext& node)
167 {
168 // Take the default options for tests...
169 CTxMemPool::Options mempool_opts{MemPoolOptionsForTest(node)};
170
171 mempool_opts.check_ratio = 1;
172
173 // Require standardness rules otherwise ephemeral dust is no-op
174 mempool_opts.require_standard = true;
175
176 // And set minrelay to 0 to allow ephemeral parent tx even with non-TRUC
177 mempool_opts.min_relay_feerate = CFeeRate(0);
178
179 bilingual_str error;
180 // ...and construct a CTxMemPool from it
181 auto mempool{std::make_unique<CTxMemPool>(std::move(mempool_opts), error)};
182 Assert(error.empty());
183 return mempool;
184 }
185
186 // Scan mempool for a tx that has spent dust and return a
187 // prevout of the child that isn't the dusty parent itself.
188 // This is used to double-spend the child out of the mempool,
189 // leaving the parent childless.
190 // This assumes CheckMempoolEphemeralInvariants has passed for tx_pool.
191 std::optional<COutPoint> GetChildEvictingPrevout(const CTxMemPool& tx_pool)
192 {
193 LOCK(tx_pool.cs);
194 for (const auto& tx_info : tx_pool.infoAll()) {
195 const auto& entry = *Assert(tx_pool.GetEntry(tx_info.tx->GetHash()));
196 std::vector<uint32_t> dust_indexes{GetDust(*tx_info.tx, tx_pool.m_opts.dust_relay_feerate)};
197 if (!dust_indexes.empty()) {
198 const auto& children = tx_pool.GetChildren(entry);
199 if (!children.empty()) {
200 Assert(children.size() == 1);
201 // Find an input that doesn't spend from parent's txid
202 const auto& only_child = children.begin()->get().GetTx();
203 for (const auto& tx_input : only_child.vin) {
204 if (tx_input.prevout.hash != tx_info.tx->GetHash()) {
205 return tx_input.prevout;
206 }
207 }
208 }
209 }
210 }
211
212 return std::nullopt;
213 }
214
215 FUZZ_TARGET(ephemeral_package_eval, .init = initialize_tx_pool)
216 {
217 SeedRandomStateForTest(SeedRand::ZEROS);
218 FuzzedDataProvider fuzzed_data_provider(buffer.data(), buffer.size());
219 const auto& node = g_setup->m_node;
220 auto& chainstate{static_cast<DummyChainState&>(node.chainman->ActiveChainstate())};
221
222 MockTime(fuzzed_data_provider, chainstate);
223
224 // All RBF-spendable outpoints outside of the unsubmitted package
225 std::set<COutPoint> mempool_outpoints;
226 std::unordered_map<COutPoint, CAmount, SaltedOutpointHasher> outpoints_value;
227 for (const auto& outpoint : g_outpoints_coinbase_init_mature) {
228 Assert(mempool_outpoints.insert(outpoint).second);
229 outpoints_value[outpoint] = 50 * COIN;
230 }
231
232 auto outpoints_updater = std::make_shared<OutpointsUpdater>(mempool_outpoints);
233 node.validation_signals->RegisterSharedValidationInterface(outpoints_updater);
234
235 auto tx_pool_{MakeEphemeralMempool(node)};
236 MockedTxPool& tx_pool = *static_cast<MockedTxPool*>(tx_pool_.get());
237
238 chainstate.SetMempool(&tx_pool);
239
240 LIMITED_WHILE(fuzzed_data_provider.remaining_bytes() > 0, 300)
241 {
242 Assert(!mempool_outpoints.empty());
243
244 std::vector<CTransactionRef> txs;
245
246 // Find something we may want to double-spend with two input single tx
247 std::optional<COutPoint> outpoint_to_rbf{fuzzed_data_provider.ConsumeBool() ? GetChildEvictingPrevout(tx_pool) : std::nullopt};
248
249 // Make small packages
250 const auto num_txs = outpoint_to_rbf ? 1 : fuzzed_data_provider.ConsumeIntegralInRange<size_t>(1, 4);
251
252 std::set<COutPoint> package_outpoints;
253 while (txs.size() < num_txs) {
254 // Create transaction to add to the mempool
255 txs.emplace_back([&] {
256 CMutableTransaction tx_mut;
257 tx_mut.version = CTransaction::CURRENT_VERSION;
258 tx_mut.nLockTime = 0;
259 // Last transaction in a package needs to be a child of parents to get further in validation
260 // so the last transaction to be generated(in a >1 package) must spend all package-made outputs
261 // Note that this test currently only spends package outputs in last transaction.
262 bool last_tx = num_txs > 1 && txs.size() == num_txs - 1;
263 const auto num_in = outpoint_to_rbf ? 2 :
264 last_tx ? fuzzed_data_provider.ConsumeIntegralInRange<int>(package_outpoints.size()/2 + 1, package_outpoints.size()) :
265 fuzzed_data_provider.ConsumeIntegralInRange<int>(1, 4);
266 const auto num_out = outpoint_to_rbf ? 1 : fuzzed_data_provider.ConsumeIntegralInRange<int>(1, 4);
267
268 auto& outpoints = last_tx ? package_outpoints : mempool_outpoints;
269
270 Assert((int)outpoints.size() >= num_in && num_in > 0);
271
272 CAmount amount_in{0};
273 for (int i = 0; i < num_in; ++i) {
274 // Pop random outpoint. We erase them to avoid double-spending
275 // while in this loop, but later add them back (unless last_tx).
276 auto pop = outpoints.begin();
277 std::advance(pop, fuzzed_data_provider.ConsumeIntegralInRange<size_t>(0, outpoints.size() - 1));
278 auto outpoint = *pop;
279
280 if (i == 0 && outpoint_to_rbf) {
281 outpoint = *outpoint_to_rbf;
282 outpoints.erase(outpoint);
283 } else {
284 outpoints.erase(pop);
285 }
286 // no need to update or erase from outpoints_value
287 amount_in += outpoints_value.at(outpoint);
288
289 // Create input
290 CTxIn in;
291 in.prevout = outpoint;
292 in.scriptWitness.stack = P2WSH_EMPTY_TRUE_STACK;
293
294 tx_mut.vin.push_back(in);
295 }
296
297 const auto amount_fee = fuzzed_data_provider.ConsumeIntegralInRange<CAmount>(0, amount_in);
298 const auto amount_out = (amount_in - amount_fee) / num_out;
299 for (int i = 0; i < num_out; ++i) {
300 tx_mut.vout.emplace_back(amount_out, P2WSH_EMPTY);
301 }
302
303 // Note output amounts can naturally drop to dust on their own.
304 if (!outpoint_to_rbf && fuzzed_data_provider.ConsumeBool()) {
305 uint32_t dust_index = fuzzed_data_provider.ConsumeIntegralInRange<uint32_t>(0, num_out);
306 tx_mut.vout.insert(tx_mut.vout.begin() + dust_index, CTxOut(0, P2WSH_EMPTY));
307 }
308
309 auto tx = MakeTransactionRef(tx_mut);
310 // Restore previously removed outpoints, except in-package outpoints (to allow RBF)
311 if (!last_tx) {
312 for (const auto& in : tx->vin) {
313 Assert(outpoints.insert(in.prevout).second);
314 }
315 // Cache the in-package outpoints being made
316 for (size_t i = 0; i < tx->vout.size(); ++i) {
317 package_outpoints.emplace(tx->GetHash(), i);
318 }
319 }
320 // We need newly-created values for the duration of this run
321 for (size_t i = 0; i < tx->vout.size(); ++i) {
322 outpoints_value[COutPoint(tx->GetHash(), i)] = tx->vout[i].nValue;
323 }
324 return tx;
325 }());
326 }
327
328 if (fuzzed_data_provider.ConsumeBool()) {
329 const auto& txid = fuzzed_data_provider.ConsumeBool() ?
330 txs.back()->GetHash() :
331 PickValue(fuzzed_data_provider, mempool_outpoints).hash;
332 const auto delta = fuzzed_data_provider.ConsumeIntegralInRange<CAmount>(-50 * COIN, +50 * COIN);
333 // We only prioritise out of mempool transactions since PrioritiseTransaction doesn't
334 // filter for ephemeral dust
335 if (tx_pool.exists(txid)) {
336 const auto tx_info{tx_pool.info(txid)};
337 if (GetDust(*tx_info.tx, tx_pool.m_opts.dust_relay_feerate).empty()) {
338 tx_pool.PrioritiseTransaction(txid, delta);
339 }
340 }
341 }
342
343 auto single_submit = txs.size() == 1;
344
345 const auto result_package = WITH_LOCK(::cs_main,
346 return ProcessNewPackage(chainstate, tx_pool, txs, /*test_accept=*/single_submit, /*client_maxfeerate=*/{}));
347
348 const auto res = WITH_LOCK(::cs_main, return AcceptToMemoryPool(chainstate, txs.back(), GetTime(),
349 /*bypass_limits=*/false, /*test_accept=*/!single_submit));
350
351 if (!single_submit && result_package.m_state.GetResult() != PackageValidationResult::PCKG_POLICY) {
352 // We don't know anything about the validity since transactions were randomly generated, so
353 // just use result_package.m_state here. This makes the expect_valid check meaningless, but
354 // we can still verify that the contents of m_tx_results are consistent with m_state.
355 const bool expect_valid{result_package.m_state.IsValid()};
356 Assert(!CheckPackageMempoolAcceptResult(txs, result_package, expect_valid, &tx_pool));
357 }
358
359 node.validation_signals->SyncWithValidationInterfaceQueue();
360
361 CheckMempoolEphemeralInvariants(tx_pool);
362 }
363
364 node.validation_signals->UnregisterSharedValidationInterface(outpoints_updater);
365
366 WITH_LOCK(::cs_main, tx_pool.check(chainstate.CoinsTip(), chainstate.m_chain.Height() + 1));
367 }
368
369
370 FUZZ_TARGET(tx_package_eval, .init = initialize_tx_pool)
371 {
372 SeedRandomStateForTest(SeedRand::ZEROS);
373 FuzzedDataProvider fuzzed_data_provider(buffer.data(), buffer.size());
374 const auto& node = g_setup->m_node;
375 auto& chainstate{static_cast<DummyChainState&>(node.chainman->ActiveChainstate())};
376
377 MockTime(fuzzed_data_provider, chainstate);
378
379 // All RBF-spendable outpoints outside of the unsubmitted package
380 std::set<COutPoint> mempool_outpoints;
381 std::unordered_map<COutPoint, CAmount, SaltedOutpointHasher> outpoints_value;
382 for (const auto& outpoint : g_outpoints_coinbase_init_mature) {
383 Assert(mempool_outpoints.insert(outpoint).second);
384 outpoints_value[outpoint] = 50 * COIN;
385 }
386
387 auto outpoints_updater = std::make_shared<OutpointsUpdater>(mempool_outpoints);
388 node.validation_signals->RegisterSharedValidationInterface(outpoints_updater);
389
390 auto tx_pool_{MakeMempool(fuzzed_data_provider, node)};
391 MockedTxPool& tx_pool = *static_cast<MockedTxPool*>(tx_pool_.get());
392
393 chainstate.SetMempool(&tx_pool);
394
395 LIMITED_WHILE(fuzzed_data_provider.remaining_bytes() > 0, 300)
396 {
397 Assert(!mempool_outpoints.empty());
398
399 std::vector<CTransactionRef> txs;
400
401 // Make packages of 1-to-26 transactions
402 const auto num_txs = fuzzed_data_provider.ConsumeIntegralInRange<size_t>(1, 26);
403 std::set<COutPoint> package_outpoints;
404 while (txs.size() < num_txs) {
405 // Create transaction to add to the mempool
406 txs.emplace_back([&] {
407 CMutableTransaction tx_mut;
408 tx_mut.version = fuzzed_data_provider.ConsumeBool() ? TRUC_VERSION : CTransaction::CURRENT_VERSION;
409 tx_mut.nLockTime = fuzzed_data_provider.ConsumeBool() ? 0 : fuzzed_data_provider.ConsumeIntegral<uint32_t>();
410 // Last transaction in a package needs to be a child of parents to get further in validation
411 // so the last transaction to be generated(in a >1 package) must spend all package-made outputs
412 // Note that this test currently only spends package outputs in last transaction.
413 bool last_tx = num_txs > 1 && txs.size() == num_txs - 1;
414 const auto num_in = last_tx ? package_outpoints.size() : fuzzed_data_provider.ConsumeIntegralInRange<int>(1, mempool_outpoints.size());
415 auto num_out = fuzzed_data_provider.ConsumeIntegralInRange<int>(1, mempool_outpoints.size() * 2);
416
417 auto& outpoints = last_tx ? package_outpoints : mempool_outpoints;
418
419 Assert(!outpoints.empty());
420
421 CAmount amount_in{0};
422 for (size_t i = 0; i < num_in; ++i) {
423 // Pop random outpoint. We erase them to avoid double-spending
424 // while in this loop, but later add them back (unless last_tx).
425 auto pop = outpoints.begin();
426 std::advance(pop, fuzzed_data_provider.ConsumeIntegralInRange<size_t>(0, outpoints.size() - 1));
427 const auto outpoint = *pop;
428 outpoints.erase(pop);
429 // no need to update or erase from outpoints_value
430 amount_in += outpoints_value.at(outpoint);
431
432 // Create input
433 const auto sequence = ConsumeSequence(fuzzed_data_provider);
434 const auto script_sig = CScript{};
435 const auto script_wit_stack = fuzzed_data_provider.ConsumeBool() ? P2WSH_EMPTY_TRUE_STACK : P2WSH_EMPTY_TWO_STACK;
436
437 CTxIn in;
438 in.prevout = outpoint;
439 in.nSequence = sequence;
440 in.scriptSig = script_sig;
441 in.scriptWitness.stack = script_wit_stack;
442
443 tx_mut.vin.push_back(in);
444 }
445
446 // Duplicate an input
447 bool dup_input = fuzzed_data_provider.ConsumeBool();
448 if (dup_input) {
449 tx_mut.vin.push_back(tx_mut.vin.back());
450 }
451
452 // Refer to a non-existent input
453 if (fuzzed_data_provider.ConsumeBool()) {
454 tx_mut.vin.emplace_back();
455 }
456
457 // Make a p2pk output to make sigops adjusted vsize to violate TRUC rules, potentially, which is never spent
458 if (last_tx && amount_in > 1000 && fuzzed_data_provider.ConsumeBool()) {
459 tx_mut.vout.emplace_back(1000, CScript() << std::vector<unsigned char>(33, 0x02) << OP_CHECKSIG);
460 // Don't add any other outputs.
461 num_out = 1;
462 amount_in -= 1000;
463 }
464
465 const auto amount_fee = fuzzed_data_provider.ConsumeIntegralInRange<CAmount>(0, amount_in);
466 const auto amount_out = (amount_in - amount_fee) / num_out;
467 for (int i = 0; i < num_out; ++i) {
468 tx_mut.vout.emplace_back(amount_out, P2WSH_EMPTY);
469 }
470 auto tx = MakeTransactionRef(tx_mut);
471 // Restore previously removed outpoints, except in-package outpoints
472 if (!last_tx) {
473 for (const auto& in : tx->vin) {
474 // It's a fake input, or a new input, or a duplicate
475 Assert(in == CTxIn() || outpoints.insert(in.prevout).second || dup_input);
476 }
477 // Cache the in-package outpoints being made
478 for (size_t i = 0; i < tx->vout.size(); ++i) {
479 package_outpoints.emplace(tx->GetHash(), i);
480 }
481 }
482 // We need newly-created values for the duration of this run
483 for (size_t i = 0; i < tx->vout.size(); ++i) {
484 outpoints_value[COutPoint(tx->GetHash(), i)] = tx->vout[i].nValue;
485 }
486 return tx;
487 }());
488 }
489
490 if (fuzzed_data_provider.ConsumeBool()) {
491 MockTime(fuzzed_data_provider, chainstate);
492 }
493 if (fuzzed_data_provider.ConsumeBool()) {
494 tx_pool.RollingFeeUpdate();
495 }
496 if (fuzzed_data_provider.ConsumeBool()) {
497 const auto& txid = fuzzed_data_provider.ConsumeBool() ?
498 txs.back()->GetHash() :
499 PickValue(fuzzed_data_provider, mempool_outpoints).hash;
500 const auto delta = fuzzed_data_provider.ConsumeIntegralInRange<CAmount>(-50 * COIN, +50 * COIN);
501 tx_pool.PrioritiseTransaction(txid, delta);
502 }
503
504 // Remember all added transactions
505 std::set<CTransactionRef> added;
506 auto txr = std::make_shared<TransactionsDelta>(added);
507 node.validation_signals->RegisterSharedValidationInterface(txr);
508
509 // When there are multiple transactions in the package, we call ProcessNewPackage(txs, test_accept=false)
510 // and AcceptToMemoryPool(txs.back(), test_accept=true). When there is only 1 transaction, we might flip it
511 // (the package is a test accept and ATMP is a submission).
512 auto single_submit = txs.size() == 1 && fuzzed_data_provider.ConsumeBool();
513
514 // Exercise client_maxfeerate logic
515 std::optional<CFeeRate> client_maxfeerate{};
516 if (fuzzed_data_provider.ConsumeBool()) {
517 client_maxfeerate = CFeeRate(fuzzed_data_provider.ConsumeIntegralInRange<CAmount>(-1, 50 * COIN), 100);
518 }
519
520 const auto result_package = WITH_LOCK(::cs_main,
521 return ProcessNewPackage(chainstate, tx_pool, txs, /*test_accept=*/single_submit, client_maxfeerate));
522
523 // Always set bypass_limits to false because it is not supported in ProcessNewPackage and
524 // can be a source of divergence.
525 const auto res = WITH_LOCK(::cs_main, return AcceptToMemoryPool(chainstate, txs.back(), GetTime(),
526 /*bypass_limits=*/false, /*test_accept=*/!single_submit));
527 const bool passed = res.m_result_type == MempoolAcceptResult::ResultType::VALID;
528
529 node.validation_signals->SyncWithValidationInterfaceQueue();
530 node.validation_signals->UnregisterSharedValidationInterface(txr);
531
532 // There is only 1 transaction in the package. We did a test-package-accept and a ATMP
533 if (single_submit) {
534 Assert(passed != added.empty());
535 Assert(passed == res.m_state.IsValid());
536 if (passed) {
537 Assert(added.size() == 1);
538 Assert(txs.back() == *added.begin());
539 }
540 } else if (result_package.m_state.GetResult() != PackageValidationResult::PCKG_POLICY) {
541 // We don't know anything about the validity since transactions were randomly generated, so
542 // just use result_package.m_state here. This makes the expect_valid check meaningless, but
543 // we can still verify that the contents of m_tx_results are consistent with m_state.
544 const bool expect_valid{result_package.m_state.IsValid()};
545 Assert(!CheckPackageMempoolAcceptResult(txs, result_package, expect_valid, &tx_pool));
546 } else {
547 // This is empty if it fails early checks, or "full" if transactions are looked at deeper
548 Assert(result_package.m_tx_results.size() == txs.size() || result_package.m_tx_results.empty());
549 }
550
551 CheckMempoolTRUCInvariants(tx_pool);
552
553 // Dust checks only make sense when dust is enforced
554 if (tx_pool.m_opts.require_standard) {
555 CheckMempoolEphemeralInvariants(tx_pool);
556 }
557 }
558
559 node.validation_signals->UnregisterSharedValidationInterface(outpoints_updater);
560
561 WITH_LOCK(::cs_main, tx_pool.check(chainstate.CoinsTip(), chainstate.m_chain.Height() + 1));
562 }
563 } // namespace
564