fix(mempool): reconcile conflicts before insertion - #254
Conversation
7b7b677 to
4356819
Compare
| // local view immediately, before the next periodic sync has a chance to remove transactions | ||
| // they replaced in bitcoind. Reconcile those conflicts here so the indexes never contain two | ||
| // spenders for one outpoint. Descendants of a replaced transaction are no longer valid either. | ||
| let conflicts = self.conflicts_and_descendants(&txs_map)?; |
There was a problem hiding this comment.
Periodic full snapshot syncing through Mempool::update() is already guaranteed to be consistent and free of conflicts (we evict anything that bitcoind has before adding), so we could avoid the conflicts_and_descendants check in that path.
I would make a separate method for manual tx insertion, that first evicts the conflicts and then calls Mempool::add(), which can continue to assume the given transactions are conflict-free (would be good to also clearly document that expectation).
There was a problem hiding this comment.
Agreed. Moved conflict/descendant reconciliation into a separate add_submitted() method used only by add_by_txid() and add_by_txids(). The periodic snapshot path continues calling add() directly and retains its conflict-free input assumption.
| .collect(); | ||
|
|
||
| // Lookup remaining spent prevouts in mempool & on-chain | ||
| // Fails if any are missing. |
There was a problem hiding this comment.
This can fail when inserting submitted txs, if they descent from ancestor transactions that bitcoind has in its mempool view but we don't yet.
We could proactively fetch missing ancestors for submitted txs, but I would opt to keep this simpler and just avoid adding it to the local mempool view until the next periodic sync (the current behavior).
There's one thing we could improve though: broadcast_raw()/submit_package() could explicitly identify this failure case and return a 202 Accepted with a message saying the tx was submitted to the network but still not available in the local view. Currently this will fail with a 400, despite the network submission being successful.
There was a problem hiding this comment.
Agreed that we should not proactively fetch missing unconfirmed ancestors here. If a submitted transaction depends on an ancestor present in bitcoind but not yet in electrs’s local view, immediate insertion fails and the next periodic snapshot adds the complete dependency set.
I traced the response path again: both broadcast_raw() and submit_package() currently discard the local add_by_txid(s) result after successful daemon submission (let _ = ...). Therefore this case returns the normal successful daemon response (currently 200, not 400) while local visibility is deferred. I left that behavior unchanged. We could separately expose this outcome as 202 Accepted, but that would require changing the query/REST response contract. Maybe better in a new PR?
Summary
Prevent submitted replacement transactions from temporarily coexisting with the transactions they replace in electrs' local mempool.
broadcast_rawandsubmit_packageimmediate-insertion paths through the sharedMempool::add()implementationThis prevents the inconsistent state tracked in #236, where
txstoreand history could contain two conflicting spenders whileedgescould represent only one.Fixes #236
Tests
cargo test --test rest test_rest_mempool_rbf_reconciled -- --nocapturecargo test --test rest -- --nocapture(26 passed)cargo check --testscargo check --tests --features liquidThe REST regressions verify immediately after both raw and package broadcast—before the next periodic mempool sync—that the replaced transaction is absent and the replacement is queryable.