Skip to content

Commit

Permalink
Send payment to sciddir_or_pubkey
Browse files Browse the repository at this point in the history
  • Loading branch information
thomash-acinq committed Oct 23, 2023
1 parent 49416e2 commit 7379591
Show file tree
Hide file tree
Showing 26 changed files with 150 additions and 122 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -559,7 +559,7 @@ object Bolt11Invoice {
val nextNodeIds = extraRoute.map(_.nodeId).drop(1) :+ targetNodeId
extraRoute.zip(nextNodeIds).map {
case (extraHop, nextNodeId) =>
Invoice.ExtraEdge(extraHop.nodeId, nextNodeId, extraHop.shortChannelId, extraHop.feeBase, extraHop.feeProportionalMillionths, extraHop.cltvExpiryDelta, 1 msat, None)
Invoice.ExtraEdge(Right(extraHop.nodeId), nextNodeId, extraHop.shortChannelId, extraHop.feeBase, extraHop.feeProportionalMillionths, extraHop.cltvExpiryDelta, 1 msat, None)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import fr.acinq.bitcoin.scalacompat.ByteVector32
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
import fr.acinq.eclair.payment.relay.Relayer
import fr.acinq.eclair.wire.protocol.ChannelUpdate
import fr.acinq.eclair.wire.protocol.OfferTypes.ShortChannelIdDir
import fr.acinq.eclair.{CltvExpiryDelta, Features, InvoiceFeature, MilliSatoshi, ShortChannelId, TimestampSecond}

import scala.concurrent.duration.FiniteDuration
Expand All @@ -41,7 +42,7 @@ trait Invoice {

object Invoice {
/** An extra edge that can be used to pay a given invoice and may not be part of the public graph. */
case class ExtraEdge(sourceNodeId: PublicKey,
case class ExtraEdge(sourceNodeId: Either[ShortChannelIdDir, PublicKey],
targetNodeId: PublicKey,
shortChannelId: ShortChannelId,
feeBase: MilliSatoshi,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ object OutgoingPaymentPacket {
case class CannotDecryptBlindedRoute(message: String) extends OutgoingPaymentError { override def getMessage: String = message }
case class InvalidRouteRecipient(expected: PublicKey, actual: PublicKey) extends OutgoingPaymentError { override def getMessage: String = s"expected route to $expected, got route to $actual" }
case class MissingTrampolineHop(trampolineNodeId: PublicKey) extends OutgoingPaymentError { override def getMessage: String = s"expected route to trampoline node $trampolineNodeId" }
case class MissingBlindedHop(introductionNodeIds: Set[PublicKey]) extends OutgoingPaymentError { override def getMessage: String = s"expected blinded route using one of the following introduction nodes: ${introductionNodeIds.mkString(", ")}" }
case object MissingBlindedHop extends OutgoingPaymentError { override def getMessage: String = s"expected blinded route as final hop" }
case object EmptyRoute extends OutgoingPaymentError { override def getMessage: String = "route cannot be empty" }

sealed trait Upstream
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -359,8 +359,8 @@ object MultiPartHandler {
context.pipeToSelf(Future.sequence(r.routes.map(route => {
val dummyHops = route.dummyHops.map(h => {
// We don't want to restrict HTLC size in dummy hops, so we use htlc_minimum_msat = 1 msat and htlc_maximum_msat = None.
val edge = Invoice.ExtraEdge(nodeParams.nodeId, nodeParams.nodeId, ShortChannelId.toSelf, h.feeBase, h.feeProportionalMillionths, h.cltvExpiryDelta, htlcMinimum = 1 msat, htlcMaximum_opt = None)
ChannelHop(edge.shortChannelId, edge.sourceNodeId, edge.targetNodeId, HopRelayParams.FromHint(edge))
val edge = Invoice.ExtraEdge(Right(nodeParams.nodeId), nodeParams.nodeId, ShortChannelId.toSelf, h.feeBase, h.feeProportionalMillionths, h.cltvExpiryDelta, htlcMinimum = 1 msat, htlcMaximum_opt = None)
ChannelHop(edge.shortChannelId, nodeParams.nodeId, edge.targetNodeId, HopRelayParams.FromHint(edge))
})
if (route.nodes.length == 1) {
val blindedRoute = if (dummyHops.isEmpty) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -300,18 +300,18 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A
log.info("received an update for a routing hint (shortChannelId={} nodeId={} enabled={} update={})", failure.update.shortChannelId, nodeId, failure.update.channelFlags.isEnabled, failure.update)
if (failure.update.channelFlags.isEnabled) {
data.recipient.extraEdges.map {
case edge: ExtraEdge if edge.sourceNodeId == nodeId && edge.targetNodeId == hop.nextNodeId => edge.update(failure.update)
case edge: ExtraEdge if edge.sourceNodeId == Right(nodeId) && edge.targetNodeId == hop.nextNodeId => edge.update(failure.update)
case edge: ExtraEdge => edge
}
} else {
// if the channel is disabled, we temporarily exclude it: this is necessary because the routing hint doesn't
// contain channel flags to indicate that it's disabled
// we want the exclusion to be router-wide so that sister payments in the case of MPP are aware the channel is faulty
data.recipient.extraEdges
.find(edge => edge.sourceNodeId == nodeId && edge.targetNodeId == hop.nextNodeId)
.foreach(edge => router ! ExcludeChannel(ChannelDesc(edge.shortChannelId, edge.sourceNodeId, edge.targetNodeId), Some(nodeParams.routerConf.channelExcludeDuration)))
.find(edge => edge.sourceNodeId == Right(nodeId) && edge.targetNodeId == hop.nextNodeId)
.foreach(edge => router ! ExcludeChannel(ChannelDesc(edge.shortChannelId, nodeId, edge.targetNodeId), Some(nodeParams.routerConf.channelExcludeDuration)))
// we remove this edge for our next payment attempt
data.recipient.extraEdges.filterNot(edge => edge.sourceNodeId == nodeId && edge.targetNodeId == hop.nextNodeId)
data.recipient.extraEdges.filterNot(edge => edge.sourceNodeId == Right(nodeId) && edge.targetNodeId == hop.nextNodeId)
}
}
case None =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
import fr.acinq.eclair.crypto.Sphinx
import fr.acinq.eclair.payment.Invoice.ExtraEdge
import fr.acinq.eclair.payment.OutgoingPaymentPacket._
import fr.acinq.eclair.payment.{Bolt11Invoice, Bolt12Invoice, OutgoingPaymentPacket}
import fr.acinq.eclair.payment.{Bolt11Invoice, Bolt12Invoice, OutgoingPaymentPacket, PaymentBlindedRoute}
import fr.acinq.eclair.router.Router._
import fr.acinq.eclair.wire.protocol.PaymentOnion.{FinalPayload, IntermediatePayload, OutgoingBlindedPerHopPayload}
import fr.acinq.eclair.wire.protocol.{GenericTlv, OnionRoutingPacket, PaymentOnionCodecs}
import fr.acinq.eclair.{CltvExpiry, Features, InvoiceFeature, MilliSatoshi, MilliSatoshiLong, ShortChannelId}
import fr.acinq.eclair.wire.protocol.{GenericTlv, OfferTypes, OnionRoutingPacket, PaymentOnionCodecs}
import fr.acinq.eclair.{Alias, CltvExpiry, Features, InvoiceFeature, MilliSatoshi, MilliSatoshiLong, ShortChannelId}
import scodec.bits.ByteVector

/**
Expand Down Expand Up @@ -121,18 +121,30 @@ case class BlindedRecipient(nodeId: PublicKey,
features: Features[InvoiceFeature],
totalAmount: MilliSatoshi,
expiry: CltvExpiry,
blindedHops: Seq[BlindedHop],
blindedPaths: Map[Alias, PaymentBlindedRoute],
customTlvs: Set[GenericTlv] = Set.empty) extends Recipient {
require(blindedHops.nonEmpty, "blinded routes must be provided")
require(blindedPaths.nonEmpty, "blinded routes must be provided")

override val extraEdges = blindedHops.map { h =>
ExtraEdge(h.route.introductionNodeId, nodeId, h.dummyId, h.paymentInfo.feeBase, h.paymentInfo.feeProportionalMillionths, h.paymentInfo.cltvExpiryDelta, h.paymentInfo.minHtlc, Some(h.paymentInfo.maxHtlc))
}
override val extraEdges = blindedPaths.map { case (scid, path) =>
val introductionNodeId = path.route match {
case OfferTypes.BlindedPath(route) => Right(route.introductionNodeId)
case OfferTypes.CompactBlindedPath(introductionNode, _, _) => Left(introductionNode)
}
ExtraEdge(
introductionNodeId,
nodeId,
scid,
path.paymentInfo.feeBase,
path.paymentInfo.feeProportionalMillionths,
path.paymentInfo.cltvExpiryDelta,
path.paymentInfo.minHtlc,
Some(path.paymentInfo.maxHtlc))
}.toSeq

private def validateRoute(route: Route): Either[OutgoingPaymentError, BlindedHop] = {
route.finalHop_opt match {
case Some(blindedHop: BlindedHop) => Right(blindedHop)
case _ => Left(MissingBlindedHop(blindedHops.map(_.route.introductionNodeId).toSet))
case _ => Left(MissingBlindedHop)
}
}

Expand Down Expand Up @@ -167,14 +179,7 @@ case class BlindedRecipient(nodeId: PublicKey,

object BlindedRecipient {
def apply(invoice: Bolt12Invoice, totalAmount: MilliSatoshi, expiry: CltvExpiry, customTlvs: Set[GenericTlv]): BlindedRecipient = {
val blindedHops = invoice.blindedPaths.map(
path => {
// We don't know the scids of channels inside the blinded route, but it's useful to have an ID to refer to a
// given edge in the graph, so we create a dummy one for the duration of the payment attempt.
val dummyId = ShortChannelId.generateLocalAlias()
BlindedHop(dummyId, path.route, path.paymentInfo)
})
BlindedRecipient(invoice.nodeId, invoice.features, totalAmount, expiry, blindedHops, customTlvs)
BlindedRecipient(invoice.nodeId, invoice.features, totalAmount, expiry, invoice.blindedPaths.map((ShortChannelId.generateLocalAlias(), _)).toMap, customTlvs)
}
}

Expand All @@ -201,7 +206,7 @@ case class ClearTrampolineRecipient(invoice: Bolt11Invoice,

override val nodeId = invoice.nodeId
override val features = invoice.features
override val extraEdges = Seq(ExtraEdge(trampolineNodeId, nodeId, ShortChannelId.generateLocalAlias(), trampolineFee, 0, trampolineHop.cltvExpiryDelta, 1 msat, None))
override val extraEdges = Seq(ExtraEdge(Right(trampolineNodeId), nodeId, ShortChannelId.generateLocalAlias(), trampolineFee, 0, trampolineHop.cltvExpiryDelta, 1 msat, None))

private def validateRoute(route: Route): Either[OutgoingPaymentError, NodeHop] = {
route.finalHop_opt match {
Expand Down
19 changes: 14 additions & 5 deletions eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,12 @@ import fr.acinq.bitcoin.scalacompat.{Btc, BtcDouble, MilliBtc, Satoshi}
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.{GraphEdge, DirectedGraph}
import fr.acinq.eclair.router.Graph.GraphStructure.{DirectedGraph, GraphEdge}
import fr.acinq.eclair.router.Router._
import fr.acinq.eclair.wire.protocol.{ChannelUpdate, NodeAnnouncement}

import scala.annotation.tailrec
import scala.collection.immutable.SortedMap
import scala.collection.mutable

object Graph {
Expand Down Expand Up @@ -612,15 +613,23 @@ object Graph {
balance_opt = pc.getBalanceSameSideAs(u)
)

def apply(e: Invoice.ExtraEdge): GraphEdge = {
def fromExtraEdge(e: Invoice.ExtraEdge, publicChannels: SortedMap[RealShortChannelId, PublicChannel]): Option[GraphEdge] = {
val maxBtc = 21e6.btc
GraphEdge(
desc = ChannelDesc(e.shortChannelId, e.sourceNodeId, e.targetNodeId),
val sourceNodeId = e.sourceNodeId match {
case Left(scidDir) => publicChannels.get(scidDir.scid) match {
case Some(pc) if scidDir.isNode1 => pc.nodeId1
case Some(pc) => pc.nodeId2
case None => return None
}
case Right(publicKey) => publicKey
}
Some(GraphEdge(
desc = ChannelDesc(e.shortChannelId, sourceNodeId, e.targetNodeId),
params = HopRelayParams.FromHint(e),
// Routing hints don't include the channel's capacity, so we assume it's big enough.
capacity = maxBtc.toSatoshi,
balance_opt = None,
)
))
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,18 @@ import com.softwaremill.quicklens.ModifyPimp
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
import fr.acinq.eclair.Logs.LogCategory
import fr.acinq.eclair._
import fr.acinq.eclair.message.SendingMessage
import fr.acinq.eclair.crypto.Sphinx.RouteBlinding
import fr.acinq.eclair.payment.send._
import fr.acinq.eclair.router.Graph.GraphStructure.DirectedGraph.graphEdgeToHop
import fr.acinq.eclair.router.Graph.GraphStructure.{DirectedGraph, GraphEdge}
import fr.acinq.eclair.router.Graph.{InfiniteLoop, MessagePath, NegativeProbability, RichWeight}
import fr.acinq.eclair.router.Monitoring.{Metrics, Tags}
import fr.acinq.eclair.router.Router._
import fr.acinq.eclair.wire.protocol.OfferTypes
import kamon.tag.TagSet

import scala.annotation.tailrec
import scala.collection.immutable.SortedMap
import scala.collection.mutable
import scala.util.{Failure, Random, Success, Try}

Expand Down Expand Up @@ -66,7 +68,7 @@ object RouteCalculation {
paymentHash_opt = fr.paymentContext.map(_.paymentHash))) {
implicit val sender: ActorRef = ctx.self // necessary to preserve origin when sending messages to other actors

val extraEdges = fr.extraEdges.map(GraphEdge(_))
val extraEdges = fr.extraEdges.flatMap(GraphEdge.fromExtraEdge(_, d.channels))
val g = extraEdges.foldLeft(d.graphWithBalances.graph) { case (g: DirectedGraph, e: GraphEdge) => g.addEdge(e) }

fr.route match {
Expand Down Expand Up @@ -129,7 +131,7 @@ object RouteCalculation {
*
* The routes found must then be post-processed by calling [[addFinalHop]].
*/
private def computeTarget(r: RouteRequest, ignoredEdges: Set[ChannelDesc]): (PublicKey, MilliSatoshi, MilliSatoshi, Set[GraphEdge]) = {
private def computeTarget(r: RouteRequest, ignoredEdges: Set[ChannelDesc], publicChannels: SortedMap[RealShortChannelId, PublicChannel]): (PublicKey, MilliSatoshi, MilliSatoshi, Set[GraphEdge]) = {
val pendingAmount = r.pendingPayments.map(_.amount).sum
val totalMaxFee = r.routeParams.getMaxFee(r.target.totalAmount)
val pendingChannelFee = r.pendingPayments.map(_.channelFee(r.routeParams.includeLocalChannelCost)).sum
Expand All @@ -139,8 +141,8 @@ object RouteCalculation {
val amountToSend = recipient.totalAmount - pendingAmount
val maxFee = totalMaxFee - pendingChannelFee
val extraEdges = recipient.extraEdges
.filter(_.sourceNodeId != r.source) // we ignore routing hints for our own channels, we have more accurate information
.map(GraphEdge(_))
.flatMap(GraphEdge.fromExtraEdge(_, publicChannels))
.filterNot(e => (e.desc.a == r.source) || (e.desc.b == r.source)) // we ignore routing hints for our own channels, we have more accurate information
.filterNot(e => ignoredEdges.contains(e.desc))
.toSet
(targetNodeId, amountToSend, maxFee, extraEdges)
Expand All @@ -156,7 +158,7 @@ object RouteCalculation {
.map(_.copy(targetNodeId = targetNodeId))
.filterNot(e => ignoredEdges.exists(_.shortChannelId == e.shortChannelId))
// For blinded routes, the maximum htlc field is used to indicate the maximum amount that can be sent through the route.
.map(e => GraphEdge(e).copy(balance_opt = e.htlcMaximum_opt))
.flatMap(e => GraphEdge.fromExtraEdge(e, publicChannels).map(_.copy(balance_opt = e.htlcMaximum_opt)))
.toSet
val amountToSend = recipient.totalAmount - pendingAmount
// When we are the introduction node and includeLocalChannelCost is false, we cannot easily remove the fee for
Expand All @@ -182,11 +184,16 @@ object RouteCalculation {
case _: SpontaneousRecipient => Some(route)
case recipient: ClearTrampolineRecipient => Some(route.copy(finalHop_opt = Some(recipient.trampolineHop)))
case recipient: BlindedRecipient =>
route.hops.lastOption.flatMap {
hop => recipient.blindedHops.find(_.dummyId == hop.shortChannelId)
}.map {
blindedHop => Route(route.amount, route.hops.dropRight(1), Some(blindedHop))
}
route.hops.lastOption.flatMap(lastHop =>{
val alias = Alias(lastHop.shortChannelId.toLong)
recipient.blindedPaths.get(alias).map(path => {
val blindedRoute = path.route match {
case OfferTypes.BlindedPath(blindedRoute) => blindedRoute
case OfferTypes.CompactBlindedPath(_, blindingKey, blindedNodes) => RouteBlinding.BlindedRoute(lastHop.nodeId, blindingKey, blindedNodes)
}
Route(route.amount, route.hops.dropRight(1), Some(BlindedHop(alias, blindedRoute, path.paymentInfo)))
})
})
}
})
}
Expand All @@ -200,7 +207,7 @@ object RouteCalculation {
implicit val sender: ActorRef = ctx.self // necessary to preserve origin when sending messages to other actors

val ignoredEdges = r.ignore.channels ++ d.excludedChannels.keySet
val (targetNodeId, amountToSend, maxFee, extraEdges) = computeTarget(r, ignoredEdges)
val (targetNodeId, amountToSend, maxFee, extraEdges) = computeTarget(r, ignoredEdges, d.channels)
val routesToFind = if (r.routeParams.randomize) DEFAULT_ROUTES_COUNT else 1

log.info(s"finding routes ${r.source}->$targetNodeId with assistedChannels={} ignoreNodes={} ignoreChannels={} excludedChannels={}", extraEdges.map(_.desc.shortChannelId).mkString(","), r.ignore.nodes.map(_.value).mkString(","), r.ignore.channels.mkString(","), d.excludedChannels.mkString(","))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -388,8 +388,9 @@ trait ChannelStateTestsBase extends Assertions with Eventually {
}

def makeSingleHopRoute(amount: MilliSatoshi, destination: PublicKey): Route = {
val dummyParams = HopRelayParams.FromHint(Invoice.ExtraEdge(randomKey().publicKey, destination, ShortChannelId(0), 0 msat, 0, CltvExpiryDelta(0), 0 msat, None))
Route(amount, Seq(ChannelHop(ShortChannelId(0), dummyParams.extraHop.sourceNodeId, dummyParams.extraHop.targetNodeId, dummyParams)), None)
val dummyNodeId = randomKey().publicKey
val dummyParams = HopRelayParams.FromHint(Invoice.ExtraEdge(Right(dummyNodeId), destination, ShortChannelId(0), 0 msat, 0, CltvExpiryDelta(0), 0 msat, None))
Route(amount, Seq(ChannelHop(ShortChannelId(0), dummyNodeId, dummyParams.extraHop.targetNodeId, dummyParams)), None)
}

def addHtlc(amount: MilliSatoshi, s: TestFSMRef[ChannelState, ChannelData, Channel], r: TestFSMRef[ChannelState, ChannelData, Channel], s2r: TestProbe, r2s: TestProbe): (ByteVector32, UpdateAddHtlc) = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -801,7 +801,7 @@ object PaymentsDbSpec {
def createBolt12Invoice(amount: MilliSatoshi, payerKey: PrivateKey, recipientKey: PrivateKey, preimage: ByteVector32): Bolt12Invoice = {
val offer = Offer(Some(amount), "some offer", recipientKey.publicKey, Features.empty, Block.TestnetGenesisBlock.hash)
val invoiceRequest = InvoiceRequest(offer, 789 msat, 1, Features.empty, payerKey, Block.TestnetGenesisBlock.hash)
val dummyRoute = PaymentBlindedRoute(RouteBlinding.create(randomKey(), Seq(randomKey().publicKey), Seq(randomBytes(100))).route, PaymentInfo(0 msat, 0, CltvExpiryDelta(0), 0 msat, 0 msat, Features.empty))
val dummyRoute = PaymentBlindedRoute(BlindedPath(RouteBlinding.create(randomKey(), Seq(randomKey().publicKey), Seq(randomBytes(100))).route), PaymentInfo(0 msat, 0, CltvExpiryDelta(0), 0 msat, 0 msat, Features.empty))
Bolt12Invoice(invoiceRequest, preimage, recipientKey, 1 hour, Features.empty, Seq(dummyRoute))
}
}
Loading

0 comments on commit 7379591

Please sign in to comment.