Skip to content

Commit 013967b

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 013967b

File tree

2 files changed

+225
-9
lines changed

2 files changed

+225
-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: 224 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,128 @@
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_msat: Option<u64>,
41+
pub counterparty_balance_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: u64,
78+
value_to_remote_after_htlcs: 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.checked_sub(total_anchors_sat * 1000)
95+
} else {
96+
Some(value_to_self_after_htlcs)
97+
};
98+
99+
let remote_balance_before_fee_msat = if !is_outbound_from_holder {
100+
value_to_remote_after_htlcs.checked_sub(total_anchors_sat * 1000)
101+
} else {
102+
Some(value_to_remote_after_htlcs)
103+
};
104+
105+
(local_balance_before_fee_msat, remote_balance_before_fee_msat)
106+
}
107+
108+
fn get_dust_buffer_feerate(feerate_per_kw: u32) -> u32 {
109+
// When calculating our exposure to dust HTLCs, we assume that the channel feerate
110+
// may, at any point, increase by at least 10 sat/vB (i.e 2530 sat/kWU) or 25%,
111+
// whichever is higher. This ensures that we aren't suddenly exposed to significantly
112+
// more dust balance if the feerate increases when we have several HTLCs pending
113+
// which are near the dust limit.
114+
let feerate_plus_quarter = feerate_per_kw.checked_mul(1250).map(|v| v / 1000);
115+
cmp::max(feerate_per_kw.saturating_add(2530), feerate_plus_quarter.unwrap_or(u32::MAX))
116+
}
117+
16118
pub(crate) trait TxBuilder {
119+
fn get_next_commitment_stats(
120+
&self, local: bool, is_outbound_from_holder: bool, channel_value_satoshis: u64,
121+
value_to_holder_msat: u64, next_commitment_htlcs: &[HTLCAmountDirection],
122+
addl_nondust_htlc_count: usize, feerate_per_kw: u32,
123+
dust_exposure_limiting_feerate: Option<u32>, broadcaster_dust_limit_satoshis: u64,
124+
channel_type: &ChannelTypeFeatures,
125+
) -> NextCommitmentStats;
17126
fn commit_tx_fee_sat(
18127
&self, feerate_per_kw: u32, nondust_htlc_count: usize, channel_type: &ChannelTypeFeatures,
19128
) -> u64;
@@ -25,7 +134,7 @@ pub(crate) trait TxBuilder {
25134
&self, local: bool, commitment_number: u64, per_commitment_point: &PublicKey,
26135
channel_parameters: &ChannelTransactionParameters, secp_ctx: &Secp256k1<secp256k1::All>,
27136
value_to_self_msat: u64, htlcs_in_tx: Vec<HTLCOutputInCommitment>, feerate_per_kw: u32,
28-
broadcaster_dust_limit_sat: u64, logger: &L,
137+
broadcaster_dust_limit_satoshis: u64, logger: &L,
29138
) -> (CommitmentTransaction, CommitmentStats)
30139
where
31140
L::Target: Logger;
@@ -34,6 +143,113 @@ pub(crate) trait TxBuilder {
34143
pub(crate) struct SpecTxBuilder {}
35144

36145
impl TxBuilder for SpecTxBuilder {
146+
fn get_next_commitment_stats(
147+
&self, local: bool, is_outbound_from_holder: bool, channel_value_satoshis: u64,
148+
value_to_holder_msat: u64, next_commitment_htlcs: &[HTLCAmountDirection],
149+
addl_nondust_htlc_count: usize, feerate_per_kw: u32,
150+
dust_exposure_limiting_feerate: Option<u32>, broadcaster_dust_limit_satoshis: u64,
151+
channel_type: &ChannelTypeFeatures,
152+
) -> NextCommitmentStats {
153+
let excess_feerate_opt =
154+
feerate_per_kw.checked_sub(dust_exposure_limiting_feerate.unwrap_or(0));
155+
// Dust exposure is only decoupled from feerate for zero fee commitment channels.
156+
let is_zero_fee_comm = channel_type.supports_anchor_zero_fee_commitments();
157+
debug_assert_eq!(is_zero_fee_comm, dust_exposure_limiting_feerate.is_none());
158+
if is_zero_fee_comm {
159+
debug_assert_eq!(feerate_per_kw, 0);
160+
debug_assert_eq!(excess_feerate_opt, Some(0));
161+
debug_assert_eq!(addl_nondust_htlc_count, 0);
162+
}
163+
164+
// Calculate inbound htlc count
165+
let inbound_htlcs_count =
166+
next_commitment_htlcs.iter().filter(|htlc| !htlc.outbound).count();
167+
168+
// Calculate balances after htlcs
169+
let value_to_counterparty_msat = (channel_value_satoshis * 1000)
170+
.checked_sub(value_to_holder_msat)
171+
.expect("value_to_holder_msat outgrew the value of the channel!");
172+
let outbound_htlcs_value_msat: u64 = next_commitment_htlcs
173+
.iter()
174+
.filter_map(|htlc| htlc.outbound.then_some(htlc.amount_msat))
175+
.sum();
176+
let inbound_htlcs_value_msat: u64 = next_commitment_htlcs
177+
.iter()
178+
.filter_map(|htlc| (!htlc.outbound).then_some(htlc.amount_msat))
179+
.sum();
180+
let value_to_holder_after_htlcs = value_to_holder_msat
181+
.checked_sub(outbound_htlcs_value_msat)
182+
.expect("outbound_htlcs_value_msat is greater than value_to_holder_msat");
183+
let value_to_counterparty_after_htlcs = value_to_counterparty_msat
184+
.checked_sub(inbound_htlcs_value_msat)
185+
.expect("inbound_htlcs_value_msat is greater than value_to_counterparty_msat");
186+
187+
// Subtract the anchors from the channel funder
188+
let (holder_balance_msat, counterparty_balance_msat) = subtract_addl_outputs(
189+
is_outbound_from_holder,
190+
value_to_holder_after_htlcs,
191+
value_to_counterparty_after_htlcs,
192+
channel_type,
193+
);
194+
195+
// Increment the feerate by a buffer to calculate dust exposure
196+
let dust_buffer_feerate = get_dust_buffer_feerate(feerate_per_kw);
197+
198+
// Calculate fees on commitment transaction
199+
let nondust_htlc_count = next_commitment_htlcs
200+
.iter()
201+
.filter(|htlc| {
202+
!htlc.is_dust(local, feerate_per_kw, broadcaster_dust_limit_satoshis, channel_type)
203+
})
204+
.count();
205+
let commit_tx_fee_sat = commit_tx_fee_sat(
206+
feerate_per_kw,
207+
nondust_htlc_count + addl_nondust_htlc_count,
208+
channel_type,
209+
);
210+
211+
// Calculate dust exposure on commitment transaction
212+
let dust_exposure_msat = next_commitment_htlcs
213+
.iter()
214+
.filter_map(|htlc| {
215+
htlc.is_dust(
216+
local,
217+
dust_buffer_feerate,
218+
broadcaster_dust_limit_satoshis,
219+
channel_type,
220+
)
221+
.then_some(htlc.amount_msat)
222+
})
223+
.sum();
224+
225+
// Count the excess fees on the counterparty's transaction as dust
226+
let (dust_exposure_msat, extra_nondust_htlc_on_counterparty_tx_dust_exposure_msat) =
227+
if let (Some(excess_feerate), false) = (excess_feerate_opt, local) {
228+
let (dust_exposure_msat, extra_nondust_htlc_exposure_msat) =
229+
excess_fees_on_counterparty_tx_dust_exposure_msat(
230+
&next_commitment_htlcs,
231+
dust_buffer_feerate,
232+
excess_feerate,
233+
broadcaster_dust_limit_satoshis,
234+
dust_exposure_msat,
235+
channel_type,
236+
);
237+
(dust_exposure_msat, Some(extra_nondust_htlc_exposure_msat))
238+
} else {
239+
(dust_exposure_msat, None)
240+
};
241+
242+
NextCommitmentStats {
243+
inbound_htlcs_count,
244+
inbound_htlcs_value_msat,
245+
holder_balance_msat,
246+
counterparty_balance_msat,
247+
nondust_htlc_count,
248+
commit_tx_fee_sat,
249+
dust_exposure_msat,
250+
extra_nondust_htlc_on_counterparty_tx_dust_exposure_msat,
251+
}
252+
}
37253
fn commit_tx_fee_sat(
38254
&self, feerate_per_kw: u32, nondust_htlc_count: usize, channel_type: &ChannelTypeFeatures,
39255
) -> u64 {
@@ -74,7 +290,7 @@ impl TxBuilder for SpecTxBuilder {
74290
&self, local: bool, commitment_number: u64, per_commitment_point: &PublicKey,
75291
channel_parameters: &ChannelTransactionParameters, secp_ctx: &Secp256k1<secp256k1::All>,
76292
value_to_self_msat: u64, mut htlcs_in_tx: Vec<HTLCOutputInCommitment>, feerate_per_kw: u32,
77-
broadcaster_dust_limit_sat: u64, logger: &L,
293+
broadcaster_dust_limit_satoshis: u64, logger: &L,
78294
) -> (CommitmentTransaction, CommitmentStats)
79295
where
80296
L::Target: Logger,
@@ -95,7 +311,7 @@ impl TxBuilder for SpecTxBuilder {
95311
// As required by the spec, round down
96312
feerate_per_kw as u64 * htlc_tx_weight / 1000
97313
};
98-
amount_msat / 1000 < broadcaster_dust_limit_sat + htlc_tx_fee_sat
314+
amount_msat / 1000 < broadcaster_dust_limit_satoshis + htlc_tx_fee_sat
99315
};
100316

101317
// Trim dust htlcs
@@ -107,7 +323,7 @@ impl TxBuilder for SpecTxBuilder {
107323
remote_htlc_total_msat += htlc.amount_msat;
108324
}
109325
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);
326+
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);
111327
false
112328
} else {
113329
true
@@ -142,13 +358,13 @@ impl TxBuilder for SpecTxBuilder {
142358
let mut to_broadcaster_value_sat = if local { value_to_self } else { value_to_remote };
143359
let mut to_countersignatory_value_sat = if local { value_to_remote } else { value_to_self };
144360

145-
if to_broadcaster_value_sat >= broadcaster_dust_limit_sat {
361+
if to_broadcaster_value_sat >= broadcaster_dust_limit_satoshis {
146362
log_trace!(logger, " ...including {} output with value {}", if local { "to_local" } else { "to_remote" }, to_broadcaster_value_sat);
147363
} else {
148364
to_broadcaster_value_sat = 0;
149365
}
150366

151-
if to_countersignatory_value_sat >= broadcaster_dust_limit_sat {
367+
if to_countersignatory_value_sat >= broadcaster_dust_limit_satoshis {
152368
log_trace!(logger, " ...including {} output with value {}", if local { "to_remote" } else { "to_local" }, to_countersignatory_value_sat);
153369
} else {
154370
to_countersignatory_value_sat = 0;

0 commit comments

Comments
 (0)