Skip to content
Closed
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
2 changes: 2 additions & 0 deletions eclair-core/src/main/resources/reference.conf
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,8 @@ eclair {
}

path-finding {
blip18-inbound-fees = false
exclude-channels-with-positive-inbound-fees = false
default {
randomize-route-selection = true // when computing a route for a payment we randomize the final selection

Expand Down
2 changes: 1 addition & 1 deletion eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala
Original file line number Diff line number Diff line change
Expand Up @@ -487,7 +487,7 @@ class EclairImpl(val appKit: Kit) extends Eclair with Logging with SpendFromChan
for {
ignoredChannels <- getChannelDescs(ignoreShortChannelIds.toSet)
ignore = Ignore(ignoreNodeIds.toSet, ignoredChannels)
response <- appKit.router.toTyped.ask[PaymentRouteResponse](replyTo => RouteRequest(replyTo, sourceNodeId, target, routeParams1, ignore)).flatMap {
response <- appKit.router.toTyped.ask[PaymentRouteResponse](replyTo => RouteRequest(replyTo, sourceNodeId, target, routeParams1, ignore, blip18InboundFees = appKit.nodeParams.routerConf.blip18InboundFees, excludePositiveInboundFees = appKit.nodeParams.routerConf.excludePositiveInboundFees)).flatMap {
case r: RouteResponse => Future.successful(r)
case PaymentRouteNotFound(error) => Future.failed(error)
}
Expand Down
5 changes: 3 additions & 2 deletions eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala
Original file line number Diff line number Diff line change
Expand Up @@ -481,7 +481,6 @@ object NodeParams extends Logging {
experimentName = name,
experimentPercentage = config.getInt("percentage"))


def getPathFindingExperimentConf(config: Config): PathFindingExperimentConf = {
val experiments = config.root.asScala.keys.map(name => name -> getPathFindingConf(config.getConfig(name), name))
PathFindingExperimentConf(experiments.toMap)
Expand Down Expand Up @@ -676,7 +675,9 @@ object NodeParams extends Logging {
pathFindingExperimentConf = getPathFindingExperimentConf(config.getConfig("router.path-finding.experiments")),
messageRouteParams = getMessageRouteParams(config.getConfig("router.message-path-finding")),
balanceEstimateHalfLife = FiniteDuration(config.getDuration("router.balance-estimate-half-life").getSeconds, TimeUnit.SECONDS),
),
blip18InboundFees = config.getBoolean("router.path-finding.blip18-inbound-fees"),
excludePositiveInboundFees = config.getBoolean("router.path-finding.exclude-channels-with-positive-inbound-fees"),
),
socksProxy_opt = socksProxy_opt,
maxPaymentAttempts = config.getInt("max-payment-attempts"),
paymentFinalExpiry = PaymentFinalExpiryConf(CltvExpiryDelta(config.getInt("send.recipient-final-expiry.min-delta")), CltvExpiryDelta(config.getInt("send.recipient-final-expiry.max-delta"))),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ object DefaultOfferHandler {
val routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams
.modify(_.boundaries.maxRouteLength).setTo(nodeParams.offersConfig.paymentPathLength)
.modify(_.boundaries.maxCltv).setTo(nodeParams.offersConfig.paymentPathCltvExpiryDelta)
router ! BlindedRouteRequest(context.messageAdapter(WrappedRouteResponse), blindedPathFirstNodeId, nodeParams.nodeId, invoiceRequest.amount, routeParams, nodeParams.offersConfig.paymentPathCount)
router ! BlindedRouteRequest(context.messageAdapter(WrappedRouteResponse), blindedPathFirstNodeId, nodeParams.nodeId, invoiceRequest.amount, routeParams, nodeParams.offersConfig.paymentPathCount, blip18InboundFees = nodeParams.routerConf.blip18InboundFees, excludePositiveInboundFees = nodeParams.routerConf.excludePositiveInboundFees)
waitForRoute(nodeParams, replyTo, invoiceRequest, blindedPathFirstNodeId, context)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,14 @@ object Relayer extends Logging {
val zero: RelayFees = RelayFees(MilliSatoshi(0), 0)
}

case class InboundFees(feeBase: MilliSatoshi, feeProportionalMillionths: Long)

object InboundFees {
def apply(feeBaseInt32: Int, feeProportionalMillionthsInt32: Int): InboundFees = {
InboundFees(MilliSatoshi(feeBaseInt32), feeProportionalMillionthsInt32)
}
}

case class AsyncPaymentsParams(holdTimeoutBlocks: Int, cancelSafetyBeforeTimeout: CltvExpiryDelta)

case class RelayParams(publicChannelFees: RelayFees,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,7 @@ object MultiPartPaymentLifecycle {
case class PaymentSucceeded(request: SendMultiPartPayment, preimage: ByteVector32, parts: Seq[PartialPayment], pending: Set[UUID]) extends Data

private def createRouteRequest(replyTo: ActorRef, nodeParams: NodeParams, routeParams: RouteParams, d: PaymentProgress, cfg: SendPaymentConfig): RouteRequest = {
RouteRequest(replyTo.toTyped, nodeParams.nodeId, d.request.recipient, routeParams, d.ignore, allowMultiPart = true, d.pending.values.toSeq, Some(cfg.paymentContext))
RouteRequest(replyTo.toTyped, nodeParams.nodeId, d.request.recipient, routeParams, d.ignore, allowMultiPart = true, d.pending.values.toSeq, Some(cfg.paymentContext), blip18InboundFees = nodeParams.routerConf.blip18InboundFees, excludePositiveInboundFees = nodeParams.routerConf.excludePositiveInboundFees)
}

private def createChildPayment(replyTo: ActorRef, route: Route, request: SendMultiPartPayment): SendPaymentToRoute = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A
case Event(request: SendPaymentToRoute, WaitingForRequest) =>
log.debug("sending {} to route {}", request.amount, request.printRoute())
request.route.fold(
hops => router ! FinalizeRoute(self, hops, request.recipient.extraEdges, paymentContext = Some(cfg.paymentContext)),
hops => router ! FinalizeRoute(self, hops, request.recipient.extraEdges, paymentContext = Some(cfg.paymentContext), nodeParams.routerConf.blip18InboundFees, nodeParams.routerConf.excludePositiveInboundFees),
route => self ! RouteResponse(route :: Nil)
)
if (cfg.storeInDb) {
Expand All @@ -64,7 +64,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A

case Event(request: SendPaymentToNode, WaitingForRequest) =>
log.debug("sending {} to {}", request.amount, request.recipient.nodeId)
router ! RouteRequest(self, nodeParams.nodeId, request.recipient, request.routeParams, paymentContext = Some(cfg.paymentContext))
router ! RouteRequest(self, nodeParams.nodeId, request.recipient, request.routeParams, paymentContext = Some(cfg.paymentContext), blip18InboundFees = nodeParams.routerConf.blip18InboundFees, excludePositiveInboundFees = nodeParams.routerConf.excludePositiveInboundFees)
if (cfg.storeInDb) {
paymentsDb.addOutgoingPayment(OutgoingPayment(id, cfg.parentId, cfg.externalId, paymentHash, cfg.paymentType, request.amount, request.recipient.totalAmount, request.recipient.nodeId, TimestampMilli.now(), cfg.invoice, cfg.payerKey_opt, OutgoingPaymentStatus.Pending))
}
Expand Down Expand Up @@ -135,7 +135,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A
data.request match {
case request: SendPaymentToNode =>
val ignore1 = PaymentFailure.updateIgnored(failure, data.ignore)
router ! RouteRequest(self, nodeParams.nodeId, data.recipient, request.routeParams, ignore1, paymentContext = Some(cfg.paymentContext))
router ! RouteRequest(self, nodeParams.nodeId, data.recipient, request.routeParams, ignore1, paymentContext = Some(cfg.paymentContext), blip18InboundFees = nodeParams.routerConf.blip18InboundFees, excludePositiveInboundFees = nodeParams.routerConf.excludePositiveInboundFees)
goto(WAITING_FOR_ROUTE) using WaitingForRoute(data.request, data.failures :+ failure, ignore1)
case _: SendPaymentToRoute =>
log.error("unexpected retry during SendPaymentToRoute")
Expand Down Expand Up @@ -241,7 +241,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A
log.error("unexpected retry during SendPaymentToRoute")
stop(FSM.Normal)
case request: SendPaymentToNode =>
router ! RouteRequest(self, nodeParams.nodeId, recipient1, request.routeParams, ignore1, paymentContext = Some(cfg.paymentContext))
router ! RouteRequest(self, nodeParams.nodeId, recipient1, request.routeParams, ignore1, paymentContext = Some(cfg.paymentContext), blip18InboundFees = nodeParams.routerConf.blip18InboundFees, excludePositiveInboundFees = nodeParams.routerConf.excludePositiveInboundFees)
goto(WAITING_FOR_ROUTE) using WaitingForRoute(request.copy(recipient = recipient1), failures :+ failure, ignore1)
}
} else {
Expand All @@ -252,7 +252,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A
log.error("unexpected retry during SendPaymentToRoute")
stop(FSM.Normal)
case request: SendPaymentToNode =>
router ! RouteRequest(self, nodeParams.nodeId, recipient, request.routeParams, ignore + nodeId, paymentContext = Some(cfg.paymentContext))
router ! RouteRequest(self, nodeParams.nodeId, recipient, request.routeParams, ignore + nodeId, paymentContext = Some(cfg.paymentContext), blip18InboundFees = nodeParams.routerConf.blip18InboundFees, excludePositiveInboundFees = nodeParams.routerConf.excludePositiveInboundFees)
goto(WAITING_FOR_ROUTE) using WaitingForRoute(request, failures :+ failure, ignore + nodeId)
}
}
Expand All @@ -266,7 +266,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A
log.error("unexpected retry during SendPaymentToRoute")
stop(FSM.Normal)
case request: SendPaymentToNode =>
router ! RouteRequest(self, nodeParams.nodeId, recipient, request.routeParams, ignore1, paymentContext = Some(cfg.paymentContext))
router ! RouteRequest(self, nodeParams.nodeId, recipient, request.routeParams, ignore1, paymentContext = Some(cfg.paymentContext), blip18InboundFees = nodeParams.routerConf.blip18InboundFees, excludePositiveInboundFees = nodeParams.routerConf.excludePositiveInboundFees)
goto(WAITING_FOR_ROUTE) using WaitingForRoute(request, failures :+ failure, ignore1)
}
case Right(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage)) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,9 @@ object EclairInternalsSerializer {
("syncConf" | syncConfCodec) ::
("pathFindingExperimentConf" | pathFindingExperimentConfCodec) ::
("messageRouteParams" | messageRouteParamsCodec) ::
("balanceEstimateHalfLife" | finiteDurationCodec)).as[RouterConf]
("balanceEstimateHalfLife" | finiteDurationCodec) ::
("blip18InboundFees" | bool(8)) ::
("excludePositiveInboundFees" | bool(8))).as[RouterConf]

val overrideFeaturesListCodec: Codec[List[(PublicKey, Features[Feature])]] = listOfN(uint16, publicKey ~ lengthPrefixedFeaturesCodec)

Expand Down
28 changes: 21 additions & 7 deletions eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import fr.acinq.eclair._
import fr.acinq.eclair.payment.Invoice
import fr.acinq.eclair.payment.relay.Relayer.RelayFees
import fr.acinq.eclair.router.Graph.GraphStructure.{DirectedGraph, GraphEdge}
import fr.acinq.eclair.router.Router.HopRelayParams
import fr.acinq.eclair.router.Router._
import fr.acinq.eclair.wire.protocol.{ChannelUpdate, NodeAnnouncement}

Expand Down Expand Up @@ -256,10 +257,11 @@ object Graph {
wr: WeightRatios[PaymentPathWeight],
currentBlockHeight: BlockHeight,
boundaries: PaymentPathWeight => Boolean,
includeLocalChannelCost: Boolean): Seq[WeightedPath[PaymentPathWeight]] = {
includeLocalChannelCost: Boolean,
excludePositiveInboundFees: Boolean = false): Seq[WeightedPath[PaymentPathWeight]] = {
// find the shortest path (k = 0)
val targetWeight = PaymentPathWeight(amount)
dijkstraShortestPath(graph, sourceNode, targetNode, ignoredEdges, ignoredVertices, extraEdges, targetWeight, boundaries, Features.empty, currentBlockHeight, wr, includeLocalChannelCost) match {
dijkstraShortestPath(graph, sourceNode, targetNode, ignoredEdges, ignoredVertices, extraEdges, targetWeight, boundaries, Features.empty, currentBlockHeight, wr, includeLocalChannelCost, excludePositiveInboundFees) match {
case None => Seq.empty // if we can't even find a single path, avoid returning a Seq(Seq.empty)
case Some(shortestPath) =>

Expand Down Expand Up @@ -297,7 +299,7 @@ object Graph {
val alreadyExploredVertices = rootPathEdges.map(_.desc.b).toSet
val rootPathWeight = pathWeight(sourceNode, rootPathEdges, amount, currentBlockHeight, wr, includeLocalChannelCost)
// find the "spur" path, a sub-path going from the spur node to the target avoiding previously found sub-paths
dijkstraShortestPath(graph, sourceNode, spurNode, ignoredEdges ++ alreadyExploredEdges, ignoredVertices ++ alreadyExploredVertices, extraEdges, rootPathWeight, boundaries, Features.empty, currentBlockHeight, wr, includeLocalChannelCost) match {
dijkstraShortestPath(graph, sourceNode, spurNode, ignoredEdges ++ alreadyExploredEdges, ignoredVertices ++ alreadyExploredVertices, extraEdges, rootPathWeight, boundaries, Features.empty, currentBlockHeight, wr, includeLocalChannelCost, excludePositiveInboundFees) match {
case Some(spurPath) =>
val completePath = spurPath ++ rootPathEdges
val candidatePath = WeightedPath(completePath, pathWeight(sourceNode, completePath, amount, currentBlockHeight, wr, includeLocalChannelCost))
Expand Down Expand Up @@ -349,7 +351,8 @@ object Graph {
nodeFeatures: Features[NodeFeature],
currentBlockHeight: BlockHeight,
wr: WeightRatios[RichWeight],
includeLocalChannelCost: Boolean): Option[Seq[GraphEdge]] = {
includeLocalChannelCost: Boolean,
excludePositiveInboundFees: Boolean): Option[Seq[GraphEdge]] = {
// the graph does not contain source/destination nodes
val sourceNotInGraph = !g.containsVertex(sourceNode) && !extraEdges.exists(_.desc.a == sourceNode)
val targetNotInGraph = !g.containsVertex(targetNode) && !extraEdges.exists(_.desc.b == targetNode)
Expand Down Expand Up @@ -388,6 +391,7 @@ object Graph {
if (current.weight.canUseEdge(edge) &&
!ignoredEdges.contains(edge.desc) &&
!ignoredVertices.contains(neighbor) &&
(!excludePositiveInboundFees || g.getBackEdge(edge).flatMap(_.getChannelUpdate).flatMap(_.blip18InboundFees_opt).forall(i => i.feeBase.toLong <= 0 && i.feeProportionalMillionths <= 0)) &&
(neighbor == sourceNode || g.getVertexFeatures(neighbor).areSupported(nodeFeatures))) {
// NB: this contains the amount (including fees) that will need to be sent to `neighbor`, but the amount that
// will be relayed through that edge is the one in `currentWeight`.
Expand Down Expand Up @@ -432,7 +436,7 @@ object Graph {
boundaries: MessagePathWeight => Boolean,
currentBlockHeight: BlockHeight,
wr: MessageWeightRatios): Option[Seq[GraphEdge]] =
dijkstraShortestPath(g, sourceNode, targetNode, ignoredEdges = Set.empty, ignoredVertices, extraEdges = Set.empty, MessagePathWeight.zero, boundaries, Features(Features.OnionMessages -> FeatureSupport.Mandatory), currentBlockHeight, wr, includeLocalChannelCost = true)
dijkstraShortestPath(g, sourceNode, targetNode, ignoredEdges = Set.empty, ignoredVertices, extraEdges = Set.empty, MessagePathWeight.zero, boundaries, Features(Features.OnionMessages -> FeatureSupport.Mandatory), currentBlockHeight, wr, includeLocalChannelCost = true, excludePositiveInboundFees = false)

/**
* Find non-overlapping (no vertices shared) payment paths that support route blinding
Expand All @@ -449,12 +453,13 @@ object Graph {
pathsToFind: Int,
wr: WeightRatios[PaymentPathWeight],
currentBlockHeight: BlockHeight,
boundaries: PaymentPathWeight => Boolean): Seq[WeightedPath[PaymentPathWeight]] = {
boundaries: PaymentPathWeight => Boolean,
excludePositiveInboundFees: Boolean): Seq[WeightedPath[PaymentPathWeight]] = {
val paths = new mutable.ArrayBuffer[WeightedPath[PaymentPathWeight]](pathsToFind)
val verticesToIgnore = new mutable.HashSet[PublicKey]()
verticesToIgnore.addAll(ignoredVertices)
for (_ <- 1 to pathsToFind) {
dijkstraShortestPath(graph, sourceNode, targetNode, ignoredEdges, verticesToIgnore.toSet, extraEdges = Set.empty, PaymentPathWeight(amount), boundaries, Features(Features.RouteBlinding -> FeatureSupport.Mandatory), currentBlockHeight, wr, includeLocalChannelCost = true) match {
dijkstraShortestPath(graph, sourceNode, targetNode, ignoredEdges, verticesToIgnore.toSet, extraEdges = Set.empty, PaymentPathWeight(amount), boundaries, Features(Features.RouteBlinding -> FeatureSupport.Mandatory), currentBlockHeight, wr, includeLocalChannelCost = true, excludePositiveInboundFees) match {
case Some(path) =>
val weight = pathWeight(sourceNode, path, amount, currentBlockHeight, wr, includeLocalChannelCost = true)
paths += WeightedPath(path, weight)
Expand Down Expand Up @@ -553,6 +558,11 @@ object Graph {
).flatten.min.max(0 msat)

def fee(amount: MilliSatoshi): MilliSatoshi = params.fee(amount)

def getChannelUpdate: Option[ChannelUpdate] = params match {
case HopRelayParams.FromAnnouncement(update, _) => Some(update)
case _ => None
}
}

object GraphEdge {
Expand Down Expand Up @@ -668,6 +678,10 @@ object Graph {
def getEdge(desc: ChannelDesc): Option[GraphEdge] =
vertices.get(desc.b).flatMap(_.incomingEdges.get(desc))

def getBackEdge(desc: ChannelDesc): Option[GraphEdge] = getEdge(desc.copy(a = desc.b, b = desc.a))

def getBackEdge(edge: GraphEdge): Option[GraphEdge] = getBackEdge(edge.desc)

/**
* @param keyA the key associated with the starting vertex
* @param keyB the key associated with the ending vertex
Expand Down
Loading