1
0
Fork 0
mirror of https://github.com/ACINQ/eclair.git synced 2025-02-24 06:47:46 +01:00

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.
This commit is contained in:
Bastien Teinturier 2021-05-28 17:13:37 +02:00 committed by GitHub
parent 43a89f8659
commit af618bc44f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 70 additions and 25 deletions

View file

@ -65,8 +65,9 @@ eclair {
dust-limit-satoshis = 546
max-remote-dust-limit-satoshis = 600
max-htlc-value-in-flight-msat = 5000000000 // 50 mBTC
htlc-minimum-msat = 1
// 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
max-htlc-value-in-flight-msat = 5000000000 // 50 mBTC
max-accepted-htlcs = 30
reserve-to-funding-ratio = 0.01 // recommended by BOLT #2

View file

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

View file

@ -106,7 +106,7 @@ object TestConstants {
defaultFeerateTolerance = FeerateTolerance(0.5, 8.0, anchorOutputsFeeratePerKw),
perNodeFeerateTolerance = Map.empty
),
maxHtlcValueInFlightMsat = UInt64(150000000),
maxHtlcValueInFlightMsat = UInt64(500000000),
maxAcceptedHtlcs = 100,
expiryDelta = CltvExpiryDelta(144),
fulfillSafetyBeforeTimeout = CltvExpiryDelta(6),

View file

@ -68,6 +68,10 @@ object StateTestsTags {
val ChannelsPublic = "channels_public"
/** If set, no amount will be pushed when opening a channel (by default we push a small amount). */
val NoPushMsat = "no_push_msat"
/** If set, max-htlc-value-in-flight will be set to the highest possible value for Alice and Bob. */
val NoMaxHtlcValueInFlight = "no_max_htlc_value_in_flight"
/** If set, max-htlc-value-in-flight will be set to a low value for Alice. */
val AliceLowMaxHtlcValueInFlight = "alice_low_max_htlc_value_in_flight"
}
trait StateTestsHelperMethods extends TestKitBase {
@ -127,8 +131,13 @@ trait StateTestsHelperMethods extends TestKitBase {
).reduce(_ | _)
val channelFlags = if (tags.contains(StateTestsTags.ChannelsPublic)) ChannelFlags.AnnounceChannel else ChannelFlags.Empty
val aliceParams = setChannelFeatures(Alice.channelParams, tags).modify(_.walletStaticPaymentBasepoint).setToIf(channelVersion.paysDirectlyToWallet)(Some(Helpers.getWalletPaymentBasepoint(wallet)))
val bobParams = setChannelFeatures(Bob.channelParams, tags).modify(_.walletStaticPaymentBasepoint).setToIf(channelVersion.paysDirectlyToWallet)(Some(Helpers.getWalletPaymentBasepoint(wallet)))
val aliceParams = setChannelFeatures(Alice.channelParams, tags)
.modify(_.walletStaticPaymentBasepoint).setToIf(channelVersion.paysDirectlyToWallet)(Some(Helpers.getWalletPaymentBasepoint(wallet)))
.modify(_.maxHtlcValueInFlightMsat).setToIf(tags.contains(StateTestsTags.NoMaxHtlcValueInFlight))(UInt64.MaxValue)
.modify(_.maxHtlcValueInFlightMsat).setToIf(tags.contains(StateTestsTags.AliceLowMaxHtlcValueInFlight))(UInt64(150000000))
val bobParams = setChannelFeatures(Bob.channelParams, tags)
.modify(_.walletStaticPaymentBasepoint).setToIf(channelVersion.paysDirectlyToWallet)(Some(Helpers.getWalletPaymentBasepoint(wallet)))
.modify(_.maxHtlcValueInFlightMsat).setToIf(tags.contains(StateTestsTags.NoMaxHtlcValueInFlight))(UInt64.MaxValue)
val initialFeeratePerKw = if (tags.contains(StateTestsTags.AnchorOutputs)) TestConstants.anchorOutputsFeeratePerKw else TestConstants.feeratePerKw
val (fundingSatoshis, pushMsat) = if (tags.contains(StateTestsTags.NoPushMsat)) {
(TestConstants.fundingSatoshis, 0.msat)

View file

@ -94,7 +94,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
val sender = TestProbe()
val h = randomBytes32()
for (i <- 0 until 10) {
alice ! CMD_ADD_HTLC(sender.ref, 50000000 msat, h, CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, localOrigin(sender.ref))
alice ! CMD_ADD_HTLC(sender.ref, 500000 msat, h, CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, localOrigin(sender.ref))
sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]]
val htlc = alice2bob.expectMsgType[UpdateAddHtlc]
assert(htlc.id == i && htlc.paymentHash == h)
@ -237,7 +237,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
bob2alice.expectNoMsg(200 millis)
}
test("recv CMD_ADD_HTLC (HTLC dips into remote funder fee reserve)") { f =>
test("recv CMD_ADD_HTLC (HTLC dips into remote funder fee reserve)", Tag(StateTestsTags.NoMaxHtlcValueInFlight)) { f =>
import f._
val sender = TestProbe()
addHtlc(758640000 msat, alice, bob, alice2bob, bob2alice)
@ -246,13 +246,12 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
// actual test begins
// at this point alice has the minimal amount to sustain a channel
// alice maintains an extra reserve to accommodate for a few more HTLCs, so the first two HTLCs should be allowed
// alice maintains an extra reserve to accommodate for a few more HTLCs, so the first few HTLCs should be allowed
for (_ <- 1 to 7) {
bob ! CMD_ADD_HTLC(sender.ref, 12000000 msat, randomBytes32(), CltvExpiry(400144), TestConstants.emptyOnionPacket, localOrigin(sender.ref))
sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]]
}
// but this one will dip alice below her reserve: we must wait for the two previous HTLCs to settle before sending any more
// but this one will dip alice below her reserve: we must wait for the previous HTLCs to settle before sending any more
val failedAdd = CMD_ADD_HTLC(sender.ref, 11000000 msat, randomBytes32(), CltvExpiry(400144), TestConstants.emptyOnionPacket, localOrigin(sender.ref))
bob ! failedAdd
@ -260,7 +259,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
sender.expectMsg(RES_ADD_FAILED(failedAdd, error, Some(bob.stateData.asInstanceOf[DATA_NORMAL].channelUpdate)))
}
test("recv CMD_ADD_HTLC (insufficient funds w/ pending htlcs and 0 balance)") { f =>
test("recv CMD_ADD_HTLC (insufficient funds w/ pending htlcs and 0 balance)", Tag(StateTestsTags.NoMaxHtlcValueInFlight)) { f =>
import f._
val sender = TestProbe()
val initialState = alice.stateData.asInstanceOf[DATA_NORMAL]
@ -280,7 +279,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
alice2bob.expectNoMsg(200 millis)
}
test("recv CMD_ADD_HTLC (insufficient funds w/ pending htlcs 2/2)") { f =>
test("recv CMD_ADD_HTLC (insufficient funds w/ pending htlcs 2/2)", Tag(StateTestsTags.NoMaxHtlcValueInFlight)) { f =>
import f._
val sender = TestProbe()
val initialState = alice.stateData.asInstanceOf[DATA_NORMAL]
@ -297,10 +296,12 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
alice2bob.expectNoMsg(200 millis)
}
test("recv CMD_ADD_HTLC (over max inflight htlc value)") { f =>
test("recv CMD_ADD_HTLC (over remote max inflight htlc value)", Tag(StateTestsTags.AliceLowMaxHtlcValueInFlight)) { f =>
import f._
val sender = TestProbe()
val initialState = bob.stateData.asInstanceOf[DATA_NORMAL]
assert(initialState.commitments.localParams.maxHtlcValueInFlightMsat === UInt64.MaxValue)
assert(initialState.commitments.remoteParams.maxHtlcValueInFlightMsat === UInt64(150000000))
val add = CMD_ADD_HTLC(sender.ref, 151000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, localOrigin(sender.ref))
bob ! add
val error = HtlcValueTooHighInFlight(channelId(bob), maximum = 150000000, actual = 151000000 msat)
@ -308,10 +309,12 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
bob2alice.expectNoMsg(200 millis)
}
test("recv CMD_ADD_HTLC (over max inflight htlc value with duplicate amounts)") { f =>
test("recv CMD_ADD_HTLC (over remote max inflight htlc value with duplicate amounts)", Tag(StateTestsTags.AliceLowMaxHtlcValueInFlight)) { f =>
import f._
val sender = TestProbe()
val initialState = bob.stateData.asInstanceOf[DATA_NORMAL]
assert(initialState.commitments.localParams.maxHtlcValueInFlightMsat === UInt64.MaxValue)
assert(initialState.commitments.remoteParams.maxHtlcValueInFlightMsat === UInt64(150000000))
val add = CMD_ADD_HTLC(sender.ref, 75500000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, localOrigin(sender.ref))
bob ! add
sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]]
@ -323,12 +326,26 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
bob2alice.expectNoMsg(200 millis)
}
test("recv CMD_ADD_HTLC (over max accepted htlcs)") { f =>
test("recv CMD_ADD_HTLC (over local max inflight htlc value)", Tag(StateTestsTags.AliceLowMaxHtlcValueInFlight)) { f =>
import f._
val sender = TestProbe()
val initialState = alice.stateData.asInstanceOf[DATA_NORMAL]
// Bob accepts a maximum of 30 htlcs
for (i <- 0 until 30) {
assert(initialState.commitments.localParams.maxHtlcValueInFlightMsat === UInt64(150000000))
assert(initialState.commitments.remoteParams.maxHtlcValueInFlightMsat === UInt64.MaxValue)
val add = CMD_ADD_HTLC(sender.ref, 151000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, localOrigin(sender.ref))
alice ! add
val error = HtlcValueTooHighInFlight(channelId(alice), maximum = 150000000, actual = 151000000 msat)
sender.expectMsg(RES_ADD_FAILED(add, error, Some(initialState.channelUpdate)))
alice2bob.expectNoMsg(200 millis)
}
test("recv CMD_ADD_HTLC (over remote max accepted htlcs)") { f =>
import f._
val sender = TestProbe()
val initialState = alice.stateData.asInstanceOf[DATA_NORMAL]
assert(initialState.commitments.localParams.maxAcceptedHtlcs === 100)
assert(initialState.commitments.remoteParams.maxAcceptedHtlcs === 30) // Bob accepts a maximum of 30 htlcs
for (_ <- 0 until 30) {
alice ! CMD_ADD_HTLC(sender.ref, 10000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, localOrigin(sender.ref))
sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]]
alice2bob.expectMsgType[UpdateAddHtlc]
@ -340,7 +357,25 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
alice2bob.expectNoMsg(200 millis)
}
test("recv CMD_ADD_HTLC (over capacity)") { f =>
test("recv CMD_ADD_HTLC (over local max accepted htlcs)") { f =>
import f._
val sender = TestProbe()
val initialState = bob.stateData.asInstanceOf[DATA_NORMAL]
assert(initialState.commitments.localParams.maxAcceptedHtlcs === 30) // Bob accepts a maximum of 30 htlcs
assert(initialState.commitments.remoteParams.maxAcceptedHtlcs === 100) // Alice accepts more, but Bob will stop at 30 HTLCs
for (_ <- 0 until 30) {
bob ! CMD_ADD_HTLC(sender.ref, 2500000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, localOrigin(sender.ref))
sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]]
bob2alice.expectMsgType[UpdateAddHtlc]
}
val add = CMD_ADD_HTLC(sender.ref, 2500000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, localOrigin(sender.ref))
bob ! add
val error = TooManyAcceptedHtlcs(channelId(bob), maximum = 30)
sender.expectMsg(RES_ADD_FAILED(add, error, Some(initialState.channelUpdate)))
bob2alice.expectNoMsg(200 millis)
}
test("recv CMD_ADD_HTLC (over capacity)", Tag(StateTestsTags.NoMaxHtlcValueInFlight)) { f =>
import f._
val sender = TestProbe()
val initialState = alice.stateData.asInstanceOf[DATA_NORMAL]
@ -405,14 +440,14 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
val sender = TestProbe()
val initialState = alice.stateData.asInstanceOf[DATA_NORMAL]
// let's make alice send an htlc
val add1 = CMD_ADD_HTLC(sender.ref, 500000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, localOrigin(sender.ref))
val add1 = CMD_ADD_HTLC(sender.ref, 50000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, localOrigin(sender.ref))
alice ! add1
sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]]
// at the same time bob initiates a closing
bob ! CMD_CLOSE(sender.ref, None)
sender.expectMsgType[RES_SUCCESS[CMD_CLOSE]]
// this command will be received by alice right after having received the shutdown
val add2 = CMD_ADD_HTLC(sender.ref, 100000000 msat, randomBytes32(), CltvExpiry(300000), TestConstants.emptyOnionPacket, localOrigin(sender.ref))
val add2 = CMD_ADD_HTLC(sender.ref, 10000000 msat, randomBytes32(), CltvExpiry(300000), TestConstants.emptyOnionPacket, localOrigin(sender.ref))
// messages cross
alice2bob.expectMsgType[UpdateAddHtlc]
alice2bob.forward(bob)
@ -527,7 +562,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
bob2blockchain.expectMsgType[WatchTxConfirmed]
}
test("recv UpdateAddHtlc (over max inflight htlc value)") { f =>
test("recv UpdateAddHtlc (over max inflight htlc value)", Tag(StateTestsTags.AliceLowMaxHtlcValueInFlight)) { f =>
import f._
val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
alice2bob.forward(alice, UpdateAddHtlc(ByteVector32.Zeroes, 0, 151000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket))