Skip to content

Commit 6574090

Browse files
committed
Use remote funding when setting max_htlc_value_in_flight
When using dual-funding, both peers may contribute to the funding amount and it usually cannot be known ahead of time how much the remote peer will contribute, which usually leads to underestimating the channel capacity and thus using a lower `max_htlc_value_in_flight` than what should be used. However, when we use liquidity ads, we will: - contribute to the funding transaction if we're not the opener - cancel the funding attempt if we're the opener and our peers doesn't contribute at least the amount we requested So in that case, we can use a better estimate of the channel capacity when computing our `max_htlc_value_in_flight`.
1 parent 8827a04 commit 6574090

File tree

3 files changed

+30
-9
lines changed

3 files changed

+30
-9
lines changed

eclair-core/src/main/scala/fr/acinq/eclair/io/OpenChannelInterceptor.scala

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import akka.actor.typed.scaladsl.adapter.TypedActorRefOps
2222
import akka.actor.typed.scaladsl.{ActorContext, Behaviors}
2323
import akka.actor.typed.{ActorRef, Behavior}
2424
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
25-
import fr.acinq.bitcoin.scalacompat.{BtcDouble, ByteVector32, Satoshi, Script}
25+
import fr.acinq.bitcoin.scalacompat.{BtcDouble, ByteVector32, Satoshi, SatoshiLong, Script}
2626
import fr.acinq.eclair.Features.Wumbo
2727
import fr.acinq.eclair.blockchain.OnchainPubkeyCache
2828
import fr.acinq.eclair.channel._
@@ -84,8 +84,8 @@ object OpenChannelInterceptor {
8484
}
8585
}
8686

87-
def makeChannelParams(nodeParams: NodeParams, initFeatures: Features[InitFeature], upfrontShutdownScript_opt: Option[ByteVector], walletStaticPaymentBasepoint_opt: Option[PublicKey], isChannelOpener: Boolean, paysCommitTxFees: Boolean, dualFunded: Boolean, fundingAmount: Satoshi, unlimitedMaxHtlcValueInFlight: Boolean): LocalParams = {
88-
val maxHtlcValueInFlightMsat = if (unlimitedMaxHtlcValueInFlight) {
87+
private def computeMaxHtlcValueInFlight(nodeParams: NodeParams, fundingAmount: Satoshi, unlimitedMaxHtlcValueInFlight: Boolean): MilliSatoshi = {
88+
if (unlimitedMaxHtlcValueInFlight) {
8989
// We don't want to impose limits on the amount in flight, typically to allow fully emptying the channel.
9090
21e6.btc.toMilliSatoshi
9191
} else {
@@ -94,11 +94,14 @@ object OpenChannelInterceptor {
9494
// base it on the amount that we're contributing instead of the total funding amount.
9595
nodeParams.channelConf.maxHtlcValueInFlightMsat.min(fundingAmount * nodeParams.channelConf.maxHtlcValueInFlightPercent / 100)
9696
}
97+
}
98+
99+
def makeChannelParams(nodeParams: NodeParams, initFeatures: Features[InitFeature], upfrontShutdownScript_opt: Option[ByteVector], walletStaticPaymentBasepoint_opt: Option[PublicKey], isChannelOpener: Boolean, paysCommitTxFees: Boolean, dualFunded: Boolean, fundingAmount: Satoshi, unlimitedMaxHtlcValueInFlight: Boolean): LocalParams = {
97100
LocalParams(
98101
nodeParams.nodeId,
99102
nodeParams.channelKeyManager.newFundingKeyPath(isChannelOpener), // we make sure that opener and non-opener key paths end differently
100103
dustLimit = nodeParams.channelConf.dustLimit,
101-
maxHtlcValueInFlightMsat = maxHtlcValueInFlightMsat,
104+
maxHtlcValueInFlightMsat = computeMaxHtlcValueInFlight(nodeParams, fundingAmount, unlimitedMaxHtlcValueInFlight),
102105
initialRequestedChannelReserve_opt = if (dualFunded) None else Some((fundingAmount * nodeParams.channelConf.reserveToFundingRatio).max(nodeParams.channelConf.dustLimit)), // BOLT #2: make sure that our reserve is above our dust limit
103106
htlcMinimum = nodeParams.channelConf.htlcMinimum,
104107
toSelfDelay = nodeParams.channelConf.toRemoteDelay, // we choose their delay
@@ -142,7 +145,9 @@ private class OpenChannelInterceptor(peer: ActorRef[Any],
142145
val channelType = request.open.channelType_opt.getOrElse(ChannelTypes.defaultFromFeatures(request.localFeatures, request.remoteFeatures, channelFlags.announceChannel))
143146
val dualFunded = Features.canUseFeature(request.localFeatures, request.remoteFeatures, Features.DualFunding)
144147
val upfrontShutdownScript = Features.canUseFeature(request.localFeatures, request.remoteFeatures, Features.UpfrontShutdownScript)
145-
val localParams = createLocalParams(nodeParams, request.localFeatures, upfrontShutdownScript, channelType, isChannelOpener = true, paysCommitTxFees = true, dualFunded = dualFunded, request.open.fundingAmount, request.open.disableMaxHtlcValueInFlight)
148+
// If we're purchasing liquidity, we expect our peer to contribute at least the amount we're purchasing, otherwise we'll cancel the funding attempt.
149+
val expectedFundingAmount = request.open.fundingAmount + request.open.requestFunding_opt.map(_.requestedAmount).getOrElse(0 sat)
150+
val localParams = createLocalParams(nodeParams, request.localFeatures, upfrontShutdownScript, channelType, isChannelOpener = true, paysCommitTxFees = true, dualFunded = dualFunded, expectedFundingAmount, request.open.disableMaxHtlcValueInFlight)
146151
peer ! Peer.SpawnChannelInitiator(request.replyTo, request.open, ChannelConfig.standard, channelType, localParams)
147152
waitForRequest()
148153
}
@@ -210,7 +215,10 @@ private class OpenChannelInterceptor(peer: ActorRef[Any],
210215
request.open.fold(_ => None, _.requestFunding_opt) match {
211216
case Some(requestFunding) if Features.canUseFeature(request.localFeatures, request.remoteFeatures, Features.OnTheFlyFunding) && localParams.paysCommitTxFees =>
212217
val addFunding = LiquidityAds.AddFunding(requestFunding.requestedAmount, nodeParams.willFundRates_opt)
213-
val accept = SpawnChannelNonInitiator(request.open, ChannelConfig.standard, channelType, Some(addFunding), localParams, request.peerConnection.toClassic)
218+
// Now that we know how much we'll contribute to the funding transaction, we update the maxHtlcValueInFlight.
219+
val maxHtlcValueInFlight = localParams.maxHtlcValueInFlightMsat.max(computeMaxHtlcValueInFlight(nodeParams, request.fundingAmount + addFunding.fundingAmount, unlimitedMaxHtlcValueInFlight = false))
220+
val localParams1 = localParams.copy(maxHtlcValueInFlightMsat = maxHtlcValueInFlight)
221+
val accept = SpawnChannelNonInitiator(request.open, ChannelConfig.standard, channelType, Some(addFunding), localParams1, request.peerConnection.toClassic)
214222
checkNoExistingChannel(request, accept)
215223
case _ =>
216224
// We don't honor liquidity ads for new channels: node operators should use plugin for that.

eclair-core/src/test/scala/fr/acinq/eclair/io/OpenChannelInterceptorSpec.scala

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import fr.acinq.eclair.channel._
3131
import fr.acinq.eclair.channel.fsm.Channel
3232
import fr.acinq.eclair.channel.states.ChannelStateTestsTags
3333
import fr.acinq.eclair.io.OpenChannelInterceptor.{DefaultParams, OpenChannelInitiator, OpenChannelNonInitiator}
34-
import fr.acinq.eclair.io.Peer.{OpenChannelResponse, OutgoingMessage, SpawnChannelNonInitiator}
34+
import fr.acinq.eclair.io.Peer.{OpenChannelResponse, OutgoingMessage, SpawnChannelInitiator, SpawnChannelNonInitiator}
3535
import fr.acinq.eclair.io.PeerSpec.{createOpenChannelMessage, createOpenDualFundedChannelMessage}
3636
import fr.acinq.eclair.io.PendingChannelsRateLimiter.AddOrRejectChannel
3737
import fr.acinq.eclair.transactions.Transactions.{ClosingTx, InputInfo}
@@ -146,10 +146,23 @@ class OpenChannelInterceptorSpec extends ScalaTestWithActorTestKit(ConfigFactory
146146
val result = peer.expectMessageType[SpawnChannelNonInitiator]
147147
assert(!result.localParams.isChannelOpener)
148148
assert(result.localParams.paysCommitTxFees)
149+
assert(result.localParams.maxHtlcValueInFlightMsat == 500_000_000.msat)
149150
assert(result.addFunding_opt.map(_.fundingAmount).contains(250_000 sat))
150151
assert(result.addFunding_opt.flatMap(_.rates_opt).contains(TestConstants.defaultLiquidityRates))
151152
}
152153

154+
test("expect remote funding contribution in max_htlc_value_in_flight") { f =>
155+
import f._
156+
157+
val probe = TestProbe[Any]()
158+
val requestFunding = LiquidityAds.RequestFunding(150_000 sat, LiquidityAds.FundingRate(0 sat, 200_000 sat, 400, 100, 0 sat, 0 sat), LiquidityAds.PaymentDetails.FromChannelBalance)
159+
val openChannelInitiator = OpenChannelInitiator(probe.ref, remoteNodeId, Peer.OpenChannel(remoteNodeId, 300_000 sat, None, None, None, None, Some(requestFunding), None, None), defaultFeatures, defaultFeatures)
160+
openChannelInterceptor ! openChannelInitiator
161+
val result = peer.expectMessageType[SpawnChannelInitiator]
162+
assert(result.cmd == openChannelInitiator.open)
163+
assert(result.localParams.maxHtlcValueInFlightMsat == 450_000_000.msat)
164+
}
165+
153166
test("continue channel open if no interceptor plugin registered and pending channels rate limiter accepts it") { f =>
154167
import f._
155168

eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -803,11 +803,11 @@ object PeerSpec {
803803
}
804804

805805
def createOpenChannelMessage(openTlv: TlvStream[OpenChannelTlv] = TlvStream.empty): protocol.OpenChannel = {
806-
protocol.OpenChannel(Block.RegtestGenesisBlock.hash, randomBytes32(), 25000 sat, 0 msat, 483 sat, UInt64(100), 1000 sat, 1 msat, TestConstants.feeratePerKw, CltvExpiryDelta(144), 10, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, ChannelFlags(announceChannel = false), openTlv)
806+
protocol.OpenChannel(Block.RegtestGenesisBlock.hash, randomBytes32(), 250_000 sat, 0 msat, 483 sat, UInt64(100), 1000 sat, 1 msat, TestConstants.feeratePerKw, CltvExpiryDelta(144), 10, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, ChannelFlags(announceChannel = false), openTlv)
807807
}
808808

809809
def createOpenDualFundedChannelMessage(): protocol.OpenDualFundedChannel = {
810-
protocol.OpenDualFundedChannel(Block.RegtestGenesisBlock.hash, randomBytes32(), TestConstants.feeratePerKw, TestConstants.anchorOutputsFeeratePerKw, 25000 sat, 483 sat, UInt64(100), 1 msat, CltvExpiryDelta(144), 10, 0, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, ChannelFlags(announceChannel = false))
810+
protocol.OpenDualFundedChannel(Block.RegtestGenesisBlock.hash, randomBytes32(), TestConstants.feeratePerKw, TestConstants.anchorOutputsFeeratePerKw, 250_000 sat, 483 sat, UInt64(100), 1 msat, CltvExpiryDelta(144), 10, 0, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, ChannelFlags(announceChannel = false))
811811
}
812812

813813
}

0 commit comments

Comments
 (0)