diff --git a/src/Makefile.am b/src/Makefile.am index 1d7004ac86..2bfaf4c44d 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -224,6 +224,7 @@ BITCOIN_CORE_H = \ node/validation_cache_args.h \ noui.h \ outputtype.h \ + policy/coin_age_priority.h \ policy/feerate.h \ policy/fees.h \ policy/fees_args.h \ @@ -414,6 +415,7 @@ libbitcoin_node_a_SOURCES = \ node/utxo_snapshot.cpp \ node/validation_cache_args.cpp \ noui.cpp \ + policy/coin_age_priority.cpp \ policy/fees.cpp \ policy/fees_args.cpp \ policy/packages.cpp \ diff --git a/src/bench/mempool_eviction.cpp b/src/bench/mempool_eviction.cpp index 735dc92dfb..6b22687667 100644 --- a/src/bench/mempool_eviction.cpp +++ b/src/bench/mempool_eviction.cpp @@ -12,13 +12,14 @@ static void AddTx(const CTransactionRef& tx, const CAmount& nFee, CTxMemPool& pool) EXCLUSIVE_LOCKS_REQUIRED(cs_main, pool.cs) { int64_t nTime = 0; + double dPriority = 10.0; unsigned int nHeight = 1; bool spendsCoinbase = false; unsigned int sigOpCost = 4; LockPoints lp; pool.addUnchecked(CTxMemPoolEntry( - tx, nFee, nTime, nHeight, - spendsCoinbase, sigOpCost, lp)); + tx, nFee, nTime, dPriority, nHeight, + tx->GetValueOut(), spendsCoinbase, sigOpCost, lp)); } // Right now this is only testing eviction performance in an extremely small diff --git a/src/bench/mempool_stress.cpp b/src/bench/mempool_stress.cpp index 80c959cdfb..5f73dc33a8 100644 --- a/src/bench/mempool_stress.cpp +++ b/src/bench/mempool_stress.cpp @@ -14,11 +14,12 @@ static void AddTx(const CTransactionRef& tx, CTxMemPool& pool) EXCLUSIVE_LOCKS_REQUIRED(cs_main, pool.cs) { int64_t nTime = 0; + const double coinage_priority = 10.0; unsigned int nHeight = 1; bool spendsCoinbase = false; unsigned int sigOpCost = 4; LockPoints lp; - pool.addUnchecked(CTxMemPoolEntry(tx, 1000, nTime, nHeight, spendsCoinbase, sigOpCost, lp)); + pool.addUnchecked(CTxMemPoolEntry(tx, 1000, nTime, coinage_priority, nHeight, tx->GetValueOut(), spendsCoinbase, sigOpCost, lp)); } struct Available { diff --git a/src/bench/rpc_mempool.cpp b/src/bench/rpc_mempool.cpp index e3e1a07c83..8cd98dc2b0 100644 --- a/src/bench/rpc_mempool.cpp +++ b/src/bench/rpc_mempool.cpp @@ -16,7 +16,7 @@ static void AddTx(const CTransactionRef& tx, const CAmount& fee, CTxMemPool& pool) EXCLUSIVE_LOCKS_REQUIRED(cs_main, pool.cs) { LockPoints lp; - pool.addUnchecked(CTxMemPoolEntry(tx, fee, /*time=*/0, /*entry_height=*/1, /*spends_coinbase=*/false, /*sigops_cost=*/4, lp)); + pool.addUnchecked(CTxMemPoolEntry(tx, fee, /*time=*/0, /*entry_priority=*/0, /*entry_height=*/0, /*in_chain_input_value=*/0, /*spends_coinbase=*/false, /*sigops_cost=*/4, lp)); } static void RpcMempool(benchmark::Bench& bench) diff --git a/src/init.cpp b/src/init.cpp index 5b3afb40cf..4c78dece91 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -571,7 +571,7 @@ void SetupServerArgs(ArgsManager& argsman) strprintf("Maximum tip age in seconds to consider node in initial block download (default: %u)", Ticks(DEFAULT_MAX_TIP_AGE)), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::DEBUG_TEST); - argsman.AddArg("-printpriority", strprintf("Log transaction fee rate in " + CURRENCY_UNIT + "/kvB when mining blocks (default: %u)", DEFAULT_PRINTPRIORITY), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::DEBUG_TEST); + argsman.AddArg("-printpriority", strprintf("Log transaction priority and fee rate in " + CURRENCY_UNIT + "/kvB when mining blocks (default: %u)", DEFAULT_PRINTPRIORITY), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::DEBUG_TEST); argsman.AddArg("-uaappend=", "Append literal to the user agent string (should only be used for software embedding)", ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION); argsman.AddArg("-uacomment=", "Append comment to the user agent string", ArgsManager::ALLOW_ANY, OptionsCategory::DEBUG_TEST); @@ -595,6 +595,7 @@ void SetupServerArgs(ArgsManager& argsman) argsman.AddArg("-blockmaxsize=", strprintf("Set maximum block size in bytes (default: %d)", DEFAULT_BLOCK_MAX_SIZE), ArgsManager::ALLOW_ANY, OptionsCategory::BLOCK_CREATION); argsman.AddArg("-blockmaxweight=", strprintf("Set maximum BIP141 block weight (default: %d)", DEFAULT_BLOCK_MAX_WEIGHT), ArgsManager::ALLOW_ANY, OptionsCategory::BLOCK_CREATION); argsman.AddArg("-blockmintxfee=", strprintf("Set lowest fee rate (in %s/kvB) for transactions to be included in block creation. (default: %s)", CURRENCY_UNIT, FormatMoney(DEFAULT_BLOCK_MIN_TX_FEE)), ArgsManager::ALLOW_ANY, OptionsCategory::BLOCK_CREATION); + argsman.AddArg("-blockprioritysize=", strprintf("Set maximum size of high-priority/low-fee transactions in bytes (default: %d)", DEFAULT_BLOCK_PRIORITY_SIZE), ArgsManager::ALLOW_ANY, OptionsCategory::BLOCK_CREATION); argsman.AddArg("-blockversion=", "Override block version to test forking scenarios", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::BLOCK_CREATION); argsman.AddArg("-rest", strprintf("Accept public REST requests (default: %u)", DEFAULT_REST_ENABLE), ArgsManager::ALLOW_ANY, OptionsCategory::RPC); diff --git a/src/kernel/mempool_entry.h b/src/kernel/mempool_entry.h index e1ba4296ef..5ed47d3b1d 100644 --- a/src/kernel/mempool_entry.h +++ b/src/kernel/mempool_entry.h @@ -8,12 +8,14 @@ #include #include #include +#include #include #include #include #include #include +#include #include #include #include @@ -78,9 +80,14 @@ private: const size_t nTxWeight; //!< ... and avoid recomputing tx weight (also used for GetTxSize()) const size_t nUsageSize; //!< ... and total memory usage const int64_t nTime; //!< Local time when entering the mempool + const double entryPriority; //!< Priority when entering the mempool const unsigned int entryHeight; //!< Chain height when entering the mempool + double cachedPriority; //!< Last calculated priority + unsigned int cachedHeight; //!< Height at which priority was last calculated + CAmount inChainInputValue; //!< Sum of all txin values that are already in blockchain const bool spendsCoinbase; //!< keep track of transactions that spend a coinbase const int64_t sigOpCost; //!< Total sigop cost + const size_t nModSize; //!< Cached modified size for priority CAmount m_modified_fee; //!< Used for determining the priority of the transaction for mining in a block LockPoints lockPoints; //!< Track the height and time at which tx was final @@ -99,27 +106,47 @@ private: public: CTxMemPoolEntry(const CTransactionRef& tx, CAmount fee, - int64_t time, unsigned int entry_height, - bool spends_coinbase, + int64_t time, double entry_priority, unsigned int entry_height, + CAmount in_chain_input_value, bool spends_coinbase, int64_t sigops_cost, LockPoints lp) : tx{tx}, nFee{fee}, nTxWeight(GetTransactionWeight(*tx)), nUsageSize{RecursiveDynamicUsage(tx)}, nTime{time}, + entryPriority{entry_priority}, entryHeight{entry_height}, + cachedPriority{entry_priority}, + // Since entries arrive *after* the tip's height, their entry priority is for the height+1 + cachedHeight{entry_height + 1}, + inChainInputValue{in_chain_input_value}, spendsCoinbase{spends_coinbase}, sigOpCost{sigops_cost}, + nModSize{CalculateModifiedSize(*tx, GetTxSize())}, m_modified_fee{nFee}, lockPoints{lp}, nSizeWithDescendants{GetTxSize()}, nModFeesWithDescendants{nFee}, nSizeWithAncestors{GetTxSize()}, nModFeesWithAncestors{nFee}, - nSigOpCostWithAncestors{sigOpCost} {} + nSigOpCostWithAncestors{sigOpCost} { + CAmount nValueIn = tx->GetValueOut() + nFee; + assert(inChainInputValue <= nValueIn); + } const CTransaction& GetTx() const { return *this->tx; } CTransactionRef GetSharedTx() const { return this->tx; } + double GetStartingPriority() const {return entryPriority; } + /** + * Fast calculation of priority as update from cached value, but only valid if + * currentHeight is greater than last height it was recalculated. + */ + double GetPriority(unsigned int currentHeight) const; + /** + * Recalculate the cached priority as of currentHeight and adjust inChainInputValue by + * valueInCurrentBlock which represents input that was just added to or removed from the blockchain. + */ + void UpdateCachedPriority(unsigned int currentHeight, CAmount valueInCurrentBlock); const CAmount& GetFee() const { return nFee; } size_t GetTxSize() const { diff --git a/src/node/interfaces.cpp b/src/node/interfaces.cpp index 50ed4c2ab8..46b6a03f96 100644 --- a/src/node/interfaces.cpp +++ b/src/node/interfaces.cpp @@ -672,7 +672,7 @@ public: { if (!m_node.mempool) return true; LockPoints lp; - CTxMemPoolEntry entry(tx, 0, 0, 0, false, 0, lp); + CTxMemPoolEntry entry(tx, 0, 0, 0, 0, 0, false, 0, lp); const CTxMemPool::Limits& limits{m_node.mempool->m_limits}; LOCK(m_node.mempool->cs); return m_node.mempool->CalculateMemPoolAncestors(entry, limits).has_value(); diff --git a/src/node/miner.cpp b/src/node/miner.cpp index a91e2cbe47..b38192e929 100644 --- a/src/node/miner.cpp +++ b/src/node/miner.cpp @@ -119,6 +119,9 @@ void BlockAssembler::resetBlock() // These counters do not include coinbase tx nBlockTx = 0; nFees = 0; + + lastFewTxs = 0; + blockFinished = false; } std::unique_ptr BlockAssembler::CreateNewBlock(const CScript& scriptPubKeyIn) @@ -138,6 +141,10 @@ std::unique_ptr BlockAssembler::CreateNewBlock(const CScript& sc pblock->vtx.emplace_back(); pblocktemplate->vTxFees.push_back(-1); // updated at end pblocktemplate->vTxSigOpsCost.push_back(-1); // updated at end + bool fPrintPriority = gArgs.GetBoolArg("-printpriority", DEFAULT_PRINTPRIORITY); + if (fPrintPriority) { + pblocktemplate->vTxPriorities.push_back(-1); // n/a + } LOCK(::cs_main); CBlockIndex* pindexPrev = m_chainstate.m_chain.Tip(); @@ -158,6 +165,7 @@ std::unique_ptr BlockAssembler::CreateNewBlock(const CScript& sc int nDescendantsUpdated = 0; if (m_mempool) { LOCK(m_mempool->cs); + addPriorityTxs(*m_mempool, nPackagesSelected); addPackageTxs(*m_mempool, nPackagesSelected, nDescendantsUpdated); } @@ -269,9 +277,12 @@ void BlockAssembler::AddToBlock(CTxMemPool::txiter iter) bool fPrintPriority = gArgs.GetBoolArg("-printpriority", DEFAULT_PRINTPRIORITY); if (fPrintPriority) { - LogPrintf("fee rate %s txid %s\n", + double dPriority = iter->GetPriority(nHeight); + LogPrintf("priority %.1f fee rate %s txid %s\n", + dPriority, CFeeRate(iter->GetModifiedFee(), iter->GetTxSize()).ToString(), iter->GetTx().GetHash().ToString()); + pblocktemplate->vTxPriorities.push_back(dPriority); } } @@ -336,6 +347,9 @@ void BlockAssembler::addPackageTxs(const CTxMemPool& mempool, int& nPackagesSele // Keep track of entries that failed inclusion, to avoid duplicate work CTxMemPool::setEntries failedTx; + // Start by adding all descendants of previously added txs to mapModifiedTx + // and modifying them for their already included ancestors + nDescendantsUpdated += UpdatePackagesForAdded(mempool, inBlock, mapModifiedTx); CTxMemPool::indexed_transaction_set::index::type::iterator mi = mempool.mapTx.get().begin(); CTxMemPool::txiter iter; diff --git a/src/node/miner.h b/src/node/miner.h index 9b2c4a3438..9170439983 100644 --- a/src/node/miner.h +++ b/src/node/miner.h @@ -33,6 +33,7 @@ struct CBlockTemplate CBlock block; std::vector vTxFees; std::vector vTxSigOpsCost; + std::vector vTxPriorities; std::vector vchCoinbaseCommitment; }; @@ -151,6 +152,10 @@ private: const CTxMemPool* const m_mempool; Chainstate& m_chainstate; + // Variables used for addPriorityTxs + int lastFewTxs; + bool blockFinished; + public: struct Options { // Configuration parameters for the block size @@ -181,11 +186,19 @@ private: void AddToBlock(CTxMemPool::txiter iter); // Methods for how to add transactions to a block. + /** Add transactions based on tx "priority" */ + void addPriorityTxs(const CTxMemPool& mempool, int &nPackagesSelected) EXCLUSIVE_LOCKS_REQUIRED(mempool.cs); /** Add transactions based on feerate including unconfirmed ancestors * Increments nPackagesSelected / nDescendantsUpdated with corresponding * statistics from the package selection (for logging statistics). */ void addPackageTxs(const CTxMemPool& mempool, int& nPackagesSelected, int& nDescendantsUpdated) EXCLUSIVE_LOCKS_REQUIRED(mempool.cs); + // helper function for addPriorityTxs + /** Test if tx will still "fit" in the block */ + bool TestForBlock(CTxMemPool::txiter iter); + /** Test if tx still has unconfirmed parents not yet in block */ + bool isStillDependent(const CTxMemPool& mempool, CTxMemPool::txiter iter) EXCLUSIVE_LOCKS_REQUIRED(mempool.cs); + // helper functions for addPackageTxs() /** Remove confirmed (inBlock) entries from given set */ void onlyUnconfirmed(CTxMemPool::setEntries& testSet); diff --git a/src/policy/coin_age_priority.cpp b/src/policy/coin_age_priority.cpp new file mode 100644 index 0000000000..565d9856e3 --- /dev/null +++ b/src/policy/coin_age_priority.cpp @@ -0,0 +1,253 @@ +// Copyright (c) 2012-2017 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +using node::BlockAssembler; + +unsigned int CalculateModifiedSize(const CTransaction& tx, unsigned int nTxSize) +{ + // In order to avoid disincentivizing cleaning up the UTXO set we don't count + // the constant overhead for each txin and up to 110 bytes of scriptSig (which + // is enough to cover a compressed pubkey p2sh redemption) for priority. + // Providing any more cleanup incentive than making additional inputs free would + // risk encouraging people to create junk outputs to redeem later. + if (nTxSize == 0) + nTxSize = (GetTransactionWeight(tx) + WITNESS_SCALE_FACTOR - 1) / WITNESS_SCALE_FACTOR; + for (std::vector::const_iterator it(tx.vin.begin()); it != tx.vin.end(); ++it) + { + unsigned int offset = 41U + std::min(110U, (unsigned int)it->scriptSig.size()); + if (nTxSize > offset) + nTxSize -= offset; + } + return nTxSize; +} + +double ComputePriority(const CTransaction& tx, double dPriorityInputs, unsigned int nTxSize) +{ + nTxSize = CalculateModifiedSize(tx, nTxSize); + if (nTxSize == 0) return 0.0; + + return dPriorityInputs / nTxSize; +} + +double GetPriority(const CTransaction &tx, const CCoinsViewCache& view, int nHeight, CAmount &inChainInputValue) +{ + inChainInputValue = 0; + if (tx.IsCoinBase()) + return 0.0; + double dResult = 0.0; + for (const CTxIn& txin : tx.vin) + { + const Coin& coin = view.AccessCoin(txin.prevout); + if (coin.IsSpent()) { + continue; + } + if (coin.nHeight <= nHeight) { + dResult += (double)(coin.out.nValue) * (nHeight - coin.nHeight); + inChainInputValue += coin.out.nValue; + } + } + return ComputePriority(tx, dResult); +} + +void CTxMemPoolEntry::UpdateCachedPriority(unsigned int currentHeight, CAmount valueInCurrentBlock) +{ + int heightDiff = int(currentHeight) - int(cachedHeight); + double deltaPriority = ((double)heightDiff*inChainInputValue)/nModSize; + cachedPriority += deltaPriority; + cachedHeight = currentHeight; + inChainInputValue += valueInCurrentBlock; + assert(MoneyRange(inChainInputValue)); +} + +struct update_priority +{ + update_priority(unsigned int _height, CAmount _value) : + height(_height), value(_value) + {} + + void operator() (CTxMemPoolEntry &e) + { e.UpdateCachedPriority(height, value); } + + private: + unsigned int height; + CAmount value; +}; + +void CTxMemPool::UpdateDependentPriorities(const CTransaction &tx, unsigned int nBlockHeight, bool addToChain) +{ + LOCK(cs); + for (unsigned int i = 0; i < tx.vout.size(); i++) { + auto it = mapNextTx.find(COutPoint(tx.GetHash(), i)); + if (it == mapNextTx.end()) + continue; + uint256 hash = it->second->GetHash(); + txiter iter = mapTx.find(hash); + mapTx.modify(iter, update_priority(nBlockHeight, addToChain ? tx.vout[i].nValue : -tx.vout[i].nValue)); + } +} + +double +CTxMemPoolEntry::GetPriority(unsigned int currentHeight) const +{ + // This will only return accurate results when currentHeight >= the heights + // at which all the in-chain inputs of the tx were included in blocks. + // Typical usage of GetPriority with chainActive.Height() will ensure this. + int heightDiff = currentHeight - cachedHeight; + double deltaPriority = ((double)heightDiff*inChainInputValue)/nModSize; + double dResult = cachedPriority + deltaPriority; + if (dResult < 0) // This should only happen if it was called with an invalid height + dResult = 0; + return dResult; +} + +// We want to sort transactions by coin age priority +typedef std::pair TxCoinAgePriority; + +struct TxCoinAgePriorityCompare +{ + bool operator()(const TxCoinAgePriority& a, const TxCoinAgePriority& b) + { + if (a.first == b.first) + return CompareTxMemPoolEntryByScore()(*(b.second), *(a.second)); //Reverse order to make sort less than + return a.first < b.first; + } +}; + +bool BlockAssembler::isStillDependent(const CTxMemPool& mempool, CTxMemPool::txiter iter) +{ + assert(iter != mempool.mapTx.end()); + for (const auto& parent : iter->GetMemPoolParentsConst()) { + auto parent_it = mempool.mapTx.iterator_to(parent); + if (!inBlock.count(parent_it)) { + return true; + } + } + return false; +} + +bool BlockAssembler::TestForBlock(CTxMemPool::txiter iter) +{ + uint64_t packageSize = iter->GetSizeWithAncestors(); + int64_t packageSigOps = iter->GetSigOpCostWithAncestors(); + if (!TestPackage(packageSize, packageSigOps)) { + // If the block is so close to full that no more txs will fit + // or if we've tried more than 50 times to fill remaining space + // then flag that the block is finished + if (nBlockWeight > m_options.nBlockMaxWeight - 400 || nBlockSigOpsCost > MAX_BLOCK_SIGOPS_COST - 8 || lastFewTxs > 50) { + blockFinished = true; + return false; + } + // Once we're within 4000 weight of a full block, only look at 50 more txs + // to try to fill the remaining space. + if (nBlockWeight > m_options.nBlockMaxWeight - 4000) { + ++lastFewTxs; + } + return false; + } + + CTxMemPool::setEntries package; + package.insert(iter); + if (!TestPackageTransactions(package)) { + if (nBlockSize > m_options.nBlockMaxSize - 100 || lastFewTxs > 50) { + blockFinished = true; + return false; + } + if (nBlockSize > m_options.nBlockMaxSize - 1000) { + ++lastFewTxs; + } + return false; + } + + return true; +} + +void BlockAssembler::addPriorityTxs(const CTxMemPool& mempool, int &nPackagesSelected) +{ + AssertLockHeld(mempool.cs); + + // How much of the block should be dedicated to high-priority transactions, + // included regardless of the fees they pay + uint64_t nBlockPrioritySize = gArgs.GetIntArg("-blockprioritysize", DEFAULT_BLOCK_PRIORITY_SIZE); + nBlockPrioritySize = std::min(m_options.nBlockMaxSize, nBlockPrioritySize); + + if (nBlockPrioritySize == 0) { + return; + } + + bool fSizeAccounting = fNeedSizeAccounting; + fNeedSizeAccounting = true; + + // This vector will be sorted into a priority queue: + std::vector vecPriority; + TxCoinAgePriorityCompare pricomparer; + std::map waitPriMap; + typedef std::map::iterator waitPriIter; + double actualPriority = -1; + + vecPriority.reserve(mempool.mapTx.size()); + for (auto mi = mempool.mapTx.begin(); mi != mempool.mapTx.end(); ++mi) { + double dPriority = mi->GetPriority(nHeight); + vecPriority.push_back(TxCoinAgePriority(dPriority, mi)); + } + std::make_heap(vecPriority.begin(), vecPriority.end(), pricomparer); + + CTxMemPool::txiter iter; + while (!vecPriority.empty() && !blockFinished) { // add a tx from priority queue to fill the blockprioritysize + iter = vecPriority.front().second; + actualPriority = vecPriority.front().first; + std::pop_heap(vecPriority.begin(), vecPriority.end(), pricomparer); + vecPriority.pop_back(); + + // If tx already in block, skip + if (inBlock.count(iter)) { + assert(false); // shouldn't happen for priority txs + continue; + } + + // If tx is dependent on other mempool txs which haven't yet been included + // then put it in the waitSet + if (isStillDependent(mempool, iter)) { + waitPriMap.insert(std::make_pair(iter, actualPriority)); + continue; + } + + // If this tx fits in the block add it, otherwise keep looping + if (TestForBlock(iter)) { + AddToBlock(iter); + + ++nPackagesSelected; + + // If now that this txs is added we've surpassed our desired priority size + // or have dropped below the minimum priority threshold, then we're done adding priority txs + if (nBlockSize >= nBlockPrioritySize || actualPriority <= MINIMUM_TX_PRIORITY) { + break; + } + + // This tx was successfully added, so + // add transactions that depend on this one to the priority queue to try again + for (const auto& child : iter->GetMemPoolChildrenConst()) + { + auto child_it = mempool.mapTx.iterator_to(child); + waitPriIter wpiter = waitPriMap.find(child_it); + if (wpiter != waitPriMap.end()) { + vecPriority.push_back(TxCoinAgePriority(wpiter->second, child_it)); + std::push_heap(vecPriority.begin(), vecPriority.end(), pricomparer); + waitPriMap.erase(wpiter); + } + } + } + } + fNeedSizeAccounting = fSizeAccounting; +} diff --git a/src/policy/coin_age_priority.h b/src/policy/coin_age_priority.h new file mode 100644 index 0000000000..ab2914f519 --- /dev/null +++ b/src/policy/coin_age_priority.h @@ -0,0 +1,26 @@ +// Copyright (c) 2012-2017 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_POLICY_COIN_AGE_PRIORITY_H +#define BITCOIN_POLICY_COIN_AGE_PRIORITY_H + +#include + +class CCoinsViewCache; +class CTransaction; + +// Compute modified tx size for priority calculation (optionally given tx size) +unsigned int CalculateModifiedSize(const CTransaction& tx, unsigned int nTxSize=0); + +// Compute priority, given sum coin-age of inputs and (optionally) tx size +double ComputePriority(const CTransaction& tx, double dPriorityInputs, unsigned int nTxSize=0); + +/** + * Return priority of tx at height nHeight. Also calculate the sum of the values of the inputs + * that are already in the chain. These are the inputs that will age and increase priority as + * new blocks are added to the chain. + */ +double GetPriority(const CTransaction &tx, const CCoinsViewCache& view, int nHeight, CAmount &inChainInputValue); + +#endif // BITCOIN_POLICY_COIN_AGE_PRIORITY_H diff --git a/src/policy/policy.h b/src/policy/policy.h index e3464bd84c..622a14eb02 100644 --- a/src/policy/policy.h +++ b/src/policy/policy.h @@ -21,6 +21,10 @@ class CScript; /** Default for -blockmaxsize, which controls the maximum size of block the mining code will create **/ static const unsigned int DEFAULT_BLOCK_MAX_SIZE = MAX_BLOCK_SERIALIZED_SIZE; +/** Default for -blockprioritysize, maximum space for zero/low-fee transactions **/ +static const unsigned int DEFAULT_BLOCK_PRIORITY_SIZE = 0; +/** Minimum priority for transactions to be accepted into the priority area **/ +static const double MINIMUM_TX_PRIORITY = COIN * 144 / 250; /** Default for -blockmaxweight, which controls the range of block weights the mining code will create **/ static constexpr unsigned int DEFAULT_BLOCK_MAX_WEIGHT{MAX_BLOCK_WEIGHT - 4000}; /** Default for -blockmintxfee, which sets the minimum feerate for a transaction in blocks created by mining code **/ diff --git a/src/test/fuzz/partially_downloaded_block.cpp b/src/test/fuzz/partially_downloaded_block.cpp index f8ba4f08d9..b959fde55b 100644 --- a/src/test/fuzz/partially_downloaded_block.cpp +++ b/src/test/fuzz/partially_downloaded_block.cpp @@ -72,7 +72,7 @@ FUZZ_TARGET_INIT(partially_downloaded_block, initialize_pdb) available.insert(i); } - if (add_to_mempool) { + if (add_to_mempool && SanityCheckForConsumeTxMemPoolEntry(*tx)) { LOCK2(cs_main, pool.cs); pool.addUnchecked(ConsumeTxMemPoolEntry(fuzzed_data_provider, *tx)); available.insert(i); diff --git a/src/test/fuzz/policy_estimator.cpp b/src/test/fuzz/policy_estimator.cpp index 116fbd9015..4d827bf882 100644 --- a/src/test/fuzz/policy_estimator.cpp +++ b/src/test/fuzz/policy_estimator.cpp @@ -41,6 +41,7 @@ FUZZ_TARGET_INIT(policy_estimator, initialize_policy_estimator) return; } const CTransaction tx{*mtx}; + if (!SanityCheckForConsumeTxMemPoolEntry(tx)) return; block_policy_estimator.processTransaction(ConsumeTxMemPoolEntry(fuzzed_data_provider, tx), fuzzed_data_provider.ConsumeBool()); if (fuzzed_data_provider.ConsumeBool()) { (void)block_policy_estimator.removeTx(tx.GetHash(), /*inBlock=*/fuzzed_data_provider.ConsumeBool()); @@ -54,6 +55,7 @@ FUZZ_TARGET_INIT(policy_estimator, initialize_policy_estimator) break; } const CTransaction tx{*mtx}; + if (!SanityCheckForConsumeTxMemPoolEntry(tx)) return; mempool_entries.push_back(ConsumeTxMemPoolEntry(fuzzed_data_provider, tx)); } std::vector ptrs; diff --git a/src/test/fuzz/rbf.cpp b/src/test/fuzz/rbf.cpp index 57a9a15a85..d173b84a3f 100644 --- a/src/test/fuzz/rbf.cpp +++ b/src/test/fuzz/rbf.cpp @@ -37,6 +37,8 @@ FUZZ_TARGET_INIT(rbf, initialize_rbf) if (!mtx) { return; } + const CTransaction tx{*mtx}; + if (!SanityCheckForConsumeTxMemPoolEntry(tx)) return; CTxMemPool pool{MemPoolOptionsForTest(g_setup->m_node)}; @@ -47,13 +49,13 @@ FUZZ_TARGET_INIT(rbf, initialize_rbf) break; } const CTransaction another_tx{*another_mtx}; + if (!SanityCheckForConsumeTxMemPoolEntry(another_tx)) break; if (fuzzed_data_provider.ConsumeBool() && !mtx->vin.empty()) { mtx->vin[0].prevout = COutPoint{another_tx.GetHash(), 0}; } LOCK2(cs_main, pool.cs); pool.addUnchecked(ConsumeTxMemPoolEntry(fuzzed_data_provider, another_tx)); } - const CTransaction tx{*mtx}; if (fuzzed_data_provider.ConsumeBool()) { LOCK2(cs_main, pool.cs); pool.addUnchecked(ConsumeTxMemPoolEntry(fuzzed_data_provider, tx)); diff --git a/src/test/fuzz/util/mempool.cpp b/src/test/fuzz/util/mempool.cpp index 4baca5ec77..5e823a1ac8 100644 --- a/src/test/fuzz/util/mempool.cpp +++ b/src/test/fuzz/util/mempool.cpp @@ -1,3 +1,4 @@ + // Copyright (c) 2022 The Bitcoin Core developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. @@ -14,6 +15,17 @@ #include #include +bool SanityCheckForConsumeTxMemPoolEntry(const CTransaction& tx) noexcept +{ + try { + (void)tx.GetValueOut(); + return true; + } catch (const std::runtime_error&) { + return false; + } +} + +// NOTE: Transaction must pass SanityCheckForConsumeTxMemPoolEntry first CTxMemPoolEntry ConsumeTxMemPoolEntry(FuzzedDataProvider& fuzzed_data_provider, const CTransaction& tx) noexcept { // Avoid: @@ -23,8 +35,9 @@ CTxMemPoolEntry ConsumeTxMemPoolEntry(FuzzedDataProvider& fuzzed_data_provider, const CAmount fee{ConsumeMoney(fuzzed_data_provider, /*max=*/std::numeric_limits::max() / CAmount{100'000})}; assert(MoneyRange(fee)); const int64_t time = fuzzed_data_provider.ConsumeIntegral(); - const unsigned int entry_height = fuzzed_data_provider.ConsumeIntegral(); + const double coinage_priority = fuzzed_data_provider.ConsumeFloatingPoint(); + const unsigned int entry_height = fuzzed_data_provider.ConsumeIntegralInRange(0, std::numeric_limits::max() - 1); const bool spends_coinbase = fuzzed_data_provider.ConsumeBool(); const unsigned int sig_op_cost = fuzzed_data_provider.ConsumeIntegralInRange(0, MAX_BLOCK_SIGOPS_COST); - return CTxMemPoolEntry{MakeTransactionRef(tx), fee, time, entry_height, spends_coinbase, sig_op_cost, {}}; + return CTxMemPoolEntry{MakeTransactionRef(tx), fee, time, coinage_priority, entry_height, tx.GetValueOut(), spends_coinbase, sig_op_cost, {}}; } diff --git a/src/test/fuzz/util/mempool.h b/src/test/fuzz/util/mempool.h index 31b578dc4b..740225a0ee 100644 --- a/src/test/fuzz/util/mempool.h +++ b/src/test/fuzz/util/mempool.h @@ -21,6 +21,7 @@ public: } }; +[[nodiscard]] bool SanityCheckForConsumeTxMemPoolEntry(const CTransaction& tx) noexcept; [[nodiscard]] CTxMemPoolEntry ConsumeTxMemPoolEntry(FuzzedDataProvider& fuzzed_data_provider, const CTransaction& tx) noexcept; #endif // BITCOIN_TEST_FUZZ_UTIL_MEMPOOL_H diff --git a/src/test/util/setup_common.cpp b/src/test/util/setup_common.cpp index 58593c9d5b..bf7dc24aa7 100644 --- a/src/test/util/setup_common.cpp +++ b/src/test/util/setup_common.cpp @@ -424,8 +424,8 @@ std::vector TestChain100Setup::PopulateMempool(FastRandomContex LOCK2(cs_main, m_node.mempool->cs); LockPoints lp; m_node.mempool->addUnchecked(CTxMemPoolEntry(ptx, /*fee=*/(total_in - num_outputs * amount_per_output), - /*time=*/0, /*entry_height=*/1, - /*spends_coinbase=*/false, /*sigops_cost=*/4, lp)); + /*time=*/0, /*entry_priority=*/0, /*entry_height=*/1, + /*in_chain_input_value=*/0, /*spends_coinbase=*/false, /*sigops_cost=*/4, lp)); } --num_transactions; } diff --git a/src/test/util/txmempool.cpp b/src/test/util/txmempool.cpp index 1873cf5ec8..57caf7cfb8 100644 --- a/src/test/util/txmempool.cpp +++ b/src/test/util/txmempool.cpp @@ -34,5 +34,7 @@ CTxMemPoolEntry TestMemPoolEntryHelper::FromTx(const CMutableTransaction& tx) co CTxMemPoolEntry TestMemPoolEntryHelper::FromTx(const CTransactionRef& tx) const { - return CTxMemPoolEntry{tx, nFee, TicksSinceEpoch(time), nHeight, spendsCoinbase, sigOpCost, lp}; + const double dPriority = 0; + const CAmount inChainValue = 0; + return CTxMemPoolEntry{tx, nFee, TicksSinceEpoch(time), dPriority, nHeight, inChainValue, spendsCoinbase, sigOpCost, lp}; } diff --git a/src/txmempool.cpp b/src/txmempool.cpp index ee2b4a9787..4e3001c7ed 100644 --- a/src/txmempool.cpp +++ b/src/txmempool.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -634,6 +635,7 @@ void CTxMemPool::removeForBlock(const std::vector& vtx, unsigne if (minerPolicyEstimator) {minerPolicyEstimator->processBlock(nBlockHeight, entries);} for (const auto& tx : vtx) { + UpdateDependentPriorities(*tx, nBlockHeight, true); txiter it = mapTx.find(tx->GetHash()); if (it != mapTx.end()) { setEntries stage; @@ -666,6 +668,13 @@ void CTxMemPool::check(const CCoinsViewCache& active_coins_tip, int64_t spendhei for (const auto& it : GetSortedDepthAndScore()) { checkTotal += it->GetTxSize(); + CAmount dummyValue; + double freshPriority = GetPriority(it->GetTx(), active_coins_tip, spendheight, dummyValue); + double cachePriority = it->GetPriority(spendheight); + double priDiff = cachePriority > freshPriority ? cachePriority - freshPriority : freshPriority - cachePriority; + // Verify that the difference between the on the fly calculation and a fresh calculation + // is small enough to be a result of double imprecision. + assert(priDiff < .0001 * freshPriority + 1); check_total_fee += it->GetFee(); innerUsage += it->DynamicMemoryUsage(); const CTransaction& tx = it->GetTx(); diff --git a/src/txmempool.h b/src/txmempool.h index 2f57c4c047..90643f2ecb 100644 --- a/src/txmempool.h +++ b/src/txmempool.h @@ -510,6 +510,12 @@ public: * the tx is not dependent on other mempool transactions to be included in a block. */ bool HasNoInputsOf(const CTransaction& tx) const EXCLUSIVE_LOCKS_REQUIRED(cs); + /** + * Update all transactions in the mempool which depend on tx to recalculate their priority + * and adjust the input value that will age to reflect that the inputs from this transaction have + * either just been added to the chain or just been removed. + */ + void UpdateDependentPriorities(const CTransaction &tx, unsigned int nBlockHeight, bool addToChain); /** Affect CreateNewBlock prioritisation of transactions */ void PrioritiseTransaction(const uint256& hash, const CAmount& nFeeDelta); diff --git a/src/validation.cpp b/src/validation.cpp index 1fd8f0e326..bbc4e4ed9c 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -826,6 +827,10 @@ bool MemPoolAccept::PreChecks(ATMPArgs& args, Workspace& ws) ws.m_modified_fees = ws.m_base_fees; m_pool.ApplyDelta(hash, ws.m_modified_fees); + CAmount inChainInputValue; + // Since entries arrive *after* the tip's height, their priority is for the height+1 + double dPriority = GetPriority(tx, m_view, m_active_chainstate.m_chain.Height() + 1, inChainInputValue); + // Keep track of transactions that spend a coinbase, which we re-scan // during reorgs to ensure COINBASE_MATURITY is still met. bool fSpendsCoinbase = false; @@ -837,8 +842,8 @@ bool MemPoolAccept::PreChecks(ATMPArgs& args, Workspace& ws) } } - entry.reset(new CTxMemPoolEntry(ptx, ws.m_base_fees, nAcceptTime, m_active_chainstate.m_chain.Height(), - fSpendsCoinbase, nSigOpsCost, lock_points.value())); + entry.reset(new CTxMemPoolEntry(ptx, ws.m_base_fees, nAcceptTime, dPriority, m_active_chainstate.m_chain.Height(), + inChainInputValue, fSpendsCoinbase, nSigOpsCost, lock_points.value())); ws.m_vsize = entry->GetTxSize(); if (nSigOpsCost > MAX_STANDARD_TX_SIGOPS_COST) @@ -2733,6 +2738,7 @@ bool Chainstate::DisconnectTip(BlockValidationState& state, DisconnectedBlockTra if (disconnectpool && m_mempool) { // Save transactions to re-add to mempool at end of reorg for (auto it = block.vtx.rbegin(); it != block.vtx.rend(); ++it) { + m_mempool->UpdateDependentPriorities(*(*it), pindexDelete->nHeight, false); disconnectpool->addTransaction(*it); } while (disconnectpool->DynamicMemoryUsage() > MAX_DISCONNECTED_TX_POOL_SIZE * 1000) {