forked from dashpay/dash
-
Notifications
You must be signed in to change notification settings - Fork 725
[net_processing] Improve/upgrade orphan transactions handling #2301
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
random-zebra
merged 14 commits into
PIVX-Project:master
from
furszy:2021_upgrade_orphan_tx_handling
Apr 16, 2021
Merged
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
d7461b8
Track orphan by prev COutPoint rather than prev hash
sipa 59ff590
Adds an expiration time for orphan tx.
gmaxwell a065252
Treat orphans as implicit inv for parents, discard when parents rejec…
gmaxwell b5e650e
Increase maximum orphan size to 100,000 bytes.
gmaxwell 408a51a
Align struct COrphan definition
sipa 71fd4b2
Remove mapOrphanTransactionsByPrev from DoS_tests
sipa f048869
net_processing: Add missing erase orphan transactions when a block is…
furszy ba63837
net_processing: Do not process single zc transactions messages anymore.
furszy 75cd6eb
net_processing: Remove single zc transactions message ATMP call.
furszy 3b7c9e5
Create new mutex for orphans, no cs_main in PLV::BlockConnected
TheBlueMatt d6e3ebe
tests: Add missing locking annotations and locks
practicalswift 488a0e3
Select orphan transaction uniformly for eviction
sipa 9ec918f
Deduplicate missing parents of orphan transactions
sdaftuar 57221af
net_processing: Orphans map contemplating large shield transactions.
furszy File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,15 +22,28 @@ int64_t nTimeBestReceived = 0; // Used only to inform the wallet of when we las | |
|
|
||
| static const uint64_t RANDOMIZER_ID_ADDRESS_RELAY = 0x3cac0035b5866b90ULL; // SHA256("main address relay")[0:8] | ||
|
|
||
| struct IteratorComparator | ||
| { | ||
| template<typename I> | ||
| bool operator()(const I& a, const I& b) const | ||
| { | ||
| return &(*a) < &(*b); | ||
| } | ||
| }; | ||
|
|
||
| struct COrphanTx { | ||
| // When modifying, adapt the copy of this definition in tests/DoS_tests. | ||
| CTransactionRef tx; | ||
| NodeId fromPeer; | ||
| int64_t nTimeExpire; | ||
| size_t list_pos; | ||
| }; | ||
| RecursiveMutex g_cs_orphans; | ||
| std::map<uint256, COrphanTx> mapOrphanTransactions GUARDED_BY(g_cs_orphans); | ||
| std::map<COutPoint, std::set<std::map<uint256, COrphanTx>::iterator, IteratorComparator>> mapOrphanTransactionsByPrev GUARDED_BY(g_cs_orphans); | ||
| std::vector<std::map<uint256, COrphanTx>::iterator> g_orphan_list GUARDED_BY(g_cs_orphans); //! For random eviction | ||
|
|
||
| std::map<uint256, COrphanTx> mapOrphanTransactions GUARDED_BY(cs_main); | ||
| std::map<uint256, std::set<uint256> > mapOrphanTransactionsByPrev GUARDED_BY(cs_main); | ||
|
|
||
| void EraseOrphansFor(NodeId peer) EXCLUSIVE_LOCKS_REQUIRED(cs_main); | ||
| void EraseOrphansFor(NodeId peer); | ||
|
|
||
| // Internal stuff | ||
| namespace { | ||
|
|
@@ -490,7 +503,7 @@ void UnregisterNodeSignals(CNodeSignals& nodeSignals) | |
| // mapOrphanTransactions | ||
| // | ||
|
|
||
| bool AddOrphanTx(const CTransactionRef& tx, NodeId peer) EXCLUSIVE_LOCKS_REQUIRED(cs_main) | ||
| bool AddOrphanTx(const CTransactionRef& tx, NodeId peer) EXCLUSIVE_LOCKS_REQUIRED(g_cs_orphans) | ||
| { | ||
| const uint256& hash = tx->GetHash(); | ||
| if (mapOrphanTransactions.count(hash)) | ||
|
|
@@ -501,66 +514,100 @@ bool AddOrphanTx(const CTransactionRef& tx, NodeId peer) EXCLUSIVE_LOCKS_REQUIRE | |
| // 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: | ||
| // 25 orphans, each of which is at most 400,000 bytes big is | ||
| // at most 10 megabytes of orphans and somewhat more byprev index (in the worst case): | ||
| unsigned int sz = tx->GetTotalSize(); | ||
| if (sz > 5000) { | ||
| unsigned int nMaxSize = tx->IsShieldedTx() ? MAX_TX_SIZE_AFTER_SAPLING : MAX_STANDARD_TX_SIZE; | ||
| if (sz >= nMaxSize) { | ||
| LogPrint(BCLog::MEMPOOL, "ignoring large orphan tx (size: %u, hash: %s)\n", sz, hash.ToString()); | ||
| return false; | ||
| } | ||
|
|
||
| auto ret = mapOrphanTransactions.emplace(hash, COrphanTx{tx, peer}); | ||
| auto ret = mapOrphanTransactions.emplace(hash, COrphanTx{tx, peer, GetTime() + ORPHAN_TX_EXPIRE_TIME, g_orphan_list.size()}); | ||
| assert(ret.second); | ||
| for (const CTxIn& txin : tx->vin) | ||
| mapOrphanTransactionsByPrev[txin.prevout.hash].insert(hash); | ||
| g_orphan_list.emplace_back(ret.first); | ||
| for (const CTxIn& txin : tx->vin) { | ||
| mapOrphanTransactionsByPrev[txin.prevout].insert(ret.first); | ||
| } | ||
|
|
||
| LogPrint(BCLog::MEMPOOL, "stored orphan tx %s (mapsz %u prevsz %u)\n", hash.ToString(), | ||
| LogPrint(BCLog::MEMPOOL, "stored orphan tx %s (mapsz %u outsz %u)\n", hash.ToString(), | ||
| mapOrphanTransactions.size(), mapOrphanTransactionsByPrev.size()); | ||
| return true; | ||
| } | ||
|
|
||
| void static EraseOrphanTx(uint256 hash) EXCLUSIVE_LOCKS_REQUIRED(cs_main) | ||
| int static EraseOrphanTx(uint256 hash) EXCLUSIVE_LOCKS_REQUIRED(g_cs_orphans) | ||
| { | ||
| std::map<uint256, COrphanTx>::iterator it = mapOrphanTransactions.find(hash); | ||
| if (it == mapOrphanTransactions.end()) | ||
| return; | ||
| return 0; | ||
| for (const CTxIn& txin : it->second.tx->vin) { | ||
| std::map<uint256, std::set<uint256> >::iterator itPrev = mapOrphanTransactionsByPrev.find(txin.prevout.hash); | ||
| auto itPrev = mapOrphanTransactionsByPrev.find(txin.prevout); | ||
| if (itPrev == mapOrphanTransactionsByPrev.end()) | ||
| continue; | ||
| itPrev->second.erase(hash); | ||
| itPrev->second.erase(it); | ||
| if (itPrev->second.empty()) | ||
| mapOrphanTransactionsByPrev.erase(itPrev); | ||
| } | ||
|
|
||
| size_t old_pos = it->second.list_pos; | ||
| assert(g_orphan_list[old_pos] == it); | ||
| if (old_pos + 1 != g_orphan_list.size()) { | ||
| // Unless we're deleting the last entry in g_orphan_list, move the last | ||
| // entry to the position we're deleting. | ||
| auto it_last = g_orphan_list.back(); | ||
| g_orphan_list[old_pos] = it_last; | ||
| it_last->second.list_pos = old_pos; | ||
| } | ||
| g_orphan_list.pop_back(); | ||
|
|
||
| mapOrphanTransactions.erase(it); | ||
| return 1; | ||
| } | ||
|
|
||
| void EraseOrphansFor(NodeId peer) EXCLUSIVE_LOCKS_REQUIRED(cs_main) | ||
| void EraseOrphansFor(NodeId peer) | ||
| { | ||
| LOCK(g_cs_orphans); | ||
| int nErased = 0; | ||
| std::map<uint256, COrphanTx>::iterator iter = mapOrphanTransactions.begin(); | ||
| while (iter != mapOrphanTransactions.end()) { | ||
| std::map<uint256, COrphanTx>::iterator maybeErase = iter++; // increment to avoid iterator becoming invalid | ||
| if (maybeErase->second.fromPeer == peer) { | ||
| EraseOrphanTx(maybeErase->second.tx->GetHash()); | ||
| ++nErased; | ||
| nErased += EraseOrphanTx(maybeErase->second.tx->GetHash()); | ||
| } | ||
| } | ||
| if (nErased > 0) LogPrint(BCLog::MEMPOOL, "Erased %d orphan tx from peer %d\n", nErased, peer); | ||
| } | ||
|
|
||
|
|
||
| unsigned int LimitOrphanTxSize(unsigned int nMaxOrphans) EXCLUSIVE_LOCKS_REQUIRED(cs_main) | ||
| unsigned int LimitOrphanTxSize(unsigned int nMaxOrphans) | ||
| { | ||
| LOCK(g_cs_orphans); | ||
|
|
||
| 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; | ||
| auto iter = mapOrphanTransactions.begin(); | ||
| while (iter != mapOrphanTransactions.end()) { | ||
| auto 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(BCLog::MEMPOOL, "Erased %d orphan tx due to expiration\n", nErased); | ||
| } | ||
| FastRandomContext rng; | ||
| while (mapOrphanTransactions.size() > nMaxOrphans) { | ||
| // Evict a random orphan: | ||
| uint256 randomhash = rng.rand256(); | ||
| std::map<uint256, COrphanTx>::iterator it = mapOrphanTransactions.lower_bound(randomhash); | ||
| if (it == mapOrphanTransactions.end()) | ||
| it = mapOrphanTransactions.begin(); | ||
| EraseOrphanTx(it->first); | ||
| size_t randompos = rng.randrange(g_orphan_list.size()); | ||
| EraseOrphanTx(g_orphan_list[randompos]->first); | ||
| ++nEvicted; | ||
| } | ||
| return nEvicted; | ||
|
|
@@ -601,6 +648,37 @@ PeerLogicValidation::PeerLogicValidation(CConnman* connmanIn) : | |
| recentRejects.reset(new CRollingBloomFilter(120000, 0.000001)); | ||
| } | ||
|
|
||
| void PeerLogicValidation::BlockConnected(const std::shared_ptr<const CBlock>& pblock, const CBlockIndex* pindex) | ||
| { | ||
| LOCK(g_cs_orphans); | ||
|
|
||
| std::vector<uint256> vOrphanErase; | ||
|
|
||
| for (const CTransactionRef& ptx : pblock->vtx) { | ||
| const CTransaction& tx = *ptx; | ||
|
|
||
| // 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.emplace_back(orphanHash); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Erase orphan transactions include or precluded by this block | ||
| if (!vOrphanErase.empty()) { | ||
| int nErased = 0; | ||
| for (uint256& orphanHash : vOrphanErase) { | ||
| nErased += EraseOrphanTx(orphanHash); | ||
| } | ||
| LogPrint(BCLog::MEMPOOL, "Erased %d orphan tx included or conflicted by block\n", nErased); | ||
| } | ||
| } | ||
|
|
||
| void PeerLogicValidation::UpdatedBlockTip(const CBlockIndex* pindexNew, const CBlockIndex* pindexFork, bool fInitialDownload) | ||
| { | ||
| const int nNewHeight = pindexNew->nHeight; | ||
|
|
@@ -661,10 +739,13 @@ bool static AlreadyHave(const CInv& inv) EXCLUSIVE_LOCKS_REQUIRED(cs_main) | |
| recentRejects->reset(); | ||
| } | ||
|
|
||
| { | ||
| LOCK(g_cs_orphans); | ||
| if (mapOrphanTransactions.count(inv.hash)) return true; | ||
| } | ||
|
|
||
| return recentRejects->contains(inv.hash) || | ||
| mempool.exists(inv.hash) || | ||
| mapOrphanTransactions.count(inv.hash) || | ||
| pcoinsTip->HaveCoinInCache(COutPoint(inv.hash, 0)) || // Best effort: only try output 0 and 1 | ||
| pcoinsTip->HaveCoinInCache(COutPoint(inv.hash, 1)); | ||
| } | ||
|
|
@@ -1413,59 +1494,69 @@ bool static ProcessMessage(CNode* pfrom, std::string strCommand, CDataStream& vR | |
|
|
||
|
|
||
| else if (strCommand == NetMsgType::TX) { | ||
| std::vector<uint256> vWorkQueue; | ||
| std::deque<COutPoint> vWorkQueue; | ||
| std::vector<uint256> vEraseQueue; | ||
| CTransaction tx(deserialize, vRecv); | ||
| CTransactionRef ptx = MakeTransactionRef(tx); | ||
|
|
||
| CInv inv(MSG_TX, tx.GetHash()); | ||
| pfrom->AddInventoryKnown(inv); | ||
|
|
||
| LOCK(cs_main); | ||
| LOCK2(cs_main, g_cs_orphans); | ||
|
|
||
| bool ignoreFees = false; | ||
| bool fMissingInputs = false; | ||
| bool fMissingZerocoinInputs = false; | ||
| CValidationState state; | ||
|
|
||
| mapAlreadyAskedFor.erase(inv); | ||
|
|
||
| if (!tx.HasZerocoinSpendInputs() && AcceptToMemoryPool(mempool, state, ptx, true, &fMissingInputs, false, ignoreFees)) { | ||
| if (ptx->ContainsZerocoins()) { | ||
| // Don't even try to check zerocoins at all. | ||
| Misbehaving(pfrom->GetId(), 100); | ||
| LogPrint(BCLog::NET, " misbehaving peer, received a zc transaction, peer: %s\n", pfrom->GetAddrName()); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: un-necessary whitespace in log output |
||
| } | ||
|
|
||
| if (AcceptToMemoryPool(mempool, state, ptx, true, &fMissingInputs, false, ignoreFees)) { | ||
| mempool.check(pcoinsTip); | ||
| RelayTransaction(tx, connman); | ||
| vWorkQueue.push_back(inv.hash); | ||
| for (unsigned int i = 0; i < tx.vout.size(); i++) { | ||
| vWorkQueue.emplace_back(inv.hash, i); | ||
| } | ||
|
|
||
| LogPrint(BCLog::MEMPOOL, "%s : peer=%d %s : accepted %s (poolsz %u txn, %u kB)\n", | ||
| __func__, pfrom->id, pfrom->cleanSubVer, tx.GetHash().ToString(), | ||
| mempool.size(), mempool.DynamicMemoryUsage() / 1000); | ||
|
|
||
| // Recursively process any orphan transactions that depended on this one | ||
| std::set<NodeId> setMisbehaving; | ||
| for(unsigned int i = 0; i < vWorkQueue.size(); i++) { | ||
| std::map<uint256, std::set<uint256> >::iterator itByPrev = mapOrphanTransactionsByPrev.find(vWorkQueue[i]); | ||
| while (!vWorkQueue.empty()) { | ||
| auto itByPrev = mapOrphanTransactionsByPrev.find(vWorkQueue.front()); | ||
| vWorkQueue.pop_front(); | ||
| if(itByPrev == mapOrphanTransactionsByPrev.end()) | ||
| continue; | ||
| for(std::set<uint256>::iterator mi = itByPrev->second.begin(); | ||
| for (auto mi = itByPrev->second.begin(); | ||
| mi != itByPrev->second.end(); | ||
| ++mi) { | ||
| const uint256 &orphanHash = *mi; | ||
| const auto &orphanTx = mapOrphanTransactions[orphanHash].tx; | ||
| NodeId fromPeer = mapOrphanTransactions[orphanHash].fromPeer; | ||
| const CTransactionRef& orphanTx = (*mi)->second.tx; | ||
| const uint256& orphanHash = orphanTx->GetHash(); | ||
| NodeId fromPeer = (*mi)->second.fromPeer; | ||
| 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(fromPeer)) | ||
| continue; | ||
| if(AcceptToMemoryPool(mempool, stateDummy, orphanTx, true, &fMissingInputs2)) { | ||
| if (AcceptToMemoryPool(mempool, stateDummy, orphanTx, true, &fMissingInputs2)) { | ||
| LogPrint(BCLog::MEMPOOL, " accepted orphan tx %s\n", orphanHash.ToString()); | ||
| RelayTransaction(*orphanTx, connman); | ||
| vWorkQueue.push_back(orphanHash); | ||
| for (unsigned int i = 0; i < orphanTx->vout.size(); i++) { | ||
| vWorkQueue.emplace_back(orphanHash, i); | ||
| } | ||
| vEraseQueue.push_back(orphanHash); | ||
| } else if(!fMissingInputs2) { | ||
| } else if (!fMissingInputs2) { | ||
| int nDos = 0; | ||
| if(stateDummy.IsInvalid(nDos) && nDos > 0) { | ||
| // Punish peer that gave us an invalid orphan tx | ||
|
|
@@ -1486,22 +1577,41 @@ bool static ProcessMessage(CNode* pfrom, std::string strCommand, CDataStream& vR | |
|
|
||
| for (uint256& hash : vEraseQueue) EraseOrphanTx(hash); | ||
|
|
||
| } else if (tx.HasZerocoinSpendInputs() && AcceptToMemoryPool(mempool, state, ptx, true, &fMissingZerocoinInputs, false, false, ignoreFees)) { | ||
| //Presstab: ZCoin has a bunch of code commented out here. Is this something that should have more going on? | ||
| //Also there is nothing that handles fMissingZerocoinInputs. Does there need to be? | ||
| RelayTransaction(tx, connman); | ||
| LogPrint(BCLog::MEMPOOL, "AcceptToMemoryPool: Zerocoinspend peer=%d %s : accepted %s (poolsz %u)\n", | ||
| pfrom->id, pfrom->cleanSubVer, | ||
| tx.GetHash().ToString(), | ||
| mempool.mapTx.size()); | ||
| } else if (fMissingInputs) { | ||
| AddOrphanTx(ptx, pfrom->GetId()); | ||
| bool fRejectedParents = false; // It may be the case that the orphans parents have all been rejected | ||
|
|
||
| // Deduplicate parent txids, so that we don't have to loop over | ||
| // the same parent txid more than once down below. | ||
| std::vector<uint256> unique_parents; | ||
| unique_parents.reserve(tx.vin.size()); | ||
| for (const CTxIn& txin : ptx->vin) { | ||
| // We start with all parents, and then remove duplicates below. | ||
| unique_parents.emplace_back(txin.prevout.hash); | ||
| } | ||
| std::sort(unique_parents.begin(), unique_parents.end()); | ||
| unique_parents.erase(std::unique(unique_parents.begin(), unique_parents.end()), unique_parents.end()); | ||
| for (const uint256& parent_txid : unique_parents) { | ||
| if (recentRejects->contains(parent_txid)) { | ||
| fRejectedParents = true; | ||
| break; | ||
| } | ||
| } | ||
| if (!fRejectedParents) { | ||
| for (const uint256& parent_txid : unique_parents) { | ||
| CInv _inv(MSG_TX, parent_txid); | ||
| pfrom->AddInventoryKnown(_inv); | ||
| if (!AlreadyHave(_inv)) pfrom->AskFor(_inv); | ||
| } | ||
| AddOrphanTx(ptx, pfrom->GetId()); | ||
|
|
||
| // DoS prevention: do not allow mapOrphanTransactions to grow unbounded | ||
| unsigned int nMaxOrphanTx = (unsigned int)std::max((int64_t)0, gArgs.GetArg("-maxorphantx", DEFAULT_MAX_ORPHAN_TRANSACTIONS)); | ||
| unsigned int nEvicted = LimitOrphanTxSize(nMaxOrphanTx); | ||
| if (nEvicted > 0) | ||
| LogPrint(BCLog::MEMPOOL, "mapOrphan overflow, removed %u tx\n", nEvicted); | ||
| // DoS prevention: do not allow mapOrphanTransactions to grow unbounded | ||
| unsigned int nMaxOrphanTx = (unsigned int)std::max((int64_t)0, gArgs.GetArg("-maxorphantx", DEFAULT_MAX_ORPHAN_TRANSACTIONS)); | ||
| unsigned int nEvicted = LimitOrphanTxSize(nMaxOrphanTx); | ||
| if (nEvicted > 0) | ||
| LogPrint(BCLog::MEMPOOL, "mapOrphan overflow, removed %u tx\n", nEvicted); | ||
| } else { | ||
| LogPrint(BCLog::MEMPOOL, "not keeping orphan with rejected parents %s\n",tx.GetHash().ToString()); | ||
| } | ||
| } else { | ||
| // AcceptToMemoryPool() returned false, possibly because the tx is | ||
| // already in the mempool; if the tx isn't in the mempool that | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.