Repository navigation
mempool: reject a replacement that spends its conflict's output - #1076
Conversation
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>
f8a2f59 to
01ac5f5
Compare
There was a problem hiding this comment.
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
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).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.
getblocktemplatehands it to miners, and the resulting block isbad-txns-inputs-missingorspentin Core and in rbitcoin.generatetoaddressfails the same way. The reject sits inplan_after_script, so sendraw, P2P relay, single-txtestmempoolaccept,submitpackage, orphan and 1p1c admission, and reorg re-admission all reach it. Multi-txtestmempoolacceptalready 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>, andreject-detailsintestmempoolaccept). 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.
sendrawtransaction Nbad-txns-spends-conflicting-txbad-txns-spends-conflicting-tx[P][N][P]getblocktemplate, submitted to Corebad-txns-inputs-missingorspentgeneratetoaddress 1consensus: bad-txns-inputs-missingorspentesplora_broadcast_visible_in_rpc_and_electrumjourney (cross_surface.rs), after the low-fee RBF beats. A tx that spendsrpc_cband the original's output, with a fee well above the RBF floor:testmempoolacceptreturns"allowed": true.testmempoolacceptgivesbad-txns-spends-conflicting-tx,sendrawtransactiongives-26with the same reason, and the original stays in the mempool.cargo test --workspace, andci-os-smoke.shpass 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