Update handling of `BumpTransactionEvent::ChannelClose` for 0FC channels
What changed, and why it matters
This commit updates how Lightning Dev Kit handles fee-bumping anchor transactions for a newer type of channel (zero-fee commitment channels, or 0FC). It makes two key changes: it uses a much smaller empty witness for the new-style P2A anchor output, and it sets the anchor transaction version to 3 instead of 2. These are protocol-specific adjustments needed so the new channel type can close and fee-bump correctly. The commit does not describe itself as a security fix, and the changes appear to be compatibility/correctness updates rather than a patch for an active vulnerability.
Treat as a protocol-compatibility and correctness update rather than an urgent security patch. Reviewers should verify that the empty-witness P2A spend and version-3 transaction format align with the latest Lightning spec for zero-fee commitment channels, and that the conditional logic correctly distinguishes 0FC from legacy anchor channels to avoid invalid transactions or failed force-closes.
Security signals we found
Protocol-specific transaction version change (v2 -> v3) for 0FC channels
Witness weight reduction for P2A anchor inputs
Conditional skipping of holder-keyed anchor signature for 0FC channels
Fee-sufficiency check retained for commitment transactions
Evidence from the diff
The patch modifies BumpTransactionEvent::ChannelClose handling in lightning/src/events/bump_transaction/mod.rs and adds a constant in lightning/src/ln/chan_utils.rs. For channels that support anchor zero-fee commitments (supports_anchor_zero_fee_commitments), it uses P2A_ANCHOR_INPUT_WITNESS_WEIGHT (1 weight unit) instead of the legacy ANCHOR_INPUT_WITNESS_WEIGHT (~114/115), and sets the anchor transaction version to a non-standard 3 instead of Version::TWO. For 0FC channels, it skips producing a signature witness for the P2A anchor, leaving the witness empty. The code still checks whether the commitment transaction has enough fees to be broadcast alone, because trimmed/rounded outputs can make these transactions non-zero fee.
Changed components
lightning/src/events/bump_transaction/mod.rslightning/src/ln/chan_utils.rsBumpTransactionEvent::ChannelClose handlingP2A anchor spending path0FC (zero-fee commitment) channel close flowInspect captured patch +39 / −16
diff --git a/lightning/src/events/bump_transaction/mod.rs b/lightning/src/events/bump_transaction/mod.rs
index c5b7885..f600397 100644
--- a/lightning/src/events/bump_transaction/mod.rs
+++ b/lightning/src/events/bump_transaction/mod.rs
@@ -25,6 +25,7 @@ use crate::ln::chan_utils;
use crate::ln::chan_utils::{
shared_anchor_script_pubkey, HTLCOutputInCommitment, ANCHOR_INPUT_WITNESS_WEIGHT,
HTLC_SUCCESS_INPUT_ANCHOR_WITNESS_WEIGHT, HTLC_TIMEOUT_INPUT_ANCHOR_WITNESS_WEIGHT,
+ P2A_ANCHOR_INPUT_WITNESS_WEIGHT,
};
use crate::ln::types::ChannelId;
use crate::prelude::*;
@@ -669,6 +670,16 @@ where
commitment_tx: &Transaction, commitment_tx_fee_sat: u64,
anchor_descriptor: &AnchorDescriptor,
) -> Result<(), ()> {
+ let channel_type = &anchor_descriptor
+ .channel_derivation_parameters
+ .transaction_parameters
+ .channel_type_features;
+ let anchor_input_witness_weight = if channel_type.supports_anchor_zero_fee_commitments() {
+ P2A_ANCHOR_INPUT_WITNESS_WEIGHT
+ } else {
+ ANCHOR_INPUT_WITNESS_WEIGHT
+ };
+
// First, check if the commitment transaction has sufficient fees on its own.
let commitment_tx_feerate_sat_per_1000_weight = compute_feerate_sat_per_1000_weight(
commitment_tx_fee_sat,
@@ -689,7 +700,7 @@ where
let commitment_tx_fee_sat = Amount::from_sat(commitment_tx_fee_sat);
anchor_utxo.value += commitment_tx_fee_sat;
let starting_package_and_fixed_input_satisfaction_weight =
- commitment_tx.weight().to_wu() + ANCHOR_INPUT_WITNESS_WEIGHT + EMPTY_SCRIPT_SIG_WEIGHT;
+ commitment_tx.weight().to_wu() + anchor_input_witness_weight + EMPTY_SCRIPT_SIG_WEIGHT;
let mut package_and_fixed_input_satisfaction_weight =
starting_package_and_fixed_input_satisfaction_weight;
@@ -714,8 +725,14 @@ where
)
.await?;
+ let version = if channel_type.supports_anchor_zero_fee_commitments() {
+ Version::non_standard(3)
+ } else {
+ Version::TWO
+ };
+
let mut anchor_tx = Transaction {
- version: Version::TWO,
+ version,
lock_time: LockTime::ZERO, // TODO: Use next best height.
input: vec![anchor_descriptor.unsigned_tx_input()],
output: vec![],
@@ -724,7 +741,7 @@ where
let input_satisfaction_weight: u64 =
coin_selection.confirmed_utxos.iter().map(|utxo| utxo.satisfaction_weight).sum();
let total_satisfaction_weight =
- ANCHOR_INPUT_WITNESS_WEIGHT + EMPTY_SCRIPT_SIG_WEIGHT + input_satisfaction_weight;
+ anchor_input_witness_weight + EMPTY_SCRIPT_SIG_WEIGHT + input_satisfaction_weight;
let total_input_amount = must_spend_amount
+ coin_selection.confirmed_utxos.iter().map(|utxo| utxo.output.value).sum();
@@ -780,18 +797,21 @@ where
log_debug!(self.logger, "Signing anchor transaction {}", anchor_txid);
anchor_tx = self.utxo_source.sign_psbt(anchor_psbt).await?;
- let signer = self
- .signer_provider
- .derive_channel_signer(anchor_descriptor.channel_derivation_parameters.keys_id);
- let channel_parameters =
- &anchor_descriptor.channel_derivation_parameters.transaction_parameters;
- let anchor_sig = signer.sign_holder_keyed_anchor_input(
- channel_parameters,
- &anchor_tx,
- 0,
- &self.secp,
- )?;
- anchor_tx.input[0].witness = anchor_descriptor.tx_input_witness(&anchor_sig);
+ // No need to produce any witness to spend P2A anchors
+ if channel_type.supports_anchors_zero_fee_htlc_tx() {
+ let signer = self
+ .signer_provider
+ .derive_channel_signer(anchor_descriptor.channel_derivation_parameters.keys_id);
+ let channel_parameters =
+ &anchor_descriptor.channel_derivation_parameters.transaction_parameters;
+ let anchor_sig = signer.sign_holder_keyed_anchor_input(
+ channel_parameters,
+ &anchor_tx,
+ 0,
+ &self.secp,
+ )?;
+ anchor_tx.input[0].witness = anchor_descriptor.tx_input_witness(&anchor_sig);
+ }
#[cfg(debug_assertions)]
{
diff --git a/lightning/src/ln/chan_utils.rs b/lightning/src/ln/chan_utils.rs
index f7aaf39..0a5e372 100644
--- a/lightning/src/ln/chan_utils.rs
+++ b/lightning/src/ln/chan_utils.rs
@@ -89,7 +89,10 @@ pub const ANCHOR_INPUT_WITNESS_WEIGHT: u64 = 114;
#[cfg(not(feature = "grind_signatures"))]
pub const ANCHOR_INPUT_WITNESS_WEIGHT: u64 = 115;
-/// The maximum value of the P2A anchor
+/// The weight of a P2A anchor witness.
+pub const P2A_ANCHOR_INPUT_WITNESS_WEIGHT: u64 = 1;
+
+/// The maximum value of a P2A anchor.
pub const P2A_MAX_VALUE: u64 = 240;
/// The upper bound weight of an HTLC timeout input from a commitment transaction with anchor
Why this scored 42/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.