Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions fuzz/src/router.rs
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,7 @@ pub fn do_test<Out: test_logger::Output>(data: &[u8], out: Out) {
outbound_capacity_msat: capacity.saturating_mul(1000),
next_outbound_htlc_limit_msat: capacity.saturating_mul(1000),
next_outbound_htlc_minimum_msat: 0,
next_splice_out_maximum_sat: capacity,
inbound_htlc_minimum_msat: None,
inbound_htlc_maximum_msat: None,
config: None,
Expand Down
254 changes: 172 additions & 82 deletions lightning/src/ln/channel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,8 @@ pub struct AvailableBalances {
pub next_outbound_htlc_limit_msat: u64,
/// The minimum value we can assign to the next outbound HTLC
pub next_outbound_htlc_minimum_msat: u64,
/// The maximum value of the next splice-out
pub next_splice_out_maximum_sat: u64,
}

#[derive(Debug, Clone, Copy, PartialEq)]
Expand Down Expand Up @@ -5259,6 +5261,107 @@ impl<SP: SignerProvider> ChannelContext<SP> {
}
}

/// The balances returned here should only be used to check that both parties still hold
/// their respective reserves *after* a splice. This function also checks that both local
/// and remote commitments still have at least one output after the splice, which is
/// particularly relevant for zero-reserve channels.
///
/// Do NOT use this to determine how much the holder can splice out of the channel. The balance
/// of the holder after a splice is not necessarily equal to the funds they can splice out
/// of the channel due to the reserve, and the zero-reserve-at-least-one-output
/// requirements. Note you cannot simply subtract out the reserve, as splicing funds out
/// of the channel changes the reserve the holder must keep in the channel.
///
/// See [`FundedChannel::get_next_splice_out_maximum`] for the maximum value of the next
/// splice out of the holder's balance.
fn get_post_splice_holder_counterparty_balances(
&self, funding: &FundingScope, our_contribution_candidate: SignedAmount,
their_contribution_candidate: SignedAmount, addl_nondust_htlc_count: usize,
feerate_per_kw: u32,
) -> Result<(Amount, Amount), String> {
let htlc_candidate = None;
// There shouldn't be any pending HTLC adds at this point. We nonetheless choose to
// take the remote's view of the pending HTLCs to make this function consistent
// with `FundedChannel::get_next_splice_out_maximum`. In debug mode, we check that
// these two functions are consistent *before* clearing any HTLC updates from the
// channel, see `FundedChannel::funding_contributed`.
let include_counterparty_unknown_htlcs = true;
let post_splice_channel_value_satoshis = funding.compute_post_splice_value(
our_contribution_candidate.to_sat(),
their_contribution_candidate.to_sat(),
);
let value_to_self_msat = self.get_next_commitment_value_to_self_msat(
false, // Take the remote's view of the pending HTLCs
funding,
);
let our_contribution_candidate_msat = our_contribution_candidate.to_sat() * 1000;
let post_splice_value_to_self_msat = value_to_self_msat
.checked_add_signed(our_contribution_candidate_msat)
.ok_or(format!(
"Our contribution candidate {our_contribution_candidate_msat}msat is \
greater than our total balance in the channel {value_to_self_msat}msat"
))?;

let commitment_htlcs = self.get_next_commitment_htlcs(
false, // Take the remote's view of the pending HTLCs
htlc_candidate,
include_counterparty_unknown_htlcs,
);

// We are not interested in dust exposure at this point, so use the 250sat/kw default
let max_dust_htlc_exposure_msat = self.get_max_dust_htlc_exposure_msat(None);
// Again, not interested in dust exposure, so no need to pass this feerate
let dust_exposure_limiting_feerate = None;

let channel_constraints = self.get_channel_constraints(funding);

let get_channel_stats = |local: bool| {
SpecTxBuilder {}.get_channel_stats(
local,
funding.is_outbound(),
post_splice_channel_value_satoshis,
post_splice_value_to_self_msat,
&commitment_htlcs,
addl_nondust_htlc_count,
feerate_per_kw,
dust_exposure_limiting_feerate,
max_dust_htlc_exposure_msat,
channel_constraints,
funding.get_channel_type(),
)
};

// Different dust limits on the local and remote commitments cause the commitment
// transaction fee to be different depending on the commitment, so we grab the floor
// of both balances across both commitments here.
//
// `get_channel_stats` also checks for at least one output on the commitment given
// these parameters. This is particularly relevant for zero-reserve channels.
//
// This "at-least-one-output" check is why we still run both checks on
// zero-fee-commitment channels, even though those channels don't suffer from the
// commitment transaction fee asymmetry.
let local_stats =
get_channel_stats(true).map_err(|()| "Balance exhausted on local commitment")?;
let remote_stats =
get_channel_stats(false).map_err(|()| "Balance exhausted on remote commitment")?;

let holder_balance_floor = Amount::from_sat(
cmp::min(
local_stats.commitment_stats.holder_balance_msat,
remote_stats.commitment_stats.holder_balance_msat,
) / 1000,
);
let counterparty_balance_floor = Amount::from_sat(
cmp::min(
local_stats.commitment_stats.counterparty_balance_msat,
remote_stats.commitment_stats.counterparty_balance_msat,
) / 1000,
);

Ok((holder_balance_floor, counterparty_balance_floor))
}

fn get_next_local_commitment_stats(
&self, funding: &FundingScope, htlc_candidate: Option<HTLCAmountDirection>,
include_counterparty_unknown_htlcs: bool, addl_nondust_htlc_count: usize,
Expand Down Expand Up @@ -6758,7 +6861,7 @@ pub(crate) fn get_legacy_default_holder_selected_channel_reserve_satoshis(
///
/// This is used both for outbound and inbound channels and has lower bound
/// of `dust_limit_satoshis`.
fn get_v2_channel_reserve_satoshis(
pub(crate) fn get_v2_channel_reserve_satoshis(
channel_value_satoshis: u64, dust_limit_satoshis: u64, is_0reserve: bool,
) -> u64 {
if is_0reserve {
Expand Down Expand Up @@ -12354,10 +12457,7 @@ where
"build_prior_contribution requires pending_splice"
);
let prior = self.pending_splice.as_ref()?.contributions.last()?;
let holder_balance = self
.get_holder_counterparty_balances_floor_incl_fee(&self.funding)
.map(|(h, _)| h)
.ok();
let holder_balance = self.get_next_splice_out_maximum(&self.funding).ok();
Some(PriorContribution::new(prior.clone(), holder_balance))
}

Expand Down Expand Up @@ -12432,10 +12532,7 @@ where
return contribution;
}

let holder_balance = match self
.get_holder_counterparty_balances_floor_incl_fee(&self.funding)
.map(|(holder, _)| holder)
{
let holder_balance = match self.get_next_splice_out_maximum(&self.funding) {
Ok(balance) => balance,
Err(_) => return contribution,
};
Expand Down Expand Up @@ -12536,7 +12633,13 @@ where
// For splice-out, our_funding_contribution is adjusted to cover fees if there
// aren't any inputs.
let our_funding_contribution = contribution.net_value();
self.validate_splice_contributions(our_funding_contribution, SignedAmount::ZERO)
let next_splice_out_maximum = self.get_next_splice_out_maximum(&self.funding)?;
if our_funding_contribution.is_negative() && our_funding_contribution.unsigned_abs() > next_splice_out_maximum {
let unsigned_contribution = our_funding_contribution.unsigned_abs();
Err(format!("Our splice-out value of {unsigned_contribution} is greater than the maximum {next_splice_out_maximum}"))
} else {
Ok(())
}
}) {
log_error!(logger, "Channel {} cannot be funded: {}", self.context.channel_id(), e);

Expand Down Expand Up @@ -12772,11 +12875,6 @@ where
));
}

let (holder_balance_remaining, counterparty_balance_remaining) =
self.get_holder_counterparty_balances_floor_incl_fee(&self.funding).map_err(|e| {
format!("Channel {} cannot be spliced; {}", self.context.channel_id(), e)
})?;

let post_channel_value = self.funding.compute_post_splice_value(
our_funding_contribution.to_sat(),
their_funding_contribution.to_sat(),
Expand All @@ -12796,20 +12894,35 @@ where
self.funding.holder_selected_channel_reserve_satoshis == 0,
));

// We allow parties to draw from their previous reserve, as long as they satisfy their v2 reserve
// At all times, require that the channel funder have the fees for an additional
// inbound HTLC on the channel.
let addl_nondust_htlc_count = 1;
// Note that the feerate is 0 in zero-fee commitment channels, so this statement is a noop
let feerate_per_kw = if !self.funding.get_channel_type().supports_anchors_zero_fee_htlc_tx()
{
// Similar to HTLC-add validation, require the funder to have enough funds
// reserved for fees such that the feerate can jump without rendering the
// channel useless.
self.context.feerate_per_kw * FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE as u32
} else {
self.context.feerate_per_kw
};

if our_funding_contribution != SignedAmount::ZERO {
let post_splice_holder_balance = Amount::from_sat(
holder_balance_remaining.to_sat()
.checked_add_signed(our_funding_contribution.to_sat())
.ok_or(format!(
"Channel {} cannot be spliced out; our remaining balance {} does not cover our negative funding contribution {}",
self.context.channel_id(),
holder_balance_remaining,
our_funding_contribution,
))?,
);
let (post_splice_holder_balance, post_splice_counterparty_balance) = self
.context
.get_post_splice_holder_counterparty_balances(
&self.funding,
our_funding_contribution,
their_funding_contribution,
addl_nondust_htlc_count,
feerate_per_kw,
)
.map_err(|e| {
format!("Channel {} cannot be spliced; {}", self.context.channel_id(), e)
})?;

// We allow parties to draw from their previous reserve, as long as they satisfy their v2 reserve
if our_funding_contribution != SignedAmount::ZERO {
post_splice_holder_balance.checked_sub(counterparty_selected_channel_reserve)
.ok_or(format!(
"Channel {} cannot be {}; our post-splice channel balance {} is smaller than their selected v2 reserve {}",
Expand All @@ -12821,17 +12934,6 @@ where
}

if their_funding_contribution != SignedAmount::ZERO {
let post_splice_counterparty_balance = Amount::from_sat(
counterparty_balance_remaining.to_sat()
.checked_add_signed(their_funding_contribution.to_sat())
.ok_or(format!(
"Channel {} cannot be spliced out; their remaining balance {} does not cover their negative funding contribution {}",
self.context.channel_id(),
counterparty_balance_remaining,
their_funding_contribution,
))?,
);

post_splice_counterparty_balance.checked_sub(holder_selected_channel_reserve)
.ok_or(format!(
"Channel {} cannot be {}; their post-splice channel balance {} is smaller than our selected v2 reserve {}",
Expand All @@ -12849,8 +12951,7 @@ where
&self, feerate: FeeRate, logger: &L,
) -> Result<(Option<SignedAmount>, Option<Amount>), ChannelError> {
let holder_balance = self
.get_holder_counterparty_balances_floor_incl_fee(&self.funding)
.map(|(holder, _)| holder)
.get_next_splice_out_maximum(&self.funding)
.map_err(|e| {
log_info!(
logger,
Expand Down Expand Up @@ -13282,63 +13383,49 @@ where
))
}

fn get_holder_counterparty_balances_floor_incl_fee(
&self, funding: &FundingScope,
) -> Result<(Amount, Amount), String> {
/// Determines the maximum value that the holder can splice out of the channel, accounting
/// for the updated reserves after said splice. This maximum also makes sure the local
/// commitment retains at least one output after the splice, which is particularly relevant
/// for zero-reserve channels.
fn get_next_splice_out_maximum(&self, funding: &FundingScope) -> Result<Amount, String> {
let include_counterparty_unknown_htlcs = true;
// Make sure that that the funder of the channel can pay the transaction fees for an additional
// nondust HTLC on the channel.
let addl_nondust_htlc_count = 1;
// We are not interested in dust exposure
let dust_exposure_limiting_feerate = None;

// Note that the feerate is 0 in zero-fee commitment channels, so this statement is a noop
let feerate_per_kw = if !funding.get_channel_type().supports_anchors_zero_fee_htlc_tx() {
// Similar to HTLC additions, require the funder to have enough funds reserved for
// fees such that the feerate can jump without rendering the channel useless.
self.context.feerate_per_kw * FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE as u32
} else {
self.context.feerate_per_kw
};

let (local_stats, _local_htlcs) = self
.context
.get_next_local_commitment_stats(
funding,
None, // htlc_candidate
include_counterparty_unknown_htlcs,
addl_nondust_htlc_count,
feerate_per_kw,
dust_exposure_limiting_feerate,
)
.map_err(|()| "Balance exhausted on local commitment")?;

// When reading the available balances, we take the remote's view of the pending
// HTLCs, see `tx_builder` for further details
let (remote_stats, _remote_htlcs) = self
.context
.get_next_remote_commitment_stats(
funding,
None, // htlc_candidate
include_counterparty_unknown_htlcs,
addl_nondust_htlc_count,
feerate_per_kw,
0,
self.context.feerate_per_kw,
dust_exposure_limiting_feerate,
)
.map_err(|()| "Balance exhausted on remote commitment")?;

let holder_balance_floor = Amount::from_sat(
cmp::min(
local_stats.commitment_stats.holder_balance_msat,
remote_stats.commitment_stats.holder_balance_msat,
) / 1000,
);
let counterparty_balance_floor = Amount::from_sat(
cmp::min(
local_stats.commitment_stats.counterparty_balance_msat,
remote_stats.commitment_stats.counterparty_balance_msat,
) / 1000,
);
let next_splice_out_maximum_sat =
remote_stats.available_balances.next_splice_out_maximum_sat;

Ok((holder_balance_floor, counterparty_balance_floor))
#[cfg(debug_assertions)]
{
// After this max splice out, validation passes, accounting for the updated reserves
self.validate_splice_contributions(
SignedAmount::from_sat(-(next_splice_out_maximum_sat as i64)),
SignedAmount::ZERO,
)
.unwrap();
// Splice-out an additional satoshi, and validation fails!
self.validate_splice_contributions(
SignedAmount::from_sat(-((next_splice_out_maximum_sat + 1) as i64)),
SignedAmount::ZERO,
)
.unwrap_err();
}

Ok(Amount::from_sat(next_splice_out_maximum_sat))
}

pub fn splice_locked<NS: NodeSigner, L: Logger>(
Expand Down Expand Up @@ -13566,6 +13653,9 @@ where
next_outbound_htlc_minimum_msat: acc
.next_outbound_htlc_minimum_msat
.max(e.next_outbound_htlc_minimum_msat),
next_splice_out_maximum_sat: acc
.next_splice_out_maximum_sat
.min(e.next_splice_out_maximum_sat),
})
})
}
Expand Down
Loading
Loading