Skip to content

Commit 9c91289

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 0df4730 commit 9c91289

File tree

2 files changed

+94
-1
lines changed

2 files changed

+94
-1
lines changed

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
@@ -5077,6 +5077,99 @@ fn do_test_splice_rbf_tiebreak(
50775077
}
50785078
}
50795079

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

0 commit comments

Comments
 (0)