Create a single P2A anchor on commitment transactions in 0FC channels
What changed, and why it matters
This commit changes how Bitcoin transaction fees are handled for a new type of Lightning channel. Instead of each party having their own small 'anchor' output to bump the transaction fee later, the commit creates one shared anchor output. It also switches these zero-fee commitment transactions to use a non-standard version 3. This is part of an ongoing protocol update and does not by itself look like a security bug fix; it is more like feature construction.
Treat this as a protocol feature change rather than an urgent security patch. Reviewers should verify that the P2A output value calculation cannot underflow or exceed consensus limits, that version-3 transactions are accepted by relevant Bitcoin relay policy, and that the single shared anchor preserves fee-bumping rights for both channel parties. No immediate action is indicated by the commit alone.
Security signals we found
New P2A anchor output introduced for zero-fee commitment channels
Commitment transaction version changed to non-standard Version(3) for zero-fee anchor channels
Anchor value derived from channel value remainder and capped at 240 sat
Existing anchor logic retained for non-zero-fee channel types
No explicit bug fix, bounds check, or vulnerability disclosure language in commit message
Evidence from the diff
The patch updates rust-lightning’s channel commitment transaction construction for zero-fee commitment channels. It replaces per-party to_local/to_remote anchor outputs with a single P2A (pay-to-anchor) output, adds the P2A script pubkey constant and a 240-sat maximum value, and sets transaction version to non-standard 3 when the channel type supports anchor zero-fee commitments. The shared anchor value is computed as the trimmed remainder of the channel value after HTLCs and counterparty/broadcaster outputs, capped at P2A_MAX_VALUE. Several internal signatures are changed to pass the sum of non-dust HTLC values instead of a boolean flag.
Changed components
lightning/src/ln/chan_utils.rsCommitmentTransaction constructionDirectedChannelTransactionParametersZero-fee commitment channel anchor outputsInspect captured patch +38 / −8
diff --git a/lightning/src/ln/chan_utils.rs b/lightning/src/ln/chan_utils.rs
index 545d529..dfd2db9 100644
--- a/lightning/src/ln/chan_utils.rs
+++ b/lightning/src/ln/chan_utils.rs
@@ -89,6 +89,12 @@ pub const ANCHOR_INPUT_WITNESS_WEIGHT: u64 = 114;
#[cfg(not(feature = "grind_signatures"))]
pub const ANCHOR_INPUT_WITNESS_WEIGHT: u64 = 115;
+/// The P2A scriptpubkey
+pub const P2A_SCRIPT: &[u8] = &[0x51, 0x02, 0x4e, 0x73];
+
+/// The maximum value of the P2A anchor
+pub const P2A_MAX_VALUE: u64 = 240;
+
/// The upper bound weight of an HTLC timeout input from a commitment transaction with anchor
/// outputs.
pub const HTLC_TIMEOUT_INPUT_ANCHOR_WITNESS_WEIGHT: u64 = 288;
@@ -1232,6 +1238,11 @@ impl<'a> DirectedChannelTransactionParameters<'a> {
pub fn channel_type_features(&self) -> &'a ChannelTypeFeatures {
&self.inner.channel_type_features
}
+
+ /// The value locked in the channel, denominated in satoshis.
+ pub fn channel_value_satoshis(&self) -> u64 {
+ self.inner.channel_value_satoshis
+ }
}
/// Information needed to build and sign a holder's commitment transaction.
@@ -1637,7 +1648,7 @@ impl CommitmentTransaction {
let outputs = Self::build_outputs_and_htlcs(&keys, to_broadcaster_value_sat, to_countersignatory_value_sat, &mut nondust_htlcs, channel_parameters);
let (obscured_commitment_transaction_number, txins) = Self::build_inputs(commitment_number, channel_parameters);
- let transaction = Self::make_transaction(obscured_commitment_transaction_number, txins, outputs);
+ let transaction = Self::make_transaction(obscured_commitment_transaction_number, txins, outputs, channel_parameters);
let txid = transaction.compute_txid();
CommitmentTransaction {
commitment_number,
@@ -1691,6 +1702,8 @@ impl CommitmentTransaction {
// First rebuild the htlc outputs, note that `outputs` is now the same length as `self.nondust_htlcs`
let mut outputs = Self::build_htlc_outputs(keys, &self.nondust_htlcs, channel_parameters.channel_type_features());
+ let nondust_htlcs_value_sum_sat = self.nondust_htlcs.iter().map(|htlc| htlc.to_bitcoin_amount()).sum();
+
// Check that the HTLC outputs are sorted by value, script pubkey, and cltv expiry.
// Note that this only iterates if the length of `outputs` and `self.nondust_htlcs` is >= 2.
if (1..outputs.len()).into_iter().any(|i| Self::is_left_greater(i, &outputs, &self.nondust_htlcs)) {
@@ -1713,11 +1726,11 @@ impl CommitmentTransaction {
self.to_broadcaster_value_sat,
self.to_countersignatory_value_sat,
channel_parameters,
- !self.nondust_htlcs.is_empty(),
+ nondust_htlcs_value_sum_sat,
insert_non_htlc_output
);
- let transaction = Self::make_transaction(obscured_commitment_transaction_number, txins, outputs);
+ let transaction = Self::make_transaction(obscured_commitment_transaction_number, txins, outputs, channel_parameters);
let txid = transaction.compute_txid();
let built_transaction = BuiltCommitmentTransaction {
transaction,
@@ -1727,9 +1740,14 @@ impl CommitmentTransaction {
}
#[rustfmt::skip]
- fn make_transaction(obscured_commitment_transaction_number: u64, txins: Vec<TxIn>, outputs: Vec<TxOut>) -> Transaction {
+ fn make_transaction(obscured_commitment_transaction_number: u64, txins: Vec<TxIn>, outputs: Vec<TxOut>, channel_parameters: &DirectedChannelTransactionParameters) -> Transaction {
+ let version = if channel_parameters.channel_type_features().supports_anchor_zero_fee_commitments() {
+ Version::non_standard(3)
+ } else {
+ Version::TWO
+ };
Transaction {
- version: Version::TWO,
+ version,
lock_time: LockTime::from_consensus(((0x20 as u32) << 8 * 3) | ((obscured_commitment_transaction_number & 0xffffffu64) as u32)),
input: txins,
output: outputs,
@@ -1747,7 +1765,8 @@ impl CommitmentTransaction {
// First build and sort the HTLC outputs.
// Also sort the HTLC output data in `nondust_htlcs` in the same order.
let mut outputs = Self::build_sorted_htlc_outputs(keys, nondust_htlcs, channel_parameters.channel_type_features());
- let tx_has_htlc_outputs = !outputs.is_empty();
+
+ let nondust_htlcs_value_sum_sat = nondust_htlcs.iter().map(|htlc| htlc.to_bitcoin_amount()).sum();
// Initialize the transaction output indices; we will update them below when we
// add the non-htlc transaction outputs.
@@ -1784,7 +1803,7 @@ impl CommitmentTransaction {
to_broadcaster_value_sat,
to_countersignatory_value_sat,
channel_parameters,
- tx_has_htlc_outputs,
+ nondust_htlcs_value_sum_sat,
insert_non_htlc_output
);
@@ -1797,7 +1816,7 @@ impl CommitmentTransaction {
to_broadcaster_value_sat: Amount,
to_countersignatory_value_sat: Amount,
channel_parameters: &DirectedChannelTransactionParameters,
- tx_has_htlc_outputs: bool,
+ nondust_htlcs_value_sum_sat: Amount,
mut insert_non_htlc_output: F,
) where
F: FnMut(TxOut),
@@ -1807,6 +1826,7 @@ impl CommitmentTransaction {
let broadcaster_funding_key = &channel_parameters.broadcaster_pubkeys().funding_pubkey;
let channel_type = channel_parameters.channel_type_features();
let contest_delay = channel_parameters.contest_delay();
+ let tx_has_htlc_outputs = nondust_htlcs_value_sum_sat != Amount::ZERO;
if to_countersignatory_value_sat > Amount::ZERO {
let script = if channel_type.supports_anchors_zero_fee_htlc_tx() {
@@ -1849,6 +1869,16 @@ impl CommitmentTransaction {
});
}
}
+
+ if channel_type.supports_anchor_zero_fee_commitments() {
+ let channel_value_satoshis = Amount::from_sat(channel_parameters.channel_value_satoshis());
+ // These subtractions panic on underflow, but this should never happen
+ let trimmed_sum_sat = channel_value_satoshis - nondust_htlcs_value_sum_sat - to_broadcaster_value_sat - to_countersignatory_value_sat;
+ insert_non_htlc_output(TxOut {
+ script_pubkey: ScriptBuf::from_bytes(P2A_SCRIPT.to_vec()),
+ value: cmp::min(Amount::from_sat(P2A_MAX_VALUE), trimmed_sum_sat),
+ });
+ }
}
#[rustfmt::skip]
Why this scored 32/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.