Skip to content

validate package transactions with their in-package ancestor sets

This contains everything to make mempool/validation logic ready for package relay (see #27463).

Design goals:

  • Be able to gracefully deal with any arbitrary list of transactions (these come from p2p)
  • Validate any ancestor package so that the incentive-compatible transactions end up in our mempool.
    • If the transactions would be accepted individually, they should also be accepted through AcceptPackage. We don't want to accidentally reject things just because we happened to download them as a package.
    • Bug prior to these changes: we required IsChildWithParents but if there were dependencies between the parents, we could end up (1) accepting a low-feerate child or (2) rejecting a high-feerate parent CPFPing another parent. See the "interdependent parents" test case for a specific example.
  • Be DoS-resistant.
    • Avoid quadratic validation costs.
    • Avoid loading a lot of stuff from disk, or loading repeatedly.

There are 2 main improvements to package evaluation here: (1) We submit transactions with their in-package ancestor sets. - See unit test package_ppfp: without this change, we would reject everything. - See unit test package_ppfc: shows that this doesn't let us do "parent pays for child;" we only do this when the individual and ancestor feerates meet mempool minimum feerate (2) We linearize the package transactions based on ancestor set scoring. - See unit test package_needs_reorder: without this change, if we use the original order of transactions, we would only accept 1 grandparent, even if we submit subpackages. - See unit test package_desc_limits: without this change, we accept one of the lower-feerate transactions (a bit more of a "nice to have" than a "must have" example).

A description of the package validation logic (originally https://github.com/bitcoin/bitcoin/pull/26711#issuecomment-1647523520):

  • Basic sanitization. Quit if it's too big, inconsistent, etc.
  • Linearize (Topological sort only)
  • PreChecks loop For each tx, grab UTXOs to calculate fees and filter out anything we should skip:
    • If already in mempool (or same txid in mempool), mark as skip
    • If missing inputs or conflict, record this failure and mark this and all descendants as skip.
    • If no failures or TX_SINGLE_FAILURE, continue
    • For some failures that we expect due to differing chainstates, skip these transactions and their descendants, but continue.
    • Otherwise, record this failure and mark that we will quit_early (i.e. not do the Subpackage validation loop).
  • Refine our linearization using the fee information.
  • Subpackage validation loop For each transaction in the new linearized order:
    • Get the transaction's ancestor subpackage.
    • If the feerate of this transaction is insufficient, continue;
    • If the feerate of this subpackage is insufficient, continue;
    • Otherwise, try to submit the subpackage, using AcceptSingleTransaction() if it's just 1 tx
    • if at any point we get a non-fee-related error, abort all.
  • Call LimitMempoolSize
  • Backfill results:
    • If the transaction was in mempool, check to see if it's still there in case it was trimmed in LimitMempoolSize.
    • Try to use results from the subpackage validation loop.
    • If that doesn't exist (i.e. we quit early), use results from prechecks loop.
    • If that doesn't exist (i.e. we quit early and we hadn't found a failure with it yet), fill with TX_UNKNOWN.

This means we will call PreChecks for each transaction 2 times (fewer if we quit early), and run all other validation checks at most 1 time. A transaction shouldn't be validated in the subpackage validation loop more than once.

Merge request reports

Loading