Skip to content

Drop stale BMM requests from the mempool and templates - #672

Closed
bitlewis wants to merge 1 commit into
LayerTwo-Labs:masterfrom
block-fabric:mempool-stale-bmm
Closed

bitlewis wants to merge 1 commit into
LayerTwo-Labs:masterfrom
block-fabric:mempool-stale-bmm

Conversation

@bitlewis

@bitlewis bitlewis commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Branch: block-fabric:mempool-stale-bmm. One commit, on master (ceb608a). It is independent of the other two
PRs. This is policy only: it does not change validity, so there is no activation height.

Motivation

A BMM request (M8) commits to its previous mainchain block. A block is valid only if each of its M8s commits to that
block's parent (BmmRequestExpired). The enforcer's mempool (--enable-mempool) already handles two cases:

  • it refuses an M8 that does not commit to the current tip;
  • when a block connects, it drops the M8s it saw on top of that block's parent.

It misses one case. After a reorg, the M8s seen on top of the disconnected block stay in the mempool. Nothing
removes them, and the block template server can serve them. A miner using that template, including the enforcer's
own GenerateToAddress, then produces a block that the enforcer rejects.

The change

  • Validator::disconnect_block now returns, in DisconnectBlockAction::remove_mempool_txs, the M8s it saw on
    top of the disconnected block. The mempool sync drops them along with their descendants. This mirrors what
    connect_block already does for a block's parent.
  • The block producer tracks the M8s it accepts into the mempool:
    • each tracked M8 is stored with the previous block it commits to. This is in memory only, like
      mempool_payouts; the mempool replays accept_tx on restart;
    • when a block connects, every tracked M8 that does not build on it is added to remove_mempool_txs. This also
      catches any left over from a reorg;
    • initial_block_template excludes any tracked M8 that commits to another block than the template's parent.
      Templates can then never contain a stale request, even before the mempool catches up.

Tests

  • validator::cusf_enforcer::tests::connect_block_modes::disconnect_drops_bmm_requests_on_top_of_the_block: after a
    disconnect, the M8 seen on top of the block is in remove_mempool_txs, and one on top of another block is not.
  • block_producer::stale_bmm_request_tests::only_requests_on_top_of_the_tip_are_fresh: the selection used both for
    eviction and for template exclusion.

Results:

  • cargo test --workspace (208 lib + 13 app tests), cargo clippy --all-targets, nightly fmt --check and nightly
    unqualified_local_imports clippy (which needed an explicit drop of the lock): all pass.
  • No integration test was added or run. A reorg-with-M8-in-mempool integration test, along the lines of
    test_bmm_bid_auction with invalidate_block, would be the natural next step. It is left out because the
    integration harness could not be run locally.

Betanet (v0.3.4, 2026-09-16-betanet @ 17eb2ce)

It does not cherry-pick cleanly. There are 8 conflicts:

  • lib/block_producer/mod.rs (7): betanet lacks e08aae4's mempool_payouts tracking, which this change sits next
    to;
  • lib/validator/cusf_enforcer.rs (1).

The logic ports easily, but the hunks need to be rewritten by hand.

Open questions for maintainers

  1. Deprioritizing. Should stale M8s also be deprioritized in the node (RejectTx) on disconnect, as they are on
    connect? The mempool sync crate's disconnect path only removes them.
  2. Spending an unconfirmed M8. Chains also refuses, in the mempool, a transaction that spends an unconfirmed M8,
    unless that transaction is itself an M8. Is that worth adding here as a follow-up?

A BMM request (M8) commits to its previous mainchain block, so it can
only be mined on top of that block. Connecting a block drops the
requests seen on top of its parent, but those on top of a disconnected
block stayed in the mempool after a reorg, where a served template
could pick them up and make the block invalid.

Disconnecting a block now drops the requests seen on top of it. The
block producer tracks the M8s it accepts into the mempool: connecting
a block drops every one that does not build on it, and templates never
include one committing to another block than their parent.

Claude-Session: https://claude.ai/code/session_01MkqNezLq2qX1jG9BCoYqRK
@torkelrogstad

Copy link
Copy Markdown
Collaborator

Thanks. I reimplemented this in #678. You retain commit authorship. The reimplementation is simpler, and has test coverage.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants