Skip to content

Commit

Permalink
[txorphanage] track size of stored orphans, total and by peer
Browse files Browse the repository at this point in the history
No effect for now, just additional tracking. Enables:
- load balance orphan resolution, limit per-peer orphanage usage
- limit the total size of orphans instead of just the count
  • Loading branch information
glozow committed Jul 31, 2024
1 parent 43c9102 commit ec7b530
Show file tree
Hide file tree
Showing 3 changed files with 92 additions and 2 deletions.
27 changes: 25 additions & 2 deletions src/test/orphanage_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

#include <arith_uint256.h>
#include <primitives/transaction.h>
#include <consensus/validation.h>
#include <pubkey.h>
#include <script/sign.h>
#include <script/signingprovider.h>
Expand Down Expand Up @@ -112,6 +113,9 @@ BOOST_AUTO_TEST_CASE(DoS_mapOrphans)
FillableSigningProvider keystore;
BOOST_CHECK(keystore.AddKey(key));

size_t expected_count{0};
size_t expected_total_size{0};

// 50 orphan transactions:
for (int i = 0; i < 50; i++)
{
Expand All @@ -124,8 +128,14 @@ BOOST_AUTO_TEST_CASE(DoS_mapOrphans)
tx.vout[0].nValue = 1*CENT;
tx.vout[0].scriptPubKey = GetScriptForDestination(PKHash(key.GetPubKey()));

orphanage.AddTx(MakeTransactionRef(tx), i, {});
auto ptx{MakeTransactionRef(tx)};
if (orphanage.AddTx(ptx, i, {})) {
++expected_count;
expected_total_size += ptx->GetTotalSize();
}
}
BOOST_CHECK_EQUAL(orphanage.Size(), expected_count);
BOOST_CHECK_EQUAL(orphanage.TotalOrphanBytes(), expected_total_size);

// ... and 50 that depend on other orphans:
for (int i = 0; i < 50; i++)
Expand All @@ -142,8 +152,14 @@ BOOST_AUTO_TEST_CASE(DoS_mapOrphans)
SignatureData empty;
BOOST_CHECK(SignSignature(keystore, *txPrev, tx, 0, SIGHASH_ALL, empty));

orphanage.AddTx(MakeTransactionRef(tx), i, {});
auto ptx{MakeTransactionRef(tx)};
if (orphanage.AddTx(ptx, i, {})) {
++expected_count;
expected_total_size += ptx->GetTotalSize();
}
}
BOOST_CHECK_EQUAL(orphanage.Size(), expected_count);
BOOST_CHECK_EQUAL(orphanage.TotalOrphanBytes(), expected_total_size);

// This really-big orphan should be ignored:
for (int i = 0; i < 10; i++)
Expand All @@ -169,6 +185,8 @@ BOOST_AUTO_TEST_CASE(DoS_mapOrphans)

BOOST_CHECK(!orphanage.AddTx(MakeTransactionRef(tx), i, {}));
}
BOOST_CHECK_EQUAL(orphanage.Size(), expected_count);
BOOST_CHECK_EQUAL(orphanage.TotalOrphanBytes(), expected_total_size);

// Test EraseOrphansFor:
for (NodeId i = 0; i < 3; i++)
Expand All @@ -186,6 +204,11 @@ BOOST_AUTO_TEST_CASE(DoS_mapOrphans)
BOOST_CHECK(orphanage.CountOrphans() <= 10);
orphanage.LimitOrphans(0, rng);
BOOST_CHECK(orphanage.CountOrphans() == 0);

expected_count = 0;
expected_total_size = 0;
BOOST_CHECK_EQUAL(orphanage.CountOrphans(), expected_count);
BOOST_CHECK_EQUAL(orphanage.TotalOrphanBytes(), expected_total_size);
}

BOOST_AUTO_TEST_CASE(same_txid_diff_witness)
Expand Down
38 changes: 38 additions & 0 deletions src/txorphanage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,28 @@ static constexpr auto ORPHAN_TX_EXPIRE_TIME{20min};
/** Minimum time between orphan transactions expire time checks */
static constexpr auto ORPHAN_TX_EXPIRE_INTERVAL{5min};

void TxOrphanage::AddOrphanBytes(unsigned int size, NodeId peer)
{
m_peer_bytes_used.try_emplace(peer, 0);
m_peer_bytes_used.at(peer) += size;
}

void TxOrphanage::SubtractOrphanBytes(unsigned int size, NodeId peer)
{
// If our accounting is off, control damage by ensuring we clean up m_peer_bytes_used.
auto it = m_peer_bytes_used.find(peer);
if (!Assume(it != m_peer_bytes_used.end())) return;
if (!Assume(it->second >= size)) {
// Equivalent of bytes going to 0.
m_peer_bytes_used.erase(it);
return;
}

it->second -= size;
if (it->second == 0) {
m_peer_bytes_used.erase(it);
}
}

bool TxOrphanage::AddTx(const CTransactionRef& tx, NodeId peer, const std::vector<Txid>& parent_txids)
{
Expand All @@ -27,6 +49,7 @@ bool TxOrphanage::AddTx(const CTransactionRef& tx, NodeId peer, const std::vecto
Assume(!it->second.announcers.empty());
const auto ret = it->second.announcers.insert(peer);
if (ret.second) {
AddOrphanBytes(it->second.tx->GetTotalSize(), peer);
LogPrint(BCLog::TXPACKAGES, "added peer=%d as announcer of orphan tx %s\n", peer, wtxid.ToString());
}
// Even if an announcer was added, no new orphan entry was created.
Expand Down Expand Up @@ -56,6 +79,8 @@ bool TxOrphanage::AddTx(const CTransactionRef& tx, NodeId peer, const std::vecto

LogPrint(BCLog::TXPACKAGES, "stored orphan tx %s (wtxid=%s), weight: %u (mapsz %u outsz %u)\n", hash.ToString(), wtxid.ToString(), sz,
m_orphans.size(), m_outpoint_to_orphan_it.size());
AddOrphanBytes(tx->GetTotalSize(), peer);
m_total_orphan_bytes += tx->GetTotalSize();
return true;
}

Expand All @@ -66,6 +91,7 @@ bool TxOrphanage::AddAnnouncer(const Wtxid& wtxid, NodeId peer)
Assume(!it->second.announcers.empty());
const auto ret = it->second.announcers.insert(peer);
if (ret.second) {
AddOrphanBytes(it->second.tx->GetTotalSize(), peer);
LogPrint(BCLog::TXPACKAGES, "added peer=%d as announcer of orphan tx %s\n", peer, wtxid.ToString());
return true;
}
Expand All @@ -78,6 +104,12 @@ int TxOrphanage::EraseTx(const Wtxid& wtxid)
std::map<Wtxid, OrphanTx>::iterator it = m_orphans.find(wtxid);
if (it == m_orphans.end())
return 0;

m_total_orphan_bytes -= it->second.tx->GetTotalSize();
for (const auto fromPeer : it->second.announcers) {
SubtractOrphanBytes(it->second.tx->GetTotalSize(), fromPeer);
}

for (const CTxIn& txin : it->second.tx->vin)
{
auto itPrev = m_outpoint_to_orphan_it.find(txin.prevout);
Expand Down Expand Up @@ -129,10 +161,15 @@ void TxOrphanage::EraseForPeer(NodeId peer)
} else {
// Don't erase this orphan. Another peer has also announced it, so it may still be useful.
orphan.announcers.erase(peer);
SubtractOrphanBytes(orphan.tx->GetTotalSize(), peer);
}
}
}
if (nErased > 0) LogPrint(BCLog::TXPACKAGES, "Erased %d orphan transaction(s) from peer=%d\n", nErased, peer);

// Belt-and-suspenders if our accounting is off. We shouldn't keep an entry for a disconnected
// peer as we will have no other opportunity to delete it.
if (!Assume(m_peer_bytes_used.count(peer) == 0)) m_peer_bytes_used.erase(peer);
}

std::vector<Wtxid> TxOrphanage::LimitOrphans(unsigned int max_orphans, FastRandomContext& rng)
Expand Down Expand Up @@ -365,6 +402,7 @@ void TxOrphanage::EraseOrphanOfPeer(const Wtxid& wtxid, NodeId peer)
} else {
// Don't erase this orphan. Another peer has also announced it, so it may still be useful.
it->second.announcers.erase(peer);
SubtractOrphanBytes(it->second.tx->GetTotalSize(), peer);
}
}
}
Expand Down
29 changes: 29 additions & 0 deletions src/txorphanage.h
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,19 @@ class TxOrphanage {
/** Get an orphan's parent_txids, or std::nullopt if the orphan is not present. */
std::optional<std::vector<Txid>> GetParentTxids(const Wtxid& wtxid);

/** Return total memory usage of the transactions stored. Does not include overhead of
* m_orphans, m_peer_work_set, etc. */
unsigned int TotalOrphanBytes() const
{
return m_total_orphan_bytes;
}
/** Return total amount of orphans stored by this peer, in bytes. */
unsigned int BytesFromPeer(NodeId peer) const
{
auto peer_bytes_it = m_peer_bytes_used.find(peer);
return peer_bytes_it == m_peer_bytes_used.end() ? 0 : peer_bytes_it->second;
}

protected:
struct OrphanTx {
CTransactionRef tx;
Expand Down Expand Up @@ -141,8 +154,24 @@ class TxOrphanage {
/** Timestamp for the next scheduled sweep of expired orphans */
NodeSeconds m_next_sweep{0s};

/** Total bytes of all transactions. */
unsigned int m_total_orphan_bytes{0};

/** Total bytes of all protected orphans. */
size_t m_total_protected_orphan_bytes{0};

/** Map from nodeid to the amount of orphans provided by this peer, in bytes.
* The sum of all values in this map may exceed m_total_orphan_bytes, since multiple peers may
* provide the same orphan and its bytes are included in all peers' entries. */
std::map<NodeId, unsigned int> m_peer_bytes_used;

/** Add bytes to this peer's entry in m_peer_bytes_used, adding a new entry if it doesn't
* already exist. */
void AddOrphanBytes(unsigned int size, NodeId peer);

/** Subtract bytes from this peer's entry in m_peer_bytes_used, removing the peer's entry from
* the map if its value becomes 0. */
void SubtractOrphanBytes(unsigned int size, NodeId peer);
};

#endif // BITCOIN_TXORPHANAGE_H

0 comments on commit ec7b530

Please sign in to comment.