Enforce `min_funding_satoshis` after splices
What changed, and why it matters
This commit fixes a consistency gap in the Lightning Dev Kit: a setting that normally blocks very small inbound channels when they are first opened was not being checked after a 'splice' that shrinks the channel. A splice lets both parties add or remove funds from an existing channel without closing it. The change now applies the same minimum-channel-size rule to inbound splices where the other side is taking money out, preventing the channel from becoming smaller than the node operator allowed. The patch is defensive and adds tests, but it is narrowly scoped to one configuration check.
Review whether any other handshake limits (e.g., max funding, max HTLC in-flight, channel reserve rules) are similarly bypassed by splice paths. Consider whether the check should also apply to outbound channels or to all splices, not only those with a negative counterparty contribution. The current patch is a targeted fix and should be backported if the affected release branch supports splicing.
Security signals we found
Missing authorization/policy enforcement: an existing inbound-channel policy (`min_funding_satoshis`) was not enforced after splice operations that reduce channel capacity.
Defensive hardening: the patch closes the gap by re-applying the configured minimum to splice-in/splice-out/RBF flows where the counterparty removes funds.
Peer-disconnect on violation: failures are surfaced as `WarnAndDisconnect`, terminating the offending peer's connection.
Test coverage: six new unit tests assert the new behavior for both accepted and rejected cases across splice message types.
Evidence from the diff
The patch threads channel_handshake_limits.min_funding_satoshis into the splice validation path. FundingScope::for_splice now receives min_funding_satoshis and, for inbound channels (!prev_funding.is_outbound()), rejects the splice when post_channel_value_sat < min_funding_satoshis and their_funding_contribution.is_negative(). The limit is passed through validate_splice_contributions, splice_init, tx_init_rbf, tx_ack_rbf, splice_ack, and their corresponding validation helpers. channelmanager.rs reads the value from config and passes it at each call site. The change does not apply when the counterparty contributes non-negative funds, nor to outbound channels. Tests cover positive/negative counterparty contributions for SpliceInit, SpliceAck, TxInitRbf, and TxAckRbf.
Changed components
lightning/src/ln/channel.rs - FundingScope::for_splice and splice/RBF validation helperslightning/src/ln/channelmanager.rs - splice/RBF message handlerslightning/src/ln/splicing_tests.rs - new regression testslightning/src/util/config.rs - documentation for ChannelHandshakeLimits::min_funding_satoshisInspect captured patch +339 / −13
diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs
index 58dd6ea..fde56a9 100644
--- a/lightning/src/ln/channel.rs
+++ b/lightning/src/ln/channel.rs
@@ -2778,7 +2778,7 @@ impl FundingScope {
fn for_splice<SP: SignerProvider>(
prev_funding: &Self, context: &ChannelContext<SP>, our_funding_contribution: SignedAmount,
their_funding_contribution: SignedAmount, counterparty_funding_pubkey: PublicKey,
- our_new_holder_keys: ChannelPublicKeys,
+ our_new_holder_keys: ChannelPublicKeys, min_funding_satoshis: u64,
) -> Result<Self, String> {
if our_funding_contribution.unsigned_abs() > Amount::MAX_MONEY {
return Err(format!(
@@ -2817,13 +2817,27 @@ impl FundingScope {
),
)?;
- let post_channel_value_sat = prev_funding.get_value_satoshis()
+ let post_channel_value_sat = prev_funding
+ .get_value_satoshis()
.checked_add_signed(our_funding_contribution.to_sat())
.and_then(|v| v.checked_add_signed(their_funding_contribution.to_sat()))
- .ok_or(format!("The sum of contributions {our_funding_contribution} and {their_funding_contribution} is greater than the channel's value"))?;
+ .ok_or(format!(
+ "The sum of contributions {our_funding_contribution} and \
+ {their_funding_contribution} is greater than the channel's value"
+ ))?;
if post_channel_value_sat < MIN_CHANNEL_VALUE_SATOSHIS {
return Err(format!(
- "Spliced channel value must be at least 1000 satoshis. It would be {post_channel_value_sat}",
+ "Spliced channel value must be at least 1000 satoshis. It would be \
+ {post_channel_value_sat}"
+ ));
+ }
+ if post_channel_value_sat < min_funding_satoshis
+ && their_funding_contribution.is_negative()
+ && !prev_funding.is_outbound()
+ {
+ return Err(format!(
+ "Spliced channel value {post_channel_value_sat} would be smaller \
+ than the configured min_funding_satoshis {min_funding_satoshis}"
));
}
@@ -13048,6 +13062,7 @@ where
fn validate_splice_contributions(
&self, our_funding_contribution: SignedAmount, their_funding_contribution: SignedAmount,
counterparty_funding_pubkey: PublicKey, our_new_holder_keys: ChannelPublicKeys,
+ min_funding_satoshis: u64,
) -> Result<FundingScope, String> {
let candidate_scope = FundingScope::for_splice(
&self.funding,
@@ -13056,6 +13071,7 @@ where
their_funding_contribution,
counterparty_funding_pubkey,
our_new_holder_keys,
+ min_funding_satoshis,
)
.map_err(|e| format!("Channel {} cannot be spliced; {}", self.context.channel_id(), e))?;
@@ -13166,7 +13182,7 @@ where
pub(crate) fn splice_init<ES: EntropySource, L: Logger>(
&mut self, msg: &msgs::SpliceInit, entropy_source: &ES, holder_node_id: &PublicKey,
- logger: &L,
+ min_funding_satoshis: u64, logger: &L,
) -> Result<msgs::SpliceAck, InteractiveTxMsgError> {
self.validate_splice_init(msg).map_err(|e| self.quiescent_negotiation_err(e))?;
@@ -13199,6 +13215,7 @@ where
their_funding_contribution,
msg.funding_pubkey,
holder_pubkeys,
+ min_funding_satoshis,
)
.map_err(|e| self.quiescent_negotiation_err(ChannelError::WarnAndDisconnect(e)))?;
@@ -13334,7 +13351,7 @@ where
pub(crate) fn tx_init_rbf<ES: EntropySource, F: FeeEstimator, L: Logger>(
&mut self, msg: &msgs::TxInitRbf, entropy_source: &ES, holder_node_id: &PublicKey,
- fee_estimator: &LowerBoundedFeeEstimator<F>, logger: &L,
+ fee_estimator: &LowerBoundedFeeEstimator<F>, min_funding_satoshis: u64, logger: &L,
) -> Result<msgs::TxAckRbf, InteractiveTxMsgError> {
let (holder_pubkeys, counterparty_funding_pubkey) = self
.validate_tx_init_rbf(msg, fee_estimator)
@@ -13381,6 +13398,7 @@ where
their_funding_contribution,
counterparty_funding_pubkey,
holder_pubkeys,
+ min_funding_satoshis,
)
.map_err(|e| self.quiescent_negotiation_err(ChannelError::WarnAndDisconnect(e)))?;
@@ -13452,7 +13470,9 @@ where
})
}
- fn validate_tx_ack_rbf(&self, msg: &msgs::TxAckRbf) -> Result<FundingScope, ChannelError> {
+ fn validate_tx_ack_rbf(
+ &self, msg: &msgs::TxAckRbf, min_funding_satoshis: u64,
+ ) -> Result<FundingScope, ChannelError> {
let pending_splice = self
.pending_splice
.as_ref()
@@ -13478,6 +13498,7 @@ where
their_funding_contribution,
counterparty_funding_pubkey,
holder_pubkeys,
+ min_funding_satoshis,
)
.map_err(|e| ChannelError::WarnAndDisconnect(e))?;
@@ -13486,9 +13507,9 @@ where
pub(crate) fn tx_ack_rbf<ES: EntropySource, L: Logger>(
&mut self, msg: &msgs::TxAckRbf, entropy_source: &ES, holder_node_id: &PublicKey,
- logger: &L,
+ min_funding_satoshis: u64, logger: &L,
) -> Result<Option<InteractiveTxMessageSend>, ChannelError> {
- let rbf_funding = self.validate_tx_ack_rbf(msg)?;
+ let rbf_funding = self.validate_tx_ack_rbf(msg, min_funding_satoshis)?;
log_info!(
logger,
@@ -13519,9 +13540,9 @@ where
pub(crate) fn splice_ack<ES: EntropySource, L: Logger>(
&mut self, msg: &msgs::SpliceAck, entropy_source: &ES, holder_node_id: &PublicKey,
- logger: &L,
+ min_funding_satoshis: u64, logger: &L,
) -> Result<Option<InteractiveTxMessageSend>, ChannelError> {
- let splice_funding = self.validate_splice_ack(msg)?;
+ let splice_funding = self.validate_splice_ack(msg, min_funding_satoshis)?;
log_info!(
logger,
@@ -13553,7 +13574,9 @@ where
Ok(tx_msg_opt)
}
- fn validate_splice_ack(&self, msg: &msgs::SpliceAck) -> Result<FundingScope, ChannelError> {
+ fn validate_splice_ack(
+ &self, msg: &msgs::SpliceAck, min_funding_satoshis: u64,
+ ) -> Result<FundingScope, ChannelError> {
// TODO(splicing): Add check that we are the splice (quiescence) initiator
let pending_splice = self
@@ -13576,6 +13599,7 @@ where
their_funding_contribution,
msg.funding_pubkey,
new_keys,
+ min_funding_satoshis,
)
.map_err(|e| ChannelError::WarnAndDisconnect(e))?;
@@ -13692,6 +13716,9 @@ where
SignedAmount::ZERO,
funding.counterparty_funding_pubkey().clone(),
funding.get_holder_pubkeys().clone(),
+ // When the counterparty's contribution is non-negative, we don't validate
+ // the post splice channel value against `min_funding_satoshis`
+ 0,
)
.unwrap();
// Splice-out an additional satoshi, and validation fails!
@@ -13700,6 +13727,9 @@ where
SignedAmount::ZERO,
funding.counterparty_funding_pubkey().clone(),
funding.get_holder_pubkeys().clone(),
+ // When the counterparty's contribution is non-negative, we don't validate
+ // the post splice channel value against `min_funding_satoshis`
+ 0,
)
.unwrap_err();
}
diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs
index 19fd2f9..3575414 100644
--- a/lightning/src/ln/channelmanager.rs
+++ b/lightning/src/ln/channelmanager.rs
@@ -13384,6 +13384,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
msg,
&self.entropy_source,
&self.get_our_node_id(),
+ self.config.read().unwrap().channel_handshake_limits.min_funding_satoshis,
&self.logger,
) {
Ok(splice_ack_msg) => {
@@ -13441,6 +13442,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
&self.entropy_source,
&self.get_our_node_id(),
&self.fee_estimator,
+ self.config.read().unwrap().channel_handshake_limits.min_funding_satoshis,
&self.logger,
) {
Ok(tx_ack_rbf_msg) => {
@@ -13497,6 +13499,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
msg,
&self.entropy_source,
&self.get_our_node_id(),
+ self.config.read().unwrap().channel_handshake_limits.min_funding_satoshis,
&self.logger,
);
let tx_msg_opt =
@@ -13541,6 +13544,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
msg,
&self.entropy_source,
&self.get_our_node_id(),
+ self.config.read().unwrap().channel_handshake_limits.min_funding_satoshis,
&self.logger,
);
let tx_msg_opt =
diff --git a/lightning/src/ln/splicing_tests.rs b/lightning/src/ln/splicing_tests.rs
index 6bd5d52..a6823e3 100644
--- a/lightning/src/ln/splicing_tests.rs
+++ b/lightning/src/ln/splicing_tests.rs
@@ -163,6 +163,35 @@ impl CoinSelectionSourceSync for TightBudgetWallet {
}
}
+#[cfg(test)]
+fn config_with_min_funding_satoshis(min_funding_satoshis: u64) -> UserConfig {
+ let mut config = test_default_channel_config();
+ config.channel_handshake_limits.min_funding_satoshis = min_funding_satoshis;
+ config.channel_handshake_config.announced_channel_max_inbound_htlc_value_in_flight_percentage =
+ 100;
+ config
+}
+
+#[cfg(test)]
+fn assert_min_funding_error<'a, 'b, 'c>(node: &Node<'a, 'b, 'c>, min_funding_satoshis: u64) {
+ let msg_events = node.node.get_and_clear_pending_msg_events();
+ assert_eq!(msg_events.len(), 1, "{msg_events:?}");
+ match &msg_events[0] {
+ MessageSendEvent::HandleError {
+ action: msgs::ErrorAction::DisconnectPeerWithWarning { msg },
+ ..
+ } => {
+ assert!(
+ msg.data
+ .contains(&format!("configured min_funding_satoshis {min_funding_satoshis}")),
+ "unexpected warning: {}",
+ msg.data
+ );
+ },
+ _ => panic!("Expected HandleError with warning, got {:?}", msg_events[0]),
+ }
+}
+
pub fn negotiate_splice_tx<'a, 'b, 'c, 'd>(
initiator: &'a Node<'b, 'c, 'd>, acceptor: &'a Node<'b, 'c, 'd>, channel_id: ChannelId,
funding_contribution: FundingContribution,
@@ -1209,6 +1238,268 @@ fn test_splice_in() {
let _ = send_payment(&nodes[0], &[&nodes[1]], htlc_limit_msat);
}
+#[test]
+fn test_min_funding_satoshis_allows_splice_init_with_positive_counterparty_contribution() {
+ let chanmon_cfgs = create_chanmon_cfgs(2);
+ let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
+ 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 min_funding_satoshis = 150_000;
+ let initial_channel_value_sat = 100_000;
+ let (_, _, channel_id, _) =
+ create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0);
+ nodes[1].node.set_current_config(config_with_min_funding_satoshis(min_funding_satoshis));
+
+ let added_value = Amount::from_sat(10_000);
+ provide_utxo_reserves(&nodes, 1, Amount::from_sat(100_000));
+ let _funding_contribution = initiate_splice_in(&nodes[0], &nodes[1], channel_id, added_value);
+
+ let stfu_init = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1);
+ nodes[1].node.handle_stfu(node_id_0, &stfu_init);
+ let stfu_ack = get_event_msg!(nodes[1], MessageSendEvent::SendStfu, node_id_0);
+ nodes[0].node.handle_stfu(node_id_1, &stfu_ack);
+
+ let splice_init = get_event_msg!(nodes[0], MessageSendEvent::SendSpliceInit, node_id_1);
+ assert!(splice_init.funding_contribution_satoshis > 0);
+ nodes[1].node.handle_splice_init(node_id_0, &splice_init);
+ let _splice_ack = get_event_msg!(nodes[1], MessageSendEvent::SendSpliceAck, node_id_0);
+}
+
+#[test]
+fn test_min_funding_satoshis_rejects_splice_init_with_negative_counterparty_contribution() {
+ let chanmon_cfgs = create_chanmon_cfgs(2);
+ let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
+ 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 min_funding_satoshis = 150_000;
+ let initial_channel_value_sat = 100_000;
+ let (_, _, channel_id, _) =
+ create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0);
+ nodes[1].node.set_current_config(config_with_min_funding_satoshis(min_funding_satoshis));
+
+ let outputs = vec![TxOut {
+ value: Amount::from_sat(10_000),
+ script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(),
+ }];
+ let _funding_contribution =
+ initiate_splice_out(&nodes[0], &nodes[1], channel_id, outputs).unwrap();
+
+ let stfu_init = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1);
+ nodes[1].node.handle_stfu(node_id_0, &stfu_init);
+ let stfu_ack = get_event_msg!(nodes[1], MessageSendEvent::SendStfu, node_id_0);
+ nodes[0].node.handle_stfu(node_id_1, &stfu_ack);
+
+ let splice_init = get_event_msg!(nodes[0], MessageSendEvent::SendSpliceInit, node_id_1);
+ assert!(splice_init.funding_contribution_satoshis < 0);
+ nodes[1].node.handle_splice_init(node_id_0, &splice_init);
+ assert_min_funding_error(&nodes[1], min_funding_satoshis);
+}
+
+#[test]
+fn test_min_funding_satoshis_allows_outbound_splice_ack_with_negative_counterparty_contribution() {
+ let chanmon_cfgs = create_chanmon_cfgs(2);
+ let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
+ 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 min_funding_satoshis = 150_000;
+ let initial_channel_value_sat = 100_000;
+ let (_, _, channel_id, _) = create_announced_chan_between_nodes_with_value(
+ &nodes,
+ 0,
+ 1,
+ initial_channel_value_sat,
+ 50_000_000,
+ );
+ nodes[0].node.set_current_config(config_with_min_funding_satoshis(min_funding_satoshis));
+
+ let added_value = Amount::from_sat(10_000);
+ provide_utxo_reserves(&nodes, 1, Amount::from_sat(100_000));
+ let _node_0_contribution = initiate_splice_in(&nodes[0], &nodes[1], channel_id, added_value);
+ let outputs = vec![TxOut {
+ value: Amount::from_sat(10_000),
+ script_pubkey: nodes[1].wallet_source.get_change_script().unwrap(),
+ }];
+ let _node_1_contribution =
+ initiate_splice_out(&nodes[1], &nodes[0], channel_id, outputs).unwrap();
+
+ let stfu_0 = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1);
+ let stfu_1 = get_event_msg!(nodes[1], MessageSendEvent::SendStfu, node_id_0);
+ nodes[1].node.handle_stfu(node_id_0, &stfu_0);
+ assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty());
+ nodes[0].node.handle_stfu(node_id_1, &stfu_1);
+
+ 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 splice_ack = get_event_msg!(nodes[1], MessageSendEvent::SendSpliceAck, node_id_0);
+ assert!(splice_ack.funding_contribution_satoshis < 0);
+ nodes[0].node.handle_splice_ack(node_id_1, &splice_ack);
+
+ let msg_events = nodes[0].node.get_and_clear_pending_msg_events();
+ assert!(!msg_events.is_empty(), "{msg_events:?}");
+ assert!(
+ !msg_events.iter().any(|event| matches!(event, MessageSendEvent::HandleError { .. })),
+ "{msg_events:?}"
+ );
+}
+
+#[test]
+fn test_min_funding_satoshis_rejects_splice_ack_with_negative_counterparty_contribution() {
+ let chanmon_cfgs = create_chanmon_cfgs(2);
+ let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
+ 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 min_funding_satoshis = 150_000;
+ let initial_channel_value_sat = 100_000;
+ let (_, _, channel_id, _) = create_announced_chan_between_nodes_with_value(
+ &nodes,
+ 1,
+ 0,
+ initial_channel_value_sat,
+ 50_000_000,
+ );
+ nodes[0].node.set_current_config(config_with_min_funding_satoshis(min_funding_satoshis));
+
+ let added_value = Amount::from_sat(10_000);
+ provide_utxo_reserves(&nodes, 1, Amount::from_sat(100_000));
+ let _node_0_contribution = initiate_splice_in(&nodes[0], &nodes[1], channel_id, added_value);
+
+ let stfu_init = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1);
+ nodes[1].node.handle_stfu(node_id_0, &stfu_init);
+
+ let outputs = vec![TxOut {
+ value: Amount::from_sat(10_000),
+ script_pubkey: nodes[1].wallet_source.get_change_script().unwrap(),
+ }];
+ let _node_1_contribution =
+ initiate_splice_out(&nodes[1], &nodes[0], channel_id, outputs).unwrap();
+
+ let stfu_ack = get_event_msg!(nodes[1], MessageSendEvent::SendStfu, node_id_0);
+ assert!(!stfu_ack.initiator);
+ nodes[0].node.handle_stfu(node_id_1, &stfu_ack);
+
+ 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 splice_ack = get_event_msg!(nodes[1], MessageSendEvent::SendSpliceAck, node_id_0);
+ assert!(splice_ack.funding_contribution_satoshis < 0);
+ nodes[0].node.handle_splice_ack(node_id_1, &splice_ack);
+ assert_min_funding_error(&nodes[0], min_funding_satoshis);
+}
+
+#[test]
+fn test_min_funding_satoshis_rejects_tx_init_rbf_with_negative_counterparty_contribution() {
+ let chanmon_cfgs = create_chanmon_cfgs(2);
+ let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
+ 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 min_funding_satoshis = 150_000;
+ let initial_channel_value_sat = 100_000;
+ let (_, _, channel_id, _) =
+ create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0);
+ nodes[1].node.set_current_config(config_with_min_funding_satoshis(min_funding_satoshis));
+
+ let added_value = Amount::from_sat(10_000);
+ provide_utxo_reserves(&nodes, 1, Amount::from_sat(100_000));
+ let first_contribution = initiate_splice_in(&nodes[1], &nodes[0], channel_id, added_value);
+ let (_first_splice_tx, _) =
+ splice_channel(&nodes[1], &nodes[0], channel_id, first_contribution);
+
+ let funding_template = nodes[0].node.splice_channel(&channel_id, &node_id_1).unwrap();
+ let rbf_feerate = funding_template.min_rbf_feerate().unwrap();
+ let outputs = vec![TxOut {
+ value: Amount::from_sat(10_000),
+ script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(),
+ }];
+ let rbf_contribution = funding_template.splice_out(outputs, rbf_feerate, FeeRate::MAX).unwrap();
+ nodes[0].node.funding_contributed(&channel_id, &node_id_1, rbf_contribution, None).unwrap();
+
+ let stfu_init = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1);
+ nodes[1].node.handle_stfu(node_id_0, &stfu_init);
+ let stfu_ack = get_event_msg!(nodes[1], MessageSendEvent::SendStfu, node_id_0);
+ nodes[0].node.handle_stfu(node_id_1, &stfu_ack);
+
+ let tx_init_rbf = get_event_msg!(nodes[0], MessageSendEvent::SendTxInitRbf, node_id_1);
+ assert!(tx_init_rbf.funding_output_contribution.unwrap() < 0);
+ nodes[1].node.handle_tx_init_rbf(node_id_0, &tx_init_rbf);
+ assert_min_funding_error(&nodes[1], min_funding_satoshis);
+}
+
+#[test]
+fn test_min_funding_satoshis_rejects_tx_ack_rbf_with_negative_counterparty_contribution() {
+ let chanmon_cfgs = create_chanmon_cfgs(2);
+ let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
+ 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 min_funding_satoshis = 150_000;
+ let initial_channel_value_sat = 100_000;
+ let (_, _, channel_id, _) = create_announced_chan_between_nodes_with_value(
+ &nodes,
+ 1,
+ 0,
+ initial_channel_value_sat,
+ 50_000_000,
+ );
+ nodes[0].node.set_current_config(config_with_min_funding_satoshis(min_funding_satoshis));
+
+ let added_value = Amount::from_sat(10_000);
+ provide_utxo_reserves(&nodes, 1, Amount::from_sat(100_000));
+ let first_contribution = initiate_splice_in(&nodes[0], &nodes[1], channel_id, added_value);
+ let (_first_splice_tx, _) =
+ splice_channel(&nodes[0], &nodes[1], channel_id, first_contribution);
+
+ let funding_template_0 = nodes[0].node.splice_channel(&channel_id, &node_id_1).unwrap();
+ let rbf_feerate = funding_template_0.min_rbf_feerate().unwrap();
+ let node_0_contribution =
+ funding_template_0.with_prior_contribution(rbf_feerate, FeeRate::MAX).build().unwrap();
+ nodes[0].node.funding_contributed(&channel_id, &node_id_1, node_0_contribution, None).unwrap();
+
+ let stfu_init = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1);
+ nodes[1].node.handle_stfu(node_id_0, &stfu_init);
+
+ let outputs = vec![TxOut {
+ value: Amount::from_sat(10_000),
+ script_pubkey: nodes[1].wallet_source.get_change_script().unwrap(),
+ }];
+ let funding_template_1 = nodes[1].node.splice_channel(&channel_id, &node_id_0).unwrap();
+ let node_1_contribution =
+ funding_template_1.splice_out(outputs, rbf_feerate, FeeRate::MAX).unwrap();
+ nodes[1].node.funding_contributed(&channel_id, &node_id_0, node_1_contribution, None).unwrap();
+
+ let stfu_ack = get_event_msg!(nodes[1], MessageSendEvent::SendStfu, node_id_0);
+ assert!(!stfu_ack.initiator);
+ nodes[0].node.handle_stfu(node_id_1, &stfu_ack);
+
+ let tx_init_rbf = get_event_msg!(nodes[0], MessageSendEvent::SendTxInitRbf, node_id_1);
+ nodes[1].node.handle_tx_init_rbf(node_id_0, &tx_init_rbf);
+ let tx_ack_rbf = get_event_msg!(nodes[1], MessageSendEvent::SendTxAckRbf, node_id_0);
+ assert!(tx_ack_rbf.funding_output_contribution.unwrap() < 0);
+ nodes[0].node.handle_tx_ack_rbf(node_id_1, &tx_ack_rbf);
+ assert_min_funding_error(&nodes[0], min_funding_satoshis);
+}
+
#[test]
fn test_splice_out() {
let chanmon_cfgs = create_chanmon_cfgs(2);
diff --git a/lightning/src/util/config.rs b/lightning/src/util/config.rs
index 78ab45d..fa01f8e 100644
--- a/lightning/src/util/config.rs
+++ b/lightning/src/util/config.rs
@@ -323,7 +323,8 @@ impl Readable for ChannelHandshakeConfig {
#[derive(Copy, Clone, Debug)]
pub struct ChannelHandshakeLimits {
/// Minimum allowed satoshis when a channel is funded. This is supplied by the sender and so
- /// only applies to inbound channels.
+ /// only applies to inbound channels. It is also enforced for inbound channels on splices in
+ /// which the counterparty's contribution is negative.
///
/// Default value: `1000`
///
Why this scored 52/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.