Skip to content

Commit 54eb085

Browse files
jkczyzclaude
andcommitted
Honor the funding template's RBF minimum feerate when splicing
Contributing to an already-pending splice — e.g. adding our funds to a counterparty-initiated splice via splice_in or splice_out — replaces the in-flight funding transaction, so the funding template requires at least the RBF minimum feerate. We passed our plain ChannelFunding feerate estimate, which can sit below that minimum (it does at the regtest floor), so the contribution was rejected with FeeRateBelowRbfMinimum. Raise the contribution feerate to the template's RBF minimum when one applies, capped by our max, so it can replace the pending splice. A node can therefore now contribute to a counterparty's pending splice; the rbf_splice_channel check that expected splice_out to fail while a splice was pending relied on this very bug and is dropped. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 5135534 commit 54eb085

2 files changed

Lines changed: 74 additions & 11 deletions

File tree

src/lib.rs

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1658,11 +1658,21 @@ impl Node {
16581658
return Err(Error::ChannelSplicingFailed);
16591659
}
16601660

1661+
// When contributing to a pending splice, the funding template requires at least the RBF
1662+
// minimum feerate to replace the in-flight transaction. Use it in place of our funding
1663+
// feerate estimate when it's higher, as long as it stays within our max.
1664+
let feerate = match funding_template.min_rbf_feerate() {
1665+
Some(min_rbf_feerate) if min_rbf_feerate <= max_feerate => {
1666+
min_feerate.max(min_rbf_feerate)
1667+
},
1668+
_ => min_feerate,
1669+
};
1670+
16611671
let contribution = self
16621672
.runtime
16631673
.block_on(funding_template.splice_in(
16641674
Amount::from_sat(splice_amount_sats),
1665-
min_feerate,
1675+
feerate,
16661676
max_feerate,
16671677
Arc::clone(&self.wallet),
16681678
))
@@ -1781,12 +1791,22 @@ impl Node {
17811791
return Err(Error::ChannelSplicingFailed);
17821792
}
17831793

1794+
// When contributing to a pending splice, the funding template requires at least the RBF
1795+
// minimum feerate to replace the in-flight transaction. Use it in place of our funding
1796+
// feerate estimate when it's higher, as long as it stays within our max.
1797+
let feerate = match funding_template.min_rbf_feerate() {
1798+
Some(min_rbf_feerate) if min_rbf_feerate <= max_feerate => {
1799+
min_feerate.max(min_rbf_feerate)
1800+
},
1801+
_ => min_feerate,
1802+
};
1803+
17841804
let outputs = vec![bitcoin::TxOut {
17851805
value: Amount::from_sat(splice_amount_sats),
17861806
script_pubkey: address.script_pubkey(),
17871807
}];
17881808
let contribution =
1789-
funding_template.splice_out(outputs, min_feerate, max_feerate).map_err(|e| {
1809+
funding_template.splice_out(outputs, feerate, max_feerate).map_err(|e| {
17901810
log_error!(self.logger, "Failed to splice channel: {}", e);
17911811
Error::ChannelSplicingFailed
17921812
})?;

tests/integration_tests_rust.rs

Lines changed: 52 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1387,7 +1387,7 @@ async fn run_rbf_splice_channel_test(confirm_original: bool) {
13871387
node_a.sync_wallets().unwrap();
13881388
node_b.sync_wallets().unwrap();
13891389

1390-
let user_channel_id_a = expect_channel_ready_event!(node_a, node_b.node_id());
1390+
let _user_channel_id_a = expect_channel_ready_event!(node_a, node_b.node_id());
13911391
let user_channel_id_b = expect_channel_ready_event!(node_b, node_a.node_id());
13921392

13931393
// bump_channel_funding_fee should fail when there's no pending splice
@@ -1424,19 +1424,13 @@ async fn run_rbf_splice_channel_test(confirm_original: bool) {
14241424
None
14251425
};
14261426

1427-
// splice_in should fail when there's a pending splice (RBF guard)
1427+
// Re-splicing the pending splice we already contributed to is rejected; the RBF guard points at
1428+
// bump_channel_funding_fee instead.
14281429
assert_eq!(
14291430
node_b.splice_in(&user_channel_id_b, node_a.node_id(), 1_000_000),
14301431
Err(NodeError::ChannelSplicingFailed),
14311432
);
14321433

1433-
// splice_out should fail when there's a pending splice (RBF guard)
1434-
let address = node_a.onchain_payment().new_address().unwrap();
1435-
assert_eq!(
1436-
node_a.splice_out(&user_channel_id_a, node_b.node_id(), &address, 100_000),
1437-
Err(NodeError::ChannelSplicingFailed),
1438-
);
1439-
14401434
// bump_channel_funding_fee should succeed when there's a pending splice
14411435
node_b.bump_channel_funding_fee(&user_channel_id_b, node_a.node_id()).unwrap();
14421436

@@ -1687,6 +1681,55 @@ async fn splice_payment_reorged_to_unconfirmed() {
16871681
node_b.stop().unwrap();
16881682
}
16891683

1684+
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
1685+
async fn splice_in_rbf_joins_counterparty_splice() {
1686+
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
1687+
let chain_source = random_chain_source(&bitcoind, &electrsd);
1688+
let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false);
1689+
1690+
let address_a = node_a.onchain_payment().new_address().unwrap();
1691+
let address_b = node_b.onchain_payment().new_address().unwrap();
1692+
let premine_amount_sat = 5_000_000;
1693+
premine_and_distribute_funds(
1694+
&bitcoind.client,
1695+
&electrsd.client,
1696+
vec![address_a, address_b],
1697+
Amount::from_sat(premine_amount_sat),
1698+
)
1699+
.await;
1700+
1701+
node_a.sync_wallets().unwrap();
1702+
node_b.sync_wallets().unwrap();
1703+
1704+
open_channel(&node_a, &node_b, 4_000_000, false, &electrsd).await;
1705+
generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await;
1706+
node_a.sync_wallets().unwrap();
1707+
node_b.sync_wallets().unwrap();
1708+
1709+
let user_channel_id_a = expect_channel_ready_event!(node_a, node_b.node_id());
1710+
let user_channel_id_b = expect_channel_ready_event!(node_b, node_a.node_id());
1711+
1712+
// node_b (which didn't fund the channel open, so holds the on-chain balance) initiates a
1713+
// splice-in; node_a does not contribute to this first candidate.
1714+
node_b.splice_in(&user_channel_id_b, node_a.node_id(), 1_000_000).unwrap();
1715+
let counterparty_txo = expect_splice_negotiated_event!(node_a, node_b.node_id());
1716+
expect_splice_negotiated_event!(node_b, node_a.node_id());
1717+
wait_for_tx(&electrsd.client, counterparty_txo.txid).await;
1718+
node_a.sync_wallets().unwrap();
1719+
node_b.sync_wallets().unwrap();
1720+
1721+
// node_a contributes to the pending splice via RBF. Before honoring the funding template's RBF
1722+
// minimum feerate, this was rejected with FeeRateBelowRbfMinimum because node_a's funding
1723+
// feerate estimate sat below the minimum required to replace the in-flight transaction.
1724+
node_a.splice_in(&user_channel_id_a, node_b.node_id(), 100_000).unwrap();
1725+
let rbf_txo = expect_splice_negotiated_event!(node_a, node_b.node_id());
1726+
expect_splice_negotiated_event!(node_b, node_a.node_id());
1727+
assert_ne!(counterparty_txo, rbf_txo, "node_a's RBF should produce a different funding txo");
1728+
1729+
node_a.stop().unwrap();
1730+
node_b.stop().unwrap();
1731+
}
1732+
16901733
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
16911734
async fn simple_bolt12_send_receive() {
16921735
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();

0 commit comments

Comments
 (0)