Skip to content

Commit 329b60f

Browse files
committed
Merge bitcoin/bitcoin#31810: TxOrphanage: account for size of orphans and count announcements
e107bf7 [fuzz] TxOrphanage::SanityCheck accounting (glozow) 22dccea [fuzz] txorphan byte accounting (glozow) 982ce10 add orphanage byte accounting to TxDownloadManagerImpl::CheckIsEmpty() (glozow) c289217 [txorphanage] track the total number of announcements (glozow) e5ea7da [txorphanage] add per-peer weight accounting (glozow) 672c69c [refactor] change per-peer workset to info map within orphanage (glozow) 59cd0f0 [txorphanage] account for weight of orphans (glozow) Pull request description: Part of orphan resolution project, see #27463. Definitions: - **Announcement** is a unique pair (wtxid, nodeid). We can have multiple announcers for the same orphan since #31397. - **Size** is the weight of an orphan. I'm calling it "size" and "bytes" because I think we can refine it in the future to be memusage or be otherwise more representative of the orphan's actual cost on our memory. However, I am open to naming changes. This is part 1/2 of a project to also add limits on orphan size and count. However, this PR **does not change behavior**, just adds internal counters/tracking and a fuzzer. I will also open a second PR that adds behavior changes, which requires updating a lot of our tests and careful thinking about DoS. ACKs for top commit: instagibbs: reACK bitcoin/bitcoin@e107bf7 marcofleon: reACK e107bf7 sipa: utACK e107bf7 Tree-SHA512: 855d725d5eb521d131e36dacc51990725e3ca7881beb13364d5ba72ab2202bbfd14ab83864b13b1b945a4ec5e17890458d0112270b891a41b1e27324a8545d72
2 parents bc3f59c + e107bf7 commit 329b60f

File tree

5 files changed

+175
-24
lines changed

5 files changed

+175
-24
lines changed

src/node/txdownloadman_impl.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -578,9 +578,11 @@ CTransactionRef TxDownloadManagerImpl::GetTxToReconsider(NodeId nodeid)
578578
void TxDownloadManagerImpl::CheckIsEmpty(NodeId nodeid)
579579
{
580580
assert(m_txrequest.Count(nodeid) == 0);
581+
assert(m_orphanage.UsageByPeer(nodeid) == 0);
581582
}
582583
void TxDownloadManagerImpl::CheckIsEmpty()
583584
{
585+
assert(m_orphanage.TotalOrphanUsage() == 0);
584586
assert(m_orphanage.Size() == 0);
585587
assert(m_txrequest.Size() == 0);
586588
assert(m_num_wtxid_peers == 0);

src/test/fuzz/txdownloadman.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,7 @@ static void CheckInvariants(const node::TxDownloadManagerImpl& txdownload_impl,
285285

286286
// Orphanage usage should never exceed what is allowed
287287
Assert(orphanage.Size() <= max_orphan_count);
288+
txdownload_impl.m_orphanage.SanityCheck();
288289

289290
// We should never have more than the maximum in-flight requests out for a peer.
290291
for (NodeId peer = 0; peer < NUM_PEERS; ++peer) {
@@ -437,8 +438,8 @@ FUZZ_TARGET(txdownloadman_impl, .init = initialize)
437438
auto time_skip = fuzzed_data_provider.PickValueInArray(TIME_SKIPS);
438439
if (fuzzed_data_provider.ConsumeBool()) time_skip *= -1;
439440
time += time_skip;
440-
CheckInvariants(txdownload_impl, max_orphan_count);
441441
}
442+
CheckInvariants(txdownload_impl, max_orphan_count);
442443
// Disconnect everybody, check that all data structures are empty.
443444
for (NodeId nodeid = 0; nodeid < NUM_PEERS; ++nodeid) {
444445
txdownload_impl.DisconnectedPeer(nodeid);

src/test/fuzz/txorphan.cpp

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ FUZZ_TARGET(txorphan, .init = initialize_orphanage)
7575
return new_tx;
7676
}();
7777

78+
const auto wtxid{tx->GetWitnessHash()};
79+
7880
// Trigger orphanage functions that are called using parents. ptx_potential_parent is a tx we constructed in a
7981
// previous loop and potentially the parent of this tx.
8082
if (ptx_potential_parent) {
@@ -94,6 +96,9 @@ FUZZ_TARGET(txorphan, .init = initialize_orphanage)
9496
LIMITED_WHILE(fuzzed_data_provider.ConsumeBool(), 10 * DEFAULT_MAX_ORPHAN_TRANSACTIONS)
9597
{
9698
NodeId peer_id = fuzzed_data_provider.ConsumeIntegral<NodeId>();
99+
const auto total_bytes_start{orphanage.TotalOrphanUsage()};
100+
const auto total_peer_bytes_start{orphanage.UsageByPeer(peer_id)};
101+
const auto tx_weight{GetTransactionWeight(*tx)};
97102

98103
CallOneOf(
99104
fuzzed_data_provider,
@@ -113,12 +118,29 @@ FUZZ_TARGET(txorphan, .init = initialize_orphanage)
113118
bool add_tx = orphanage.AddTx(tx, peer_id);
114119
// have_tx == true -> add_tx == false
115120
Assert(!have_tx || !add_tx);
121+
122+
if (add_tx) {
123+
Assert(orphanage.UsageByPeer(peer_id) == tx_weight + total_peer_bytes_start);
124+
Assert(orphanage.TotalOrphanUsage() == tx_weight + total_bytes_start);
125+
Assert(tx_weight <= MAX_STANDARD_TX_WEIGHT);
126+
} else {
127+
// Peer may have been added as an announcer.
128+
if (orphanage.UsageByPeer(peer_id) == tx_weight + total_peer_bytes_start) {
129+
Assert(orphanage.HaveTxFromPeer(wtxid, peer_id));
130+
} else {
131+
// Otherwise, there must not be any change to the peer byte count.
132+
Assert(orphanage.UsageByPeer(peer_id) == total_peer_bytes_start);
133+
}
134+
135+
// Regardless, total bytes should not have changed.
136+
Assert(orphanage.TotalOrphanUsage() == total_bytes_start);
137+
}
116138
}
117139
have_tx = orphanage.HaveTx(tx->GetWitnessHash());
118140
{
119141
bool add_tx = orphanage.AddTx(tx, peer_id);
120142
// if have_tx is still false, it must be too big
121-
Assert(!have_tx == (GetTransactionWeight(*tx) > MAX_STANDARD_TX_WEIGHT));
143+
Assert(!have_tx == (tx_weight > MAX_STANDARD_TX_WEIGHT));
122144
Assert(!have_tx || !add_tx);
123145
}
124146
},
@@ -132,23 +154,46 @@ FUZZ_TARGET(txorphan, .init = initialize_orphanage)
132154
Assert(have_tx || !added_announcer);
133155
// have_tx_and_peer == true -> added_announcer == false
134156
Assert(!have_tx_and_peer || !added_announcer);
157+
158+
// Total bytes should not have changed. If peer was added as announcer, byte
159+
// accounting must have been updated.
160+
Assert(orphanage.TotalOrphanUsage() == total_bytes_start);
161+
if (added_announcer) {
162+
Assert(orphanage.UsageByPeer(peer_id) == tx_weight + total_peer_bytes_start);
163+
} else {
164+
Assert(orphanage.UsageByPeer(peer_id) == total_peer_bytes_start);
165+
}
135166
}
136167
},
137168
[&] {
138169
bool have_tx = orphanage.HaveTx(tx->GetWitnessHash());
170+
bool have_tx_and_peer{orphanage.HaveTxFromPeer(wtxid, peer_id)};
139171
// EraseTx should return 0 if m_orphans doesn't have the tx
140172
{
173+
auto bytes_from_peer_before{orphanage.UsageByPeer(peer_id)};
141174
Assert(have_tx == orphanage.EraseTx(tx->GetWitnessHash()));
175+
if (have_tx) {
176+
Assert(orphanage.TotalOrphanUsage() == total_bytes_start - tx_weight);
177+
if (have_tx_and_peer) {
178+
Assert(orphanage.UsageByPeer(peer_id) == bytes_from_peer_before - tx_weight);
179+
} else {
180+
Assert(orphanage.UsageByPeer(peer_id) == bytes_from_peer_before);
181+
}
182+
} else {
183+
Assert(orphanage.TotalOrphanUsage() == total_bytes_start);
184+
}
142185
}
143186
have_tx = orphanage.HaveTx(tx->GetWitnessHash());
187+
have_tx_and_peer = orphanage.HaveTxFromPeer(wtxid, peer_id);
144188
// have_tx should be false and EraseTx should fail
145189
{
146-
Assert(!have_tx && !orphanage.EraseTx(tx->GetWitnessHash()));
190+
Assert(!have_tx && !have_tx_and_peer && !orphanage.EraseTx(wtxid));
147191
}
148192
},
149193
[&] {
150194
orphanage.EraseForPeer(peer_id);
151195
Assert(!orphanage.HaveTxFromPeer(tx->GetWitnessHash(), peer_id));
196+
Assert(orphanage.UsageByPeer(peer_id) == 0);
152197
},
153198
[&] {
154199
// test mocktime and expiry
@@ -159,6 +204,7 @@ FUZZ_TARGET(txorphan, .init = initialize_orphanage)
159204
});
160205

161206
}
207+
162208
// Set tx as potential parent to be used for future GetChildren() calls.
163209
if (!ptx_potential_parent || fuzzed_data_provider.ConsumeBool()) {
164210
ptx_potential_parent = tx;
@@ -168,4 +214,5 @@ FUZZ_TARGET(txorphan, .init = initialize_orphanage)
168214
const bool get_tx_nonnull{orphanage.GetTx(tx->GetWitnessHash()) != nullptr};
169215
Assert(have_tx == get_tx_nonnull);
170216
}
217+
orphanage.SanityCheck();
171218
}

src/txorphanage.cpp

Lines changed: 78 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ bool TxOrphanage::AddTx(const CTransactionRef& tx, NodeId peer)
4242
for (const CTxIn& txin : tx->vin) {
4343
m_outpoint_to_orphan_it[txin.prevout].insert(ret.first);
4444
}
45+
m_total_orphan_usage += sz;
46+
m_total_announcements += 1;
47+
auto& peer_info = m_peer_orphanage_info.try_emplace(peer).first->second;
48+
peer_info.m_total_usage += sz;
4549

4650
LogDebug(BCLog::TXPACKAGES, "stored orphan tx %s (wtxid=%s), weight: %u (mapsz %u outsz %u)\n", hash.ToString(), wtxid.ToString(), sz,
4751
m_orphans.size(), m_outpoint_to_orphan_it.size());
@@ -55,6 +59,9 @@ bool TxOrphanage::AddAnnouncer(const Wtxid& wtxid, NodeId peer)
5559
Assume(!it->second.announcers.empty());
5660
const auto ret = it->second.announcers.insert(peer);
5761
if (ret.second) {
62+
auto& peer_info = m_peer_orphanage_info.try_emplace(peer).first->second;
63+
peer_info.m_total_usage += it->second.GetUsage();
64+
m_total_announcements += 1;
5865
LogDebug(BCLog::TXPACKAGES, "added peer=%d as announcer of orphan tx %s\n", peer, wtxid.ToString());
5966
return true;
6067
}
@@ -77,6 +84,17 @@ int TxOrphanage::EraseTx(const Wtxid& wtxid)
7784
m_outpoint_to_orphan_it.erase(itPrev);
7885
}
7986

87+
const auto tx_size{it->second.GetUsage()};
88+
m_total_orphan_usage -= tx_size;
89+
m_total_announcements -= it->second.announcers.size();
90+
// Decrement each announcer's m_total_usage
91+
for (const auto& peer : it->second.announcers) {
92+
auto peer_it = m_peer_orphanage_info.find(peer);
93+
if (Assume(peer_it != m_peer_orphanage_info.end())) {
94+
peer_it->second.m_total_usage -= tx_size;
95+
}
96+
}
97+
8098
size_t old_pos = it->second.list_pos;
8199
assert(m_orphan_list[old_pos] == it);
82100
if (old_pos + 1 != m_orphan_list.size()) {
@@ -99,7 +117,8 @@ int TxOrphanage::EraseTx(const Wtxid& wtxid)
99117

100118
void TxOrphanage::EraseForPeer(NodeId peer)
101119
{
102-
m_peer_work_set.erase(peer);
120+
// Zeroes out this peer's m_total_usage.
121+
m_peer_orphanage_info.erase(peer);
103122

104123
int nErased = 0;
105124
std::map<Wtxid, OrphanTx>::iterator iter = m_orphans.begin();
@@ -110,6 +129,7 @@ void TxOrphanage::EraseForPeer(NodeId peer)
110129
auto orphan_it = orphan.announcers.find(peer);
111130
if (orphan_it != orphan.announcers.end()) {
112131
orphan.announcers.erase(peer);
132+
m_total_announcements -= 1;
113133

114134
// No remaining announcers: clean up entry
115135
if (orphan.announcers.empty()) {
@@ -170,7 +190,7 @@ void TxOrphanage::AddChildrenToWorkSet(const CTransaction& tx, FastRandomContext
170190

171191
// Get this source peer's work set, emplacing an empty set if it didn't exist
172192
// (note: if this peer wasn't still connected, we would have removed the orphan tx already)
173-
std::set<Wtxid>& orphan_work_set = m_peer_work_set.try_emplace(announcer).first->second;
193+
std::set<Wtxid>& orphan_work_set = m_peer_orphanage_info.try_emplace(announcer).first->second.m_work_set;
174194
// Add this tx to the work set
175195
orphan_work_set.insert(elem->first);
176196
LogDebug(BCLog::TXPACKAGES, "added %s (wtxid=%s) to peer %d workset\n",
@@ -199,30 +219,29 @@ bool TxOrphanage::HaveTxFromPeer(const Wtxid& wtxid, NodeId peer) const
199219

200220
CTransactionRef TxOrphanage::GetTxToReconsider(NodeId peer)
201221
{
202-
auto work_set_it = m_peer_work_set.find(peer);
203-
if (work_set_it != m_peer_work_set.end()) {
204-
auto& work_set = work_set_it->second;
205-
while (!work_set.empty()) {
206-
Wtxid wtxid = *work_set.begin();
207-
work_set.erase(work_set.begin());
208-
209-
const auto orphan_it = m_orphans.find(wtxid);
210-
if (orphan_it != m_orphans.end()) {
211-
return orphan_it->second.tx;
212-
}
222+
auto peer_it = m_peer_orphanage_info.find(peer);
223+
if (peer_it == m_peer_orphanage_info.end()) return nullptr;
224+
225+
auto& work_set = peer_it->second.m_work_set;
226+
while (!work_set.empty()) {
227+
Wtxid wtxid = *work_set.begin();
228+
work_set.erase(work_set.begin());
229+
230+
const auto orphan_it = m_orphans.find(wtxid);
231+
if (orphan_it != m_orphans.end()) {
232+
return orphan_it->second.tx;
213233
}
214234
}
215235
return nullptr;
216236
}
217237

218238
bool TxOrphanage::HaveTxToReconsider(NodeId peer)
219239
{
220-
auto work_set_it = m_peer_work_set.find(peer);
221-
if (work_set_it != m_peer_work_set.end()) {
222-
auto& work_set = work_set_it->second;
223-
return !work_set.empty();
224-
}
225-
return false;
240+
auto peer_it = m_peer_orphanage_info.find(peer);
241+
if (peer_it == m_peer_orphanage_info.end()) return false;
242+
243+
auto& work_set = peer_it->second.m_work_set;
244+
return !work_set.empty();
226245
}
227246

228247
void TxOrphanage::EraseForBlock(const CBlock& block)
@@ -302,3 +321,43 @@ std::vector<TxOrphanage::OrphanTxBase> TxOrphanage::GetOrphanTransactions() cons
302321
}
303322
return ret;
304323
}
324+
325+
void TxOrphanage::SanityCheck() const
326+
{
327+
// Check that cached m_total_announcements is correct
328+
unsigned int counted_total_announcements{0};
329+
// Check that m_total_orphan_usage is correct
330+
unsigned int counted_total_usage{0};
331+
332+
// Check that cached PeerOrphanInfo::m_total_size is correct
333+
std::map<NodeId, unsigned int> counted_size_per_peer;
334+
335+
for (const auto& [wtxid, orphan] : m_orphans) {
336+
counted_total_announcements += orphan.announcers.size();
337+
counted_total_usage += orphan.GetUsage();
338+
339+
Assume(!orphan.announcers.empty());
340+
for (const auto& peer : orphan.announcers) {
341+
auto& count_peer_entry = counted_size_per_peer.try_emplace(peer).first->second;
342+
count_peer_entry += orphan.GetUsage();
343+
}
344+
}
345+
346+
Assume(m_total_announcements >= m_orphans.size());
347+
Assume(counted_total_announcements == m_total_announcements);
348+
Assume(counted_total_usage == m_total_orphan_usage);
349+
350+
// There must be an entry in m_peer_orphanage_info for each peer
351+
// However, there may be m_peer_orphanage_info entries corresponding to peers for whom we
352+
// previously had orphans but no longer do.
353+
Assume(counted_size_per_peer.size() <= m_peer_orphanage_info.size());
354+
355+
for (const auto& [peerid, info] : m_peer_orphanage_info) {
356+
auto it_counted = counted_size_per_peer.find(peerid);
357+
if (it_counted == counted_size_per_peer.end()) {
358+
Assume(info.m_total_usage == 0);
359+
} else {
360+
Assume(it_counted->second == info.m_total_usage);
361+
}
362+
}
363+
}

src/txorphanage.h

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#ifndef BITCOIN_TXORPHANAGE_H
66
#define BITCOIN_TXORPHANAGE_H
77

8+
#include <consensus/validation.h>
89
#include <net.h>
910
#include <primitives/block.h>
1011
#include <primitives/transaction.h>
@@ -83,21 +84,62 @@ class TxOrphanage {
8384
/** Peers added with AddTx or AddAnnouncer. */
8485
std::set<NodeId> announcers;
8586
NodeSeconds nTimeExpire;
87+
88+
/** Get the weight of this transaction, an approximation of its memory usage. */
89+
unsigned int GetUsage() const {
90+
return GetTransactionWeight(*tx);
91+
}
8692
};
8793

8894
std::vector<OrphanTxBase> GetOrphanTransactions() const;
8995

96+
/** Get the total usage (weight) of all orphans. If an orphan has multiple announcers, its usage is
97+
* only counted once within this total. */
98+
unsigned int TotalOrphanUsage() const { return m_total_orphan_usage; }
99+
100+
/** Total usage (weight) of orphans for which this peer is an announcer. If an orphan has multiple
101+
* announcers, its weight will be accounted for in each PeerOrphanInfo, so the total of all
102+
* peers' UsageByPeer() may be larger than TotalOrphanBytes(). */
103+
unsigned int UsageByPeer(NodeId peer) const {
104+
auto peer_it = m_peer_orphanage_info.find(peer);
105+
return peer_it == m_peer_orphanage_info.end() ? 0 : peer_it->second.m_total_usage;
106+
}
107+
108+
/** Check consistency between PeerOrphanInfo and m_orphans. Recalculate counters and ensure they
109+
* match what is cached. */
110+
void SanityCheck() const;
111+
90112
protected:
91113
struct OrphanTx : public OrphanTxBase {
92114
size_t list_pos;
93115
};
94116

117+
/** Total usage (weight) of all entries in m_orphans. */
118+
unsigned int m_total_orphan_usage{0};
119+
120+
/** Total number of <peer, tx> pairs. Can be larger than m_orphans.size() because multiple peers
121+
* may have announced the same orphan. */
122+
unsigned int m_total_announcements{0};
123+
95124
/** Map from wtxid to orphan transaction record. Limited by
96125
* -maxorphantx/DEFAULT_MAX_ORPHAN_TRANSACTIONS */
97126
std::map<Wtxid, OrphanTx> m_orphans;
98127

99-
/** Which peer provided the orphans that need to be reconsidered */
100-
std::map<NodeId, std::set<Wtxid>> m_peer_work_set;
128+
struct PeerOrphanInfo {
129+
/** List of transactions that should be reconsidered: added to in AddChildrenToWorkSet,
130+
* removed from one-by-one with each call to GetTxToReconsider. The wtxids may refer to
131+
* transactions that are no longer present in orphanage; these are lazily removed in
132+
* GetTxToReconsider. */
133+
std::set<Wtxid> m_work_set;
134+
135+
/** Total weight of orphans for which this peer is an announcer.
136+
* If orphans are provided by different peers, its weight will be accounted for in each
137+
* PeerOrphanInfo, so the total of all peers' m_total_usage may be larger than
138+
* m_total_orphan_size. If a peer is removed as an announcer, even if the orphan still
139+
* remains in the orphanage, this number will be decremented. */
140+
unsigned int m_total_usage{0};
141+
};
142+
std::map<NodeId, PeerOrphanInfo> m_peer_orphanage_info;
101143

102144
using OrphanMap = decltype(m_orphans);
103145

0 commit comments

Comments
 (0)