Skip to content

Commit 1189c8b

Browse files
jkczyzclaude
andcommitted
f - Skip RBF feerate for zero-conf channels in splice_channel
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 8f92231 commit 1189c8b

2 files changed

Lines changed: 74 additions & 37 deletions

File tree

lightning/src/ln/channel.rs

Lines changed: 36 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -11950,31 +11950,33 @@ where
1195011950
});
1195111951
}
1195211952

11953-
let (min_rbf_feerate, prior_contribution) =
11954-
if let Ok(min_rbf_feerate) = self.can_initiate_rbf() {
11955-
// A previous splice was negotiated but not yet locked. The user's splice
11956-
// will be an RBF, so provide the minimum RBF feerate and prior contribution.
11957-
let prior = self.build_prior_contribution();
11958-
(Some(min_rbf_feerate), prior)
11959-
} else if let Some(negotiation) = self
11960-
.pending_splice
11961-
.as_ref()
11962-
.and_then(|pending_splice| pending_splice.funding_negotiation.as_ref())
11963-
{
11964-
// A splice is currently being negotiated.
11965-
// - If the negotiation succeeds, the user's splice will need to satisfy the RBF
11966-
// feerate requirement. Derive the minimum RBF feerate from the negotiation's
11967-
// feerate so the user can choose an appropriate feerate.
11968-
// - If the negotiation fails (e.g., tx_abort), the splice will proceed as a fresh
11969-
// splice instead.
11970-
let prev_feerate = negotiation.funding_feerate_sat_per_1000_weight();
11971-
let min_feerate_kwu = ((prev_feerate as u64) * 25).div_ceil(24);
11972-
(Some(FeeRate::from_sat_per_kwu(min_feerate_kwu)), None)
11973-
} else {
11974-
// No RBF feerate to derive — either a fresh splice or a pending splice that
11975-
// can't be RBF'd (e.g., splice_locked already exchanged).
11976-
(None, None)
11977-
};
11953+
let (min_rbf_feerate, prior_contribution) = if self.is_rbf_compatible().is_err() {
11954+
// Channel can never RBF (e.g., zero-conf).
11955+
(None, None)
11956+
} else if let Ok(min_rbf_feerate) = self.can_initiate_rbf() {
11957+
// A previous splice was negotiated but not yet locked. The user's splice
11958+
// will be an RBF, so provide the minimum RBF feerate and prior contribution.
11959+
let prior = self.build_prior_contribution();
11960+
(Some(min_rbf_feerate), prior)
11961+
} else if let Some(negotiation) = self
11962+
.pending_splice
11963+
.as_ref()
11964+
.and_then(|pending_splice| pending_splice.funding_negotiation.as_ref())
11965+
{
11966+
// A splice is currently being negotiated.
11967+
// - If the negotiation succeeds, the user's splice will need to satisfy the RBF
11968+
// feerate requirement. Derive the minimum RBF feerate from the negotiation's
11969+
// feerate so the user can choose an appropriate feerate.
11970+
// - If the negotiation fails (e.g., tx_abort), the splice will proceed as a fresh
11971+
// splice instead.
11972+
let prev_feerate = negotiation.funding_feerate_sat_per_1000_weight();
11973+
let min_feerate_kwu = ((prev_feerate as u64) * 25).div_ceil(24);
11974+
(Some(FeeRate::from_sat_per_kwu(min_feerate_kwu)), None)
11975+
} else {
11976+
// No RBF feerate to derive — either a fresh splice or a pending splice that
11977+
// can't be RBF'd (e.g., splice_locked already exchanged).
11978+
(None, None)
11979+
};
1197811980

1197911981
let funding_txo = self.funding.get_funding_txo().expect("funding_txo should be set");
1198011982
let previous_utxo =
@@ -12000,13 +12002,19 @@ where
1200012002
Some(PriorContribution::new(prior.clone(), holder_balance))
1200112003
}
1200212004

12003-
fn can_initiate_rbf(&self) -> Result<FeeRate, String> {
12005+
/// Returns whether this channel can ever RBF, independent of splice state.
12006+
fn is_rbf_compatible(&self) -> Result<(), String> {
1200412007
if self.context.minimum_depth(&self.funding) == Some(0) {
1200512008
return Err(format!(
12006-
"Channel {} has option_zeroconf, cannot RBF splice",
12009+
"Channel {} has option_zeroconf, cannot RBF",
1200712010
self.context.channel_id(),
1200812011
));
1200912012
}
12013+
Ok(())
12014+
}
12015+
12016+
fn can_initiate_rbf(&self) -> Result<FeeRate, String> {
12017+
self.is_rbf_compatible()?;
1201012018

1201112019
let pending_splice = match &self.pending_splice {
1201212020
Some(pending_splice) => pending_splice,
@@ -12586,12 +12594,7 @@ where
1258612594
return Err(ChannelError::WarnAndDisconnect("Quiescence needed for RBF".to_owned()));
1258712595
}
1258812596

12589-
if self.context.minimum_depth(&self.funding) == Some(0) {
12590-
return Err(ChannelError::WarnAndDisconnect(format!(
12591-
"Channel {} has option_zeroconf, cannot RBF splice",
12592-
self.context.channel_id(),
12593-
)));
12594-
}
12597+
self.is_rbf_compatible().map_err(|msg| ChannelError::WarnAndDisconnect(msg))?;
1259512598

1259612599
let pending_splice = match &self.pending_splice {
1259712600
Some(pending_splice) => pending_splice,

lightning/src/ln/splicing_tests.rs

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4580,6 +4580,43 @@ fn test_splice_rbf_after_splice_locked() {
45804580
}
45814581
}
45824582

4583+
#[test]
4584+
fn test_splice_zeroconf_no_rbf_feerate() {
4585+
// Test that splice_channel returns a FundingTemplate with min_rbf_feerate = None for a
4586+
// zero-conf channel, even when a splice negotiation is in progress.
4587+
let chanmon_cfgs = create_chanmon_cfgs(2);
4588+
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
4589+
let mut config = test_default_channel_config();
4590+
config.channel_handshake_limits.trust_own_funding_0conf = true;
4591+
let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]);
4592+
let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
4593+
4594+
let node_id_0 = nodes[0].node.get_our_node_id();
4595+
4596+
let initial_channel_value_sat = 100_000;
4597+
let (funding_tx, channel_id) =
4598+
open_zero_conf_channel_with_value(&nodes[0], &nodes[1], None, initial_channel_value_sat, 0);
4599+
mine_transaction(&nodes[0], &funding_tx);
4600+
mine_transaction(&nodes[1], &funding_tx);
4601+
4602+
let added_value = Amount::from_sat(50_000);
4603+
provide_utxo_reserves(&nodes, 1, added_value * 2);
4604+
4605+
// Initiate a splice (node 0) and complete the handshake so a funding negotiation is in
4606+
// progress.
4607+
let _funding_contribution =
4608+
do_initiate_splice_in(&nodes[0], &nodes[1], channel_id, added_value);
4609+
let _new_funding_script = complete_splice_handshake(&nodes[0], &nodes[1]);
4610+
4611+
// The acceptor (node 1) calling splice_channel should return no RBF feerate since
4612+
// zero-conf channels cannot RBF.
4613+
let funding_template = nodes[1].node.splice_channel(&channel_id, &node_id_0).unwrap();
4614+
assert!(funding_template.min_rbf_feerate().is_none());
4615+
4616+
// Drain pending interactive tx messages from the splice handshake.
4617+
nodes[0].node.get_and_clear_pending_msg_events();
4618+
}
4619+
45834620
#[test]
45844621
fn test_splice_rbf_zeroconf_rejected() {
45854622
// Test that tx_init_rbf is rejected when option_zeroconf is negotiated.
@@ -4622,10 +4659,7 @@ fn test_splice_rbf_zeroconf_rejected() {
46224659
msgs::ErrorAction::DisconnectPeerWithWarning {
46234660
msg: msgs::WarningMessage {
46244661
channel_id,
4625-
data: format!(
4626-
"Channel {} has option_zeroconf, cannot RBF splice",
4627-
channel_id,
4628-
),
4662+
data: format!("Channel {} has option_zeroconf, cannot RBF", channel_id,),
46294663
},
46304664
}
46314665
);

0 commit comments

Comments
 (0)