Skip to content

Commit ec0bb6a

Browse files
committed
Unwrap get_available_balances with no counterparty unknown HTLCs
We can unwrap `get_available_balances` when not counting counterparty unknown HTLCs because all balance changes in channel are validated with the same `get_next_remote_commitment_stats` call before getting applied to the channel's state. Now calls to `get_available_balances` external to `ChannelContext` will only be updated to include HTLCs in the holding cell, and `LocalAnnounced` HTLCs once we receive the ack from the counterparty for these updates. `FundedChannel::send_htlc` includes these HTLCs when calculating available balances, so it will always have the latest view of the channel's state and can fail graciously in case some balance has been overdrawn.
1 parent a0f0935 commit ec0bb6a

File tree

2 files changed

+92
-28
lines changed

2 files changed

+92
-28
lines changed

lightning/src/ln/channel.rs

Lines changed: 79 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2446,24 +2446,56 @@ where
24462446
/// Doesn't bother handling the
24472447
/// if-we-removed-it-already-but-haven't-fully-resolved-they-can-still-send-an-inbound-HTLC
24482448
/// corner case properly.
2449-
pub fn get_available_balances<F: FeeEstimator>(
2449+
fn get_available_balances_internal<F: FeeEstimator>(
24502450
&self, fee_estimator: &LowerBoundedFeeEstimator<F>,
2451-
) -> AvailableBalances {
2451+
include_counterparty_unknown_htlcs: bool,
2452+
) -> Result<AvailableBalances, ()> {
24522453
match &self.phase {
24532454
ChannelPhase::Undefined => unreachable!(),
2454-
ChannelPhase::Funded(chan) => chan.get_available_balances(fee_estimator),
2455+
ChannelPhase::Funded(chan) => chan
2456+
.get_available_balances_internal(fee_estimator, include_counterparty_unknown_htlcs),
24552457
ChannelPhase::UnfundedOutboundV1(chan) => {
2456-
chan.context.get_available_balances_for_scope(&chan.funding, fee_estimator)
2457-
},
2458-
ChannelPhase::UnfundedInboundV1(chan) => {
2459-
chan.context.get_available_balances_for_scope(&chan.funding, fee_estimator)
2460-
},
2461-
ChannelPhase::UnfundedV2(chan) => {
2462-
chan.context.get_available_balances_for_scope(&chan.funding, fee_estimator)
2458+
chan.context.get_available_balances_for_scope(
2459+
&chan.funding,
2460+
fee_estimator,
2461+
include_counterparty_unknown_htlcs,
2462+
)
24632463
},
2464+
ChannelPhase::UnfundedInboundV1(chan) => chan.context.get_available_balances_for_scope(
2465+
&chan.funding,
2466+
fee_estimator,
2467+
include_counterparty_unknown_htlcs,
2468+
),
2469+
ChannelPhase::UnfundedV2(chan) => chan.context.get_available_balances_for_scope(
2470+
&chan.funding,
2471+
fee_estimator,
2472+
include_counterparty_unknown_htlcs,
2473+
),
24642474
}
24652475
}
24662476

2477+
#[cfg(any(test, feature = "_externalize_tests"))]
2478+
pub fn get_available_balances_with_counterparty_unknown_htlcs<F: FeeEstimator>(
2479+
&self, fee_estimator: &LowerBoundedFeeEstimator<F>,
2480+
) -> Result<AvailableBalances, ()> {
2481+
self.get_available_balances_internal(fee_estimator, true)
2482+
}
2483+
2484+
pub fn get_available_balances<F: FeeEstimator>(
2485+
&self, fee_estimator: &LowerBoundedFeeEstimator<F>,
2486+
) -> AvailableBalances {
2487+
let balances_result = self.get_available_balances_internal(fee_estimator, false);
2488+
balances_result.unwrap_or_else(|()| {
2489+
debug_assert!(false, "some channel balance has been overdrawn");
2490+
AvailableBalances {
2491+
inbound_capacity_msat: 0,
2492+
outbound_capacity_msat: 0,
2493+
next_outbound_htlc_limit_msat: 0,
2494+
next_outbound_htlc_minimum_msat: u64::MAX,
2495+
}
2496+
})
2497+
}
2498+
24672499
pub fn minimum_depth(&self) -> Option<u32> {
24682500
self.context().minimum_depth(self.funding())
24692501
}
@@ -5783,8 +5815,8 @@ impl<SP: SignerProvider> ChannelContext<SP> {
57835815
#[rustfmt::skip]
57845816
fn get_available_balances_for_scope<F: FeeEstimator>(
57855817
&self, funding: &FundingScope, fee_estimator: &LowerBoundedFeeEstimator<F>,
5786-
) -> AvailableBalances {
5787-
let include_counterparty_unknown_htlcs = true;
5818+
include_counterparty_unknown_htlcs: bool,
5819+
) -> Result<AvailableBalances, ()> {
57885820
let dust_exposure_limiting_feerate = self.get_dust_exposure_limiting_feerate(
57895821
&fee_estimator, funding.get_channel_type(),
57905822
);
@@ -5796,7 +5828,7 @@ impl<SP: SignerProvider> ChannelContext<SP> {
57965828
0,
57975829
self.feerate_per_kw,
57985830
dust_exposure_limiting_feerate
5799-
).map(|(remote_stats, _)| remote_stats.available_balances).unwrap();
5831+
).map(|(remote_stats, _)| remote_stats.available_balances)?;
58005832

58015833
#[cfg(debug_assertions)]
58025834
if balances.next_outbound_htlc_limit_msat >= balances.next_outbound_htlc_minimum_msat
@@ -5820,7 +5852,7 @@ impl<SP: SignerProvider> ChannelContext<SP> {
58205852
>= funding.counterparty_selected_channel_reserve_satoshis.unwrap_or(0) * 1000);
58215853
}
58225854

5823-
balances
5855+
Ok(balances)
58245856
}
58255857

58265858
#[rustfmt::skip]
@@ -12487,7 +12519,13 @@ where
1248712519
return Err((LocalHTLCFailureReason::ZeroAmount, "Cannot send 0-msat HTLC".to_owned()));
1248812520
}
1248912521

12490-
let available_balances = self.get_available_balances(fee_estimator);
12522+
let available_balances =
12523+
self.get_available_balances_internal(fee_estimator, true).map_err(|()| {
12524+
(
12525+
LocalHTLCFailureReason::ChannelBalanceOverdrawn,
12526+
"Channel balance overdrawn".to_owned(),
12527+
)
12528+
})?;
1249112529
if amount_msat < available_balances.next_outbound_htlc_minimum_msat {
1249212530
return Err((
1249312531
LocalHTLCFailureReason::HTLCMinimum,
@@ -12582,21 +12620,37 @@ where
1258212620
}
1258312621

1258412622
#[rustfmt::skip]
12585-
pub(super) fn get_available_balances<F: FeeEstimator>(
12586-
&self, fee_estimator: &LowerBoundedFeeEstimator<F>,
12587-
) -> AvailableBalances {
12588-
core::iter::once(&self.funding)
12589-
.chain(self.pending_funding().iter())
12590-
.map(|funding| self.context.get_available_balances_for_scope(funding, fee_estimator))
12591-
.reduce(|acc, e| {
12592-
AvailableBalances {
12623+
pub(super) fn get_available_balances_internal<F: FeeEstimator>(
12624+
&self, fee_estimator: &LowerBoundedFeeEstimator<F>, include_counterparty_unknown_htlcs: bool,
12625+
) -> Result<AvailableBalances, ()> {
12626+
let init = self.context.get_available_balances_for_scope(&self.funding, fee_estimator, include_counterparty_unknown_htlcs)?;
12627+
self.pending_funding().iter().try_fold(
12628+
init,
12629+
|acc, funding| {
12630+
let e = self.context.get_available_balances_for_scope(funding, fee_estimator, include_counterparty_unknown_htlcs)?;
12631+
Ok(AvailableBalances {
1259312632
inbound_capacity_msat: acc.inbound_capacity_msat.min(e.inbound_capacity_msat),
1259412633
outbound_capacity_msat: acc.outbound_capacity_msat.min(e.outbound_capacity_msat),
1259512634
next_outbound_htlc_limit_msat: acc.next_outbound_htlc_limit_msat.min(e.next_outbound_htlc_limit_msat),
1259612635
next_outbound_htlc_minimum_msat: acc.next_outbound_htlc_minimum_msat.max(e.next_outbound_htlc_minimum_msat),
12597-
}
12636+
})
1259812637
})
12599-
.expect("At least one FundingScope is always provided")
12638+
}
12639+
12640+
#[rustfmt::skip]
12641+
pub(super) fn get_available_balances<F: FeeEstimator>(
12642+
&self, fee_estimator: &LowerBoundedFeeEstimator<F>,
12643+
) -> AvailableBalances {
12644+
let balances_result = self.get_available_balances_internal(fee_estimator, false);
12645+
balances_result.unwrap_or_else(|()| {
12646+
debug_assert!(false, "some channel balance has been overdrawn");
12647+
AvailableBalances {
12648+
inbound_capacity_msat: 0,
12649+
outbound_capacity_msat: 0,
12650+
next_outbound_htlc_limit_msat: 0,
12651+
next_outbound_htlc_minimum_msat: u64::MAX,
12652+
}
12653+
})
1260012654
}
1260112655

1260212656
fn build_commitment_no_status_check<L: Logger>(&mut self, logger: &L) -> ChannelMonitorUpdate {

lightning/src/ln/functional_tests.rs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10106,9 +10106,19 @@ pub fn test_dust_exposure_holding_cell_assertion() {
1010610106
expect_and_process_pending_htlcs(&nodes[1], false);
1010710107
assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty());
1010810108

10109-
let bs_chans = nodes[1].node.list_channels();
10110-
let bc_chan = bs_chans.iter().find(|chan| chan.counterparty.node_id == node_c_id).unwrap();
10111-
assert!(bc_chan.next_outbound_htlc_minimum_msat > DUST_HTLC_VALUE_MSAT);
10109+
{
10110+
let per_peer_state_lock;
10111+
let mut peer_state_lock;
10112+
let chan =
10113+
get_channel_ref!(nodes[1], nodes[2], per_peer_state_lock, peer_state_lock, bc_chan_id);
10114+
// This how `chan.send_htlc` determines whether it can send another HTLC
10115+
let balances = chan
10116+
.get_available_balances_with_counterparty_unknown_htlcs(&LowerBoundedFeeEstimator::new(
10117+
nodes[1].fee_estimator,
10118+
))
10119+
.unwrap();
10120+
assert!(balances.next_outbound_htlc_minimum_msat > DUST_HTLC_VALUE_MSAT);
10121+
}
1011210122

1011310123
// Send an additional HTLC from C to B. This will make B unable to forward the HTLC already in
1011410124
// its holding cell as it would be over-exposed to dust.

0 commit comments

Comments
 (0)