Skip to content
Merged
2 changes: 1 addition & 1 deletion src/rpc/rawtransaction.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -903,7 +903,7 @@ static RPCHelpMan testmempoolaccept()
RPCResult{
RPCResult::Type::ARR, "", "The result of the mempool acceptance test for each raw transaction in the input array.\n"
"Returns results for each transaction in the same order they were passed in.\n"
"It is possible for transactions to not be fully validated ('allowed' unset) if another transaction failed.\n",
"Transactions that cannot be fully validated due to failures in other transactions will not contain an 'allowed' result.\n",
{
{RPCResult::Type::OBJ, "", "",
{
Expand Down
116 changes: 87 additions & 29 deletions src/txmempool.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -151,33 +151,17 @@ void CTxMemPool::UpdateTransactionsFromBlock(const std::vector<uint256> &vHashes
}
}

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
bool CTxMemPool::CalculateAncestorsAndCheckLimits(size_t entry_size,
size_t entry_count,
setEntries& setAncestors,
CTxMemPoolEntry::Parents& staged_ancestors,
uint64_t limitAncestorCount,
uint64_t limitAncestorSize,
uint64_t limitDescendantCount,
uint64_t limitDescendantSize,
std::string &errString) const
{
CTxMemPoolEntry::Parents staged_ancestors;
const CTransaction &tx = entry.GetTx();

if (fSearchForParents) {
// Get parents of this transaction that are in the mempool
// GetMemPoolParents() is only valid for entries in the mempool, so we
// iterate mapTx to find parents.
for (unsigned int i = 0; i < tx.vin.size(); i++) {
std::optional<txiter> piter = GetIter(tx.vin[i].prevout.hash);
if (piter) {
staged_ancestors.insert(**piter);
if (staged_ancestors.size() + 1 > limitAncestorCount) {
errString = strprintf("too many unconfirmed parents [limit: %u]", limitAncestorCount);
return false;
}
}
}
} else {
// If we're not searching for parents, we require this to be an
// entry in the mempool already.
txiter it = mapTx.iterator_to(entry);
staged_ancestors = it->GetMemPoolParentsConst();
}

size_t totalSizeWithAncestors = entry.GetTxSize();
size_t totalSizeWithAncestors = entry_size;

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

if (stageit->GetSizeWithDescendants() + entry.GetTxSize() > limitDescendantSize) {
if (stageit->GetSizeWithDescendants() + entry_size > limitDescendantSize) {
errString = strprintf("exceeds descendant size limit for tx %s [limit: %u]", stageit->GetTx().GetHash().ToString(), limitDescendantSize);
return false;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this check doesn't have new coverage in `rpc_package.py ? I got a success for the following diff:

diff --git a/src/txmempool.cpp b/src/txmempool.cpp
index 85911e15d..c7fa9fc62 100644
--- a/src/txmempool.cpp
+++ b/src/txmempool.cpp
@@ -173,7 +173,7 @@ bool CTxMemPool::CalculateAncestorsAndCheckLimits(size_t entry_size,
 
         if (stageit->GetSizeWithDescendants() + entry_size > limitDescendantSize) {
             errString = strprintf("exceeds descendant size limit for tx %s [limit: %u]", stageit->GetTx().GetHash().ToString(), limitDescendantSize);
-            return false;
+            return true;
         } else if (stageit->GetCountWithDescendants() + entry_count > limitDescendantCount) {
             errString = strprintf("too many descendants for tx %s [limit: %u]", stageit->GetTx().GetHash().ToString(), limitDescendantCount);
             return false;

Copy link
Member Author

@glozow glozow Aug 5, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This error should be caught in the mempool_tests unit test?

It's true, though, that I didn't write tests for the size limits, just did count limits. I can add size limit tests.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a functional test for size limits - test_desc_size_limits() will fail if you apply this diff

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This error should be caught in the mempool_tests unit test?

It should but I don't' get an error for any mempool_* unit tests. Won't be surprised there is a hole coverage before to this PR.

Added a functional test for size limits - test_desc_size_limits() will fail if you apply this diff

Thanks for adding one, there is at least one behavior change to cover introduce with this PR, entry_size can be > 1.

} else if (stageit->GetCountWithDescendants() + 1 > limitDescendantCount) {
} else if (stageit->GetCountWithDescendants() + entry_count > limitDescendantCount) {
errString = strprintf("too many descendants for tx %s [limit: %u]", stageit->GetTx().GetHash().ToString(), limitDescendantCount);
return false;
} else if (totalSizeWithAncestors > limitAncestorSize) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we have topologies of in-mempool transactions and packages bypassing this limit.

Let's say you have in-mempool, independent Tx_A and Tx_B where their virtual sizes are 30 KvB each.
Let's say you have packages transactions Tx_C and _Tx_D where Tx_C is a child of A and B and Tx_D is a child of Tx_C their virtual sizes are 30 KvB each.

In CheckPackageLimits, Tx_C isn't already in the mempool (mapTx is only updated in Finalize) and as such shouldn't part of staged_ancestors. This set should be composed of Tx_A and Tx_B only.

In CalculateAncestorsAndCheckLimits, totalSizeWithAncestors is the union Tx_A and Tx_B and its sum of 60 KvB is inferior to 101 KvB. After Tx_C and Tx_D are added to the mempool, the chain of transactions ABCD is of the sum 120 KvB.

Further, I don't think it's rejected by the check against limitDescendantSize, as Tx_A and Tx_B are evaluated independently, and not as a single cluster as they should be (Tx_A + Tx_C + Tx_D or Tx_B + Tx_C + Tx_D)

Note, you need to replace Tx by chain of transactions because of MAX_STANDARD_TX_WEIGHT but I think this reasoning holds ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, I don't think this case bypasses the algorithm. I've implemented this test in the latest push (see test_anc_size_limits()) and it passes for me. It's possible I misunderstood your description, but it seems like we're good here?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for writing the test, I noticed where my reasoning was flawed!

totalSizeWithAncestors is the union Tx_A and Tx_B and its sum of 60 KvB is inferior to 101 KvB.

We init totalSizeWithAncestors with the whole package size from now, instead of the entry only (L164 in src/txmempool.cpp). So we have A+B+C+D and the check against limitAncestorSize rejects the package. Maybe the variable could be renamed packageSizeWithAncestors, but that's a nit.

Expand All @@ -206,7 +190,7 @@ bool CTxMemPool::CalculateMemPoolAncestors(const CTxMemPoolEntry &entry, setEntr
if (setAncestors.count(parent_it) == 0) {
staged_ancestors.insert(parent);
}
if (staged_ancestors.size() + setAncestors.size() + 1 > limitAncestorCount) {
if (staged_ancestors.size() + setAncestors.size() + entry_count > limitAncestorCount) {
errString = strprintf("too many unconfirmed ancestors [limit: %u]", limitAncestorCount);
return false;
}
Expand All @@ -216,6 +200,80 @@ bool CTxMemPool::CalculateMemPoolAncestors(const CTxMemPoolEntry &entry, setEntr
return true;
}

bool CTxMemPool::CheckPackageLimits(const Package& package,
uint64_t limitAncestorCount,
uint64_t limitAncestorSize,
uint64_t limitDescendantCount,
uint64_t limitDescendantSize,
std::string &errString) const
{
CTxMemPoolEntry::Parents staged_ancestors;
size_t total_size = 0;
for (const auto& tx : package) {
total_size += GetVirtualTransactionSize(*tx);
for (const auto& input : tx->vin) {
std::optional<txiter> piter = GetIter(input.prevout.hash);
if (piter) {
staged_ancestors.insert(**piter);
if (staged_ancestors.size() + package.size() > limitAncestorCount) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the future, if packages are allowed to replace in-mempool transactions do we have concerns of the same transaction accounted twice falsifying this check ?

Let's say you have in-mempool { Tx_A } and in-package { Tx_A, Tx_B } where Tx_A is parent of Tx_B. Within this configuration, Tx_A is going to be counted twice, firstly as a staged_ancestors member and secondly as a package member.

If this reasoning holds, one solution to be future-proof could be to proceed to the evaluation against limitAncestorCount after the for iteration, and once dedup between staged_ancestors and package has been done.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, this is absolutely a consideration for replacements and duplicates in packages in the future. I agree, we'll want to de-duplicate first (CMPA and CheckPackageLimits wouldn't be called for transactions already in the mempool, since PreChecks looks for txn-already-in-mempool before this). Additionally, we'll want to subtract the size/count of transactions being replaced from descendant limits before calling this function.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(I will note this down for the future - I think we're on the same page about this, and it's not yet applicable for this PR)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additionally, we'll want to subtract the size/count of transactions being replaced from descendant limits before calling this function.

Exactly, this is another limit case to be aware of. And even trickier one like an ancestor of a second-stage package member being replaced by a first-stage package member and thus failing the whole package acceptance.

Yes, not yet applicable for this PR, good to not forget about it!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think a simpler API would be to have staged always contain all the entries themselves? is there a reason not to (I think the epoch algorithm is relatively lightweight for this).

errString = strprintf("too many unconfirmed parents [limit: %u]", limitAncestorCount);
return false;
}
}
}
}
// When multiple transactions are passed in, the ancestors and descendants of all transactions
// considered together must be within limits even if they are not interdependent. This may be
// stricter than the limits for each individual transaction.
setEntries setAncestors;
const auto ret = CalculateAncestorsAndCheckLimits(total_size, package.size(),
setAncestors, staged_ancestors,
limitAncestorCount, limitAncestorSize,
limitDescendantCount, limitDescendantSize, errString);
// It's possible to overestimate the ancestor/descendant totals.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that this errString is only used for the PackageMempoolAcceptResult, I think you can just drop the "possibly" prefix. It may have been useful on individual MempoolAcceptResult to disambiguate between failure because a single transaction definitely exceeded the limits and failure because a transaction was in a package that possibly exceeded limits. Now that we're not using it there, I think it should just be removed.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In fact, I don't think this string ever gets used (in logging or returned to users). Should testmempoolaccept be updated to return the reject reason and debug message for the transaction result and package result (see ValidationState::ToString(), which indirectly gets called if sendrawtransaction fails).

if (!ret) errString.insert(0, "possibly ");
return ret;
}

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
{
CTxMemPoolEntry::Parents staged_ancestors;
const CTransaction &tx = entry.GetTx();

if (fSearchForParents) {
// Get parents of this transaction that are in the mempool
// GetMemPoolParents() is only valid for entries in the mempool, so we
// iterate mapTx to find parents.
for (unsigned int i = 0; i < tx.vin.size(); i++) {
std::optional<txiter> piter = GetIter(tx.vin[i].prevout.hash);
if (piter) {
staged_ancestors.insert(**piter);
if (staged_ancestors.size() + 1 > limitAncestorCount) {
errString = strprintf("too many unconfirmed parents [limit: %u]", limitAncestorCount);
return false;
}
}
}
} else {
// If we're not searching for parents, we require this to already be an
// entry in the mempool and use the entry's cached parents.
txiter it = mapTx.iterator_to(entry);
staged_ancestors = it->GetMemPoolParentsConst();
}

return CalculateAncestorsAndCheckLimits(entry.GetTxSize(), /* entry_count */ 1,
setAncestors, staged_ancestors,
limitAncestorCount, limitAncestorSize,
limitDescendantCount, limitDescendantSize, errString);
}

void CTxMemPool::UpdateAncestorsOf(bool add, txiter it, setEntries &setAncestors)
{
CTxMemPoolEntry::Parents parents = it->GetMemPoolParents();
Expand Down
42 changes: 42 additions & 0 deletions src/txmempool.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#include <coins.h>
#include <indirectmap.h>
#include <policy/feerate.h>
#include <policy/packages.h>
#include <primitives/transaction.h>
#include <random.h>
#include <sync.h>
Expand Down Expand Up @@ -585,6 +586,25 @@ class CTxMemPool
*/
std::set<uint256> m_unbroadcast_txids GUARDED_BY(cs);


/**
* Helper function to calculate all in-mempool ancestors of staged_ancestors and apply ancestor
* and descendant limits (including staged_ancestors thsemselves, entry_size and entry_count).
* param@[in] entry_size Virtual size to include in the limits.
* param@[in] entry_count How many entries to include in the limits.
* param@[in] staged_ancestors Should contain entries in the mempool.
* param@[out] setAncestors Will be populated with all mempool ancestors.
*/
bool CalculateAncestorsAndCheckLimits(size_t entry_size,
size_t entry_count,
setEntries& setAncestors,
CTxMemPoolEntry::Parents &staged_ancestors,
uint64_t limitAncestorCount,
uint64_t limitAncestorSize,
uint64_t limitDescendantCount,
uint64_t limitDescendantSize,
std::string &errString) const EXCLUSIVE_LOCKS_REQUIRED(cs);

public:
indirectmap<COutPoint, const CTransaction*> mapNextTx GUARDED_BY(cs);
std::map<uint256, CAmount> mapDeltas GUARDED_BY(cs);
Expand Down Expand Up @@ -681,6 +701,28 @@ class CTxMemPool
*/
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);

/** Calculate all in-mempool ancestors of a set of transactions not already in the mempool and
* check ancestor and descendant limits. Heuristics are used to estimate the ancestor and
* descendant count of all entries if the package were to be added to the mempool. The limits
* are applied to the union of all package transactions. For example, if the package has 3
* transactions and limitAncestorCount = 25, the union of all 3 sets of ancestors (including the
* transactions themselves) must be <= 22.
* @param[in] package Transaction package being evaluated for acceptance
* to mempool. The transactions need not be direct
* ancestors/descendants of each other.
* @param[in] limitAncestorCount Max number of txns including ancestors.
* @param[in] limitAncestorSize Max virtual size including ancestors.
* @param[in] limitDescendantCount Max number of txns including descendants.
* @param[in] limitDescendantSize Max virtual size including descendants.
* @param[out] errString Populated with error reason if a limit is hit.
*/
bool CheckPackageLimits(const Package& package,
uint64_t limitAncestorCount,
uint64_t limitAncestorSize,
uint64_t limitDescendantCount,
uint64_t limitDescendantSize,
std::string &errString) const EXCLUSIVE_LOCKS_REQUIRED(cs);

/** Populate setDescendants with all in-mempool descendants of hash.
* Assumes that setDescendants includes all in-mempool descendants of anything
* already in it. */
Expand Down
13 changes: 13 additions & 0 deletions src/validation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1079,6 +1079,19 @@ PackageMempoolAcceptResult MemPoolAccept::AcceptMultipleTransactions(const std::
m_viewmempool.PackageAddTransaction(ws.m_ptx);
}

// Apply package mempool ancestor/descendant limits. Skip if there is only one transaction,
// because it's unnecessary. Also, CPFP carve out can increase the limit for individual
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note, I still wonder if we need to make CPFP carve-out composable with package acceptance in the future. Otherwise a counterparty can build a branch of descendants from an output A on a multi-party unconfirmed ancestor to block package acceptance on an output B.

I don't think this is required for current LN safety, as least as long as implementation backends don't try to chain their commitment+CPFP transactions. But it might be useful for other second-layers applications (e.g Lightning Pool's batch execution+commitment txn).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we use packages to just remove the carve out?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally yes, carve-out don't scale for multi-party (n > 2) fee-bumping.

Though extending the carve-out to safely chain CPFPs was argued by Matt on the LDK side (see here https://lightningdevkit.slack.com/archives/CTBLT3CAU/p1625786787422100). Personally, I think this approach is a bit doomed as we can assume first-stage of the CPFP-chain can always be replaced by a valid claim of a counterparty, thus downgrading the feerate of your other broadcasted commitments and I prefer the concurrent domino fee-bumping approach even if it's more complexity swallowed by the LN backend.

Further, I think there is another multi-party contract issue we have to solve. Namely, the first-stage state transaction can be symmetric but the second-stage states asymmetric-though-composable. E.g a CTV tree with a root transaction with branch A/branch B spending 2 isolated outputs. Alice broadcast root+branch A and Bob broadcast root+branch B, thus accidentally or maliciously evicting Alice's branch. I think we would like the mempool logic to aggregate those in-flight packages by conserving the highest-feerate subgraphs and that would require some form of carve-out to avoid package limits interfering ?

// transactions, but this exemption is not extended to packages in CheckPackageLimits().
std::string err_string;
if (txns.size() > 1 &&
!m_pool.CheckPackageLimits(txns, m_limit_ancestors, m_limit_ancestor_size, m_limit_descendants,
m_limit_descendant_size, err_string)) {
// All transactions must have individually passed mempool ancestor and descendant limits
// inside of PreChecks(), so this is separate from an individual transaction error.
package_state.Invalid(PackageValidationResult::PCKG_POLICY, "package-mempool-limits", err_string);
return PackageMempoolAcceptResult(package_state, std::move(results));
}

for (Workspace& ws : workspaces) {
PrecomputedTransactionData txdata;
if (!PolicyScriptChecks(args, ws, txdata)) {
Expand Down
6 changes: 4 additions & 2 deletions src/validation.h
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,8 @@ struct PackageMempoolAcceptResult
/**
* Map from wtxid to finished MempoolAcceptResults. The client is responsible
* for keeping track of the transaction objects themselves. If a result is not
* present, it means validation was unfinished for that transaction.
* present, it means validation was unfinished for that transaction. If there
* was a package-wide error (see result in m_state), m_tx_results will be empty.
*/
std::map<const uint256, const MempoolAcceptResult> m_tx_results;

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