Skip to content

Commit 0bf652d

Browse files
authored
Merge pull request #4574 from tankyleo/2026-04-reserve-breach
Don't trim HTLCs when calculating the commit tx fee including the fee spike multiple
2 parents 090930c + deb51ae commit 0bf652d

3 files changed

Lines changed: 457 additions & 82 deletions

File tree

lightning/src/ln/channel.rs

Lines changed: 31 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -4253,6 +4253,7 @@ impl<SP: SignerProvider> ChannelContext<SP> {
42534253
include_counterparty_unknown_htlcs,
42544254
addl_nondust_htlc_count,
42554255
channel_context.feerate_per_kw,
4256+
false,
42564257
dust_exposure_limiting_feerate,
42574258
)
42584259
.map_err(|()| {
@@ -4549,6 +4550,7 @@ impl<SP: SignerProvider> ChannelContext<SP> {
45494550
include_counterparty_unknown_htlcs,
45504551
addl_nondust_htlc_count,
45514552
channel_context.feerate_per_kw,
4553+
false,
45524554
dust_exposure_limiting_feerate,
45534555
)
45544556
.map_err(|()| APIError::APIMisuseError {
@@ -5339,7 +5341,7 @@ impl<SP: SignerProvider> ChannelContext<SP> {
53395341
fn get_next_local_commitment_stats(
53405342
&self, funding: &FundingScope, htlc_candidate: Option<HTLCAmountDirection>,
53415343
include_counterparty_unknown_htlcs: bool, addl_nondust_htlc_count: usize,
5342-
feerate_per_kw: u32, dust_exposure_limiting_feerate: Option<u32>,
5344+
feerate_per_kw: u32, assume_fee_spike: bool, dust_exposure_limiting_feerate: Option<u32>,
53435345
) -> Result<(ChannelStats, Vec<HTLCAmountDirection>), ()> {
53445346
let next_commitment_htlcs = self.get_next_commitment_htlcs(
53455347
true,
@@ -5361,6 +5363,7 @@ impl<SP: SignerProvider> ChannelContext<SP> {
53615363
&next_commitment_htlcs,
53625364
addl_nondust_htlc_count,
53635365
feerate_per_kw,
5366+
assume_fee_spike,
53645367
dust_exposure_limiting_feerate,
53655368
max_dust_htlc_exposure_msat,
53665369
channel_constraints,
@@ -5385,6 +5388,7 @@ impl<SP: SignerProvider> ChannelContext<SP> {
53855388
&next_commitment_htlcs,
53865389
0,
53875390
feerate_per_kw,
5391+
false,
53885392
dust_exposure_limiting_feerate,
53895393
max_dust_htlc_exposure_msat,
53905394
channel_constraints,
@@ -5406,7 +5410,7 @@ impl<SP: SignerProvider> ChannelContext<SP> {
54065410
fn get_next_remote_commitment_stats(
54075411
&self, funding: &FundingScope, htlc_candidate: Option<HTLCAmountDirection>,
54085412
include_counterparty_unknown_htlcs: bool, addl_nondust_htlc_count: usize,
5409-
feerate_per_kw: u32, dust_exposure_limiting_feerate: Option<u32>,
5413+
feerate_per_kw: u32, assume_fee_spike: bool, dust_exposure_limiting_feerate: Option<u32>,
54105414
) -> Result<(ChannelStats, Vec<HTLCAmountDirection>), ()> {
54115415
let next_commitment_htlcs = self.get_next_commitment_htlcs(
54125416
false,
@@ -5428,6 +5432,7 @@ impl<SP: SignerProvider> ChannelContext<SP> {
54285432
&next_commitment_htlcs,
54295433
addl_nondust_htlc_count,
54305434
feerate_per_kw,
5435+
assume_fee_spike,
54315436
dust_exposure_limiting_feerate,
54325437
max_dust_htlc_exposure_msat,
54335438
channel_constraints,
@@ -5452,6 +5457,7 @@ impl<SP: SignerProvider> ChannelContext<SP> {
54525457
&next_commitment_htlcs,
54535458
0,
54545459
feerate_per_kw,
5460+
false,
54555461
dust_exposure_limiting_feerate,
54565462
max_dust_htlc_exposure_msat,
54575463
channel_constraints,
@@ -5494,6 +5500,7 @@ impl<SP: SignerProvider> ChannelContext<SP> {
54945500
include_counterparty_unknown_htlcs,
54955501
fee_spike_buffer_htlc,
54965502
self.feerate_per_kw,
5503+
false,
54975504
dust_exposure_limiting_feerate,
54985505
)
54995506
.map_err(|()| {
@@ -5552,6 +5559,7 @@ impl<SP: SignerProvider> ChannelContext<SP> {
55525559
include_counterparty_unknown_htlcs,
55535560
fee_spike_buffer_htlc,
55545561
self.feerate_per_kw,
5562+
false,
55555563
dust_exposure_limiting_feerate,
55565564
)
55575565
.map_err(|()| {
@@ -5578,6 +5586,7 @@ impl<SP: SignerProvider> ChannelContext<SP> {
55785586
include_counterparty_unknown_htlcs,
55795587
0,
55805588
new_feerate_per_kw,
5589+
false,
55815590
dust_exposure_limiting_feerate,
55825591
)
55835592
.map_err(|()| {
@@ -5599,6 +5608,7 @@ impl<SP: SignerProvider> ChannelContext<SP> {
55995608
include_counterparty_unknown_htlcs,
56005609
0,
56015610
new_feerate_per_kw,
5611+
false,
56025612
dust_exposure_limiting_feerate,
56035613
)
56045614
.map_err(|()| {
@@ -5779,6 +5789,7 @@ impl<SP: SignerProvider> ChannelContext<SP> {
57795789
include_counterparty_unknown_htlcs,
57805790
CONCURRENT_INBOUND_HTLC_FEE_BUFFER as usize,
57815791
feerate_per_kw,
5792+
false,
57825793
dust_exposure_limiting_feerate,
57835794
) {
57845795
stats
@@ -5818,6 +5829,7 @@ impl<SP: SignerProvider> ChannelContext<SP> {
58185829
include_counterparty_unknown_htlcs,
58195830
CONCURRENT_INBOUND_HTLC_FEE_BUFFER as usize,
58205831
feerate_per_kw,
5832+
false,
58215833
dust_exposure_limiting_feerate,
58225834
) {
58235835
stats
@@ -5865,6 +5877,7 @@ impl<SP: SignerProvider> ChannelContext<SP> {
58655877
include_counterparty_unknown_htlcs,
58665878
fee_spike_buffer_htlc,
58675879
feerate,
5880+
false,
58685881
dust_exposure_limiting_feerate,
58695882
)
58705883
.map_err(|()| {
@@ -5881,6 +5894,7 @@ impl<SP: SignerProvider> ChannelContext<SP> {
58815894
include_counterparty_unknown_htlcs,
58825895
fee_spike_buffer_htlc,
58835896
feerate,
5897+
false,
58845898
dust_exposure_limiting_feerate,
58855899
)
58865900
.map_err(|()| {
@@ -5917,21 +5931,14 @@ impl<SP: SignerProvider> ChannelContext<SP> {
59175931
if !funding.is_outbound() {
59185932
// Note that with anchor outputs we are no longer as sensitive to fee spikes, so we don't need
59195933
// to account for them.
5920-
let fee_spike_multiple =
5921-
if !funding.get_channel_type().supports_anchors_zero_fee_htlc_tx() {
5922-
FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE as u32
5923-
} else {
5924-
1
5925-
};
5926-
// Note that the feerate is 0 in zero-fee commitment channels, so this statement is a noop
5927-
let spiked_feerate = feerate.saturating_mul(fee_spike_multiple);
59285934
let (remote_stats, _remote_htlcs) = self
59295935
.get_next_remote_commitment_stats(
59305936
funding,
59315937
None,
59325938
include_counterparty_unknown_htlcs,
59335939
fee_spike_buffer_htlc,
5934-
spiked_feerate,
5940+
feerate,
5941+
true,
59355942
dust_exposure_limiting_feerate,
59365943
)
59375944
.map_err(|()| {
@@ -6286,6 +6293,7 @@ impl<SP: SignerProvider> ChannelContext<SP> {
62866293
include_counterparty_unknown_htlcs,
62876294
addl_nondust_htlc_count,
62886295
self.feerate_per_kw,
6296+
false,
62896297
dust_exposure_limiting_feerate,
62906298
)
62916299
.map(|(remote_stats, _)| remote_stats.available_balances)?;
@@ -6307,6 +6315,7 @@ impl<SP: SignerProvider> ChannelContext<SP> {
63076315
include_counterparty_unknown_htlcs,
63086316
addl_nondust_htlc_count,
63096317
self.feerate_per_kw,
6318+
false,
63106319
dust_exposure_limiting_feerate,
63116320
)
63126321
.unwrap();
@@ -13596,16 +13605,6 @@ where
1359613605
// We are not interested in dust exposure
1359713606
let dust_exposure_limiting_feerate = None;
1359813607

13599-
// Note that the feerate is 0 in zero-fee commitment channels, so this statement is a noop
13600-
let feerate_per_kw = if !funding.get_channel_type().supports_anchors_zero_fee_htlc_tx() {
13601-
// Similar to HTLC additions, require the funder to have enough funds reserved for
13602-
// fees such that the feerate can jump without rendering the channel useless.
13603-
let spike_mul = FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE as u32;
13604-
self.context.feerate_per_kw.saturating_mul(spike_mul)
13605-
} else {
13606-
self.context.feerate_per_kw
13607-
};
13608-
1360913608
// Different dust limits on the local and remote commitments cause the commitment
1361013609
// transaction fee to be different depending on the commitment, so we grab the floor
1361113610
// of both balances across both commitments here.
@@ -13623,7 +13622,8 @@ where
1362313622
None, // htlc_candidate
1362413623
include_counterparty_unknown_htlcs,
1362513624
addl_nondust_htlc_count,
13626-
feerate_per_kw,
13625+
self.context.feerate_per_kw,
13626+
true,
1362713627
dust_exposure_limiting_feerate,
1362813628
)
1362913629
.map_err(|()| "Balance exhausted on local commitment")?;
@@ -13635,7 +13635,8 @@ where
1363513635
None, // htlc_candidate
1363613636
include_counterparty_unknown_htlcs,
1363713637
addl_nondust_htlc_count,
13638-
feerate_per_kw,
13638+
self.context.feerate_per_kw,
13639+
true,
1363913640
dust_exposure_limiting_feerate,
1364013641
)
1364113642
.map_err(|()| "Balance exhausted on remote commitment")?;
@@ -13675,6 +13676,7 @@ where
1367513676
include_counterparty_unknown_htlcs,
1367613677
0,
1367713678
self.context.feerate_per_kw,
13679+
false,
1367813680
dust_exposure_limiting_feerate,
1367913681
)
1368013682
.map_err(|()| "Balance exhausted on remote commitment")?;
@@ -17459,7 +17461,7 @@ mod tests {
1745917461
// Make sure when Node A calculates their local commitment transaction, none of the HTLCs pass
1746017462
// the dust limit check.
1746117463
let htlc_candidate = HTLCAmountDirection { amount_msat: htlc_amount_msat, outbound: true };
17462-
let local_commit_tx_fee = node_a_chan.context.get_next_local_commitment_stats(&node_a_chan.funding, Some(htlc_candidate), false, 0, node_a_chan.context.feerate_per_kw, None).unwrap().0.commitment_stats.commit_tx_fee_sat * 1000;
17464+
let local_commit_tx_fee = node_a_chan.context.get_next_local_commitment_stats(&node_a_chan.funding, Some(htlc_candidate), false, 0, node_a_chan.context.feerate_per_kw, false, None).unwrap().0.commitment_stats.commit_tx_fee_sat * 1000;
1746317465
let local_commit_fee_0_htlcs = commit_tx_fee_sat(node_a_chan.context.feerate_per_kw, 0, node_a_chan.funding.get_channel_type()) * 1000;
1746417466
assert_eq!(local_commit_tx_fee, local_commit_fee_0_htlcs);
1746517467

@@ -17468,7 +17470,7 @@ mod tests {
1746817470
node_a_chan.funding.channel_transaction_parameters.is_outbound_from_holder = false;
1746917471
let remote_commit_fee_3_htlcs = commit_tx_fee_sat(node_a_chan.context.feerate_per_kw, 3, node_a_chan.funding.get_channel_type()) * 1000;
1747017472
let htlc_candidate = HTLCAmountDirection { amount_msat: htlc_amount_msat, outbound: true };
17471-
let remote_commit_tx_fee = node_a_chan.context.get_next_remote_commitment_stats(&node_a_chan.funding, Some(htlc_candidate), false, 0, node_a_chan.context.feerate_per_kw, None).unwrap().0.commitment_stats.commit_tx_fee_sat * 1000;
17473+
let remote_commit_tx_fee = node_a_chan.context.get_next_remote_commitment_stats(&node_a_chan.funding, Some(htlc_candidate), false, 0, node_a_chan.context.feerate_per_kw, false, None).unwrap().0.commitment_stats.commit_tx_fee_sat * 1000;
1747217474
assert_eq!(remote_commit_tx_fee, remote_commit_fee_3_htlcs);
1747317475
}
1747417476

@@ -17503,27 +17505,27 @@ mod tests {
1750317505
// counted as dust when it shouldn't be.
1750417506
let htlc_amt_above_timeout = (htlc_timeout_tx_fee_sat + chan.context.holder_dust_limit_satoshis + 1) * 1000;
1750517507
let htlc_candidate = HTLCAmountDirection { amount_msat: htlc_amt_above_timeout, outbound: true };
17506-
let commitment_tx_fee = chan.context.get_next_local_commitment_stats(&chan.funding, Some(htlc_candidate), false, 0, chan.context.feerate_per_kw, None).unwrap().0.commitment_stats.commit_tx_fee_sat * 1000;
17508+
let commitment_tx_fee = chan.context.get_next_local_commitment_stats(&chan.funding, Some(htlc_candidate), false, 0, chan.context.feerate_per_kw, false, None).unwrap().0.commitment_stats.commit_tx_fee_sat * 1000;
1750717509
assert_eq!(commitment_tx_fee, commitment_tx_fee_1_htlc);
1750817510

1750917511
// If swapped: this HTLC would be counted as non-dust when it shouldn't be.
1751017512
let dust_htlc_amt_below_success = (htlc_success_tx_fee_sat + chan.context.holder_dust_limit_satoshis - 1) * 1000;
1751117513
let htlc_candidate = HTLCAmountDirection { amount_msat: dust_htlc_amt_below_success, outbound: false };
17512-
let commitment_tx_fee = chan.context.get_next_local_commitment_stats(&chan.funding, Some(htlc_candidate), false, 0, chan.context.feerate_per_kw, None).unwrap().0.commitment_stats.commit_tx_fee_sat * 1000;
17514+
let commitment_tx_fee = chan.context.get_next_local_commitment_stats(&chan.funding, Some(htlc_candidate), false, 0, chan.context.feerate_per_kw, false, None).unwrap().0.commitment_stats.commit_tx_fee_sat * 1000;
1751317515
assert_eq!(commitment_tx_fee, commitment_tx_fee_0_htlcs);
1751417516

1751517517
chan.funding.channel_transaction_parameters.is_outbound_from_holder = false;
1751617518

1751717519
// If swapped: this HTLC would be counted as non-dust when it shouldn't be.
1751817520
let dust_htlc_amt_above_timeout = (htlc_timeout_tx_fee_sat + chan.context.counterparty_dust_limit_satoshis + 1) * 1000;
1751917521
let htlc_candidate = HTLCAmountDirection { amount_msat: dust_htlc_amt_above_timeout, outbound: true };
17520-
let commitment_tx_fee = chan.context.get_next_remote_commitment_stats(&chan.funding, Some(htlc_candidate), false, 0, chan.context.feerate_per_kw, None).unwrap().0.commitment_stats.commit_tx_fee_sat * 1000;
17522+
let commitment_tx_fee = chan.context.get_next_remote_commitment_stats(&chan.funding, Some(htlc_candidate), false, 0, chan.context.feerate_per_kw, false, None).unwrap().0.commitment_stats.commit_tx_fee_sat * 1000;
1752117523
assert_eq!(commitment_tx_fee, commitment_tx_fee_0_htlcs);
1752217524

1752317525
// If swapped: this HTLC would be counted as dust when it shouldn't be.
1752417526
let htlc_amt_below_success = (htlc_success_tx_fee_sat + chan.context.counterparty_dust_limit_satoshis - 1) * 1000;
1752517527
let htlc_candidate = HTLCAmountDirection { amount_msat: htlc_amt_below_success, outbound: false };
17526-
let commitment_tx_fee = chan.context.get_next_remote_commitment_stats(&chan.funding, Some(htlc_candidate), false, 0, chan.context.feerate_per_kw, None).unwrap().0.commitment_stats.commit_tx_fee_sat * 1000;
17528+
let commitment_tx_fee = chan.context.get_next_remote_commitment_stats(&chan.funding, Some(htlc_candidate), false, 0, chan.context.feerate_per_kw, false, None).unwrap().0.commitment_stats.commit_tx_fee_sat * 1000;
1752717529
assert_eq!(commitment_tx_fee, commitment_tx_fee_1_htlc);
1752817530
}
1752917531

0 commit comments

Comments
 (0)