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
IsChildWithParentsbut 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.
- If the transactions would be accepted individually, they should also be accepted through
- 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.
- If the transaction was in mempool, check to see if it's still there in case it was trimmed in
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.