fix(mempool): reconcile conflicts before insertion - #254
Conversation
7b7b677 to
4356819
Compare
| .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?
There was a problem hiding this comment.
Ah right, sorry, I misremembered add_by_txid(s) errors as being propagated.
I agree that the current behavior, returning 200 in both cases, is already acceptable.
But I still think it would be nicer to use 202 to indicate deferred processing to clients, e.g. so that Esplora can display an appropriate message instead of immediately redirecting to the tx page and showing a "tx not found" error.
It's not high priority and could be done in a separate PR, but it's also a small change that could make sense to bundle with this PR if we expand its scope to more generally improve handling of submitted txs. It does have some potential to break clients that explicitly check for 200, but I think it should be quite common to treat any 2xx as successful.
Otoh, the fact that we don't have official point releases, a changelog for breaking changes or versioned API endpoints does make me more hesitant to change the REST contract. Perhaps a good time to prioritize addressing that?
| } | ||
|
|
||
| #[trace] | ||
| fn add(&mut self, txs_map: HashMap<Txid, Transaction>) -> Result<()> { |
There was a problem hiding this comment.
It's worth documenting the conflict-free assumption we have here for add().
| .collect(); | ||
|
|
||
| // Lookup remaining spent prevouts in mempool & on-chain | ||
| // Fails if any are missing. |
There was a problem hiding this comment.
Ah right, sorry, I misremembered add_by_txid(s) errors as being propagated.
I agree that the current behavior, returning 200 in both cases, is already acceptable.
But I still think it would be nicer to use 202 to indicate deferred processing to clients, e.g. so that Esplora can display an appropriate message instead of immediately redirecting to the tx page and showing a "tx not found" error.
It's not high priority and could be done in a separate PR, but it's also a small change that could make sense to bundle with this PR if we expand its scope to more generally improve handling of submitted txs. It does have some potential to break clients that explicitly check for 200, but I think it should be quite common to treat any 2xx as successful.
Otoh, the fact that we don't have official point releases, a changelog for breaking changes or versioned API endpoints does make me more hesitant to change the REST contract. Perhaps a good time to prioritize addressing that?
| for txin in &tx.input { | ||
| if let Some(other_txid) = incoming_spends.insert(txin.previous_output, *txid) { | ||
| if other_txid != *txid { | ||
| bail!( |
There was a problem hiding this comment.
Can this failure case actually happen if the tx package was considered valid and accepted by bitcoind?
|
This PR makes This is the current
It can fail in three ways:
Failure 1 is easy to fix: combine steps Failure 2 is also quite easy: detect already-indexed txids, either dropping them before calling Failure 3 is more tricky. It requires conflict detection similar to what this PR implements, checking the txs fetched from bitcoind against the local view for conflicts, between steps There's also a much simpler alternative we can consider that solves all 3 failures and also avoids other similar failures more holistically, at the cost of degraded user experience: prevent interleaved |
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.