From eb341de7884db765fd42d58b62bffe713940a404 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Wed, 5 Nov 2025 11:10:14 -0600 Subject: [PATCH 1/5] Use 72 WU instead of 73 WU for signature weight When estimating signature weight, 73 WU was used in some places while 72 WU was used in others. Consistently use 72 WU and replace hardcoded values with constants. 73 WU signatures are non-standard and won't be produced by LDK. Additionally, using 73 WU along with grind_signatures adjustment is nonsensical. --- lightning/src/ln/chan_utils.rs | 17 ++++++---- lightning/src/ln/channel.rs | 20 ++++++------ lightning/src/ln/interactivetxs.rs | 28 ++++++++++------- lightning/src/sign/mod.rs | 50 ++++++++++++++++++++---------- 4 files changed, 70 insertions(+), 45 deletions(-) diff --git a/lightning/src/ln/chan_utils.rs b/lightning/src/ln/chan_utils.rs index ff4005024e2..242f5609ba1 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 75905dba1cd..234168e37c6 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 97047eb3a0d..b223fa37ad9 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 1d771d22783..ca8033895cb 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 From 469802612762ef747f532972e472f23f1031c702 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Wed, 5 Nov 2025 11:21:43 -0600 Subject: [PATCH 2/5] Account for grind_signatures in splice funding tx When estimating the splice funding transaction fees, adjust for grind_signatures. Since LDK supplies one of the signatures, only adjust by 1 WU even though spending the shared input requires two signatures. --- lightning/src/ln/chan_utils.rs | 3 + lightning/src/ln/channel.rs | 102 ++++++++++++++++------------- lightning/src/ln/funding.rs | 8 ++- lightning/src/ln/interactivetxs.rs | 21 +++++- lightning/src/sign/mod.rs | 6 +- lightning/src/util/test_utils.rs | 6 ++ 6 files changed, 97 insertions(+), 49 deletions(-) diff --git a/lightning/src/ln/chan_utils.rs b/lightning/src/ln/chan_utils.rs index 242f5609ba1..e70d935778c 100644 --- a/lightning/src/ln/chan_utils.rs +++ b/lightning/src/ln/chan_utils.rs @@ -127,6 +127,9 @@ const MULTISIG_SCRIPT_SIZE: u64 = 1 + // OP_2 /// Unlike in the [spec], 72 WU is used for the max signature size since 73 WU signatures are /// non-standard. /// +/// Note: If you have the `grind_signatures` feature enabled, this will be at least 1 byte +/// shorter. +/// /// [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 diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 234168e37c6..eab70559417 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -6547,6 +6547,11 @@ fn estimate_v2_funding_transaction_fee( .saturating_add(BASE_INPUT_WEIGHT) .saturating_add(EMPTY_SCRIPT_SIG_WEIGHT) .saturating_add(FUNDING_TRANSACTION_WITNESS_WEIGHT); + #[cfg(feature = "grind_signatures")] + { + // Guarantees a low R signature + weight -= 1; + } } } @@ -17397,19 +17402,19 @@ mod tests { // 2 inputs, initiator, 2000 sat/kw feerate assert_eq!( estimate_v2_funding_transaction_fee(&two_inputs, &[], true, false, 2000), - 1516, + if cfg!(feature = "grind_signatures") { 1512 } else { 1516 }, ); // higher feerate assert_eq!( estimate_v2_funding_transaction_fee(&two_inputs, &[], true, false, 3000), - 2274, + if cfg!(feature = "grind_signatures") { 2268 } else { 2274 }, ); // only 1 input assert_eq!( estimate_v2_funding_transaction_fee(&one_input, &[], true, false, 2000), - 972, + if cfg!(feature = "grind_signatures") { 970 } else { 972 }, ); // 0 inputs @@ -17427,13 +17432,13 @@ mod tests { // splice initiator assert_eq!( estimate_v2_funding_transaction_fee(&one_input, &[], true, true, 2000), - 1740, + if cfg!(feature = "grind_signatures") { 1736 } else { 1740 }, ); // splice acceptor assert_eq!( estimate_v2_funding_transaction_fee(&one_input, &[], false, true, 2000), - 544, + if cfg!(feature = "grind_signatures") { 542 } else { 544 }, ); } @@ -17457,40 +17462,46 @@ mod tests { use crate::ln::channel::check_v2_funding_inputs_sufficient; // positive case, inputs well over intended contribution - assert_eq!( - check_v2_funding_inputs_sufficient( - 220_000, - &[ - funding_input_sats(200_000), - funding_input_sats(100_000), - ], - true, - true, - 2000, - ).unwrap(), - 2284, - ); + { + let expected_fee = if cfg!(feature = "grind_signatures") { 2278 } else { 2284 }; + assert_eq!( + check_v2_funding_inputs_sufficient( + 220_000, + &[ + funding_input_sats(200_000), + funding_input_sats(100_000), + ], + true, + true, + 2000, + ).unwrap(), + expected_fee, + ); + } // negative case, inputs clearly insufficient { - let res = check_v2_funding_inputs_sufficient( - 220_000, - &[ - funding_input_sats(100_000), - ], - true, - true, - 2000, - ); + let expected_fee = if cfg!(feature = "grind_signatures") { 1736 } else { 1740 }; assert_eq!( - res.err().unwrap(), - "Total input amount 100000 is lower than needed for contribution 220000, considering fees of 1740. Need more inputs.", + check_v2_funding_inputs_sufficient( + 220_000, + &[ + funding_input_sats(100_000), + ], + true, + true, + 2000, + ), + Err(format!( + "Total input amount 100000 is lower than needed for contribution 220000, considering fees of {}. Need more inputs.", + expected_fee, + )), ); } // barely covers { - let expected_fee: u64 = 2284; + let expected_fee = if cfg!(feature = "grind_signatures") { 2278 } else { 2284 }; assert_eq!( check_v2_funding_inputs_sufficient( (300_000 - expected_fee - 20) as i64, @@ -17508,25 +17519,28 @@ mod tests { // higher fee rate, does not cover { - let res = check_v2_funding_inputs_sufficient( - 298032, - &[ - funding_input_sats(200_000), - funding_input_sats(100_000), - ], - true, - true, - 2200, - ); + let expected_fee = if cfg!(feature = "grind_signatures") { 2506 } else { 2513 }; assert_eq!( - res.err().unwrap(), - "Total input amount 300000 is lower than needed for contribution 298032, considering fees of 2513. Need more inputs.", + check_v2_funding_inputs_sufficient( + 298032, + &[ + funding_input_sats(200_000), + funding_input_sats(100_000), + ], + true, + true, + 2200, + ), + Err(format!( + "Total input amount 300000 is lower than needed for contribution 298032, considering fees of {}. Need more inputs.", + expected_fee + )), ); } - // barely covers, less fees (no extra weight, no init) + // barely covers, less fees (no extra weight, not initiator) { - let expected_fee: u64 = 1088; + let expected_fee = if cfg!(feature = "grind_signatures") { 1084 } else { 1088 }; assert_eq!( check_v2_funding_inputs_sufficient( (300_000 - expected_fee - 20) as i64, diff --git a/lightning/src/ln/funding.rs b/lightning/src/ln/funding.rs index 3cabb8201a9..7281a5f6d69 100644 --- a/lightning/src/ln/funding.rs +++ b/lightning/src/ln/funding.rs @@ -143,7 +143,13 @@ impl FundingTxInput { /// [`TxIn::sequence`]: bitcoin::TxIn::sequence /// [`set_sequence`]: Self::set_sequence pub fn new_p2wpkh(prevtx: Transaction, vout: u32) -> Result { - let witness_weight = Weight::from_wu(P2WPKH_WITNESS_WEIGHT); + let witness_weight = Weight::from_wu(P2WPKH_WITNESS_WEIGHT) + - if cfg!(feature = "grind_signatures") { + // Guarantees a low R signature + Weight::from_wu(1) + } else { + Weight::ZERO + }; FundingTxInput::new(prevtx, vout, witness_weight, Script::is_p2wpkh) } diff --git a/lightning/src/ln/interactivetxs.rs b/lightning/src/ln/interactivetxs.rs index b223fa37ad9..4c585e6a2d1 100644 --- a/lightning/src/ln/interactivetxs.rs +++ b/lightning/src/ln/interactivetxs.rs @@ -1706,7 +1706,15 @@ impl InputOwned { InputOwned::Single(single) => single.satisfaction_weight, // TODO(taproot): Needs to consider different weights based on channel type InputOwned::Shared(_) => { - Weight::from_wu(EMPTY_SCRIPT_SIG_WEIGHT + FUNDING_TRANSACTION_WITNESS_WEIGHT) + let mut weight = 0; + weight += EMPTY_SCRIPT_SIG_WEIGHT + FUNDING_TRANSACTION_WITNESS_WEIGHT; + #[cfg(feature = "grind_signatures")] + { + // Guarantees a low R signature + weight -= 1; + } + + Weight::from_wu(weight) }, } } @@ -2314,6 +2322,11 @@ pub(super) fn calculate_change_output_value( weight = weight.saturating_add(BASE_INPUT_WEIGHT); weight = weight.saturating_add(EMPTY_SCRIPT_SIG_WEIGHT); weight = weight.saturating_add(FUNDING_TRANSACTION_WITNESS_WEIGHT); + #[cfg(feature = "grind_signatures")] + { + // Guarantees a low R signature + weight -= 1; + } } } @@ -3359,7 +3372,11 @@ mod tests { 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 fees = if cfg!(feature = "grind_signatures") { + Amount::from_sat(1734) + } else { + Amount::from_sat(1740) + }; let common_fees = Amount::from_sat(234); // There is leftover for change diff --git a/lightning/src/sign/mod.rs b/lightning/src/sign/mod.rs index ca8033895cb..77076a408e4 100644 --- a/lightning/src/sign/mod.rs +++ b/lightning/src/sign/mod.rs @@ -117,7 +117,8 @@ pub struct DelayedPaymentOutputDescriptor { 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 + /// + /// Note: If you have the `grind_signatures` feature enabled, this will be at least 1 byte /// shorter. pub const MAX_WITNESS_LENGTH: u64 = (1 /* witness items */ + 1 /* sig push */ @@ -198,7 +199,8 @@ impl StaticPaymentOutputDescriptor { } /// 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 + /// + /// Note: If you have the `grind_signatures` feature enabled, this will be at least 1 byte /// shorter. pub fn max_witness_length(&self) -> u64 { if self.needs_csv_1_for_spend() { diff --git a/lightning/src/util/test_utils.rs b/lightning/src/util/test_utils.rs index ad8ea224205..e195b481362 100644 --- a/lightning/src/util/test_utils.rs +++ b/lightning/src/util/test_utils.rs @@ -2226,10 +2226,16 @@ impl TestWalletSource { utxo.output.value, EcdsaSighashType::All, )?; + #[cfg(not(feature = "grind_signatures"))] let signature = self.secp.sign_ecdsa( &secp256k1::Message::from_digest(sighash.to_byte_array()), &self.secret_key, ); + #[cfg(feature = "grind_signatures")] + let signature = self.secp.sign_ecdsa_low_r( + &secp256k1::Message::from_digest(sighash.to_byte_array()), + &self.secret_key, + ); let bitcoin_sig = bitcoin::ecdsa::Signature { signature, sighash_type: EcdsaSighashType::All }; tx.input[i].witness = From 3aa457407df22771c08a24a0f5c739b78442c20a Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Wed, 5 Nov 2025 16:11:46 -0600 Subject: [PATCH 3/5] Use a tolerance when estimating remote fees The interactive-tx construction protocol uses an agreed upon fee rate. Since the bitcoind coin selection algorithm may underpay fees when no change output is needed, providing a tolerance when checking if the remote's fee contribution could avoid some unexpected failures. This commit introduces a 95% tolerance similar to Eclair. --- lightning/src/ln/interactivetxs.rs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/lightning/src/ln/interactivetxs.rs b/lightning/src/ln/interactivetxs.rs index 4c585e6a2d1..e729f455a98 100644 --- a/lightning/src/ln/interactivetxs.rs +++ b/lightning/src/ln/interactivetxs.rs @@ -257,6 +257,9 @@ impl_writeable_tlv_based!(ConstructedTransaction, { (11, shared_output_index, required), }); +/// The percent tolerance given to the remote when estimating if they paid enough fees. +const REMOTE_FEE_TOLERANCE_PERCENT: u64 = 95; + impl ConstructedTransaction { fn new(context: NegotiationContext) -> Result { let remote_inputs_value = context.remote_inputs_value(); @@ -315,8 +318,10 @@ impl ConstructedTransaction { // - the peer's paid feerate does not meet or exceed the agreed feerate (based on the minimum fee). let remote_fees_contributed = remote_inputs_value.saturating_sub(remote_outputs_value); - let required_remote_contribution_fee = - fee_for_weight(context.feerate_sat_per_kw, remote_weight_contributed); + let required_remote_contribution_fee = fee_for_weight( + (context.feerate_sat_per_kw as u64 * REMOTE_FEE_TOLERANCE_PERCENT / 100) as u32, + remote_weight_contributed, + ); if remote_fees_contributed < required_remote_contribution_fee { return Err(AbortReason::InsufficientFees); } @@ -2379,7 +2384,7 @@ mod tests { use super::{ get_output_weight, ConstructedTransaction, InteractiveTxSigningSession, TxInMetadata, P2TR_INPUT_WEIGHT_LOWER_BOUND, P2WPKH_INPUT_WEIGHT_LOWER_BOUND, - P2WSH_INPUT_WEIGHT_LOWER_BOUND, TX_COMMON_FIELDS_WEIGHT, + P2WSH_INPUT_WEIGHT_LOWER_BOUND, REMOTE_FEE_TOLERANCE_PERCENT, TX_COMMON_FIELDS_WEIGHT, }; const TEST_FEERATE_SATS_PER_KW: u32 = FEERATE_FLOOR_SATS_PER_KW * 10; @@ -2844,7 +2849,7 @@ mod tests { let outputs_weight = get_output_weight(&generate_p2wsh_script_pubkey()).to_wu(); let amount_adjusted_with_p2wpkh_fee = 1_000_000 - fee_for_weight( - TEST_FEERATE_SATS_PER_KW, + (TEST_FEERATE_SATS_PER_KW as u64 * REMOTE_FEE_TOLERANCE_PERCENT / 100) as u32, P2WPKH_INPUT_WEIGHT_LOWER_BOUND + TX_COMMON_FIELDS_WEIGHT + outputs_weight, ); do_test_interactive_tx_constructor(TestSession { @@ -2880,7 +2885,7 @@ mod tests { }); let amount_adjusted_with_p2wsh_fee = 1_000_000 - fee_for_weight( - TEST_FEERATE_SATS_PER_KW, + (TEST_FEERATE_SATS_PER_KW as u64 * REMOTE_FEE_TOLERANCE_PERCENT / 100) as u32, P2WSH_INPUT_WEIGHT_LOWER_BOUND + TX_COMMON_FIELDS_WEIGHT + outputs_weight, ); do_test_interactive_tx_constructor(TestSession { @@ -2916,7 +2921,7 @@ mod tests { }); let amount_adjusted_with_p2tr_fee = 1_000_000 - fee_for_weight( - TEST_FEERATE_SATS_PER_KW, + (TEST_FEERATE_SATS_PER_KW as u64 * REMOTE_FEE_TOLERANCE_PERCENT / 100) as u32, P2TR_INPUT_WEIGHT_LOWER_BOUND + TX_COMMON_FIELDS_WEIGHT + outputs_weight, ); do_test_interactive_tx_constructor(TestSession { From b4803c7e34567f6903de9848726cc73e8bf1234a Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Mon, 10 Nov 2025 17:34:56 -0600 Subject: [PATCH 4/5] Fix MAX_STANDARD_TX_WEIGHT check The interactive-tx construction protocol needs to make sure the constructed transaction does not exceed MAX_STANDARD_TX_WEIGHT. A naive estimate of the transaction weight after signing was used, but was not accurate. Specifically, it double-counted EMPTY_SCRIPT_SIG_WEIGHT and didn't include SEGWIT_MARKER_FLAG_WEIGHT. --- lightning/src/ln/interactivetxs.rs | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/lightning/src/ln/interactivetxs.rs b/lightning/src/ln/interactivetxs.rs index e729f455a98..4dceff93ad9 100644 --- a/lightning/src/ln/interactivetxs.rs +++ b/lightning/src/ln/interactivetxs.rs @@ -29,6 +29,7 @@ use bitcoin::{ use crate::chain::chaininterface::fee_for_weight; use crate::ln::chan_utils::{ BASE_INPUT_WEIGHT, EMPTY_SCRIPT_SIG_WEIGHT, FUNDING_TRANSACTION_WITNESS_WEIGHT, + SEGWIT_MARKER_FLAG_WEIGHT, }; use crate::ln::channel::{FundingNegotiationContext, TOTAL_BITCOIN_SUPPLY_SATOSHIS}; use crate::ln::funding::FundingTxInput; @@ -266,10 +267,11 @@ impl ConstructedTransaction { let remote_outputs_value = context.remote_outputs_value(); let remote_weight_contributed = context.remote_weight_contributed(); - let satisfaction_weight = - Weight::from_wu(context.inputs.iter().fold(0u64, |value, (_, input)| { - value.saturating_add(input.satisfaction_weight().to_wu()) - })); + let expected_witness_weight = context.inputs.iter().fold(0u64, |value, (_, input)| { + value + .saturating_add(input.satisfaction_weight().to_wu()) + .saturating_sub(EMPTY_SCRIPT_SIG_WEIGHT) + }); let lock_time = context.tx_locktime; @@ -342,8 +344,13 @@ impl ConstructedTransaction { return Err(AbortReason::MissingFundingOutput); } - let tx_weight = tx.tx.weight().checked_add(satisfaction_weight).unwrap_or(Weight::MAX); - if tx_weight > Weight::from_wu(MAX_STANDARD_TX_WEIGHT as u64) { + let tx_weight = tx + .tx + .weight() + .to_wu() + .saturating_add(SEGWIT_MARKER_FLAG_WEIGHT) + .saturating_add(expected_witness_weight); + if tx_weight > MAX_STANDARD_TX_WEIGHT as u64 { return Err(AbortReason::TransactionTooLarge); } From 2330ed9892f64c09762e77f6e4dfad59f89e0d1b Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Mon, 10 Nov 2025 12:16:32 -0600 Subject: [PATCH 5/5] Add method to convert FundingTxInput to Utxo Useful for re-using code producing a FundingTxInput set to implement CoinSelectionSource. --- lightning/src/ln/funding.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lightning/src/ln/funding.rs b/lightning/src/ln/funding.rs index 7281a5f6d69..f80b2b6daea 100644 --- a/lightning/src/ln/funding.rs +++ b/lightning/src/ln/funding.rs @@ -230,4 +230,9 @@ impl FundingTxInput { pub fn set_sequence(&mut self, sequence: Sequence) { self.sequence = sequence; } + + /// Converts the [`FundingTxInput`] into a [`Utxo`] for coin selection. + pub fn into_utxo(self) -> Utxo { + self.utxo + } }