mirror of
https://github.com/ACINQ/eclair.git
synced 2025-03-12 10:30:45 +01:00
Add path finding for blinded routes (#3027)
When generating a blot12 invoice, we may need to find a payment path using only nodes that support route blinding.
This commit is contained in:
parent
4729876cac
commit
939e25da66
4 changed files with 118 additions and 2 deletions
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue