Skip to content

Commit 71f1903

Browse files
committed
Store disconnected block transactions outside mempool during reorg
Rather than re-add disconnected block transactions back to the mempool immediately, store them in a separate disconnectpool for later processing, because we expect most such transactions to reappear in the chain that is still to be connected (and thus we can avoid the work of reprocessing those transactions through the mempool altogether).
1 parent 9decd64 commit 71f1903

File tree

3 files changed

+195
-31
lines changed

3 files changed

+195
-31
lines changed

src/txmempool.h

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
#include "boost/multi_index_container.hpp"
2424
#include "boost/multi_index/ordered_index.hpp"
2525
#include "boost/multi_index/hashed_index.hpp"
26+
#include <boost/multi_index/sequenced_index.hpp>
2627

2728
#include <boost/signals2/signal.hpp>
2829

@@ -191,14 +192,19 @@ struct update_lock_points
191192
const LockPoints& lp;
192193
};
193194

194-
// extracts a TxMemPoolEntry's transaction hash
195+
// extracts a transaction hash from CTxMempoolEntry or CTransactionRef
195196
struct mempoolentry_txid
196197
{
197198
typedef uint256 result_type;
198199
result_type operator() (const CTxMemPoolEntry &entry) const
199200
{
200201
return entry.GetTx().GetHash();
201202
}
203+
204+
result_type operator() (const CTransactionRef& tx) const
205+
{
206+
return tx->GetHash();
207+
}
202208
};
203209

204210
/** \class CompareTxMemPoolEntryByDescendantScore
@@ -676,4 +682,95 @@ class CCoinsViewMemPool : public CCoinsViewBacked
676682
bool HaveCoins(const uint256 &txid) const;
677683
};
678684

685+
/**
686+
* DisconnectedBlockTransactions
687+
688+
* During the reorg, it's desirable to re-add previously confirmed transactions
689+
* to the mempool, so that anything not re-confirmed in the new chain is
690+
* available to be mined. However, it's more efficient to wait until the reorg
691+
* is complete and process all still-unconfirmed transactions at that time,
692+
* since we expect most confirmed transactions to (typically) still be
693+
* confirmed in the new chain, and re-accepting to the memory pool is expensive
694+
* (and therefore better to not do in the middle of reorg-processing).
695+
* Instead, store the disconnected transactions (in order!) as we go, remove any
696+
* that are included in blocks in the new chain, and then process the remaining
697+
* still-unconfirmed transactions at the end.
698+
*/
699+
700+
// multi_index tag names
701+
struct txid_index {};
702+
struct insertion_order {};
703+
704+
struct DisconnectedBlockTransactions {
705+
typedef boost::multi_index_container<
706+
CTransactionRef,
707+
boost::multi_index::indexed_by<
708+
// sorted by txid
709+
boost::multi_index::hashed_unique<
710+
boost::multi_index::tag<txid_index>,
711+
mempoolentry_txid,
712+
SaltedTxidHasher
713+
>,
714+
// sorted by order in the blockchain
715+
boost::multi_index::sequenced<
716+
boost::multi_index::tag<insertion_order>
717+
>
718+
>
719+
> indexed_disconnected_transactions;
720+
721+
// It's almost certainly a logic bug if we don't clear out queuedTx before
722+
// destruction, as we add to it while disconnecting blocks, and then we
723+
// need to re-process remaining transactions to ensure mempool consistency.
724+
// For now, assert() that we've emptied out this object on destruction.
725+
// This assert() can always be removed if the reorg-processing code were
726+
// to be refactored such that this assumption is no longer true (for
727+
// instance if there was some other way we cleaned up the mempool after a
728+
// reorg, besides draining this object).
729+
~DisconnectedBlockTransactions() { assert(queuedTx.empty()); }
730+
731+
indexed_disconnected_transactions queuedTx;
732+
uint64_t cachedInnerUsage = 0;
733+
734+
// Estimate the overhead of queuedTx to be 6 pointers + an allocation, as
735+
// no exact formula for boost::multi_index_contained is implemented.
736+
size_t DynamicMemoryUsage() const {
737+
return memusage::MallocUsage(sizeof(CTransactionRef) + 6 * sizeof(void*)) * queuedTx.size() + cachedInnerUsage;
738+
}
739+
740+
void addTransaction(const CTransactionRef& tx)
741+
{
742+
queuedTx.insert(tx);
743+
cachedInnerUsage += RecursiveDynamicUsage(*tx) + memusage::DynamicUsage(tx);
744+
}
745+
746+
// Remove entries based on txid_index, and update memory usage.
747+
void removeForBlock(const std::vector<CTransactionRef>& vtx)
748+
{
749+
// Short-circuit in the common case of a block being added to the tip
750+
if (queuedTx.empty()) {
751+
return;
752+
}
753+
for (auto const &tx : vtx) {
754+
auto it = queuedTx.find(tx->GetHash());
755+
if (it != queuedTx.end()) {
756+
cachedInnerUsage -= RecursiveDynamicUsage(**it) + memusage::DynamicUsage(*it);
757+
queuedTx.erase(it);
758+
}
759+
}
760+
}
761+
762+
// Remove an entry by insertion_order index, and update memory usage.
763+
void removeEntry(indexed_disconnected_transactions::index<insertion_order>::type::iterator entry)
764+
{
765+
cachedInnerUsage -= RecursiveDynamicUsage(**entry) + memusage::DynamicUsage(*entry);
766+
queuedTx.get<insertion_order>().erase(entry);
767+
}
768+
769+
void clear()
770+
{
771+
cachedInnerUsage = 0;
772+
queuedTx.clear();
773+
}
774+
};
775+
679776
#endif // BITCOIN_TXMEMPOOL_H

src/validation.cpp

Lines changed: 95 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -539,6 +539,56 @@ static bool IsCurrentForFeeEstimation()
539539
return true;
540540
}
541541

542+
/* Make mempool consistent after a reorg, by re-adding or recursively erasing
543+
* disconnected block transactions from the mempool, and also removing any
544+
* other transactions from the mempool that are no longer valid given the new
545+
* tip/height.
546+
*
547+
* Note: we assume that disconnectpool only contains transactions that are NOT
548+
* confirmed in the current chain nor already in the mempool (otherwise,
549+
* in-mempool descendants of such transactions would be removed).
550+
*
551+
* Passing fAddToMempool=false will skip trying to add the transactions back,
552+
* and instead just erase from the mempool as needed.
553+
*/
554+
555+
void UpdateMempoolForReorg(DisconnectedBlockTransactions &disconnectpool, bool fAddToMempool)
556+
{
557+
AssertLockHeld(cs_main);
558+
std::vector<uint256> vHashUpdate;
559+
// disconnectpool's insertion_order index sorts the entries from
560+
// oldest to newest, but the oldest entry will be the last tx from the
561+
// latest mined block that was disconnected.
562+
// Iterate disconnectpool in reverse, so that we add transactions
563+
// back to the mempool starting with the earliest transaction that had
564+
// been previously seen in a block.
565+
auto it = disconnectpool.queuedTx.get<insertion_order>().rbegin();
566+
while (it != disconnectpool.queuedTx.get<insertion_order>().rend()) {
567+
// ignore validation errors in resurrected transactions
568+
CValidationState stateDummy;
569+
if (!fAddToMempool || (*it)->IsCoinBase() || !AcceptToMemoryPool(mempool, stateDummy, *it, false, NULL, NULL, true)) {
570+
// If the transaction doesn't make it in to the mempool, remove any
571+
// transactions that depend on it (which would now be orphans).
572+
mempool.removeRecursive(**it, MemPoolRemovalReason::REORG);
573+
} else if (mempool.exists((*it)->GetHash())) {
574+
vHashUpdate.push_back((*it)->GetHash());
575+
}
576+
++it;
577+
}
578+
disconnectpool.queuedTx.clear();
579+
// AcceptToMemoryPool/addUnchecked all assume that new mempool entries have
580+
// no in-mempool children, which is generally not true when adding
581+
// previously-confirmed transactions back to the mempool.
582+
// UpdateTransactionsFromBlock finds descendants of any transactions in
583+
// the disconnectpool that were added back and cleans up the mempool state.
584+
mempool.UpdateTransactionsFromBlock(vHashUpdate);
585+
586+
// We also need to remove any now-immature transactions
587+
mempool.removeForReorg(pcoinsTip, chainActive.Tip()->nHeight + 1, STANDARD_LOCKTIME_VERIFY_FLAGS);
588+
// Re-limit mempool size, in case we added any transactions
589+
LimitMempoolSize(mempool, GetArg("-maxmempool", DEFAULT_MAX_MEMPOOL_SIZE) * 1000000, GetArg("-mempoolexpiry", DEFAULT_MEMPOOL_EXPIRY) * 60 * 60);
590+
}
591+
542592
bool AcceptToMemoryPoolWorker(CTxMemPool& pool, CValidationState& state, const CTransactionRef& ptx, bool fLimitFree,
543593
bool* pfMissingInputs, int64_t nAcceptTime, std::list<CTransactionRef>* plTxnReplaced,
544594
bool fOverrideMempoolLimit, const CAmount& nAbsurdFee, std::vector<uint256>& vHashTxnToUncache)
@@ -2119,8 +2169,17 @@ void static UpdateTip(CBlockIndex *pindexNew, const CChainParams& chainParams) {
21192169

21202170
}
21212171

2122-
/** Disconnect chainActive's tip. You probably want to call mempool.removeForReorg and manually re-limit mempool size after this, with cs_main held. */
2123-
bool static DisconnectTip(CValidationState& state, const CChainParams& chainparams, bool fBare = false)
2172+
/** Disconnect chainActive's tip.
2173+
* After calling, the mempool will be in an inconsistent state, with
2174+
* transactions from disconnected blocks being added to disconnectpool. You
2175+
* should make the mempool consistent again by calling UpdateMempoolForReorg.
2176+
* with cs_main held.
2177+
*
2178+
* If disconnectpool is NULL, then no disconnected transactions are added to
2179+
* disconnectpool (note that the caller is responsible for mempool consistency
2180+
* in any case).
2181+
*/
2182+
bool static DisconnectTip(CValidationState& state, const CChainParams& chainparams, DisconnectedBlockTransactions *disconnectpool)
21242183
{
21252184
CBlockIndex *pindexDelete = chainActive.Tip();
21262185
assert(pindexDelete);
@@ -2143,25 +2202,17 @@ bool static DisconnectTip(CValidationState& state, const CChainParams& chainpara
21432202
if (!FlushStateToDisk(state, FLUSH_STATE_IF_NEEDED))
21442203
return false;
21452204

2146-
if (!fBare) {
2147-
// Resurrect mempool transactions from the disconnected block.
2148-
std::vector<uint256> vHashUpdate;
2149-
for (const auto& it : block.vtx) {
2150-
const CTransaction& tx = *it;
2151-
// ignore validation errors in resurrected transactions
2152-
CValidationState stateDummy;
2153-
if (tx.IsCoinBase() || !AcceptToMemoryPool(mempool, stateDummy, it, false, NULL, NULL, true)) {
2154-
mempool.removeRecursive(tx, MemPoolRemovalReason::REORG);
2155-
} else if (mempool.exists(tx.GetHash())) {
2156-
vHashUpdate.push_back(tx.GetHash());
2157-
}
2205+
if (disconnectpool) {
2206+
// Save transactions to re-add to mempool at end of reorg
2207+
for (auto it = block.vtx.rbegin(); it != block.vtx.rend(); ++it) {
2208+
disconnectpool->addTransaction(*it);
2209+
}
2210+
while (disconnectpool->DynamicMemoryUsage() > MAX_DISCONNECTED_TX_POOL_SIZE * 1000) {
2211+
// Drop the earliest entry, and remove its children from the mempool.
2212+
auto it = disconnectpool->queuedTx.get<insertion_order>().begin();
2213+
mempool.removeRecursive(**it, MemPoolRemovalReason::REORG);
2214+
disconnectpool->removeEntry(it);
21582215
}
2159-
// AcceptToMemoryPool/addUnchecked all assume that new mempool entries have
2160-
// no in-mempool children, which is generally not true when adding
2161-
// previously-confirmed transactions back to the mempool.
2162-
// UpdateTransactionsFromBlock finds descendants of any transactions in this
2163-
// block that were added back and cleans up the mempool state.
2164-
mempool.UpdateTransactionsFromBlock(vHashUpdate);
21652216
}
21662217

21672218
// Update chainActive and related variables.
@@ -2249,7 +2300,7 @@ class ConnectTrace {
22492300
*
22502301
* The block is added to connectTrace if connection succeeds.
22512302
*/
2252-
bool static ConnectTip(CValidationState& state, const CChainParams& chainparams, CBlockIndex* pindexNew, const std::shared_ptr<const CBlock>& pblock, ConnectTrace& connectTrace)
2303+
bool static ConnectTip(CValidationState& state, const CChainParams& chainparams, CBlockIndex* pindexNew, const std::shared_ptr<const CBlock>& pblock, ConnectTrace& connectTrace, DisconnectedBlockTransactions &disconnectpool)
22532304
{
22542305
assert(pindexNew->pprev == chainActive.Tip());
22552306
// Read block from disk.
@@ -2291,6 +2342,7 @@ bool static ConnectTip(CValidationState& state, const CChainParams& chainparams,
22912342
LogPrint(BCLog::BENCH, " - Writing chainstate: %.2fms [%.2fs]\n", (nTime5 - nTime4) * 0.001, nTimeChainState * 0.000001);
22922343
// Remove conflicting transactions from the mempool.;
22932344
mempool.removeForBlock(blockConnecting.vtx, pindexNew->nHeight);
2345+
disconnectpool.removeForBlock(blockConnecting.vtx);
22942346
// Update chainActive & related variables.
22952347
UpdateTip(pindexNew, chainparams);
22962348

@@ -2384,9 +2436,14 @@ static bool ActivateBestChainStep(CValidationState& state, const CChainParams& c
23842436

23852437
// Disconnect active blocks which are no longer in the best chain.
23862438
bool fBlocksDisconnected = false;
2439+
DisconnectedBlockTransactions disconnectpool;
23872440
while (chainActive.Tip() && chainActive.Tip() != pindexFork) {
2388-
if (!DisconnectTip(state, chainparams))
2441+
if (!DisconnectTip(state, chainparams, &disconnectpool)) {
2442+
// This is likely a fatal error, but keep the mempool consistent,
2443+
// just in case. Only remove from the mempool in this case.
2444+
UpdateMempoolForReorg(disconnectpool, false);
23892445
return false;
2446+
}
23902447
fBlocksDisconnected = true;
23912448
}
23922449

@@ -2409,7 +2466,7 @@ static bool ActivateBestChainStep(CValidationState& state, const CChainParams& c
24092466

24102467
// Connect new blocks.
24112468
BOOST_REVERSE_FOREACH(CBlockIndex *pindexConnect, vpindexToConnect) {
2412-
if (!ConnectTip(state, chainparams, pindexConnect, pindexConnect == pindexMostWork ? pblock : std::shared_ptr<const CBlock>(), connectTrace)) {
2469+
if (!ConnectTip(state, chainparams, pindexConnect, pindexConnect == pindexMostWork ? pblock : std::shared_ptr<const CBlock>(), connectTrace, disconnectpool)) {
24132470
if (state.IsInvalid()) {
24142471
// The block violates a consensus rule.
24152472
if (!state.CorruptionPossible())
@@ -2420,6 +2477,9 @@ static bool ActivateBestChainStep(CValidationState& state, const CChainParams& c
24202477
break;
24212478
} else {
24222479
// A system error occurred (disk space, database error, ...).
2480+
// Make the mempool consistent with the current tip, just in case
2481+
// any observers try to use it before shutdown.
2482+
UpdateMempoolForReorg(disconnectpool, false);
24232483
return false;
24242484
}
24252485
} else {
@@ -2434,8 +2494,9 @@ static bool ActivateBestChainStep(CValidationState& state, const CChainParams& c
24342494
}
24352495

24362496
if (fBlocksDisconnected) {
2437-
mempool.removeForReorg(pcoinsTip, chainActive.Tip()->nHeight + 1, STANDARD_LOCKTIME_VERIFY_FLAGS);
2438-
LimitMempoolSize(mempool, GetArg("-maxmempool", DEFAULT_MAX_MEMPOOL_SIZE) * 1000000, GetArg("-mempoolexpiry", DEFAULT_MEMPOOL_EXPIRY) * 60 * 60);
2497+
// If any blocks were disconnected, disconnectpool may be non empty. Add
2498+
// any disconnected transactions back to the mempool.
2499+
UpdateMempoolForReorg(disconnectpool, true);
24392500
}
24402501
mempool.check(pcoinsTip);
24412502

@@ -2584,20 +2645,25 @@ bool InvalidateBlock(CValidationState& state, const CChainParams& chainparams, C
25842645
setDirtyBlockIndex.insert(pindex);
25852646
setBlockIndexCandidates.erase(pindex);
25862647

2648+
DisconnectedBlockTransactions disconnectpool;
25872649
while (chainActive.Contains(pindex)) {
25882650
CBlockIndex *pindexWalk = chainActive.Tip();
25892651
pindexWalk->nStatus |= BLOCK_FAILED_CHILD;
25902652
setDirtyBlockIndex.insert(pindexWalk);
25912653
setBlockIndexCandidates.erase(pindexWalk);
25922654
// ActivateBestChain considers blocks already in chainActive
25932655
// unconditionally valid already, so force disconnect away from it.
2594-
if (!DisconnectTip(state, chainparams)) {
2595-
mempool.removeForReorg(pcoinsTip, chainActive.Tip()->nHeight + 1, STANDARD_LOCKTIME_VERIFY_FLAGS);
2656+
if (!DisconnectTip(state, chainparams, &disconnectpool)) {
2657+
// It's probably hopeless to try to make the mempool consistent
2658+
// here if DisconnectTip failed, but we can try.
2659+
UpdateMempoolForReorg(disconnectpool, false);
25962660
return false;
25972661
}
25982662
}
25992663

2600-
LimitMempoolSize(mempool, GetArg("-maxmempool", DEFAULT_MAX_MEMPOOL_SIZE) * 1000000, GetArg("-mempoolexpiry", DEFAULT_MEMPOOL_EXPIRY) * 60 * 60);
2664+
// DisconnectTip will add transactions to disconnectpool; try to add these
2665+
// back to the mempool.
2666+
UpdateMempoolForReorg(disconnectpool, true);
26012667

26022668
// The resulting new best tip may not be in setBlockIndexCandidates anymore, so
26032669
// add it again.
@@ -2610,7 +2676,6 @@ bool InvalidateBlock(CValidationState& state, const CChainParams& chainparams, C
26102676
}
26112677

26122678
InvalidChainFound(pindex);
2613-
mempool.removeForReorg(pcoinsTip, chainActive.Tip()->nHeight + 1, STANDARD_LOCKTIME_VERIFY_FLAGS);
26142679
uiInterface.NotifyBlockTip(IsInitialBlockDownload(), pindex->pprev);
26152680
return true;
26162681
}
@@ -3724,7 +3789,7 @@ bool RewindBlockIndex(const CChainParams& params)
37243789
// of the blockchain).
37253790
break;
37263791
}
3727-
if (!DisconnectTip(state, params, true)) {
3792+
if (!DisconnectTip(state, params, NULL)) {
37283793
return error("RewindBlockIndex: unable to disconnect block at height %i", pindex->nHeight);
37293794
}
37303795
// Occasionally flush state to disk.

src/validation.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ static const unsigned int DEFAULT_DESCENDANT_LIMIT = 25;
6969
static const unsigned int DEFAULT_DESCENDANT_SIZE_LIMIT = 101;
7070
/** Default for -mempoolexpiry, expiration time for mempool transactions in hours */
7171
static const unsigned int DEFAULT_MEMPOOL_EXPIRY = 336;
72+
/** Maximum kilobytes for transactions to store for processing during reorg */
73+
static const unsigned int MAX_DISCONNECTED_TX_POOL_SIZE = 20000;
7274
/** The maximum size of a blk?????.dat file (since 0.8) */
7375
static const unsigned int MAX_BLOCKFILE_SIZE = 0x8000000; // 128 MiB
7476
/** The pre-allocation chunk size for blk?????.dat files (since 0.8) */

0 commit comments

Comments
 (0)