Add UserConfig::reject_inbound_splices
What changed, and why it matters
This commit adds a new configuration option called `reject_inbound_splices` to the Lightning Dev Kit (LDK). When enabled (which is the default), a node will refuse incoming requests from other nodes to splice (resize) an existing channel. The commit also changes LDK to publicly advertise support for splicing and quiescence features, rather than only in tests. The stated purpose is to avoid breaking backwards compatibility with older LDK versions while a splice is in progress. It is a defensive, opt-in/out configuration change rather than a fix for an active vulnerability.
Treat as a routine feature/configuration addition, not as an emergency security patch. Operators using LDK should review the new `reject_inbound_splices` default and consider compatibility implications with peers running older LDK versions. Monitor vendor release notes for any further guidance on splicing support.
Security signals we found
New config flag defaults to rejecting inbound splicing
Backwards-compatibility concern explicitly mentioned by vendor
Feature bits now publicly advertise splicing/quiescence support
Rejection uses warning+disconnect rather than channel closure
No CVE, advisory, or researcher attribution present in commit
Evidence from the diff
The patch introduces UserConfig::reject_inbound_splices (default true). In ChannelManager::handle_splice_init, if the flag is set, the node returns ChannelError::WarnAndDisconnect with the message “Inbound channel splices are currently not allowed”. The feature bits advertised in provided_init_features are updated to unconditionally set quiescence_optional and splicing_optional, removing the previous #[cfg(any(test, fuzzing))] guard. Serialization/deserialization and fuzzing seeds are updated to account for the new config byte. A test verifies that a node with the flag rejects inbound splices but can still initiate outbound splices.
Changed components
lightning/src/util/config.rslightning/src/ln/channelmanager.rslightning/src/ln/splicing_tests.rslightning/src/ln/functional_test_utils.rsfuzz/src/full_stack.rsInspect captured patch +88 / −7
diff --git a/fuzz/src/full_stack.rs b/fuzz/src/full_stack.rs
index ee5f457..6b00f29 100644
--- a/fuzz/src/full_stack.rs
+++ b/fuzz/src/full_stack.rs
@@ -1048,7 +1048,7 @@ fn two_peer_forwarding_seed() -> Vec<u8> {
// our network key
ext_from_hex("0100000000000000000000000000000000000000000000000000000000000000", &mut test);
// config
- ext_from_hex("000000000090000000000000000064000100000000000100ffff0000000000000000ffffffffffffffffffffffffffffffff0000000000000000ffffffffffffffff000000ffffffff00ffff1a000400010000020400000000040200000a08ffffffffffffffff0001000000000000", &mut test);
+ ext_from_hex("000000000090000000000000000064000100000000000100ffff0000000000000000ffffffffffffffffffffffffffffffff0000000000000000ffffffffffffffff000000ffffffff00ffff1a000400010000020400000000040200000a08ffffffffffffffff000100000000000000", &mut test);
// new outbound connection with id 0
ext_from_hex("00", &mut test);
@@ -1502,7 +1502,7 @@ fn gossip_exchange_seed() -> Vec<u8> {
// our network key
ext_from_hex("0100000000000000000000000000000000000000000000000000000000000000", &mut test);
// config
- ext_from_hex("000000000090000000000000000064000100000000000100ffff0000000000000000ffffffffffffffffffffffffffffffff0000000000000000ffffffffffffffff000000ffffffff00ffff1a000400010000020400000000040200000a08ffffffffffffffff0001000000000000", &mut test);
+ ext_from_hex("000000000090000000000000000064000100000000000100ffff0000000000000000ffffffffffffffffffffffffffffffff0000000000000000ffffffffffffffff000000ffffffff00ffff1a000400010000020400000000040200000a08ffffffffffffffff000100000000000000", &mut test);
// new outbound connection with id 0
ext_from_hex("00", &mut test);
diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs
index e448802..5cd5e80 100644
--- a/lightning/src/ln/channelmanager.rs
+++ b/lightning/src/ln/channelmanager.rs
@@ -11476,6 +11476,13 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
counterparty_node_id, msg.channel_id,
), msg.channel_id)),
hash_map::Entry::Occupied(mut chan_entry) => {
+ if self.config.read().unwrap().reject_inbound_splices {
+ let err = ChannelError::WarnAndDisconnect(
+ "Inbound channel splices are currently not allowed".to_owned()
+ );
+ return Err(MsgHandleErrInternal::from_chan_no_close(err, msg.channel_id));
+ }
+
if let Some(ref mut funded_channel) = chan_entry.get_mut().as_funded_mut() {
let init_res = funded_channel.splice_init(
msg, our_funding_contribution, &self.signer_provider, &self.entropy_source,
@@ -15325,16 +15332,15 @@ pub fn provided_init_features(config: &UserConfig) -> InitFeatures {
features.set_provide_storage_optional();
#[cfg(simple_close)]
features.set_simple_close_optional();
+ features.set_quiescence_optional();
+ features.set_splicing_optional();
+
if config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx {
features.set_anchors_zero_fee_htlc_tx_optional();
}
if config.enable_dual_funded_channels {
features.set_dual_fund_optional();
}
- // Only signal quiescence support in tests for now, as we don't yet support any
- // quiescent-dependent protocols (e.g., splicing).
- #[cfg(any(test, fuzzing))]
- features.set_quiescence_optional();
if config.channel_handshake_config.negotiate_anchor_zero_fee_commitments {
features.set_anchor_zero_fee_commitments_optional();
diff --git a/lightning/src/ln/functional_test_utils.rs b/lightning/src/ln/functional_test_utils.rs
index fbfe320..0bf60e4 100644
--- a/lightning/src/ln/functional_test_utils.rs
+++ b/lightning/src/ln/functional_test_utils.rs
@@ -1372,7 +1372,7 @@ macro_rules! reload_node {
($node: expr, $chanman_encoded: expr, $monitors_encoded: expr, $persister: ident, $new_chain_monitor: ident, $new_channelmanager: ident) => {
reload_node!(
$node,
- $crate::util::config::UserConfig::default(),
+ test_default_channel_config(),
$chanman_encoded,
$monitors_encoded,
$persister,
@@ -4331,6 +4331,7 @@ pub fn test_default_channel_config() -> UserConfig {
// feerate of 253).
default_config.channel_config.max_dust_htlc_exposure =
MaxDustHTLCExposure::FeeRateMultiplier(50_000_000 / 253);
+ default_config.reject_inbound_splices = false;
default_config
}
diff --git a/lightning/src/ln/splicing_tests.rs b/lightning/src/ln/splicing_tests.rs
index 62e1064..ac84eea 100644
--- a/lightning/src/ln/splicing_tests.rs
+++ b/lightning/src/ln/splicing_tests.rs
@@ -507,6 +507,66 @@ fn do_test_splice_state_reset_on_disconnect(reload: bool) {
lock_splice_after_blocks(&nodes[0], &nodes[1], channel_id, ANTI_REORG_DELAY - 1);
}
+#[test]
+fn test_config_reject_inbound_splices() {
+ // Tests that nodes with `reject_inbound_splices` properly reject inbound splices but still
+ // allow outbound ones.
+ let chanmon_cfgs = create_chanmon_cfgs(2);
+ let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
+ let mut config = test_default_channel_config();
+ config.reject_inbound_splices = true;
+ let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, Some(config)]);
+ let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
+
+ let node_id_0 = nodes[0].node.get_our_node_id();
+ let node_id_1 = nodes[1].node.get_our_node_id();
+
+ let (_, _, channel_id, _) =
+ create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100_000, 50_000_000);
+
+ let contribution = SpliceContribution::SpliceOut {
+ outputs: vec![TxOut {
+ value: Amount::from_sat(1_000),
+ script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(),
+ }],
+ };
+ nodes[0]
+ .node
+ .splice_channel(
+ &channel_id,
+ &node_id_1,
+ contribution.clone(),
+ FEERATE_FLOOR_SATS_PER_KW,
+ None,
+ )
+ .unwrap();
+
+ let stfu = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1);
+ nodes[1].node.handle_stfu(node_id_0, &stfu);
+ let stfu = get_event_msg!(nodes[1], MessageSendEvent::SendStfu, node_id_0);
+ nodes[0].node.handle_stfu(node_id_1, &stfu);
+
+ let splice_init = get_event_msg!(nodes[0], MessageSendEvent::SendSpliceInit, node_id_1);
+ nodes[1].node.handle_splice_init(node_id_0, &splice_init);
+
+ let msg_events = nodes[1].node.get_and_clear_pending_msg_events();
+ assert_eq!(msg_events.len(), 1);
+ if let MessageSendEvent::HandleError { action, .. } = &msg_events[0] {
+ assert!(matches!(action, msgs::ErrorAction::DisconnectPeerWithWarning { .. }));
+ } else {
+ panic!("Expected MessageSendEvent::HandleError");
+ }
+
+ nodes[0].node.peer_disconnected(node_id_1);
+ nodes[1].node.peer_disconnected(node_id_0);
+ let mut reconnect_args = ReconnectArgs::new(&nodes[0], &nodes[1]);
+ reconnect_args.send_channel_ready = (true, true);
+ reconnect_args.send_announcement_sigs = (true, true);
+ reconnect_nodes(reconnect_args);
+
+ let _ = splice_channel(&nodes[1], &nodes[0], channel_id, contribution);
+}
+
#[test]
fn test_splice_in() {
let chanmon_cfgs = create_chanmon_cfgs(2);
diff --git a/lightning/src/util/config.rs b/lightning/src/util/config.rs
index 079ed19..500c0b7 100644
--- a/lightning/src/util/config.rs
+++ b/lightning/src/util/config.rs
@@ -956,6 +956,18 @@ pub struct UserConfig {
///
/// [`StaticInvoice`]: crate::offers::static_invoice::StaticInvoice
pub hold_outbound_htlcs_at_next_hop: bool,
+ /// If this is set to `true`, then inbound channel splice requests will be rejected. This
+ /// ensures backwards compatibility is not broken with LDK versions < 0.2 while a splice is
+ /// pending.
+ ///
+ /// Outbound channel splice requests (via [`ChannelManager::splice_channel`], an opt-in API) are
+ /// still allowed as users should be aware of the backwards compatibility risk prior to using
+ /// the functionality.
+ ///
+ /// Default value: `true`
+ ///
+ /// [`ChannelManager::splice_channel`]: crate::ln::channelmanager::ChannelManager::splice_channel
+ pub reject_inbound_splices: bool,
}
impl Default for UserConfig {
@@ -972,6 +984,7 @@ impl Default for UserConfig {
enable_dual_funded_channels: false,
enable_htlc_hold: false,
hold_outbound_htlcs_at_next_hop: false,
+ reject_inbound_splices: true,
}
}
}
@@ -994,6 +1007,7 @@ impl Readable for UserConfig {
enable_dual_funded_channels: Readable::read(reader)?,
hold_outbound_htlcs_at_next_hop: Readable::read(reader)?,
enable_htlc_hold: Readable::read(reader)?,
+ reject_inbound_splices: Readable::read(reader)?,
})
}
}
Why this scored 29/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.