Skip to content

Commit 94ab58b

Browse files
committed
Merge #8179: Evict orphans which are included or precluded by accepted blocks.
54326a6 Increase maximum orphan size to 100,000 bytes. (Gregory Maxwell) 8c99d1b Treat orphans as implicit inv for parents, discard when parents rejected. (Gregory Maxwell) 11cc143 Adds an expiration time for orphan tx. (Gregory Maxwell) db0ffe8 This eliminates the primary leak that causes the orphan map to always grow to its maximum size. (Gregory Maxwell) 1b0bcc5 Track orphan by prev COutPoint rather than prev hash (Pieter Wuille)
2 parents f6598df + 54326a6 commit 94ab58b

File tree

3 files changed

+108
-32
lines changed

3 files changed

+108
-32
lines changed

src/main.cpp

Lines changed: 103 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -88,12 +88,22 @@ CAmount maxTxFee = DEFAULT_TRANSACTION_MAXFEE;
8888
CTxMemPool mempool(::minRelayTxFee);
8989
FeeFilterRounder filterRounder(::minRelayTxFee);
9090

91+
struct IteratorComparator
92+
{
93+
template<typename I>
94+
bool operator()(const I& a, const I& b)
95+
{
96+
return &(*a) < &(*b);
97+
}
98+
};
99+
91100
struct COrphanTx {
92101
CTransaction tx;
93102
NodeId fromPeer;
103+
int64_t nTimeExpire;
94104
};
95105
map<uint256, COrphanTx> mapOrphanTransactions GUARDED_BY(cs_main);
96-
map<uint256, set<uint256> > mapOrphanTransactionsByPrev GUARDED_BY(cs_main);
106+
map<COutPoint, set<map<uint256, COrphanTx>::iterator, IteratorComparator>> mapOrphanTransactionsByPrev GUARDED_BY(cs_main);
97107
void EraseOrphansFor(NodeId peer) EXCLUSIVE_LOCKS_REQUIRED(cs_main);
98108

99109
/**
@@ -623,40 +633,42 @@ bool AddOrphanTx(const CTransaction& tx, NodeId peer) EXCLUSIVE_LOCKS_REQUIRED(c
623633
// large transaction with a missing parent then we assume
624634
// it will rebroadcast it later, after the parent transaction(s)
625635
// have been mined or received.
626-
// 10,000 orphans, each of which is at most 5,000 bytes big is
627-
// at most 500 megabytes of orphans:
636+
// 100 orphans, each of which is at most 99,999 bytes big is
637+
// at most 10 megabytes of orphans and somewhat more byprev index (in the worst case):
628638
unsigned int sz = tx.GetSerializeSize(SER_NETWORK, CTransaction::CURRENT_VERSION);
629-
if (sz > 5000)
639+
if (sz >= MAX_STANDARD_TX_SIZE)
630640
{
631641
LogPrint("mempool", "ignoring large orphan tx (size: %u, hash: %s)\n", sz, hash.ToString());
632642
return false;
633643
}
634644

635-
mapOrphanTransactions[hash].tx = tx;
636-
mapOrphanTransactions[hash].fromPeer = peer;
637-
BOOST_FOREACH(const CTxIn& txin, tx.vin)
638-
mapOrphanTransactionsByPrev[txin.prevout.hash].insert(hash);
645+
auto ret = mapOrphanTransactions.emplace(hash, COrphanTx{tx, peer, GetTime() + ORPHAN_TX_EXPIRE_TIME});
646+
assert(ret.second);
647+
BOOST_FOREACH(const CTxIn& txin, tx.vin) {
648+
mapOrphanTransactionsByPrev[txin.prevout].insert(ret.first);
649+
}
639650

640-
LogPrint("mempool", "stored orphan tx %s (mapsz %u prevsz %u)\n", hash.ToString(),
651+
LogPrint("mempool", "stored orphan tx %s (mapsz %u outsz %u)\n", hash.ToString(),
641652
mapOrphanTransactions.size(), mapOrphanTransactionsByPrev.size());
642653
return true;
643654
}
644655

645-
void static EraseOrphanTx(uint256 hash) EXCLUSIVE_LOCKS_REQUIRED(cs_main)
656+
int static EraseOrphanTx(uint256 hash) EXCLUSIVE_LOCKS_REQUIRED(cs_main)
646657
{
647658
map<uint256, COrphanTx>::iterator it = mapOrphanTransactions.find(hash);
648659
if (it == mapOrphanTransactions.end())
649-
return;
660+
return 0;
650661
BOOST_FOREACH(const CTxIn& txin, it->second.tx.vin)
651662
{
652-
map<uint256, set<uint256> >::iterator itPrev = mapOrphanTransactionsByPrev.find(txin.prevout.hash);
663+
auto itPrev = mapOrphanTransactionsByPrev.find(txin.prevout);
653664
if (itPrev == mapOrphanTransactionsByPrev.end())
654665
continue;
655-
itPrev->second.erase(hash);
666+
itPrev->second.erase(it);
656667
if (itPrev->second.empty())
657668
mapOrphanTransactionsByPrev.erase(itPrev);
658669
}
659670
mapOrphanTransactions.erase(it);
671+
return 1;
660672
}
661673

662674
void EraseOrphansFor(NodeId peer)
@@ -668,8 +680,7 @@ void EraseOrphansFor(NodeId peer)
668680
map<uint256, COrphanTx>::iterator maybeErase = iter++; // increment to avoid iterator becoming invalid
669681
if (maybeErase->second.fromPeer == peer)
670682
{
671-
EraseOrphanTx(maybeErase->second.tx.GetHash());
672-
++nErased;
683+
nErased += EraseOrphanTx(maybeErase->second.tx.GetHash());
673684
}
674685
}
675686
if (nErased > 0) LogPrint("mempool", "Erased %d orphan tx from peer %d\n", nErased, peer);
@@ -679,6 +690,26 @@ void EraseOrphansFor(NodeId peer)
679690
unsigned int LimitOrphanTxSize(unsigned int nMaxOrphans) EXCLUSIVE_LOCKS_REQUIRED(cs_main)
680691
{
681692
unsigned int nEvicted = 0;
693+
static int64_t nNextSweep;
694+
int64_t nNow = GetTime();
695+
if (nNextSweep <= nNow) {
696+
// Sweep out expired orphan pool entries:
697+
int nErased = 0;
698+
int64_t nMinExpTime = nNow + ORPHAN_TX_EXPIRE_TIME - ORPHAN_TX_EXPIRE_INTERVAL;
699+
map<uint256, COrphanTx>::iterator iter = mapOrphanTransactions.begin();
700+
while (iter != mapOrphanTransactions.end())
701+
{
702+
map<uint256, COrphanTx>::iterator maybeErase = iter++;
703+
if (maybeErase->second.nTimeExpire <= nNow) {
704+
nErased += EraseOrphanTx(maybeErase->second.tx.GetHash());
705+
} else {
706+
nMinExpTime = std::min(maybeErase->second.nTimeExpire, nMinExpTime);
707+
}
708+
}
709+
// Sweep again 5 minutes after the next entry that expires in order to batch the linear scan.
710+
nNextSweep = nMinExpTime + ORPHAN_TX_EXPIRE_INTERVAL;
711+
if (nErased > 0) LogPrint("mempool", "Erased %d orphan tx due to expiration\n", nErased);
712+
}
682713
while (mapOrphanTransactions.size() > nMaxOrphans)
683714
{
684715
// Evict a random orphan:
@@ -2335,6 +2366,7 @@ bool ConnectBlock(const CBlock& block, CValidationState& state, CBlockIndex* pin
23352366

23362367
CCheckQueueControl<CScriptCheck> control(fScriptChecks && nScriptCheckThreads ? &scriptcheckqueue : NULL);
23372368

2369+
std::vector<uint256> vOrphanErase;
23382370
std::vector<int> prevheights;
23392371
CAmount nFees = 0;
23402372
int nInputs = 0;
@@ -2367,6 +2399,17 @@ bool ConnectBlock(const CBlock& block, CValidationState& state, CBlockIndex* pin
23672399
prevheights[j] = view.AccessCoins(tx.vin[j].prevout.hash)->nHeight;
23682400
}
23692401

2402+
// Which orphan pool entries must we evict?
2403+
for (size_t j = 0; j < tx.vin.size(); j++) {
2404+
auto itByPrev = mapOrphanTransactionsByPrev.find(tx.vin[j].prevout);
2405+
if (itByPrev == mapOrphanTransactionsByPrev.end()) continue;
2406+
for (auto mi = itByPrev->second.begin(); mi != itByPrev->second.end(); ++mi) {
2407+
const CTransaction& orphanTx = (*mi)->second.tx;
2408+
const uint256& orphanHash = orphanTx.GetHash();
2409+
vOrphanErase.push_back(orphanHash);
2410+
}
2411+
}
2412+
23702413
if (!SequenceLocks(tx, nLockTimeFlags, &prevheights, *pindex)) {
23712414
return state.DoS(100, error("%s: contains a non-BIP68-final transaction", __func__),
23722415
REJECT_INVALID, "bad-txns-nonfinal");
@@ -2454,6 +2497,15 @@ bool ConnectBlock(const CBlock& block, CValidationState& state, CBlockIndex* pin
24542497
GetMainSignals().UpdatedTransaction(hashPrevBestCoinBase);
24552498
hashPrevBestCoinBase = block.vtx[0].GetHash();
24562499

2500+
// Erase orphan transactions include or precluded by this block
2501+
if (vOrphanErase.size()) {
2502+
int nErased = 0;
2503+
BOOST_FOREACH(uint256 &orphanHash, vOrphanErase) {
2504+
nErased += EraseOrphanTx(orphanHash);
2505+
}
2506+
LogPrint("mempool", "Erased %d orphan tx included or conflicted by block\n", nErased);
2507+
}
2508+
24572509
int64_t nTime6 = GetTimeMicros(); nTimeCallbacks += nTime6 - nTime5;
24582510
LogPrint("bench", " - Callbacks: %.2fms [%.2fs]\n", 0.001 * (nTime6 - nTime5), nTimeCallbacks * 0.000001);
24592511

@@ -5041,7 +5093,7 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv,
50415093
return true;
50425094
}
50435095

5044-
vector<uint256> vWorkQueue;
5096+
deque<COutPoint> vWorkQueue;
50455097
vector<uint256> vEraseQueue;
50465098
CTransaction tx;
50475099
vRecv >> tx;
@@ -5060,7 +5112,9 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv,
50605112
if (!AlreadyHave(inv) && AcceptToMemoryPool(mempool, state, tx, true, &fMissingInputs)) {
50615113
mempool.check(pcoinsTip);
50625114
RelayTransaction(tx);
5063-
vWorkQueue.push_back(inv.hash);
5115+
for (unsigned int i = 0; i < tx.vout.size(); i++) {
5116+
vWorkQueue.emplace_back(inv.hash, i);
5117+
}
50645118

50655119
pfrom->nLastTXTime = GetTime();
50665120

@@ -5071,18 +5125,18 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv,
50715125

50725126
// Recursively process any orphan transactions that depended on this one
50735127
set<NodeId> setMisbehaving;
5074-
for (unsigned int i = 0; i < vWorkQueue.size(); i++)
5075-
{
5076-
map<uint256, set<uint256> >::iterator itByPrev = mapOrphanTransactionsByPrev.find(vWorkQueue[i]);
5128+
while (!vWorkQueue.empty()) {
5129+
auto itByPrev = mapOrphanTransactionsByPrev.find(vWorkQueue.front());
5130+
vWorkQueue.pop_front();
50775131
if (itByPrev == mapOrphanTransactionsByPrev.end())
50785132
continue;
5079-
for (set<uint256>::iterator mi = itByPrev->second.begin();
5133+
for (auto mi = itByPrev->second.begin();
50805134
mi != itByPrev->second.end();
50815135
++mi)
50825136
{
5083-
const uint256& orphanHash = *mi;
5084-
const CTransaction& orphanTx = mapOrphanTransactions[orphanHash].tx;
5085-
NodeId fromPeer = mapOrphanTransactions[orphanHash].fromPeer;
5137+
const CTransaction& orphanTx = (*mi)->second.tx;
5138+
const uint256& orphanHash = orphanTx.GetHash();
5139+
NodeId fromPeer = (*mi)->second.fromPeer;
50865140
bool fMissingInputs2 = false;
50875141
// Use a dummy CValidationState so someone can't setup nodes to counter-DoS based on orphan
50885142
// resolution (that is, feeding people an invalid transaction based on LegitTxX in order to get
@@ -5095,7 +5149,9 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv,
50955149
if (AcceptToMemoryPool(mempool, stateDummy, orphanTx, true, &fMissingInputs2)) {
50965150
LogPrint("mempool", " accepted orphan tx %s\n", orphanHash.ToString());
50975151
RelayTransaction(orphanTx);
5098-
vWorkQueue.push_back(orphanHash);
5152+
for (unsigned int i = 0; i < orphanTx.vout.size(); i++) {
5153+
vWorkQueue.emplace_back(orphanHash, i);
5154+
}
50995155
vEraseQueue.push_back(orphanHash);
51005156
}
51015157
else if (!fMissingInputs2)
@@ -5124,13 +5180,29 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv,
51245180
}
51255181
else if (fMissingInputs)
51265182
{
5127-
AddOrphanTx(tx, pfrom->GetId());
5183+
bool fRejectedParents = false; // It may be the case that the orphans parents have all been rejected
5184+
BOOST_FOREACH(const CTxIn& txin, tx.vin) {
5185+
if (recentRejects->contains(txin.prevout.hash)) {
5186+
fRejectedParents = true;
5187+
break;
5188+
}
5189+
}
5190+
if (!fRejectedParents) {
5191+
BOOST_FOREACH(const CTxIn& txin, tx.vin) {
5192+
CInv inv(MSG_TX, txin.prevout.hash);
5193+
pfrom->AddInventoryKnown(inv);
5194+
if (!AlreadyHave(inv)) pfrom->AskFor(inv);
5195+
}
5196+
AddOrphanTx(tx, pfrom->GetId());
51285197

5129-
// DoS prevention: do not allow mapOrphanTransactions to grow unbounded
5130-
unsigned int nMaxOrphanTx = (unsigned int)std::max((int64_t)0, GetArg("-maxorphantx", DEFAULT_MAX_ORPHAN_TRANSACTIONS));
5131-
unsigned int nEvicted = LimitOrphanTxSize(nMaxOrphanTx);
5132-
if (nEvicted > 0)
5133-
LogPrint("mempool", "mapOrphan overflow, removed %u tx\n", nEvicted);
5198+
// DoS prevention: do not allow mapOrphanTransactions to grow unbounded
5199+
unsigned int nMaxOrphanTx = (unsigned int)std::max((int64_t)0, GetArg("-maxorphantx", DEFAULT_MAX_ORPHAN_TRANSACTIONS));
5200+
unsigned int nEvicted = LimitOrphanTxSize(nMaxOrphanTx);
5201+
if (nEvicted > 0)
5202+
LogPrint("mempool", "mapOrphan overflow, removed %u tx\n", nEvicted);
5203+
} else {
5204+
LogPrint("mempool", "not keeping orphan with rejected parents %s\n",tx.GetHash().ToString());
5205+
}
51345206
} else {
51355207
assert(recentRejects);
51365208
recentRejects->insert(tx.GetHash());

src/main.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ static const CAmount HIGH_TX_FEE_PER_KB = 0.01 * COIN;
5656
static const CAmount HIGH_MAX_TX_FEE = 100 * HIGH_TX_FEE_PER_KB;
5757
/** Default for -maxorphantx, maximum number of orphan transactions kept in memory */
5858
static const unsigned int DEFAULT_MAX_ORPHAN_TRANSACTIONS = 100;
59+
/** Expiration time for orphan transactions in seconds */
60+
static const int64_t ORPHAN_TX_EXPIRE_TIME = 20 * 60;
61+
/** Minimum time between orphan transactions expire time checks in seconds */
62+
static const int64_t ORPHAN_TX_EXPIRE_INTERVAL = 5 * 60;
5963
/** Default for -limitancestorcount, max number of in-mempool ancestors */
6064
static const unsigned int DEFAULT_ANCESTOR_LIMIT = 25;
6165
/** Default for -limitancestorsize, maximum kilobytes of tx + all in-mempool ancestors */

src/test/DoS_tests.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ BOOST_AUTO_TEST_CASE(DoS_mapOrphans)
162162
tx.vout.resize(1);
163163
tx.vout[0].nValue = 1*CENT;
164164
tx.vout[0].scriptPubKey = GetScriptForDestination(key.GetPubKey().GetID());
165-
tx.vin.resize(500);
165+
tx.vin.resize(2777);
166166
for (unsigned int j = 0; j < tx.vin.size(); j++)
167167
{
168168
tx.vin[j].prevout.n = j;

0 commit comments

Comments
 (0)