Skip to content

Commit

Permalink
Fixup funder fee buffer (#1364)
Browse files Browse the repository at this point in the history
We were previously only counting the additional HTLC at twice
the current feerate.

An HTLC in isolation doesn't make much sense: the feerate
applies to the whole commit tx. Our fee buffer needs to account
for a x2 feerate increase on the commit tx + an additional htlc.

Note that this introduces another subtlety. The commit tx fee at twice the
current feerate may actually be lower than the commit tx fee at the current
feerate (if the commit tx contains many htlcs that are only slightly above
the trim threshold).
  • Loading branch information
t-bast authored Sep 29, 2020
1 parent 36c9b9b commit 8a27b4c
Show file tree
Hide file tree
Showing 3 changed files with 118 additions and 110 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,32 @@ case class Commitments(channelVersion: ChannelVersion,

val announceChannel: Boolean = (channelFlags & 0x01) != 0

// NB: when computing availableBalanceForSend and availableBalanceForReceive, the funder keeps an extra buffer on top
// of its usual channel reserve to avoid getting channels stuck in case the on-chain feerate increases (see
// https://github.com/lightningnetwork/lightning-rfc/issues/728 for details).
//
// This extra buffer (which we call "funder fee buffer") is calculated as follows:
// 1) Simulate a x2 feerate increase and compute the corresponding commit tx fee (note that it may trim some HTLCs)
// 2) Add the cost of adding a new untrimmed HTLC at that increased feerate. This ensures that we'll be able to
// actually use the channel to add new HTLCs if the feerate doubles.
//
// If for example the current feerate is 1000 sat/kw, the dust limit 546 sat, and we have 3 pending outgoing HTLCs for
// respectively 1250 sat, 2000 sat and 2500 sat.
// commit tx fee = commitWeight * feerate + 3 * htlcOutputWeight * feerate = 724 * 1000 + 3 * 172 * 1000 = 1240 sat
// To calculate the funder fee buffer, we first double the feerate and calculate the corresponding commit tx fee.
// By doubling the feerate, the first HTLC becomes trimmed so the result is: 724 * 2000 + 2 * 172 * 2000 = 2136 sat
// We then add the additional fee for a potential new untrimmed HTLC: 172 * 2000 = 344 sat
// The funder fee buffer is 2136 + 344 = 2480 sat
//
// If there are many pending HTLCs that are only slightly above the trim threshold, the funder fee buffer may be
// smaller than the current commit tx fee because those HTLCs will be trimmed and the commit tx weight will decrease.
// For example if we have 10 outgoing HTLCs of 1250 sat:
// - commit tx fee = 724 * 1000 + 10 * 172 * 1000 = 2444 sat
// - commit tx fee at twice the feerate = 724 * 2000 = 1448 sat (all HTLCs have been trimmed)
// - cost of an additional untrimmed HTLC = 172 * 2000 = 344 sat
// - funder fee buffer = 1448 + 344 = 1792 sat
// In that case the current commit tx fee is higher than the funder fee buffer and will dominate the balance restrictions.

lazy val availableBalanceForSend: MilliSatoshi = {
// we need to base the next current commitment on the last sig we sent, even if we didn't yet receive their revocation
val remoteCommit1 = remoteNextCommitInfo.left.toOption.map(_.nextRemoteCommit).getOrElse(remoteCommit)
Expand All @@ -109,16 +135,19 @@ case class Commitments(channelVersion: ChannelVersion,
if (localParams.isFunder) {
// The funder always pays the on-chain fees, so we must subtract that from the amount we can send.
val commitFees = commitTxFeeMsat(remoteParams.dustLimit, reduced, commitmentFormat)
// the funder needs to keep an extra reserve to be able to handle fee increase without getting the channel stuck
// (see https://github.com/lightningnetwork/lightning-rfc/issues/728)
val funderFeeReserve = htlcOutputFee(reduced.feeratePerKw * 2, commitmentFormat)
val htlcFees = htlcOutputFee(reduced.feeratePerKw, commitmentFormat)
if (balanceNoFees - commitFees < offeredHtlcTrimThreshold(remoteParams.dustLimit, reduced, commitmentFormat)) {
// the funder needs to keep a "funder fee buffer" (see explanation above)
val funderFeeBuffer = commitTxFeeMsat(remoteParams.dustLimit, reduced.copy(feeratePerKw = reduced.feeratePerKw * 2), commitmentFormat) + htlcOutputFee(reduced.feeratePerKw * 2, commitmentFormat)
val amountToReserve = commitFees.max(funderFeeBuffer)
if (balanceNoFees - amountToReserve < offeredHtlcTrimThreshold(remoteParams.dustLimit, reduced, commitmentFormat)) {
// htlc will be trimmed
(balanceNoFees - commitFees - funderFeeReserve).max(0 msat)
(balanceNoFees - amountToReserve).max(0 msat)
} else {
// htlc will have an output in the commitment tx, so there will be additional fees.
(balanceNoFees - commitFees - funderFeeReserve - htlcFees).max(0 msat)
val commitFees1 = commitFees + htlcOutputFee(reduced.feeratePerKw, commitmentFormat)
// we take the additional fees for that htlc output into account in the fee buffer at a x2 feerate increase
val funderFeeBuffer1 = funderFeeBuffer + htlcOutputFee(reduced.feeratePerKw * 2, commitmentFormat)
val amountToReserve1 = commitFees1.max(funderFeeBuffer1)
(balanceNoFees - amountToReserve1).max(0 msat)
}
} else {
// The fundee doesn't pay on-chain fees.
Expand All @@ -135,16 +164,19 @@ case class Commitments(channelVersion: ChannelVersion,
} else {
// The funder always pays the on-chain fees, so we must subtract that from the amount we can receive.
val commitFees = commitTxFeeMsat(localParams.dustLimit, reduced, commitmentFormat)
// we expect the funder to keep an extra reserve to be able to handle fee increase without getting the channel stuck
// (see https://github.com/lightningnetwork/lightning-rfc/issues/728)
val funderFeeReserve = htlcOutputFee(reduced.feeratePerKw * 2, commitmentFormat)
val htlcFees = htlcOutputFee(reduced.feeratePerKw, commitmentFormat)
if (balanceNoFees - commitFees < receivedHtlcTrimThreshold(localParams.dustLimit, reduced, commitmentFormat)) {
// we expected the funder to keep a "funder fee buffer" (see explanation above)
val funderFeeBuffer = commitTxFeeMsat(localParams.dustLimit, reduced.copy(feeratePerKw = reduced.feeratePerKw * 2), commitmentFormat) + htlcOutputFee(reduced.feeratePerKw * 2, commitmentFormat)
val amountToReserve = commitFees.max(funderFeeBuffer)
if (balanceNoFees - amountToReserve < receivedHtlcTrimThreshold(localParams.dustLimit, reduced, commitmentFormat)) {
// htlc will be trimmed
(balanceNoFees - commitFees - funderFeeReserve).max(0 msat)
(balanceNoFees - amountToReserve).max(0 msat)
} else {
// htlc will have an output in the commitment tx, so there will be additional fees.
(balanceNoFees - commitFees - funderFeeReserve - htlcFees).max(0 msat)
val commitFees1 = commitFees + htlcOutputFee(reduced.feeratePerKw, commitmentFormat)
// we take the additional fees for that htlc output into account in the fee buffer at a x2 feerate increase
val funderFeeBuffer1 = funderFeeBuffer + htlcOutputFee(reduced.feeratePerKw * 2, commitmentFormat)
val amountToReserve1 = commitFees1.max(funderFeeBuffer1)
(balanceNoFees - amountToReserve1).max(0 msat)
}
}
}
Expand Down Expand Up @@ -176,7 +208,7 @@ object Commitments {
*
* @param commitments current commitments
* @param cmd add HTLC command
* @return either Left(failure, error message) where failure is a failure message (see BOLT #4 and the Failure Message class) or Right((new commitments, updateAddHtlc)
* @return either Left(failure, error message) where failure is a failure message (see BOLT #4 and the Failure Message class) or Right(new commitments, updateAddHtlc)
*/
def sendAdd(commitments: Commitments, cmd: CMD_ADD_HTLC, blockHeight: Long, feeConf: OnChainFeeConf): Either[ChannelException, (Commitments, UpdateAddHtlc)] = {
// we don't want to use too high a refund timeout, because our funds will be locked during that time if the payment is never fulfilled
Expand Down Expand Up @@ -210,10 +242,12 @@ object Commitments {

// note that the funder pays the fee, so if sender != funder, both sides will have to afford this payment
val fees = commitTxFee(commitments1.remoteParams.dustLimit, reduced, commitments.commitmentFormat)
// the funder needs to keep an extra reserve to be able to handle fee increase without getting the channel stuck
// (see https://github.com/lightningnetwork/lightning-rfc/issues/728)
val funderFeeReserve = htlcOutputFee(reduced.feeratePerKw * 2, commitments.commitmentFormat)
val missingForSender = reduced.toRemote - commitments1.remoteParams.channelReserve - (if (commitments1.localParams.isFunder) fees + funderFeeReserve else 0.msat)
// the funder needs to keep an extra buffer to be able to handle a x2 feerate increase and an additional htlc to avoid
// getting the channel stuck (see https://github.com/lightningnetwork/lightning-rfc/issues/728).
val funderFeeBuffer = commitTxFeeMsat(commitments1.remoteParams.dustLimit, reduced.copy(feeratePerKw = reduced.feeratePerKw * 2), commitments.commitmentFormat) + htlcOutputFee(reduced.feeratePerKw * 2, commitments.commitmentFormat)
// NB: increasing the feerate can actually remove htlcs from the commit tx (if they fall below the trim threshold)
// which may result in a lower commit tx fee; this is why we take the max of the two.
val missingForSender = reduced.toRemote - commitments1.remoteParams.channelReserve - (if (commitments1.localParams.isFunder) fees.max(funderFeeBuffer.truncateToSatoshi) else 0.sat)
val missingForReceiver = reduced.toLocal - commitments1.localParams.channelReserve - (if (commitments1.localParams.isFunder) 0.sat else fees)
if (missingForSender < 0.msat) {
return Left(InsufficientFunds(commitments.channelId, amount = cmd.amount, missing = -missingForSender.truncateToSatoshi, reserve = commitments1.remoteParams.channelReserve, fees = if (commitments1.localParams.isFunder) fees else 0.sat))
Expand Down
Loading

0 comments on commit 8a27b4c

Please sign in to comment.