Use a tolerance when estimating remote fees
What changed, and why it matters
This change relaxes a fee-checking rule during Bitcoin Lightning channel transactions. When two parties jointly build a transaction, the code now accepts a remote party's fee contribution that is up to 5% lower than the agreed rate, matching how another major implementation (Eclair) behaves. This avoids false failures caused by real-world coin-selection quirks, but it also means the local node may accept slightly less fee than strictly required.
Treat as a minor protocol-hardening/compatibility fix rather than an emergency security patch. Review whether a 5% tolerance is sufficient for all coin-selection edge cases and ensure the tolerance is documented in protocol specs. Consider whether an adversarial peer could exploit the relaxed check to systematically underpay fees across many transactions.
Security signals we found
Protocol fee validation relaxed by 5%
Change aligns with Eclair behavior, reducing cross-implementation incompatibility
Potential for local node to accept lower-than-agreed fee contribution from remote peer
No input validation, serialization, or memory-safety changes observed
Evidence from the diff
The patch introduces a 95% tolerance constant (REMOTE_FEE_TOLERANCE_PERCENT) applied to the agreed feerate when verifying that the remote peer contributed enough fees in the interactive-tx protocol. Previously the remote’s contribution was compared against fee_for_weight(context.feerate_sat_per_kw, remote_weight_contributed); now it is compared against fee_for_weight(feerate * 95 / 100, remote_weight_contributed). Tests are updated to expect the reduced threshold. The commit message attributes the need for this to bitcoind coin selection potentially underpaying fees when no change output is needed.
Changed components
lightning/src/ln/interactivetxs.rsInteractive transaction construction protocol fee validationConstructedTransaction::newInspect captured patch +11 / −6
diff --git a/lightning/src/ln/interactivetxs.rs b/lightning/src/ln/interactivetxs.rs
index 4c585e6..e729f45 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<Self, AbortReason> {
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 {
Why this scored 33/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.