Assert peer supports splicing before splicing channel
What changed, and why it matters
This commit adds a missing safety check in the Lightning Dev Kit's channel manager. Before allowing a user to initiate a channel splice, it now verifies that the connected peer has advertised support for splicing and for quiescence (a required prerequisite). Without this check, a node could try to start a splice with a peer that doesn't understand the protocol, likely causing the operation to fail or the channel to close unexpectedly. The fix is defensive and prevents API misuse rather than fixing an active remote exploit.
Treat as a low-severity hardening fix. Reviewers should confirm that `latest_features` is reliably updated on reconnect and that no other splice entry points bypass this check. No immediate security response is indicated unless splicing is already deployed in production.
Security signals we found
Missing input validation on peer feature bits before initiating protocol extension
Defensive check prevents protocol mismatch that could lead to channel unavailability or forced closure
New unit tests cover both missing splicing and missing quiescence feature cases
Evidence from the diff
The patch modifies ChannelManager::splice_channel in lightning/src/ln/channelmanager.rs to inspect peer_state.latest_features and return APIError::ChannelUnavailable if the peer does not advertise splicing or quiescence feature bits. Previously, the code located the channel without validating peer feature support. A new unit test in splicing_tests.rs exercises both rejection paths: one where splicing is disabled, and one where splicing is enabled but quiescence is disabled. The change is purely an API-level guard with no wire-protocol or cryptographic changes.
Changed components
lightning/src/ln/channelmanager.rslightning/src/ln/splicing_tests.rsChannelManager::splice_channelInspect captured patch +77 / −3
diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs
index 72585d6..8595b23 100644
--- a/lightning/src/ln/channelmanager.rs
+++ b/lightning/src/ln/channelmanager.rs
@@ -4774,8 +4774,17 @@ where
Err(e) => return Err(e),
};
- let mut peer_state_lock = peer_state_mutex.lock().unwrap();
- let peer_state = &mut *peer_state_lock;
+ let mut peer_state = peer_state_mutex.lock().unwrap();
+ if !peer_state.latest_features.supports_splicing() {
+ return Err(APIError::ChannelUnavailable {
+ err: "Peer does not support splicing".to_owned(),
+ });
+ }
+ if !peer_state.latest_features.supports_quiescence() {
+ return Err(APIError::ChannelUnavailable {
+ err: "Peer does not support quiescence, a splicing prerequisite".to_owned(),
+ });
+ }
// Look for the channel
match peer_state.channel_by_id.entry(*channel_id) {
diff --git a/lightning/src/ln/splicing_tests.rs b/lightning/src/ln/splicing_tests.rs
index a96af7b..a05c0bd 100644
--- a/lightning/src/ln/splicing_tests.rs
+++ b/lightning/src/ln/splicing_tests.rs
@@ -17,7 +17,9 @@ use crate::events::bump_transaction::sync::WalletSourceSync;
use crate::events::{ClosureReason, Event, FundingInfo, HTLCHandlingFailureType};
use crate::ln::chan_utils;
use crate::ln::channel::CHANNEL_ANNOUNCEMENT_PROPAGATION_DELAY;
-use crate::ln::channelmanager::{PaymentId, RecipientOnionFields, BREAKDOWN_TIMEOUT};
+use crate::ln::channelmanager::{
+ provided_init_features, PaymentId, RecipientOnionFields, BREAKDOWN_TIMEOUT,
+};
use crate::ln::functional_test_utils::*;
use crate::ln::funding::{FundingTxInput, SpliceContribution};
use crate::ln::msgs::{self, BaseMessageHandler, ChannelMessageHandler, MessageSendEvent};
@@ -30,6 +32,69 @@ use crate::util::test_channel_signer::SignerOp;
use bitcoin::secp256k1::PublicKey;
use bitcoin::{Amount, OutPoint as BitcoinOutPoint, ScriptBuf, Transaction, TxOut};
+#[test]
+fn test_splicing_not_supported_api_error() {
+ let chanmon_cfgs = create_chanmon_cfgs(2);
+ let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
+ let mut features = provided_init_features(&test_default_channel_config());
+ features.clear_splicing();
+ *node_cfgs[0].override_init_features.borrow_mut() = Some(features);
+ let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]);
+ 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(&nodes, 0, 1);
+
+ let bs_contribution = SpliceContribution::SpliceIn {
+ value: Amount::ZERO,
+ inputs: Vec::new(),
+ change_script: None,
+ };
+
+ let res = nodes[1].node.splice_channel(
+ &channel_id,
+ &node_id_0,
+ bs_contribution.clone(),
+ 0, // funding_feerate_per_kw,
+ None, // locktime
+ );
+ match res {
+ Err(APIError::ChannelUnavailable { err }) => {
+ assert!(err.contains("Peer does not support splicing"))
+ },
+ _ => panic!("Wrong error {:?}", res.err().unwrap()),
+ }
+
+ nodes[0].node.peer_disconnected(node_id_1);
+ nodes[1].node.peer_disconnected(node_id_0);
+
+ let mut features = nodes[0].node.init_features();
+ features.set_splicing_optional();
+ features.clear_quiescence();
+ *nodes[0].override_init_features.borrow_mut() = Some(features);
+
+ 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 res = nodes[1].node.splice_channel(
+ &channel_id,
+ &node_id_0,
+ bs_contribution,
+ 0, // funding_feerate_per_kw,
+ None, // locktime
+ );
+ match res {
+ Err(APIError::ChannelUnavailable { err }) => {
+ assert!(err.contains("Peer does not support quiescence, a splicing prerequisite"))
+ },
+ _ => panic!("Wrong error {:?}", res.err().unwrap()),
+ }
+}
+
#[test]
fn test_v1_splice_in_negative_insufficient_inputs() {
let chanmon_cfgs = create_chanmon_cfgs(2);
Why this scored 34/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.