Skip to content

Commit f76abb6

Browse files
jkczyzclaude
andcommitted
Handle FeeRateAdjustmentError variants in tx_init_rbf acceptor path
Apply the same explicit FeeRateAdjustmentError handling to the tx_init_rbf acceptor path as was done for splice_init: - TooLow: proceed without queued contribution, preserve QuiescentAction for an RBF retry at our preferred feerate. - TooHigh: reject with WarnAndDisconnect. - BudgetInsufficient: proceed without queued contribution. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 51675a7 commit f76abb6

File tree

3 files changed

+129
-9
lines changed

3 files changed

+129
-9
lines changed

lightning/src/ln/channel.rs

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12555,21 +12555,48 @@ where
1255512555
.ok();
1255612556

1255712557
// Try queued contribution from QuiescentAction (tiebreak scenario).
12558-
let queued_net_value =
12559-
holder_balance.and_then(|_| self.queued_funding_contribution()).and_then(|c| {
12560-
c.net_value_for_acceptor_at_feerate(feerate, holder_balance.unwrap())
12561-
.map_err(|e| {
12558+
let queued_net_value = match holder_balance.and_then(|_| self.queued_funding_contribution())
12559+
{
12560+
Some(c) => {
12561+
match c.net_value_for_acceptor_at_feerate(feerate, holder_balance.unwrap()) {
12562+
Ok(net_value) => Some(net_value),
12563+
Err(e @ FeeRateAdjustmentError::FeeRateTooLow { .. }) => {
12564+
log_info!(
12565+
logger,
12566+
"Initiator's RBF feerate ({}) for channel {} is below our minimum: {}; \
12567+
proceeding without queued contribution, will RBF later",
12568+
feerate,
12569+
self.context.channel_id(),
12570+
e,
12571+
);
12572+
None
12573+
},
12574+
Err(e @ FeeRateAdjustmentError::FeeRateTooHigh { .. }) => {
12575+
return Err(ChannelError::WarnAndDisconnect(format!(
12576+
"Cannot accommodate initiator's RBF feerate ({}) for channel {}: {}",
12577+
feerate,
12578+
self.context.channel_id(),
12579+
e,
12580+
)));
12581+
},
12582+
Err(
12583+
e @ FeeRateAdjustmentError::FeeBufferInsufficient { .. }
12584+
| e @ FeeRateAdjustmentError::FeeBufferOverflow { .. },
12585+
) => {
1256212586
log_info!(
1256312587
logger,
12564-
"Cannot accommodate initiator's feerate ({}) for channel {}: {}; \
12588+
"Cannot accommodate initiator's RBF feerate ({}) for channel {}: {}; \
1256512589
proceeding without contribution",
1256612590
feerate,
1256712591
self.context.channel_id(),
1256812592
e,
1256912593
);
12570-
})
12571-
.ok()
12572-
});
12594+
None
12595+
},
12596+
}
12597+
},
12598+
None => None,
12599+
};
1257312600

1257412601
// If no queued contribution, try prior contribution from previous negotiation.
1257512602
// Failing here means the RBF would erase our splice — reject it.

lightning/src/ln/funding.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ use crate::util::wallet_utils::{
4242
#[derive(Debug)]
4343
pub(super) enum FeeRateAdjustmentError {
4444
/// The counterparty's proposed feerate is below `min_feerate`, which was used as the feerate
45-
/// during coin selection.
45+
/// during coin selection. We'll retry via RBF at our preferred feerate.
4646
FeeRateTooLow { target_feerate: FeeRate, min_feerate: FeeRate },
4747
/// The counterparty's proposed feerate is above `max_feerate` and the re-estimated fee for
4848
/// our contributed inputs and outputs exceeds the original fee estimate (computed at

lightning/src/ln/splicing_tests.rs

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5067,6 +5067,99 @@ fn do_test_splice_rbf_tiebreak(
50675067
}
50685068
}
50695069

5070+
#[test]
5071+
fn test_splice_rbf_tiebreak_feerate_too_high_rejected() {
5072+
// Node 0 (winner) proposes an RBF feerate far above node 1's (loser) max_feerate, and
5073+
// node 1's fair fee at that feerate exceeds its budget. This triggers
5074+
// FeeRateAdjustmentError::TooHigh in the queued contribution path, causing node 1 to
5075+
// reject with WarnAndDisconnect.
5076+
let chanmon_cfgs = create_chanmon_cfgs(2);
5077+
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
5078+
let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]);
5079+
let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
5080+
5081+
let node_id_0 = nodes[0].node.get_our_node_id();
5082+
let node_id_1 = nodes[1].node.get_our_node_id();
5083+
5084+
let initial_channel_value_sat = 100_000;
5085+
let (_, _, channel_id, _) =
5086+
create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0);
5087+
5088+
let added_value = Amount::from_sat(50_000);
5089+
provide_utxo_reserves(&nodes, 2, added_value * 2);
5090+
5091+
// Complete an initial splice-in from node 0.
5092+
let funding_contribution = do_initiate_splice_in(&nodes[0], &nodes[1], channel_id, added_value);
5093+
let (_first_splice_tx, _new_funding_script) =
5094+
splice_channel(&nodes[0], &nodes[1], channel_id, funding_contribution);
5095+
5096+
// Provide more UTXOs for both nodes' RBF attempts.
5097+
provide_utxo_reserves(&nodes, 2, added_value * 2);
5098+
5099+
// Node 0 uses an extremely high feerate (100,000 sat/kwu). Node 1 uses the minimum RBF
5100+
// feerate with a moderate splice-in (50,000 sats) and a low max_feerate (3,000 sat/kwu).
5101+
// The target (100k) far exceeds node 1's max (3k), and the fair fee at 100k exceeds
5102+
// node 1's budget, triggering TooHigh.
5103+
let high_feerate = FeeRate::from_sat_per_kwu(100_000);
5104+
let min_rbf_feerate_sat_per_kwu = (FEERATE_FLOOR_SATS_PER_KW as u64 * 25 + 23) / 24;
5105+
let min_rbf_feerate = FeeRate::from_sat_per_kwu(min_rbf_feerate_sat_per_kwu);
5106+
let node_1_max_feerate = FeeRate::from_sat_per_kwu(3_000);
5107+
5108+
let funding_template_0 =
5109+
nodes[0].node.rbf_channel(&channel_id, &node_id_1, high_feerate, FeeRate::MAX).unwrap();
5110+
let wallet_0 = WalletSync::new(Arc::clone(&nodes[0].wallet_source), nodes[0].logger);
5111+
let node_0_funding_contribution =
5112+
funding_template_0.splice_in_sync(added_value, &wallet_0).unwrap();
5113+
nodes[0]
5114+
.node
5115+
.funding_contributed(&channel_id, &node_id_1, node_0_funding_contribution.clone(), None)
5116+
.unwrap();
5117+
5118+
let funding_template_1 = nodes[1]
5119+
.node
5120+
.rbf_channel(&channel_id, &node_id_0, min_rbf_feerate, node_1_max_feerate)
5121+
.unwrap();
5122+
let wallet_1 = WalletSync::new(Arc::clone(&nodes[1].wallet_source), nodes[1].logger);
5123+
let node_1_funding_contribution =
5124+
funding_template_1.splice_in_sync(added_value, &wallet_1).unwrap();
5125+
nodes[1]
5126+
.node
5127+
.funding_contributed(&channel_id, &node_id_0, node_1_funding_contribution.clone(), None)
5128+
.unwrap();
5129+
5130+
// Both sent STFU.
5131+
let stfu_0 = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1);
5132+
let stfu_1 = get_event_msg!(nodes[1], MessageSendEvent::SendStfu, node_id_0);
5133+
5134+
// Tie-break: node 0 wins.
5135+
nodes[1].node.handle_stfu(node_id_0, &stfu_0);
5136+
assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty());
5137+
nodes[0].node.handle_stfu(node_id_1, &stfu_1);
5138+
5139+
// Node 0 sends tx_init_rbf at 100,000 sat/kwu.
5140+
let tx_init_rbf = get_event_msg!(nodes[0], MessageSendEvent::SendTxInitRbf, node_id_1);
5141+
assert_eq!(tx_init_rbf.feerate_sat_per_1000_weight, high_feerate.to_sat_per_kwu() as u32);
5142+
5143+
// Node 1 handles tx_init_rbf — TooHigh: target (100k) >> max (3k) and fair fee > budget.
5144+
nodes[1].node.handle_tx_init_rbf(node_id_0, &tx_init_rbf);
5145+
5146+
let msg_events = nodes[1].node.get_and_clear_pending_msg_events();
5147+
assert_eq!(msg_events.len(), 1, "{msg_events:?}");
5148+
match &msg_events[0] {
5149+
MessageSendEvent::HandleError {
5150+
action: msgs::ErrorAction::DisconnectPeerWithWarning { msg },
5151+
..
5152+
} => {
5153+
assert!(
5154+
msg.data.contains("Cannot accommodate initiator's RBF feerate"),
5155+
"Unexpected warning: {}",
5156+
msg.data
5157+
);
5158+
},
5159+
other => panic!("Expected HandleError/DisconnectPeerWithWarning, got {:?}", other),
5160+
}
5161+
}
5162+
50705163
#[test]
50715164
fn test_splice_rbf_acceptor_recontributes() {
50725165
// When the counterparty RBFs a splice and we have no pending QuiescentAction,

0 commit comments

Comments
 (0)