Skip to content

Commit aff16b2

Browse files
authored
Prioritize private channels when relaying payments (#3248)
When relaying payments, we want to select private channels first and keep as much liquidity available as possible in public channels, to ensure that we don't send a `channel_update` that would otherwise disable the public channel (and thus make the private channels also unusable since they aren't visible by path-finding algorithms) or limit the `htlc_maximum_msat` of this public channel (which also indirectly applies to private channels). We also change the order in which we select channels that have the same visibility: we prioritize channels with smaller balances, to ensure that we keep our larger balances for larger payments. When balances are equal, we prioritize the largest channel, which creates a larger inbound liquidity to allow receiving larger payments.
1 parent d735e0b commit aff16b2

File tree

2 files changed

+43
-40
lines changed

2 files changed

+43
-40
lines changed

eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelay.scala

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -382,24 +382,20 @@ class ChannelRelay private(nodeParams: NodeParams,
382382
})
383383
(channel, relayResult)
384384
}
385-
.collect {
386-
// we only keep channels that have enough balance to handle this payment
387-
case (channel, _: RelaySuccess) if channel.commitments.availableBalanceForSend > r.amountToForward => channel
388-
}
385+
// we only keep channels that have enough balance to handle this payment
386+
.collect { case (channel, _: RelaySuccess) if channel.commitments.availableBalanceForSend > r.amountToForward => channel }
389387
.toList // needed for ordering
390-
// we want to use the channel with:
391-
// - the lowest available capacity to ensure we keep high-capacity channels for big payments
392-
// - the lowest available balance to increase our incoming liquidity
393-
.sortBy { channel => (channel.commitments.latest.capacity, channel.commitments.availableBalanceForSend) }
394-
.headOption match {
395-
case Some(channel) =>
396-
if (requestedChannelId_opt.contains(channel.channelId)) {
397-
context.log.debug("requested short channel id is our preferred channel")
398-
Some(channel)
399-
} else {
400-
context.log.debug("replacing requestedShortChannelId={} by preferredShortChannelId={} with availableBalanceMsat={}", requestedShortChannelId_opt, channel.channelUpdate.shortChannelId, channel.commitments.availableBalanceForSend)
401-
Some(channel)
402-
}
388+
.sortWith {
389+
// we always prioritize private channels to avoid exhausting our public channels and disabling them or lowering their htlc_maximum_msat
390+
case (channel1, channel2) if channel1.commitments.announceChannel != channel2.commitments.announceChannel => !channel1.commitments.announceChannel
391+
// otherwise, we use the channel with the smallest capacity to ensure we keep high-capacity channels enabled
392+
// (if we ran out of liquidity in a large channels, we would send a channel_update to disable it, which would
393+
// negatively impact our score in path-finding algorithms)
394+
case (channel1, channel2) if channel1.commitments.capacity != channel2.commitments.capacity => channel1.commitments.capacity <= channel2.commitments.capacity
395+
// otherwise, we use the channel with the smallest balance, to ensure we keep higher balances for larger payments
396+
case (channel1, channel2) => channel1.commitments.availableBalanceForSend <= channel2.commitments.availableBalanceForSend
397+
}.headOption match {
398+
case Some(channel) => Some(channel)
403399
case None =>
404400
val requestedChannel_opt = requestedChannelId_opt.flatMap(channels.get)
405401
requestedChannel_opt match {

eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/ChannelRelayerSpec.scala

Lines changed: 30 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -575,31 +575,36 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a
575575
import f._
576576

577577
/** This is just a simplified helper function with random values for fields we are not using here */
578-
def dummyLocalUpdate(shortChannelId: RealShortChannelId, remoteNodeId: PublicKey, availableBalanceForSend: MilliSatoshi, capacity: Satoshi) = {
578+
def dummyLocalUpdate(shortChannelId: RealShortChannelId, remoteNodeId: PublicKey, availableBalanceForSend: MilliSatoshi, capacity: Satoshi, isPublic: Boolean = true): LocalChannelUpdate = {
579579
val channelId = randomBytes32()
580580
val update = Announcements.makeChannelUpdate(Block.RegtestGenesisBlock.hash, randomKey(), remoteNodeId, shortChannelId, CltvExpiryDelta(10), 100 msat, 1000 msat, 100, capacity.toMilliSatoshi)
581-
val ann = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, shortChannelId, TestConstants.Alice.nodeParams.nodeId, outgoingNodeId, randomKey().publicKey, randomKey().publicKey, randomBytes64(), randomBytes64(), randomBytes64(), randomBytes64())
582-
val commitments = PaymentPacketSpec.makeCommitments(channelId, availableBalanceForSend, testCapacity = capacity, announcement_opt = Some(ann))
581+
val ann_opt = if (isPublic) {
582+
Some(Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, shortChannelId, TestConstants.Alice.nodeParams.nodeId, outgoingNodeId, randomKey().publicKey, randomKey().publicKey, randomBytes64(), randomBytes64(), randomBytes64(), randomBytes64()))
583+
} else {
584+
None
585+
}
586+
val commitments = PaymentPacketSpec.makeCommitments(channelId, availableBalanceForSend, testCapacity = capacity, announcement_opt = ann_opt)
583587
val aliases = ShortIdAliases(localAlias = ShortChannelId.generateLocalAlias(), remoteAlias_opt = None)
584-
LocalChannelUpdate(null, channelId, aliases, remoteNodeId, Some(AnnouncedCommitment(commitments.latest.commitment, ann)), update, commitments)
588+
LocalChannelUpdate(null, channelId, aliases, remoteNodeId, ann_opt.map(ann => AnnouncedCommitment(commitments.latest.commitment, ann)), update, commitments)
585589
}
586590

587591
val (a, b) = (randomKey().publicKey, randomKey().publicKey)
588592

589593
val channelUpdates = Map(
590-
ShortChannelId(11111) -> dummyLocalUpdate(RealShortChannelId(11111), a, 100000000 msat, 200000 sat),
591-
ShortChannelId(12345) -> dummyLocalUpdate(RealShortChannelId(12345), a, 10000000 msat, 200000 sat),
592-
ShortChannelId(22222) -> dummyLocalUpdate(RealShortChannelId(22222), a, 10000000 msat, 100000 sat),
593-
ShortChannelId(22223) -> dummyLocalUpdate(RealShortChannelId(22223), a, 9000000 msat, 50000 sat),
594-
ShortChannelId(33333) -> dummyLocalUpdate(RealShortChannelId(33333), a, 100000 msat, 50000 sat),
595-
ShortChannelId(44444) -> dummyLocalUpdate(RealShortChannelId(44444), b, 1000000 msat, 10000 sat),
594+
ShortChannelId(11111) -> dummyLocalUpdate(RealShortChannelId(11111), a, 100_000_000 msat, 200_000 sat),
595+
ShortChannelId(12345) -> dummyLocalUpdate(RealShortChannelId(12345), a, 10_000_000 msat, 200_000 sat),
596+
ShortChannelId(22222) -> dummyLocalUpdate(RealShortChannelId(22222), a, 10_000_000 msat, 100_000 sat),
597+
ShortChannelId(22223) -> dummyLocalUpdate(RealShortChannelId(22223), a, 9_000_000 msat, 50_000 sat),
598+
ShortChannelId(33333) -> dummyLocalUpdate(RealShortChannelId(33333), a, 100_000 msat, 50_000 sat),
599+
ShortChannelId(33334) -> dummyLocalUpdate(RealShortChannelId(33334), a, 150_000 msat, 75_000 sat, isPublic = false),
600+
ShortChannelId(44444) -> dummyLocalUpdate(RealShortChannelId(44444), b, 1_000_000 msat, 10_000 sat),
596601
)
597602

598603
channelUpdates.values.foreach(u => channelRelayer ! WrappedLocalChannelUpdate(u))
599604

600605
{
601-
val payload = ChannelRelay.Standard(ShortChannelId(12345), 998900 msat, CltvExpiry(60), upgradeAccountability = false)
602-
val r = createValidIncomingPacket(payload, 1000000 msat, CltvExpiry(70))
606+
val payload = ChannelRelay.Standard(ShortChannelId(12345), 998_900 msat, CltvExpiry(60), upgradeAccountability = false)
607+
val r = createValidIncomingPacket(payload, 1_000_000 msat, CltvExpiry(70))
603608
channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId, 0.1)
604609
receiveConfidence(Reputation.Score(1.0, accountable = false))
605610
// select the channel to the same node, with the lowest capacity and balance but still high enough to handle the payment
@@ -618,41 +623,43 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a
618623
expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(TemporaryChannelFailure(Some(channelUpdates(ShortChannelId(12345)).channelUpdate))), None, commit = true))
619624
}
620625
{
621-
// higher amount payment (have to increased incoming htlc amount for fees to be sufficient)
622-
val payload = ChannelRelay.Standard(ShortChannelId(12345), 50000000 msat, CltvExpiry(60), upgradeAccountability = false)
623-
val r = createValidIncomingPacket(payload, 60000000 msat, CltvExpiry(70))
626+
// higher amount payment (have to increase incoming htlc amount for fees to be sufficient)
627+
val payload = ChannelRelay.Standard(ShortChannelId(12345), 50_000_000 msat, CltvExpiry(60), upgradeAccountability = false)
628+
val r = createValidIncomingPacket(payload, 60_000_000 msat, CltvExpiry(70))
624629
channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId, 0.1)
625630
receiveConfidence(Reputation.Score(1.0, accountable = false))
626631
expectFwdAdd(register, channelUpdates(ShortChannelId(11111)).channelId, r.amountToForward, r.outgoingCltv, outAccountable = false).message
627632
}
628633
{
629-
// lower amount payment
634+
// lower payment amount, which adds more candidate channels: we prioritize private channels regardless of capacity
630635
val payload = ChannelRelay.Standard(ShortChannelId(12345), 1000 msat, CltvExpiry(60), upgradeAccountability = false)
631-
val r = createValidIncomingPacket(payload, 60000000 msat, CltvExpiry(70))
636+
val r = createValidIncomingPacket(payload, 60_000_000 msat, CltvExpiry(70))
632637
channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId, 0.1)
633638
receiveConfidence(Reputation.Score(1.0, accountable = false))
639+
val cmd1 = expectFwdAdd(register, channelUpdates(ShortChannelId(33334)).channelId, r.amountToForward, r.outgoingCltv, outAccountable = false).message
640+
cmd1.replyTo ! RES_ADD_FAILED(cmd1, ChannelUnavailable(randomBytes32()), None)
634641
expectFwdAdd(register, channelUpdates(ShortChannelId(33333)).channelId, r.amountToForward, r.outgoingCltv, outAccountable = false).message
635642
}
636643
{
637644
// payment too high, no suitable channel found, we keep the requested one
638-
val payload = ChannelRelay.Standard(ShortChannelId(12345), 1000000000 msat, CltvExpiry(60), upgradeAccountability = false)
639-
val r = createValidIncomingPacket(payload, 1010000000 msat, CltvExpiry(70))
645+
val payload = ChannelRelay.Standard(ShortChannelId(12345), 1_000_000_000 msat, CltvExpiry(60), upgradeAccountability = false)
646+
val r = createValidIncomingPacket(payload, 1_010_000_000 msat, CltvExpiry(70))
640647
channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId, 0.1)
641648
receiveConfidence(Reputation.Score(1.0, accountable = false))
642649
expectFwdAdd(register, channelUpdates(ShortChannelId(12345)).channelId, r.amountToForward, r.outgoingCltv, outAccountable = false).message
643650
}
644651
{
645652
// cltv expiry larger than our requirements
646-
val payload = ChannelRelay.Standard(ShortChannelId(12345), 998900 msat, CltvExpiry(50), upgradeAccountability = false)
647-
val r = createValidIncomingPacket(payload, 1000000 msat, CltvExpiry(70))
653+
val payload = ChannelRelay.Standard(ShortChannelId(12345), 998_900 msat, CltvExpiry(50), upgradeAccountability = false)
654+
val r = createValidIncomingPacket(payload, 1_000_000 msat, CltvExpiry(70))
648655
channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId, 0.1)
649656
receiveConfidence(Reputation.Score(1.0, accountable = false))
650657
expectFwdAdd(register, channelUpdates(ShortChannelId(22223)).channelId, r.amountToForward, r.outgoingCltv, outAccountable = false).message
651658
}
652659
{
653660
// cltv expiry too small, no suitable channel found
654-
val payload = ChannelRelay.Standard(ShortChannelId(12345), 998900 msat, CltvExpiry(61), upgradeAccountability = false)
655-
val r = createValidIncomingPacket(payload, 1000000 msat, CltvExpiry(70))
661+
val payload = ChannelRelay.Standard(ShortChannelId(12345), 998_900 msat, CltvExpiry(61), upgradeAccountability = false)
662+
val r = createValidIncomingPacket(payload, 1_000_000 msat, CltvExpiry(70))
656663
channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId, 0.1)
657664
receiveConfidence(Reputation.Score(1.0, accountable = false))
658665
expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(IncorrectCltvExpiry(CltvExpiry(61), Some(channelUpdates(ShortChannelId(12345)).channelUpdate))), None, commit = true))

0 commit comments

Comments
 (0)