diff --git a/src/Makefile.am b/src/Makefile.am index e51301de6a806..6fed67ab64861 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -161,6 +161,7 @@ BITCOIN_CORE_H = \ torcontrol.h \ txdb.h \ txmempool.h \ + txorphancache.h \ ui_interface.h \ uint256.h \ undo.h \ @@ -204,6 +205,7 @@ libbitcoin_server_a_SOURCES = \ init.cpp \ dbwrapper.cpp \ main.cpp \ + txorphancache.cpp \ merkleblock.cpp \ miner.cpp \ net.cpp \ diff --git a/src/init.cpp b/src/init.cpp index a7b227bb738e7..684d6a0ec3346 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -35,6 +35,7 @@ #include "util.h" #include "utilmoneystr.h" #include "utilstrencodings.h" +#include "txorphancache.h" #include "validationinterface.h" #ifdef ENABLE_WALLET #include "wallet/db.h" @@ -1105,6 +1106,8 @@ bool AppInit2(boost::thread_group& threadGroup, CScheduler& scheduler) #endif // ENABLE_WALLET // ********************************************************* Step 6: network initialization + CTxOrphanCache::instance()->setLimit((unsigned int)std::max((int64_t)0, GetArg("-maxorphantx", DEFAULT_MAX_ORPHAN_TRANSACTIONS))); + RegisterNodeSignals(GetNodeSignals()); if (mapArgs.count("-onlynet")) { diff --git a/src/main.cpp b/src/main.cpp index 1f52fabe3cbce..905b889718ec7 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -28,6 +28,7 @@ #include "tinyformat.h" #include "txdb.h" #include "txmempool.h" +#include "txorphancache.h" #include "ui_interface.h" #include "undo.h" #include "util.h" @@ -82,14 +83,6 @@ CFeeRate minRelayTxFee = CFeeRate(DEFAULT_MIN_RELAY_TX_FEE); CTxMemPool mempool(::minRelayTxFee); -struct COrphanTx { - CTransaction tx; - NodeId fromPeer; -}; -map mapOrphanTransactions GUARDED_BY(cs_main);; -map > mapOrphanTransactionsByPrev GUARDED_BY(cs_main);; -void EraseOrphansFor(NodeId peer) EXCLUSIVE_LOCKS_REQUIRED(cs_main); - /** * Returns true if there are nRequired or more blocks of minVersion or above * in the last Consensus::Params::nMajorityWindow blocks, starting at pstart and going backwards. @@ -591,91 +584,6 @@ CBlockIndex* FindForkInGlobalIndex(const CChain& chain, const CBlockLocator& loc CCoinsViewCache *pcoinsTip = NULL; CBlockTreeDB *pblocktree = NULL; -////////////////////////////////////////////////////////////////////////////// -// -// mapOrphanTransactions -// - -bool AddOrphanTx(const CTransaction& tx, NodeId peer) EXCLUSIVE_LOCKS_REQUIRED(cs_main) -{ - uint256 hash = tx.GetHash(); - if (mapOrphanTransactions.count(hash)) - return false; - - // Ignore big transactions, to avoid a - // send-big-orphans memory exhaustion attack. If a peer has a legitimate - // large transaction with a missing parent then we assume - // it will rebroadcast it later, after the parent transaction(s) - // have been mined or received. - // 10,000 orphans, each of which is at most 5,000 bytes big is - // at most 500 megabytes of orphans: - unsigned int sz = tx.GetSerializeSize(SER_NETWORK, CTransaction::CURRENT_VERSION); - if (sz > 5000) - { - LogPrint("mempool", "ignoring large orphan tx (size: %u, hash: %s)\n", sz, hash.ToString()); - return false; - } - - mapOrphanTransactions[hash].tx = tx; - mapOrphanTransactions[hash].fromPeer = peer; - BOOST_FOREACH(const CTxIn& txin, tx.vin) - mapOrphanTransactionsByPrev[txin.prevout.hash].insert(hash); - - LogPrint("mempool", "stored orphan tx %s (mapsz %u prevsz %u)\n", hash.ToString(), - mapOrphanTransactions.size(), mapOrphanTransactionsByPrev.size()); - return true; -} - -void static EraseOrphanTx(uint256 hash) EXCLUSIVE_LOCKS_REQUIRED(cs_main) -{ - map::iterator it = mapOrphanTransactions.find(hash); - if (it == mapOrphanTransactions.end()) - return; - BOOST_FOREACH(const CTxIn& txin, it->second.tx.vin) - { - map >::iterator itPrev = mapOrphanTransactionsByPrev.find(txin.prevout.hash); - if (itPrev == mapOrphanTransactionsByPrev.end()) - continue; - itPrev->second.erase(hash); - if (itPrev->second.empty()) - mapOrphanTransactionsByPrev.erase(itPrev); - } - mapOrphanTransactions.erase(it); -} - -void EraseOrphansFor(NodeId peer) -{ - int nErased = 0; - map::iterator iter = mapOrphanTransactions.begin(); - while (iter != mapOrphanTransactions.end()) - { - map::iterator maybeErase = iter++; // increment to avoid iterator becoming invalid - if (maybeErase->second.fromPeer == peer) - { - EraseOrphanTx(maybeErase->second.tx.GetHash()); - ++nErased; - } - } - if (nErased > 0) LogPrint("mempool", "Erased %d orphan tx from peer %d\n", nErased, peer); -} - - -unsigned int LimitOrphanTxSize(unsigned int nMaxOrphans) EXCLUSIVE_LOCKS_REQUIRED(cs_main) -{ - unsigned int nEvicted = 0; - while (mapOrphanTransactions.size() > nMaxOrphans) - { - // Evict a random orphan: - uint256 randomhash = GetRandHash(); - map::iterator it = mapOrphanTransactions.lower_bound(randomhash); - if (it == mapOrphanTransactions.end()) - it = mapOrphanTransactions.begin(); - EraseOrphanTx(it->first); - ++nEvicted; - } - return nEvicted; -} - bool IsFinalTx(const CTransaction &tx, int nBlockHeight, int64_t nBlockTime) { if (tx.nLockTime == 0) @@ -3774,8 +3682,7 @@ void UnloadBlockIndex() pindexBestInvalid = NULL; pindexBestHeader = NULL; mempool.clear(); - mapOrphanTransactions.clear(); - mapOrphanTransactionsByPrev.clear(); + CTxOrphanCache::clear(); nSyncStarted = 0; mapBlocksUnlinked.clear(); vinfoBlockFile.clear(); @@ -4210,7 +4117,7 @@ bool static AlreadyHave(const CInv& inv) EXCLUSIVE_LOCKS_REQUIRED(cs_main) return recentRejects->contains(inv.hash) || mempool.exists(inv.hash) || - mapOrphanTransactions.count(inv.hash) || + CTxOrphanCache::contains(inv.hash) || pcoinsTip->HaveCoins(inv.hash); } case MSG_BLOCK: @@ -4685,10 +4592,7 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv, if (pfrom->mapThinBlocksInFlight.size() < 1 && pfrom->ThinBlockCapable()) { // We can only send one thinblock per peer at a time pfrom->mapThinBlocksInFlight[inv2.hash] = GetTime(); inv2.type = MSG_XTHINBLOCK; - std::vector vOrphanHashes; - for (map::iterator mi = mapOrphanTransactions.begin(); mi != mapOrphanTransactions.end(); ++mi) - vOrphanHashes.push_back((*mi).first); - CBloomFilter filterMemPool = createSeededBloomFilter(vOrphanHashes); + CBloomFilter filterMemPool = createSeededBloomFilter(CTxOrphanCache::instance()->fetchTransactionIds()); ss << inv2; ss << filterMemPool; pfrom->PushMessage(NetMsgType::GET_XTHIN, ss); @@ -4701,10 +4605,7 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv, if (pfrom->mapThinBlocksInFlight.size() < 1 && pfrom->ThinBlockCapable()) { // We can only send one thinblock per peer at a time pfrom->mapThinBlocksInFlight[inv2.hash] = GetTime(); inv2.type = MSG_XTHINBLOCK; - std::vector vOrphanHashes; - for (map::iterator mi = mapOrphanTransactions.begin(); mi != mapOrphanTransactions.end(); ++mi) - vOrphanHashes.push_back((*mi).first); - CBloomFilter filterMemPool = createSeededBloomFilter(vOrphanHashes); + CBloomFilter filterMemPool = createSeededBloomFilter(CTxOrphanCache::instance()->fetchTransactionIds()); ss << inv2; ss << filterMemPool; pfrom->PushMessage(NetMsgType::GET_XTHIN, ss); @@ -4904,24 +4805,17 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv, set setMisbehaving; for (unsigned int i = 0; i < vWorkQueue.size(); i++) { - map >::iterator itByPrev = mapOrphanTransactionsByPrev.find(vWorkQueue[i]); - if (itByPrev == mapOrphanTransactionsByPrev.end()) - continue; - for (set::iterator mi = itByPrev->second.begin(); - mi != itByPrev->second.end(); - ++mi) - { - const uint256& orphanHash = *mi; - const CTransaction& orphanTx = mapOrphanTransactions[orphanHash].tx; - NodeId fromPeer = mapOrphanTransactions[orphanHash].fromPeer; + auto orphans = CTxOrphanCache::instance()->fetchTransactionsByPrev(vWorkQueue[i]); + for (auto mi = orphans.begin(); mi != orphans.end(); ++mi) { + const CTransaction& orphanTx = mi->tx; + const uint256 orphanHash = orphanTx.GetHash(); bool fMissingInputs2 = false; // 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 // anyone relaying LegitTxX banned) CValidationState stateDummy; - - if (setMisbehaving.count(fromPeer)) + if (setMisbehaving.count(mi->fromPeer)) continue; if (AcceptToMemoryPool(mempool, stateDummy, orphanTx, true, &fMissingInputs2)) { @@ -4936,8 +4830,8 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv, if (stateDummy.IsInvalid(nDos) && nDos > 0) { // Punish peer that gave us an invalid orphan tx - Misbehaving(fromPeer, nDos); - setMisbehaving.insert(fromPeer); + Misbehaving(mi->fromPeer, nDos); + setMisbehaving.insert(mi->fromPeer); LogPrint("mempool", " invalid orphan tx %s\n", orphanHash.ToString()); } // Has inputs but not accepted to mempool @@ -4951,16 +4845,15 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv, } } - BOOST_FOREACH(uint256 hash, vEraseQueue) - EraseOrphanTx(hash); + CTxOrphanCache::instance()->EraseOrphans(vEraseQueue); + CTxOrphanCache::instance()->EraseOrphansByTime(); } else if (fMissingInputs) { - AddOrphanTx(tx, pfrom->GetId()); - - // 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 nEvicted = LimitOrphanTxSize(nMaxOrphanTx); + CTxOrphanCache *cache = CTxOrphanCache::instance(); + // DoS prevention: do not allow CTxOrphanCache to grow unbounded + cache->AddOrphanTx(tx, pfrom->GetId()); + std::uint32_t nEvicted = cache->LimitOrphanTxSize(); if (nEvicted > 0) LogPrint("mempool", "mapOrphan overflow, removed %u tx\n", nEvicted); } else { @@ -5157,12 +5050,6 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv, collision = true; mapPartialTxHash[cheapHash] = memPoolHashes[i]; } - for (map::iterator mi = mapOrphanTransactions.begin(); mi != mapOrphanTransactions.end(); ++mi) { - uint64_t cheapHash = (*mi).first.GetCheapHash(); - if(mapPartialTxHash.count(cheapHash)) //Check for collisions - collision = true; - mapPartialTxHash[cheapHash] = (*mi).first; - } for (map::iterator mi = mapMissingTx.begin(); mi != mapMissingTx.end(); ++mi) { uint64_t cheapHash = (*mi).first.GetCheapHash(); // Check for cheap hash collision. Only mark as collision if the full hash is not the same, @@ -5203,13 +5090,12 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv, { bool inMemPool = mempool.lookup(hash, tx); bool inMissingTx = mapMissingTx.count(hash) > 0; - bool inOrphanCache = mapOrphanTransactions.count(hash) > 0; + bool inOrphanCache = false; if ((inMemPool && inMissingTx) || (inOrphanCache && inMissingTx)) unnecessaryCount++; if (inOrphanCache) { - tx = mapOrphanTransactions[hash].tx; setUnVerifiedOrphanTxHash.insert(hash); } else if (inMemPool && fXVal) @@ -5240,8 +5126,6 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv, #endif HandleBlockMessage(pfrom, strCommand, pfrom->thinBlock, inv); // clears the thin block - BOOST_FOREACH(uint64_t &cheapHash, thinBlock.vTxHashes) - EraseOrphanTx(mapPartialTxHash[cheapHash]); } else if (pfrom->thinBlockWaitingForTxns > 0) { // This marks the end of the transactions we've received. If we get this and we have NOT been able to @@ -5304,10 +5188,14 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv, ); #endif - std::vector vTx = pfrom->thinBlock.vtx; + // For correctness sake, assume all came from the orphans cache + std::vector orphans; + orphans.reserve(pfrom->thinBlock.vtx.size()); + for (unsigned int i = 0; i < pfrom->thinBlock.vtx.size(); i++) { + orphans.push_back(pfrom->thinBlock.vtx[i].GetHash()); + } HandleBlockMessage(pfrom, strCommand, pfrom->thinBlock, inv); - for (unsigned int i = 0; i < vTx.size(); i++) - EraseOrphanTx(vTx[i].GetHash()); + CTxOrphanCache::instance()->EraseOrphans(orphans); } else { LogPrint("thin", "Failed to retrieve all transactions for block - DOS Banned\n"); @@ -5316,7 +5204,6 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv, } } - else if (strCommand == NetMsgType::GET_XBLOCKTX && !fImporting && !fReindex) // return Re-requested xthinblock transactions { CXRequestThinBlockTx thinRequestBlockTx; @@ -5386,8 +5273,12 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv, // BUIP010 Extreme Thinblocks: Handle Block Message HandleBlockMessage(pfrom, strCommand, block, inv); - for (unsigned int i = 0; i < block.vtx.size(); i++) - EraseOrphanTx(block.vtx[i].GetHash()); + std::vector orphans; + orphans.reserve(block.vtx.size()); + for (unsigned int i = 0; i < block.vtx.size(); i++) { + orphans.push_back(block.vtx[i].GetHash()); + } + CTxOrphanCache::instance()->EraseOrphans(orphans); } @@ -6050,10 +5941,7 @@ bool SendMessages(CNode* pto) // Must download a block from a ThinBlock peer if (pto->mapThinBlocksInFlight.size() < 1 && pto->ThinBlockCapable()) { // We can only send one thinblock per peer at a time pto->mapThinBlocksInFlight[pindex->GetBlockHash()] = GetTime(); - std::vector vOrphanHashes; - for (map::iterator mi = mapOrphanTransactions.begin(); mi != mapOrphanTransactions.end(); ++mi) - vOrphanHashes.push_back((*mi).first); - CBloomFilter filterMemPool = createSeededBloomFilter(vOrphanHashes); + CBloomFilter filterMemPool = createSeededBloomFilter(CTxOrphanCache::instance()->fetchTransactionIds()); ss << CInv(MSG_XTHINBLOCK, pindex->GetBlockHash()); ss << filterMemPool; pto->PushMessage(NetMsgType::GET_XTHIN, ss); @@ -6066,10 +5954,7 @@ bool SendMessages(CNode* pto) // Try to download a thinblock if possible otherwise just download a regular block if (pto->mapThinBlocksInFlight.size() < 1 && pto->ThinBlockCapable()) { // We can only send one thinblock per peer at a time pto->mapThinBlocksInFlight[pindex->GetBlockHash()] = GetTime(); - std::vector vOrphanHashes; - for (map::iterator mi = mapOrphanTransactions.begin(); mi != mapOrphanTransactions.end(); ++mi) - vOrphanHashes.push_back((*mi).first); - CBloomFilter filterMemPool = createSeededBloomFilter(vOrphanHashes); + CBloomFilter filterMemPool = createSeededBloomFilter(CTxOrphanCache::instance()->fetchTransactionIds()); ss << CInv(MSG_XTHINBLOCK, pindex->GetBlockHash()); ss << filterMemPool; pto->PushMessage(NetMsgType::GET_XTHIN, ss); @@ -6149,9 +6034,5 @@ class CMainCleanup for (; it1 != mapBlockIndex.end(); it1++) delete (*it1).second; mapBlockIndex.clear(); - - // orphan transactions - mapOrphanTransactions.clear(); - mapOrphanTransactionsByPrev.clear(); } } instance_of_cmaincleanup; diff --git a/src/test/DoS_tests.cpp b/src/test/DoS_tests.cpp index 95342498fa782..0bfaa749251fc 100644 --- a/src/test/DoS_tests.cpp +++ b/src/test/DoS_tests.cpp @@ -1,4 +1,5 @@ // Copyright (c) 2011-2015 The Bitcoin Core developers +// Copyright (c) 2015-2016 The Bitcoin Unlimited developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. @@ -12,6 +13,7 @@ #include "script/sign.h" #include "serialize.h" #include "util.h" +#include #include "test/test_bitcoin.h" @@ -22,17 +24,6 @@ #include #include -// Tests this internal-to-main.cpp method: -extern bool AddOrphanTx(const CTransaction& tx, NodeId peer); -extern void EraseOrphansFor(NodeId peer); -extern unsigned int LimitOrphanTxSize(unsigned int nMaxOrphans); -struct COrphanTx { - CTransaction tx; - NodeId fromPeer; -}; -extern std::map mapOrphanTransactions; -extern std::map > mapOrphanTransactionsByPrev; - CService ip(uint32_t i) { struct in_addr s; @@ -40,6 +31,29 @@ CService ip(uint32_t i) return CService(CNetAddr(s), Params().GetDefaultPort()); } +class OrphanCacheMock : public CTxOrphanCache +{ +public: + std::map mapOrphanTransactions() { + return m_mapOrphanTransactions; + } + std::map > mapOrphanTransactionsByPrev() { + return m_mapOrphanTransactionsByPrev; + } + + void LimitOrphanTxSizePublic(unsigned int max) { + LimitOrphanTxSize(max); + } + + CTransaction RandomOrphan() + { + auto it = m_mapOrphanTransactions.lower_bound(GetRandHash()); + if (it == m_mapOrphanTransactions.end()) + it = m_mapOrphanTransactions.begin(); + return it->second.tx; + } +}; + BOOST_FIXTURE_TEST_SUITE(DoS_tests, TestingSetup) BOOST_AUTO_TEST_CASE(DoS_banning) @@ -105,14 +119,6 @@ BOOST_AUTO_TEST_CASE(DoS_bantime) BOOST_CHECK(!CNode::IsBanned(addr)); } -CTransaction RandomOrphan() -{ - std::map::iterator it; - it = mapOrphanTransactions.lower_bound(GetRandHash()); - if (it == mapOrphanTransactions.end()) - it = mapOrphanTransactions.begin(); - return it->second.tx; -} BOOST_AUTO_TEST_CASE(DoS_mapOrphans) { @@ -121,6 +127,8 @@ BOOST_AUTO_TEST_CASE(DoS_mapOrphans) CBasicKeyStore keystore; keystore.AddKey(key); + OrphanCacheMock cache; + // 50 orphan transactions: for (int i = 0; i < 50; i++) { @@ -132,14 +140,13 @@ BOOST_AUTO_TEST_CASE(DoS_mapOrphans) tx.vout.resize(1); tx.vout[0].nValue = 1*CENT; tx.vout[0].scriptPubKey = GetScriptForDestination(key.GetPubKey().GetID()); - - AddOrphanTx(tx, i); + cache.AddOrphanTx(tx, i); } // ... and 50 that depend on other orphans: for (int i = 0; i < 50; i++) { - CTransaction txPrev = RandomOrphan(); + CTransaction txPrev = cache.RandomOrphan(); CMutableTransaction tx; tx.vin.resize(1); @@ -150,13 +157,13 @@ BOOST_AUTO_TEST_CASE(DoS_mapOrphans) tx.vout[0].scriptPubKey = GetScriptForDestination(key.GetPubKey().GetID()); SignSignature(keystore, txPrev, tx, 0); - AddOrphanTx(tx, i); + cache.AddOrphanTx(tx, i); } // This really-big orphan should be ignored: for (int i = 0; i < 10; i++) { - CTransaction txPrev = RandomOrphan(); + CTransaction txPrev = cache.RandomOrphan(); CMutableTransaction tx; tx.vout.resize(1); @@ -174,25 +181,82 @@ BOOST_AUTO_TEST_CASE(DoS_mapOrphans) for (unsigned int j = 1; j < tx.vin.size(); j++) tx.vin[j].scriptSig = tx.vin[0].scriptSig; - BOOST_CHECK(!AddOrphanTx(tx, i)); + if (i == 0) { + BOOST_CHECK(cache.AddOrphanTx(tx, i)); // we keep orphans up to the configured memory limit to help xthin compression so this should succeed whereas it fails in other clients + } } // Test EraseOrphansFor: for (NodeId i = 0; i < 3; i++) { - size_t sizeBefore = mapOrphanTransactions.size(); - EraseOrphansFor(i); - BOOST_CHECK(mapOrphanTransactions.size() < sizeBefore); + size_t sizeBefore = cache.mapOrphanTransactions().size(); + cache.EraseOrphansFor(i); + BOOST_CHECK(cache.mapOrphanTransactions().size() < sizeBefore); } // Test LimitOrphanTxSize() function: - LimitOrphanTxSize(40); - BOOST_CHECK(mapOrphanTransactions.size() <= 40); - LimitOrphanTxSize(10); - BOOST_CHECK(mapOrphanTransactions.size() <= 10); - LimitOrphanTxSize(0); - BOOST_CHECK(mapOrphanTransactions.empty()); - BOOST_CHECK(mapOrphanTransactionsByPrev.empty()); + { + cache.LimitOrphanTxSizePublic(40); + BOOST_CHECK(cache.mapOrphanTransactions().size() <= 40); + cache.LimitOrphanTxSizePublic(10); + BOOST_CHECK(cache.mapOrphanTransactions().size() <= 10); + cache.LimitOrphanTxSizePublic(0); + BOOST_CHECK(cache.mapOrphanTransactions().empty()); + BOOST_CHECK(cache.mapOrphanTransactionsByPrev().empty()); + } + + // Test EraseOrphansByTime(): + { + int64_t nStartTime = GetTime(); + SetMockTime(nStartTime); // Overrides future calls to GetTime() + for (int i = 0; i < 50; i++) + { + CMutableTransaction tx; + tx.vin.resize(1); + tx.vin[0].prevout.n = 0; + tx.vin[0].prevout.hash = GetRandHash(); + tx.vin[0].scriptSig << OP_1; + tx.vout.resize(1); + tx.vout[0].nValue = 1*CENT; + tx.vout[0].scriptPubKey = GetScriptForDestination(key.GetPubKey().GetID()); + + cache.AddOrphanTx(tx, i); + } + BOOST_CHECK(cache.mapOrphanTransactions().size() == 50); + cache.EraseOrphansByTime(); + BOOST_CHECK(cache.mapOrphanTransactions().size() == 50); + + // Advance the clock 1 minute + SetMockTime(nStartTime+60); + cache.EraseOrphansByTime(); + BOOST_CHECK(cache.mapOrphanTransactions().size() == 50); + + // Advance the clock 10 minutes + SetMockTime(nStartTime+60*10); + cache.EraseOrphansByTime(); + BOOST_CHECK(cache.mapOrphanTransactions().size() == 50); + + // Advance the clock 1 hour + SetMockTime(nStartTime+60*60); + cache.EraseOrphansByTime(); + BOOST_CHECK(cache.mapOrphanTransactions().size() == 50); + + // Advance the clock 72 hours + SetMockTime(nStartTime+60*60*72); + cache.EraseOrphansByTime(); + BOOST_CHECK(cache.mapOrphanTransactions().size() == 50); + + /** Test the boundary where orphans should get purged. **/ + // Advance the clock 72 hours and 4 minutes 59 seconds + SetMockTime(nStartTime+60*60*72 + 299); + cache.EraseOrphansByTime(); + BOOST_CHECK(cache.mapOrphanTransactions().size() == 50); + + // Advance the clock 72 hours and 5 minutes + SetMockTime(nStartTime+60*60*72 + 300); + cache.EraseOrphansByTime(); + BOOST_CHECK(cache.mapOrphanTransactions().size() == 0); + } } BOOST_AUTO_TEST_SUITE_END() diff --git a/src/txorphancache.cpp b/src/txorphancache.cpp new file mode 100644 index 0000000000000..2126da93a22a2 --- /dev/null +++ b/src/txorphancache.cpp @@ -0,0 +1,205 @@ +#include "txorphancache.h" +#include "main.h" + +#include "util.h" +#include "net.h" +#include + +CTxOrphanCache::CTxOrphanCache() + : m_limit(DEFAULT_MAX_ORPHAN_TRANSACTIONS) +{ +} + +CTxOrphanCache* CTxOrphanCache::s_instance = 0; +CTxOrphanCache* CTxOrphanCache::instance() +{ + if (s_instance == 0) + s_instance = new CTxOrphanCache(); + return s_instance; +} + +bool CTxOrphanCache::AddOrphanTx(const CTransaction& tx, NodeId peer) +{ + LOCK(m_lock); + + uint256 hash = tx.GetHash(); + if (m_mapOrphanTransactions.count(hash)) + return false; + + // Ignore big transactions, to avoid a + // send-big-orphans memory exhaustion attack. If a peer has a legitimate + // large transaction with a missing parent then we assume + // it will rebroadcast it later, after the parent transaction(s) + // have been mined or received. + // 5000 orphans, each of which is at most 100,000 bytes big is + // at most 500 megabytes of orphans: + + unsigned int sz = tx.GetSerializeSize(SER_NETWORK, CTransaction::CURRENT_VERSION); + if (sz > 100000) { + LogPrint("mempool", "ignoring large orphan tx (size: %u, hash: %s)\n", sz, hash.ToString()); + return false; + } + + m_mapOrphanTransactions[hash].tx = tx; + m_mapOrphanTransactions[hash].fromPeer = peer; + m_mapOrphanTransactions[hash].nEntryTime = GetTime(); + BOOST_FOREACH(const CTxIn& txin, tx.vin) { + m_mapOrphanTransactionsByPrev[txin.prevout.hash].insert(hash); + } + + LogPrint("mempool", "stored orphan tx %s (mapsz %u prevsz %u)\n", hash.ToString(), + m_mapOrphanTransactions.size(), m_mapOrphanTransactionsByPrev.size()); + return true; +} + +void CTxOrphanCache::EraseOrphanTx(uint256 hash) +{ + std::map::iterator it = m_mapOrphanTransactions.find(hash); + if (it == m_mapOrphanTransactions.end()) + return; + BOOST_FOREACH(const CTxIn& txin, it->second.tx.vin) { + auto itPrev = m_mapOrphanTransactionsByPrev.find(txin.prevout.hash); + if (itPrev == m_mapOrphanTransactionsByPrev.end()) + continue; + itPrev->second.erase(hash); + if (itPrev->second.empty()) + m_mapOrphanTransactionsByPrev.erase(itPrev); + } + m_mapOrphanTransactions.erase(it); +} + +void CTxOrphanCache::EraseOrphansFor(NodeId peer) +{ + LOCK(m_lock); + int nErased = 0; + std::map::iterator iter = m_mapOrphanTransactions.begin(); + while (iter != m_mapOrphanTransactions.end()) { + std::map::iterator maybeErase = iter++; // increment to avoid iterator becoming invalid + if (maybeErase->second.fromPeer == peer) { + EraseOrphanTx(maybeErase->second.tx.GetHash()); + ++nErased; + } + } + if (nErased > 0) LogPrint("mempool", "Erased %d orphan tx from peer %d\n", nErased, peer); +} + +void CTxOrphanCache::EraseOrphansByTime() +{ + LOCK(m_lock); + static int64_t nLastOrphanCheck = GetTime(); + + // Because we have to iterate through the entire orphan cache which can be large we don't want to check this + // every time a tx enters the mempool but just once every 5 minutes is good enough. + if (GetTime() < nLastOrphanCheck + 5 * 60) + return; + int64_t nOrphanTxCutoffTime = GetTime() - GetArg("-mempoolexpiry", DEFAULT_MEMPOOL_EXPIRY) * 60 * 60; + std::map::iterator iter = m_mapOrphanTransactions.begin(); + while (iter != m_mapOrphanTransactions.end()) { + const auto entry = iter++; + int64_t nEntryTime = entry->second.nEntryTime; + if (nEntryTime < nOrphanTxCutoffTime) { + uint256 txHash = entry->second.tx.GetHash(); + EraseOrphanTx(txHash); + LogPrint("mempool", "Erased old orphan tx %s of age %d seconds\n", txHash.ToString(), GetTime() - nEntryTime); + } + } + + nLastOrphanCheck = GetTime(); +} + +std::uint32_t CTxOrphanCache::LimitOrphanTxSize(std::uint32_t nMaxOrphans) +{ + LOCK(m_lock); + unsigned int nEvicted = 0; + while (m_mapOrphanTransactions.size() > nMaxOrphans) { + // Evict a random orphan: + uint256 randomhash = GetRandHash(); + std::map::iterator it = m_mapOrphanTransactions.lower_bound(randomhash); + if (it == m_mapOrphanTransactions.end()) + it = m_mapOrphanTransactions.begin(); + EraseOrphanTx(it->first); + ++nEvicted; + } + return nEvicted; +} + +uint32_t CTxOrphanCache::LimitOrphanTxSize() +{ + return LimitOrphanTxSize(m_limit); +} + +void CTxOrphanCache::clear() +{ + if (s_instance) { + LOCK(s_instance->m_lock); + s_instance->m_mapOrphanTransactions.clear(); + s_instance->m_mapOrphanTransactionsByPrev.clear(); + } +} + +bool CTxOrphanCache::value(const uint256 &txid, CTransaction &output) +{ + CTxOrphanCache *s = instance(); + LOCK(s->m_lock); + auto iter = s->m_mapOrphanTransactions.find(txid); + if (iter == s->m_mapOrphanTransactions.end()) + return false; + output = iter->second.tx; + return true; +} + +bool CTxOrphanCache::contains(const uint256 &txid) +{ + CTxOrphanCache *s = instance(); + LOCK(s->m_lock); + return s->m_mapOrphanTransactions.count(txid) > 0; +} + +std::vector CTxOrphanCache::fetchTransactionIds() const +{ + LOCK(m_lock); + std::vector answer; + answer.reserve(m_mapOrphanTransactions.size()); + for (auto iter = m_mapOrphanTransactions.begin(); iter != m_mapOrphanTransactions.end(); ++iter) + answer.push_back((*iter).first); + return answer; +} + +void CTxOrphanCache::setLimit(uint32_t limit) +{ + m_limit = limit; +} + +std::vector CTxOrphanCache::fetchTransactionsByPrev(const uint256 &txid) const +{ + LOCK(m_lock); + std::vector answer; + auto itByPrev = m_mapOrphanTransactionsByPrev.find(txid); + if (itByPrev == m_mapOrphanTransactionsByPrev.end()) + return answer; + for (auto mi = itByPrev->second.begin(); mi != itByPrev->second.end(); ++mi) { + const uint256& orphanHash = *mi; + answer.push_back(m_mapOrphanTransactions.at(orphanHash)); + } + return answer; +} + +void CTxOrphanCache::EraseOrphans(const std::vector &txIds) +{ + LOCK(m_lock); + for (auto hashIter = txIds.begin(); hashIter != txIds.end(); ++hashIter) { + auto it = m_mapOrphanTransactions.find(*hashIter); + if (it == m_mapOrphanTransactions.end()) + continue; + + BOOST_FOREACH(const CTxIn& txin, it->second.tx.vin) { + auto itPrev = m_mapOrphanTransactionsByPrev.find(txin.prevout.hash); + if (itPrev == m_mapOrphanTransactionsByPrev.end()) + continue; + itPrev->second.erase(*hashIter); + if (itPrev->second.empty()) + m_mapOrphanTransactionsByPrev.erase(itPrev); + } + m_mapOrphanTransactions.erase(it); + } +} diff --git a/src/txorphancache.h b/src/txorphancache.h new file mode 100644 index 0000000000000..95f14ae6f6fa8 --- /dev/null +++ b/src/txorphancache.h @@ -0,0 +1,57 @@ +#ifndef TXORPHANCACHE_H +#define TXORPHANCACHE_H + +#include "sync.h" +#include "primitives/transaction.h" + +class CTxOrphanCache +{ +public: + CTxOrphanCache(); + static CTxOrphanCache *instance(); + + struct COrphanTx { + CTransaction tx; + int fromPeer; + uint64_t nEntryTime; + }; + bool AddOrphanTx(const CTransaction& tx, int peerId); + + void EraseOrphansFor(int peerId); + + void EraseOrphansByTime(); + + std::uint32_t LimitOrphanTxSize(); + + inline const std::map & mapOrphanTransactions() const { + return m_mapOrphanTransactions; + } + + static void clear(); + static bool value(const uint256 &txid, CTransaction &output); + static bool contains(const uint256 &txid); + + std::vector fetchTransactionIds() const; + + void setLimit(std::uint32_t limit); + + std::vector fetchTransactionsByPrev(const uint256 &txid) const; + + void EraseOrphans(const std::vector &txIds); + +protected: + mutable CCriticalSection m_lock; + std::map m_mapOrphanTransactions; + std::map > m_mapOrphanTransactionsByPrev; + + static CTxOrphanCache *s_instance; + + // this one doesn't lock! + void EraseOrphanTx(uint256 hash); + uint32_t LimitOrphanTxSize(uint32_t nMaxOrphans); + +private: + std::uint32_t m_limit; +}; + +#endif // TXORPHANCACHE_H