Skip to content

Commit 6abf54d

Browse files
committed
Prioritize private channels when relaying payments
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).
1 parent 369f042 commit 6abf54d

File tree

2 files changed

+41
-40
lines changed

2 files changed

+41
-40
lines changed

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

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -382,24 +382,18 @@ 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, if balances are equal we use the lowest available capacity to have a fully defined sorting algorithm
392+
case (channel1, channel2) if channel1.commitments.availableBalanceForSend == channel2.commitments.availableBalanceForSend => channel1.commitments.capacity <= channel2.commitments.capacity
393+
// otherwise, we use the lowest available balance to ensure we keep higher balances for larger payments
394+
case (channel1, channel2) => channel1.commitments.availableBalanceForSend <= channel2.commitments.availableBalanceForSend
395+
}.headOption match {
396+
case Some(channel) => Some(channel)
403397
case None =>
404398
val requestedChannel_opt = requestedChannelId_opt.flatMap(channels.get)
405399
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)