[optimization] Maintain at most 1 reconsiderable announcement per wtxid

This introduces an invariant that TxOrphanageImpl never holds more than one
announcement with m_reconsider=true for a given wtxid. This avoids duplicate
work, both in the caller might otherwise reconsider the same transaction multiple
times before it is ready, and internally in AddChildrenToWorkSet, which might
otherwise iterate over all announcements multiple times.
This commit is contained in:
Pieter Wuille
2025-06-26 14:56:16 -04:00
committed by glozow
parent af7402ccfa
commit ed24e01696
2 changed files with 38 additions and 16 deletions

View File

@@ -119,6 +119,9 @@ class TxOrphanageImpl final : public TxOrphanage {
* a transaction that can be reconsidered and to remove entries that conflict with a block.*/ * a transaction that can be reconsidered and to remove entries that conflict with a block.*/
std::unordered_map<COutPoint, std::set<Wtxid>, SaltedOutpointHasher> m_outpoint_to_orphan_it; std::unordered_map<COutPoint, std::set<Wtxid>, SaltedOutpointHasher> m_outpoint_to_orphan_it;
/** Set of Wtxids for which (exactly) one announcement with m_reconsider=true exists. */
std::set<Wtxid> m_reconsiderable_wtxids;
struct PeerDoSInfo { struct PeerDoSInfo {
TxOrphanage::Usage m_total_usage{0}; TxOrphanage::Usage m_total_usage{0};
TxOrphanage::Count m_count_announcements{0}; TxOrphanage::Count m_count_announcements{0};
@@ -261,6 +264,11 @@ void TxOrphanageImpl::Erase(Iter<Tag> it)
} }
} }
} }
// If this was the (unique) reconsiderable announcement for its wtxid, then the wtxid won't
// have any reconsiderable announcements left after erasing.
if (it->m_reconsider) m_reconsiderable_wtxids.erase(it->m_tx->GetWitnessHash());
m_orphans.get<Tag>().erase(it); m_orphans.get<Tag>().erase(it);
} }
@@ -521,6 +529,9 @@ std::vector<std::pair<Wtxid, NodeId>> TxOrphanageImpl::AddChildrenToWorkSet(cons
const auto it_by_prev = m_outpoint_to_orphan_it.find(COutPoint(tx.GetHash(), i)); const auto it_by_prev = m_outpoint_to_orphan_it.find(COutPoint(tx.GetHash(), i));
if (it_by_prev != m_outpoint_to_orphan_it.end()) { if (it_by_prev != m_outpoint_to_orphan_it.end()) {
for (const auto& wtxid : it_by_prev->second) { for (const auto& wtxid : it_by_prev->second) {
// If a reconsiderable announcement for this wtxid already exists, skip it.
if (m_reconsiderable_wtxids.contains(wtxid)) continue;
// Belt and suspenders, each entry in m_outpoint_to_orphan_it should always have at least 1 announcement. // Belt and suspenders, each entry in m_outpoint_to_orphan_it should always have at least 1 announcement.
auto it = index_by_wtxid.lower_bound(ByWtxidView{wtxid, MIN_PEER}); auto it = index_by_wtxid.lower_bound(ByWtxidView{wtxid, MIN_PEER});
if (!Assume(it != index_by_wtxid.end())) continue; if (!Assume(it != index_by_wtxid.end())) continue;
@@ -537,10 +548,10 @@ std::vector<std::pair<Wtxid, NodeId>> TxOrphanageImpl::AddChildrenToWorkSet(cons
// Mark this orphan as ready to be reconsidered. // Mark this orphan as ready to be reconsidered.
static constexpr auto mark_reconsidered_modifier = [](auto& ann) { ann.m_reconsider = true; }; static constexpr auto mark_reconsidered_modifier = [](auto& ann) { ann.m_reconsider = true; };
if (!it->m_reconsider) { Assume(!it->m_reconsider);
index_by_wtxid.modify(it, mark_reconsidered_modifier); index_by_wtxid.modify(it, mark_reconsidered_modifier);
ret.emplace_back(wtxid, it->m_announcer); ret.emplace_back(wtxid, it->m_announcer);
} m_reconsiderable_wtxids.insert(wtxid);
LogDebug(BCLog::TXPACKAGES, "added %s (wtxid=%s) to peer %d workset\n", LogDebug(BCLog::TXPACKAGES, "added %s (wtxid=%s) to peer %d workset\n",
it->m_tx->GetHash().ToString(), it->m_tx->GetWitnessHash().ToString(), it->m_announcer); it->m_tx->GetHash().ToString(), it->m_tx->GetWitnessHash().ToString(), it->m_announcer);
@@ -578,6 +589,9 @@ CTransactionRef TxOrphanageImpl::GetTxToReconsider(NodeId peer)
// reconsidered again until there is a new reason to do so. // reconsidered again until there is a new reason to do so.
static constexpr auto mark_reconsidered_modifier = [](auto& ann) { ann.m_reconsider = false; }; static constexpr auto mark_reconsidered_modifier = [](auto& ann) { ann.m_reconsider = false; };
m_orphans.get<ByPeer>().modify(it, mark_reconsidered_modifier); m_orphans.get<ByPeer>().modify(it, mark_reconsidered_modifier);
// As there is exactly one m_reconsider announcement per reconsiderable wtxids, flipping
// the m_reconsider flag means the wtxid is no longer reconsiderable.
m_reconsiderable_wtxids.erase(it->m_tx->GetWitnessHash());
return it->m_tx; return it->m_tx;
} }
return nullptr; return nullptr;
@@ -680,6 +694,7 @@ void TxOrphanageImpl::SanityCheck() const
std::unordered_map<NodeId, PeerDoSInfo> reconstructed_peer_info; std::unordered_map<NodeId, PeerDoSInfo> reconstructed_peer_info;
std::map<Wtxid, std::pair<TxOrphanage::Usage, TxOrphanage::Count>> unique_wtxids_to_scores; std::map<Wtxid, std::pair<TxOrphanage::Usage, TxOrphanage::Count>> unique_wtxids_to_scores;
std::set<COutPoint> all_outpoints; std::set<COutPoint> all_outpoints;
std::set<Wtxid> reconstructed_reconsiderable_wtxids;
for (auto it = m_orphans.begin(); it != m_orphans.end(); ++it) { for (auto it = m_orphans.begin(); it != m_orphans.end(); ++it) {
for (const auto& input : it->m_tx->vin) { for (const auto& input : it->m_tx->vin) {
@@ -691,9 +706,21 @@ void TxOrphanageImpl::SanityCheck() const
peer_info.m_total_usage += it->GetMemUsage(); peer_info.m_total_usage += it->GetMemUsage();
peer_info.m_count_announcements += 1; peer_info.m_count_announcements += 1;
peer_info.m_total_latency_score += it->GetLatencyScore(); peer_info.m_total_latency_score += it->GetLatencyScore();
if (it->m_reconsider) {
auto [_, added] = reconstructed_reconsiderable_wtxids.insert(it->m_tx->GetWitnessHash());
// Check that there is only ever 1 announcement per wtxid with m_reconsider set.
assert(added);
}
} }
assert(reconstructed_peer_info.size() == m_peer_orphanage_info.size()); assert(reconstructed_peer_info.size() == m_peer_orphanage_info.size());
// Recalculated per-peer stats are identical to m_peer_orphanage_info
assert(reconstructed_peer_info == m_peer_orphanage_info);
// Recalculated set of reconsiderable wtxids must match.
assert(m_reconsiderable_wtxids == reconstructed_reconsiderable_wtxids);
// All outpoints exist in m_outpoint_to_orphan_it, all keys in m_outpoint_to_orphan_it correspond to some // All outpoints exist in m_outpoint_to_orphan_it, all keys in m_outpoint_to_orphan_it correspond to some
// orphan, and all wtxids referenced in m_outpoint_to_orphan_it are also in m_orphans. // orphan, and all wtxids referenced in m_outpoint_to_orphan_it are also in m_orphans.
// This ensures m_outpoint_to_orphan_it is cleaned up. // This ensures m_outpoint_to_orphan_it is cleaned up.

View File

@@ -626,11 +626,11 @@ FUZZ_TARGET(txorphanage_sim)
auto tx = read_tx_fn(); auto tx = read_tx_fn();
FastRandomContext rand_ctx(rng.rand256()); FastRandomContext rand_ctx(rng.rand256());
auto added = real->AddChildrenToWorkSet(*txn[tx], rand_ctx); auto added = real->AddChildrenToWorkSet(*txn[tx], rand_ctx);
/** Map of all child wtxids, with value whether they already have a reconsiderable /** Set of not-already-reconsiderable child wtxids. */
announcement from some peer. */ std::set<Wtxid> child_wtxids;
std::map<Wtxid, bool> child_wtxids;
for (unsigned child_tx = 0; child_tx < NUM_TX; ++child_tx) { for (unsigned child_tx = 0; child_tx < NUM_TX; ++child_tx) {
if (!have_tx_fn(child_tx)) continue; if (!have_tx_fn(child_tx)) continue;
if (have_reconsiderable_fn(child_tx)) continue;
bool child_of = false; bool child_of = false;
for (auto& txin : txn[child_tx]->vin) { for (auto& txin : txn[child_tx]->vin) {
if (txin.prevout.hash == txn[tx]->GetHash()) { if (txin.prevout.hash == txn[tx]->GetHash()) {
@@ -639,11 +639,11 @@ FUZZ_TARGET(txorphanage_sim)
} }
} }
if (child_of) { if (child_of) {
child_wtxids[txn[child_tx]->GetWitnessHash()] = have_reconsiderable_fn(child_tx); child_wtxids.insert(txn[child_tx]->GetWitnessHash());
} }
} }
for (auto& [wtxid, peer] : added) { for (auto& [wtxid, peer] : added) {
// Wtxid must be a child of tx. // Wtxid must be a child of tx that is not yet reconsiderable.
auto child_wtxid_it = child_wtxids.find(wtxid); auto child_wtxid_it = child_wtxids.find(wtxid);
assert(child_wtxid_it != child_wtxids.end()); assert(child_wtxid_it != child_wtxids.end());
// Announcement must exist. // Announcement must exist.
@@ -653,18 +653,13 @@ FUZZ_TARGET(txorphanage_sim)
assert(sim_ann_it->reconsider == false); assert(sim_ann_it->reconsider == false);
// Make reconsiderable. // Make reconsiderable.
sim_ann_it->reconsider = true; sim_ann_it->reconsider = true;
} // Remove from child_wtxids map, to disallow it being reported a second time in added.
for (auto& [wtxid, peer] : added) {
// Remove from child_wtxids map, so we can check that only already-reconsiderable
// ones are missing from the result.
child_wtxids.erase(wtxid); child_wtxids.erase(wtxid);
} }
// Verify that AddChildrenToWorkSet does not select announcements that were already reconsiderable: // Verify that AddChildrenToWorkSet does not select announcements that were already reconsiderable:
// Check all child wtxids which did not occur at least once in the result were already reconsiderable // Check all child wtxids which did not occur at least once in the result were already reconsiderable
// due to a previous AddChildrenToWorkSet. // due to a previous AddChildrenToWorkSet.
for (auto& [wtxid, already_reconsider] : child_wtxids) { assert(child_wtxids.empty());
assert(already_reconsider);
}
break; break;
} else if (command-- == 0) { } else if (command-- == 0) {
// GetTxToReconsider. // GetTxToReconsider.