Skip to content

Conversation

@glozow
Copy link
Member

@glozow glozow commented Aug 10, 2023

While we are evaluating a package, we split it into "subpackages" for evaluation (currently subpackages all have size 1 except the last one). If a subpackage has size 1, we may add a tx to mempool and call LimitMempoolSize(), which evicts transactions if the mempool gets full. We handle the case where the just-submitted transaction is evicted immediately, but we don't handle the case in which a transaction from a previous subpackage (either just submitted or already in mempool) is evicted. Mainly, since the coins created by the evicted transaction are cached in m_view, we don't realize the UTXO has disappeared until CheckInputsFromMempoolAndCache asserts that they exist. Also, the returned PackageMempoolAcceptResult reports that the transaction is in mempool even though it isn't anymore.

Fix this by not calling LimitMempoolSize() until the very end, and editing the results map with "mempool full" if things fall out.

Pointed out by instagibbs in faeed68 on top of the v3 PR.

@DrahtBot
Copy link
Contributor

DrahtBot commented Aug 10, 2023

The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

Reviews

See the guideline for information on the review process.

Type Reviewers
ACK instagibbs
Concept ACK fjahr

If your review is incorrectly listed, please react with 👎 to this comment and the bot will ignore it on the next update.

Conflicts

Reviewers, this pull request conflicts with the following ones:

  • #28453 (wallet: Receive silent payment transactions by achow101)
  • #28450 (WIP: Add package evaluation fuzzer by instagibbs)
  • #28368 (Fee Estimator updates from Validation Interface/CScheduler thread by ismaelsadeeq)

If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.

@glozow
Copy link
Member Author

glozow commented Aug 10, 2023

cc @instagibbs

@instagibbs
Copy link
Member

@sipa you might want to weigh in as well

Copy link
Member

@instagibbs instagibbs left a comment

Choose a reason for hiding this comment

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

conditional ACK c2c9dfe

on having a test case catching the crash included. Tested with my own test case in e3622f7

Copy link

@ariard ariard left a comment

Choose a reason for hiding this comment

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

Note the code path affected by the bug is under the submitpackage RPC, which is clearly indicated as an experimental RPC with an unstable interface.

More test coverage sounds good.

Copy link
Contributor

@fjahr fjahr left a comment

Choose a reason for hiding this comment

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

Concept ACK, will do a full review once the test has been added.

@glozow
Copy link
Member Author

glozow commented Aug 22, 2023

Pushed a test - it should fail on master with the assertion.

Generally, the conditions to hit the case are:

  • An almost-full mempool.
  • mempool_evicted_tx (parent) A to-be-evicted tx that is submitted ahead of time. Has a very low feerate and is evicted immediately when mempool overflows. It has an output coin_to_disappear
  • A package consisting of:
    • cpfp_parent (child_a) parent that spends coin_to_disappear and fails a check that requires its input to be loaded and makes it eligible for reconsideration (e.g. below mempool minimum feerate).
    • high-feerate parent (child_b), high feerate so that they are submitted individually and trigger eviction.
      • I've used 6 high-feerate parents in this test to make it easy to trigger overflow.
      • In the EA test, child_b triggers eviction due to proactive trimming of the sponsorless parent instead of mempool overflow.
    • package_child (child_c) spending the above parents.
      • The child must pay a fee high enough to CPFP the low-feerate parent.
    • Additional requirements:
      • The low-feerate parent needs to be checked before the higher-feerate parents so that its inputs are loaded before the eviction happens. Otherwise it will just hit missing-inputs. This also means that as a side effect of the fee-based linearization in validate package transactions with their in-package ancestor sets #26711 (which would put the low-feerate parent at the end), this test won't trigger. But I don't think the linearization is an adequate solution to this problem.
      • To make the test interesting, it should be low enough that the to-be-evicted tx is evicted regardless of when the eviction happens.

Timeline of what happens:

  1. mempool is almost full
  2. mempool_evicted_tx is submitted to mempool
  3. AcceptPackage(package):
    1. AcceptSingleTransaction(cpfp_parent). This loads coin_to_disappear into m_view . After the fee is checked, the tx fails and we continue onto the next transactions.
    2. for each of the n high-feerate parents, AcceptSingleTransaction(tx). These succeed and, in Finalize(), the transactions are submitted.
      1. (Without PR's changes) this also calls LimitMempool() which evicts by descendant score until the mempool is within size limits again.
      2. This kicks out mempool_evicted_tx, which means the m_viewmempool will no longer have a coin for coin_to_disappear. However, the coin has already been cached in m_view and it doesn't know this.
    3. AcceptSingleTransaction(package_child) fails due to missing inputs.
    4. AcceptMultipleTransactions([cpfp_parent, package_child])
      1. PreChecks looks for the inputs of each transaction, including the now-nonexistent coin_to_disappear. This succeeds because m_view has a coin for it.
      2. The CPFP works, so both transactions are submitted in SubmitPackage() , which calls ConsensusScriptChecks() which calls CheckInputsFromMempoolAndCache() which asserts that the coins all exist.
        1. We can't find the coin in the mempool (which is where it used to be).
        2. So we assume we will find it in the UTXO set. We call AccessCoin() and assert !coinFromUTXOSet.IsSpent(). This fails because the coin does not exist (which is the same as isspent).

@glozow glozow force-pushed the 2023-08-mid-package-trim branch from c4ac016 to 3729ee4 Compare August 22, 2023 14:01
@instagibbs
Copy link
Member

the above test description sounds right. The eldest ancestor gets trimmed due to size limitations, then things go wonky without this change

Copy link
Member

@instagibbs instagibbs left a comment

Choose a reason for hiding this comment

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

ACK 3729ee4

The newly added test, while slightly obscure to trigger it, makes sense and triggers the crash bug when the fix is not applied.

image

@glozow glozow force-pushed the 2023-08-mid-package-trim branch from 3729ee4 to a5a029c Compare August 23, 2023 15:19
@glozow
Copy link
Member Author

glozow commented Aug 23, 2023

Updated the approach + added a test for replacement case.

There are now 2 fixes:
(1) Clear any non-chainstate coins at the end of AcceptSubPackage so they can't be used in a different AcceptSubPackage.
(2) Don't trim in the middle of package evaluation.

Technically (1) is sufficient, but I think (2) is also beneficial as it avoids changing the mempool minimum feerate (which affects how we group transactions) mid-package evaluation. It would be sad if a CPFP package contains a parent that's just barely above mempool minimum feerate, so we submit it, then evict it (due to later parent), then can't submit the child... when we could have just submitted all of them and evicted something else.

@glozow glozow changed the title validation: avoid mempool eviction mid-package evaluation validation: fix coins disappearing mid-package evaluation Aug 23, 2023
Copy link
Member

@instagibbs instagibbs left a comment

Choose a reason for hiding this comment

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

Ifirst pass on most recent fix to a5a029c

approach ack on fixes, would be flakey to trim things within packages prematurely.

Copy link
Member

@instagibbs instagibbs left a comment

Choose a reason for hiding this comment

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

ACK 22fe13e

Copy link

@ariard ariard left a comment

Choose a reason for hiding this comment

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

Thanks for the in-depth explanation of the conditions to hit the case. Will review more the code and test coverage as high-prio.

Copy link

Choose a reason for hiding this comment

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

As I understand one of the condition to hit the case is a full mempool and when we call LimitMempool(), one interesting variation of the test case could be to limit the mempool size to the subpackage only. It might be a tricky one, though boundary conditions are always nice to exercise.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, full mempool is one of the conditions (i.e. the one tested in test_mid_package_eviction). It's not strictly necessary though, the coin can also disappear due to replacement.

one interesting variation of the test case could be to limit the mempool size to the subpackage only

Do you mean setting the mempool's maximum memory to something very tiny?

Useful to ensure that the topologies of packages/transactions are as
expected, preventing bugs caused by having unexpected mempool ancestors.
We want to be able to re-use fill_mempool so that none of the tests
affect each other.

Change the logs from info to debug because they are otherwise repeated
many times in the test output.
Test for scenario(s) outlined in PR 28251.
Test what happens when a package transaction spends a mempool coin which
is fetched and then disappears mid-package evaluation due to eviction or
replacement.
@glozow glozow force-pushed the 2023-08-mid-package-trim branch from 40d0db8 to 32c1dd1 Compare September 13, 2023 15:16
@instagibbs
Copy link
Member

instagibbs commented Sep 13, 2023

reACK 32c1dd1

inclusion of 7d7f7a1 duplicate-txid check in CheckPackage which prevents the assertion being hit for the 2nd of the duplicate txids

@fanquake fanquake merged commit f1a9fd6 into bitcoin:master Sep 13, 2023
@bitcoin bitcoin locked and limited conversation to collaborators Sep 12, 2024
@bitcoin bitcoin unlocked this conversation Oct 23, 2024
@sdaftuar
Copy link
Member

I'm reviewing this PR more carefully, after seeing @instagibbs' comment here: #31122 (comment)

As I understand it, I think there are two key behaviors introduced in this PR:

  1. Clearing state between subpackage evaluations (specifically, clearing the coins in m_viewmempool)
  2. Deferring mempool limiting until the end of package validation

However, the specific issue that @glozow described above (#28251 (comment)), where we need to ensure that package validation fails if an input is evicted mid-validation, seems to be completely mitigated by behavior (1). And this behavior is tested in the mempool_limit test introduced in this PR.

As far as I can tell from reviewing 3ea71fe (#28251), behavior (2) is designed to allow package validation to succeed in a case where we have, say, a child C with two parents A and B, and we submit a package [A, B, C] and potentially trigger this outcome:

  • A is accepted singly
  • B is accepted singly, but the mempool overflows and A is trimmed
  • C is rejected for missing inputs

So by preventing mempool limiting from taking place mid-package, the choice of what to evict from the mempool is deferred until after C is able to make it in, potentially changing the lowest-descendant-score transaction in the mempool from A to something else (as A's descendant score might be pretty high after C is accepted).

However, I don't think this PR introduced any test for this second behavior, of ensuring success is possible even if an earlier parent might be trimmed mid-validation.

Does this summary sound right to others? Have I overlooked other behavior changes/bugfixes that were introduced here?

@glozow
Copy link
Member Author

glozow commented Oct 25, 2024

As I understand it, I think there are two key behaviors introduced in this PR

However, the specific issue that @glozow described above (#28251 (comment)), where we need to ensure that package validation fails if an input is evicted mid-validation, seems to be completely mitigated by behavior (1).

As far as I can tell from reviewing 3ea71fe (#28251), behavior (2) is designed to allow package validation to succeed in a case where...

However, I don't think this PR introduced any test for this second behavior,

This looks accurate to me. I've looked a little at the tests (it's been a while 😅) and can't really point to one for deferring evictions. Will review #31152.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants