Skip to content

Commit 4f20614

Browse files
jkczyzclaude
andcommitted
f - Reject on-chain RBF of funding and splice payments
bump_fee_rbf accepted channel-funding and splice payments because they are recorded as outbound, unconfirmed on-chain payments. Replacing such a transaction via wallet RBF would broadcast one LDK isn't tracking, and for splices the shared input can't be wallet-signed. Reject these and leave splice fee-bumping to rbf_channel. Co-Authored-By: Claude <noreply@anthropic.com>
1 parent d0f7f4e commit 4f20614

2 files changed

Lines changed: 74 additions & 0 deletions

File tree

src/wallet/mod.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1553,6 +1553,23 @@ impl Wallet {
15531553
Error::InvalidPaymentId
15541554
})?;
15551555

1556+
// Funding transactions (channel opens and splices) are driven by LDK's funding/splice
1557+
// lifecycle, not the on-chain wallet. Replacing one via on-chain RBF would broadcast a
1558+
// transaction LDK isn't tracking (and, for splices, can't sign). Fee-bumping a pending
1559+
// splice goes through `bump_channel_funding_fee` instead.
1560+
if self
1561+
.pending_payment_store
1562+
.get(&payment_id)
1563+
.map_or(false, |p| p.funding_details.is_some())
1564+
{
1565+
log_error!(
1566+
self.logger,
1567+
"Cannot RBF funding payment {} via bump_fee_rbf; use bump_channel_funding_fee instead",
1568+
payment_id,
1569+
);
1570+
return Err(Error::InvalidPaymentId);
1571+
}
1572+
15561573
if let PaymentKind::Onchain { status, .. } = &payment.kind {
15571574
match status {
15581575
ConfirmationStatus::Confirmed { .. } => {

tests/integration_tests_rust.rs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1337,6 +1337,63 @@ async fn rbf_splice_channel() {
13371337
node_b.stop().unwrap();
13381338
}
13391339

1340+
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
1341+
async fn bump_fee_rbf_rejects_funding_payment() {
1342+
// A channel-funding or splice transaction is driven by LDK's funding/splice lifecycle, not the
1343+
// on-chain wallet. `bump_fee_rbf` must reject such payments — replacing the funding transaction
1344+
// via plain wallet RBF would broadcast a transaction LDK isn't tracking (and, for splices,
1345+
// can't even sign). Fee-bumping a pending splice goes through `bump_channel_funding_fee` instead.
1346+
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
1347+
let chain_source = random_chain_source(&bitcoind, &electrsd);
1348+
let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false);
1349+
1350+
let address_a = node_a.onchain_payment().new_address().unwrap();
1351+
let address_b = node_b.onchain_payment().new_address().unwrap();
1352+
let premine_amount_sat = 5_000_000;
1353+
premine_and_distribute_funds(
1354+
&bitcoind.client,
1355+
&electrsd.client,
1356+
vec![address_a, address_b],
1357+
Amount::from_sat(premine_amount_sat),
1358+
)
1359+
.await;
1360+
1361+
node_a.sync_wallets().unwrap();
1362+
node_b.sync_wallets().unwrap();
1363+
1364+
open_channel(&node_a, &node_b, 4_000_000, false, &electrsd).await;
1365+
1366+
generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await;
1367+
1368+
node_a.sync_wallets().unwrap();
1369+
node_b.sync_wallets().unwrap();
1370+
1371+
let _user_channel_id_a = expect_channel_ready_event!(node_a, node_b.node_id());
1372+
let user_channel_id_b = expect_channel_ready_event!(node_b, node_a.node_id());
1373+
1374+
// Splice-in to create a pending splice payment.
1375+
node_b.splice_in(&user_channel_id_b, node_a.node_id(), 1_000_000).unwrap();
1376+
1377+
let txo = expect_splice_negotiated_event!(node_a, node_b.node_id());
1378+
expect_splice_negotiated_event!(node_b, node_a.node_id());
1379+
1380+
// Make node_b's wallet aware of the splice transaction so `bump_fee_rbf` reaches its funding
1381+
// guard rather than failing earlier for a transaction it can't find.
1382+
wait_for_tx(&electrsd.client, txo.txid).await;
1383+
node_b.sync_wallets().unwrap();
1384+
1385+
// The splice payment is an on-chain, outbound, unconfirmed record, so it passes
1386+
// `bump_fee_rbf`'s other guards; it must nonetheless be rejected as a funding payment.
1387+
let splice_payment_id = PaymentId(txo.txid.to_byte_array());
1388+
assert_eq!(
1389+
node_b.onchain_payment().bump_fee_rbf(splice_payment_id, None),
1390+
Err(NodeError::InvalidPaymentId),
1391+
);
1392+
1393+
node_a.stop().unwrap();
1394+
node_b.stop().unwrap();
1395+
}
1396+
13401397
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
13411398
async fn simple_bolt12_send_receive() {
13421399
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();

0 commit comments

Comments
 (0)