Skip to content

Commit af618bc

Browse files
authored
Symmetrical HTLC limits (#1828)
The spec defines `max_accepted_htlcs` and `max_htlc_value_in_flight_msat` to let nodes reduce their exposure to pending HTLCs. This only applies to received HTLCs, and we use the remote peer's values for outgoing HTLCs. But when we're more restrictive than our peer, it makes sense to apply our limits to outgoing HTLCs as well.
1 parent 43a89f8 commit af618bc

File tree

5 files changed

+70
-25
lines changed

5 files changed

+70
-25
lines changed

eclair-core/src/main/resources/reference.conf

+2-1
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,9 @@ eclair {
6565

6666
dust-limit-satoshis = 546
6767
max-remote-dust-limit-satoshis = 600
68-
max-htlc-value-in-flight-msat = 5000000000 // 50 mBTC
6968
htlc-minimum-msat = 1
69+
// The following parameters apply to each HTLC direction (incoming or outgoing), which means that the total HTLC limits will be twice what is set here
70+
max-htlc-value-in-flight-msat = 5000000000 // 50 mBTC
7071
max-accepted-htlcs = 30
7172

7273
reserve-to-funding-ratio = 0.01 // recommended by BOLT #2

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

+6-6
Original file line numberDiff line numberDiff line change
@@ -324,15 +324,15 @@ object Commitments {
324324
}
325325
}
326326

327+
// We apply local *and* remote restrictions, to ensure both peers are happy with the resulting number of HTLCs.
327328
// NB: we need the `toSeq` because otherwise duplicate amountMsat would be removed (since outgoingHtlcs is a Set).
328329
val htlcValueInFlight = outgoingHtlcs.toSeq.map(_.amountMsat).sum
329-
if (commitments1.remoteParams.maxHtlcValueInFlightMsat < htlcValueInFlight) {
330-
// TODO: this should be a specific UPDATE error
331-
return Left(HtlcValueTooHighInFlight(commitments.channelId, maximum = commitments1.remoteParams.maxHtlcValueInFlightMsat, actual = htlcValueInFlight))
330+
if (Seq(commitments1.localParams.maxHtlcValueInFlightMsat, commitments1.remoteParams.maxHtlcValueInFlightMsat).min < htlcValueInFlight) {
331+
// TODO: this should be a specific UPDATE error (but it would require a spec change)
332+
return Left(HtlcValueTooHighInFlight(commitments.channelId, maximum = Seq(commitments1.localParams.maxHtlcValueInFlightMsat, commitments1.remoteParams.maxHtlcValueInFlightMsat).min, actual = htlcValueInFlight))
332333
}
333-
334-
if (outgoingHtlcs.size > commitments1.remoteParams.maxAcceptedHtlcs) {
335-
return Left(TooManyAcceptedHtlcs(commitments.channelId, maximum = commitments1.remoteParams.maxAcceptedHtlcs))
334+
if (Seq(commitments1.localParams.maxAcceptedHtlcs, commitments1.remoteParams.maxAcceptedHtlcs).min < outgoingHtlcs.size) {
335+
return Left(TooManyAcceptedHtlcs(commitments.channelId, maximum = Seq(commitments1.localParams.maxAcceptedHtlcs, commitments1.remoteParams.maxAcceptedHtlcs).min))
336336
}
337337

338338
Right(commitments1, add)

eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ object TestConstants {
106106
defaultFeerateTolerance = FeerateTolerance(0.5, 8.0, anchorOutputsFeeratePerKw),
107107
perNodeFeerateTolerance = Map.empty
108108
),
109-
maxHtlcValueInFlightMsat = UInt64(150000000),
109+
maxHtlcValueInFlightMsat = UInt64(500000000),
110110
maxAcceptedHtlcs = 100,
111111
expiryDelta = CltvExpiryDelta(144),
112112
fulfillSafetyBeforeTimeout = CltvExpiryDelta(6),

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

+11-2
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,10 @@ object StateTestsTags {
6868
val ChannelsPublic = "channels_public"
6969
/** If set, no amount will be pushed when opening a channel (by default we push a small amount). */
7070
val NoPushMsat = "no_push_msat"
71+
/** If set, max-htlc-value-in-flight will be set to the highest possible value for Alice and Bob. */
72+
val NoMaxHtlcValueInFlight = "no_max_htlc_value_in_flight"
73+
/** If set, max-htlc-value-in-flight will be set to a low value for Alice. */
74+
val AliceLowMaxHtlcValueInFlight = "alice_low_max_htlc_value_in_flight"
7175
}
7276

7377
trait StateTestsHelperMethods extends TestKitBase {
@@ -127,8 +131,13 @@ trait StateTestsHelperMethods extends TestKitBase {
127131
).reduce(_ | _)
128132

129133
val channelFlags = if (tags.contains(StateTestsTags.ChannelsPublic)) ChannelFlags.AnnounceChannel else ChannelFlags.Empty
130-
val aliceParams = setChannelFeatures(Alice.channelParams, tags).modify(_.walletStaticPaymentBasepoint).setToIf(channelVersion.paysDirectlyToWallet)(Some(Helpers.getWalletPaymentBasepoint(wallet)))
131-
val bobParams = setChannelFeatures(Bob.channelParams, tags).modify(_.walletStaticPaymentBasepoint).setToIf(channelVersion.paysDirectlyToWallet)(Some(Helpers.getWalletPaymentBasepoint(wallet)))
134+
val aliceParams = setChannelFeatures(Alice.channelParams, tags)
135+
.modify(_.walletStaticPaymentBasepoint).setToIf(channelVersion.paysDirectlyToWallet)(Some(Helpers.getWalletPaymentBasepoint(wallet)))
136+
.modify(_.maxHtlcValueInFlightMsat).setToIf(tags.contains(StateTestsTags.NoMaxHtlcValueInFlight))(UInt64.MaxValue)
137+
.modify(_.maxHtlcValueInFlightMsat).setToIf(tags.contains(StateTestsTags.AliceLowMaxHtlcValueInFlight))(UInt64(150000000))
138+
val bobParams = setChannelFeatures(Bob.channelParams, tags)
139+
.modify(_.walletStaticPaymentBasepoint).setToIf(channelVersion.paysDirectlyToWallet)(Some(Helpers.getWalletPaymentBasepoint(wallet)))
140+
.modify(_.maxHtlcValueInFlightMsat).setToIf(tags.contains(StateTestsTags.NoMaxHtlcValueInFlight))(UInt64.MaxValue)
132141
val initialFeeratePerKw = if (tags.contains(StateTestsTags.AnchorOutputs)) TestConstants.anchorOutputsFeeratePerKw else TestConstants.feeratePerKw
133142
val (fundingSatoshis, pushMsat) = if (tags.contains(StateTestsTags.NoPushMsat)) {
134143
(TestConstants.fundingSatoshis, 0.msat)

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

+50-15
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
9494
val sender = TestProbe()
9595
val h = randomBytes32()
9696
for (i <- 0 until 10) {
97-
alice ! CMD_ADD_HTLC(sender.ref, 50000000 msat, h, CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, localOrigin(sender.ref))
97+
alice ! CMD_ADD_HTLC(sender.ref, 500000 msat, h, CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, localOrigin(sender.ref))
9898
sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]]
9999
val htlc = alice2bob.expectMsgType[UpdateAddHtlc]
100100
assert(htlc.id == i && htlc.paymentHash == h)
@@ -237,7 +237,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
237237
bob2alice.expectNoMsg(200 millis)
238238
}
239239

240-
test("recv CMD_ADD_HTLC (HTLC dips into remote funder fee reserve)") { f =>
240+
test("recv CMD_ADD_HTLC (HTLC dips into remote funder fee reserve)", Tag(StateTestsTags.NoMaxHtlcValueInFlight)) { f =>
241241
import f._
242242
val sender = TestProbe()
243243
addHtlc(758640000 msat, alice, bob, alice2bob, bob2alice)
@@ -246,21 +246,20 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
246246

247247
// actual test begins
248248
// at this point alice has the minimal amount to sustain a channel
249-
// alice maintains an extra reserve to accommodate for a few more HTLCs, so the first two HTLCs should be allowed
249+
// alice maintains an extra reserve to accommodate for a few more HTLCs, so the first few HTLCs should be allowed
250250
for (_ <- 1 to 7) {
251251
bob ! CMD_ADD_HTLC(sender.ref, 12000000 msat, randomBytes32(), CltvExpiry(400144), TestConstants.emptyOnionPacket, localOrigin(sender.ref))
252252
sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]]
253253
}
254254

255-
// but this one will dip alice below her reserve: we must wait for the two previous HTLCs to settle before sending any more
256255
// but this one will dip alice below her reserve: we must wait for the previous HTLCs to settle before sending any more
257256
val failedAdd = CMD_ADD_HTLC(sender.ref, 11000000 msat, randomBytes32(), CltvExpiry(400144), TestConstants.emptyOnionPacket, localOrigin(sender.ref))
258257
bob ! failedAdd
259258
val error = RemoteCannotAffordFeesForNewHtlc(channelId(bob), failedAdd.amount, missing = 1360 sat, 10000 sat, 22720 sat)
260259
sender.expectMsg(RES_ADD_FAILED(failedAdd, error, Some(bob.stateData.asInstanceOf[DATA_NORMAL].channelUpdate)))
261260
}
262261

263-
test("recv CMD_ADD_HTLC (insufficient funds w/ pending htlcs and 0 balance)") { f =>
262+
test("recv CMD_ADD_HTLC (insufficient funds w/ pending htlcs and 0 balance)", Tag(StateTestsTags.NoMaxHtlcValueInFlight)) { f =>
264263
import f._
265264
val sender = TestProbe()
266265
val initialState = alice.stateData.asInstanceOf[DATA_NORMAL]
@@ -280,7 +279,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
280279
alice2bob.expectNoMsg(200 millis)
281280
}
282281

283-
test("recv CMD_ADD_HTLC (insufficient funds w/ pending htlcs 2/2)") { f =>
282+
test("recv CMD_ADD_HTLC (insufficient funds w/ pending htlcs 2/2)", Tag(StateTestsTags.NoMaxHtlcValueInFlight)) { f =>
284283
import f._
285284
val sender = TestProbe()
286285
val initialState = alice.stateData.asInstanceOf[DATA_NORMAL]
@@ -297,21 +296,25 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
297296
alice2bob.expectNoMsg(200 millis)
298297
}
299298

300-
test("recv CMD_ADD_HTLC (over max inflight htlc value)") { f =>
299+
test("recv CMD_ADD_HTLC (over remote max inflight htlc value)", Tag(StateTestsTags.AliceLowMaxHtlcValueInFlight)) { f =>
301300
import f._
302301
val sender = TestProbe()
303302
val initialState = bob.stateData.asInstanceOf[DATA_NORMAL]
303+
assert(initialState.commitments.localParams.maxHtlcValueInFlightMsat === UInt64.MaxValue)
304+
assert(initialState.commitments.remoteParams.maxHtlcValueInFlightMsat === UInt64(150000000))
304305
val add = CMD_ADD_HTLC(sender.ref, 151000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, localOrigin(sender.ref))
305306
bob ! add
306307
val error = HtlcValueTooHighInFlight(channelId(bob), maximum = 150000000, actual = 151000000 msat)
307308
sender.expectMsg(RES_ADD_FAILED(add, error, Some(initialState.channelUpdate)))
308309
bob2alice.expectNoMsg(200 millis)
309310
}
310311

311-
test("recv CMD_ADD_HTLC (over max inflight htlc value with duplicate amounts)") { f =>
312+
test("recv CMD_ADD_HTLC (over remote max inflight htlc value with duplicate amounts)", Tag(StateTestsTags.AliceLowMaxHtlcValueInFlight)) { f =>
312313
import f._
313314
val sender = TestProbe()
314315
val initialState = bob.stateData.asInstanceOf[DATA_NORMAL]
316+
assert(initialState.commitments.localParams.maxHtlcValueInFlightMsat === UInt64.MaxValue)
317+
assert(initialState.commitments.remoteParams.maxHtlcValueInFlightMsat === UInt64(150000000))
315318
val add = CMD_ADD_HTLC(sender.ref, 75500000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, localOrigin(sender.ref))
316319
bob ! add
317320
sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]]
@@ -323,12 +326,26 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
323326
bob2alice.expectNoMsg(200 millis)
324327
}
325328

326-
test("recv CMD_ADD_HTLC (over max accepted htlcs)") { f =>
329+
test("recv CMD_ADD_HTLC (over local max inflight htlc value)", Tag(StateTestsTags.AliceLowMaxHtlcValueInFlight)) { f =>
327330
import f._
328331
val sender = TestProbe()
329332
val initialState = alice.stateData.asInstanceOf[DATA_NORMAL]
330-
// Bob accepts a maximum of 30 htlcs
331-
for (i <- 0 until 30) {
333+
assert(initialState.commitments.localParams.maxHtlcValueInFlightMsat === UInt64(150000000))
334+
assert(initialState.commitments.remoteParams.maxHtlcValueInFlightMsat === UInt64.MaxValue)
335+
val add = CMD_ADD_HTLC(sender.ref, 151000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, localOrigin(sender.ref))
336+
alice ! add
337+
val error = HtlcValueTooHighInFlight(channelId(alice), maximum = 150000000, actual = 151000000 msat)
338+
sender.expectMsg(RES_ADD_FAILED(add, error, Some(initialState.channelUpdate)))
339+
alice2bob.expectNoMsg(200 millis)
340+
}
341+
342+
test("recv CMD_ADD_HTLC (over remote max accepted htlcs)") { f =>
343+
import f._
344+
val sender = TestProbe()
345+
val initialState = alice.stateData.asInstanceOf[DATA_NORMAL]
346+
assert(initialState.commitments.localParams.maxAcceptedHtlcs === 100)
347+
assert(initialState.commitments.remoteParams.maxAcceptedHtlcs === 30) // Bob accepts a maximum of 30 htlcs
348+
for (_ <- 0 until 30) {
332349
alice ! CMD_ADD_HTLC(sender.ref, 10000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, localOrigin(sender.ref))
333350
sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]]
334351
alice2bob.expectMsgType[UpdateAddHtlc]
@@ -340,7 +357,25 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
340357
alice2bob.expectNoMsg(200 millis)
341358
}
342359

343-
test("recv CMD_ADD_HTLC (over capacity)") { f =>
360+
test("recv CMD_ADD_HTLC (over local max accepted htlcs)") { f =>
361+
import f._
362+
val sender = TestProbe()
363+
val initialState = bob.stateData.asInstanceOf[DATA_NORMAL]
364+
assert(initialState.commitments.localParams.maxAcceptedHtlcs === 30) // Bob accepts a maximum of 30 htlcs
365+
assert(initialState.commitments.remoteParams.maxAcceptedHtlcs === 100) // Alice accepts more, but Bob will stop at 30 HTLCs
366+
for (_ <- 0 until 30) {
367+
bob ! CMD_ADD_HTLC(sender.ref, 2500000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, localOrigin(sender.ref))
368+
sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]]
369+
bob2alice.expectMsgType[UpdateAddHtlc]
370+
}
371+
val add = CMD_ADD_HTLC(sender.ref, 2500000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, localOrigin(sender.ref))
372+
bob ! add
373+
val error = TooManyAcceptedHtlcs(channelId(bob), maximum = 30)
374+
sender.expectMsg(RES_ADD_FAILED(add, error, Some(initialState.channelUpdate)))
375+
bob2alice.expectNoMsg(200 millis)
376+
}
377+
378+
test("recv CMD_ADD_HTLC (over capacity)", Tag(StateTestsTags.NoMaxHtlcValueInFlight)) { f =>
344379
import f._
345380
val sender = TestProbe()
346381
val initialState = alice.stateData.asInstanceOf[DATA_NORMAL]
@@ -405,14 +440,14 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
405440
val sender = TestProbe()
406441
val initialState = alice.stateData.asInstanceOf[DATA_NORMAL]
407442
// let's make alice send an htlc
408-
val add1 = CMD_ADD_HTLC(sender.ref, 500000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, localOrigin(sender.ref))
443+
val add1 = CMD_ADD_HTLC(sender.ref, 50000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, localOrigin(sender.ref))
409444
alice ! add1
410445
sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]]
411446
// at the same time bob initiates a closing
412447
bob ! CMD_CLOSE(sender.ref, None)
413448
sender.expectMsgType[RES_SUCCESS[CMD_CLOSE]]
414449
// this command will be received by alice right after having received the shutdown
415-
val add2 = CMD_ADD_HTLC(sender.ref, 100000000 msat, randomBytes32(), CltvExpiry(300000), TestConstants.emptyOnionPacket, localOrigin(sender.ref))
450+
val add2 = CMD_ADD_HTLC(sender.ref, 10000000 msat, randomBytes32(), CltvExpiry(300000), TestConstants.emptyOnionPacket, localOrigin(sender.ref))
416451
// messages cross
417452
alice2bob.expectMsgType[UpdateAddHtlc]
418453
alice2bob.forward(bob)
@@ -527,7 +562,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
527562
bob2blockchain.expectMsgType[WatchTxConfirmed]
528563
}
529564

530-
test("recv UpdateAddHtlc (over max inflight htlc value)") { f =>
565+
test("recv UpdateAddHtlc (over max inflight htlc value)", Tag(StateTestsTags.AliceLowMaxHtlcValueInFlight)) { f =>
531566
import f._
532567
val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
533568
alice2bob.forward(alice, UpdateAddHtlc(ByteVector32.Zeroes, 0, 151000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket))

0 commit comments

Comments
 (0)