Skip to content

Commit 0a75f92

Browse files
committed
Add TxBuilder::get_next_commitment_stats
Given a snapshot of the lightning state machine, `TxBuilder::get_next_commitment_stats` calculates the transaction fees, the dust exposure, and the holder and counterparty balances (the balances themselves do *not* account for the transaction fee).
1 parent 1f4bd17 commit 0a75f92

File tree

2 files changed

+229
-9
lines changed

2 files changed

+229
-9
lines changed

lightning/src/ln/chan_utils.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ pub(crate) fn commit_tx_fee_sat(feerate_per_kw: u32, num_htlcs: usize, channel_t
236236
}
237237

238238
/// Returns the fees for success and timeout second stage HTLC transactions.
239-
pub(super) fn second_stage_tx_fees_sat(
239+
pub(crate) fn second_stage_tx_fees_sat(
240240
channel_type: &ChannelTypeFeatures, feerate_sat_per_1000_weight: u32,
241241
) -> (u64, u64) {
242242
if channel_type.supports_anchors_zero_fee_htlc_tx()

lightning/src/sign/tx_builder.rs

Lines changed: 228 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,130 @@
11
//! Defines the `TxBuilder` trait, and the `SpecTxBuilder` type
2+
#![allow(dead_code)]
23

4+
use core::cmp;
35
use core::ops::Deref;
46

57
use bitcoin::secp256k1::{self, PublicKey, Secp256k1};
68

79
use crate::ln::chan_utils::{
8-
commit_tx_fee_sat, htlc_success_tx_weight, htlc_timeout_tx_weight,
9-
ChannelTransactionParameters, CommitmentTransaction, HTLCOutputInCommitment,
10+
commit_tx_fee_sat, htlc_success_tx_weight, htlc_timeout_tx_weight, htlc_tx_fees_sat,
11+
second_stage_tx_fees_sat, ChannelTransactionParameters, CommitmentTransaction,
12+
HTLCOutputInCommitment,
1013
};
1114
use crate::ln::channel::{CommitmentStats, ANCHOR_OUTPUT_VALUE_SATOSHI};
1215
use crate::prelude::*;
1316
use crate::types::features::ChannelTypeFeatures;
1417
use crate::util::logger::Logger;
1518

19+
pub(crate) struct HTLCAmountDirection {
20+
pub outbound: bool,
21+
pub amount_msat: u64,
22+
}
23+
24+
impl HTLCAmountDirection {
25+
fn is_dust(
26+
&self, local: bool, feerate_per_kw: u32, broadcaster_dust_limit_satoshis: u64,
27+
channel_type: &ChannelTypeFeatures,
28+
) -> bool {
29+
let (success_tx_fee_sat, timeout_tx_fee_sat) =
30+
second_stage_tx_fees_sat(channel_type, feerate_per_kw);
31+
let htlc_tx_fee_sat =
32+
if self.outbound == local { timeout_tx_fee_sat } else { success_tx_fee_sat };
33+
self.amount_msat / 1000 < broadcaster_dust_limit_satoshis + htlc_tx_fee_sat
34+
}
35+
}
36+
37+
pub(crate) struct NextCommitmentStats {
38+
pub inbound_htlcs_count: usize,
39+
pub inbound_htlcs_value_msat: u64,
40+
pub holder_balance_before_fee_msat: Option<u64>,
41+
pub counterparty_balance_before_fee_msat: Option<u64>,
42+
pub nondust_htlc_count: usize,
43+
pub commit_tx_fee_sat: u64,
44+
pub dust_exposure_msat: u64,
45+
// If the counterparty sets a feerate on the channel in excess of our dust_exposure_limiting_feerate,
46+
// this should be set to the dust exposure that would result from us adding an additional nondust outbound
47+
// htlc on the counterparty's commitment transaction.
48+
pub extra_nondust_htlc_on_counterparty_tx_dust_exposure_msat: Option<u64>,
49+
}
50+
51+
#[rustfmt::skip]
52+
fn excess_fees_on_counterparty_tx_dust_exposure_msat(
53+
next_commitment_htlcs: &[HTLCAmountDirection], dust_buffer_feerate: u32,
54+
excess_feerate: u32, counterparty_dust_limit_satoshis: u64, mut on_counterparty_tx_dust_exposure_msat: u64,
55+
channel_type: &ChannelTypeFeatures,
56+
) -> (u64, u64) {
57+
58+
let on_counterparty_tx_accepted_nondust_htlcs = next_commitment_htlcs.iter().filter(|htlc| htlc.outbound && !htlc.is_dust(false, dust_buffer_feerate, counterparty_dust_limit_satoshis, channel_type)).count();
59+
let on_counterparty_tx_offered_nondust_htlcs = next_commitment_htlcs.iter().filter(|htlc| !htlc.outbound && !htlc.is_dust(false, dust_buffer_feerate, counterparty_dust_limit_satoshis, channel_type)).count();
60+
61+
let extra_htlc_commit_tx_fee_sat = commit_tx_fee_sat(excess_feerate, on_counterparty_tx_accepted_nondust_htlcs + 1 + on_counterparty_tx_offered_nondust_htlcs, channel_type);
62+
let extra_htlc_htlc_tx_fees_sat = htlc_tx_fees_sat(excess_feerate, on_counterparty_tx_accepted_nondust_htlcs + 1, on_counterparty_tx_offered_nondust_htlcs, channel_type);
63+
64+
let commit_tx_fee_sat = commit_tx_fee_sat(excess_feerate, on_counterparty_tx_accepted_nondust_htlcs + on_counterparty_tx_offered_nondust_htlcs, channel_type);
65+
let htlc_tx_fees_sat = htlc_tx_fees_sat(excess_feerate, on_counterparty_tx_accepted_nondust_htlcs, on_counterparty_tx_offered_nondust_htlcs, channel_type);
66+
67+
let extra_htlc_dust_exposure_msat = on_counterparty_tx_dust_exposure_msat + (extra_htlc_commit_tx_fee_sat + extra_htlc_htlc_tx_fees_sat) * 1000;
68+
on_counterparty_tx_dust_exposure_msat += (commit_tx_fee_sat + htlc_tx_fees_sat) * 1000;
69+
70+
(
71+
on_counterparty_tx_dust_exposure_msat,
72+
extra_htlc_dust_exposure_msat,
73+
)
74+
}
75+
76+
fn subtract_addl_outputs(
77+
is_outbound_from_holder: bool, value_to_self_after_htlcs_msat: Option<u64>,
78+
value_to_remote_after_htlcs_msat: Option<u64>, channel_type: &ChannelTypeFeatures,
79+
) -> (Option<u64>, Option<u64>) {
80+
let total_anchors_sat = if channel_type.supports_anchors_zero_fee_htlc_tx() {
81+
ANCHOR_OUTPUT_VALUE_SATOSHI * 2
82+
} else {
83+
0
84+
};
85+
86+
// We MUST use checked subs here, as the funder's balance is not guaranteed to be greater
87+
// than or equal to `total_anchors_sat`.
88+
//
89+
// This is because when the remote party sends an `update_fee` message, we build the new
90+
// commitment transaction *before* checking whether the remote party's balance is enough to
91+
// cover the total anchor sum.
92+
93+
let local_balance_before_fee_msat = if is_outbound_from_holder {
94+
value_to_self_after_htlcs_msat
95+
.and_then(|balance_msat| balance_msat.checked_sub(total_anchors_sat * 1000))
96+
} else {
97+
value_to_self_after_htlcs_msat
98+
};
99+
100+
let remote_balance_before_fee_msat = if !is_outbound_from_holder {
101+
value_to_remote_after_htlcs_msat
102+
.and_then(|balance_msat| balance_msat.checked_sub(total_anchors_sat * 1000))
103+
} else {
104+
value_to_remote_after_htlcs_msat
105+
};
106+
107+
(local_balance_before_fee_msat, remote_balance_before_fee_msat)
108+
}
109+
110+
fn get_dust_buffer_feerate(feerate_per_kw: u32) -> u32 {
111+
// When calculating our exposure to dust HTLCs, we assume that the channel feerate
112+
// may, at any point, increase by at least 10 sat/vB (i.e 2530 sat/kWU) or 25%,
113+
// whichever is higher. This ensures that we aren't suddenly exposed to significantly
114+
// more dust balance if the feerate increases when we have several HTLCs pending
115+
// which are near the dust limit.
116+
let feerate_plus_quarter = feerate_per_kw.checked_mul(1250).map(|v| v / 1000);
117+
cmp::max(feerate_per_kw.saturating_add(2530), feerate_plus_quarter.unwrap_or(u32::MAX))
118+
}
119+
16120
pub(crate) trait TxBuilder {
121+
fn get_next_commitment_stats(
122+
&self, local: bool, is_outbound_from_holder: bool, channel_value_satoshis: u64,
123+
value_to_holder_msat: u64, next_commitment_htlcs: &[HTLCAmountDirection],
124+
addl_nondust_htlc_count: usize, feerate_per_kw: u32,
125+
dust_exposure_limiting_feerate: Option<u32>, broadcaster_dust_limit_satoshis: u64,
126+
channel_type: &ChannelTypeFeatures,
127+
) -> NextCommitmentStats;
17128
fn commit_tx_fee_sat(
18129
&self, feerate_per_kw: u32, nondust_htlc_count: usize, channel_type: &ChannelTypeFeatures,
19130
) -> u64;
@@ -25,7 +136,7 @@ pub(crate) trait TxBuilder {
25136
&self, local: bool, commitment_number: u64, per_commitment_point: &PublicKey,
26137
channel_parameters: &ChannelTransactionParameters, secp_ctx: &Secp256k1<secp256k1::All>,
27138
value_to_self_msat: u64, htlcs_in_tx: Vec<HTLCOutputInCommitment>, feerate_per_kw: u32,
28-
broadcaster_dust_limit_sat: u64, logger: &L,
139+
broadcaster_dust_limit_satoshis: u64, logger: &L,
29140
) -> (CommitmentTransaction, CommitmentStats)
30141
where
31142
L::Target: Logger;
@@ -34,6 +145,115 @@ pub(crate) trait TxBuilder {
34145
pub(crate) struct SpecTxBuilder {}
35146

36147
impl TxBuilder for SpecTxBuilder {
148+
fn get_next_commitment_stats(
149+
&self, local: bool, is_outbound_from_holder: bool, channel_value_satoshis: u64,
150+
value_to_holder_msat: u64, next_commitment_htlcs: &[HTLCAmountDirection],
151+
addl_nondust_htlc_count: usize, feerate_per_kw: u32,
152+
dust_exposure_limiting_feerate: Option<u32>, broadcaster_dust_limit_satoshis: u64,
153+
channel_type: &ChannelTypeFeatures,
154+
) -> NextCommitmentStats {
155+
let excess_feerate_opt =
156+
feerate_per_kw.checked_sub(dust_exposure_limiting_feerate.unwrap_or(0));
157+
// Dust exposure is only decoupled from feerate for zero fee commitment channels.
158+
let is_zero_fee_comm = channel_type.supports_anchor_zero_fee_commitments();
159+
debug_assert_eq!(is_zero_fee_comm, dust_exposure_limiting_feerate.is_none());
160+
if is_zero_fee_comm {
161+
debug_assert_eq!(feerate_per_kw, 0);
162+
debug_assert_eq!(excess_feerate_opt, Some(0));
163+
debug_assert_eq!(addl_nondust_htlc_count, 0);
164+
}
165+
166+
// Calculate inbound htlc count
167+
let inbound_htlcs_count =
168+
next_commitment_htlcs.iter().filter(|htlc| !htlc.outbound).count();
169+
170+
// Calculate balances after htlcs
171+
let value_to_counterparty_msat = (channel_value_satoshis * 1000)
172+
.checked_sub(value_to_holder_msat)
173+
.expect("value_to_holder_msat outgrew the value of the channel!");
174+
let outbound_htlcs_value_msat: u64 = next_commitment_htlcs
175+
.iter()
176+
.filter_map(|htlc| htlc.outbound.then_some(htlc.amount_msat))
177+
.sum();
178+
let inbound_htlcs_value_msat: u64 = next_commitment_htlcs
179+
.iter()
180+
.filter_map(|htlc| (!htlc.outbound).then_some(htlc.amount_msat))
181+
.sum();
182+
// Note there is no guarantee that the subtractions of the HTLC amounts don't
183+
// overflow, so we do not panic. Instead, we return `None` to signal an overflow
184+
// to channel, and let channel take the appropriate action.
185+
let value_to_holder_after_htlcs_msat =
186+
value_to_holder_msat.checked_sub(outbound_htlcs_value_msat);
187+
let value_to_counterparty_after_htlcs_msat =
188+
value_to_counterparty_msat.checked_sub(inbound_htlcs_value_msat);
189+
190+
// Subtract the anchors from the channel funder
191+
let (holder_balance_before_fee_msat, counterparty_balance_before_fee_msat) =
192+
subtract_addl_outputs(
193+
is_outbound_from_holder,
194+
value_to_holder_after_htlcs_msat,
195+
value_to_counterparty_after_htlcs_msat,
196+
channel_type,
197+
);
198+
199+
// Increment the feerate by a buffer to calculate dust exposure
200+
let dust_buffer_feerate = get_dust_buffer_feerate(feerate_per_kw);
201+
202+
// Calculate fees on commitment transaction
203+
let nondust_htlc_count = next_commitment_htlcs
204+
.iter()
205+
.filter(|htlc| {
206+
!htlc.is_dust(local, feerate_per_kw, broadcaster_dust_limit_satoshis, channel_type)
207+
})
208+
.count();
209+
let commit_tx_fee_sat = commit_tx_fee_sat(
210+
feerate_per_kw,
211+
nondust_htlc_count + addl_nondust_htlc_count,
212+
channel_type,
213+
);
214+
215+
// Calculate dust exposure on commitment transaction
216+
let dust_exposure_msat = next_commitment_htlcs
217+
.iter()
218+
.filter_map(|htlc| {
219+
htlc.is_dust(
220+
local,
221+
dust_buffer_feerate,
222+
broadcaster_dust_limit_satoshis,
223+
channel_type,
224+
)
225+
.then_some(htlc.amount_msat)
226+
})
227+
.sum();
228+
229+
// Count the excess fees on the counterparty's transaction as dust
230+
let (dust_exposure_msat, extra_nondust_htlc_on_counterparty_tx_dust_exposure_msat) =
231+
if let (Some(excess_feerate), false) = (excess_feerate_opt, local) {
232+
let (dust_exposure_msat, extra_nondust_htlc_exposure_msat) =
233+
excess_fees_on_counterparty_tx_dust_exposure_msat(
234+
&next_commitment_htlcs,
235+
dust_buffer_feerate,
236+
excess_feerate,
237+
broadcaster_dust_limit_satoshis,
238+
dust_exposure_msat,
239+
channel_type,
240+
);
241+
(dust_exposure_msat, Some(extra_nondust_htlc_exposure_msat))
242+
} else {
243+
(dust_exposure_msat, None)
244+
};
245+
246+
NextCommitmentStats {
247+
inbound_htlcs_count,
248+
inbound_htlcs_value_msat,
249+
holder_balance_before_fee_msat,
250+
counterparty_balance_before_fee_msat,
251+
nondust_htlc_count: nondust_htlc_count + addl_nondust_htlc_count,
252+
commit_tx_fee_sat,
253+
dust_exposure_msat,
254+
extra_nondust_htlc_on_counterparty_tx_dust_exposure_msat,
255+
}
256+
}
37257
fn commit_tx_fee_sat(
38258
&self, feerate_per_kw: u32, nondust_htlc_count: usize, channel_type: &ChannelTypeFeatures,
39259
) -> u64 {
@@ -74,7 +294,7 @@ impl TxBuilder for SpecTxBuilder {
74294
&self, local: bool, commitment_number: u64, per_commitment_point: &PublicKey,
75295
channel_parameters: &ChannelTransactionParameters, secp_ctx: &Secp256k1<secp256k1::All>,
76296
value_to_self_msat: u64, mut htlcs_in_tx: Vec<HTLCOutputInCommitment>, feerate_per_kw: u32,
77-
broadcaster_dust_limit_sat: u64, logger: &L,
297+
broadcaster_dust_limit_satoshis: u64, logger: &L,
78298
) -> (CommitmentTransaction, CommitmentStats)
79299
where
80300
L::Target: Logger,
@@ -95,7 +315,7 @@ impl TxBuilder for SpecTxBuilder {
95315
// As required by the spec, round down
96316
feerate_per_kw as u64 * htlc_tx_weight / 1000
97317
};
98-
amount_msat / 1000 < broadcaster_dust_limit_sat + htlc_tx_fee_sat
318+
amount_msat / 1000 < broadcaster_dust_limit_satoshis + htlc_tx_fee_sat
99319
};
100320

101321
// Trim dust htlcs
@@ -107,7 +327,7 @@ impl TxBuilder for SpecTxBuilder {
107327
remote_htlc_total_msat += htlc.amount_msat;
108328
}
109329
if is_dust(htlc.offered, htlc.amount_msat) {
110-
log_trace!(logger, " ...trimming {} HTLC with value {}sat, hash {}, due to dust limit {}", if htlc.offered == local { "outbound" } else { "inbound" }, htlc.amount_msat / 1000, htlc.payment_hash, broadcaster_dust_limit_sat);
330+
log_trace!(logger, " ...trimming {} HTLC with value {}sat, hash {}, due to dust limit {}", if htlc.offered == local { "outbound" } else { "inbound" }, htlc.amount_msat / 1000, htlc.payment_hash, broadcaster_dust_limit_satoshis);
111331
false
112332
} else {
113333
true
@@ -142,13 +362,13 @@ impl TxBuilder for SpecTxBuilder {
142362
let mut to_broadcaster_value_sat = if local { value_to_self } else { value_to_remote };
143363
let mut to_countersignatory_value_sat = if local { value_to_remote } else { value_to_self };
144364

145-
if to_broadcaster_value_sat >= broadcaster_dust_limit_sat {
365+
if to_broadcaster_value_sat >= broadcaster_dust_limit_satoshis {
146366
log_trace!(logger, " ...including {} output with value {}", if local { "to_local" } else { "to_remote" }, to_broadcaster_value_sat);
147367
} else {
148368
to_broadcaster_value_sat = 0;
149369
}
150370

151-
if to_countersignatory_value_sat >= broadcaster_dust_limit_sat {
371+
if to_countersignatory_value_sat >= broadcaster_dust_limit_satoshis {
152372
log_trace!(logger, " ...including {} output with value {}", if local { "to_remote" } else { "to_local" }, to_countersignatory_value_sat);
153373
} else {
154374
to_countersignatory_value_sat = 0;

0 commit comments

Comments
 (0)