Account for grind_signatures in splice funding tx
What changed, and why it matters
This commit fixes a fee-estimation bug in LDK's experimental splicing code. When the optional `grind_signatures` feature is enabled, signatures are guaranteed to be one byte smaller than the normal maximum. The splice funding-transaction fee estimator was not accounting for that smaller signature, so it could slightly overestimate fees. The patch subtracts one weight unit from the relevant estimates when the feature is on, and updates tests to match. It is a correctness/efficiency fix rather than a critical vulnerability.
Review and merge as a routine correctness fix. Users running splicing with `grind_signatures` enabled will get slightly more accurate fee estimates. No urgent security response is indicated by the diff.
Security signals we found
Fee-estimation mismatch between assumed max signature size and actual signature size when `grind_signatures` is enabled
Splicing code path affected (v2 channel funding / interactive transaction construction)
Test-only signing utility updated to use low-R signatures consistently with the feature flag
No memory-safety, cryptographic, or authorization changes
Evidence from the diff
The change adjusts weight calculations for v2/splice funding transactions and P2WPKH inputs when the grind_signatures Cargo feature is enabled. grind_signatures causes secp256k1 to produce low-R ECDSA signatures, which are always at least one byte shorter than the 72-byte upper bound used elsewhere. The patch subtracts 1 WU from FUNDING_TRANSACTION_WITNESS_WEIGHT usage in channel::estimate_v2_funding_transaction_fee, funding::FundingTxInput::new_p2wpkh, interactivetxs::InputOwned::satisfaction_weight, and interactivetxs::calculate_change_output_value. It also updates unit-test expected values and makes test utilities actually use sign_ecdsa_low_r when the feature is enabled. Two existing comments in sign/mod.rs are reformatted only.
Changed components
lightning/src/ln/channel.rslightning/src/ln/funding.rslightning/src/ln/interactivetxs.rslightning/src/ln/chan_utils.rslightning/src/sign/mod.rslightning/src/util/test_utils.rsInspect captured patch +97 / −49
diff --git a/lightning/src/ln/chan_utils.rs b/lightning/src/ln/chan_utils.rs
index 242f560..e70d935 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 234168e..eab7055 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 3cabb82..7281a5f 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<Self, ()> {
- 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 b223fa3..4c585e6 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 ca80338..77076a4 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 ad8ea22..e195b48 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 =
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.