Skip to content

Commit a98a2af

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 39e8d7d commit a98a2af

File tree

1 file changed

+233
-7
lines changed

1 file changed

+233
-7
lines changed

lightning/src/sign/tx_builder.rs

Lines changed: 233 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,134 @@
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,
10+
commit_tx_fee_sat, htlc_success_tx_weight, htlc_timeout_tx_weight, htlc_tx_fees_sat,
911
ChannelTransactionParameters, CommitmentTransaction, HTLCOutputInCommitment,
1012
};
1113
use crate::ln::channel::{CommitmentStats, ANCHOR_OUTPUT_VALUE_SATOSHI};
1214
use crate::prelude::*;
1315
use crate::types::features::ChannelTypeFeatures;
1416
use crate::util::logger::Logger;
1517

18+
#[cfg_attr(any(test, fuzzing), derive(Clone, PartialEq, PartialOrd, Eq, Ord))]
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 htlc_tx_fee_sat = if channel_type.supports_anchors_zero_fee_htlc_tx() {
30+
0
31+
} else {
32+
let htlc_tx_weight = if self.outbound == local {
33+
htlc_timeout_tx_weight(channel_type)
34+
} else {
35+
htlc_success_tx_weight(channel_type)
36+
};
37+
// As required by the spec, round down
38+
feerate_per_kw as u64 * htlc_tx_weight / 1000
39+
};
40+
self.amount_msat / 1000 < broadcaster_dust_limit_satoshis + htlc_tx_fee_sat
41+
}
42+
}
43+
44+
pub(crate) struct NextCommitmentStats {
45+
pub next_commitment_htlcs: Vec<HTLCAmountDirection>,
46+
pub inbound_htlcs_count: usize,
47+
pub inbound_htlcs_value_msat: u64,
48+
pub holder_balance_msat: Option<u64>,
49+
pub counterparty_balance_msat: Option<u64>,
50+
pub commit_tx_fee_sat: u64,
51+
pub dust_exposure_msat: u64,
52+
// If the counterparty sets a feerate on the channel in excess of our dust_exposure_limiting_feerate,
53+
// this should be set to the dust exposure that would result from us adding an additional nondust outbound
54+
// htlc on the counterparty's commitment transaction.
55+
pub extra_nondust_htlc_on_counterparty_tx_dust_exposure_msat: Option<u64>,
56+
}
57+
58+
#[rustfmt::skip]
59+
fn excess_fees_on_counterparty_tx_dust_exposure_msat(
60+
next_commitment_htlcs: &[HTLCAmountDirection], dust_buffer_feerate: u32,
61+
excess_feerate: u32, broadcaster_dust_limit_satoshis: u64, mut on_counterparty_tx_dust_exposure_msat: u64,
62+
channel_type: &ChannelTypeFeatures,
63+
) -> (u64, u64) {
64+
65+
let on_counterparty_tx_accepted_nondust_htlcs = next_commitment_htlcs.iter().filter(|htlc| !htlc.is_dust(false, dust_buffer_feerate, broadcaster_dust_limit_satoshis, channel_type) && htlc.outbound).count();
66+
let on_counterparty_tx_offered_nondust_htlcs = next_commitment_htlcs.iter().filter(|htlc| !htlc.is_dust(false, dust_buffer_feerate, broadcaster_dust_limit_satoshis, channel_type) && !htlc.outbound).count();
67+
68+
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);
69+
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);
70+
71+
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);
72+
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);
73+
74+
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;
75+
on_counterparty_tx_dust_exposure_msat += (commit_tx_fee_sat + htlc_tx_fees_sat) * 1000;
76+
77+
(
78+
on_counterparty_tx_dust_exposure_msat,
79+
extra_htlc_dust_exposure_msat,
80+
)
81+
}
82+
83+
fn subtract_addl_outputs(
84+
is_outbound_from_holder: bool, value_to_self_after_htlcs: Option<u64>,
85+
value_to_remote_after_htlcs: Option<u64>, channel_type: &ChannelTypeFeatures,
86+
) -> (Option<u64>, Option<u64>) {
87+
let total_anchors_sat = if channel_type.supports_anchors_zero_fee_htlc_tx() {
88+
ANCHOR_OUTPUT_VALUE_SATOSHI * 2
89+
} else {
90+
0
91+
};
92+
93+
let mut local_balance_before_fee_msat = value_to_self_after_htlcs;
94+
let mut remote_balance_before_fee_msat = value_to_remote_after_htlcs;
95+
96+
// We MUST use checked subs here, as the funder's balance is not guaranteed to be greater
97+
// than or equal to `total_anchors_sat`.
98+
//
99+
// This is because when the remote party sends an `update_fee` message, we build the new
100+
// commitment transaction *before* checking whether the remote party's balance is enough to
101+
// cover the total anchor sum.
102+
103+
if is_outbound_from_holder {
104+
local_balance_before_fee_msat = local_balance_before_fee_msat
105+
.and_then(|balance_msat| balance_msat.checked_sub(total_anchors_sat * 1000));
106+
} else {
107+
remote_balance_before_fee_msat = remote_balance_before_fee_msat
108+
.and_then(|balance_msat| balance_msat.checked_sub(total_anchors_sat * 1000));
109+
}
110+
111+
(local_balance_before_fee_msat, remote_balance_before_fee_msat)
112+
}
113+
114+
fn get_dust_buffer_feerate(feerate_per_kw: u32) -> u32 {
115+
// When calculating our exposure to dust HTLCs, we assume that the channel feerate
116+
// may, at any point, increase by at least 10 sat/vB (i.e 2530 sat/kWU) or 25%,
117+
// whichever is higher. This ensures that we aren't suddenly exposed to significantly
118+
// more dust balance if the feerate increases when we have several HTLCs pending
119+
// which are near the dust limit.
120+
let feerate_plus_quarter = feerate_per_kw.checked_mul(1250).map(|v| v / 1000);
121+
cmp::max(feerate_per_kw.saturating_add(2530), feerate_plus_quarter.unwrap_or(u32::MAX))
122+
}
123+
16124
pub(crate) trait TxBuilder {
125+
fn get_next_commitment_stats(
126+
&self, local: bool, is_outbound_from_holder: bool, channel_value_satoshis: u64,
127+
value_to_holder_msat: u64, next_commitment_htlcs: Vec<HTLCAmountDirection>,
128+
addl_nondust_htlc_count: usize, feerate_per_kw: u32,
129+
dust_exposure_limiting_feerate: Option<u32>, broadcaster_dust_limit_satoshis: u64,
130+
channel_type: &ChannelTypeFeatures,
131+
) -> NextCommitmentStats;
17132
fn commit_tx_fee_sat(
18133
&self, feerate_per_kw: u32, nondust_htlc_count: usize, channel_type: &ChannelTypeFeatures,
19134
) -> u64;
@@ -25,7 +140,7 @@ pub(crate) trait TxBuilder {
25140
&self, local: bool, commitment_number: u64, per_commitment_point: &PublicKey,
26141
channel_parameters: &ChannelTransactionParameters, secp_ctx: &Secp256k1<secp256k1::All>,
27142
value_to_self_msat: u64, htlcs_in_tx: Vec<HTLCOutputInCommitment>, feerate_per_kw: u32,
28-
broadcaster_dust_limit_sat: u64, logger: &L,
143+
broadcaster_dust_limit_satoshis: u64, logger: &L,
29144
) -> (CommitmentTransaction, CommitmentStats)
30145
where
31146
L::Target: Logger;
@@ -34,6 +149,117 @@ pub(crate) trait TxBuilder {
34149
pub(crate) struct SpecTxBuilder {}
35150

36151
impl TxBuilder for SpecTxBuilder {
152+
fn get_next_commitment_stats(
153+
&self, local: bool, is_outbound_from_holder: bool, channel_value_satoshis: u64,
154+
value_to_holder_msat: u64, next_commitment_htlcs: Vec<HTLCAmountDirection>,
155+
addl_nondust_htlc_count: usize, feerate_per_kw: u32,
156+
dust_exposure_limiting_feerate: Option<u32>, broadcaster_dust_limit_satoshis: u64,
157+
channel_type: &ChannelTypeFeatures,
158+
) -> NextCommitmentStats {
159+
let excess_feerate_opt =
160+
feerate_per_kw.checked_sub(dust_exposure_limiting_feerate.unwrap_or(0));
161+
// Dust exposure is only decoupled from feerate for zero fee commitment channels.
162+
let is_zero_fee_comm = channel_type.supports_anchor_zero_fee_commitments();
163+
debug_assert_eq!(is_zero_fee_comm, dust_exposure_limiting_feerate.is_none());
164+
if is_zero_fee_comm {
165+
debug_assert_eq!(feerate_per_kw, 0);
166+
debug_assert_eq!(excess_feerate_opt, Some(0));
167+
debug_assert_eq!(addl_nondust_htlc_count, 0);
168+
}
169+
170+
// Calculate inbound htlc count
171+
let inbound_htlcs_count =
172+
next_commitment_htlcs.iter().filter(|htlc| !htlc.outbound).count();
173+
174+
// Calculate balances after htlcs
175+
let value_to_counterparty_msat = channel_value_satoshis * 1000 - value_to_holder_msat;
176+
let outbound_htlcs_value_msat: u64 = next_commitment_htlcs
177+
.iter()
178+
.filter_map(|htlc| htlc.outbound.then_some(htlc.amount_msat))
179+
.sum();
180+
let inbound_htlcs_value_msat: u64 = next_commitment_htlcs
181+
.iter()
182+
.filter_map(|htlc| (!htlc.outbound).then_some(htlc.amount_msat))
183+
.sum();
184+
let value_to_holder_after_htlcs =
185+
value_to_holder_msat.checked_sub(outbound_htlcs_value_msat);
186+
let value_to_counterparty_after_htlcs =
187+
value_to_counterparty_msat.checked_sub(inbound_htlcs_value_msat);
188+
189+
// Subtract the anchors from the channel funder
190+
let (holder_balance_msat, counterparty_balance_msat) = subtract_addl_outputs(
191+
is_outbound_from_holder,
192+
value_to_holder_after_htlcs,
193+
value_to_counterparty_after_htlcs,
194+
channel_type,
195+
);
196+
197+
// Increment the feerate by a buffer to calculate dust exposure
198+
let dust_buffer_feerate = get_dust_buffer_feerate(feerate_per_kw);
199+
200+
// Calculate fees on commitment transaction
201+
let nondust_htlc_count = next_commitment_htlcs
202+
.iter()
203+
.filter(|htlc| {
204+
!htlc.is_dust(local, feerate_per_kw, broadcaster_dust_limit_satoshis, channel_type)
205+
})
206+
.count();
207+
let commit_tx_fee_sat = commit_tx_fee_sat(
208+
feerate_per_kw,
209+
nondust_htlc_count + addl_nondust_htlc_count,
210+
channel_type,
211+
);
212+
213+
// Calculate dust exposure on commitment transaction
214+
let dust_exposure_msat = next_commitment_htlcs
215+
.iter()
216+
.filter_map(|htlc| {
217+
htlc.is_dust(
218+
local,
219+
dust_buffer_feerate,
220+
broadcaster_dust_limit_satoshis,
221+
channel_type,
222+
)
223+
.then_some(htlc.amount_msat)
224+
})
225+
.sum();
226+
227+
// Count the excess fees on the counterparty's transaction as dust
228+
if let (Some(excess_feerate), false) = (excess_feerate_opt, local) {
229+
let (dust_exposure_msat, extra_htlc_dust_exposure_msat) =
230+
excess_fees_on_counterparty_tx_dust_exposure_msat(
231+
&next_commitment_htlcs,
232+
dust_buffer_feerate,
233+
excess_feerate,
234+
broadcaster_dust_limit_satoshis,
235+
dust_exposure_msat,
236+
channel_type,
237+
);
238+
NextCommitmentStats {
239+
inbound_htlcs_count,
240+
inbound_htlcs_value_msat,
241+
next_commitment_htlcs,
242+
holder_balance_msat,
243+
counterparty_balance_msat,
244+
commit_tx_fee_sat,
245+
dust_exposure_msat,
246+
extra_nondust_htlc_on_counterparty_tx_dust_exposure_msat: Some(
247+
extra_htlc_dust_exposure_msat,
248+
),
249+
}
250+
} else {
251+
NextCommitmentStats {
252+
inbound_htlcs_count,
253+
inbound_htlcs_value_msat,
254+
next_commitment_htlcs,
255+
holder_balance_msat,
256+
counterparty_balance_msat,
257+
commit_tx_fee_sat,
258+
dust_exposure_msat,
259+
extra_nondust_htlc_on_counterparty_tx_dust_exposure_msat: None,
260+
}
261+
}
262+
}
37263
fn commit_tx_fee_sat(
38264
&self, feerate_per_kw: u32, nondust_htlc_count: usize, channel_type: &ChannelTypeFeatures,
39265
) -> u64 {
@@ -74,7 +300,7 @@ impl TxBuilder for SpecTxBuilder {
74300
&self, local: bool, commitment_number: u64, per_commitment_point: &PublicKey,
75301
channel_parameters: &ChannelTransactionParameters, secp_ctx: &Secp256k1<secp256k1::All>,
76302
value_to_self_msat: u64, mut htlcs_in_tx: Vec<HTLCOutputInCommitment>, feerate_per_kw: u32,
77-
broadcaster_dust_limit_sat: u64, logger: &L,
303+
broadcaster_dust_limit_satoshis: u64, logger: &L,
78304
) -> (CommitmentTransaction, CommitmentStats)
79305
where
80306
L::Target: Logger,
@@ -95,7 +321,7 @@ impl TxBuilder for SpecTxBuilder {
95321
// As required by the spec, round down
96322
feerate_per_kw as u64 * htlc_tx_weight / 1000
97323
};
98-
amount_msat / 1000 < broadcaster_dust_limit_sat + htlc_tx_fee_sat
324+
amount_msat / 1000 < broadcaster_dust_limit_satoshis + htlc_tx_fee_sat
99325
};
100326

101327
// Trim dust htlcs
@@ -107,7 +333,7 @@ impl TxBuilder for SpecTxBuilder {
107333
remote_htlc_total_msat += htlc.amount_msat;
108334
}
109335
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);
336+
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);
111337
false
112338
} else {
113339
true
@@ -142,13 +368,13 @@ impl TxBuilder for SpecTxBuilder {
142368
let mut to_broadcaster_value_sat = if local { value_to_self } else { value_to_remote };
143369
let mut to_countersignatory_value_sat = if local { value_to_remote } else { value_to_self };
144370

145-
if to_broadcaster_value_sat >= broadcaster_dust_limit_sat {
371+
if to_broadcaster_value_sat >= broadcaster_dust_limit_satoshis {
146372
log_trace!(logger, " ...including {} output with value {}", if local { "to_local" } else { "to_remote" }, to_broadcaster_value_sat);
147373
} else {
148374
to_broadcaster_value_sat = 0;
149375
}
150376

151-
if to_countersignatory_value_sat >= broadcaster_dust_limit_sat {
377+
if to_countersignatory_value_sat >= broadcaster_dust_limit_satoshis {
152378
log_trace!(logger, " ...including {} output with value {}", if local { "to_remote" } else { "to_local" }, to_countersignatory_value_sat);
153379
} else {
154380
to_countersignatory_value_sat = 0;

0 commit comments

Comments
 (0)