Skip to content

mempool: reject a replacement that spends its conflict's output - #1076

Merged
reardencode merged 1 commit into
reardencode:masterfrom
smolcars:mempool/spends-conflicting-tx
Oct 18, 2026
Merged

reardencode merged 1 commit into
reardencode:masterfrom
smolcars:mempool/spends-conflicting-tx

Conversation

@niteshbalusu11

@niteshbalusu11 niteshbalusu11 commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Summary

A replacement that spends an output of a tx it replaces is admitted. Core rejects it with bad-txns-spends-conflicting-tx (validation.cpp, EntriesAndTxidsDisjoint).

 plan_after_script (accept.rs)
   conflict_set = direct conflicts + descendants
-  parent_txids = parents.filter(not in conflict_set)   // drops the conflict silently
+  parent_txids = parents
   cluster limits (connected_weight_except already skips the conflict set)
+  if any parent in conflict_set -> "bad-txns-spends-conflicting-tx"

On master the commit evicts the parent and keeps the new tx, whose input left with that parent. The mempool now holds a tx that can't be mined. getblocktemplate hands it to miners, and the resulting block is bad-txns-inputs-missingorspent in Core and in rbitcoin. generatetoaddress fails the same way. The reject sits in plan_after_script, so sendraw, P2P relay, single-tx testmempoolaccept, submitpackage, orphan and 1p1c admission, and reorg re-admission all reach it. Multi-tx testmempoolaccept already refuses any replacement earlier (bip125-replacement-disallowed). The check runs after the cluster check, which is Core's order.

Not included: Core's RPC error also carries a detail string (<N> spends conflicting transaction <P>, and reject-details in testmempoolaccept). rbitcoin gives the short reason here, as it does for most rejects.

Evidence

Same txs on Core 31.1 and rbitcoin (regtest). P spends coinbase U. N spends U and P:0 and pays enough to replace P.

Core 31.1 master this branch
sendrawtransaction N bad-txns-spends-conflicting-tx accepted, P evicted bad-txns-spends-conflicting-tx
mempool after [P] [N] [P]
block from rbitcoin's getblocktemplate, submitted to Core — bad-txns-inputs-missingorspent accepted
generatetoaddress 1 — consensus: bad-txns-inputs-missingorspent mines P
  • New beat in the esplora_broadcast_visible_in_rpc_and_electrum journey (cross_surface.rs), after the low-fee RBF beats. A tx that spends rpc_cb and the original's output, with a fee well above the RBF floor:
    • Before: testmempoolaccept returns "allowed": true.
    • After: testmempoolaccept gives bad-txns-spends-conflicting-tx, sendrawtransaction gives -26 with the same reason, and the original stays in the mempool.
  • fmt, clippy, ast-grep, cargo test --workspace, and ci-os-smoke.sh pass locally.

Merge Danger

Door: two-way

Blast Radius: mempool

Only replacements that spend an output of a tx they would evict change behaviour. They were accepted and are now rejected, as in Core. Replacements that don't spend a conflict's output behave as before.

🤖 Generated with Claude Code

The RBF plan dropped a parent that sat in the conflict set from the
parent list and admitted the tx. The commit then evicted that parent,
so the new tx kept an input that no longer existed, and
getblocktemplate offered a block that fails
bad-txns-inputs-missingorspent. Core rejects it with
bad-txns-spends-conflicting-tx after the cluster check; so does this.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@niteshbalusu11
niteshbalusu11 force-pushed the mempool/spends-conflicting-tx branch from f8a2f59 to 01ac5f5 Compare October 18, 2026 21:47
Comment thread crates/rbitcoin-test/tests/cross_surface.rs Dismissed
Comment thread crates/rbitcoin-test/tests/cross_surface.rs Dismissed
Comment thread crates/rbitcoin-test/tests/cross_surface.rs Dismissed
Comment thread crates/rbitcoin-test/tests/cross_surface.rs Dismissed
Comment thread crates/rbitcoin-test/tests/cross_surface.rs Dismissed

@rearden-grok rearden-grok Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Summary

The replacement is rejected on every admit path, and the journey pins that reject.

plan_after_script is the only place that turns a script-checked tx into a conflict set. finish_commit and evaluate_after_script both call it, so sendrawtransaction, P2P, testmempoolaccept, submitpackage, orphan promotion, 1p1c, and reorg re-accept all hit the new rule. There is no second filter left beside it.

parent_txids is the mempool creator of each spent outpoint, not the whole conflict set. Honest RBF spends the same parent as the tx it replaces, so that parent is not in the conflict set and is still the cluster seed. A parent that is in the conflict set was already ignored by connected_weight_except, which skips seeds and neighbors in except. Keeping those parents in the set does not change the cluster count.

The disjoint check runs after that read and before Ok. plan_after_script takes &self. finish_commit unlinks only after plan_after_script returns, so the reject path does not evict and does not change fees. A tx that also blows the cluster cap still fails ClusterTooLarge first, which is the order this function already used.

conflict_set is the direct conflicts plus every descendant. Spending a descendant's output puts that descendant in parent_txids and in the set. A confirmed input plus the conflict's own output is the same intersection. A package child that spends the newly admitted replacement does not: the replacement is a parent, and it is not in the child's conflict set. Packages are applied one tx at a time through the same function.

The new journey spends rpc_cb (the input rpc_spend already spends) and rpc_spend vout 0, and sets the output to the sum of those inputs minus 100,000 sat. That fee clears min-relay and the incremental RBF rule, so an earlier rule does not steal the reason. testmempoolaccept and sendrawtransaction both require bad-txns-spends-conflicting-tx, and the original txid is still in the mempool. On master this tx is admitted.

Issue counts by severity

  • bugs: 0
  • suggestions: 0
  • nits: 0

@reardencode
reardencode merged commit 7b34a74 into reardencode:master Oct 18, 2026
18 checks passed
@niteshbalusu11
niteshbalusu11 deleted the mempool/spends-conflicting-tx branch October 19, 2026 00:11
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.

3 participants