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)
This commit is contained in:
Wladimir J. van der Laan 2016-06-20 14:45:34 +02:00
commit 94ab58b5cc
No known key found for this signature in database
GPG key ID: 74810B012346C9A6
3 changed files with 108 additions and 32 deletions

View file

@ -88,12 +88,22 @@ CAmount maxTxFee = DEFAULT_TRANSACTION_MAXFEE;
CTxMemPool mempool(::minRelayTxFee); CTxMemPool mempool(::minRelayTxFee);
FeeFilterRounder filterRounder(::minRelayTxFee); FeeFilterRounder filterRounder(::minRelayTxFee);
struct IteratorComparator
{
template<typename I>
bool operator()(const I& a, const I& b)
{
return &(*a) < &(*b);
}
};
struct COrphanTx { struct COrphanTx {
CTransaction tx; CTransaction tx;
NodeId fromPeer; NodeId fromPeer;
int64_t nTimeExpire;
}; };
map<uint256, COrphanTx> mapOrphanTransactions GUARDED_BY(cs_main); map<uint256, COrphanTx> mapOrphanTransactions GUARDED_BY(cs_main);
map<uint256, set<uint256> > mapOrphanTransactionsByPrev GUARDED_BY(cs_main); map<COutPoint, set<map<uint256, COrphanTx>::iterator, IteratorComparator>> mapOrphanTransactionsByPrev GUARDED_BY(cs_main);
void EraseOrphansFor(NodeId peer) EXCLUSIVE_LOCKS_REQUIRED(cs_main); void EraseOrphansFor(NodeId peer) EXCLUSIVE_LOCKS_REQUIRED(cs_main);
/** /**
@ -623,40 +633,42 @@ bool AddOrphanTx(const CTransaction& tx, NodeId peer) EXCLUSIVE_LOCKS_REQUIRED(c
// large transaction with a missing parent then we assume // large transaction with a missing parent then we assume
// it will rebroadcast it later, after the parent transaction(s) // it will rebroadcast it later, after the parent transaction(s)
// have been mined or received. // have been mined or received.
// 10,000 orphans, each of which is at most 5,000 bytes big is // 100 orphans, each of which is at most 99,999 bytes big is
// at most 500 megabytes of orphans: // at most 10 megabytes of orphans and somewhat more byprev index (in the worst case):
unsigned int sz = tx.GetSerializeSize(SER_NETWORK, CTransaction::CURRENT_VERSION); unsigned int sz = tx.GetSerializeSize(SER_NETWORK, CTransaction::CURRENT_VERSION);
if (sz > 5000) if (sz >= MAX_STANDARD_TX_SIZE)
{ {
LogPrint("mempool", "ignoring large orphan tx (size: %u, hash: %s)\n", sz, hash.ToString()); LogPrint("mempool", "ignoring large orphan tx (size: %u, hash: %s)\n", sz, hash.ToString());
return false; return false;
} }
mapOrphanTransactions[hash].tx = tx; auto ret = mapOrphanTransactions.emplace(hash, COrphanTx{tx, peer, GetTime() + ORPHAN_TX_EXPIRE_TIME});
mapOrphanTransactions[hash].fromPeer = peer; assert(ret.second);
BOOST_FOREACH(const CTxIn& txin, tx.vin) BOOST_FOREACH(const CTxIn& txin, tx.vin) {
mapOrphanTransactionsByPrev[txin.prevout.hash].insert(hash); mapOrphanTransactionsByPrev[txin.prevout].insert(ret.first);
}
LogPrint("mempool", "stored orphan tx %s (mapsz %u prevsz %u)\n", hash.ToString(), LogPrint("mempool", "stored orphan tx %s (mapsz %u outsz %u)\n", hash.ToString(),
mapOrphanTransactions.size(), mapOrphanTransactionsByPrev.size()); mapOrphanTransactions.size(), mapOrphanTransactionsByPrev.size());
return true; return true;
} }
void static EraseOrphanTx(uint256 hash) EXCLUSIVE_LOCKS_REQUIRED(cs_main) int static EraseOrphanTx(uint256 hash) EXCLUSIVE_LOCKS_REQUIRED(cs_main)
{ {
map<uint256, COrphanTx>::iterator it = mapOrphanTransactions.find(hash); map<uint256, COrphanTx>::iterator it = mapOrphanTransactions.find(hash);
if (it == mapOrphanTransactions.end()) if (it == mapOrphanTransactions.end())
return; return 0;
BOOST_FOREACH(const CTxIn& txin, it->second.tx.vin) BOOST_FOREACH(const CTxIn& txin, it->second.tx.vin)
{ {
map<uint256, set<uint256> >::iterator itPrev = mapOrphanTransactionsByPrev.find(txin.prevout.hash); auto itPrev = mapOrphanTransactionsByPrev.find(txin.prevout);
if (itPrev == mapOrphanTransactionsByPrev.end()) if (itPrev == mapOrphanTransactionsByPrev.end())
continue; continue;
itPrev->second.erase(hash); itPrev->second.erase(it);
if (itPrev->second.empty()) if (itPrev->second.empty())
mapOrphanTransactionsByPrev.erase(itPrev); mapOrphanTransactionsByPrev.erase(itPrev);
} }
mapOrphanTransactions.erase(it); mapOrphanTransactions.erase(it);
return 1;
} }
void EraseOrphansFor(NodeId peer) void EraseOrphansFor(NodeId peer)
@ -668,8 +680,7 @@ void EraseOrphansFor(NodeId peer)
map<uint256, COrphanTx>::iterator maybeErase = iter++; // increment to avoid iterator becoming invalid map<uint256, COrphanTx>::iterator maybeErase = iter++; // increment to avoid iterator becoming invalid
if (maybeErase->second.fromPeer == peer) if (maybeErase->second.fromPeer == peer)
{ {
EraseOrphanTx(maybeErase->second.tx.GetHash()); nErased += EraseOrphanTx(maybeErase->second.tx.GetHash());
++nErased;
} }
} }
if (nErased > 0) LogPrint("mempool", "Erased %d orphan tx from peer %d\n", nErased, peer); if (nErased > 0) LogPrint("mempool", "Erased %d orphan tx from peer %d\n", nErased, peer);
@ -679,6 +690,26 @@ void EraseOrphansFor(NodeId peer)
unsigned int LimitOrphanTxSize(unsigned int nMaxOrphans) EXCLUSIVE_LOCKS_REQUIRED(cs_main) unsigned int LimitOrphanTxSize(unsigned int nMaxOrphans) EXCLUSIVE_LOCKS_REQUIRED(cs_main)
{ {
unsigned int nEvicted = 0; unsigned int nEvicted = 0;
static int64_t nNextSweep;
int64_t nNow = GetTime();
if (nNextSweep <= nNow) {
// Sweep out expired orphan pool entries:
int nErased = 0;
int64_t nMinExpTime = nNow + ORPHAN_TX_EXPIRE_TIME - ORPHAN_TX_EXPIRE_INTERVAL;
map<uint256, COrphanTx>::iterator iter = mapOrphanTransactions.begin();
while (iter != mapOrphanTransactions.end())
{
map<uint256, COrphanTx>::iterator maybeErase = iter++;
if (maybeErase->second.nTimeExpire <= nNow) {
nErased += EraseOrphanTx(maybeErase->second.tx.GetHash());
} else {
nMinExpTime = std::min(maybeErase->second.nTimeExpire, nMinExpTime);
}
}
// Sweep again 5 minutes after the next entry that expires in order to batch the linear scan.
nNextSweep = nMinExpTime + ORPHAN_TX_EXPIRE_INTERVAL;
if (nErased > 0) LogPrint("mempool", "Erased %d orphan tx due to expiration\n", nErased);
}
while (mapOrphanTransactions.size() > nMaxOrphans) while (mapOrphanTransactions.size() > nMaxOrphans)
{ {
// Evict a random orphan: // Evict a random orphan:
@ -2335,6 +2366,7 @@ bool ConnectBlock(const CBlock& block, CValidationState& state, CBlockIndex* pin
CCheckQueueControl<CScriptCheck> control(fScriptChecks && nScriptCheckThreads ? &scriptcheckqueue : NULL); CCheckQueueControl<CScriptCheck> control(fScriptChecks && nScriptCheckThreads ? &scriptcheckqueue : NULL);
std::vector<uint256> vOrphanErase;
std::vector<int> prevheights; std::vector<int> prevheights;
CAmount nFees = 0; CAmount nFees = 0;
int nInputs = 0; int nInputs = 0;
@ -2367,6 +2399,17 @@ bool ConnectBlock(const CBlock& block, CValidationState& state, CBlockIndex* pin
prevheights[j] = view.AccessCoins(tx.vin[j].prevout.hash)->nHeight; prevheights[j] = view.AccessCoins(tx.vin[j].prevout.hash)->nHeight;
} }
// Which orphan pool entries must we evict?
for (size_t j = 0; j < tx.vin.size(); j++) {
auto itByPrev = mapOrphanTransactionsByPrev.find(tx.vin[j].prevout);
if (itByPrev == mapOrphanTransactionsByPrev.end()) continue;
for (auto mi = itByPrev->second.begin(); mi != itByPrev->second.end(); ++mi) {
const CTransaction& orphanTx = (*mi)->second.tx;
const uint256& orphanHash = orphanTx.GetHash();
vOrphanErase.push_back(orphanHash);
}
}
if (!SequenceLocks(tx, nLockTimeFlags, &prevheights, *pindex)) { if (!SequenceLocks(tx, nLockTimeFlags, &prevheights, *pindex)) {
return state.DoS(100, error("%s: contains a non-BIP68-final transaction", __func__), return state.DoS(100, error("%s: contains a non-BIP68-final transaction", __func__),
REJECT_INVALID, "bad-txns-nonfinal"); REJECT_INVALID, "bad-txns-nonfinal");
@ -2454,6 +2497,15 @@ bool ConnectBlock(const CBlock& block, CValidationState& state, CBlockIndex* pin
GetMainSignals().UpdatedTransaction(hashPrevBestCoinBase); GetMainSignals().UpdatedTransaction(hashPrevBestCoinBase);
hashPrevBestCoinBase = block.vtx[0].GetHash(); hashPrevBestCoinBase = block.vtx[0].GetHash();
// Erase orphan transactions include or precluded by this block
if (vOrphanErase.size()) {
int nErased = 0;
BOOST_FOREACH(uint256 &orphanHash, vOrphanErase) {
nErased += EraseOrphanTx(orphanHash);
}
LogPrint("mempool", "Erased %d orphan tx included or conflicted by block\n", nErased);
}
int64_t nTime6 = GetTimeMicros(); nTimeCallbacks += nTime6 - nTime5; int64_t nTime6 = GetTimeMicros(); nTimeCallbacks += nTime6 - nTime5;
LogPrint("bench", " - Callbacks: %.2fms [%.2fs]\n", 0.001 * (nTime6 - nTime5), nTimeCallbacks * 0.000001); LogPrint("bench", " - Callbacks: %.2fms [%.2fs]\n", 0.001 * (nTime6 - nTime5), nTimeCallbacks * 0.000001);
@ -5041,7 +5093,7 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv,
return true; return true;
} }
vector<uint256> vWorkQueue; deque<COutPoint> vWorkQueue;
vector<uint256> vEraseQueue; vector<uint256> vEraseQueue;
CTransaction tx; CTransaction tx;
vRecv >> tx; vRecv >> tx;
@ -5060,7 +5112,9 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv,
if (!AlreadyHave(inv) && AcceptToMemoryPool(mempool, state, tx, true, &fMissingInputs)) { if (!AlreadyHave(inv) && AcceptToMemoryPool(mempool, state, tx, true, &fMissingInputs)) {
mempool.check(pcoinsTip); mempool.check(pcoinsTip);
RelayTransaction(tx); RelayTransaction(tx);
vWorkQueue.push_back(inv.hash); for (unsigned int i = 0; i < tx.vout.size(); i++) {
vWorkQueue.emplace_back(inv.hash, i);
}
pfrom->nLastTXTime = GetTime(); pfrom->nLastTXTime = GetTime();
@ -5071,18 +5125,18 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv,
// Recursively process any orphan transactions that depended on this one // Recursively process any orphan transactions that depended on this one
set<NodeId> setMisbehaving; set<NodeId> setMisbehaving;
for (unsigned int i = 0; i < vWorkQueue.size(); i++) while (!vWorkQueue.empty()) {
{ auto itByPrev = mapOrphanTransactionsByPrev.find(vWorkQueue.front());
map<uint256, set<uint256> >::iterator itByPrev = mapOrphanTransactionsByPrev.find(vWorkQueue[i]); vWorkQueue.pop_front();
if (itByPrev == mapOrphanTransactionsByPrev.end()) if (itByPrev == mapOrphanTransactionsByPrev.end())
continue; continue;
for (set<uint256>::iterator mi = itByPrev->second.begin(); for (auto mi = itByPrev->second.begin();
mi != itByPrev->second.end(); mi != itByPrev->second.end();
++mi) ++mi)
{ {
const uint256& orphanHash = *mi; const CTransaction& orphanTx = (*mi)->second.tx;
const CTransaction& orphanTx = mapOrphanTransactions[orphanHash].tx; const uint256& orphanHash = orphanTx.GetHash();
NodeId fromPeer = mapOrphanTransactions[orphanHash].fromPeer; NodeId fromPeer = (*mi)->second.fromPeer;
bool fMissingInputs2 = false; bool fMissingInputs2 = false;
// Use a dummy CValidationState so someone can't setup nodes to counter-DoS based on orphan // Use a dummy CValidationState so someone can't setup nodes to counter-DoS based on orphan
// resolution (that is, feeding people an invalid transaction based on LegitTxX in order to get // 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,
if (AcceptToMemoryPool(mempool, stateDummy, orphanTx, true, &fMissingInputs2)) { if (AcceptToMemoryPool(mempool, stateDummy, orphanTx, true, &fMissingInputs2)) {
LogPrint("mempool", " accepted orphan tx %s\n", orphanHash.ToString()); LogPrint("mempool", " accepted orphan tx %s\n", orphanHash.ToString());
RelayTransaction(orphanTx); RelayTransaction(orphanTx);
vWorkQueue.push_back(orphanHash); for (unsigned int i = 0; i < orphanTx.vout.size(); i++) {
vWorkQueue.emplace_back(orphanHash, i);
}
vEraseQueue.push_back(orphanHash); vEraseQueue.push_back(orphanHash);
} }
else if (!fMissingInputs2) else if (!fMissingInputs2)
@ -5124,13 +5180,29 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv,
} }
else if (fMissingInputs) else if (fMissingInputs)
{ {
AddOrphanTx(tx, pfrom->GetId()); bool fRejectedParents = false; // It may be the case that the orphans parents have all been rejected
BOOST_FOREACH(const CTxIn& txin, tx.vin) {
if (recentRejects->contains(txin.prevout.hash)) {
fRejectedParents = true;
break;
}
}
if (!fRejectedParents) {
BOOST_FOREACH(const CTxIn& txin, tx.vin) {
CInv inv(MSG_TX, txin.prevout.hash);
pfrom->AddInventoryKnown(inv);
if (!AlreadyHave(inv)) pfrom->AskFor(inv);
}
AddOrphanTx(tx, pfrom->GetId());
// DoS prevention: do not allow mapOrphanTransactions to grow unbounded // DoS prevention: do not allow mapOrphanTransactions to grow unbounded
unsigned int nMaxOrphanTx = (unsigned int)std::max((int64_t)0, GetArg("-maxorphantx", DEFAULT_MAX_ORPHAN_TRANSACTIONS)); unsigned int nMaxOrphanTx = (unsigned int)std::max((int64_t)0, GetArg("-maxorphantx", DEFAULT_MAX_ORPHAN_TRANSACTIONS));
unsigned int nEvicted = LimitOrphanTxSize(nMaxOrphanTx); unsigned int nEvicted = LimitOrphanTxSize(nMaxOrphanTx);
if (nEvicted > 0) if (nEvicted > 0)
LogPrint("mempool", "mapOrphan overflow, removed %u tx\n", nEvicted); LogPrint("mempool", "mapOrphan overflow, removed %u tx\n", nEvicted);
} else {
LogPrint("mempool", "not keeping orphan with rejected parents %s\n",tx.GetHash().ToString());
}
} else { } else {
assert(recentRejects); assert(recentRejects);
recentRejects->insert(tx.GetHash()); recentRejects->insert(tx.GetHash());

View file

@ -56,6 +56,10 @@ static const CAmount HIGH_TX_FEE_PER_KB = 0.01 * COIN;
static const CAmount HIGH_MAX_TX_FEE = 100 * HIGH_TX_FEE_PER_KB; static const CAmount HIGH_MAX_TX_FEE = 100 * HIGH_TX_FEE_PER_KB;
/** Default for -maxorphantx, maximum number of orphan transactions kept in memory */ /** Default for -maxorphantx, maximum number of orphan transactions kept in memory */
static const unsigned int DEFAULT_MAX_ORPHAN_TRANSACTIONS = 100; static const unsigned int DEFAULT_MAX_ORPHAN_TRANSACTIONS = 100;
/** Expiration time for orphan transactions in seconds */
static const int64_t ORPHAN_TX_EXPIRE_TIME = 20 * 60;
/** Minimum time between orphan transactions expire time checks in seconds */
static const int64_t ORPHAN_TX_EXPIRE_INTERVAL = 5 * 60;
/** Default for -limitancestorcount, max number of in-mempool ancestors */ /** Default for -limitancestorcount, max number of in-mempool ancestors */
static const unsigned int DEFAULT_ANCESTOR_LIMIT = 25; static const unsigned int DEFAULT_ANCESTOR_LIMIT = 25;
/** Default for -limitancestorsize, maximum kilobytes of tx + all in-mempool ancestors */ /** Default for -limitancestorsize, maximum kilobytes of tx + all in-mempool ancestors */

View file

@ -162,7 +162,7 @@ BOOST_AUTO_TEST_CASE(DoS_mapOrphans)
tx.vout.resize(1); tx.vout.resize(1);
tx.vout[0].nValue = 1*CENT; tx.vout[0].nValue = 1*CENT;
tx.vout[0].scriptPubKey = GetScriptForDestination(key.GetPubKey().GetID()); tx.vout[0].scriptPubKey = GetScriptForDestination(key.GetPubKey().GetID());
tx.vin.resize(500); tx.vin.resize(2777);
for (unsigned int j = 0; j < tx.vin.size(); j++) for (unsigned int j = 0; j < tx.vin.size(); j++)
{ {
tx.vin[j].prevout.n = j; tx.vin[j].prevout.n = j;