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
1 change: 1 addition & 0 deletions docs/release-notes/eclair-vnext.md
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,7 @@ eclair.relay.reserved-for-accountable = 0.0

- `findroute`, `findroutetonode` and `findroutebetweennodes` now include a `maxCltvExpiryDelta` parameter (#3234)
- `findroute`, `findroutetonode` and `findroutebetweennodes` now include a `fee` field for each route in their full format response (#3283)
- `findroute`, `findroutetonode`, `findroutebetweennodes`, `payinvoice`, `sendtonode`, and `payoffer` now include a `routeAddrType` parameter (#3307)
- `channel-opened` was removed from the websocket in favor of `channel-funding-created`, `channel-confirmed` and `channel-ready` (#3237 and #3256)
- `networkfees` and `channelstats` are removed in favor in `relaystats` (#3245)

Expand Down
60 changes: 32 additions & 28 deletions eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,7 @@ object MultiPartPaymentLifecycle {
* @param maxAttempts maximum number of retries.
* @param routeParams parameters to fine-tune the routing algorithm.
*/
case class SendMultiPartPayment(replyTo: ActorRef, recipient: Recipient, maxAttempts: Int, routeParams: RouteParams) {
case class SendMultiPartPayment(replyTo: ActorRef, recipient: Recipient, maxAttempts: Int, routeParams: RouteParams, routeAddrType_opt: Option[AddrType] = None) {
require(recipient.totalAmount > 0.msat, "total amount must be > 0")
}

Expand Down Expand Up @@ -372,7 +372,7 @@ object MultiPartPaymentLifecycle {
case class PaymentSucceeded(request: SendMultiPartPayment, preimage: ByteVector32, parts: Seq[PaymentPart], pending: Set[UUID], remainingAttribution_opt: Option[ByteVector]) 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), d.request.routeAddrType_opt)
}

private def createChildPayment(replyTo: ActorRef, route: Route, request: SendMultiPartPayment): SendPaymentToRoute = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import fr.acinq.eclair.message.{OnionMessages, Postman}
import fr.acinq.eclair.payment.send.BlindedPathsResolver.{Resolve, ResolvedPath}
import fr.acinq.eclair.payment.send.PaymentInitiator.{SendPaymentToNode, SendTrampolinePayment}
import fr.acinq.eclair.payment.{Bolt12Invoice, PaymentBlindedRoute}
import fr.acinq.eclair.router.Router.RouteParams
import fr.acinq.eclair.router.Router.{AddrType, RouteParams}
import fr.acinq.eclair.wire.protocol.MessageOnion.{FinalPayload, InvoicePayload}
import fr.acinq.eclair.wire.protocol.OfferTypes._
import fr.acinq.eclair.wire.protocol.{OnionMessagePayloadTlv, TlvStream}
Expand Down Expand Up @@ -62,7 +62,8 @@ object OfferPayment {
maxAttempts: Int,
routeParams: RouteParams,
blocking: Boolean,
trampolineNodeId_opt: Option[PublicKey] = None)
trampolineNodeId_opt: Option[PublicKey] = None,
routeAddrType_opt: Option[AddrType] = None)

def apply(nodeParams: NodeParams,
postman: typed.ActorRef[Postman.Command],
Expand Down Expand Up @@ -155,7 +156,7 @@ private class OfferPayment(replyTo: ActorRef,
replyTo ! UnknownShortChannelIds(scids)
Behaviors.stopped
case WrappedResolvedPaths(resolved) =>
paymentInitiator ! SendPaymentToNode(replyTo, invoice.amount, invoice, resolved, maxAttempts = sendPaymentConfig.maxAttempts, externalId = sendPaymentConfig.externalId_opt, routeParams = sendPaymentConfig.routeParams, payerKey_opt = Some(payerKey), blockUntilComplete = sendPaymentConfig.blocking)
paymentInitiator ! SendPaymentToNode(replyTo, invoice.amount, invoice, resolved, maxAttempts = sendPaymentConfig.maxAttempts, externalId = sendPaymentConfig.externalId_opt, routeParams = sendPaymentConfig.routeParams, payerKey_opt = Some(payerKey), blockUntilComplete = sendPaymentConfig.blocking, routeAddrType_opt = sendPaymentConfig.routeAddrType_opt)
Behaviors.stopped
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,11 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn
replyTo ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(r.recipientAmount, Nil, UnsupportedFeatures(recipient.features)) :: Nil, startedAt = TimestampMilli.now(), settledAt = TimestampMilli.now())
} else if (Features.canUseFeature(nodeParams.features.invoiceFeatures(), recipient.features, Features.BasicMultiPartPayment)) {
val fsm = outgoingPaymentFactory.spawnOutgoingMultiPartPayment(context, paymentCfg, publishPreimage = !r.blockUntilComplete)
fsm ! MultiPartPaymentLifecycle.SendMultiPartPayment(self, recipient, r.maxAttempts, r.routeParams)
fsm ! MultiPartPaymentLifecycle.SendMultiPartPayment(self, recipient, r.maxAttempts, r.routeParams, r.routeAddrType_opt)
context become main(pending + (paymentId -> PendingPaymentToNode(replyTo, r)))
} else {
val fsm = outgoingPaymentFactory.spawnOutgoingPayment(context, paymentCfg)
fsm ! PaymentLifecycle.SendPaymentToNode(self, recipient, r.maxAttempts, r.routeParams)
fsm ! PaymentLifecycle.SendPaymentToNode(self, recipient, r.maxAttempts, r.routeParams, r.routeAddrType_opt)
context become main(pending + (paymentId -> PendingPaymentToNode(replyTo, r)))
}

Expand All @@ -75,7 +75,7 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn
val finalExpiry = nodeParams.paymentFinalExpiry.computeFinalExpiry(nodeParams.currentBlockHeight, Channel.MIN_CLTV_EXPIRY_DELTA)
val recipient = SpontaneousRecipient(r.recipientNodeId, r.recipientAmount, finalExpiry, r.paymentPreimage, r.userCustomTlvs)
val fsm = outgoingPaymentFactory.spawnOutgoingPayment(context, paymentCfg)
fsm ! PaymentLifecycle.SendPaymentToNode(self, recipient, r.maxAttempts, r.routeParams)
fsm ! PaymentLifecycle.SendPaymentToNode(self, recipient, r.maxAttempts, r.routeParams, r.routeAddrType_opt)
context become main(pending + (paymentId -> PendingSpontaneousPayment(sender(), r)))

case r: SendTrampolinePayment =>
Expand Down Expand Up @@ -125,7 +125,7 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn
case PaymentIdentifier.PaymentUUID(paymentId) => pending.get(paymentId).map(pp => (paymentId, pp))
case PaymentIdentifier.PaymentHash(paymentHash) => pending.collectFirst { case (paymentId, pp) if pp.paymentHash == paymentHash => (paymentId, pp) }
case PaymentIdentifier.OfferId(offerId) => pending.collectFirst {
case (paymentId, pp@PendingPaymentToNode(_, SendPaymentToNode(_, _, invoice: Bolt12Invoice, _, _, _, _, _, _, _))) if invoice.invoiceRequest.offer.offerId == offerId =>
case (paymentId, pp@PendingPaymentToNode(_, SendPaymentToNode(_, _, invoice: Bolt12Invoice, _, _, _, _, _, _, _, _))) if invoice.invoiceRequest.offer.offerId == offerId =>
(paymentId, pp)
}
}
Expand Down Expand Up @@ -250,7 +250,8 @@ object PaymentInitiator {
routeParams: RouteParams,
payerKey_opt: Option[PrivateKey] = None,
userCustomTlvs: Set[GenericTlv] = Set.empty,
blockUntilComplete: Boolean = false) extends SendRequestedPayment
blockUntilComplete: Boolean = false,
routeAddrType_opt: Option[AddrType] = None) extends SendRequestedPayment

/**
* @param recipientAmount amount that should be received by the final recipient.
Expand All @@ -269,7 +270,8 @@ object PaymentInitiator {
externalId: Option[String] = None,
routeParams: RouteParams,
userCustomTlvs: Set[GenericTlv] = Set.empty,
recordPathFindingMetrics: Boolean = false) {
recordPathFindingMetrics: Boolean = false,
routeAddrType_opt: Option[AddrType] = None) {
val paymentHash: ByteVector32 = Crypto.sha256(paymentPreimage)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,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), routeAddrType_opt = request.routeAddrType_opt)
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 @@ -161,7 +161,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), routeAddrType_opt = request.routeAddrType_opt)
goto(WAITING_FOR_ROUTE) using WaitingForRoute(data.request, data.failures :+ failure, ignore1)
case _: SendPaymentToRoute =>
log.error("unexpected retry during SendPaymentToRoute")
Expand Down Expand Up @@ -269,7 +269,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), routeAddrType_opt = request.routeAddrType_opt)
goto(WAITING_FOR_ROUTE) using WaitingForRoute(request.copy(recipient = recipient1), failures :+ failure, ignore1)
}
} else {
Expand All @@ -280,7 +280,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), routeAddrType_opt = request.routeAddrType_opt)
goto(WAITING_FOR_ROUTE) using WaitingForRoute(request, failures :+ failure, ignore + nodeId)
}
}
Expand All @@ -294,7 +294,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), routeAddrType_opt = request.routeAddrType_opt)
goto(WAITING_FOR_ROUTE) using WaitingForRoute(request, failures :+ failure, ignore1)
}
case Right(e@Sphinx.DecryptedFailurePacket(nodeId, _, failureMessage)) =>
Expand Down Expand Up @@ -495,7 +495,7 @@ object PaymentLifecycle {
* @param maxAttempts maximum number of retries.
* @param routeParams parameters to fine-tune the routing algorithm.
*/
case class SendPaymentToNode(replyTo: ActorRef, recipient: Recipient, maxAttempts: Int, routeParams: RouteParams) extends SendPayment {
case class SendPaymentToNode(replyTo: ActorRef, recipient: Recipient, maxAttempts: Int, routeParams: RouteParams, routeAddrType_opt: Option[AddrType] = None) extends SendPayment {
require(recipient.totalAmount > 0.msat, "amount must be > 0")
override val amount: MilliSatoshi = recipient.totalAmount
}
Expand Down
31 changes: 22 additions & 9 deletions eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import fr.acinq.eclair.payment.Invoice
import fr.acinq.eclair.payment.relay.Relayer.RelayFees
import fr.acinq.eclair.router.Graph.GraphStructure.GraphEdge
import fr.acinq.eclair.router.Router._
import fr.acinq.eclair.wire.protocol.{ChannelUpdate, NodeAnnouncement}
import fr.acinq.eclair.wire.protocol.{ChannelUpdate, IPAddress, NodeAnnouncement, OnionAddress}

import scala.annotation.tailrec
import scala.collection.mutable
Expand Down Expand Up @@ -212,10 +212,11 @@ object Graph {
wr: WeightRatios[PaymentPathWeight],
currentBlockHeight: BlockHeight,
boundaries: PaymentPathWeight => Boolean,
includeLocalChannelCost: Boolean): Seq[WeightedPath[PaymentPathWeight]] = {
includeLocalChannelCost: Boolean,
routeAddrType_opt: Option[AddrType] = None): Seq[WeightedPath[PaymentPathWeight]] = {
// find the shortest path (k = 0)
val targetWeight = PaymentPathWeight(amount)
dijkstraShortestPath(g, sourceNode, targetNode, ignoredEdges, ignoredVertices, extraEdges, targetWeight, boundaries, Features.empty, currentBlockHeight, wr, includeLocalChannelCost) match {
dijkstraShortestPath(g, sourceNode, targetNode, ignoredEdges, ignoredVertices, extraEdges, targetWeight, boundaries, Features.empty, currentBlockHeight, wr, includeLocalChannelCost, routeAddrType_opt) 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 @@ -253,7 +254,7 @@ object Graph {
val alreadyExploredVertices = rootPathEdges.map(_.desc.b).toSet
val rootPathWeight = pathWeight(g.balances, 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(g, sourceNode, spurNode, ignoredEdges ++ alreadyExploredEdges, ignoredVertices ++ alreadyExploredVertices, extraEdges, rootPathWeight, boundaries, Features.empty, currentBlockHeight, wr, includeLocalChannelCost) match {
dijkstraShortestPath(g, sourceNode, spurNode, ignoredEdges ++ alreadyExploredEdges, ignoredVertices ++ alreadyExploredVertices, extraEdges, rootPathWeight, boundaries, Features.empty, currentBlockHeight, wr, includeLocalChannelCost, routeAddrType_opt) match {
case Some(spurPath) =>
val completePath = spurPath ++ rootPathEdges
val candidatePath = WeightedPath(completePath, pathWeight(g.balances, sourceNode, completePath, amount, currentBlockHeight, wr, includeLocalChannelCost))
Expand Down Expand Up @@ -305,7 +306,8 @@ object Graph {
nodeFeatures: Features[NodeFeature],
currentBlockHeight: BlockHeight,
wr: WeightRatios[RichWeight],
includeLocalChannelCost: Boolean): Option[Seq[GraphEdge]] = {
includeLocalChannelCost: Boolean,
routeAddrType_opt: Option[AddrType] = None): Option[Seq[GraphEdge]] = {
// the graph does not contain source/destination nodes
val sourceNotInGraph = !g.graph.containsVertex(sourceNode) && !extraEdges.exists(_.desc.a == sourceNode)
val targetNotInGraph = !g.graph.containsVertex(targetNode) && !extraEdges.exists(_.desc.b == targetNode)
Expand Down Expand Up @@ -344,7 +346,8 @@ object Graph {
if (current.weight.canUseEdge(edge) &&
!ignoredEdges.contains(edge.desc) &&
!ignoredVertices.contains(neighbor) &&
(neighbor == sourceNode || g.graph.getVertexFeatures(neighbor).areSupported(nodeFeatures))) {
(neighbor == sourceNode || g.graph.getVertexFeatures(neighbor).areSupported(nodeFeatures)) &&
(neighbor == sourceNode || neighbor == targetNode || routeAddrType_opt.forall(t => g.graph.getVertexRouteAddrType(neighbor).forall(AddrType.isCompatible(t, _))))) {
// 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`.
val neighborWeight = wr.addEdgeWeight(sourceNode, edge, g.balances.get(edge), current.weight, currentBlockHeight, includeLocalChannelCost)
Expand Down Expand Up @@ -530,7 +533,7 @@ object Graph {
}
}

case class Vertex(features: Features[NodeFeature], incomingEdges: Map[ChannelDesc, GraphEdge]) {
case class Vertex(features: Features[NodeFeature], incomingEdges: Map[ChannelDesc, GraphEdge], routeAddrType_opt: Option[AddrType] = None) {
def update(desc: ChannelDesc, newShortChannelId: RealShortChannelId, newCapacity: Satoshi): Vertex =
incomingEdges.get(desc) match {
case None => this
Expand Down Expand Up @@ -638,6 +641,8 @@ object Graph {

def getVertexFeatures(key: PublicKey): Features[NodeFeature] = vertices.get(key).map(_.features).getOrElse(Features.empty)

def getVertexRouteAddrType(key: PublicKey): Option[AddrType] = vertices.get(key).flatMap(_.routeAddrType_opt)

/**
* Removes a vertex and all its associated edges (both incoming and outgoing)
*/
Expand All @@ -651,9 +656,17 @@ object Graph {
* Or update the node features if the vertex is already present.
*/
def addOrUpdateVertex(ann: NodeAnnouncement): DirectedGraph = {
val hasClearnet = ann.addresses.exists(_.isInstanceOf[IPAddress])
val hasTor = ann.addresses.exists(_.isInstanceOf[OnionAddress])
val routeAddrType_opt = (hasClearnet, hasTor) match {
case (true, false) => Some(AddrType.Clearnet)
case (true, true) => Some(AddrType.Hybrid)
case (false, true) => Some(AddrType.Tor)
case (false, false) => None
}
DirectedGraph(vertices.updatedWith(ann.nodeId) {
case Some(vertex) => Some(vertex.copy(features = ann.features.nodeAnnouncementFeatures()))
case None => Some(Vertex(ann.features.nodeAnnouncementFeatures(), Map.empty))
case Some(vertex) => Some(vertex.copy(features = ann.features.nodeAnnouncementFeatures(), routeAddrType_opt = routeAddrType_opt))
case None => Some(Vertex(ann.features.nodeAnnouncementFeatures(), Map.empty, routeAddrType_opt))
})
}

Expand Down
Loading
Loading