Skip to content

Commit 21438d5

Browse files
committed
Merge bitcoin/bitcoin#21800: mempool/validation: mempool ancestor/descendant limits for packages
accf3d5 [test] mempool package ancestor/descendant limits (glozow) 2b6b26e [test] parameterizable fee for make_chain and create_child_with_parents (glozow) 313c09f [test] helper function to increase transaction weight (glozow) f8253d6 extract/rename helper functions from rpc_packages.py (glozow) 3cd663a [policy] ancestor/descendant limits for packages (glozow) c6e016a [mempool] check ancestor/descendant limits for packages (glozow) f551841 [refactor] pass size/count instead of entry to CalculateAncestorsAndCheckLimits (glozow) 97dd1c7 MOVEONLY: add helper function for calculating ancestors and checking limits (glozow) f95bbf5 misc package validation doc improvements (glozow) Pull request description: This PR implements a function to calculate mempool ancestors for a package and enforces ancestor/descendant limits on them as a whole. It reuses a portion of `CalculateMemPoolAncestors()`; there's also a small refactor to move the reused code into a generic helper function. Instead of calculating ancestors and descendants on every single transaction in the package and their ancestors, we use a "worst case" heuristic, treating every transaction in the package as each other's ancestor and descendant. This may overestimate everyone's counts, but is still pretty accurate in the our main package use cases, in which at least one of the transactions in the package is directly related to all the others (e.g. 1 parent + 1 child, multiple parents with 1 child, or chains). Note on Terminology: While "package" is often used to describe groups of related transactions _within_ the mempool, here, I only use package to mean the group of not-in-mempool transactions we are currently validating. #### Motivation It would be a potential DoS vector to allow submission of packages to mempool without a proper guard for mempool ancestors/descendants. In general, the purpose of mempool ancestor/descendant limits is to limit the computational complexity of dealing with families during removals and additions. We want to be able to validate multiple transactions on top of the mempool, but also avoid these scenarios: - We underestimate the ancestors/descendants during package validation and end up with extremely complex families in our mempool (potentially a DoS vector). - We expend an unreasonable amount of resources calculating everyone's ancestors and descendants during package validation. ACKs for top commit: JeremyRubin: utACK accf3d5 ariard: ACK accf3d5. Tree-SHA512: 0d18ce4b77398fe872e0b7c2cc66d3aac2135e561b64029584339e1f4de2a6a16ebab3dd5784f376e119cbafc4d50168b28d3bd95d0b3d01158714ade2e3624d
2 parents b1a672d + accf3d5 commit 21438d5

File tree

9 files changed

+709
-82
lines changed

9 files changed

+709
-82
lines changed

src/rpc/rawtransaction.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -903,7 +903,7 @@ static RPCHelpMan testmempoolaccept()
903903
RPCResult{
904904
RPCResult::Type::ARR, "", "The result of the mempool acceptance test for each raw transaction in the input array.\n"
905905
"Returns results for each transaction in the same order they were passed in.\n"
906-
"It is possible for transactions to not be fully validated ('allowed' unset) if another transaction failed.\n",
906+
"Transactions that cannot be fully validated due to failures in other transactions will not contain an 'allowed' result.\n",
907907
{
908908
{RPCResult::Type::OBJ, "", "",
909909
{

src/txmempool.cpp

Lines changed: 87 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -151,33 +151,17 @@ void CTxMemPool::UpdateTransactionsFromBlock(const std::vector<uint256> &vHashes
151151
}
152152
}
153153

154-
bool CTxMemPool::CalculateMemPoolAncestors(const CTxMemPoolEntry &entry, setEntries &setAncestors, uint64_t limitAncestorCount, uint64_t limitAncestorSize, uint64_t limitDescendantCount, uint64_t limitDescendantSize, std::string &errString, bool fSearchForParents /* = true */) const
154+
bool CTxMemPool::CalculateAncestorsAndCheckLimits(size_t entry_size,
155+
size_t entry_count,
156+
setEntries& setAncestors,
157+
CTxMemPoolEntry::Parents& staged_ancestors,
158+
uint64_t limitAncestorCount,
159+
uint64_t limitAncestorSize,
160+
uint64_t limitDescendantCount,
161+
uint64_t limitDescendantSize,
162+
std::string &errString) const
155163
{
156-
CTxMemPoolEntry::Parents staged_ancestors;
157-
const CTransaction &tx = entry.GetTx();
158-
159-
if (fSearchForParents) {
160-
// Get parents of this transaction that are in the mempool
161-
// GetMemPoolParents() is only valid for entries in the mempool, so we
162-
// iterate mapTx to find parents.
163-
for (unsigned int i = 0; i < tx.vin.size(); i++) {
164-
std::optional<txiter> piter = GetIter(tx.vin[i].prevout.hash);
165-
if (piter) {
166-
staged_ancestors.insert(**piter);
167-
if (staged_ancestors.size() + 1 > limitAncestorCount) {
168-
errString = strprintf("too many unconfirmed parents [limit: %u]", limitAncestorCount);
169-
return false;
170-
}
171-
}
172-
}
173-
} else {
174-
// If we're not searching for parents, we require this to be an
175-
// entry in the mempool already.
176-
txiter it = mapTx.iterator_to(entry);
177-
staged_ancestors = it->GetMemPoolParentsConst();
178-
}
179-
180-
size_t totalSizeWithAncestors = entry.GetTxSize();
164+
size_t totalSizeWithAncestors = entry_size;
181165

182166
while (!staged_ancestors.empty()) {
183167
const CTxMemPoolEntry& stage = staged_ancestors.begin()->get();
@@ -187,10 +171,10 @@ bool CTxMemPool::CalculateMemPoolAncestors(const CTxMemPoolEntry &entry, setEntr
187171
staged_ancestors.erase(stage);
188172
totalSizeWithAncestors += stageit->GetTxSize();
189173

190-
if (stageit->GetSizeWithDescendants() + entry.GetTxSize() > limitDescendantSize) {
174+
if (stageit->GetSizeWithDescendants() + entry_size > limitDescendantSize) {
191175
errString = strprintf("exceeds descendant size limit for tx %s [limit: %u]", stageit->GetTx().GetHash().ToString(), limitDescendantSize);
192176
return false;
193-
} else if (stageit->GetCountWithDescendants() + 1 > limitDescendantCount) {
177+
} else if (stageit->GetCountWithDescendants() + entry_count > limitDescendantCount) {
194178
errString = strprintf("too many descendants for tx %s [limit: %u]", stageit->GetTx().GetHash().ToString(), limitDescendantCount);
195179
return false;
196180
} else if (totalSizeWithAncestors > limitAncestorSize) {
@@ -206,7 +190,7 @@ bool CTxMemPool::CalculateMemPoolAncestors(const CTxMemPoolEntry &entry, setEntr
206190
if (setAncestors.count(parent_it) == 0) {
207191
staged_ancestors.insert(parent);
208192
}
209-
if (staged_ancestors.size() + setAncestors.size() + 1 > limitAncestorCount) {
193+
if (staged_ancestors.size() + setAncestors.size() + entry_count > limitAncestorCount) {
210194
errString = strprintf("too many unconfirmed ancestors [limit: %u]", limitAncestorCount);
211195
return false;
212196
}
@@ -216,6 +200,80 @@ bool CTxMemPool::CalculateMemPoolAncestors(const CTxMemPoolEntry &entry, setEntr
216200
return true;
217201
}
218202

203+
bool CTxMemPool::CheckPackageLimits(const Package& package,
204+
uint64_t limitAncestorCount,
205+
uint64_t limitAncestorSize,
206+
uint64_t limitDescendantCount,
207+
uint64_t limitDescendantSize,
208+
std::string &errString) const
209+
{
210+
CTxMemPoolEntry::Parents staged_ancestors;
211+
size_t total_size = 0;
212+
for (const auto& tx : package) {
213+
total_size += GetVirtualTransactionSize(*tx);
214+
for (const auto& input : tx->vin) {
215+
std::optional<txiter> piter = GetIter(input.prevout.hash);
216+
if (piter) {
217+
staged_ancestors.insert(**piter);
218+
if (staged_ancestors.size() + package.size() > limitAncestorCount) {
219+
errString = strprintf("too many unconfirmed parents [limit: %u]", limitAncestorCount);
220+
return false;
221+
}
222+
}
223+
}
224+
}
225+
// When multiple transactions are passed in, the ancestors and descendants of all transactions
226+
// considered together must be within limits even if they are not interdependent. This may be
227+
// stricter than the limits for each individual transaction.
228+
setEntries setAncestors;
229+
const auto ret = CalculateAncestorsAndCheckLimits(total_size, package.size(),
230+
setAncestors, staged_ancestors,
231+
limitAncestorCount, limitAncestorSize,
232+
limitDescendantCount, limitDescendantSize, errString);
233+
// It's possible to overestimate the ancestor/descendant totals.
234+
if (!ret) errString.insert(0, "possibly ");
235+
return ret;
236+
}
237+
238+
bool CTxMemPool::CalculateMemPoolAncestors(const CTxMemPoolEntry &entry,
239+
setEntries &setAncestors,
240+
uint64_t limitAncestorCount,
241+
uint64_t limitAncestorSize,
242+
uint64_t limitDescendantCount,
243+
uint64_t limitDescendantSize,
244+
std::string &errString,
245+
bool fSearchForParents /* = true */) const
246+
{
247+
CTxMemPoolEntry::Parents staged_ancestors;
248+
const CTransaction &tx = entry.GetTx();
249+
250+
if (fSearchForParents) {
251+
// Get parents of this transaction that are in the mempool
252+
// GetMemPoolParents() is only valid for entries in the mempool, so we
253+
// iterate mapTx to find parents.
254+
for (unsigned int i = 0; i < tx.vin.size(); i++) {
255+
std::optional<txiter> piter = GetIter(tx.vin[i].prevout.hash);
256+
if (piter) {
257+
staged_ancestors.insert(**piter);
258+
if (staged_ancestors.size() + 1 > limitAncestorCount) {
259+
errString = strprintf("too many unconfirmed parents [limit: %u]", limitAncestorCount);
260+
return false;
261+
}
262+
}
263+
}
264+
} else {
265+
// If we're not searching for parents, we require this to already be an
266+
// entry in the mempool and use the entry's cached parents.
267+
txiter it = mapTx.iterator_to(entry);
268+
staged_ancestors = it->GetMemPoolParentsConst();
269+
}
270+
271+
return CalculateAncestorsAndCheckLimits(entry.GetTxSize(), /* entry_count */ 1,
272+
setAncestors, staged_ancestors,
273+
limitAncestorCount, limitAncestorSize,
274+
limitDescendantCount, limitDescendantSize, errString);
275+
}
276+
219277
void CTxMemPool::UpdateAncestorsOf(bool add, txiter it, setEntries &setAncestors)
220278
{
221279
CTxMemPoolEntry::Parents parents = it->GetMemPoolParents();

src/txmempool.h

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
#include <coins.h>
1919
#include <indirectmap.h>
2020
#include <policy/feerate.h>
21+
#include <policy/packages.h>
2122
#include <primitives/transaction.h>
2223
#include <random.h>
2324
#include <sync.h>
@@ -585,6 +586,25 @@ class CTxMemPool
585586
*/
586587
std::set<uint256> m_unbroadcast_txids GUARDED_BY(cs);
587588

589+
590+
/**
591+
* Helper function to calculate all in-mempool ancestors of staged_ancestors and apply ancestor
592+
* and descendant limits (including staged_ancestors thsemselves, entry_size and entry_count).
593+
* param@[in] entry_size Virtual size to include in the limits.
594+
* param@[in] entry_count How many entries to include in the limits.
595+
* param@[in] staged_ancestors Should contain entries in the mempool.
596+
* param@[out] setAncestors Will be populated with all mempool ancestors.
597+
*/
598+
bool CalculateAncestorsAndCheckLimits(size_t entry_size,
599+
size_t entry_count,
600+
setEntries& setAncestors,
601+
CTxMemPoolEntry::Parents &staged_ancestors,
602+
uint64_t limitAncestorCount,
603+
uint64_t limitAncestorSize,
604+
uint64_t limitDescendantCount,
605+
uint64_t limitDescendantSize,
606+
std::string &errString) const EXCLUSIVE_LOCKS_REQUIRED(cs);
607+
588608
public:
589609
indirectmap<COutPoint, const CTransaction*> mapNextTx GUARDED_BY(cs);
590610
std::map<uint256, CAmount> mapDeltas GUARDED_BY(cs);
@@ -681,6 +701,28 @@ class CTxMemPool
681701
*/
682702
bool CalculateMemPoolAncestors(const CTxMemPoolEntry& entry, setEntries& setAncestors, uint64_t limitAncestorCount, uint64_t limitAncestorSize, uint64_t limitDescendantCount, uint64_t limitDescendantSize, std::string& errString, bool fSearchForParents = true) const EXCLUSIVE_LOCKS_REQUIRED(cs);
683703

704+
/** Calculate all in-mempool ancestors of a set of transactions not already in the mempool and
705+
* check ancestor and descendant limits. Heuristics are used to estimate the ancestor and
706+
* descendant count of all entries if the package were to be added to the mempool. The limits
707+
* are applied to the union of all package transactions. For example, if the package has 3
708+
* transactions and limitAncestorCount = 25, the union of all 3 sets of ancestors (including the
709+
* transactions themselves) must be <= 22.
710+
* @param[in] package Transaction package being evaluated for acceptance
711+
* to mempool. The transactions need not be direct
712+
* ancestors/descendants of each other.
713+
* @param[in] limitAncestorCount Max number of txns including ancestors.
714+
* @param[in] limitAncestorSize Max virtual size including ancestors.
715+
* @param[in] limitDescendantCount Max number of txns including descendants.
716+
* @param[in] limitDescendantSize Max virtual size including descendants.
717+
* @param[out] errString Populated with error reason if a limit is hit.
718+
*/
719+
bool CheckPackageLimits(const Package& package,
720+
uint64_t limitAncestorCount,
721+
uint64_t limitAncestorSize,
722+
uint64_t limitDescendantCount,
723+
uint64_t limitDescendantSize,
724+
std::string &errString) const EXCLUSIVE_LOCKS_REQUIRED(cs);
725+
684726
/** Populate setDescendants with all in-mempool descendants of hash.
685727
* Assumes that setDescendants includes all in-mempool descendants of anything
686728
* already in it. */

src/validation.cpp

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1079,6 +1079,19 @@ PackageMempoolAcceptResult MemPoolAccept::AcceptMultipleTransactions(const std::
10791079
m_viewmempool.PackageAddTransaction(ws.m_ptx);
10801080
}
10811081

1082+
// Apply package mempool ancestor/descendant limits. Skip if there is only one transaction,
1083+
// because it's unnecessary. Also, CPFP carve out can increase the limit for individual
1084+
// transactions, but this exemption is not extended to packages in CheckPackageLimits().
1085+
std::string err_string;
1086+
if (txns.size() > 1 &&
1087+
!m_pool.CheckPackageLimits(txns, m_limit_ancestors, m_limit_ancestor_size, m_limit_descendants,
1088+
m_limit_descendant_size, err_string)) {
1089+
// All transactions must have individually passed mempool ancestor and descendant limits
1090+
// inside of PreChecks(), so this is separate from an individual transaction error.
1091+
package_state.Invalid(PackageValidationResult::PCKG_POLICY, "package-mempool-limits", err_string);
1092+
return PackageMempoolAcceptResult(package_state, std::move(results));
1093+
}
1094+
10821095
for (Workspace& ws : workspaces) {
10831096
PrecomputedTransactionData txdata;
10841097
if (!PolicyScriptChecks(args, ws, txdata)) {

src/validation.h

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,8 @@ struct PackageMempoolAcceptResult
199199
/**
200200
* Map from wtxid to finished MempoolAcceptResults. The client is responsible
201201
* for keeping track of the transaction objects themselves. If a result is not
202-
* present, it means validation was unfinished for that transaction.
202+
* present, it means validation was unfinished for that transaction. If there
203+
* was a package-wide error (see result in m_state), m_tx_results will be empty.
203204
*/
204205
std::map<const uint256, const MempoolAcceptResult> m_tx_results;
205206

@@ -227,7 +228,8 @@ MempoolAcceptResult AcceptToMemoryPool(CChainState& active_chainstate, CTxMemPoo
227228
* @param[in] txns Group of transactions which may be independent or contain
228229
* parent-child dependencies. The transactions must not conflict
229230
* with each other, i.e., must not spend the same inputs. If any
230-
* dependencies exist, parents must appear before children.
231+
* dependencies exist, parents must appear anywhere in the list
232+
* before their children.
231233
* @returns a PackageMempoolAcceptResult which includes a MempoolAcceptResult for each transaction.
232234
* If a transaction fails, validation will exit early and some results may be missing.
233235
*/

0 commit comments

Comments
 (0)