Skip to content

Commit 1ed00a0

Browse files
committed
Merge bitcoin/bitcoin#33504: Mempool: Do not enforce TRUC checks on reorg
06df14b test: add more TRUC reorg coverge (Greg Sanders) 26e71c2 Mempool: Do not enforce TRUC checks on reorg (Greg Sanders) bbe8e90 fuzz: don't bypass_limits for most mempool harnesses (Greg Sanders) Pull request description: This was the intended behavior but our tests didn't cover the scenario where in-block transactions themselves violate TRUC topological constraints. The behavior in master will potentially lead to many erroneous evictions during a reorg, where evicted TRUC packages may be very high feerate and make sense to mine all together in the next block and are well within the normal anti-DoS chain limits. This issue exists since the merge of https://github.com/bitcoin/bitcoin/pull/28948/files#diff-97c3a52bc5fad452d82670a7fd291800bae20c7bc35bb82686c2c0a4ea7b5b98R956 ACKs for top commit: sdaftuar: ACK 06df14b glozow: ACK 06df14b ismaelsadeeq: Code review ACK 06df14b Tree-SHA512: bdb6e4dd622ed8b0b11866263fff559fcca6e0ca1c56a884cca9ac4572f0026528a63a9f4c8a0660df2f5efe0766310a30e5df1d6c560f31e4324ea5d4b3c1a8
2 parents 75353a0 + 06df14b commit 1ed00a0

File tree

4 files changed

+56
-35
lines changed

4 files changed

+56
-35
lines changed

src/test/fuzz/package_eval.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -325,7 +325,7 @@ FUZZ_TARGET(ephemeral_package_eval, .init = initialize_tx_pool)
325325
return ProcessNewPackage(chainstate, tx_pool, txs, /*test_accept=*/single_submit, /*client_maxfeerate=*/{}));
326326

327327
const auto res = WITH_LOCK(::cs_main, return AcceptToMemoryPool(chainstate, txs.back(), GetTime(),
328-
/*bypass_limits=*/fuzzed_data_provider.ConsumeBool(), /*test_accept=*/!single_submit));
328+
/*bypass_limits=*/false, /*test_accept=*/!single_submit));
329329

330330
if (!single_submit && result_package.m_state.GetResult() != PackageValidationResult::PCKG_POLICY) {
331331
// We don't know anything about the validity since transactions were randomly generated, so

src/test/fuzz/tx_pool.cpp

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,6 @@ FUZZ_TARGET(tx_pool_standard, .init = initialize_tx_pool)
296296
std::set<CTransactionRef> added;
297297
auto txr = std::make_shared<TransactionsDelta>(removed, added);
298298
node.validation_signals->RegisterSharedValidationInterface(txr);
299-
const bool bypass_limits = fuzzed_data_provider.ConsumeBool();
300299

301300
// Make sure ProcessNewPackage on one transaction works.
302301
// The result is not guaranteed to be the same as what is returned by ATMP.
@@ -311,7 +310,7 @@ FUZZ_TARGET(tx_pool_standard, .init = initialize_tx_pool)
311310
it->second.m_result_type == MempoolAcceptResult::ResultType::INVALID);
312311
}
313312

314-
const auto res = WITH_LOCK(::cs_main, return AcceptToMemoryPool(chainstate, tx, GetTime(), bypass_limits, /*test_accept=*/false));
313+
const auto res = WITH_LOCK(::cs_main, return AcceptToMemoryPool(chainstate, tx, GetTime(), /*bypass_limits=*/false, /*test_accept=*/false));
315314
const bool accepted = res.m_result_type == MempoolAcceptResult::ResultType::VALID;
316315
node.validation_signals->SyncWithValidationInterfaceQueue();
317316
node.validation_signals->UnregisterSharedValidationInterface(txr);
@@ -394,6 +393,9 @@ FUZZ_TARGET(tx_pool, .init = initialize_tx_pool)
394393

395394
chainstate.SetMempool(&tx_pool);
396395

396+
// If we ever bypass limits, do not do TRUC invariants checks
397+
bool ever_bypassed_limits{false};
398+
397399
LIMITED_WHILE(fuzzed_data_provider.ConsumeBool(), 300)
398400
{
399401
const auto mut_tx = ConsumeTransaction(fuzzed_data_provider, txids);
@@ -412,13 +414,17 @@ FUZZ_TARGET(tx_pool, .init = initialize_tx_pool)
412414
tx_pool.PrioritiseTransaction(txid, delta);
413415
}
414416

417+
const bool bypass_limits{fuzzed_data_provider.ConsumeBool()};
418+
ever_bypassed_limits |= bypass_limits;
419+
415420
const auto tx = MakeTransactionRef(mut_tx);
416-
const bool bypass_limits = fuzzed_data_provider.ConsumeBool();
417421
const auto res = WITH_LOCK(::cs_main, return AcceptToMemoryPool(chainstate, tx, GetTime(), bypass_limits, /*test_accept=*/false));
418422
const bool accepted = res.m_result_type == MempoolAcceptResult::ResultType::VALID;
419423
if (accepted) {
420424
txids.push_back(tx->GetHash());
421-
CheckMempoolTRUCInvariants(tx_pool);
425+
if (!ever_bypassed_limits) {
426+
CheckMempoolTRUCInvariants(tx_pool);
427+
}
422428
}
423429
}
424430
Finish(fuzzed_data_provider, tx_pool, chainstate);

src/validation.cpp

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1044,26 +1044,28 @@ bool MemPoolAccept::PreChecks(ATMPArgs& args, Workspace& ws)
10441044
// Even though just checking direct mempool parents for inheritance would be sufficient, we
10451045
// check using the full ancestor set here because it's more convenient to use what we have
10461046
// already calculated.
1047-
if (const auto err{SingleTRUCChecks(ws.m_ptx, ws.m_ancestors, ws.m_conflicts, ws.m_vsize)}) {
1048-
// Single transaction contexts only.
1049-
if (args.m_allow_sibling_eviction && err->second != nullptr) {
1050-
// We should only be considering where replacement is considered valid as well.
1051-
Assume(args.m_allow_replacement);
1052-
1053-
// Potential sibling eviction. Add the sibling to our list of mempool conflicts to be
1054-
// included in RBF checks.
1055-
ws.m_conflicts.insert(err->second->GetHash());
1056-
// Adding the sibling to m_iters_conflicting here means that it doesn't count towards
1057-
// RBF Carve Out above. This is correct, since removing to-be-replaced transactions from
1058-
// the descendant count is done separately in SingleTRUCChecks for TRUC transactions.
1059-
ws.m_iters_conflicting.insert(m_pool.GetIter(err->second->GetHash()).value());
1060-
ws.m_sibling_eviction = true;
1061-
// The sibling will be treated as part of the to-be-replaced set in ReplacementChecks.
1062-
// Note that we are not checking whether it opts in to replaceability via BIP125 or TRUC
1063-
// (which is normally done in PreChecks). However, the only way a TRUC transaction can
1064-
// have a non-TRUC and non-BIP125 descendant is due to a reorg.
1065-
} else {
1066-
return state.Invalid(TxValidationResult::TX_MEMPOOL_POLICY, "TRUC-violation", err->first);
1047+
if (!args.m_bypass_limits) {
1048+
if (const auto err{SingleTRUCChecks(ws.m_ptx, ws.m_ancestors, ws.m_conflicts, ws.m_vsize)}) {
1049+
// Single transaction contexts only.
1050+
if (args.m_allow_sibling_eviction && err->second != nullptr) {
1051+
// We should only be considering where replacement is considered valid as well.
1052+
Assume(args.m_allow_replacement);
1053+
1054+
// Potential sibling eviction. Add the sibling to our list of mempool conflicts to be
1055+
// included in RBF checks.
1056+
ws.m_conflicts.insert(err->second->GetHash());
1057+
// Adding the sibling to m_iters_conflicting here means that it doesn't count towards
1058+
// RBF Carve Out above. This is correct, since removing to-be-replaced transactions from
1059+
// the descendant count is done separately in SingleTRUCChecks for TRUC transactions.
1060+
ws.m_iters_conflicting.insert(m_pool.GetIter(err->second->GetHash()).value());
1061+
ws.m_sibling_eviction = true;
1062+
// The sibling will be treated as part of the to-be-replaced set in ReplacementChecks.
1063+
// Note that we are not checking whether it opts in to replaceability via BIP125 or TRUC
1064+
// (which is normally done in PreChecks). However, the only way a TRUC transaction can
1065+
// have a non-TRUC and non-BIP125 descendant is due to a reorg.
1066+
} else {
1067+
return state.Invalid(TxValidationResult::TX_MEMPOOL_POLICY, "TRUC-violation", err->first);
1068+
}
10671069
}
10681070
}
10691071

test/functional/mempool_truc.py

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -165,23 +165,36 @@ def test_truc_replacement(self):
165165
def test_truc_reorg(self):
166166
node = self.nodes[0]
167167
self.log.info("Test that, during a reorg, TRUC rules are not enforced")
168-
tx_v2_block = self.wallet.send_self_transfer(from_node=node, version=2)
169-
tx_v3_block = self.wallet.send_self_transfer(from_node=node, version=3)
170-
tx_v3_block2 = self.wallet.send_self_transfer(from_node=node, version=3)
171-
self.check_mempool([tx_v3_block["txid"], tx_v2_block["txid"], tx_v3_block2["txid"]])
168+
self.check_mempool([])
169+
170+
# Testing 2<-3 versions allowed
171+
tx_v2_block = self.wallet.create_self_transfer(version=2)
172+
173+
# Testing 3<-2 versions allowed
174+
tx_v3_block = self.wallet.create_self_transfer(version=3)
175+
176+
# Testing overly-large child size
177+
tx_v3_block2 = self.wallet.create_self_transfer(version=3)
178+
179+
# Also create a linear chain of 3 TRUC transactions that will be directly mined, followed by one v2 in-mempool after block is made
180+
tx_chain_1 = self.wallet.create_self_transfer(version=3)
181+
tx_chain_2 = self.wallet.create_self_transfer(utxo_to_spend=tx_chain_1["new_utxo"], version=3)
182+
tx_chain_3 = self.wallet.create_self_transfer(utxo_to_spend=tx_chain_2["new_utxo"], version=3)
183+
184+
tx_to_mine = [tx_v3_block["hex"], tx_v2_block["hex"], tx_v3_block2["hex"], tx_chain_1["hex"], tx_chain_2["hex"], tx_chain_3["hex"]]
185+
block = self.generateblock(node, output="raw(42)", transactions=tx_to_mine)
172186

173-
block = self.generate(node, 1)
174187
self.check_mempool([])
175188
tx_v2_from_v3 = self.wallet.send_self_transfer(from_node=node, utxo_to_spend=tx_v3_block["new_utxo"], version=2)
176189
tx_v3_from_v2 = self.wallet.send_self_transfer(from_node=node, utxo_to_spend=tx_v2_block["new_utxo"], version=3)
177190
tx_v3_child_large = self.wallet.send_self_transfer(from_node=node, utxo_to_spend=tx_v3_block2["new_utxo"], target_vsize=1250, version=3)
178191
assert_greater_than(node.getmempoolentry(tx_v3_child_large["txid"])["vsize"], TRUC_CHILD_MAX_VSIZE)
179-
self.check_mempool([tx_v2_from_v3["txid"], tx_v3_from_v2["txid"], tx_v3_child_large["txid"]])
180-
node.invalidateblock(block[0])
181-
self.check_mempool([tx_v3_block["txid"], tx_v2_block["txid"], tx_v3_block2["txid"], tx_v2_from_v3["txid"], tx_v3_from_v2["txid"], tx_v3_child_large["txid"]])
182-
# This is needed because generate() will create the exact same block again.
183-
node.reconsiderblock(block[0])
192+
tx_chain_4 = self.wallet.send_self_transfer(from_node=node, utxo_to_spend=tx_chain_3["new_utxo"], version=2)
193+
self.check_mempool([tx_v2_from_v3["txid"], tx_v3_from_v2["txid"], tx_v3_child_large["txid"], tx_chain_4["txid"]])
184194

195+
# Reorg should have all block transactions re-accepted, ignoring TRUC enforcement
196+
node.invalidateblock(block["hash"])
197+
self.check_mempool([tx_v3_block["txid"], tx_v2_block["txid"], tx_v3_block2["txid"], tx_v2_from_v3["txid"], tx_v3_from_v2["txid"], tx_v3_child_large["txid"], tx_chain_1["txid"], tx_chain_2["txid"], tx_chain_3["txid"], tx_chain_4["txid"]])
185198

186199
@cleanup(extra_args=["-limitdescendantsize=10"])
187200
def test_nondefault_package_limits(self):

0 commit comments

Comments
 (0)