Count zero-fee-commitments channels in anchor reserve check
What changed, and why it matters
This commit fixes an accounting bug in the Lightning Dev Kit. When a node uses a newer type of anchor-backed channel (called TRUC or zero-fee-commitments), the software was not counting that channel when checking whether the wallet still has enough on-chain money set aside for emergency fee bumps. That could let the node open more anchor channels than it can actually afford to close safely, raising the risk of being unable to pay fees to claim funds during a force close. The fix counts both old and new anchor channel types the same way and adds a regression test.
Upgrade to a release containing this commit for any node that enables negotiate_anchor_zero_fee_commitments. Until upgraded, operators should avoid relying on the automated anchor reserve check when opening multiple TRUC/0FC anchor channels and should manually ensure sufficient on-chain reserves.
Security signals we found
Reserve-accounting bug for anchor channels
Newer channel type (anchor_zero_fee_commitments / TRUC / option 41) omitted from safety check
Risk of over-committing on-chain reserves, leading to unfunded fee bumps on force close
Regression test added to prevent future omission
Evidence from the diff
The function can_support_additional_anchor_channel in lightning/src/util/anchor_channel_reserves.rs counts existing anchor channels to decide if the wallet’s UTXO reserve can support another anchor channel. Previously it only recognized channels whose ChannelTypeFeatures set supports_anchors_zero_fee_htlc_tx. Channels negotiated with supports_anchor_zero_fee_commitments (TRUC / 0FC, option 41) were ignored, even though they require the same on-chain reserve for commitment/HTLC fee bumps. The patch introduces is_anchor_channel_type, which returns true for either feature, and uses it in both the chain-monitor loop and the channel-manager loop. A regression test opens one 0FC channel with reserves sized for exactly one channel and asserts that a second channel is refused.
Changed components
lightning/src/util/anchor_channel_reserves.rscan_support_additional_anchor_channelget_supportable_anchor_channelsChannelTypeFeatures anchor detectionInspect captured patch +54 / −2
diff --git a/lightning/src/util/anchor_channel_reserves.rs b/lightning/src/util/anchor_channel_reserves.rs
index 2c09ddd..000f543 100644
--- a/lightning/src/util/anchor_channel_reserves.rs
+++ b/lightning/src/util/anchor_channel_reserves.rs
@@ -260,6 +260,13 @@ pub fn get_supportable_anchor_channels(
num_whole_utxos + total_fractional_amount.to_sat() / reserve_per_channel.to_sat() / 2
}
+/// Returns whether a channel of the given type requires an on-chain anchor reserve, i.e. uses
+/// either the `anchors_zero_fee_htlc_tx` or `anchor_zero_fee_commitments` (TRUC / 0FC) variant.
+fn is_anchor_channel_type(channel_type: &ChannelTypeFeatures) -> bool {
+ channel_type.supports_anchors_zero_fee_htlc_tx()
+ || channel_type.supports_anchor_zero_fee_commitments()
+}
+
/// Verifies whether the anchor channel reserve provided by `utxos` is sufficient to support
/// an additional anchor channel.
///
@@ -296,7 +303,7 @@ where
} else {
continue;
};
- if channel_monitor.channel_type_features().supports_anchors_zero_fee_htlc_tx()
+ if is_anchor_channel_type(&channel_monitor.channel_type_features())
&& !channel_monitor.get_claimable_balances().is_empty()
{
anchor_channels.insert(channel_id);
@@ -305,7 +312,7 @@ where
// Also include channels that are in the middle of negotiation or anchor channels that don't have
// a ChannelMonitor yet.
for channel in a_channel_manager.get_cm().list_channels() {
- if channel.channel_type.map_or(true, |ct| ct.supports_anchors_zero_fee_htlc_tx()) {
+ if channel.channel_type.map_or(true, |ct| is_anchor_channel_type(&ct)) {
anchor_channels.insert(channel.channel_id);
}
}
@@ -315,6 +322,7 @@ where
#[cfg(test)]
mod test {
use super::*;
+ use crate::ln::functional_test_utils::*;
use bitcoin::{OutPoint, ScriptBuf, Sequence, TxOut, Txid};
use std::str::FromStr;
@@ -425,4 +433,48 @@ mod test {
1068
);
}
+
+ #[test]
+ fn test_can_support_additional_anchor_channel_zero_fee_commitments() {
+ // Regression test: a channel that uses the `anchor_zero_fee_commitments`
+ // (option 41) variant is just as much an anchor channel — and requires
+ // the same on-chain reserve — as one using `anchors_zero_fee_htlc_tx`.
+ // The reserve check must therefore count it as an existing anchor
+ // channel when deciding whether the wallet can safely support an
+ // additional one. Currently `can_support_additional_anchor_channel`
+ // only counts channels whose features set `anchors_zero_fee_htlc_tx`,
+ // so a node whose reserves are exhausted by zero-fee-commitment
+ // channels is incorrectly told it can open another anchor channel.
+ let mut cfg = test_default_channel_config();
+ cfg.channel_handshake_config.negotiate_anchor_zero_fee_commitments = true;
+
+ 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, &[Some(cfg.clone()), Some(cfg)]);
+ let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
+
+ create_chan_between_nodes(&nodes[0], &nodes[1]);
+
+ let channels = nodes[0].node.list_channels();
+ assert_eq!(channels.len(), 1);
+ let channel_type = channels[0].channel_type.as_ref().unwrap();
+ assert!(channel_type.supports_anchor_zero_fee_commitments());
+ // Sanity check: a zero-fee-commitments channel does not also set the
+ // older anchors_zero_fee_htlc_tx feature.
+ assert!(!channel_type.supports_anchors_zero_fee_htlc_tx());
+
+ let context = AnchorChannelReserveContext::default();
+ let reserve = get_reserve_per_channel(&context);
+ // Provide a single UTXO with enough value to cover one channel reserve.
+ let utxos = vec![make_p2wpkh_utxo(reserve * 2)];
+
+ // We already have one TRUC anchor channel and only enough reserve for
+ // a single channel; we must not authorize an additional one.
+ assert!(!can_support_additional_anchor_channel(
+ &context,
+ &utxos,
+ nodes[0].node,
+ &nodes[0].chain_monitor.chain_monitor,
+ ));
+ }
}
Why this scored 60/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.