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:
parent
43a89f8659
commit
af618bc44f
5 changed files with 70 additions and 25 deletions
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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))
|
||||
|
|
Loading…
Add table
Reference in a new issue