Skip to content

Commit 26a54b5

Browse files
authored
Funder reserve for future fee increase (#1319)
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 b5bd2f0 commit 26a54b5

File tree

3 files changed

+62
-21
lines changed

3 files changed

+62
-21
lines changed

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

+16-5
Original file line numberDiff line numberDiff line change
@@ -94,13 +94,16 @@ case class Commitments(channelVersion: ChannelVersion,
9494
if (localParams.isFunder) {
9595
// The funder always pays the on-chain fees, so we must subtract that from the amount we can send.
9696
val commitFees = commitTxFeeMsat(remoteParams.dustLimit, reduced)
97+
// the funder needs to keep an extra reserve to be able to handle fee increase without getting the channel stuck
98+
// (see https://github.com/lightningnetwork/lightning-rfc/issues/728)
99+
val funderFeeReserve = htlcOutputFee(2 * reduced.feeratePerKw)
97100
val htlcFees = htlcOutputFee(reduced.feeratePerKw)
98101
if (balanceNoFees - commitFees < offeredHtlcTrimThreshold(remoteParams.dustLimit, reduced)) {
99102
// htlc will be trimmed
100-
(balanceNoFees - commitFees).max(0 msat)
103+
(balanceNoFees - commitFees - funderFeeReserve).max(0 msat)
101104
} else {
102105
// htlc will have an output in the commitment tx, so there will be additional fees.
103-
(balanceNoFees - commitFees - htlcFees).max(0 msat)
106+
(balanceNoFees - commitFees - funderFeeReserve - htlcFees).max(0 msat)
104107
}
105108
} else {
106109
// The fundee doesn't pay on-chain fees.
@@ -117,13 +120,16 @@ case class Commitments(channelVersion: ChannelVersion,
117120
} else {
118121
// The funder always pays the on-chain fees, so we must subtract that from the amount we can receive.
119122
val commitFees = commitTxFeeMsat(localParams.dustLimit, reduced)
123+
// we expect the funder to keep an extra reserve to be able to handle fee increase without getting the channel stuck
124+
// (see https://github.com/lightningnetwork/lightning-rfc/issues/728)
125+
val funderFeeReserve = htlcOutputFee(2 * reduced.feeratePerKw)
120126
val htlcFees = htlcOutputFee(reduced.feeratePerKw)
121127
if (balanceNoFees - commitFees < receivedHtlcTrimThreshold(localParams.dustLimit, reduced)) {
122128
// htlc will be trimmed
123-
(balanceNoFees - commitFees).max(0 msat)
129+
(balanceNoFees - commitFees - funderFeeReserve).max(0 msat)
124130
} else {
125131
// htlc will have an output in the commitment tx, so there will be additional fees.
126-
(balanceNoFees - commitFees - htlcFees).max(0 msat)
132+
(balanceNoFees - commitFees - funderFeeReserve - htlcFees).max(0 msat)
127133
}
128134
}
129135
}
@@ -185,7 +191,10 @@ object Commitments {
185191

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

228237
// note that the funder pays the fee, so if sender != funder, both sides will have to afford this payment
229238
val fees = commitTxFee(commitments1.remoteParams.dustLimit, reduced)
239+
// NB: we don't enforce the funderFeeReserve (see sendAdd) because it would confuse a remote funder that doesn't have this mitigation in place
240+
// We could enforce it once we're confident a large portion of the network implements it.
230241
val missingForSender = reduced.toRemote - commitments1.localParams.channelReserve - (if (commitments1.localParams.isFunder) 0.sat else fees)
231242
val missingForReceiver = reduced.toLocal - commitments1.remoteParams.channelReserve - (if (commitments1.localParams.isFunder) fees else 0.sat)
232243
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 Success((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 Success((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 Failure(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)

eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala

+20-12
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
190190
val initialState = alice.stateData.asInstanceOf[DATA_NORMAL]
191191
val add = CMD_ADD_HTLC(MilliSatoshi(Int.MaxValue), randomBytes32, CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, Upstream.Local(UUID.randomUUID()))
192192
sender.send(alice, add)
193-
val error = InsufficientFunds(channelId(alice), amount = MilliSatoshi(Int.MaxValue), missing = 1376443 sat, reserve = 20000 sat, fees = 8960 sat)
193+
val error = InsufficientFunds(channelId(alice), amount = MilliSatoshi(Int.MaxValue), missing = 1379883 sat, reserve = 20000 sat, fees = 8960 sat)
194194
sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Origin.Local(add.upstream.asInstanceOf[Upstream.Local].id, Some(sender.ref)), Some(initialState.channelUpdate), Some(add))))
195195
alice2bob.expectNoMsg(200 millis)
196196
}
@@ -207,19 +207,27 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
207207
alice2bob.expectNoMsg(200 millis)
208208
}
209209

210-
test("recv CMD_ADD_HTLC (HTLC dips remote funder below reserve)") { f =>
210+
test("recv CMD_ADD_HTLC (HTLC dips into remote funder fee reserve)") { f =>
211211
import f._
212212
val sender = TestProbe()
213-
addHtlc(771000000 msat, alice, bob, alice2bob, bob2alice)
213+
addHtlc(767600000 msat, alice, bob, alice2bob, bob2alice)
214214
crossSign(alice, bob, alice2bob, bob2alice)
215-
assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.availableBalanceForSend === 40000.msat)
215+
assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.availableBalanceForSend === 0.msat)
216216

217217
// actual test begins
218-
// at this point alice has the minimal amount to sustain a channel (29000 sat ~= alice reserve + commit fee)
219-
val add = CMD_ADD_HTLC(120000000 msat, randomBytes32, CltvExpiry(400144), TestConstants.emptyOnionPacket, Upstream.Local(UUID.randomUUID()))
220-
sender.send(bob, add)
221-
val error = RemoteCannotAffordFeesForNewHtlc(channelId(bob), add.amount, missing = 1680 sat, 10000 sat, 10680 sat)
222-
sender.expectMsg(Failure(AddHtlcFailed(channelId(bob), add.paymentHash, error, Origin.Local(add.upstream.asInstanceOf[Upstream.Local].id, Some(sender.ref)), Some(bob.stateData.asInstanceOf[DATA_NORMAL].channelUpdate), Some(add))))
218+
// at this point alice has the minimal amount to sustain a channel
219+
// alice maintains an extra reserve to accommodate for a few more HTLCs, so the first two HTLCs should be allowed
220+
sender.send(bob, CMD_ADD_HTLC(12000000 msat, randomBytes32, CltvExpiry(400144), TestConstants.emptyOnionPacket, Upstream.Local(UUID.randomUUID())))
221+
sender.expectMsg(ChannelCommandResponse.Ok)
222+
223+
sender.send(bob, CMD_ADD_HTLC(12500000 msat, randomBytes32, CltvExpiry(400144), TestConstants.emptyOnionPacket, Upstream.Local(UUID.randomUUID())))
224+
sender.expectMsg(ChannelCommandResponse.Ok)
225+
226+
// but this one will dip alice below her reserve: we must wait for the two previous HTLCs to settle before sending any more
227+
val failedAdd = CMD_ADD_HTLC(11000000 msat, randomBytes32, CltvExpiry(400144), TestConstants.emptyOnionPacket, Upstream.Local(UUID.randomUUID()))
228+
sender.send(bob, failedAdd)
229+
val error = RemoteCannotAffordFeesForNewHtlc(channelId(bob), failedAdd.amount, missing = 1720 sat, 10000 sat, 14120 sat)
230+
sender.expectMsg(Failure(AddHtlcFailed(channelId(bob), failedAdd.paymentHash, error, Origin.Local(failedAdd.upstream.asInstanceOf[Upstream.Local].id, Some(sender.ref)), Some(bob.stateData.asInstanceOf[DATA_NORMAL].channelUpdate), Some(failedAdd))))
223231
}
224232

225233
test("recv CMD_ADD_HTLC (insufficient funds w/ pending htlcs and 0 balance)") { f =>
@@ -232,7 +240,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
232240
sender.send(alice, CMD_ADD_HTLC(200000000 msat, randomBytes32, CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, Upstream.Local(UUID.randomUUID())))
233241
sender.expectMsg(ChannelCommandResponse.Ok)
234242
alice2bob.expectMsgType[UpdateAddHtlc]
235-
sender.send(alice, CMD_ADD_HTLC(67600000 msat, randomBytes32, CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, Upstream.Local(UUID.randomUUID())))
243+
sender.send(alice, CMD_ADD_HTLC(64160000 msat, randomBytes32, CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, Upstream.Local(UUID.randomUUID())))
236244
sender.expectMsg(ChannelCommandResponse.Ok)
237245
alice2bob.expectMsgType[UpdateAddHtlc]
238246
val add = CMD_ADD_HTLC(1000000 msat, randomBytes32, CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, Upstream.Local(UUID.randomUUID()))
@@ -254,7 +262,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
254262
alice2bob.expectMsgType[UpdateAddHtlc]
255263
val add = CMD_ADD_HTLC(500000000 msat, randomBytes32, CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, Upstream.Local(UUID.randomUUID()))
256264
sender.send(alice, add)
257-
val error = InsufficientFunds(channelId(alice), amount = 500000000 msat, missing = 332400 sat, reserve = 20000 sat, fees = 12400 sat)
265+
val error = InsufficientFunds(channelId(alice), amount = 500000000 msat, missing = 335840 sat, reserve = 20000 sat, fees = 12400 sat)
258266
sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Origin.Local(add.upstream.asInstanceOf[Upstream.Local].id, Some(sender.ref)), Some(initialState.channelUpdate), Some(add))))
259267
alice2bob.expectNoMsg(200 millis)
260268
}
@@ -316,7 +324,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
316324
// this is over channel-capacity
317325
val add2 = CMD_ADD_HTLC(TestConstants.fundingSatoshis.toMilliSatoshi * 2 / 3, randomBytes32, CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, Upstream.Local(UUID.randomUUID()))
318326
sender.send(alice, add2)
319-
val error = InsufficientFunds(channelId(alice), add2.amount, 564013 sat, 20000 sat, 10680 sat)
327+
val error = InsufficientFunds(channelId(alice), add2.amount, 567453 sat, 20000 sat, 10680 sat)
320328
sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add2.paymentHash, error, Origin.Local(add2.upstream.asInstanceOf[Upstream.Local].id, Some(sender.ref)), Some(initialState.channelUpdate), Some(add2))))
321329
alice2bob.expectNoMsg(200 millis)
322330
}

0 commit comments

Comments
 (0)