Skip to content

Commit 51675a7

Browse files
jkczyzclaude
andcommitted
Handle FeeRateAdjustmentError variants in splice_init acceptor path
Replace the generic error handling in splice_init with explicit matching on FeeRateAdjustmentError variants: - FeeRateTooLow: initiator's feerate is below our minimum. Proceed without contribution and preserve QuiescentAction for an RBF retry at our preferred feerate. - FeeRateTooHigh: initiator's feerate exceeds our maximum and would consume too much of our change output. Reject the splice with WarnAndDisconnect. - BudgetInsufficient: our fee budget can't cover the acceptor's fair fee at this feerate. Proceed without contribution. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 08115fb commit 51675a7

2 files changed

Lines changed: 128 additions & 14 deletions

File tree

lightning/src/ln/channel.rs

Lines changed: 43 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,9 @@ use crate::ln::channelmanager::{
5555
PendingHTLCStatus, RAACommitmentOrder, SentHTLCId, BREAKDOWN_TIMEOUT,
5656
MAX_LOCAL_BREAKDOWN_TIMEOUT, MIN_CLTV_EXPIRY_DELTA,
5757
};
58-
use crate::ln::funding::{FundingContribution, FundingTemplate, FundingTxInput};
58+
use crate::ln::funding::{
59+
FeeRateAdjustmentError, FundingContribution, FundingTemplate, FundingTxInput,
60+
};
5961
use crate::ln::interactivetxs::{
6062
AbortReason, HandleTxCompleteValue, InteractiveTxConstructor, InteractiveTxConstructorArgs,
6163
InteractiveTxMessageSend, InteractiveTxSigningSession, SharedOwnedInput, SharedOwnedOutput,
@@ -12323,20 +12325,47 @@ where
1232312325
})
1232412326
.ok();
1232512327
let our_funding_contribution =
12326-
holder_balance.and_then(|_| self.queued_funding_contribution()).and_then(|c| {
12327-
c.net_value_for_acceptor_at_feerate(feerate, holder_balance.unwrap())
12328-
.map_err(|e| {
12329-
log_info!(
12330-
logger,
12331-
"Cannot accommodate initiator's feerate ({}) for channel {}: {}; \
12328+
match holder_balance.and_then(|_| self.queued_funding_contribution()) {
12329+
Some(c) => {
12330+
match c.net_value_for_acceptor_at_feerate(feerate, holder_balance.unwrap()) {
12331+
Ok(net_value) => Some(net_value),
12332+
Err(e @ FeeRateAdjustmentError::FeeRateTooLow { .. }) => {
12333+
log_info!(
12334+
logger,
12335+
"Initiator's feerate ({}) for channel {} is below our minimum: {}; \
12336+
proceeding without contribution, will RBF later",
12337+
feerate,
12338+
self.context.channel_id(),
12339+
e,
12340+
);
12341+
None
12342+
},
12343+
Err(e @ FeeRateAdjustmentError::FeeRateTooHigh { .. }) => {
12344+
return Err(ChannelError::WarnAndDisconnect(format!(
12345+
"Cannot accommodate initiator's feerate ({}) for channel {}: {}",
12346+
feerate,
12347+
self.context.channel_id(),
12348+
e,
12349+
)));
12350+
},
12351+
Err(
12352+
e @ FeeRateAdjustmentError::FeeBufferInsufficient { .. }
12353+
| e @ FeeRateAdjustmentError::FeeBufferOverflow { .. },
12354+
) => {
12355+
log_info!(
12356+
logger,
12357+
"Cannot accommodate initiator's feerate ({}) for channel {}: {}; \
1233212358
proceeding without contribution",
12333-
feerate,
12334-
self.context.channel_id(),
12335-
e,
12336-
);
12337-
})
12338-
.ok()
12339-
});
12359+
feerate,
12360+
self.context.channel_id(),
12361+
e,
12362+
);
12363+
None
12364+
},
12365+
}
12366+
},
12367+
None => None,
12368+
};
1234012369

1234112370
let splice_funding =
1234212371
self.validate_splice_init(msg, our_funding_contribution.unwrap_or(SignedAmount::ZERO))?;

lightning/src/ln/splicing_tests.rs

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1805,6 +1805,91 @@ fn do_test_splice_tiebreak(
18051805
}
18061806
}
18071807

1808+
#[test]
1809+
fn test_splice_tiebreak_feerate_too_high_rejected() {
1810+
// Node 0 (winner) proposes a feerate far above node 1's (loser) max_feerate, and node 1's
1811+
// fair fee at that feerate exceeds its budget. This triggers FeeRateAdjustmentError::TooHigh,
1812+
// causing node 1 to reject with WarnAndDisconnect.
1813+
let chanmon_cfgs = create_chanmon_cfgs(2);
1814+
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
1815+
let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]);
1816+
let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
1817+
1818+
let node_id_0 = nodes[0].node.get_our_node_id();
1819+
let node_id_1 = nodes[1].node.get_our_node_id();
1820+
1821+
let initial_channel_value_sat = 100_000;
1822+
let (_, _, channel_id, _) =
1823+
create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0);
1824+
1825+
provide_utxo_reserves(&nodes, 2, Amount::from_sat(100_000));
1826+
1827+
// Node 0 uses an extremely high feerate (100,000 sat/kwu). Node 1 uses the floor feerate
1828+
// with a moderate splice-in (50,000 sats from a 100,000 sat UTXO) and a low max_feerate
1829+
// (3,000 sat/kwu). The target (100k) far exceeds node 1's max (3k), and the fair fee at
1830+
// 100k exceeds node 1's budget, triggering TooHigh.
1831+
let high_feerate = FeeRate::from_sat_per_kwu(100_000);
1832+
let floor_feerate = FeeRate::from_sat_per_kwu(FEERATE_FLOOR_SATS_PER_KW as u64);
1833+
let node_0_added_value = Amount::from_sat(50_000);
1834+
let node_1_added_value = Amount::from_sat(50_000);
1835+
let node_1_max_feerate = FeeRate::from_sat_per_kwu(3_000);
1836+
1837+
// Node 0: very high feerate, moderate splice-in.
1838+
let funding_template_0 =
1839+
nodes[0].node.splice_channel(&channel_id, &node_id_1, high_feerate, FeeRate::MAX).unwrap();
1840+
let wallet_0 = WalletSync::new(Arc::clone(&nodes[0].wallet_source), nodes[0].logger);
1841+
let node_0_funding_contribution =
1842+
funding_template_0.splice_in_sync(node_0_added_value, &wallet_0).unwrap();
1843+
nodes[0]
1844+
.node
1845+
.funding_contributed(&channel_id, &node_id_1, node_0_funding_contribution.clone(), None)
1846+
.unwrap();
1847+
1848+
// Node 1: floor feerate, moderate splice-in, low max_feerate.
1849+
let funding_template_1 = nodes[1]
1850+
.node
1851+
.splice_channel(&channel_id, &node_id_0, floor_feerate, node_1_max_feerate)
1852+
.unwrap();
1853+
let wallet_1 = WalletSync::new(Arc::clone(&nodes[1].wallet_source), nodes[1].logger);
1854+
let node_1_funding_contribution =
1855+
funding_template_1.splice_in_sync(node_1_added_value, &wallet_1).unwrap();
1856+
nodes[1]
1857+
.node
1858+
.funding_contributed(&channel_id, &node_id_0, node_1_funding_contribution.clone(), None)
1859+
.unwrap();
1860+
1861+
// Both emit STFU.
1862+
let stfu_0 = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1);
1863+
let stfu_1 = get_event_msg!(nodes[1], MessageSendEvent::SendStfu, node_id_0);
1864+
1865+
// Tie-break: node 0 wins.
1866+
nodes[1].node.handle_stfu(node_id_0, &stfu_0);
1867+
assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty());
1868+
nodes[0].node.handle_stfu(node_id_1, &stfu_1);
1869+
1870+
// Node 0 sends SpliceInit at 100,000 sat/kwu.
1871+
let splice_init = get_event_msg!(nodes[0], MessageSendEvent::SendSpliceInit, node_id_1);
1872+
1873+
// Node 1 handles SpliceInit — TooHigh: target (100k) >> max (3k) and fair fee > budget.
1874+
nodes[1].node.handle_splice_init(node_id_0, &splice_init);
1875+
1876+
let msg_events = nodes[1].node.get_and_clear_pending_msg_events();
1877+
assert_eq!(msg_events.len(), 1, "{msg_events:?}");
1878+
match &msg_events[0] {
1879+
MessageSendEvent::HandleError {
1880+
action: msgs::ErrorAction::DisconnectPeerWithWarning { msg },
1881+
..
1882+
} => {
1883+
assert!(
1884+
msg.data.contains("Cannot accommodate initiator's feerate"),
1885+
"Unexpected warning: {}",
1886+
msg.data
1887+
);
1888+
},
1889+
other => panic!("Expected HandleError/DisconnectPeerWithWarning, got {:?}", other),
1890+
}
1891+
}
1892+
18081893
#[cfg(test)]
18091894
#[derive(PartialEq)]
18101895
enum SpliceStatus {

0 commit comments

Comments
 (0)