Skip to content

Commit

Permalink
Add path finding for blinded routes (#3027)
Browse files Browse the repository at this point in the history
When generating a blot12 invoice, we may need to find a payment path using only nodes that support route blinding.
  • Loading branch information
thomash-acinq authored Mar 5, 2025
1 parent 4729876 commit 939e25d
Show file tree
Hide file tree
Showing 4 changed files with 118 additions and 2 deletions.
32 changes: 32 additions & 0 deletions eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,38 @@ object Graph {
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)

/**
* Find non-overlapping (no vertices shared) payment paths that support route blinding
* This is used to build blinded routes for Bolt12 invoices where `sourceNode` is the first node of the blinded path and `targetNode` is ourself.
*
* @param pathsToFind Number of paths to find. We may return fewer paths if we couldn't find more non-overlapping ones.
*/
def routeBlindingPaths(graph: DirectedGraph,
sourceNode: PublicKey,
targetNode: PublicKey,
amount: MilliSatoshi,
ignoredEdges: Set[ChannelDesc],
ignoredVertices: Set[PublicKey],
pathsToFind: Int,
wr: WeightRatios[PaymentPathWeight],
currentBlockHeight: BlockHeight,
boundaries: PaymentPathWeight => 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 {
case Some(path) =>
val weight = pathWeight(sourceNode, path, amount, currentBlockHeight, wr, includeLocalChannelCost = true)
paths += WeightedPath(path, weight)
// Additional paths must keep using the source and target nodes, but shouldn't use any of the same intermediate nodes.
verticesToIgnore.addAll(path.drop(1).map(_.desc.a))
case None => return paths.toSeq
}
}
paths.toSeq
}

/**
* Calculate the minimum amount that the start node needs to receive to be able to forward @amountWithFees to the end
* node.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

package fr.acinq.eclair.router

import akka.actor.{ActorContext, ActorRef, Status}
import akka.actor.{ActorContext, ActorRef}
import akka.event.DiagnosticLoggingAdapter
import com.softwaremill.quicklens.ModifyPimp
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
Expand Down Expand Up @@ -229,6 +229,25 @@ object RouteCalculation {
}
}

def handleBlindedRouteRequest(d: Data, currentBlockHeight: BlockHeight, r: BlindedRouteRequest)(implicit log: DiagnosticLoggingAdapter): Data = {
val maxFee = r.routeParams.getMaxFee(r.amount)

val boundaries: PaymentPathWeight => Boolean = { weight =>
weight.amount - r.amount <= maxFee &&
weight.length <= r.routeParams.boundaries.maxRouteLength &&
weight.length <= ROUTE_MAX_LENGTH &&
weight.cltv <= r.routeParams.boundaries.maxCltv
}

val routes = Graph.routeBlindingPaths(d.graphWithBalances.graph, r.source, r.target, r.amount, r.ignore.channels, r.ignore.nodes, r.pathsToFind, r.routeParams.heuristics, currentBlockHeight, boundaries)
if (routes.isEmpty) {
r.replyTo ! PaymentRouteNotFound(RouteNotFound)
} else {
r.replyTo ! RouteResponse(routes.map(route => Route(r.amount, route.path.map(graphEdgeToHop), None)))
}
d
}

def handleMessageRouteRequest(d: Data, currentBlockHeight: BlockHeight, r: MessageRouteRequest, routeParams: MessageRouteParams)(implicit log: DiagnosticLoggingAdapter): Data = {
val boundaries: MessagePathWeight => Boolean = { weight =>
weight.length <= routeParams.maxRouteLength && weight.length <= ROUTE_MAX_LENGTH
Expand Down
11 changes: 11 additions & 0 deletions eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,9 @@ class Router(val nodeParams: NodeParams, watcher: typed.ActorRef[ZmqWatcher.Comm
case Event(r: RouteRequest, d) =>
stay() using RouteCalculation.handleRouteRequest(d, nodeParams.currentBlockHeight, r)

case Event(r: BlindedRouteRequest, d) =>
stay() using RouteCalculation.handleBlindedRouteRequest(d, nodeParams.currentBlockHeight, r)

case Event(r: MessageRouteRequest, d) =>
stay() using RouteCalculation.handleMessageRouteRequest(d, nodeParams.currentBlockHeight, r, nodeParams.routerConf.messageRouteParams)

Expand Down Expand Up @@ -614,6 +617,14 @@ object Router {
pendingPayments: Seq[Route] = Nil,
paymentContext: Option[PaymentContext] = None)

case class BlindedRouteRequest(replyTo: typed.ActorRef[PaymentRouteResponse],
source: PublicKey,
target: PublicKey,
amount: MilliSatoshi,
routeParams: RouteParams,
pathsToFind: Int,
ignore: Ignore = Ignore.empty)

case class FinalizeRoute(replyTo: typed.ActorRef[PaymentRouteResponse],
route: PredefinedRoute,
extraEdges: Seq[ExtraEdge] = Nil,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import fr.acinq.bitcoin.scalacompat.SatoshiLong
import fr.acinq.eclair.payment.relay.Relayer.RelayFees
import fr.acinq.eclair.router.Announcements.makeNodeAnnouncement
import fr.acinq.eclair.router.Graph.GraphStructure.{DirectedGraph, GraphEdge}
import fr.acinq.eclair.router.Graph.{HeuristicsConstants, MessagePathWeight, MessageWeightRatios, PaymentWeightRatios, dijkstraMessagePath, yenKshortestPaths}
import fr.acinq.eclair.router.Graph.{HeuristicsConstants, MessagePathWeight, MessageWeightRatios, PaymentWeightRatios, dijkstraMessagePath, routeBlindingPaths, yenKshortestPaths}
import fr.acinq.eclair.router.RouteCalculationSpec._
import fr.acinq.eclair.router.Router.ChannelDesc
import fr.acinq.eclair.wire.protocol.Color
Expand Down Expand Up @@ -479,4 +479,58 @@ class GraphSpec extends AnyFunSuite {
assert(g == g.updateChannel(ChannelDesc(ShortChannelId(1), randomKey().publicKey, b), RealShortChannelId(10), 99 sat))
}

test("blinded routes for bolt12 invoices") {
/*
D does not support route blinding
+----- B ------+
| |
A -- C -- D -- H --+
| | |
+--- E -- F ---+ |
| |
+--- G -------+
*/
val graph = DirectedGraph(Seq(
makeEdge(1L, a, b, 0 msat, 0),
makeEdge(1L, b, a, 1 msat, 1),
makeEdge(2L, b, h, 2 msat, 2),
makeEdge(2L, h, b, 3 msat, 3),
makeEdge(3L, a, c, 4 msat, 4),
makeEdge(3L, c, a, 5 msat, 5),
makeEdge(4L, c, d, 6 msat, 6),
makeEdge(4L, d, c, 7 msat, 7),
makeEdge(5L, d, h, 8 msat, 8),
makeEdge(5L, h, d, 9 msat, 9),
makeEdge(6L, a, e, 10 msat, 10),
makeEdge(6L, e, a, 11 msat, 11),
makeEdge(7L, e, f, 12 msat, 12),
makeEdge(7L, f, e, 13 msat, 13),
makeEdge(8L, f, h, 14 msat, 14),
makeEdge(8L, h, f, 15 msat, 15),
makeEdge(9L, e, g, 16 msat, 16),
makeEdge(9L, g, e, 17 msat, 17),
makeEdge(10L, g, h, 18 msat, 18),
makeEdge(10L, h, g, 19 msat, 19),
)).addOrUpdateVertex(makeNodeAnnouncement(priv_a, "A", Color(0, 0, 0), Nil, Features(Features.RouteBlinding -> FeatureSupport.Optional)))
.addOrUpdateVertex(makeNodeAnnouncement(priv_b, "B", Color(0, 0, 0), Nil, Features(Features.RouteBlinding -> FeatureSupport.Optional)))
.addOrUpdateVertex(makeNodeAnnouncement(priv_c, "C", Color(0, 0, 0), Nil, Features(Features.RouteBlinding -> FeatureSupport.Optional)))
.addOrUpdateVertex(makeNodeAnnouncement(priv_d, "D", Color(0, 0, 0), Nil, Features()))
.addOrUpdateVertex(makeNodeAnnouncement(priv_e, "E", Color(0, 0, 0), Nil, Features(Features.RouteBlinding -> FeatureSupport.Optional)))
.addOrUpdateVertex(makeNodeAnnouncement(priv_f, "F", Color(0, 0, 0), Nil, Features(Features.RouteBlinding -> FeatureSupport.Optional)))
.addOrUpdateVertex(makeNodeAnnouncement(priv_g, "G", Color(0, 0, 0), Nil, Features(Features.RouteBlinding -> FeatureSupport.Optional)))
.addOrUpdateVertex(makeNodeAnnouncement(priv_h, "H", Color(0, 0, 0), Nil, Features(Features.RouteBlinding -> FeatureSupport.Optional)))

{
val paths = routeBlindingPaths(graph, a, h, 20_000_000 msat, Set.empty, Set.empty, pathsToFind = 3, PaymentWeightRatios(1, 0, 0, 0, RelayFees(0 msat, 0)), BlockHeight(793397), _ => true)
assert(paths.length == 2)
assert(paths(0).path.map(_.desc.a) == Seq(a, b))
assert(paths(1).path.map(_.desc.a) == Seq(a, e, f))
}
{
val paths = routeBlindingPaths(graph, c, h, 20_000_000 msat, Set.empty, Set.empty, pathsToFind = 3, PaymentWeightRatios(1, 0, 0, 0, RelayFees(0 msat, 0)), BlockHeight(793397), _ => true)
assert(paths.length == 1)
assert(paths(0).path.map(_.desc.a) == Seq(c, a, b))
}
}
}

0 comments on commit 939e25d

Please sign in to comment.