Use 72 WU instead of 73 WU for signature weight
What changed, and why it matters
This commit fixes an internal inconsistency in how LDK estimates Bitcoin transaction fees. Some parts of the code assumed signatures could be 73 weight units (WU) long, while others used 72 WU. Since 73 WU signatures are non-standard and LDK never produces them, the 73 WU assumption caused fee estimates to be slightly too high. The patch standardizes on 72 WU and replaces magic numbers with named constants. The practical effect is slightly lower, more accurate fee estimates for Lightning channel funding and splicing transactions. It is not a direct vulnerability fix, but incorrect fee estimation could in edge cases cause transactions to be over-funded or, more importantly, could contribute to fee-related negotiation failures.
Review and merge. This is a correctness and maintainability improvement with minor security relevance. Consider whether any downstream consumers rely on the previous slightly-higher fee estimates, and verify that the new constants match actual maximum standard signature sizes under all feature combinations (including grind_signatures).
Security signals we found
Inconsistent fee/weight estimation between components
Use of non-standard signature size (73 WU) in estimates
Potential overestimation of transaction fees
Constants introduced to prevent future divergence
No direct cryptographic or memory-safety bug
Evidence from the diff
The change unifies signature weight assumptions across chan_utils.rs, channel.rs, interactivetxs.rs, and sign/mod.rs. It introduces MAX_STANDARD_SIGNATURE_SIZE (equal to secp256k1’s MAX_SIGNATURE_SIZE, 72 bytes) and COMPRESSED_PUBLIC_KEY_SIZE constants, replacing hardcoded 73-byte signature assumptions. FUNDING_TRANSACTION_WITNESS_WEIGHT, P2WPKH_WITNESS_WEIGHT, DelayedPaymentOutputDescriptor::MAX_WITNESS_LENGTH, StaticPaymentOutputDescriptor::max_witness_length, and P2TR_KEY_PATH_WITNESS_WEIGHT are updated. Test expectations for fee estimates are adjusted downward by small amounts (e.g., 1520 -> 1516). The commit message explicitly notes that 73 WU signatures are non-standard and won’t be produced by LDK, and that mixing 73 WU with the grind_signatures adjustment is ‘nonsensical’.
Changed components
lightning/src/ln/chan_utils.rslightning/src/ln/channel.rslightning/src/ln/interactivetxs.rslightning/src/sign/mod.rsInspect captured patch +70 / −45
diff --git a/lightning/src/ln/chan_utils.rs b/lightning/src/ln/chan_utils.rs
index ff40050..242f560 100644
--- a/lightning/src/ln/chan_utils.rs
+++ b/lightning/src/ln/chan_utils.rs
@@ -116,19 +116,24 @@ pub const HTLC_SUCCESS_INPUT_P2A_ANCHOR_WITNESS_WEIGHT: u64 = 324;
/// The size of the 2-of-2 multisig script
const MULTISIG_SCRIPT_SIZE: u64 = 1 + // OP_2
1 + // data len
- 33 + // pubkey1
+ crate::sign::COMPRESSED_PUBLIC_KEY_SIZE as u64 + // pubkey1
1 + // data len
- 33 + // pubkey2
+ crate::sign::COMPRESSED_PUBLIC_KEY_SIZE as u64 + // pubkey2
1 + // OP_2
1; // OP_CHECKMULTISIG
-/// The weight of a funding transaction input (2-of-2 P2WSH)
-/// See https://github.com/lightning/bolts/blob/master/03-transactions.md#expected-weight-of-the-commitment-transaction
+
+/// The weight of a funding transaction input (2-of-2 P2WSH).
+///
+/// Unlike in the [spec], 72 WU is used for the max signature size since 73 WU signatures are
+/// non-standard.
+///
+/// [spec]: https://github.com/lightning/bolts/blob/master/03-transactions.md#expected-weight-of-the-commitment-transaction
pub const FUNDING_TRANSACTION_WITNESS_WEIGHT: u64 = 1 + // number_of_witness_elements
1 + // nil_len
1 + // sig len
- 73 + // sig1
+ crate::sign::MAX_STANDARD_SIGNATURE_SIZE as u64 + // sig1
1 + // sig len
- 73 + // sig2
+ crate::sign::MAX_STANDARD_SIGNATURE_SIZE as u64 + // sig2
1 + // witness_script_length
MULTISIG_SCRIPT_SIZE;
diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs
index 75905db..234168e 100644
--- a/lightning/src/ln/channel.rs
+++ b/lightning/src/ln/channel.rs
@@ -17397,19 +17397,19 @@ mod tests {
// 2 inputs, initiator, 2000 sat/kw feerate
assert_eq!(
estimate_v2_funding_transaction_fee(&two_inputs, &[], true, false, 2000),
- 1520,
+ 1516,
);
// higher feerate
assert_eq!(
estimate_v2_funding_transaction_fee(&two_inputs, &[], true, false, 3000),
- 2280,
+ 2274,
);
// only 1 input
assert_eq!(
estimate_v2_funding_transaction_fee(&one_input, &[], true, false, 2000),
- 974,
+ 972,
);
// 0 inputs
@@ -17427,13 +17427,13 @@ mod tests {
// splice initiator
assert_eq!(
estimate_v2_funding_transaction_fee(&one_input, &[], true, true, 2000),
- 1746,
+ 1740,
);
// splice acceptor
assert_eq!(
estimate_v2_funding_transaction_fee(&one_input, &[], false, true, 2000),
- 546,
+ 544,
);
}
@@ -17468,7 +17468,7 @@ mod tests {
true,
2000,
).unwrap(),
- 2292,
+ 2284,
);
// negative case, inputs clearly insufficient
@@ -17484,13 +17484,13 @@ mod tests {
);
assert_eq!(
res.err().unwrap(),
- "Total input amount 100000 is lower than needed for contribution 220000, considering fees of 1746. Need more inputs.",
+ "Total input amount 100000 is lower than needed for contribution 220000, considering fees of 1740. Need more inputs.",
);
}
// barely covers
{
- let expected_fee: u64 = 2292;
+ let expected_fee: u64 = 2284;
assert_eq!(
check_v2_funding_inputs_sufficient(
(300_000 - expected_fee - 20) as i64,
@@ -17520,13 +17520,13 @@ mod tests {
);
assert_eq!(
res.err().unwrap(),
- "Total input amount 300000 is lower than needed for contribution 298032, considering fees of 2522. Need more inputs.",
+ "Total input amount 300000 is lower than needed for contribution 298032, considering fees of 2513. Need more inputs.",
);
}
// barely covers, less fees (no extra weight, no init)
{
- let expected_fee: u64 = 1092;
+ let expected_fee: u64 = 1088;
assert_eq!(
check_v2_funding_inputs_sufficient(
(300_000 - expected_fee - 20) as i64,
diff --git a/lightning/src/ln/interactivetxs.rs b/lightning/src/ln/interactivetxs.rs
index 97047eb..b223fa3 100644
--- a/lightning/src/ln/interactivetxs.rs
+++ b/lightning/src/ln/interactivetxs.rs
@@ -3353,21 +3353,19 @@ mod tests {
FundingTxInput::new_p2wpkh(prevtx, 0).unwrap()
})
.collect();
- let our_contributed = 110_000;
let txout = TxOut { value: Amount::from_sat(10_000), script_pubkey: ScriptBuf::new() };
let outputs = vec![txout];
let funding_feerate_sat_per_1000_weight = 3000;
- let total_inputs: u64 = input_prevouts.iter().map(|o| o.value.to_sat()).sum();
- let total_outputs: u64 = outputs.iter().map(|o| o.value.to_sat()).sum();
- let gross_change = total_inputs - total_outputs - our_contributed;
- let fees = 1746;
- let common_fees = 234;
+ let total_inputs: Amount = input_prevouts.iter().map(|o| o.value).sum();
+ let total_outputs: Amount = outputs.iter().map(|o| o.value).sum();
+ let fees = Amount::from_sat(1740);
+ let common_fees = Amount::from_sat(234);
// There is leftover for change
let context = FundingNegotiationContext {
is_initiator: true,
- our_funding_contribution: SignedAmount::from_sat(our_contributed as i64),
+ our_funding_contribution: SignedAmount::from_sat(110_000),
funding_tx_locktime: AbsoluteLockTime::ZERO,
funding_feerate_sat_per_1000_weight,
shared_funding_input: None,
@@ -3375,16 +3373,18 @@ mod tests {
our_funding_outputs: outputs,
change_script: None,
};
+ let gross_change =
+ total_inputs - total_outputs - context.our_funding_contribution.to_unsigned().unwrap();
assert_eq!(
calculate_change_output_value(&context, false, &ScriptBuf::new(), 300),
- Ok(Some(gross_change - fees - common_fees)),
+ Ok(Some((gross_change - fees - common_fees).to_sat())),
);
// There is leftover for change, without common fees
let context = FundingNegotiationContext { is_initiator: false, ..context };
assert_eq!(
calculate_change_output_value(&context, false, &ScriptBuf::new(), 300),
- Ok(Some(gross_change - fees)),
+ Ok(Some((gross_change - fees).to_sat())),
);
// Insufficient inputs, no leftover
@@ -3415,21 +3415,25 @@ mod tests {
our_funding_contribution: SignedAmount::from_sat(117_992),
..context
};
+ let gross_change =
+ total_inputs - total_outputs - context.our_funding_contribution.to_unsigned().unwrap();
assert_eq!(
calculate_change_output_value(&context, false, &ScriptBuf::new(), 100),
- Ok(Some(262)),
+ Ok(Some((gross_change - fees).to_sat())),
);
// Larger fee, smaller change
let context = FundingNegotiationContext {
is_initiator: true,
- our_funding_contribution: SignedAmount::from_sat(our_contributed as i64),
+ our_funding_contribution: SignedAmount::from_sat(110_000),
funding_feerate_sat_per_1000_weight: funding_feerate_sat_per_1000_weight * 3,
..context
};
+ let gross_change =
+ total_inputs - total_outputs - context.our_funding_contribution.to_unsigned().unwrap();
assert_eq!(
calculate_change_output_value(&context, false, &ScriptBuf::new(), 300),
- Ok(Some(4060)),
+ Ok(Some((gross_change - fees * 3 - common_fees * 3).to_sat())),
);
}
diff --git a/lightning/src/sign/mod.rs b/lightning/src/sign/mod.rs
index 1d771d2..ca80338 100644
--- a/lightning/src/sign/mod.rs
+++ b/lightning/src/sign/mod.rs
@@ -81,6 +81,11 @@ pub mod ecdsa;
pub mod taproot;
pub mod tx_builder;
+pub(crate) const COMPRESSED_PUBLIC_KEY_SIZE: usize = bitcoin::secp256k1::constants::PUBLIC_KEY_SIZE;
+
+pub(crate) const MAX_STANDARD_SIGNATURE_SIZE: usize =
+ bitcoin::secp256k1::constants::MAX_SIGNATURE_SIZE;
+
/// Information about a spendable output to a P2WSH script.
///
/// See [`SpendableOutputDescriptor::DelayedPaymentOutput`] for more details on how to spend this.
@@ -114,10 +119,12 @@ impl DelayedPaymentOutputDescriptor {
/// The maximum length a well-formed witness spending one of these should have.
/// Note: If you have the grind_signatures feature enabled, this will be at least 1 byte
/// shorter.
- // Calculated as 1 byte length + 73 byte signature, 1 byte empty vec push, 1 byte length plus
- // redeemscript push length.
- pub const MAX_WITNESS_LENGTH: u64 =
- 1 + 73 + 1 + chan_utils::REVOKEABLE_REDEEMSCRIPT_MAX_LENGTH as u64 + 1;
+ pub const MAX_WITNESS_LENGTH: u64 = (1 /* witness items */
+ + 1 /* sig push */
+ + MAX_STANDARD_SIGNATURE_SIZE
+ + 1 /* empty vec push */
+ + 1 /* redeemscript push */
+ + chan_utils::REVOKEABLE_REDEEMSCRIPT_MAX_LENGTH) as u64;
}
impl_writeable_tlv_based!(DelayedPaymentOutputDescriptor, {
@@ -131,15 +138,18 @@ impl_writeable_tlv_based!(DelayedPaymentOutputDescriptor, {
(13, channel_transaction_parameters, (option: ReadableArgs, Some(channel_value_satoshis.0.unwrap()))),
});
-pub(crate) const P2WPKH_WITNESS_WEIGHT: u64 = 1 /* num stack items */ +
- 1 /* sig length */ +
- 73 /* sig including sighash flag */ +
- 1 /* pubkey length */ +
- 33 /* pubkey */;
+/// Witness weight for satisfying a P2WPKH spend.
+pub(crate) const P2WPKH_WITNESS_WEIGHT: u64 = (1 /* witness items */
+ + 1 /* sig push */
+ + MAX_STANDARD_SIGNATURE_SIZE
+ + 1 /* pubkey push */
+ + COMPRESSED_PUBLIC_KEY_SIZE) as u64;
-/// Witness weight for satisying a P2TR key-path spend.
-pub(crate) const P2TR_KEY_PATH_WITNESS_WEIGHT: u64 = 1 /* witness items */
- + 1 /* schnorr sig len */ + 64 /* schnorr sig */;
+/// Witness weight for satisfying a P2TR key-path spend.
+pub(crate) const P2TR_KEY_PATH_WITNESS_WEIGHT: u64 = (1 /* witness items */
+ + 1 /* sig push */
+ + bitcoin::secp256k1::constants::SCHNORR_SIGNATURE_SIZE)
+ as u64;
/// If a [`KeysManager`] is built with [`KeysManager::new`] with `v2_remote_key_derivation` set
/// (and for all channels after they've been spliced), the script which we receive funds to on-chain
@@ -192,10 +202,16 @@ impl StaticPaymentOutputDescriptor {
/// shorter.
pub fn max_witness_length(&self) -> u64 {
if self.needs_csv_1_for_spend() {
- let witness_script_weight = 1 /* pubkey push */ + 33 /* pubkey */ +
- 1 /* OP_CHECKSIGVERIFY */ + 1 /* OP_1 */ + 1 /* OP_CHECKSEQUENCEVERIFY */;
- 1 /* num witness items */ + 1 /* sig push */ + 73 /* sig including sighash flag */ +
- 1 /* witness script push */ + witness_script_weight
+ let witness_script_weight = 1 /* pubkey push */
+ + COMPRESSED_PUBLIC_KEY_SIZE
+ + 1 /* OP_CHECKSIGVERIFY */
+ + 1 /* OP_1 */
+ + 1 /* OP_CHECKSEQUENCEVERIFY */;
+ (1 /* num witness items */
+ + 1 /* sig push */
+ + MAX_STANDARD_SIGNATURE_SIZE
+ + 1 /* witness script push */
+ + witness_script_weight) as u64
} else {
P2WPKH_WITNESS_WEIGHT
}
@@ -511,7 +527,7 @@ impl SpendableOutputDescriptor {
sequence: Sequence::ZERO,
witness: Witness::new(),
});
- witness_weight += 1 + 73 + 34;
+ witness_weight += P2WPKH_WITNESS_WEIGHT;
#[cfg(feature = "grind_signatures")]
{
// Guarantees a low R signature
Why this scored 36/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.