Disallow holders from selecting 0-reserve in legacy channels
What changed, and why it matters
This commit tightens the rules for a special kind of Lightning channel. In older-style ('legacy') channels, it now prevents the local side from setting a zero channel reserve. A channel reserve is a small amount of bitcoin each party must keep on their side of the channel as a security deposit. Allowing it to be zero on legacy channels could remove an economic deterrent that helps stop certain cheating tactics, so the code now rejects that configuration. Anchor-style channels, which use a newer design, can still use zero reserve.
Review whether counterparties setting 0-reserve on legacy channels also needs restriction, since the commit intentionally allows that direction. Ensure the new validation covers all channel-upgrade and dual-funding paths. Consider adding release-note guidance for users relying on 0-reserve trusted opens.
Security signals we found
0-reserve restriction added specifically for legacy/anchorless channels
Economic disincentive against revoked commitment broadcast may be weakened when reserve is zero
Validation added at channel open, accept, reestablish, and splice paths
Documentation updated to warn that zero-reserve is not allowed on legacy/anchorless channels
Evidence from the diff
The patch adds validation in four places in lightning/src/ln/channel.rs and updates documentation in channelmanager.rs. It rejects holder-selected 0-reserve when the channel type does not support anchors (neither supports_anchors_zero_fee_htlc_tx nor supports_anchor_zero_fee_commitments). Counterparties may still propose 0-reserve on legacy channels; only the holder side is blocked. The fuzz test chanmon_consistency.rs is adjusted so legacy channel test cases no longer attempt holder 0-reserve.
Changed components
lightning/src/ln/channel.rslightning/src/ln/channelmanager.rsfuzz/src/chanmon_consistency.rsInspect captured patch +43 / −7
diff --git a/fuzz/src/chanmon_consistency.rs b/fuzz/src/chanmon_consistency.rs
index 4a182c3..4ff0e4a 100644
--- a/fuzz/src/chanmon_consistency.rs
+++ b/fuzz/src/chanmon_consistency.rs
@@ -628,7 +628,7 @@ fn assert_action_timeout_awaiting_response(action: &msgs::ErrorAction) {
);
}
-#[derive(Copy, Clone)]
+#[derive(Clone, Copy, PartialEq)]
enum ChanType {
Legacy,
KeyedAnchors,
@@ -2082,19 +2082,20 @@ impl<'a, Out: Output + MaybeSend + MaybeSync> Harness<'a, Out> {
connect_peers(&nodes[0], &nodes[1]);
connect_peers(&nodes[1], &nodes[2]);
+ let set_0reserve = chan_type != ChanType::Legacy;
// Create 3 channels between A-B and 3 channels between B-C (6 total).
//
// Use distinct version numbers for each funding transaction so each test
// channel gets its own txid and funding outpoint.
// A-B: channel 2 A and B have 0-reserve (trusted open + trusted accept),
- // channel 3 A has 0-reserve (trusted accept).
+ // channel 3 A has 0-reserve (trusted accept), if channels are non-legacy.
make_channel(&nodes[0], &nodes[1], 1, false, false, &mut chain_state);
- make_channel(&nodes[0], &nodes[1], 2, true, true, &mut chain_state);
- make_channel(&nodes[0], &nodes[1], 3, false, true, &mut chain_state);
+ make_channel(&nodes[0], &nodes[1], 2, set_0reserve, set_0reserve, &mut chain_state);
+ make_channel(&nodes[0], &nodes[1], 3, false, set_0reserve, &mut chain_state);
// B-C: channel 4 B has 0-reserve (via trusted accept),
- // channel 5 C has 0-reserve (via trusted open).
- make_channel(&nodes[1], &nodes[2], 4, false, true, &mut chain_state);
- make_channel(&nodes[1], &nodes[2], 5, true, false, &mut chain_state);
+ // channel 5 C has 0-reserve (via trusted open), if channels are non-legacy.
+ make_channel(&nodes[1], &nodes[2], 4, false, set_0reserve, &mut chain_state);
+ make_channel(&nodes[1], &nodes[2], 5, set_0reserve, false, &mut chain_state);
make_channel(&nodes[1], &nodes[2], 6, false, false, &mut chain_state);
// Wipe the transactions-broadcasted set to make sure we don't broadcast
diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs
index 863256f..063b530 100644
--- a/lightning/src/ln/channel.rs
+++ b/lightning/src/ln/channel.rs
@@ -3832,6 +3832,14 @@ impl<SP: SignerProvider> ChannelContext<SP> {
"Funding must be smaller than the total bitcoin supply. It was {channel_value_satoshis}"
)));
}
+ if !channel_type.supports_anchors_zero_fee_htlc_tx()
+ && !channel_type.supports_anchor_zero_fee_commitments()
+ && holder_selected_channel_reserve_satoshis == 0
+ {
+ return Err(ChannelError::close(
+ "0-reserve is not allowed on legacy channels".to_owned(),
+ ));
+ }
if msg_channel_reserve_satoshis > channel_value_satoshis {
return Err(ChannelError::close(format!(
"Bogus channel_reserve_satoshis ({msg_channel_reserve_satoshis}). Must be no greater than channel_value_satoshis: {channel_value_satoshis}"
@@ -4323,6 +4331,14 @@ impl<SP: SignerProvider> ChannelContext<SP> {
}
let channel_type = get_initial_channel_type(&config, their_features);
+ if !channel_type.supports_anchors_zero_fee_htlc_tx()
+ && !channel_type.supports_anchor_zero_fee_commitments()
+ && holder_selected_channel_reserve_satoshis == 0
+ {
+ return Err(APIError::APIMisuseError {
+ err: "0-reserve is not allowed on legacy channels".to_owned(),
+ });
+ }
debug_assert!(!channel_type.supports_any_optional_bits());
debug_assert!(!channel_type
.requires_unknown_bits_from(&channelmanager::provided_channel_type_features(&config)));
@@ -4869,6 +4885,14 @@ impl<SP: SignerProvider> ChannelContext<SP> {
}
let channel_type = funding.get_channel_type();
+ if !channel_type.supports_anchors_zero_fee_htlc_tx()
+ && !channel_type.supports_anchor_zero_fee_commitments()
+ && funding.holder_selected_channel_reserve_satoshis == 0
+ {
+ return Err(ChannelError::close(
+ "0-reserve is not allowed on legacy channels".to_owned(),
+ ));
+ }
if common_fields.max_accepted_htlcs > max_htlcs(channel_type) {
return Err(ChannelError::close(format!(
"max_accepted_htlcs was {}. It must not be larger than {}",
@@ -6576,6 +6600,13 @@ impl<SP: SignerProvider> ChannelContext<SP> {
}
let next_channel_type = get_initial_channel_type(user_config, &eligible_features);
+ if !next_channel_type.supports_anchors_zero_fee_htlc_tx()
+ && !next_channel_type.supports_anchor_zero_fee_commitments()
+ && funding.holder_selected_channel_reserve_satoshis == 0
+ {
+ // 0-reserve is not allowed on legacy channels
+ return Err(());
+ }
self.feerate_per_kw =
selected_commitment_sat_per_1000_weight(&fee_estimator, &next_channel_type);
diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs
index 19fd2f9..55fe83b 100644
--- a/lightning/src/ln/channelmanager.rs
+++ b/lightning/src/ln/channelmanager.rs
@@ -3617,6 +3617,8 @@ pub enum TrustedChannelFeatures {
/// with a revoked commitment transaction *for free*.
///
/// Note that there is no guarantee that the counterparty accepts such a channel themselves.
+ ///
+ /// The zero-reserve feature is not allowed on legacy / anchorless channels.
ZeroReserve,
/// Sets the combination of [`TrustedChannelFeatures::ZeroConf`] and [`TrustedChannelFeatures::ZeroReserve`]
ZeroConfZeroReserve,
@@ -3873,6 +3875,8 @@ impl<
/// transaction *for free*.
///
/// Note that there is no guarantee that the counterparty accepts such a channel.
+ ///
+ /// The zero-reserve feature is not allowed on legacy / anchorless channels.
pub fn create_channel_to_trusted_peer_0reserve(
&self, their_network_key: PublicKey, channel_value_satoshis: u64, push_msat: u64,
user_channel_id: u128, temporary_channel_id: Option<ChannelId>,
Why this scored 59/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.