Set 0FC `HolderHTLCOutput`, `HolderFundingOutput` to require addl funds
What changed, and why it matters
This commit fixes how a Bitcoin Lightning node handles a newer channel type (zero-fee commitment transactions, or 0FC). Before the patch, the node did not recognize that 0FC channels need extra on-chain funds to broadcast certain rescue transactions, and it treated some 0FC outputs as easier to manipulate than they should be. The change makes 0FC channels behave more like the older CSV anchor channels for safety checks and fee requirements. This is a defensive correctness fix in fee-bumping logic; the commit message does not describe an active exploit.
Treat as a defensive fix and include in the next maintenance release. Review related 0FC paths for any other places where `supports_anchors_zero_fee_htlc_tx()` is checked without also considering `supports_anchor_zero_fee_commitments()`. No emergency response is indicated by the available materials.
Security signals we found
Fee-bumping requirement corrected for zero-fee commitment (0FC) channel outputs
Malleability classification of HolderHTLCOutput aligned with CSV anchor channels
Debug assertions extended to cover 0FC alongside existing zero-fee HTLC anchor checks
No explicit vulnerability, CVE, or exploit described in commit or supplied references
Evidence from the diff
The patch updates HolderHTLCOutput and HolderFundingOutput handling in lightning/src/chain/package.rs and lightning/src/chain/channelmonitor.rs so that channels supporting supports_anchor_zero_fee_commitments() (0FC) are treated the same as supports_anchors_zero_fee_htlc_tx() (ZFHTLC / CSV anchor) for: (1) requiring external funding (requires_external_funding), (2) debug assertions about which code paths are reached, and (3) package malleability classification of holder HTLC outputs. It also adds a missing debug assertion in channelmonitor.rs to exclude 0FC from a legacy funding-outpoint branch. The change is a partial alignment of 0FC with existing anchor semantics; it is not a complete feature addition.
Changed components
lightning/src/chain/package.rslightning/src/chain/channelmonitor.rsPackageSolvingData::HolderHTLCOutputPackageSolvingData::HolderFundingOutputPackageTemplate::requires_external_fundingChannelMonitorImpl funding outpoint selectionInspect captured patch +37 / −11
diff --git a/lightning/src/chain/channelmonitor.rs b/lightning/src/chain/channelmonitor.rs
index d882262..405de0b 100644
--- a/lightning/src/chain/channelmonitor.rs
+++ b/lightning/src/chain/channelmonitor.rs
@@ -2715,6 +2715,7 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
} else {
let funding = get_confirmed_funding_scope!(self);
debug_assert!(!funding.channel_type_features().supports_anchors_zero_fee_htlc_tx());
+ debug_assert!(!funding.channel_type_features().supports_anchor_zero_fee_commitments());
BitcoinOutPoint::new(*txid, 0)
}
} else {
diff --git a/lightning/src/chain/package.rs b/lightning/src/chain/package.rs
index 5b509fa..b67bb1c 100644
--- a/lightning/src/chain/package.rs
+++ b/lightning/src/chain/package.rs
@@ -750,11 +750,17 @@ impl PackageSolvingData {
PackageSolvingData::CounterpartyOfferedHTLCOutput(ref outp) => outp.htlc.amount_msat / 1000,
PackageSolvingData::CounterpartyReceivedHTLCOutput(ref outp) => outp.htlc.amount_msat / 1000,
PackageSolvingData::HolderHTLCOutput(ref outp) => {
- debug_assert!(outp.channel_type_features.supports_anchors_zero_fee_htlc_tx());
+ let free_htlcs = outp.channel_type_features.supports_anchors_zero_fee_htlc_tx();
+ let free_commitments =
+ outp.channel_type_features.supports_anchor_zero_fee_commitments();
+ debug_assert!(free_htlcs || free_commitments);
outp.amount_msat / 1000
},
PackageSolvingData::HolderFundingOutput(ref outp) => {
- debug_assert!(outp.channel_type_features.supports_anchors_zero_fee_htlc_tx());
+ let free_htlcs = outp.channel_type_features.supports_anchors_zero_fee_htlc_tx();
+ let free_commitments =
+ outp.channel_type_features.supports_anchor_zero_fee_commitments();
+ debug_assert!(free_htlcs || free_commitments);
outp.funding_amount_sats.unwrap()
}
};
@@ -768,7 +774,10 @@ impl PackageSolvingData {
PackageSolvingData::CounterpartyOfferedHTLCOutput(ref outp) => weight_offered_htlc(&outp.channel_type_features) as usize,
PackageSolvingData::CounterpartyReceivedHTLCOutput(ref outp) => weight_received_htlc(&outp.channel_type_features) as usize,
PackageSolvingData::HolderHTLCOutput(ref outp) => {
- debug_assert!(outp.channel_type_features.supports_anchors_zero_fee_htlc_tx());
+ let free_htlcs = outp.channel_type_features.supports_anchors_zero_fee_htlc_tx();
+ let free_commitments =
+ outp.channel_type_features.supports_anchor_zero_fee_commitments();
+ debug_assert!(free_htlcs || free_commitments);
if outp.preimage.is_none() {
weight_offered_htlc(&outp.channel_type_features) as usize
} else {
@@ -988,6 +997,7 @@ impl PackageSolvingData {
match self {
PackageSolvingData::HolderHTLCOutput(ref outp) => {
debug_assert!(!outp.channel_type_features.supports_anchors_zero_fee_htlc_tx());
+ debug_assert!(!outp.channel_type_features.supports_anchor_zero_fee_commitments());
outp.get_maybe_signed_htlc_tx(onchain_handler, outpoint)
}
PackageSolvingData::HolderFundingOutput(ref outp) => {
@@ -1040,14 +1050,20 @@ impl PackageSolvingData {
PackageMalleability::Malleable(AggregationCluster::Unpinnable),
PackageSolvingData::CounterpartyReceivedHTLCOutput(..) =>
PackageMalleability::Malleable(AggregationCluster::Pinnable),
- PackageSolvingData::HolderHTLCOutput(ref outp) if outp.channel_type_features.supports_anchors_zero_fee_htlc_tx() => {
- if outp.preimage.is_some() {
- PackageMalleability::Malleable(AggregationCluster::Unpinnable)
+ PackageSolvingData::HolderHTLCOutput(ref outp) => {
+ let free_htlcs = outp.channel_type_features.supports_anchors_zero_fee_htlc_tx();
+ let free_commits = outp.channel_type_features.supports_anchor_zero_fee_commitments();
+
+ if free_htlcs || free_commits {
+ if outp.preimage.is_some() {
+ PackageMalleability::Malleable(AggregationCluster::Unpinnable)
+ } else {
+ PackageMalleability::Malleable(AggregationCluster::Pinnable)
+ }
} else {
- PackageMalleability::Malleable(AggregationCluster::Pinnable)
+ PackageMalleability::Untractable
}
},
- PackageSolvingData::HolderHTLCOutput(..) => PackageMalleability::Untractable,
PackageSolvingData::HolderFundingOutput(..) => PackageMalleability::Untractable,
}
}
@@ -1364,7 +1380,10 @@ impl PackageTemplate {
for (previous_output, input) in &self.inputs {
match input {
PackageSolvingData::HolderHTLCOutput(ref outp) => {
- debug_assert!(outp.channel_type_features.supports_anchors_zero_fee_htlc_tx());
+ let free_htlcs = outp.channel_type_features.supports_anchors_zero_fee_htlc_tx();
+ let free_commitments =
+ outp.channel_type_features.supports_anchor_zero_fee_commitments();
+ debug_assert!(free_htlcs || free_commitments);
outp.get_htlc_descriptor(onchain_handler, &previous_output).map(|htlc| {
htlcs.get_or_insert_with(|| Vec::with_capacity(self.inputs.len())).push(htlc);
});
@@ -1559,8 +1578,14 @@ impl PackageTemplate {
#[rustfmt::skip]
pub(crate) fn requires_external_funding(&self) -> bool {
self.inputs.iter().find(|input| match input.1 {
- PackageSolvingData::HolderFundingOutput(ref outp) => outp.channel_type_features.supports_anchors_zero_fee_htlc_tx(),
- PackageSolvingData::HolderHTLCOutput(ref outp) => outp.channel_type_features.supports_anchors_zero_fee_htlc_tx(),
+ PackageSolvingData::HolderFundingOutput(ref outp) => {
+ outp.channel_type_features.supports_anchors_zero_fee_htlc_tx()
+ || outp.channel_type_features.supports_anchor_zero_fee_commitments()
+ },
+ PackageSolvingData::HolderHTLCOutput(ref outp) => {
+ outp.channel_type_features.supports_anchors_zero_fee_htlc_tx()
+ || outp.channel_type_features.supports_anchor_zero_fee_commitments()
+ },
_ => false,
}).is_some()
}
Why this scored 57/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.