Skip to content

Commit 5acd6f5

Browse files
committed
Funder reserve for future fee increase
See lightning/bolts#728 Add an additional reserve on the funder to prevent emptying and then being stuck with an unusable channel. As fundee we don't verify funders comply with that change. We may enforce it in the future when we're confident the network as a whole enforces that.
1 parent bd05eb1 commit 5acd6f5

File tree

2 files changed

+42
-9
lines changed

2 files changed

+42
-9
lines changed

eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala

+16-5
Original file line numberDiff line numberDiff line change
@@ -92,13 +92,16 @@ case class Commitments(channelVersion: ChannelVersion,
9292
if (localParams.isFunder) {
9393
// The funder always pays the on-chain fees, so we must subtract that from the amount we can send.
9494
val commitFees = commitTxFeeMsat(remoteParams.dustLimit, reduced)
95+
// the funder needs to keep an extra reserve to be able to handle fee increase without getting the channel stuck
96+
// (see https://github.com/lightningnetwork/lightning-rfc/issues/728)
97+
val funderFeeReserve = htlcOutputFee(2 * reduced.feeratePerKw)
9598
val htlcFees = htlcOutputFee(reduced.feeratePerKw)
9699
if (balanceNoFees - commitFees < offeredHtlcTrimThreshold(remoteParams.dustLimit, reduced)) {
97100
// htlc will be trimmed
98-
(balanceNoFees - commitFees).max(0 msat)
101+
(balanceNoFees - commitFees - funderFeeReserve).max(0 msat)
99102
} else {
100103
// htlc will have an output in the commitment tx, so there will be additional fees.
101-
(balanceNoFees - commitFees - htlcFees).max(0 msat)
104+
(balanceNoFees - commitFees - funderFeeReserve - htlcFees).max(0 msat)
102105
}
103106
} else {
104107
// The fundee doesn't pay on-chain fees.
@@ -115,13 +118,16 @@ case class Commitments(channelVersion: ChannelVersion,
115118
} else {
116119
// The funder always pays the on-chain fees, so we must subtract that from the amount we can receive.
117120
val commitFees = commitTxFeeMsat(localParams.dustLimit, reduced)
121+
// we expect the funder to keep an extra reserve to be able to handle fee increase without getting the channel stuck
122+
// (see https://github.com/lightningnetwork/lightning-rfc/issues/728)
123+
val funderFeeReserve = htlcOutputFee(2 * reduced.feeratePerKw)
118124
val htlcFees = htlcOutputFee(reduced.feeratePerKw)
119125
if (balanceNoFees - commitFees < receivedHtlcTrimThreshold(localParams.dustLimit, reduced)) {
120126
// htlc will be trimmed
121-
(balanceNoFees - commitFees).max(0 msat)
127+
(balanceNoFees - commitFees - funderFeeReserve).max(0 msat)
122128
} else {
123129
// htlc will have an output in the commitment tx, so there will be additional fees.
124-
(balanceNoFees - commitFees - htlcFees).max(0 msat)
130+
(balanceNoFees - commitFees - funderFeeReserve - htlcFees).max(0 msat)
125131
}
126132
}
127133
}
@@ -183,7 +189,10 @@ object Commitments {
183189

184190
// note that the funder pays the fee, so if sender != funder, both sides will have to afford this payment
185191
val fees = commitTxFee(commitments1.remoteParams.dustLimit, reduced)
186-
val missingForSender = reduced.toRemote - commitments1.remoteParams.channelReserve - (if (commitments1.localParams.isFunder) fees else 0.sat)
192+
// the funder needs to keep an extra reserve to be able to handle fee increase without getting the channel stuck
193+
// (see https://github.com/lightningnetwork/lightning-rfc/issues/728)
194+
val funderFeeReserve = htlcOutputFee(2 * reduced.feeratePerKw)
195+
val missingForSender = reduced.toRemote - commitments1.remoteParams.channelReserve - (if (commitments1.localParams.isFunder) fees + funderFeeReserve else 0.msat)
187196
val missingForReceiver = reduced.toLocal - commitments1.localParams.channelReserve - (if (commitments1.localParams.isFunder) 0.sat else fees)
188197
if (missingForSender < 0.msat) {
189198
return Left(InsufficientFunds(commitments.channelId, amount = cmd.amount, missing = -missingForSender.truncateToSatoshi, reserve = commitments1.remoteParams.channelReserve, fees = if (commitments1.localParams.isFunder) fees else 0.sat))
@@ -225,6 +234,8 @@ object Commitments {
225234

226235
// note that the funder pays the fee, so if sender != funder, both sides will have to afford this payment
227236
val fees = commitTxFee(commitments1.remoteParams.dustLimit, reduced)
237+
// NB: we don't enforce the funderFeeReserve (see sendAdd) because it would confuse a remote funder that doesn't have this mitigation in place
238+
// We could enforce it once we're confident a large portion of the network implements it.
228239
val missingForSender = reduced.toRemote - commitments1.localParams.channelReserve - (if (commitments1.localParams.isFunder) 0.sat else fees)
229240
val missingForReceiver = reduced.toLocal - commitments1.remoteParams.channelReserve - (if (commitments1.localParams.isFunder) fees else 0.sat)
230241
if (missingForSender < 0.sat) {

eclair-core/src/test/scala/fr/acinq/eclair/channel/CommitmentsSpec.scala

+26-4
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,9 @@ class CommitmentsSpec extends TestkitBaseClass with StateTestsHelperMethods {
5353

5454
test("take additional HTLC fee into account") { f =>
5555
import f._
56-
val htlcOutputFee = 1720000 msat
56+
// The fee for a single HTLC is 1720000 msat but the funder keeps an extra reserve to make sure we're able to handle
57+
// an additional HTLC at twice the feerate (hence the multiplier).
58+
val htlcOutputFee = 3 * 1720000 msat
5759
val a = 772760000 msat // initial balance alice
5860
val ac0 = alice.stateData.asInstanceOf[DATA_NORMAL].commitments
5961
val bc0 = bob.stateData.asInstanceOf[DATA_NORMAL].commitments
@@ -75,7 +77,8 @@ class CommitmentsSpec extends TestkitBaseClass with StateTestsHelperMethods {
7577
import f._
7678

7779
val fee = 1720000 msat // fee due to the additional htlc output
78-
val a = (772760000 msat) - fee // initial balance alice
80+
val funderFeeReserve = fee * 2 // extra reserve to handle future fee increase
81+
val a = (772760000 msat) - fee - funderFeeReserve // initial balance alice
7982
val b = 190000000 msat // initial balance bob
8083
val p = 42000000 msat // a->b payment
8184

@@ -159,7 +162,8 @@ class CommitmentsSpec extends TestkitBaseClass with StateTestsHelperMethods {
159162
import f._
160163

161164
val fee = 1720000 msat // fee due to the additional htlc output
162-
val a = (772760000 msat) - fee // initial balance alice
165+
val funderFeeReserve = fee * 2 // extra reserve to handle future fee increase
166+
val a = (772760000 msat) - fee - funderFeeReserve // initial balance alice
163167
val b = 190000000 msat // initial balance bob
164168
val p = 42000000 msat // a->b payment
165169

@@ -243,7 +247,8 @@ class CommitmentsSpec extends TestkitBaseClass with StateTestsHelperMethods {
243247
import f._
244248

245249
val fee = 1720000 msat // fee due to the additional htlc output
246-
val a = (772760000 msat) - fee // initial balance alice
250+
val funderFeeReserve = fee * 2 // extra reserve to handle future fee increase
251+
val a = (772760000 msat) - fee - funderFeeReserve // initial balance alice
247252
val b = 190000000 msat // initial balance bob
248253
val p1 = 10000000 msat // a->b payment
249254
val p2 = 20000000 msat // a->b payment
@@ -386,6 +391,23 @@ class CommitmentsSpec extends TestkitBaseClass with StateTestsHelperMethods {
386391
assert(ac16.availableBalanceForReceive == b + p1 - p3)
387392
}
388393

394+
// See https://github.com/lightningnetwork/lightning-rfc/issues/728
395+
test("funder keeps additional reserve to avoid channel being stuck") { f =>
396+
val isFunder = true
397+
val c = CommitmentsSpec.makeCommitments(100000000 msat, 50000000 msat, 2500, 546 sat, isFunder)
398+
val (_, cmdAdd) = makeCmdAdd(c.availableBalanceForSend, randomKey.publicKey, f.currentBlockHeight)
399+
val Right((c1, _)) = sendAdd(c, cmdAdd, Local(UUID.randomUUID, None), f.currentBlockHeight)
400+
assert(c1.availableBalanceForSend === 0.msat)
401+
402+
// We should be able to handle a fee increase.
403+
val (c2, _) = sendFee(c1, CMD_UPDATE_FEE(3000))
404+
405+
// Now we shouldn't be able to send until we receive enough to handle the updated commit tx fee (even trimmed HTLCs shouldn't be sent).
406+
val (_, cmdAdd1) = makeCmdAdd(100 msat, randomKey.publicKey, f.currentBlockHeight)
407+
val Left(e) = sendAdd(c2, cmdAdd1, Local(UUID.randomUUID, None), f.currentBlockHeight)
408+
assert(e.isInstanceOf[InsufficientFunds])
409+
}
410+
389411
test("can send availableForSend") { f =>
390412
for (isFunder <- Seq(true, false)) {
391413
val c = CommitmentsSpec.makeCommitments(702000000 msat, 52000000 msat, 2679, 546 sat, isFunder)

0 commit comments

Comments
 (0)