Skip to content

Commit 50222af

Browse files
committed
Add TxBuilder::get_builder_stats
Given a snapshot of the lightning state machine, `TxBuilder::get_builder_stats` calculates the transaction fees, the dust exposure, and the holder and counterparty balances before subtracting the transaction fee from the funder of the channel.
1 parent 39e8d7d commit 50222af

File tree

1 file changed

+260
-7
lines changed

1 file changed

+260
-7
lines changed

lightning/src/sign/tx_builder.rs

Lines changed: 260 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,161 @@
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(any(test, fuzzing))]
19+
#[derive(Clone, PartialEq, PartialOrd, Eq, Ord)]
20+
pub(crate) struct HTLCAmountDirection {
21+
pub outbound: bool,
22+
pub amount_msat: u64,
23+
}
24+
25+
#[cfg(not(any(test, fuzzing)))]
26+
pub(crate) struct HTLCAmountDirection {
27+
pub outbound: bool,
28+
pub amount_msat: u64,
29+
}
30+
31+
impl HTLCAmountDirection {
32+
fn is_dust(
33+
&self, local: bool, feerate_per_kw: u32, broadcaster_dust_limit_satoshis: u64,
34+
channel_type: &ChannelTypeFeatures,
35+
) -> bool {
36+
let htlc_tx_fee_sat = if channel_type.supports_anchors_zero_fee_htlc_tx() {
37+
0
38+
} else {
39+
let htlc_tx_weight = if self.outbound == local {
40+
htlc_timeout_tx_weight(channel_type)
41+
} else {
42+
htlc_success_tx_weight(channel_type)
43+
};
44+
// As required by the spec, round down
45+
feerate_per_kw as u64 * htlc_tx_weight / 1000
46+
};
47+
self.amount_msat / 1000 < broadcaster_dust_limit_satoshis + htlc_tx_fee_sat
48+
}
49+
}
50+
51+
pub(crate) struct NextCommitmentStats {
52+
pub next_commitment_htlcs: Vec<HTLCAmountDirection>,
53+
pub holder_balance_msat: Option<u64>,
54+
pub counterparty_balance_msat: Option<u64>,
55+
pub commit_tx_fee_sat: u64,
56+
pub dust_exposure_msat: u64,
57+
// If the counterparty sets a feerate on the channel in excess of our dust_exposure_limiting_feerate,
58+
// this should be set to the dust exposure that would result from us adding an additional nondust outbound
59+
// htlc on the counterparty's commitment transaction.
60+
pub extra_nondust_htlc_on_counterparty_tx_dust_exposure_msat: Option<u64>,
61+
}
62+
63+
fn on_holder_tx_dust_exposure_msat(
64+
next_commitment_htlcs: &[HTLCAmountDirection], dust_buffer_feerate: u32,
65+
broadcaster_dust_limit_satoshis: u64, channel_type: &ChannelTypeFeatures,
66+
) -> u64 {
67+
next_commitment_htlcs
68+
.iter()
69+
.filter_map(|htlc| {
70+
htlc.is_dust(true, dust_buffer_feerate, broadcaster_dust_limit_satoshis, channel_type)
71+
.then_some(htlc.amount_msat)
72+
})
73+
.sum()
74+
}
75+
76+
fn on_counterparty_tx_dust_exposure_msat(
77+
next_commitment_htlcs: &[HTLCAmountDirection], dust_buffer_feerate: u32,
78+
excess_feerate_opt: Option<u32>, broadcaster_dust_limit_satoshis: u64,
79+
channel_type: &ChannelTypeFeatures,
80+
) -> (u64, Option<u64>) {
81+
let mut on_counterparty_tx_dust_exposure_msat: u64 = next_commitment_htlcs
82+
.iter()
83+
.filter_map(|htlc| {
84+
htlc.is_dust(false, dust_buffer_feerate, broadcaster_dust_limit_satoshis, channel_type)
85+
.then_some(htlc.amount_msat)
86+
})
87+
.sum();
88+
89+
#[rustfmt::skip]
90+
let extra_nondust_htlc_on_counterparty_tx_dust_exposure_msat = excess_feerate_opt.map(|excess_feerate| {
91+
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();
92+
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();
93+
94+
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);
95+
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);
96+
97+
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);
98+
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);
99+
100+
let extra_htlc_dust_exposure = on_counterparty_tx_dust_exposure_msat + (extra_htlc_commit_tx_fee_sat + extra_htlc_htlc_tx_fees_sat) * 1000;
101+
on_counterparty_tx_dust_exposure_msat += (commit_tx_fee_sat + htlc_tx_fees_sat) * 1000;
102+
extra_htlc_dust_exposure
103+
});
104+
105+
(
106+
on_counterparty_tx_dust_exposure_msat,
107+
extra_nondust_htlc_on_counterparty_tx_dust_exposure_msat,
108+
)
109+
}
110+
111+
fn subtract_addl_outputs(
112+
is_outbound_from_holder: bool, value_to_self_after_htlcs: Option<u64>,
113+
value_to_remote_after_htlcs: Option<u64>, channel_type: &ChannelTypeFeatures,
114+
) -> (Option<u64>, Option<u64>) {
115+
let total_anchors_sat = if channel_type.supports_anchors_zero_fee_htlc_tx() {
116+
ANCHOR_OUTPUT_VALUE_SATOSHI * 2
117+
} else {
118+
0
119+
};
120+
121+
let mut local_balance_before_fee_msat = value_to_self_after_htlcs;
122+
let mut remote_balance_before_fee_msat = value_to_remote_after_htlcs;
123+
124+
// We MUST use checked subs here, as the funder's balance is not guaranteed to be greater
125+
// than or equal to `total_anchors_sat`.
126+
//
127+
// This is because when the remote party sends an `update_fee` message, we build the new
128+
// commitment transaction *before* checking whether the remote party's balance is enough to
129+
// cover the total anchor sum.
130+
131+
if is_outbound_from_holder {
132+
local_balance_before_fee_msat = local_balance_before_fee_msat
133+
.and_then(|balance_msat| balance_msat.checked_sub(total_anchors_sat * 1000));
134+
} else {
135+
remote_balance_before_fee_msat = remote_balance_before_fee_msat
136+
.and_then(|balance_msat| balance_msat.checked_sub(total_anchors_sat * 1000));
137+
}
138+
139+
(local_balance_before_fee_msat, remote_balance_before_fee_msat)
140+
}
141+
142+
fn get_dust_buffer_feerate(feerate_per_kw: u32) -> u32 {
143+
// When calculating our exposure to dust HTLCs, we assume that the channel feerate
144+
// may, at any point, increase by at least 10 sat/vB (i.e 2530 sat/kWU) or 25%,
145+
// whichever is higher. This ensures that we aren't suddenly exposed to significantly
146+
// more dust balance if the feerate increases when we have several HTLCs pending
147+
// which are near the dust limit.
148+
let feerate_plus_quarter = feerate_per_kw.checked_mul(1250).map(|v| v / 1000);
149+
cmp::max(feerate_per_kw.saturating_add(2530), feerate_plus_quarter.unwrap_or(u32::MAX))
150+
}
151+
16152
pub(crate) trait TxBuilder {
153+
fn get_next_commitment_stats(
154+
&self, local: bool, is_outbound_from_holder: bool, channel_value_satoshis: u64,
155+
value_to_holder_msat: u64, next_commitment_htlcs: Vec<HTLCAmountDirection>,
156+
nondust_htlcs: usize, feerate_per_kw: u32, dust_exposure_limiting_feerate: Option<u32>,
157+
broadcaster_dust_limit_satoshis: u64, channel_type: &ChannelTypeFeatures,
158+
) -> NextCommitmentStats;
17159
fn commit_tx_fee_sat(
18160
&self, feerate_per_kw: u32, nondust_htlc_count: usize, channel_type: &ChannelTypeFeatures,
19161
) -> u64;
@@ -25,7 +167,7 @@ pub(crate) trait TxBuilder {
25167
&self, local: bool, commitment_number: u64, per_commitment_point: &PublicKey,
26168
channel_parameters: &ChannelTransactionParameters, secp_ctx: &Secp256k1<secp256k1::All>,
27169
value_to_self_msat: u64, htlcs_in_tx: Vec<HTLCOutputInCommitment>, feerate_per_kw: u32,
28-
broadcaster_dust_limit_sat: u64, logger: &L,
170+
broadcaster_dust_limit_satoshis: u64, logger: &L,
29171
) -> (CommitmentTransaction, CommitmentStats)
30172
where
31173
L::Target: Logger;
@@ -34,6 +176,117 @@ pub(crate) trait TxBuilder {
34176
pub(crate) struct SpecTxBuilder {}
35177

36178
impl TxBuilder for SpecTxBuilder {
179+
fn get_next_commitment_stats(
180+
&self, local: bool, is_outbound_from_holder: bool, channel_value_satoshis: u64,
181+
value_to_holder_msat: u64, next_commitment_htlcs: Vec<HTLCAmountDirection>,
182+
nondust_htlcs: usize, feerate_per_kw: u32, dust_exposure_limiting_feerate: Option<u32>,
183+
broadcaster_dust_limit_satoshis: u64, channel_type: &ChannelTypeFeatures,
184+
) -> NextCommitmentStats {
185+
let excess_feerate_opt =
186+
feerate_per_kw.checked_sub(dust_exposure_limiting_feerate.unwrap_or(0));
187+
// Dust exposure is only decoupled from feerate for zero fee commitment channels.
188+
let is_zero_fee_comm = channel_type.supports_anchor_zero_fee_commitments();
189+
debug_assert_eq!(is_zero_fee_comm, dust_exposure_limiting_feerate.is_none());
190+
if is_zero_fee_comm {
191+
debug_assert_eq!(feerate_per_kw, 0);
192+
debug_assert_eq!(excess_feerate_opt, Some(0));
193+
debug_assert_eq!(nondust_htlcs, 0);
194+
}
195+
196+
// Calculate balances after htlcs
197+
let value_to_counterparty_msat = channel_value_satoshis * 1000 - value_to_holder_msat;
198+
let outbound_htlcs_value_msat: u64 = next_commitment_htlcs
199+
.iter()
200+
.filter_map(|htlc| htlc.outbound.then_some(htlc.amount_msat))
201+
.sum();
202+
let inbound_htlcs_value_msat: u64 = next_commitment_htlcs
203+
.iter()
204+
.filter_map(|htlc| (!htlc.outbound).then_some(htlc.amount_msat))
205+
.sum();
206+
let value_to_holder_after_htlcs =
207+
value_to_holder_msat.checked_sub(outbound_htlcs_value_msat);
208+
let value_to_counterparty_after_htlcs =
209+
value_to_counterparty_msat.checked_sub(inbound_htlcs_value_msat);
210+
211+
// Subtract the anchors from the channel funder
212+
let (holder_balance_msat, counterparty_balance_msat) = subtract_addl_outputs(
213+
is_outbound_from_holder,
214+
value_to_holder_after_htlcs,
215+
value_to_counterparty_after_htlcs,
216+
channel_type,
217+
);
218+
219+
// Increment the feerate by a buffer to calculate dust exposure
220+
let dust_buffer_feerate = get_dust_buffer_feerate(feerate_per_kw);
221+
222+
if local {
223+
// Calculate fees and dust exposure on holder's commitment transaction
224+
let on_holder_htlc_count = next_commitment_htlcs
225+
.iter()
226+
.filter(|htlc| {
227+
!htlc.is_dust(
228+
true,
229+
feerate_per_kw,
230+
broadcaster_dust_limit_satoshis,
231+
channel_type,
232+
)
233+
})
234+
.count();
235+
let commit_tx_fee_sat = commit_tx_fee_sat(
236+
feerate_per_kw,
237+
on_holder_htlc_count + nondust_htlcs,
238+
channel_type,
239+
);
240+
let on_holder_tx_dust_exposure_msat = on_holder_tx_dust_exposure_msat(
241+
&next_commitment_htlcs,
242+
dust_buffer_feerate,
243+
broadcaster_dust_limit_satoshis,
244+
channel_type,
245+
);
246+
NextCommitmentStats {
247+
next_commitment_htlcs,
248+
holder_balance_msat,
249+
counterparty_balance_msat,
250+
commit_tx_fee_sat,
251+
dust_exposure_msat: on_holder_tx_dust_exposure_msat,
252+
extra_nondust_htlc_on_counterparty_tx_dust_exposure_msat: None,
253+
}
254+
} else {
255+
// Calculate fees and dust exposure on counterparty's commitment transaction
256+
let on_counterparty_htlc_count = next_commitment_htlcs
257+
.iter()
258+
.filter(|htlc| {
259+
!htlc.is_dust(
260+
false,
261+
feerate_per_kw,
262+
broadcaster_dust_limit_satoshis,
263+
channel_type,
264+
)
265+
})
266+
.count();
267+
let commit_tx_fee_sat = commit_tx_fee_sat(
268+
feerate_per_kw,
269+
on_counterparty_htlc_count + nondust_htlcs,
270+
channel_type,
271+
);
272+
let (dust_exposure_msat, extra_nondust_htlc_on_counterparty_tx_dust_exposure_msat) =
273+
on_counterparty_tx_dust_exposure_msat(
274+
&next_commitment_htlcs,
275+
dust_buffer_feerate,
276+
excess_feerate_opt,
277+
broadcaster_dust_limit_satoshis,
278+
channel_type,
279+
);
280+
NextCommitmentStats {
281+
next_commitment_htlcs,
282+
holder_balance_msat,
283+
counterparty_balance_msat,
284+
commit_tx_fee_sat,
285+
dust_exposure_msat,
286+
extra_nondust_htlc_on_counterparty_tx_dust_exposure_msat,
287+
}
288+
}
289+
}
37290
fn commit_tx_fee_sat(
38291
&self, feerate_per_kw: u32, nondust_htlc_count: usize, channel_type: &ChannelTypeFeatures,
39292
) -> u64 {
@@ -74,7 +327,7 @@ impl TxBuilder for SpecTxBuilder {
74327
&self, local: bool, commitment_number: u64, per_commitment_point: &PublicKey,
75328
channel_parameters: &ChannelTransactionParameters, secp_ctx: &Secp256k1<secp256k1::All>,
76329
value_to_self_msat: u64, mut htlcs_in_tx: Vec<HTLCOutputInCommitment>, feerate_per_kw: u32,
77-
broadcaster_dust_limit_sat: u64, logger: &L,
330+
broadcaster_dust_limit_satoshis: u64, logger: &L,
78331
) -> (CommitmentTransaction, CommitmentStats)
79332
where
80333
L::Target: Logger,
@@ -95,7 +348,7 @@ impl TxBuilder for SpecTxBuilder {
95348
// As required by the spec, round down
96349
feerate_per_kw as u64 * htlc_tx_weight / 1000
97350
};
98-
amount_msat / 1000 < broadcaster_dust_limit_sat + htlc_tx_fee_sat
351+
amount_msat / 1000 < broadcaster_dust_limit_satoshis + htlc_tx_fee_sat
99352
};
100353

101354
// Trim dust htlcs
@@ -107,7 +360,7 @@ impl TxBuilder for SpecTxBuilder {
107360
remote_htlc_total_msat += htlc.amount_msat;
108361
}
109362
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);
363+
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);
111364
false
112365
} else {
113366
true
@@ -142,13 +395,13 @@ impl TxBuilder for SpecTxBuilder {
142395
let mut to_broadcaster_value_sat = if local { value_to_self } else { value_to_remote };
143396
let mut to_countersignatory_value_sat = if local { value_to_remote } else { value_to_self };
144397

145-
if to_broadcaster_value_sat >= broadcaster_dust_limit_sat {
398+
if to_broadcaster_value_sat >= broadcaster_dust_limit_satoshis {
146399
log_trace!(logger, " ...including {} output with value {}", if local { "to_local" } else { "to_remote" }, to_broadcaster_value_sat);
147400
} else {
148401
to_broadcaster_value_sat = 0;
149402
}
150403

151-
if to_countersignatory_value_sat >= broadcaster_dust_limit_sat {
404+
if to_countersignatory_value_sat >= broadcaster_dust_limit_satoshis {
152405
log_trace!(logger, " ...including {} output with value {}", if local { "to_remote" } else { "to_local" }, to_countersignatory_value_sat);
153406
} else {
154407
to_countersignatory_value_sat = 0;

0 commit comments

Comments
 (0)