Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions changelog.d/mempool-spends-conflicting-tx.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Fixed

- **The mempool rejects a replacement that spends an output of a tx it
replaces** (`bad-txns-spends-conflicting-tx`, as in Core). It used to be
admitted with an input that left with the replaced tx, so
`getblocktemplate` offered a block that fails
`bad-txns-inputs-missingorspent`.
11 changes: 6 additions & 5 deletions crates/rbitcoin-mempool/src/accept.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1316,11 +1316,7 @@ impl ActiveMempool {
} else {
BTreeSet::new()
};
let parent_txids: BTreeSet<Txid> = scan
.parent_txids
.into_iter()
.filter(|p| !conflict_set.contains(p))
.collect();
let parent_txids = scan.parent_txids;
// Cluster limits count raw weight (Libre): at 20 B/sigop an adjusted
// cap would reject any tx above ~20,200 sigop cost.
let (n_members, base_w) = self
Expand All @@ -1335,6 +1331,11 @@ impl ActiveMempool {
weight: combined_w,
});
}
// Core `bad-txns-spends-conflicting-tx`. A parent in the conflict set
// is evicted by this accept, and its outputs with it.
if parent_txids.iter().any(|p| conflict_set.contains(p)) {
return Err(AcceptError::Policy("bad-txns-spends-conflicting-tx"));
}

Ok((conflict_set, fee_sat, weight))
}
Expand Down
38 changes: 38 additions & 0 deletions crates/rbitcoin-test/tests/cross_surface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1800,6 +1800,44 @@
"too-low RBF must leave the original: {mem}"
);

// Core `bad-txns-spends-conflicting-tx`: a replacement cannot spend an
// output of the tx it evicts; that output leaves with it.
let mut spends_conflict = acs_spend(
rpc_cb,
50_0000_0000 + 50_0000_0000 - 1_000,
100_000,
ScriptBuf::from_bytes(vec![0x58]),
);
let mut conflict_out = spends_conflict.input[0].clone();
conflict_out.previous_output = OutPoint {
txid: rpc_spend.compute_txid(),
vout: 0,
};
spends_conflict.input.push(conflict_out);
let spends_conflict_hex = encode_tx(&spends_conflict);
let tma = jsonrpc(
rpc_addr,
"testmempoolaccept",
json!([[spends_conflict_hex.clone()]]),
)
.await;
assert_eq!(tma["result"][0]["allowed"], false, "{tma}");
Comment thread
reardencode marked this conversation as resolved.
Dismissed
assert_eq!(
tma["result"][0]["reject-reason"], "bad-txns-spends-conflicting-tx",
"{tma}"
Comment thread
reardencode marked this conversation as resolved.
Dismissed
);
let rejected = jsonrpc(rpc_addr, "sendrawtransaction", json!([spends_conflict_hex])).await;
assert_eq!(rejected["error"]["code"], -26, "{rejected}");
Comment thread
reardencode marked this conversation as resolved.
Dismissed
assert_eq!(
rejected["error"]["message"], "bad-txns-spends-conflicting-tx",
"{rejected}"
Comment thread
reardencode marked this conversation as resolved.
Dismissed
);
let mem = jsonrpc(rpc_addr, "getrawmempool", json!([])).await;
assert!(
mempool_has(&mem, &rpc_txid),
"a replacement spending its conflict must leave the original: {mem}"
Comment thread
reardencode marked this conversation as resolved.
Dismissed
);

let high = acs_spend(
rpc_cb,
50_0000_0000,
Expand Down
Loading