Skip to content

Commit

Permalink
Symmetrical HTLC limits (#1828)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
t-bast authored May 28, 2021
1 parent 43a89f8 commit af618bc
Show file tree
Hide file tree
Showing 5 changed files with 70 additions and 25 deletions.
3 changes: 2 additions & 1 deletion eclair-core/src/main/resources/reference.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -246,21 +246,20 @@ 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
val error = RemoteCannotAffordFeesForNewHtlc(channelId(bob), failedAdd.amount, missing = 1360 sat, 10000 sat, 22720 sat)
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]
Expand All @@ -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]
Expand All @@ -297,21 +296,25 @@ 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)
sender.expectMsg(RES_ADD_FAILED(add, error, Some(initialState.channelUpdate)))
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]]
Expand All @@ -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]
Expand All @@ -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]
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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))
Expand Down

0 comments on commit af618bc

Please sign in to comment.