Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions eclair-core/src/main/resources/reference.conf
Original file line number Diff line number Diff line change
Expand Up @@ -244,10 +244,19 @@ eclair {
fee-base-msat = 1000
fee-proportional-millionths = 100
}
// Minimum fees for trampoline relays
// Minimum *total* fee budget for trampoline relays (including fees for the whole path that must be found to reach
// the next trampoline node).
min-trampoline {
fee-base-msat = 2000
fee-proportional-millionths = 400
}
// Minimum trampoline fees that must be collected when relaying trampoline payments: the difference between those
// fees and min-trampoline above will be allocated to the rest of the payment path(s).
// It may make sense to use smaller values here than what is used for channel fees to ensure that more payments
// succeed, even when the recipient's LSP collects a large fee in their routing hint (or blinded path).
min-local-trampoline {
fee-base-msat = 1000
fee-proportional-millionths = 100
fee-proportional-millionths = 200
}
// By default, if the fees values are updated in configuration, they will automatically be applied to existing
// channels on restart (except if custom per-node settings have been defined, in which case they take precedence).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -666,6 +666,7 @@ object NodeParams extends Logging {
publicChannelFees = getRelayFees(config.getConfig("relay.fees.public-channels")),
privateChannelFees = getRelayFees(config.getConfig("relay.fees.private-channels")),
minTrampolineFees = getRelayFees(config.getConfig("relay.fees.min-trampoline")),
minLocalTrampolineFees = getRelayFees(config.getConfig("relay.fees.min-local-trampoline")),
resetExistingChannels = config.getBoolean("relay.fees.reset-existing-channels"),
enforcementDelay = FiniteDuration(config.getDuration("relay.fees.enforcement-delay").getSeconds, TimeUnit.SECONDS),
asyncPaymentsParams = AsyncPaymentsParams(asyncPaymentHoldTimeoutBlocks, asyncPaymentCancelSafetyBeforeTimeoutBlocks),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import fr.acinq.eclair.payment.send.PaymentLifecycle.SendPaymentToNode
import fr.acinq.eclair.payment.send._
import fr.acinq.eclair.reputation.Reputation
import fr.acinq.eclair.reputation.ReputationRecorder.GetConfidence
import fr.acinq.eclair.router.Router.{ChannelHop, HopRelayParams, Route, RouteParams}
import fr.acinq.eclair.router.Router.{ChannelHop, HopRelayParams, Route}
import fr.acinq.eclair.router.{BalanceTooLow, RouteNotFound}
import fr.acinq.eclair.wire.protocol.PaymentOnion.IntermediatePayload
import fr.acinq.eclair.wire.protocol._
Expand Down Expand Up @@ -140,19 +140,6 @@ object NodeRelay {
}
}

/** Compute route params that honor our fee and cltv requirements. */
private def computeRouteParams(nodeParams: NodeParams, amountIn: MilliSatoshi, expiryIn: CltvExpiry, amountOut: MilliSatoshi, expiryOut: CltvExpiry): RouteParams = {
val routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams
routeParams.copy(
boundaries = routeParams.boundaries.copy(
maxFeeProportional = 0, // we disable percent-based max fee calculation, we're only interested in collecting our node fee
maxFeeFlat = amountIn - amountOut,
maxCltv = expiryIn - expiryOut
),
includeLocalChannelCost = true
)
}

/** If we fail to relay a payment, we may want to attempt on-the-fly funding if it makes sense. */
private def shouldAttemptOnTheFlyFunding(nodeParams: NodeParams, recipientFeatures_opt: Option[Features[InitFeature]], failures: Seq[PaymentFailure])(implicit context: ActorContext[Command]): Boolean = {
val featureOk = Features.canUseFeature(nodeParams.features.initFeatures(), recipientFeatures_opt.getOrElse(Features.empty), Features.OnTheFlyFunding)
Expand Down Expand Up @@ -357,7 +344,23 @@ class NodeRelay private(nodeParams: NodeParams,
accountable0
}
val paymentCfg = SendPaymentConfig(relayId, relayId, None, paymentHash, recipient.nodeId, upstream, None, None, storeInDb = false, publishEvent = false, recordPathFindingMetrics = true, accountable)
val routeParams = computeRouteParams(nodeParams, upstream.amountIn, upstream.expiryIn, amountOut, expiryOut)
val defaultRouteParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams
val routeParams = defaultRouteParams.copy(
boundaries = defaultRouteParams.boundaries.copy(
maxFeeProportional = 0, // we disable percent-based max fee calculation, we're only interested in collecting our node fee
maxFeeFlat = if (recipient.extraEdges.isEmpty) {
// The payment doesn't contain any routing hint, so we'll enforce our local channel fees.
upstream.amountIn - amountOut
} else {
// We allow dipping into our local channel fees to ensure that payments can be relayed. We will earn less
// than expected, but it's a better UX for users and allows other LSPs to earn a fee when their users receive
// payments (by setting a somewhat large fee in the routing hint or the blinded path).
(upstream.amountIn - amountOut - nodeFee(nodeParams.relayParams.minLocalTrampolineFees, amountOut)).max(0 msat)
},
maxCltv = upstream.expiryIn - expiryOut
),
includeLocalChannelCost = recipient.extraEdges.isEmpty
)
// If the next node is using trampoline, we assume that they support MPP.
val useMultiPart = recipient.features.hasFeature(Features.BasicMultiPartPayment) || packetOut_opt.nonEmpty
val payFsmAdapters = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ object Relayer extends Logging {
case class RelayParams(publicChannelFees: RelayFees,
privateChannelFees: RelayFees,
minTrampolineFees: RelayFees,
minLocalTrampolineFees: RelayFees,
resetExistingChannels: Boolean,
enforcementDelay: FiniteDuration,
asyncPaymentsParams: AsyncPaymentsParams,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -252,11 +252,16 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig,
case _: Upstream.Local => 0.msat // no local fees when we are the origin of the payment
case u: Upstream.Hot.Channel => u.amountIn - paymentSent.amountWithFees
case _: Upstream.Hot.Trampoline =>
// in case of a relayed payment, we need to take into account the fee of the first channels
paymentSent.parts.collect {
// NB: the route attribute will always be defined here
case p@PaymentPart(_, _, _, Some(route), _) => route.head.fee(p.amountWithFees)
}.sum
// In case of a relayed payment, we need to take into account the fee of the first channels, unless we
// explicitly chose to relay without collecting that fee.
if (request.routeParams.includeLocalChannelCost) {
paymentSent.parts.collect {
// NB: the route attribute will always be defined here
case p@PaymentPart(_, _, _, Some(route), _) => route.head.fee(p.amountWithFees)
}.sum
} else {
0 msat

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I figure we could maybe set a minimum relay fee here (and in PaymentLifeCycle), WDYT?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then I think we should repurpose the relay.fees.min-trampoline section of eclair.conf for that, shouldn't we?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've tried a different approach in 88102e1, which lets us more flexibly configure a threshold trampoline fee that's different from the channel fees.

Note that this is somewhat unrelated to the line of code you're commenting on, which just computes local vs total path fees for reporting.

}
}
paymentSent.feesPaid + localFees
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -420,11 +420,20 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A
case _: Upstream.Local => 0.msat // no local fees when we are the origin of the payment
case u: Upstream.Hot.Channel => u.amountIn - paymentSent.amountWithFees
case _: Upstream.Hot.Trampoline =>
// in case of a relayed payment, we need to take into account the fee of the first channels
paymentSent.parts.collect {
// NB: the route attribute will always be defined here
case p@PaymentPart(_, _, _, Some(route), _) => route.head.fee(p.amountWithFees)
}.sum
// In case of a relayed payment, we need to take into account the fee of the first channels, unless we
// explicitly chose to relay without collecting that fee.
val includeLocalChannelCost = request match {
case request: SendPaymentToNode => request.routeParams.includeLocalChannelCost
case _: SendPaymentToRoute => true
}
if (includeLocalChannelCost) {
paymentSent.parts.collect {
// NB: the route attribute will always be defined here
case p@PaymentPart(_, _, _, Some(route), _) => route.head.fee(p.amountWithFees)
}.sum
} else {
0 msat
}
}
paymentSent.feesPaid + localFees
case Left(paymentFailed) =>
Expand Down
26 changes: 8 additions & 18 deletions eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala
Original file line number Diff line number Diff line change
Expand Up @@ -176,15 +176,10 @@ object TestConstants {
perNodeFeerateTolerance = Map.empty
),
relayParams = RelayParams(
publicChannelFees = RelayFees(
feeBase = 546000 msat,
feeProportionalMillionths = 10),
privateChannelFees = RelayFees(
feeBase = 547000 msat,
feeProportionalMillionths = 20),
minTrampolineFees = RelayFees(
feeBase = 548000 msat,
feeProportionalMillionths = 30),
publicChannelFees = RelayFees(feeBase = 546000 msat, feeProportionalMillionths = 10),
privateChannelFees = RelayFees(feeBase = 547000 msat, feeProportionalMillionths = 20),
minTrampolineFees = RelayFees(feeBase = 548000 msat, feeProportionalMillionths = 30),
minLocalTrampolineFees = RelayFees(feeBase = 546000 msat, feeProportionalMillionths = 10),
resetExistingChannels = true,
enforcementDelay = 10 minutes,
asyncPaymentsParams = AsyncPaymentsParams(1008, CltvExpiryDelta(144)),
Expand Down Expand Up @@ -405,15 +400,10 @@ object TestConstants {
perNodeFeerateTolerance = Map.empty
),
relayParams = RelayParams(
publicChannelFees = RelayFees(
feeBase = 546000 msat,
feeProportionalMillionths = 10),
privateChannelFees = RelayFees(
feeBase = 547000 msat,
feeProportionalMillionths = 20),
minTrampolineFees = RelayFees(
feeBase = 548000 msat,
feeProportionalMillionths = 30),
publicChannelFees = RelayFees(feeBase = 546000 msat, feeProportionalMillionths = 10),
privateChannelFees = RelayFees(feeBase = 547000 msat, feeProportionalMillionths = 20),
minTrampolineFees = RelayFees(feeBase = 548000 msat, feeProportionalMillionths = 30),
minLocalTrampolineFees = RelayFees(feeBase = 546000 msat, feeProportionalMillionths = 10),
resetExistingChannels = true,
enforcementDelay = 10 minutes,
asyncPaymentsParams = AsyncPaymentsParams(1008, CltvExpiryDelta(144)),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package fr.acinq.eclair.integration.basic.fixtures.composite

import akka.actor.ActorSystem
import akka.testkit.TestKit
import fr.acinq.eclair.NodeParams
import fr.acinq.eclair.integration.basic.fixtures.{FixtureUtils, MinimalNodeFixture}

case class FourNodesFixture private(system: ActorSystem,
alice: MinimalNodeFixture,
bob: MinimalNodeFixture,
carol: MinimalNodeFixture,
dave: MinimalNodeFixture) {
implicit val implicitSystem: ActorSystem = system

def cleanup(): Unit = {
TestKit.shutdownActorSystem(alice.system)
TestKit.shutdownActorSystem(bob.system)
TestKit.shutdownActorSystem(carol.system)
TestKit.shutdownActorSystem(dave.system)
TestKit.shutdownActorSystem(system)
}
}

object FourNodesFixture {
def apply(aliceParams: NodeParams, bobParams: NodeParams, carolParams: NodeParams, daveParams: NodeParams, testName: String): FourNodesFixture = {
FourNodesFixture(
system = ActorSystem("system-test", FixtureUtils.actorSystemConfig(testName)),
alice = MinimalNodeFixture(aliceParams, testName),
bob = MinimalNodeFixture(bobParams, testName),
carol = MinimalNodeFixture(carolParams, testName),
dave = MinimalNodeFixture(daveParams, testName),
)
}
}
Loading
Loading