1
0
Fork 0
mirror of https://github.com/ACINQ/eclair.git synced 2025-03-13 11:35:47 +01:00

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`.
This commit is contained in:
t-bast 2025-01-15 15:50:30 +01:00
parent 8827a04349
commit 65740907f1
No known key found for this signature in database
GPG key ID: 34F377B0100ED6BB
3 changed files with 30 additions and 9 deletions

View file

@ -22,7 +22,7 @@ import akka.actor.typed.scaladsl.adapter.TypedActorRefOps
import akka.actor.typed.scaladsl.{ActorContext, Behaviors}
import akka.actor.typed.{ActorRef, Behavior}
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
import fr.acinq.bitcoin.scalacompat.{BtcDouble, ByteVector32, Satoshi, Script}
import fr.acinq.bitcoin.scalacompat.{BtcDouble, ByteVector32, Satoshi, SatoshiLong, Script}
import fr.acinq.eclair.Features.Wumbo
import fr.acinq.eclair.blockchain.OnchainPubkeyCache
import fr.acinq.eclair.channel._
@ -84,8 +84,8 @@ object OpenChannelInterceptor {
}
}
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 = {
val maxHtlcValueInFlightMsat = if (unlimitedMaxHtlcValueInFlight) {
private def computeMaxHtlcValueInFlight(nodeParams: NodeParams, fundingAmount: Satoshi, unlimitedMaxHtlcValueInFlight: Boolean): MilliSatoshi = {
if (unlimitedMaxHtlcValueInFlight) {
// We don't want to impose limits on the amount in flight, typically to allow fully emptying the channel.
21e6.btc.toMilliSatoshi
} else {
@ -94,11 +94,14 @@ object OpenChannelInterceptor {
// base it on the amount that we're contributing instead of the total funding amount.
nodeParams.channelConf.maxHtlcValueInFlightMsat.min(fundingAmount * nodeParams.channelConf.maxHtlcValueInFlightPercent / 100)
}
}
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 = {
LocalParams(
nodeParams.nodeId,
nodeParams.channelKeyManager.newFundingKeyPath(isChannelOpener), // we make sure that opener and non-opener key paths end differently
dustLimit = nodeParams.channelConf.dustLimit,
maxHtlcValueInFlightMsat = maxHtlcValueInFlightMsat,
maxHtlcValueInFlightMsat = computeMaxHtlcValueInFlight(nodeParams, fundingAmount, unlimitedMaxHtlcValueInFlight),
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
htlcMinimum = nodeParams.channelConf.htlcMinimum,
toSelfDelay = nodeParams.channelConf.toRemoteDelay, // we choose their delay
@ -142,7 +145,9 @@ private class OpenChannelInterceptor(peer: ActorRef[Any],
val channelType = request.open.channelType_opt.getOrElse(ChannelTypes.defaultFromFeatures(request.localFeatures, request.remoteFeatures, channelFlags.announceChannel))
val dualFunded = Features.canUseFeature(request.localFeatures, request.remoteFeatures, Features.DualFunding)
val upfrontShutdownScript = Features.canUseFeature(request.localFeatures, request.remoteFeatures, Features.UpfrontShutdownScript)
val localParams = createLocalParams(nodeParams, request.localFeatures, upfrontShutdownScript, channelType, isChannelOpener = true, paysCommitTxFees = true, dualFunded = dualFunded, request.open.fundingAmount, request.open.disableMaxHtlcValueInFlight)
// 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.
val expectedFundingAmount = request.open.fundingAmount + request.open.requestFunding_opt.map(_.requestedAmount).getOrElse(0 sat)
val localParams = createLocalParams(nodeParams, request.localFeatures, upfrontShutdownScript, channelType, isChannelOpener = true, paysCommitTxFees = true, dualFunded = dualFunded, expectedFundingAmount, request.open.disableMaxHtlcValueInFlight)
peer ! Peer.SpawnChannelInitiator(request.replyTo, request.open, ChannelConfig.standard, channelType, localParams)
waitForRequest()
}
@ -210,7 +215,10 @@ private class OpenChannelInterceptor(peer: ActorRef[Any],
request.open.fold(_ => None, _.requestFunding_opt) match {
case Some(requestFunding) if Features.canUseFeature(request.localFeatures, request.remoteFeatures, Features.OnTheFlyFunding) && localParams.paysCommitTxFees =>
val addFunding = LiquidityAds.AddFunding(requestFunding.requestedAmount, nodeParams.willFundRates_opt)
val accept = SpawnChannelNonInitiator(request.open, ChannelConfig.standard, channelType, Some(addFunding), localParams, request.peerConnection.toClassic)
// Now that we know how much we'll contribute to the funding transaction, we update the maxHtlcValueInFlight.
val maxHtlcValueInFlight = localParams.maxHtlcValueInFlightMsat.max(computeMaxHtlcValueInFlight(nodeParams, request.fundingAmount + addFunding.fundingAmount, unlimitedMaxHtlcValueInFlight = false))
val localParams1 = localParams.copy(maxHtlcValueInFlightMsat = maxHtlcValueInFlight)
val accept = SpawnChannelNonInitiator(request.open, ChannelConfig.standard, channelType, Some(addFunding), localParams1, request.peerConnection.toClassic)
checkNoExistingChannel(request, accept)
case _ =>
// We don't honor liquidity ads for new channels: node operators should use plugin for that.

View file

@ -31,7 +31,7 @@ import fr.acinq.eclair.channel._
import fr.acinq.eclair.channel.fsm.Channel
import fr.acinq.eclair.channel.states.ChannelStateTestsTags
import fr.acinq.eclair.io.OpenChannelInterceptor.{DefaultParams, OpenChannelInitiator, OpenChannelNonInitiator}
import fr.acinq.eclair.io.Peer.{OpenChannelResponse, OutgoingMessage, SpawnChannelNonInitiator}
import fr.acinq.eclair.io.Peer.{OpenChannelResponse, OutgoingMessage, SpawnChannelInitiator, SpawnChannelNonInitiator}
import fr.acinq.eclair.io.PeerSpec.{createOpenChannelMessage, createOpenDualFundedChannelMessage}
import fr.acinq.eclair.io.PendingChannelsRateLimiter.AddOrRejectChannel
import fr.acinq.eclair.transactions.Transactions.{ClosingTx, InputInfo}
@ -146,10 +146,23 @@ class OpenChannelInterceptorSpec extends ScalaTestWithActorTestKit(ConfigFactory
val result = peer.expectMessageType[SpawnChannelNonInitiator]
assert(!result.localParams.isChannelOpener)
assert(result.localParams.paysCommitTxFees)
assert(result.localParams.maxHtlcValueInFlightMsat == 500_000_000.msat)
assert(result.addFunding_opt.map(_.fundingAmount).contains(250_000 sat))
assert(result.addFunding_opt.flatMap(_.rates_opt).contains(TestConstants.defaultLiquidityRates))
}
test("expect remote funding contribution in max_htlc_value_in_flight") { f =>
import f._
val probe = TestProbe[Any]()
val requestFunding = LiquidityAds.RequestFunding(150_000 sat, LiquidityAds.FundingRate(0 sat, 200_000 sat, 400, 100, 0 sat, 0 sat), LiquidityAds.PaymentDetails.FromChannelBalance)
val openChannelInitiator = OpenChannelInitiator(probe.ref, remoteNodeId, Peer.OpenChannel(remoteNodeId, 300_000 sat, None, None, None, None, Some(requestFunding), None, None), defaultFeatures, defaultFeatures)
openChannelInterceptor ! openChannelInitiator
val result = peer.expectMessageType[SpawnChannelInitiator]
assert(result.cmd == openChannelInitiator.open)
assert(result.localParams.maxHtlcValueInFlightMsat == 450_000_000.msat)
}
test("continue channel open if no interceptor plugin registered and pending channels rate limiter accepts it") { f =>
import f._

View file

@ -803,11 +803,11 @@ object PeerSpec {
}
def createOpenChannelMessage(openTlv: TlvStream[OpenChannelTlv] = TlvStream.empty): protocol.OpenChannel = {
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)
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)
}
def createOpenDualFundedChannelMessage(): protocol.OpenDualFundedChannel = {
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))
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))
}
}