|
| 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