[txorphanage] support multiple announcers

Add ability to add and track multiple announcers per orphan transaction,
erasing announcers but not the entire orphan.

The tx creation code in orphanage_tests needs to be updated so that each
tx is unique, because the CountOrphans() check assumes that calling
EraseForPeer necessarily means its orphans are deleted.

Unused for now.
This commit is contained in:
glozow 2023-04-18 16:02:58 +01:00
parent 62a9ff1870
commit e810842acd
4 changed files with 68 additions and 23 deletions

View file

@ -843,7 +843,9 @@ static UniValue OrphanToJSON(const TxOrphanage::OrphanTxBase& orphan)
o.pushKV("entry", int64_t{TicksSinceEpoch<std::chrono::seconds>(orphan.nTimeExpire - ORPHAN_TX_EXPIRE_TIME)}); o.pushKV("entry", int64_t{TicksSinceEpoch<std::chrono::seconds>(orphan.nTimeExpire - ORPHAN_TX_EXPIRE_TIME)});
o.pushKV("expiration", int64_t{TicksSinceEpoch<std::chrono::seconds>(orphan.nTimeExpire)}); o.pushKV("expiration", int64_t{TicksSinceEpoch<std::chrono::seconds>(orphan.nTimeExpire)});
UniValue from(UniValue::VARR); UniValue from(UniValue::VARR);
from.push_back(orphan.fromPeer); // only one fromPeer for now for (const auto fromPeer: orphan.announcers) {
from.push_back(fromPeer);
}
o.pushKV("from", from); o.pushKV("from", from);
return o; return o;
} }

View file

@ -132,7 +132,7 @@ BOOST_AUTO_TEST_CASE(DoS_mapOrphans)
tx.vin[0].prevout.hash = Txid::FromUint256(m_rng.rand256()); tx.vin[0].prevout.hash = Txid::FromUint256(m_rng.rand256());
tx.vin[0].scriptSig << OP_1; tx.vin[0].scriptSig << OP_1;
tx.vout.resize(1); tx.vout.resize(1);
tx.vout[0].nValue = 1*CENT; tx.vout[0].nValue = i*CENT;
tx.vout[0].scriptPubKey = GetScriptForDestination(PKHash(key.GetPubKey())); tx.vout[0].scriptPubKey = GetScriptForDestination(PKHash(key.GetPubKey()));
orphanage.AddTx(MakeTransactionRef(tx), i); orphanage.AddTx(MakeTransactionRef(tx), i);
@ -148,7 +148,7 @@ BOOST_AUTO_TEST_CASE(DoS_mapOrphans)
tx.vin[0].prevout.n = 0; tx.vin[0].prevout.n = 0;
tx.vin[0].prevout.hash = txPrev->GetHash(); tx.vin[0].prevout.hash = txPrev->GetHash();
tx.vout.resize(1); tx.vout.resize(1);
tx.vout[0].nValue = 1*CENT; tx.vout[0].nValue = i*CENT;
tx.vout[0].scriptPubKey = GetScriptForDestination(PKHash(key.GetPubKey())); tx.vout[0].scriptPubKey = GetScriptForDestination(PKHash(key.GetPubKey()));
SignatureData empty; SignatureData empty;
BOOST_CHECK(SignSignature(keystore, *txPrev, tx, 0, SIGHASH_ALL, empty)); BOOST_CHECK(SignSignature(keystore, *txPrev, tx, 0, SIGHASH_ALL, empty));

View file

@ -16,8 +16,11 @@ bool TxOrphanage::AddTx(const CTransactionRef& tx, NodeId peer)
{ {
const Txid& hash = tx->GetHash(); const Txid& hash = tx->GetHash();
const Wtxid& wtxid = tx->GetWitnessHash(); const Wtxid& wtxid = tx->GetWitnessHash();
if (m_orphans.count(wtxid)) if (auto it{m_orphans.find(wtxid)}; it != m_orphans.end()) {
AddAnnouncer(wtxid, peer);
// No new orphan entry was created. An announcer may have been added.
return false; return false;
}
// Ignore big transactions, to avoid a // Ignore big transactions, to avoid a
// send-big-orphans memory exhaustion attack. If a peer has a legitimate // send-big-orphans memory exhaustion attack. If a peer has a legitimate
@ -33,7 +36,7 @@ bool TxOrphanage::AddTx(const CTransactionRef& tx, NodeId peer)
return false; return false;
} }
auto ret = m_orphans.emplace(wtxid, OrphanTx{{tx, peer, Now<NodeSeconds>() + ORPHAN_TX_EXPIRE_TIME}, m_orphan_list.size()}); auto ret = m_orphans.emplace(wtxid, OrphanTx{{tx, {peer}, Now<NodeSeconds>() + ORPHAN_TX_EXPIRE_TIME}, m_orphan_list.size()});
assert(ret.second); assert(ret.second);
m_orphan_list.push_back(ret.first); m_orphan_list.push_back(ret.first);
for (const CTxIn& txin : tx->vin) { for (const CTxIn& txin : tx->vin) {
@ -45,6 +48,20 @@ bool TxOrphanage::AddTx(const CTransactionRef& tx, NodeId peer)
return true; return true;
} }
bool TxOrphanage::AddAnnouncer(const Wtxid& wtxid, NodeId peer)
{
const auto it = m_orphans.find(wtxid);
if (it != m_orphans.end()) {
Assume(!it->second.announcers.empty());
const auto ret = it->second.announcers.insert(peer);
if (ret.second) {
LogDebug(BCLog::TXPACKAGES, "added peer=%d as announcer of orphan tx %s\n", peer, wtxid.ToString());
return true;
}
}
return false;
}
int TxOrphanage::EraseTx(const Wtxid& wtxid) int TxOrphanage::EraseTx(const Wtxid& wtxid)
{ {
std::map<Wtxid, OrphanTx>::iterator it = m_orphans.find(wtxid); std::map<Wtxid, OrphanTx>::iterator it = m_orphans.find(wtxid);
@ -89,9 +106,15 @@ void TxOrphanage::EraseForPeer(NodeId peer)
while (iter != m_orphans.end()) while (iter != m_orphans.end())
{ {
// increment to avoid iterator becoming invalid after erasure // increment to avoid iterator becoming invalid after erasure
const auto& [wtxid, orphan] = *iter++; auto& [wtxid, orphan] = *iter++;
if (orphan.fromPeer == peer) { auto orphan_it = orphan.announcers.find(peer);
nErased += EraseTx(wtxid); if (orphan_it != orphan.announcers.end()) {
orphan.announcers.erase(peer);
// No remaining annnouncers: clean up entry
if (orphan.announcers.empty()) {
nErased += EraseTx(orphan.tx->GetWitnessHash());
}
} }
} }
if (nErased > 0) LogDebug(BCLog::TXPACKAGES, "Erased %d orphan transaction(s) from peer=%d\n", nErased, peer); if (nErased > 0) LogDebug(BCLog::TXPACKAGES, "Erased %d orphan transaction(s) from peer=%d\n", nErased, peer);
@ -110,7 +133,7 @@ void TxOrphanage::LimitOrphans(unsigned int max_orphans, FastRandomContext& rng)
{ {
std::map<Wtxid, OrphanTx>::iterator maybeErase = iter++; std::map<Wtxid, OrphanTx>::iterator maybeErase = iter++;
if (maybeErase->second.nTimeExpire <= nNow) { if (maybeErase->second.nTimeExpire <= nNow) {
nErased += EraseTx(maybeErase->second.tx->GetWitnessHash()); nErased += EraseTx(maybeErase->first);
} else { } else {
nMinExpTime = std::min(maybeErase->second.nTimeExpire, nMinExpTime); nMinExpTime = std::min(maybeErase->second.nTimeExpire, nMinExpTime);
} }
@ -123,7 +146,7 @@ void TxOrphanage::LimitOrphans(unsigned int max_orphans, FastRandomContext& rng)
{ {
// Evict a random orphan: // Evict a random orphan:
size_t randompos = rng.randrange(m_orphan_list.size()); size_t randompos = rng.randrange(m_orphan_list.size());
EraseTx(m_orphan_list[randompos]->second.tx->GetWitnessHash()); EraseTx(m_orphan_list[randompos]->first);
++nEvicted; ++nEvicted;
} }
if (nEvicted > 0) LogDebug(BCLog::TXPACKAGES, "orphanage overflow, removed %u tx\n", nEvicted); if (nEvicted > 0) LogDebug(BCLog::TXPACKAGES, "orphanage overflow, removed %u tx\n", nEvicted);
@ -135,13 +158,17 @@ void TxOrphanage::AddChildrenToWorkSet(const CTransaction& tx)
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& elem : it_by_prev->second) { for (const auto& elem : it_by_prev->second) {
// Get this source peer's work set, emplacing an empty set if it didn't exist // Belt and suspenders, each orphan should always have at least 1 announcer.
// (note: if this peer wasn't still connected, we would have removed the orphan tx already) if (!Assume(!elem->second.announcers.empty())) continue;
std::set<Wtxid>& orphan_work_set = m_peer_work_set.try_emplace(elem->second.fromPeer).first->second; for (const auto announcer: elem->second.announcers) {
// Add this tx to the work set // Get this source peer's work set, emplacing an empty set if it didn't exist
orphan_work_set.insert(elem->first); // (note: if this peer wasn't still connected, we would have removed the orphan tx already)
LogDebug(BCLog::TXPACKAGES, "added %s (wtxid=%s) to peer %d workset\n", std::set<Wtxid>& orphan_work_set = m_peer_work_set.try_emplace(announcer).first->second;
tx.GetHash().ToString(), tx.GetWitnessHash().ToString(), elem->second.fromPeer); // Add this tx to the work set
orphan_work_set.insert(elem->first);
LogDebug(BCLog::TXPACKAGES, "added %s (wtxid=%s) to peer %d workset\n",
tx.GetHash().ToString(), tx.GetWitnessHash().ToString(), announcer);
}
} }
} }
} }
@ -152,6 +179,12 @@ bool TxOrphanage::HaveTx(const Wtxid& wtxid) const
return m_orphans.count(wtxid); return m_orphans.count(wtxid);
} }
bool TxOrphanage::HaveTxFromPeer(const Wtxid& wtxid, NodeId peer) const
{
auto it = m_orphans.find(wtxid);
return (it != m_orphans.end() && it->second.announcers.contains(peer));
}
CTransactionRef TxOrphanage::GetTxToReconsider(NodeId peer) CTransactionRef TxOrphanage::GetTxToReconsider(NodeId peer)
{ {
auto work_set_it = m_peer_work_set.find(peer); auto work_set_it = m_peer_work_set.find(peer);
@ -219,7 +252,7 @@ std::vector<CTransactionRef> TxOrphanage::GetChildrenFromSamePeer(const CTransac
const auto it_by_prev = m_outpoint_to_orphan_it.find(COutPoint(parent->GetHash(), i)); const auto it_by_prev = m_outpoint_to_orphan_it.find(COutPoint(parent->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& elem : it_by_prev->second) { for (const auto& elem : it_by_prev->second) {
if (elem->second.fromPeer == nodeid) { if (elem->second.announcers.contains(nodeid)) {
iters.emplace_back(elem); iters.emplace_back(elem);
} }
} }
@ -258,7 +291,7 @@ std::vector<std::pair<CTransactionRef, NodeId>> TxOrphanage::GetChildrenFromDiff
const auto it_by_prev = m_outpoint_to_orphan_it.find(COutPoint(parent->GetHash(), i)); const auto it_by_prev = m_outpoint_to_orphan_it.find(COutPoint(parent->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& elem : it_by_prev->second) { for (const auto& elem : it_by_prev->second) {
if (elem->second.fromPeer != nodeid) { if (!elem->second.announcers.contains(nodeid)) {
iters.emplace_back(elem); iters.emplace_back(elem);
} }
} }
@ -273,7 +306,9 @@ std::vector<std::pair<CTransactionRef, NodeId>> TxOrphanage::GetChildrenFromDiff
std::vector<std::pair<CTransactionRef, NodeId>> children_found; std::vector<std::pair<CTransactionRef, NodeId>> children_found;
children_found.reserve(iters.size()); children_found.reserve(iters.size());
for (const auto& child_iter : iters) { for (const auto& child_iter : iters) {
children_found.emplace_back(child_iter->second.tx, child_iter->second.fromPeer); // Use first peer in announcers list
auto peer = *(child_iter->second.announcers.begin());
children_found.emplace_back(child_iter->second.tx, peer);
} }
return children_found; return children_found;
} }
@ -283,7 +318,7 @@ std::vector<TxOrphanage::OrphanTxBase> TxOrphanage::GetOrphanTransactions() cons
std::vector<OrphanTxBase> ret; std::vector<OrphanTxBase> ret;
ret.reserve(m_orphans.size()); ret.reserve(m_orphans.size());
for (auto const& o : m_orphans) { for (auto const& o : m_orphans) {
ret.push_back({o.second.tx, o.second.fromPeer, o.second.nTimeExpire}); ret.push_back({o.second.tx, o.second.announcers, o.second.nTimeExpire});
} }
return ret; return ret;
} }

View file

@ -30,9 +30,15 @@ public:
/** Add a new orphan transaction */ /** Add a new orphan transaction */
bool AddTx(const CTransactionRef& tx, NodeId peer); bool AddTx(const CTransactionRef& tx, NodeId peer);
/** Add an additional announcer to an orphan if it exists. Otherwise, do nothing. */
bool AddAnnouncer(const Wtxid& wtxid, NodeId peer);
/** Check if we already have an orphan transaction (by wtxid only) */ /** Check if we already have an orphan transaction (by wtxid only) */
bool HaveTx(const Wtxid& wtxid) const; bool HaveTx(const Wtxid& wtxid) const;
/** Check if a {tx, peer} exists in the orphanage.*/
bool HaveTxFromPeer(const Wtxid& wtxid, NodeId peer) const;
/** Extract a transaction from a peer's work set /** Extract a transaction from a peer's work set
* Returns nullptr if there are no transactions to work on. * Returns nullptr if there are no transactions to work on.
* Otherwise returns the transaction reference, and removes * Otherwise returns the transaction reference, and removes
@ -43,7 +49,8 @@ public:
/** Erase an orphan by wtxid */ /** Erase an orphan by wtxid */
int EraseTx(const Wtxid& wtxid); int EraseTx(const Wtxid& wtxid);
/** Erase all orphans announced by a peer (eg, after that peer disconnects) */ /** Maybe erase all orphans announced by a peer (eg, after that peer disconnects). If an orphan
* has been announced by another peer, don't erase, just remove this peer from the list of announcers. */
void EraseForPeer(NodeId peer); void EraseForPeer(NodeId peer);
/** Erase all orphans included in or invalidated by a new block */ /** Erase all orphans included in or invalidated by a new block */
@ -75,7 +82,8 @@ public:
/** Allows providing orphan information externally */ /** Allows providing orphan information externally */
struct OrphanTxBase { struct OrphanTxBase {
CTransactionRef tx; CTransactionRef tx;
NodeId fromPeer; /** Peers added with AddTx or AddAnnouncer. */
std::set<NodeId> announcers;
NodeSeconds nTimeExpire; NodeSeconds nTimeExpire;
}; };