From 3d18cc236a0e4794ca34193633334d4208bbf183 Mon Sep 17 00:00:00 2001 From: Thomas HUET Date: Wed, 14 Sep 2022 10:45:14 +0200 Subject: [PATCH 1/8] Send to blinded route - Add an optional blinded route at the end of routes returned by the router - Because the destination is not known befoure routing (the router may chose between several blinded routes), we defer creating the final payload to after the routing --- .../acinq/eclair/json/JsonSerializers.scala | 14 +- .../acinq/eclair/payment/Bolt12Invoice.scala | 15 +- .../acinq/eclair/payment/PaymentPacket.scala | 140 +++++++++++++++--- .../eclair/payment/relay/NodeRelay.scala | 5 +- .../send/MultiPartPaymentLifecycle.scala | 4 +- .../payment/send/PaymentInitiator.scala | 21 +-- .../payment/send/PaymentLifecycle.scala | 128 +++++++++------- .../acinq/eclair/router/BalanceEstimate.scala | 4 +- .../eclair/router/RouteCalculation.scala | 12 +- .../scala/fr/acinq/eclair/router/Router.scala | 27 ++-- .../eclair/wire/protocol/OfferTypes.scala | 6 +- .../eclair/wire/protocol/PaymentOnion.scala | 29 +++- .../eclair/wire/protocol/RouteBlinding.scala | 6 +- .../fr/acinq/eclair/channel/FuzzySpec.scala | 2 +- .../ChannelStateTestsHelperMethods.scala | 2 +- .../channel/states/f/ShutdownStateSpec.scala | 4 +- .../eclair/payment/Bolt12InvoiceSpec.scala | 2 +- .../eclair/payment/MultiPartHandlerSpec.scala | 2 +- .../MultiPartPaymentLifecycleSpec.scala | 108 +++++++------- .../eclair/payment/PaymentInitiatorSpec.scala | 42 +++--- .../eclair/payment/PaymentLifecycleSpec.scala | 78 +++++----- .../eclair/payment/PaymentPacketSpec.scala | 59 ++++---- .../payment/PostRestartHtlcCleanerSpec.scala | 4 +- .../payment/relay/NodeRelayerSpec.scala | 7 +- .../eclair/payment/relay/RelayerSpec.scala | 14 +- .../eclair/router/RouteCalculationSpec.scala | 22 +-- .../fr/acinq/eclair/router/RouterSpec.scala | 66 ++++----- .../src/test/resources/api/findroute-full | 2 +- .../fr/acinq/eclair/api/ApiServiceSpec.scala | 2 +- 29 files changed, 483 insertions(+), 344 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala index dfd05bb85b..f173ea3015 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala @@ -295,12 +295,12 @@ object ColorSerializer extends MinimalSerializer({ // @formatter:off private case class ChannelHopJson(nodeId: PublicKey, nextNodeId: PublicKey, source: ChannelRelayParams) -private case class RouteFullJson(amount: MilliSatoshi, hops: Seq[ChannelHopJson]) -object RouteFullSerializer extends ConvertClassSerializer[Route](route => RouteFullJson(route.amount, route.hops.map(h => ChannelHopJson(h.nodeId, h.nextNodeId, h.params)))) +private case class RouteFullJson(amount: MilliSatoshi, clearHops: Seq[ChannelHopJson], blindedEnd: Option[BlindedPaymentRoute]) +object RouteFullSerializer extends ConvertClassSerializer[Route](route => RouteFullJson(route.amount, route.clearHops.map(h => ChannelHopJson(h.nodeId, h.nextNodeId, h.params)), route.blinded_opt)) private case class RouteNodeIdsJson(amount: MilliSatoshi, nodeIds: Seq[PublicKey]) object RouteNodeIdsSerializer extends ConvertClassSerializer[Route](route => { - val nodeIds = route.hops match { + val nodeIds = route.clearHops match { case rest :+ last => rest.map(_.nodeId) :+ last.nodeId :+ last.nextNodeId case Nil => Nil } @@ -308,7 +308,7 @@ object RouteNodeIdsSerializer extends ConvertClassSerializer[Route](route => { }) private case class RouteShortChannelIdsJson(amount: MilliSatoshi, shortChannelIds: Seq[ShortChannelId]) -object RouteShortChannelIdsSerializer extends ConvertClassSerializer[Route](route => RouteShortChannelIdsJson(route.amount, route.hops.map(_.shortChannelId))) +object RouteShortChannelIdsSerializer extends ConvertClassSerializer[Route](route => RouteShortChannelIdsJson(route.amount, route.clearHops.map(_.shortChannelId))) // @formatter:on // @formatter:off @@ -404,10 +404,10 @@ object InvoiceSerializer extends MinimalSerializer({ FeatureSupportSerializer + UnknownFeatureSerializer )), - JField("blindedPaths", JArray(p.blindedPaths.map(path => { + JField("blindedPaths", JArray(p.blindedPaymentRoutes.map(paymentRoute => { JObject(List( - JField("introductionNodeId", JString(path.introductionNodeId.toString())), - JField("blindedNodeIds", JArray(path.blindedNodes.map(n => JString(n.blindedPublicKey.toString())).toList)) + JField("introductionNodeId", JString(paymentRoute.route.introductionNodeId.toString())), + JField("blindedNodeIds", JArray(paymentRoute.route.blindedNodes.map(n => JString(n.blindedPublicKey.toString())).toList)) )) }).toList)), JField("createdAt", JLong(p.createdAt.toLong)), diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt12Invoice.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt12Invoice.scala index 555ca3473a..d361d7e1ec 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt12Invoice.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt12Invoice.scala @@ -31,6 +31,8 @@ import java.util.concurrent.TimeUnit import scala.concurrent.duration.FiniteDuration import scala.util.{Failure, Try} +case class BlindedPaymentRoute(route: RouteBlinding.BlindedRoute, paymentInfo: PaymentInfo, capacity_opt: Option[MilliSatoshi]) + /** * Lightning Bolt 12 invoice * see https://github.com/lightning/bolts/blob/master/12-offer-encoding.md @@ -53,7 +55,17 @@ case class Bolt12Invoice(records: TlvStream[InvoiceTlv]) extends Invoice { override val features: Features[InvoiceFeature] = records.get[FeaturesTlv].map(_.features.invoiceFeatures()).getOrElse(Features.empty) val chain: ByteVector32 = records.get[Chain].map(_.hash).getOrElse(Block.LivenetGenesisBlock.hash) val offerId: Option[ByteVector32] = records.get[OfferId].map(_.offerId) - val blindedPaths: Seq[RouteBlinding.BlindedRoute] = records.get[Paths].get.paths + val blindedPaymentRoutes: Seq[BlindedPaymentRoute] = { + val routesAndInfos = records.get[Paths].get.paths.zip(records.get[PaymentPathsInfo].get.paymentInfo) + records.get[PaymentPathsCapacities] match { + case Some(PaymentPathsCapacities(capacities)) => routesAndInfos.zip(capacities).map { + case ((route, payInfo), capacity) => BlindedPaymentRoute(route, payInfo, Some(capacity)) + } + case None => routesAndInfos.map { + case (route, payInfo) => BlindedPaymentRoute(route, payInfo, None) + } + } + } val issuer: Option[String] = records.get[Issuer].map(_.issuer) val quantity: Option[Long] = records.get[Quantity].map(_.quantity) val refundFor: Option[ByteVector32] = records.get[RefundFor].map(_.refundedPaymentHash) @@ -153,6 +165,7 @@ object Bolt12Invoice { if (records.get[Description].isEmpty) return Left(MissingRequiredTlv(UInt64(10))) if (records.get[Paths].isEmpty) return Left(MissingRequiredTlv(UInt64(16))) if (records.get[PaymentPathsInfo].map(_.paymentInfo.length) != records.get[Paths].map(_.paths.length)) return Left(MissingRequiredTlv(UInt64(18))) + if (records.get[PaymentPathsCapacities].exists(_.capacities.length != records.get[Paths].get.paths.length)) return Left(MissingRequiredTlv(UInt64(19))) if (records.get[NodeId].isEmpty) return Left(MissingRequiredTlv(UInt64(30))) if (records.get[CreatedAt].isEmpty) return Left(MissingRequiredTlv(UInt64(40))) if (records.get[PaymentHash].isEmpty) return Left(MissingRequiredTlv(UInt64(42))) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala index 2a4cb71588..eed14f2bc8 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala @@ -23,14 +23,16 @@ import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} import fr.acinq.eclair.channel.{CMD_ADD_HTLC, CMD_FAIL_HTLC, CannotExtractSharedSecret, Origin} import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.router.Router.{ChannelHop, Hop, NodeHop} +import fr.acinq.eclair.wire.protocol.OnionPaymentPayloadTlv.EncryptedRecipientData import fr.acinq.eclair.wire.protocol.PaymentOnion.{FinalPayload, IntermediatePayload, PerHopPayload} +import fr.acinq.eclair.wire.protocol.RouteBlindingEncryptedDataCodecs.RouteBlindingDecryptedData import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Feature, Features, MilliSatoshi, ShortChannelId, UInt64, randomBytes32, randomKey} import scodec.bits.ByteVector import scodec.{Attempt, DecodeResult} import java.util.UUID -import scala.util.Try +import scala.util.{Failure, Try} /** * Created by t-bast on 08/10/2019. @@ -235,15 +237,41 @@ object OutgoingPaymentPacket { /** * Build the onion payloads for each hop. * - * @param hops the hops as computed by the router + extra routes from the invoice - * @param finalPayload payload data for the final node (amount, expiry, etc) + * @param clearHops the hops as computed by the router + extra routes from the invoice + * @param blindedEnd_opt blinded part of the route * @return a (firstAmount, firstExpiry, payloads) tuple where: * - firstAmount is the amount for the first htlc in the route * - firstExpiry is the cltv expiry for the first htlc in the route * - a sequence of payloads that will be used to build the onion */ - def buildPayloads(hops: Seq[Hop], finalPayload: FinalPayload): (MilliSatoshi, CltvExpiry, Seq[PerHopPayload]) = { - hops.reverse.foldLeft((finalPayload.amount, finalPayload.expiry, Seq[PerHopPayload](finalPayload))) { + def buildPayloads(clearHops: Seq[Hop], + blindedEnd_opt: Option[BlindedPaymentRoute], + amount: MilliSatoshi, + totalAmount: MilliSatoshi, + expiry: CltvExpiry, + paymentSecret: ByteVector32, + paymentMetadata: Option[ByteVector], + additionalTlvs: Seq[OnionPaymentPayloadTlv], + userCustomTlvs: Seq[GenericTlv], + skipIntroduction: Boolean): (MilliSatoshi, CltvExpiry, Seq[PerHopPayload]) = { + val (endAmount, endExpiry, endPayloads): (MilliSatoshi, CltvExpiry, Seq[PaymentOnion.PerHopPayload]) = blindedEnd_opt match { + case Some(blinded) => + val blindedPayloads = if (blinded.route.encryptedPayloads.length > 1) { + val middlePayloads = blinded.route.encryptedPayloads.drop(1).dropRight(1).map(IntermediatePayload.ChannelRelay.Blinded.create(_, None)) + val finalPayload = FinalPayload.Blinded.create(amount, expiry, blinded.route.encryptedPayloads.last, None, additionalTlvs, userCustomTlvs) + if (skipIntroduction) { + middlePayloads :+ finalPayload + } else { + val introductionPayload = IntermediatePayload.ChannelRelay.Blinded.create(blinded.route.encryptedPayloads.head, Some(blinded.route.blindingKey)) + introductionPayload +: middlePayloads :+ finalPayload + } + } else { + Seq(FinalPayload.Blinded.create(amount, expiry, blinded.route.encryptedPayloads.last, Some(blinded.route.blindingKey), additionalTlvs, userCustomTlvs)) + } + (amount + blinded.paymentInfo.fee(amount), expiry + blinded.paymentInfo.cltvExpiryDelta, blindedPayloads) + case None => (amount, expiry, Seq(FinalPayload.Standard.createMultiPartPayload(amount, totalAmount, expiry, paymentSecret, paymentMetadata, additionalTlvs, userCustomTlvs))) + } + clearHops.reverse.foldLeft((endAmount, endExpiry, endPayloads)) { case ((amount, expiry, payloads), hop) => val payload = hop match { case hop: ChannelHop => IntermediatePayload.ChannelRelay.Standard(hop.shortChannelId, amount, expiry) @@ -256,47 +284,77 @@ object OutgoingPaymentPacket { /** * Build an encrypted onion packet with the given final payload. * - * @param hops the hops as computed by the router + extra routes from the invoice, including ourselves in the first hop - * @param finalPayload payload data for the final node (amount, expiry, etc) + * @param clearHops the hops as computed by the router + extra routes from the invoice, including ourselves in the first hop + * @param blindedEnd_opt blinded part of the route * @return a (firstAmount, firstExpiry, onion) tuple where: * - firstAmount is the amount for the first htlc in the route * - firstExpiry is the cltv expiry for the first htlc in the route * - the onion to include in the HTLC */ - private def buildPacket(packetPayloadLength: Int, paymentHash: ByteVector32, hops: Seq[Hop], finalPayload: FinalPayload): Try[(MilliSatoshi, CltvExpiry, Sphinx.PacketAndSecrets)] = { - val (firstAmount, firstExpiry, payloads) = buildPayloads(hops.drop(1), finalPayload) - val nodes = hops.map(_.nextNodeId) + private def buildPacket(packetPayloadLength: Int, + paymentHash: ByteVector32, + clearHops: Seq[Hop], + blindedEnd_opt: Option[BlindedPaymentRoute], + amount: MilliSatoshi, + totalAmount: MilliSatoshi, + expiry: CltvExpiry, + paymentSecret: ByteVector32, + paymentMetadata: Option[ByteVector], + additionalTlvs: Seq[OnionPaymentPayloadTlv], + userCustomTlvs: Seq[GenericTlv]): Try[(MilliSatoshi, CltvExpiry, Sphinx.PacketAndSecrets)] = { + val (firstAmount, firstExpiry, payloads) = buildPayloads(clearHops.drop(1), blindedEnd_opt, amount, totalAmount, expiry, paymentSecret, paymentMetadata, additionalTlvs, userCustomTlvs, clearHops.isEmpty) + val clearNodes = clearHops.map(_.nextNodeId) + val nodes = blindedEnd_opt match { + case Some(blinded) => clearNodes ++ blinded.route.blindedNodeIds.drop(1) + case None => clearNodes + } // BOLT 2 requires that associatedData == paymentHash buildOnion(packetPayloadLength, nodes, payloads, paymentHash).map(onion => (firstAmount, firstExpiry, onion)) } - def buildPaymentPacket(paymentHash: ByteVector32, hops: Seq[Hop], finalPayload: FinalPayload): Try[(MilliSatoshi, CltvExpiry, Sphinx.PacketAndSecrets)] = - buildPacket(PaymentOnionCodecs.paymentOnionPayloadLength, paymentHash, hops, finalPayload) + def buildPaymentPacket(paymentHash: ByteVector32, + clearHops: Seq[Hop], + blindedEnd_opt: Option[BlindedPaymentRoute], + amount: MilliSatoshi, + totalAmount: MilliSatoshi, + expiry: CltvExpiry, + paymentSecret: ByteVector32, + paymentMetadata: Option[ByteVector], + additionalTlvs: Seq[OnionPaymentPayloadTlv], + userCustomTlvs: Seq[GenericTlv]): Try[(MilliSatoshi, CltvExpiry, Sphinx.PacketAndSecrets)] = + buildPacket(PaymentOnionCodecs.paymentOnionPayloadLength, paymentHash, clearHops, blindedEnd_opt, amount, totalAmount, expiry, paymentSecret, paymentMetadata, additionalTlvs, userCustomTlvs) - def buildTrampolinePacket(paymentHash: ByteVector32, hops: Seq[Hop], finalPayload: FinalPayload): Try[(MilliSatoshi, CltvExpiry, Sphinx.PacketAndSecrets)] = - buildPacket(PaymentOnionCodecs.trampolineOnionPayloadLength, paymentHash, hops, finalPayload) + def buildTrampolinePacket(paymentHash: ByteVector32, + hops: Seq[Hop], + amount: MilliSatoshi, + totalAmount: MilliSatoshi, + expiry: CltvExpiry, + paymentSecret: ByteVector32, + paymentMetadata: Option[ByteVector], + additionalTlvs: Seq[OnionPaymentPayloadTlv], + userCustomTlvs: Seq[GenericTlv]): Try[(MilliSatoshi, CltvExpiry, Sphinx.PacketAndSecrets)] = + buildPacket(PaymentOnionCodecs.trampolineOnionPayloadLength, paymentHash, hops, None, amount, totalAmount, expiry, paymentSecret, paymentMetadata, additionalTlvs, userCustomTlvs) /** * Build an encrypted trampoline onion packet when the final recipient doesn't support trampoline. * The next-to-last trampoline node payload will contain instructions to convert to a legacy payment. * - * @param invoice Bolt 11 invoice (features and routing hints will be provided to the next-to-last node). - * @param hops the trampoline hops (including ourselves in the first hop, and the non-trampoline final recipient in the last hop). - * @param finalPayload payload data for the final node (amount, expiry, etc) + * @param invoice Bolt 11 invoice (features and routing hints will be provided to the next-to-last node). + * @param hops the trampoline hops (including ourselves in the first hop, and the non-trampoline final recipient in the last hop). * @return a (firstAmount, firstExpiry, onion) tuple where: * - firstAmount is the amount for the trampoline node in the route * - firstExpiry is the cltv expiry for the first trampoline node in the route * - the trampoline onion to include in final payload of a normal onion */ - def buildTrampolineToLegacyPacket(invoice: Bolt11Invoice, hops: Seq[NodeHop], finalPayload: FinalPayload): Try[(MilliSatoshi, CltvExpiry, Sphinx.PacketAndSecrets)] = { + def buildTrampolineToLegacyPacket(invoice: Bolt11Invoice, hops: Seq[NodeHop], amount: MilliSatoshi, expiry: CltvExpiry): Try[(MilliSatoshi, CltvExpiry, Sphinx.PacketAndSecrets)] = { // NB: the final payload will never reach the recipient, since the next-to-last node in the trampoline route will convert that to a non-trampoline payment. // We use the smallest final payload possible, otherwise we may overflow the trampoline onion size. - val dummyFinalPayload = FinalPayload.Standard.createSinglePartPayload(finalPayload.amount, finalPayload.expiry, randomBytes32(), None) - val (firstAmount, firstExpiry, payloads) = hops.drop(1).reverse.foldLeft((finalPayload.amount, finalPayload.expiry, Seq[PerHopPayload](dummyFinalPayload))) { + val dummyFinalPayload = FinalPayload.Standard.createSinglePartPayload(amount, expiry, randomBytes32(), None) + val (firstAmount, firstExpiry, payloads) = hops.drop(1).reverse.foldLeft((amount, expiry, Seq[PerHopPayload](dummyFinalPayload))) { case ((amount, expiry, payloads), hop) => // The next-to-last node in the trampoline route must receive invoice data to indicate the conversion to a non-trampoline payment. val payload = if (payloads.length == 1) { - IntermediatePayload.NodeRelay.Standard.createNodeRelayToNonTrampolinePayload(finalPayload.amount, finalPayload.totalAmount, finalPayload.expiry, hop.nextNodeId, invoice) + IntermediatePayload.NodeRelay.Standard.createNodeRelayToNonTrampolinePayload(amount, amount, expiry, hop.nextNodeId, invoice) } else { IntermediatePayload.NodeRelay.Standard(amount, expiry, hop.nextNodeId) } @@ -322,10 +380,44 @@ object OutgoingPaymentPacket { * * @return the command and the onion shared secrets (used to decrypt the error in case of payment failure) */ - def buildCommand(replyTo: ActorRef, upstream: Upstream, paymentHash: ByteVector32, hops: Seq[ChannelHop], finalPayload: FinalPayload): Try[(CMD_ADD_HTLC, Seq[(ByteVector32, PublicKey)])] = { - buildPaymentPacket(paymentHash, hops, finalPayload).map { + def buildCommand(privateKey: PrivateKey, + replyTo: ActorRef, + upstream: Upstream, + paymentHash: ByteVector32, + clearHops: Seq[ChannelHop], + blindedEnd_opt: Option[BlindedPaymentRoute], + amount: MilliSatoshi, + totalAmount: MilliSatoshi, + expiry: CltvExpiry, + paymentSecret: ByteVector32, + paymentMetadata: Option[ByteVector], + additionalTlvs: Seq[OnionPaymentPayloadTlv], + userCustomTlvs: Seq[GenericTlv]): Try[(CMD_ADD_HTLC, Seq[(ByteVector32, PublicKey)], ShortChannelId)] = { + val (shortChannelId, nextBlindingKey_opt, blindedRoute_opt) = if (clearHops.nonEmpty) { + (clearHops.head.shortChannelId, None, blindedEnd_opt) + } else { + blindedEnd_opt match { + case Some(paymentRoute) if paymentRoute.route.introductionNodeId == privateKey.publicKey => + // We assume that there is a next node that is not us, that should be checked before calling the router. + RouteBlindingEncryptedDataCodecs.decode(privateKey, paymentRoute.route.blindingKey, paymentRoute.route.encryptedPayloads.head) match { + case Left(e) => return Failure(e) + case Right(RouteBlindingDecryptedData(encryptedDataTlvs, nextBlindingKey)) => + IntermediatePayload.ChannelRelay.Blinded.validate(TlvStream(EncryptedRecipientData(ByteVector.empty)), encryptedDataTlvs, nextBlindingKey) match { + case Left(invalidTlv) => return Failure(RouteBlindingEncryptedDataCodecs.CannotDecodeData(invalidTlv.failureMessage.message)) + case Right(payload) => + // We assume that fees were checked in the router. + val amountWithFees = amount + paymentRoute.paymentInfo.fee(amount) + val remainingFee = amountWithFees - payload.amountToForward(amountWithFees) + val tailPaymentInfo = paymentRoute.paymentInfo.copy(feeBase = remainingFee, feeProportionalMillionths = 0, cltvExpiryDelta = paymentRoute.paymentInfo.cltvExpiryDelta - payload.cltvExpiryDelta) + (payload.outgoingChannelId, Some(nextBlindingKey), Some(paymentRoute.copy(paymentInfo = tailPaymentInfo))) + } + } + case _ => return Failure(new Exception("Invalid payment route")) + } + } + buildPaymentPacket(paymentHash, clearHops, blindedRoute_opt, amount, totalAmount, expiry, paymentSecret, paymentMetadata, additionalTlvs, userCustomTlvs).map { case (firstAmount, firstExpiry, onion) => - CMD_ADD_HTLC(replyTo, firstAmount, paymentHash, firstExpiry, onion.packet, None, Origin.Hot(replyTo, upstream), commit = true) -> onion.sharedSecrets + (CMD_ADD_HTLC(replyTo, firstAmount, paymentHash, firstExpiry, onion.packet, nextBlindingKey_opt, Origin.Hot(replyTo, upstream), commit = true), onion.sharedSecrets, shortChannelId) } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelay.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelay.scala index 5dcdfba6cc..8362540770 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelay.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelay.scala @@ -38,7 +38,7 @@ import fr.acinq.eclair.payment.send.PaymentLifecycle.SendPaymentToNode import fr.acinq.eclair.payment.send.{MultiPartPaymentLifecycle, PaymentInitiator, PaymentLifecycle} import fr.acinq.eclair.router.Router.RouteParams import fr.acinq.eclair.router.{BalanceTooLow, RouteNotFound} -import fr.acinq.eclair.wire.protocol.PaymentOnion.{FinalPayload, IntermediatePayload} +import fr.acinq.eclair.wire.protocol.PaymentOnion.IntermediatePayload import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.{BlockHeight, CltvExpiry, Features, Logs, MilliSatoshi, NodeParams, UInt64, nodeFee, randomBytes32} @@ -317,8 +317,7 @@ class NodeRelay private(nodeParams: NodeParams, payFSM } else { context.log.debug("sending the payment to non-trampoline recipient without MPP") - val finalPayload = FinalPayload.Standard.createSinglePartPayload(payloadOut.amountToForward, payloadOut.outgoingCltv, paymentSecret, payloadOut.paymentMetadata) - val payment = SendPaymentToNode(payFsmAdapters, payloadOut.outgoingNodeId, finalPayload, nodeParams.maxPaymentAttempts, extraEdges, routeParams) + val payment = SendPaymentToNode(payFsmAdapters, payloadOut.outgoingNodeId, payloadOut.amountToForward, payloadOut.amountToForward, payloadOut.outgoingCltv, paymentSecret, payloadOut.paymentMetadata, nodeParams.maxPaymentAttempts, extraEdges, routeParams) val payFSM = outgoingPaymentFactory.spawnOutgoingPayFSM(context, paymentCfg, multiPart = false) payFSM ! payment payFSM diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala index d0bc976e6e..09ea7bb296 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala @@ -30,7 +30,6 @@ import fr.acinq.eclair.payment._ import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentConfig import fr.acinq.eclair.payment.send.PaymentLifecycle.SendPaymentToRoute import fr.acinq.eclair.router.Router._ -import fr.acinq.eclair.wire.protocol.PaymentOnion.FinalPayload import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.{CltvExpiry, FSMDiagnosticActorLogging, Logs, MilliSatoshi, MilliSatoshiLong, NodeParams, TimestampMilli} import scodec.bits.ByteVector @@ -405,8 +404,7 @@ object MultiPartPaymentLifecycle { Some(cfg.paymentContext)) private def createChildPayment(replyTo: ActorRef, route: Route, request: SendMultiPartPayment): SendPaymentToRoute = { - val finalPayload = FinalPayload.Standard.createMultiPartPayload(route.amount, request.totalAmount, request.targetExpiry, request.paymentSecret, request.paymentMetadata, request.additionalTlvs, request.userCustomTlvs) - SendPaymentToRoute(replyTo, Right(route), finalPayload) + SendPaymentToRoute(replyTo, Right(route), route.amount, request.totalAmount, request.targetExpiry, request.paymentSecret, request.paymentMetadata, additionalTlvs = request.additionalTlvs, userCustomTlvs = request.userCustomTlvs) } /** When we receive an error from the final recipient or payment gets settled on chain, we should fail the whole payment, it's useless to retry. */ diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala index 6ba2e001d4..793582bd9c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala @@ -62,9 +62,8 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn fsm ! MultiPartPaymentLifecycle.SendMultiPartPayment(self, paymentSecret, r.recipientNodeId, r.recipientAmount, finalExpiry, r.maxAttempts, r.invoice.paymentMetadata, r.invoice.extraEdges, r.routeParams, userCustomTlvs = r.userCustomTlvs) context become main(pending + (paymentId -> PendingPaymentToNode(sender(), r))) case Some(paymentSecret) => - val finalPayload = FinalPayload.Standard.createSinglePartPayload(r.recipientAmount, finalExpiry, paymentSecret, r.invoice.paymentMetadata, r.userCustomTlvs) val fsm = outgoingPaymentFactory.spawnOutgoingPayment(context, paymentCfg) - fsm ! PaymentLifecycle.SendPaymentToNode(self, r.recipientNodeId, finalPayload, r.maxAttempts, r.invoice.extraEdges, r.routeParams) + fsm ! PaymentLifecycle.SendPaymentToNode(self, r.recipientNodeId, r.recipientAmount, r.recipientAmount, finalExpiry, paymentSecret, r.invoice.paymentMetadata, r.maxAttempts, r.invoice.extraEdges, r.routeParams, userCustomTlvs = r.userCustomTlvs) context become main(pending + (paymentId -> PendingPaymentToNode(sender(), r))) } @@ -73,9 +72,8 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn sender() ! paymentId val paymentCfg = SendPaymentConfig(paymentId, paymentId, r.externalId, r.paymentHash, r.recipientAmount, r.recipientNodeId, Upstream.Local(paymentId), None, storeInDb = true, publishEvent = true, recordPathFindingMetrics = r.recordPathFindingMetrics, Nil) val finalExpiry = Channel.MIN_CLTV_EXPIRY_DELTA.toCltvExpiry(nodeParams.currentBlockHeight + 1) - val finalPayload = FinalPayload.Standard(TlvStream(Seq(OnionPaymentPayloadTlv.AmountToForward(r.recipientAmount), OnionPaymentPayloadTlv.OutgoingCltv(finalExpiry), OnionPaymentPayloadTlv.PaymentData(randomBytes32(), r.recipientAmount), OnionPaymentPayloadTlv.KeySend(r.paymentPreimage)), r.userCustomTlvs)) val fsm = outgoingPaymentFactory.spawnOutgoingPayment(context, paymentCfg) - fsm ! PaymentLifecycle.SendPaymentToNode(self, r.recipientNodeId, finalPayload, r.maxAttempts, routeParams = r.routeParams) + fsm ! PaymentLifecycle.SendPaymentToNode(self, r.recipientNodeId, r.recipientAmount, r.recipientAmount, finalExpiry, randomBytes32(), None, r.maxAttempts, routeParams = r.routeParams, additionalTlvs = Seq(OnionPaymentPayloadTlv.KeySend(r.paymentPreimage))) context become main(pending + (paymentId -> PendingSpontaneousPayment(sender(), r))) case r: SendTrampolinePayment => @@ -114,7 +112,7 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn val trampolineSecret = r.trampolineSecret.getOrElse(randomBytes32()) sender() ! SendPaymentToRouteResponse(paymentId, parentPaymentId, Some(trampolineSecret)) val payFsm = outgoingPaymentFactory.spawnOutgoingPayment(context, paymentCfg) - payFsm ! PaymentLifecycle.SendPaymentToRoute(self, Left(r.route), FinalPayload.Standard.createMultiPartPayload(r.amount, trampolineAmount, trampolineExpiry, trampolineSecret, r.invoice.paymentMetadata, Seq(OnionPaymentPayloadTlv.TrampolineOnion(trampolineOnion))), r.invoice.extraEdges) + payFsm ! PaymentLifecycle.SendPaymentToRoute(self, Left(r.route), r.amount, trampolineAmount, trampolineExpiry, trampolineSecret, r.invoice.paymentMetadata, r.invoice.extraEdges, additionalTlvs = Seq(OnionPaymentPayloadTlv.TrampolineOnion(trampolineOnion))) context become main(pending + (paymentId -> PendingPaymentToRoute(sender(), r))) case Failure(t) => log.warning("cannot send outgoing trampoline payment: {}", t.getMessage) @@ -123,7 +121,7 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn case Nil => sender() ! SendPaymentToRouteResponse(paymentId, parentPaymentId, None) val payFsm = outgoingPaymentFactory.spawnOutgoingPayment(context, paymentCfg) - payFsm ! PaymentLifecycle.SendPaymentToRoute(self, Left(r.route), FinalPayload.Standard.createMultiPartPayload(r.amount, r.recipientAmount, finalExpiry, r.invoice.paymentSecret.get, r.invoice.paymentMetadata), r.invoice.extraEdges) + payFsm ! PaymentLifecycle.SendPaymentToRoute(self, Left(r.route), r.amount, r.recipientAmount, finalExpiry, r.invoice.paymentSecret.get, r.invoice.paymentMetadata, r.invoice.extraEdges) context become main(pending + (paymentId -> PendingPaymentToRoute(sender(), r))) case _ => sender() ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(r.recipientAmount, Nil, TrampolineMultiNodeNotSupported) :: Nil) @@ -196,17 +194,12 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn NodeHop(nodeParams.nodeId, trampolineNodeId, nodeParams.channelConf.expiryDelta, 0 msat), NodeHop(trampolineNodeId, r.recipientNodeId, trampolineExpiryDelta, trampolineFees) // for now we only use a single trampoline hop ) - val finalPayload = if (r.invoice.features.hasFeature(Features.BasicMultiPartPayment)) { - FinalPayload.Standard.createMultiPartPayload(r.recipientAmount, r.recipientAmount, r.finalExpiry(nodeParams.currentBlockHeight), r.invoice.paymentSecret.get, r.invoice.paymentMetadata) - } else { - FinalPayload.Standard.createSinglePartPayload(r.recipientAmount, r.finalExpiry(nodeParams.currentBlockHeight), r.invoice.paymentSecret.get, r.invoice.paymentMetadata) - } // We assume that the trampoline node supports multi-part payments (it should). val trampolinePacket_opt = if (r.invoice.features.hasFeature(Features.TrampolinePaymentPrototype)) { - OutgoingPaymentPacket.buildTrampolinePacket(r.paymentHash, trampolineRoute, finalPayload) + OutgoingPaymentPacket.buildTrampolinePacket(r.paymentHash, trampolineRoute, r.recipientAmount, r.recipientAmount, r.finalExpiry(nodeParams.currentBlockHeight), r.invoice.paymentSecret.get, r.invoice.paymentMetadata, Nil, Nil) } else { r.invoice match { - case invoice: Bolt11Invoice => OutgoingPaymentPacket.buildTrampolineToLegacyPacket(invoice, trampolineRoute, finalPayload) + case invoice: Bolt11Invoice => OutgoingPaymentPacket.buildTrampolineToLegacyPacket(invoice, trampolineRoute, r.recipientAmount, r.finalExpiry(nodeParams.currentBlockHeight)) case _ => Failure(new Exception("Trampoline to legacy is only supported for Bolt11 invoices.")) } } @@ -423,7 +416,7 @@ object PaymentInitiator { publishEvent: Boolean, recordPathFindingMetrics: Boolean, additionalHops: Seq[NodeHop]) { - def fullRoute(route: Route): Seq[Hop] = route.hops ++ additionalHops + def fullRoute(route: Route): Seq[Hop] = route.clearHops ++ additionalHops def createPaymentSent(preimage: ByteVector32, parts: Seq[PaymentSent.PartialPayment]) = PaymentSent(parentId, paymentHash, preimage, recipientAmount, recipientNodeId, parts) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala index 561dd517df..d2fef2750f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala @@ -34,8 +34,8 @@ import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentConfig import fr.acinq.eclair.payment.send.PaymentLifecycle._ import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.router._ -import fr.acinq.eclair.wire.protocol.PaymentOnion._ import fr.acinq.eclair.wire.protocol._ +import scodec.bits.ByteVector import java.util.concurrent.TimeUnit import scala.util.{Failure, Success} @@ -55,21 +55,21 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A when(WAITING_FOR_REQUEST) { case Event(c: SendPaymentToRoute, WaitingForRequest) => - log.debug("sending {} to route {}", c.finalPayload.amount, c.printRoute()) + log.debug("sending {} to route {}", c.amount, c.printRoute()) c.route.fold( - hops => router ! FinalizeRoute(c.finalPayload.amount, hops, c.extraEdges, paymentContext = Some(cfg.paymentContext)), + hops => router ! FinalizeRoute(c.amount, hops, c.extraEdges, paymentContext = Some(cfg.paymentContext)), route => self ! RouteResponse(route :: Nil) ) if (cfg.storeInDb) { - paymentsDb.addOutgoingPayment(OutgoingPayment(id, cfg.parentId, cfg.externalId, paymentHash, PaymentType.Standard, c.finalPayload.amount, cfg.recipientAmount, cfg.recipientNodeId, TimestampMilli.now(), cfg.invoice, OutgoingPaymentStatus.Pending)) + paymentsDb.addOutgoingPayment(OutgoingPayment(id, cfg.parentId, cfg.externalId, paymentHash, PaymentType.Standard, c.amount, cfg.recipientAmount, cfg.recipientNodeId, TimestampMilli.now(), cfg.invoice, OutgoingPaymentStatus.Pending)) } goto(WAITING_FOR_ROUTE) using WaitingForRoute(c, Nil, Ignore.empty) case Event(c: SendPaymentToNode, WaitingForRequest) => - log.debug("sending {} to {}", c.finalPayload.amount, c.targetNodeId) - router ! RouteRequest(nodeParams.nodeId, c.targetNodeId, c.finalPayload.amount, c.maxFee, c.extraEdges, routeParams = c.routeParams, paymentContext = Some(cfg.paymentContext)) + log.debug("sending {} to {}", c.amount, c.targetNodeId) + router ! RouteRequest(nodeParams.nodeId, c.targetNodeId, c.amount, c.maxFee, c.extraEdges, routeParams = c.routeParams, paymentContext = Some(cfg.paymentContext)) if (cfg.storeInDb) { - paymentsDb.addOutgoingPayment(OutgoingPayment(id, cfg.parentId, cfg.externalId, paymentHash, PaymentType.Standard, c.finalPayload.amount, cfg.recipientAmount, cfg.recipientNodeId, TimestampMilli.now(), cfg.invoice, OutgoingPaymentStatus.Pending)) + paymentsDb.addOutgoingPayment(OutgoingPayment(id, cfg.parentId, cfg.externalId, paymentHash, PaymentType.Standard, c.amount, cfg.recipientAmount, cfg.recipientNodeId, TimestampMilli.now(), cfg.invoice, OutgoingPaymentStatus.Pending)) } goto(WAITING_FOR_ROUTE) using WaitingForRoute(c, Nil, Ignore.empty) } @@ -77,20 +77,20 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A when(WAITING_FOR_ROUTE) { case Event(RouteResponse(route +: _), WaitingForRoute(c, failures, ignore)) => log.info(s"route found: attempt=${failures.size + 1}/${c.maxAttempts} route=${route.printNodes()} channels=${route.printChannels()}") - OutgoingPaymentPacket.buildCommand(self, cfg.upstream, paymentHash, route.hops, c.finalPayload) match { - case Success((cmd, sharedSecrets)) => - register ! Register.ForwardShortId(self.toTyped[Register.ForwardShortIdFailure[CMD_ADD_HTLC]], route.hops.head.shortChannelId, cmd) + OutgoingPaymentPacket.buildCommand(nodeParams.privateKey, self, cfg.upstream, paymentHash, route.clearHops, route.blinded_opt, c.amount, c.totalAmount, c.targetExpiry, c.paymentSecret, c.paymentMetadata, c.additionalTlvs, c.userCustomTlvs) match { + case Success((cmd, sharedSecrets, shortChannelId: ShortChannelId)) => + register ! Register.ForwardShortId(self.toTyped[Register.ForwardShortIdFailure[CMD_ADD_HTLC]], shortChannelId, cmd) goto(WAITING_FOR_PAYMENT_COMPLETE) using WaitingForComplete(c, cmd, failures, sharedSecrets, ignore, route) case Failure(t) => log.warning("cannot send outgoing payment: {}", t.getMessage) - Metrics.PaymentError.withTag(Tags.Failure, Tags.FailureType(LocalFailure(c.finalPayload.amount, Nil, t))).increment() - myStop(c, Left(PaymentFailed(id, paymentHash, failures :+ LocalFailure(c.finalPayload.amount, Nil, t)))) + Metrics.PaymentError.withTag(Tags.Failure, Tags.FailureType(LocalFailure(c.amount, Nil, t))).increment() + myStop(c, Left(PaymentFailed(id, paymentHash, failures :+ LocalFailure(c.amount, Nil, t)))) } case Event(Status.Failure(t), WaitingForRoute(c, failures, _)) => log.warning("router error: {}", t.getMessage) - Metrics.PaymentError.withTag(Tags.Failure, Tags.FailureType(LocalFailure(c.finalPayload.amount, Nil, t))).increment() - myStop(c, Left(PaymentFailed(id, paymentHash, failures :+ LocalFailure(c.finalPayload.amount, Nil, t)))) + Metrics.PaymentError.withTag(Tags.Failure, Tags.FailureType(LocalFailure(c.amount, Nil, t))).increment() + myStop(c, Left(PaymentFailed(id, paymentHash, failures :+ LocalFailure(c.amount, Nil, t)))) } when(WAITING_FOR_PAYMENT_COMPLETE) { @@ -105,7 +105,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A case Event(RES_ADD_SETTLED(_, htlc, fulfill: HtlcResult.Fulfill), d: WaitingForComplete) => router ! Router.RouteDidRelay(d.route) Metrics.PaymentAttempt.withTag(Tags.MultiPart, value = false).record(d.failures.size + 1) - val p = PartialPayment(id, d.c.finalPayload.amount, d.cmd.amount - d.c.finalPayload.amount, htlc.channelId, Some(cfg.fullRoute(d.route))) + val p = PartialPayment(id, d.c.amount, d.cmd.amount - d.c.amount, htlc.channelId, Some(cfg.fullRoute(d.route))) myStop(d.c, Right(cfg.createPaymentSent(fulfill.paymentPreimage, p :: Nil))) case Event(RES_ADD_SETTLED(_, _, fail: HtlcResult.Fail), d: WaitingForComplete) => @@ -139,7 +139,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A data.c match { case sendPaymentToNode: SendPaymentToNode => val ignore1 = PaymentFailure.updateIgnored(failure, data.ignore) - router ! RouteRequest(nodeParams.nodeId, data.c.targetNodeId, data.c.finalPayload.amount, sendPaymentToNode.maxFee, data.c.extraEdges, ignore1, sendPaymentToNode.routeParams, paymentContext = Some(cfg.paymentContext)) + router ! RouteRequest(nodeParams.nodeId, data.c.targetNodeId, data.c.amount, sendPaymentToNode.maxFee, data.c.extraEdges, ignore1, sendPaymentToNode.routeParams, paymentContext = Some(cfg.paymentContext)) goto(WAITING_FOR_ROUTE) using WaitingForRoute(data.c, data.failures :+ failure, ignore1) case _: SendPaymentToRoute => log.error("unexpected retry during SendPaymentToRoute") @@ -153,11 +153,11 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A private def handleLocalFail(d: WaitingForComplete, t: Throwable, isFatal: Boolean) = { t match { case UpdateMalformedException => Metrics.PaymentError.withTag(Tags.Failure, Tags.FailureType.Malformed).increment() - case _ => Metrics.PaymentError.withTag(Tags.Failure, Tags.FailureType(LocalFailure(d.c.finalPayload.amount, cfg.fullRoute(d.route), t))).increment() + case _ => Metrics.PaymentError.withTag(Tags.Failure, Tags.FailureType(LocalFailure(d.c.amount, cfg.fullRoute(d.route), t))).increment() } // we only retry if the error isn't fatal, and we haven't exhausted the max number of retried val doRetry = !isFatal && (d.failures.size + 1 < d.c.maxAttempts) - val localFailure = LocalFailure(d.c.finalPayload.amount, cfg.fullRoute(d.route), t) + val localFailure = LocalFailure(d.c.amount, cfg.fullRoute(d.route), t) if (doRetry) { log.info(s"received an error message from local, trying to use a different channel (failure=${t.getMessage})") retry(localFailure, d) @@ -170,21 +170,21 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A import d._ ((Sphinx.FailurePacket.decrypt(fail.reason, sharedSecrets) match { case success@Success(e) => - Metrics.PaymentError.withTag(Tags.Failure, Tags.FailureType(RemoteFailure(d.c.finalPayload.amount, Nil, e))).increment() + Metrics.PaymentError.withTag(Tags.Failure, Tags.FailureType(RemoteFailure(d.c.amount, Nil, e))).increment() success case failure@Failure(_) => - Metrics.PaymentError.withTag(Tags.Failure, Tags.FailureType(UnreadableRemoteFailure(d.c.finalPayload.amount, Nil))).increment() + Metrics.PaymentError.withTag(Tags.Failure, Tags.FailureType(UnreadableRemoteFailure(d.c.amount, Nil))).increment() failure }) match { case res@Success(Sphinx.DecryptedFailurePacket(nodeId, failureMessage)) => // We have discovered some liquidity information with this payment: we update the router accordingly. val stoppedRoute = d.route.stopAt(nodeId) - if (stoppedRoute.hops.length > 1) { + if (stoppedRoute.clearHops.length > 1) { router ! Router.RouteCouldRelay(stoppedRoute) } failureMessage match { case TemporaryChannelFailure(update) => - d.route.hops.find(_.nodeId == nodeId) match { + d.route.clearHops.find(_.nodeId == nodeId) match { case Some(failingHop) if ChannelRelayParams.areSame(failingHop.params, ChannelRelayParams.FromAnnouncement(update), ignoreHtlcSize = true) => router ! Router.ChannelCouldNotRelay(stoppedRoute.amount, failingHop) case _ => // otherwise the relay parameters may have changed, so it's not necessarily a liquidity issue @@ -197,7 +197,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A case Success(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage)) if nodeId == c.targetNodeId => // if destination node returns an error, we fail the payment immediately log.warning(s"received an error message from target nodeId=$nodeId, failing the payment (failure=$failureMessage)") - myStop(c, Left(PaymentFailed(id, paymentHash, failures :+ RemoteFailure(d.c.finalPayload.amount, cfg.fullRoute(route), e)))) + myStop(c, Left(PaymentFailed(id, paymentHash, failures :+ RemoteFailure(d.c.amount, cfg.fullRoute(route), e)))) case res if failures.size + 1 >= c.maxAttempts => // otherwise we never try more than maxAttempts, no matter the kind of error returned val failure = res match { @@ -207,24 +207,24 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A case failureMessage: Update => handleUpdate(nodeId, failureMessage, d) case _ => } - RemoteFailure(d.c.finalPayload.amount, cfg.fullRoute(route), e) + RemoteFailure(d.c.amount, cfg.fullRoute(route), e) case Failure(t) => log.warning(s"cannot parse returned error ${fail.reason.toHex} with sharedSecrets=$sharedSecrets: ${t.getMessage}") - UnreadableRemoteFailure(d.c.finalPayload.amount, cfg.fullRoute(route)) + UnreadableRemoteFailure(d.c.amount, cfg.fullRoute(route)) } log.warning(s"too many failed attempts, failing the payment") myStop(c, Left(PaymentFailed(id, paymentHash, failures :+ failure))) case Failure(t) => log.warning(s"cannot parse returned error: ${t.getMessage}, route=${route.printNodes()}") - val failure = UnreadableRemoteFailure(d.c.finalPayload.amount, cfg.fullRoute(route)) + val failure = UnreadableRemoteFailure(d.c.amount, cfg.fullRoute(route)) retry(failure, d) case Success(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage: Node)) => log.info(s"received 'Node' type error message from nodeId=$nodeId, trying to route around it (failure=$failureMessage)") - val failure = RemoteFailure(d.c.finalPayload.amount, cfg.fullRoute(route), e) + val failure = RemoteFailure(d.c.amount, cfg.fullRoute(route), e) retry(failure, d) case Success(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage: Update)) => log.info(s"received 'Update' type error message from nodeId=$nodeId, retrying payment (failure=$failureMessage)") - val failure = RemoteFailure(d.c.finalPayload.amount, cfg.fullRoute(route), e) + val failure = RemoteFailure(d.c.amount, cfg.fullRoute(route), e) if (Announcements.checkSig(failureMessage.update, nodeId)) { val extraEdges1 = handleUpdate(nodeId, failureMessage, d) val ignore1 = PaymentFailure.updateIgnored(failure, ignore) @@ -234,7 +234,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A log.error("unexpected retry during SendPaymentToRoute") stop(FSM.Normal) case c: SendPaymentToNode => - router ! RouteRequest(nodeParams.nodeId, c.targetNodeId, c.finalPayload.amount, c.maxFee, extraEdges1, ignore1, c.routeParams, paymentContext = Some(cfg.paymentContext)) + router ! RouteRequest(nodeParams.nodeId, c.targetNodeId, c.amount, c.maxFee, extraEdges1, ignore1, c.routeParams, paymentContext = Some(cfg.paymentContext)) goto(WAITING_FOR_ROUTE) using WaitingForRoute(c, failures :+ failure, ignore1) } } else { @@ -245,13 +245,13 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A log.error("unexpected retry during SendPaymentToRoute") stop(FSM.Normal) case c: SendPaymentToNode => - router ! RouteRequest(nodeParams.nodeId, c.targetNodeId, c.finalPayload.amount, c.maxFee, c.extraEdges, ignore + nodeId, c.routeParams, paymentContext = Some(cfg.paymentContext)) + router ! RouteRequest(nodeParams.nodeId, c.targetNodeId, c.amount, c.maxFee, c.extraEdges, ignore + nodeId, c.routeParams, paymentContext = Some(cfg.paymentContext)) goto(WAITING_FOR_ROUTE) using WaitingForRoute(c, failures :+ failure, ignore + nodeId) } } case Success(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage)) => log.info(s"received an error message from nodeId=$nodeId, trying to use a different channel (failure=$failureMessage)") - val failure = RemoteFailure(d.c.finalPayload.amount, cfg.fullRoute(route), e) + val failure = RemoteFailure(d.c.amount, cfg.fullRoute(route), e) retry(failure, d) } } @@ -262,7 +262,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A * @return updated routing hints if applicable. */ private def handleUpdate(nodeId: PublicKey, failure: Update, data: WaitingForComplete): Seq[ExtraEdge] = { - val extraEdges1 = data.route.hops.find(_.nodeId == nodeId) match { + val extraEdges1 = data.route.clearHops.find(_.nodeId == nodeId) match { case Some(hop) => hop.params match { case ann: ChannelRelayParams.FromAnnouncement => if (ann.channelUpdate.shortChannelId != failure.update.shortChannelId) { @@ -359,7 +359,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A } request match { case request: SendPaymentToNode => - context.system.eventStream.publish(PathFindingExperimentMetrics(cfg.paymentHash, request.finalPayload.amount, fees, status, duration, now, isMultiPart = false, request.routeParams.experimentName, cfg.recipientNodeId, request.extraEdges)) + context.system.eventStream.publish(PathFindingExperimentMetrics(cfg.paymentHash, request.amount, fees, status, duration, now, isMultiPart = false, request.routeParams.experimentName, cfg.recipientNodeId, request.extraEdges)) case _: SendPaymentToRoute => () } } @@ -390,7 +390,13 @@ object PaymentLifecycle { sealed trait SendPayment { // @formatter:off def replyTo: ActorRef - def finalPayload: FinalPayload + def paymentSecret: ByteVector32 + def amount: MilliSatoshi + def totalAmount: MilliSatoshi + def targetExpiry: CltvExpiry + def paymentMetadata: Option[ByteVector] + def additionalTlvs: Seq[OnionPaymentPayloadTlv] + def userCustomTlvs: Seq[GenericTlv] def extraEdges: Seq[ExtraEdge] def targetNodeId: PublicKey def maxAttempts: Int @@ -401,44 +407,64 @@ object PaymentLifecycle { * Send a payment to a given route. * * @param route payment route to use. - * @param finalPayload onion payload for the target node. + * @param amount amount to send to the target node. + * @param targetExpiry expiry at the target node (CLTV for the target node's received HTLCs). + * @param paymentSecret payment secret to protect against probing (usually from a Bolt 11 invoice). + * @param paymentMetadata payment metadata (usually from the Bolt 11 invoice). */ case class SendPaymentToRoute(replyTo: ActorRef, route: Either[PredefinedRoute, Route], - finalPayload: FinalPayload, - extraEdges: Seq[ExtraEdge] = Nil) extends SendPayment { - require(route.fold(!_.isEmpty, _.hops.nonEmpty), "payment route must not be empty") - - val targetNodeId: PublicKey = route.fold(_.targetNodeId, _.hops.last.nextNodeId) + amount: MilliSatoshi, + totalAmount: MilliSatoshi, + targetExpiry: CltvExpiry, + paymentSecret: ByteVector32, + paymentMetadata: Option[ByteVector], + extraEdges: Seq[ExtraEdge] = Nil, + additionalTlvs: Seq[OnionPaymentPayloadTlv] = Nil, + userCustomTlvs: Seq[GenericTlv] = Nil) extends SendPayment { + require(route.fold(!_.isEmpty, _.clearHops.nonEmpty), "payment route must not be empty") + + val targetNodeId: PublicKey = route.fold(_.targetNodeId, _.clearHops.last.nextNodeId) override def maxAttempts: Int = 1 def printRoute(): String = route match { case Left(PredefinedChannelRoute(_, channels)) => channels.mkString("->") case Left(PredefinedNodeRoute(nodes)) => nodes.mkString("->") - case Right(route) => route.hops.map(_.nextNodeId).mkString("->") + case Right(route) => route.clearHops.map(_.nextNodeId).mkString("->") } } /** * Send a payment to a given node. A path-finding algorithm will run to find a suitable payment route. * - * @param targetNodeId target node (may be the final recipient when using source-routing, or the first trampoline - * node when using trampoline). - * @param finalPayload onion payload for the target node. - * @param maxAttempts maximum number of retries. - * @param extraEdges routing hints (usually from a Bolt 11 invoice). - * @param routeParams parameters to fine-tune the routing algorithm. + * @param targetNodeId target node (may be the final recipient when using source-routing, or the first trampoline + * node when using trampoline). + * @param paymentSecret payment secret to protect against probing (usually from a Bolt 11 invoice). + * @param amount amount to send to the target node. + * @param targetExpiry expiry at the target node (CLTV for the target node's received HTLCs). + * @param paymentMetadata payment metadata (usually from the Bolt 11 invoice). + * @param maxAttempts maximum number of retries. + * @param extraEdges routing hints (usually from a Bolt 11 invoice). + * @param routeParams parameters to fine-tune the routing algorithm. + * @param additionalTlvs when provided, additional tlvs that will be added to the onion sent to the target node. + * @param userCustomTlvs when provided, additional user-defined custom tlvs that will be added to the onion sent to the target node. */ case class SendPaymentToNode(replyTo: ActorRef, targetNodeId: PublicKey, - finalPayload: FinalPayload, + amount: MilliSatoshi, + totalAmount: MilliSatoshi, + targetExpiry: CltvExpiry, + paymentSecret: ByteVector32, + paymentMetadata: Option[ByteVector], maxAttempts: Int, extraEdges: Seq[ExtraEdge] = Nil, - routeParams: RouteParams) extends SendPayment { - require(finalPayload.amount > 0.msat, s"amount must be > 0") + routeParams: RouteParams, + additionalTlvs: Seq[OnionPaymentPayloadTlv] = Nil, + userCustomTlvs: Seq[GenericTlv] = Nil) extends SendPayment { + require(amount > 0.msat, s"total amount must be > 0") - val maxFee: MilliSatoshi = routeParams.getMaxFee(finalPayload.amount) + val maxFee: MilliSatoshi = routeParams.getMaxFee(amount) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/BalanceEstimate.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/BalanceEstimate.scala index 18ea2a810d..06e2572b01 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/BalanceEstimate.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/BalanceEstimate.scala @@ -294,7 +294,7 @@ case class GraphWithBalanceEstimates(graph: DirectedGraph, private val balances: ) def routeCouldRelay(route: Route): GraphWithBalanceEstimates = { - val (balances1, _) = route.hops.foldRight((balances, route.amount)) { + val (balances1, _) = route.clearHops.foldRight((balances, route.amount)) { case (hop, (balances, amount)) => (balances.channelCouldSend(hop, amount), amount + hop.fee(amount)) } @@ -302,7 +302,7 @@ case class GraphWithBalanceEstimates(graph: DirectedGraph, private val balances: } def routeDidRelay(route: Route): GraphWithBalanceEstimates = { - val (balances1, _) = route.hops.foldRight((balances, route.amount)) { + val (balances1, _) = route.clearHops.foldRight((balances, route.amount)) { case (hop, (balances, amount)) => (balances.channelDidSend(hop, amount), amount + hop.fee(amount)) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala index b15763634b..4693369a90 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala @@ -53,7 +53,7 @@ object RouteCalculation { // select the largest edge (using balance when available, otherwise capacity). val selectedEdges = edges.map(es => es.maxBy(e => e.balance_opt.getOrElse(e.capacity.toMilliSatoshi))) val hops = selectedEdges.map(d => ChannelHop(d.desc.shortChannelId, d.desc.a, d.desc.b, d.params)) - ctx.sender() ! RouteResponse(Route(fr.amount, hops) :: Nil) + ctx.sender() ! RouteResponse(Route(fr.amount, hops, None) :: Nil) case _ => // some nodes in the supplied route aren't connected in our graph ctx.sender() ! Status.Failure(new IllegalArgumentException("Not all the nodes in the supplied route are connected with public channels")) @@ -82,7 +82,7 @@ object RouteCalculation { if (end != targetNodeId || hops.length != shortChannelIds.length) { ctx.sender() ! Status.Failure(new IllegalArgumentException("The sequence of channels provided cannot be used to build a route to the target node")) } else { - ctx.sender() ! RouteResponse(Route(fr.amount, hops) :: Nil) + ctx.sender() ! RouteResponse(Route(fr.amount, hops, None) :: Nil) } } @@ -186,7 +186,7 @@ object RouteCalculation { routeParams: RouteParams, currentBlockHeight: BlockHeight): Try[Seq[Route]] = Try { findRouteInternal(g, localNodeId, targetNodeId, amount, maxFee, numRoutes, extraEdges, ignoredEdges, ignoredVertices, routeParams, currentBlockHeight) match { - case Right(routes) => routes.map(route => Route(amount, route.path.map(graphEdgeToHop))) + case Right(routes) => routes.map(route => Route(amount, route.path.map(graphEdgeToHop), None)) case Left(ex) => return Failure(ex) } } @@ -354,7 +354,7 @@ object RouteCalculation { val edgeMaxAmount = edge.maxHtlcAmount(usedCapacity.getOrElse(edge.desc.shortChannelId, 0 msat)) amountMinusFees.min(edgeMaxAmount) } - Route(amount.max(0 msat), route.map(graphEdgeToHop)) + Route(amount.max(0 msat), route.map(graphEdgeToHop), None) } /** Initialize known used capacity based on pending HTLCs. */ @@ -362,13 +362,13 @@ object RouteCalculation { val usedCapacity = mutable.Map.empty[ShortChannelId, MilliSatoshi] // We always skip the first hop: since they are local channels, we already take into account those sent HTLCs in the // channel balance (which overrides the channel capacity in route calculation). - pendingHtlcs.filter(_.hops.length > 1).foreach(route => updateUsedCapacity(route.copy(hops = route.hops.tail), usedCapacity)) + pendingHtlcs.filter(_.clearHops.length > 1).foreach(route => updateUsedCapacity(route.copy(clearHops = route.clearHops.tail), usedCapacity)) usedCapacity } /** Update used capacity by taking into account an HTLC sent to the given route. */ private def updateUsedCapacity(route: Route, usedCapacity: mutable.Map[ShortChannelId, MilliSatoshi]): Unit = { - route.hops.reverse.foldLeft(route.amount) { case (amount, hop) => + route.clearHops.reverse.foldLeft(route.amount) { case (amount, hop) => usedCapacity.updateWith(hop.shortChannelId)(previous => Some(amount + previous.getOrElse(0 msat))) amount + hop.fee(amount) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala index 8ff883bccb..d62efe4c23 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala @@ -33,7 +33,7 @@ import fr.acinq.eclair.db.NetworkDb import fr.acinq.eclair.io.Peer.PeerRoutingMessage import fr.acinq.eclair.payment.Invoice.ExtraEdge import fr.acinq.eclair.payment.relay.Relayer -import fr.acinq.eclair.payment.{Bolt11Invoice, Invoice} +import fr.acinq.eclair.payment.{BlindedPaymentRoute, Bolt11Invoice, Invoice} import fr.acinq.eclair.remote.EclairInternalsSerializer.RemoteTypes import fr.acinq.eclair.router.Graph.GraphStructure.DirectedGraph import fr.acinq.eclair.router.Graph.{HeuristicsConstants, WeightRatios} @@ -406,7 +406,7 @@ object Router { } // @formatter:on - trait Hop { + sealed trait Hop { /** @return the id of the start node. */ def nodeId: PublicKey @@ -534,24 +534,29 @@ object Router { */ case class PaymentContext(id: UUID, parentId: UUID, paymentHash: ByteVector32) - case class Route(amount: MilliSatoshi, hops: Seq[ChannelHop]) { - require(hops.nonEmpty, "route cannot be empty") + /* A route is composed of zero or more hops chosen by us, optionally followed by blinded hops chosen by someone else. + * There must be a next node to relay the payment to. If there are no clear hops, it must end with a blinded route for + * which we are the introduction point and there must be a second blinded hop that is not us. + */ + case class Route(amount: MilliSatoshi, clearHops: Seq[ChannelHop], blinded_opt: Option[BlindedPaymentRoute]) { + require(clearHops.nonEmpty || blinded_opt.nonEmpty, "route cannot be empty") - val length = hops.length + val length: Int = clearHops.length def fee(includeLocalChannelCost: Boolean): MilliSatoshi = { - val hopsToPay = if (includeLocalChannelCost) hops else hops.drop(1) - val amountToSend = hopsToPay.reverse.foldLeft(amount) { case (amount1, hop) => amount1 + hop.fee(amount1) } + val hopsToPay = if (includeLocalChannelCost) clearHops else clearHops.drop(1) + val amountBeforeBlinded = amount + blinded_opt.map(_.paymentInfo.fee(amount)).getOrElse(0 msat) + val amountToSend = hopsToPay.reverse.foldLeft(amountBeforeBlinded) { case (amount1, hop) => amount1 + hop.fee(amount1) } amountToSend - amount } - def printNodes(): String = hops.map(_.nextNodeId).mkString("->") + def printNodes(): String = clearHops.map(_.nextNodeId).mkString("->") - def printChannels(): String = hops.map(_.shortChannelId).mkString("->") + def printChannels(): String = clearHops.map(_.shortChannelId).mkString("->") def stopAt(nodeId: PublicKey): Route = { - val amountAtStop = hops.reverse.takeWhile(_.nextNodeId != nodeId).foldLeft(amount) { case (amount1, hop) => amount1 + hop.fee(amount1) } - Route(amountAtStop, hops.takeWhile(_.nodeId != nodeId)) + val amountAtStop = clearHops.reverse.takeWhile(_.nextNodeId != nodeId).foldLeft(amount) { case (amount1, hop) => amount1 + hop.fee(amount1) } + Route(amountAtStop, clearHops.takeWhile(_.nodeId != nodeId), None) } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OfferTypes.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OfferTypes.scala index 61c3c047eb..abb284cb18 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OfferTypes.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OfferTypes.scala @@ -22,7 +22,7 @@ import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, ByteVector64, Crypto, import fr.acinq.eclair.crypto.Sphinx.RouteBlinding.BlindedRoute import fr.acinq.eclair.wire.protocol.OnionRoutingCodecs.{InvalidTlvPayload, MissingRequiredTlv} import fr.acinq.eclair.wire.protocol.TlvCodecs.genericTlv -import fr.acinq.eclair.{CltvExpiryDelta, Feature, Features, InvoiceFeature, MilliSatoshi, TimestampSecond, UInt64} +import fr.acinq.eclair.{CltvExpiryDelta, Feature, Features, InvoiceFeature, MilliSatoshi, TimestampSecond, UInt64, nodeFee} import fr.acinq.secp256k1.Secp256k1JvmKt import scodec.Codec import scodec.bits.ByteVector @@ -65,7 +65,9 @@ object OfferTypes { cltvExpiryDelta: CltvExpiryDelta, minHtlc: MilliSatoshi, maxHtlc: MilliSatoshi, - allowedFeatures: Features[Feature]) + allowedFeatures: Features[Feature]) { + def fee(amount: MilliSatoshi): MilliSatoshi = nodeFee(feeBase, feeProportionalMillionths, amount) + } case class PaymentPathsInfo(paymentInfo: Seq[PaymentInfo]) extends InvoiceTlv diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/PaymentOnion.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/PaymentOnion.scala index 3c2ebbcfda..3b17b3c564 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/PaymentOnion.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/PaymentOnion.scala @@ -22,7 +22,7 @@ import fr.acinq.eclair.payment.Bolt11Invoice import fr.acinq.eclair.wire.protocol.CommonCodecs._ import fr.acinq.eclair.wire.protocol.OnionRoutingCodecs.{ForbiddenTlv, InvalidTlvPayload, MissingRequiredTlv} import fr.acinq.eclair.wire.protocol.TlvCodecs._ -import fr.acinq.eclair.{CltvExpiry, Features, MilliSatoshi, MilliSatoshiLong, ShortChannelId, UInt64} +import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Features, MilliSatoshi, MilliSatoshiLong, ShortChannelId, UInt64} import scodec.bits.{BitVector, ByteVector} /** @@ -217,6 +217,9 @@ object PaymentOnion { def records: TlvStream[OnionPaymentPayloadTlv] } + /** An opaque blinded payload. */ + case class BlindPerHopPayload(records: TlvStream[OnionPaymentPayloadTlv]) extends PerHopPayload + /** Per-hop payload for an intermediate node. */ sealed trait IntermediatePayload extends PerHopPayload @@ -265,6 +268,7 @@ object PaymentOnion { val allowedFeatures = blindedRecords.get[RouteBlindingEncryptedDataTlv.AllowedFeatures].map(_.features).getOrElse(Features.empty) override def amountToForward(incomingAmount: MilliSatoshi): MilliSatoshi = ((incomingAmount - paymentRelay.feeBase).toLong * 1_000_000 + 1_000_000 + paymentRelay.feeProportionalMillionths - 1).msat / (1_000_000 + paymentRelay.feeProportionalMillionths) override def outgoingCltv(incomingCltv: CltvExpiry): CltvExpiry = incomingCltv - paymentRelay.cltvExpiryDelta + val cltvExpiryDelta: CltvExpiryDelta = paymentRelay.cltvExpiryDelta // @formatter:on } @@ -283,6 +287,9 @@ object PaymentOnion { } BlindedRouteData.validatePaymentRelayData(blindedRecords).map(blindedRecords => Blinded(records, blindedRecords, nextBlinding)) } + + def create(encryptedRecipientData: ByteVector, blinding_opt: Option[PublicKey]): BlindPerHopPayload = + BlindPerHopPayload(TlvStream(Seq(blinding_opt.map(BlindingPoint), Some(EncryptedRecipientData(encryptedRecipientData))).flatten)) } } @@ -394,11 +401,6 @@ object PaymentOnion { ).flatten Standard(TlvStream(tlvs ++ additionalTlvs, userCustomTlvs)) } - - /** Create a trampoline outer payload. */ - def createTrampolinePayload(amount: MilliSatoshi, totalAmount: MilliSatoshi, expiry: CltvExpiry, paymentSecret: ByteVector32, trampolinePacket: OnionRoutingPacket): Standard = { - Standard(TlvStream(AmountToForward(amount), OutgoingCltv(expiry), PaymentData(paymentSecret, totalAmount), TrampolineOnion(trampolinePacket))) - } } /** @@ -433,6 +435,21 @@ object PaymentOnion { } BlindedRouteData.validPaymentRecipientData(blindedRecords).map(blindedRecords => Blinded(records, blindedRecords)) } + + def create(amount: MilliSatoshi, + expiry: CltvExpiry, + encryptedRecipientData: ByteVector, + blinding_opt: Option[PublicKey], + additionalTlvs: Seq[OnionPaymentPayloadTlv] = Nil, + userCustomTlvs: Seq[GenericTlv] = Nil): BlindPerHopPayload = { + val tlvs = Seq( + blinding_opt.map(BlindingPoint), + Some(AmountToForward(amount)), + Some(OutgoingCltv(expiry)), + Some(EncryptedRecipientData(encryptedRecipientData)), + ).flatten + BlindPerHopPayload(TlvStream(tlvs ++ additionalTlvs, userCustomTlvs)) + } } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/RouteBlinding.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/RouteBlinding.scala index 4e779dbc67..d27b1ee467 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/RouteBlinding.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/RouteBlinding.scala @@ -141,9 +141,9 @@ object RouteBlindingEncryptedDataCodecs { // @formatter:off case class RouteBlindingDecryptedData(tlvs: TlvStream[RouteBlindingEncryptedDataTlv], nextBlinding: PublicKey) - sealed trait InvalidEncryptedData - case class CannotDecryptData(message: String) extends InvalidEncryptedData - case class CannotDecodeData(message: String) extends InvalidEncryptedData + sealed trait InvalidEncryptedData extends Exception + case class CannotDecryptData(message: String) extends Exception(message) with InvalidEncryptedData + case class CannotDecodeData(message: String) extends Exception(message) with InvalidEncryptedData // @formatter:on /** diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala index 05db2b56ec..2f42dfd8ff 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala @@ -122,7 +122,7 @@ class FuzzySpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Channe // allow overpaying (no more than 2 times the required amount) val amount = requiredAmount + Random.nextInt(requiredAmount.toLong.toInt).msat val expiry = (Channel.MIN_CLTV_EXPIRY_DELTA + 1).toCltvExpiry(currentBlockHeight = BlockHeight(400000)) - OutgoingPaymentPacket.buildCommand(self, Upstream.Local(UUID.randomUUID()), paymentHash, ChannelHop(null, null, dest, null) :: Nil, PaymentOnion.FinalPayload.Standard.createSinglePartPayload(amount, expiry, paymentSecret, None)).get._1 + OutgoingPaymentPacket.buildCommand(randomKey(), self, Upstream.Local(UUID.randomUUID()), paymentHash, ChannelHop(null, null, dest, null) :: Nil, None, amount, amount, expiry, paymentSecret, None, Nil, Nil).get._1 } def initiatePaymentOrStop(remaining: Int): Unit = diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala index 49b6aa3aed..beacc1013c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala @@ -357,7 +357,7 @@ trait ChannelStateTestsBase extends Assertions with Eventually { def makeCmdAdd(amount: MilliSatoshi, cltvExpiryDelta: CltvExpiryDelta, destination: PublicKey, paymentPreimage: ByteVector32, currentBlockHeight: BlockHeight, upstream: Upstream, replyTo: ActorRef = TestProbe().ref): (ByteVector32, CMD_ADD_HTLC) = { val paymentHash: ByteVector32 = Crypto.sha256(paymentPreimage) val expiry = cltvExpiryDelta.toCltvExpiry(currentBlockHeight) - val cmd = OutgoingPaymentPacket.buildCommand(replyTo, upstream, paymentHash, ChannelHop(null, null, destination, null) :: Nil, PaymentOnion.FinalPayload.Standard.createSinglePartPayload(amount, expiry, randomBytes32(), None)).get._1.copy(commit = false) + val cmd = OutgoingPaymentPacket.buildCommand(randomKey(), replyTo, upstream, paymentHash, ChannelHop(null, null, destination, null) :: Nil, None, amount, amount, expiry, randomBytes32(), None, Nil, Nil).get._1.copy(commit = false) (paymentPreimage, cmd) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala index 4f79a07e74..4b51ad2f2d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala @@ -60,7 +60,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit val h1 = Crypto.sha256(r1) val amount1 = 300000000 msat val expiry1 = CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight) - val cmd1 = OutgoingPaymentPacket.buildCommand(sender.ref, Upstream.Local(UUID.randomUUID), h1, ChannelHop(null, null, TestConstants.Bob.nodeParams.nodeId, null) :: Nil, PaymentOnion.FinalPayload.Standard.createSinglePartPayload(amount1, expiry1, randomBytes32(), None)).get._1.copy(commit = false) + val cmd1 = OutgoingPaymentPacket.buildCommand(TestConstants.Alice.nodeParams.privateKey, sender.ref, Upstream.Local(UUID.randomUUID), h1, ChannelHop(null, null, TestConstants.Bob.nodeParams.nodeId, null) :: Nil, None, amount1, amount1, expiry1, randomBytes32(), None, Nil, Nil).get._1.copy(commit = false) alice ! cmd1 sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]] val htlc1 = alice2bob.expectMsgType[UpdateAddHtlc] @@ -70,7 +70,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit val h2 = Crypto.sha256(r2) val amount2 = 200000000 msat val expiry2 = CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight) - val cmd2 = OutgoingPaymentPacket.buildCommand(sender.ref, Upstream.Local(UUID.randomUUID), h2, ChannelHop(null, null, TestConstants.Bob.nodeParams.nodeId, null) :: Nil, PaymentOnion.FinalPayload.Standard.createSinglePartPayload(amount2, expiry2, randomBytes32(), None)).get._1.copy(commit = false) + val cmd2 = OutgoingPaymentPacket.buildCommand(TestConstants.Alice.nodeParams.privateKey, sender.ref, Upstream.Local(UUID.randomUUID), h2, ChannelHop(null, null, TestConstants.Bob.nodeParams.nodeId, null) :: Nil, None, amount2, amount2, expiry2, randomBytes32(), None, Nil, Nil).get._1.copy(commit = false) alice ! cmd2 sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]] val htlc2 = alice2bob.expectMsgType[UpdateAddHtlc] diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt12InvoiceSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt12InvoiceSpec.scala index 77a7728a6b..86b175e2f9 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt12InvoiceSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt12InvoiceSpec.scala @@ -340,7 +340,7 @@ class Bolt12InvoiceSpec extends AnyFunSuite { assert(codedDecoded.features == features) assert(codedDecoded.issuer.contains(issuer)) assert(codedDecoded.nodeId.value.drop(1) == nodeKey.publicKey.value.drop(1)) - assert(codedDecoded.blindedPaths == Seq(path)) + assert(codedDecoded.blindedPaymentRoutes == Seq(BlindedPaymentRoute(path, payInfo, None))) assert(codedDecoded.quantity.contains(quantity)) assert(codedDecoded.payerKey.contains(payerKey)) assert(codedDecoded.payerNote.contains(payerNote)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala index da6cc9aa10..694a504bcb 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala @@ -262,7 +262,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val invoice = sender.expectMsgType[Bolt12Invoice] assert(invoice.amount == 25_000.msat) assert(invoice.nodeId == privKey.publicKey) - assert(invoice.blindedPaths.nonEmpty) + assert(invoice.blindedPaymentRoutes.nonEmpty) assert(invoice.features.hasFeature(RouteBlinding, Some(Mandatory))) assert(invoice.description == Left("a blinded coffee please")) assert(invoice.offerId.contains(offer.offerId)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala index 2d72e08bec..8e15b17ad6 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala @@ -86,15 +86,14 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS router.expectMsg(RouteRequest(nodeParams.nodeId, e, finalAmount, maxFee, routeParams = routeParams.copy(randomize = false), allowMultiPart = true, paymentContext = Some(cfg.paymentContext))) assert(payFsm.stateName == WAIT_FOR_ROUTES) - val singleRoute = Route(finalAmount, hop_ab_1 :: hop_be :: Nil) + val singleRoute = Route(finalAmount, hop_ab_1 :: hop_be :: Nil, None) router.send(payFsm, RouteResponse(Seq(singleRoute))) val childPayment = childPayFsm.expectMsgType[SendPaymentToRoute] assert(childPayment.route == Right(singleRoute)) - assert(childPayment.finalPayload.isInstanceOf[FinalPayload.Standard]) - assert(childPayment.finalPayload.expiry == expiry) - assert(childPayment.finalPayload.asInstanceOf[FinalPayload.Standard].paymentSecret == payment.paymentSecret) - assert(childPayment.finalPayload.amount == finalAmount) - assert(childPayment.finalPayload.totalAmount == finalAmount) + assert(childPayment.targetExpiry == expiry) + assert(childPayment.paymentSecret == payment.paymentSecret) + assert(childPayment.amount == finalAmount) + assert(childPayment.totalAmount == finalAmount) assert(payFsm.stateName == PAYMENT_IN_PROGRESS) val result = fulfillPendingPayments(f, 1) @@ -121,18 +120,17 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS assert(payFsm.stateName == WAIT_FOR_ROUTES) val routes = Seq( - Route(500000 msat, hop_ab_1 :: hop_be :: Nil), - Route(700000 msat, hop_ac_1 :: hop_ce :: Nil), + Route(500000 msat, hop_ab_1 :: hop_be :: Nil, None), + Route(700000 msat, hop_ac_1 :: hop_ce :: Nil, None), ) router.send(payFsm, RouteResponse(routes)) val childPayments = childPayFsm.expectMsgType[SendPaymentToRoute] :: childPayFsm.expectMsgType[SendPaymentToRoute] :: Nil assert(childPayments.map(_.route).toSet == routes.map(r => Right(r)).toSet) - assert(childPayments.map(_.finalPayload.expiry).toSet == Set(expiry)) - childPayments.foreach(childPayment => assert(childPayment.finalPayload.isInstanceOf[FinalPayload.Standard])) - assert(childPayments.map(_.finalPayload.asInstanceOf[FinalPayload.Standard].paymentSecret).toSet == Set(payment.paymentSecret)) - assert(childPayments.map(_.finalPayload.asInstanceOf[FinalPayload.Standard].paymentMetadata).toSet == Set(Some(hex"012345"))) - assert(childPayments.map(_.finalPayload.amount).toSet == Set(500000 msat, 700000 msat)) - assert(childPayments.map(_.finalPayload.totalAmount).toSet == Set(1200000 msat)) + assert(childPayments.map(_.targetExpiry).toSet == Set(expiry)) + assert(childPayments.map(_.paymentSecret).toSet == Set(payment.paymentSecret)) + assert(childPayments.map(_.paymentMetadata).toSet == Set(Some(hex"012345"))) + assert(childPayments.map(_.amount).toSet == Set(500000 msat, 700000 msat)) + assert(childPayments.map(_.totalAmount).toSet == Set(1200000 msat)) assert(payFsm.stateName == PAYMENT_IN_PROGRESS) val result = fulfillPendingPayments(f, 2) @@ -157,11 +155,11 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount + 1000.msat, expiry, 1, None, routeParams = routeParams, additionalTlvs = Seq(trampolineTlv), userCustomTlvs = Seq(userCustomTlv)) sender.send(payFsm, payment) router.expectMsgType[RouteRequest] - router.send(payFsm, RouteResponse(Seq(Route(500000 msat, hop_ab_1 :: hop_be :: Nil), Route(501000 msat, hop_ac_1 :: hop_ce :: Nil)))) + router.send(payFsm, RouteResponse(Seq(Route(500000 msat, hop_ab_1 :: hop_be :: Nil, None), Route(501000 msat, hop_ac_1 :: hop_ce :: Nil, None)))) val childPayments = childPayFsm.expectMsgType[SendPaymentToRoute] :: childPayFsm.expectMsgType[SendPaymentToRoute] :: Nil - childPayments.map(_.finalPayload).foreach(p => { - assert(p.records.get[OnionPaymentPayloadTlv.TrampolineOnion].contains(trampolineTlv)) - assert(p.records.unknown.toSeq == Seq(userCustomTlv)) + childPayments.foreach(p => { + assert(p.additionalTlvs.contains(trampolineTlv)) + assert(p.userCustomTlvs == Seq(userCustomTlv)) }) val result = fulfillPendingPayments(f, 2) @@ -181,16 +179,16 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 3, None, routeParams = routeParams) sender.send(payFsm, payment) router.expectMsgType[RouteRequest] - val failingRoute = Route(finalAmount, hop_ab_1 :: hop_be :: Nil) + val failingRoute = Route(finalAmount, hop_ab_1 :: hop_be :: Nil, None) router.send(payFsm, RouteResponse(Seq(failingRoute))) childPayFsm.expectMsgType[SendPaymentToRoute] childPayFsm.expectNoMessage(100 millis) val childId = payFsm.stateData.asInstanceOf[PaymentProgress].pending.keys.head - childPayFsm.send(payFsm, PaymentFailed(childId, paymentHash, Seq(RemoteFailure(failingRoute.amount, failingRoute.hops, Sphinx.DecryptedFailurePacket(b, PermanentChannelFailure))))) + childPayFsm.send(payFsm, PaymentFailed(childId, paymentHash, Seq(RemoteFailure(failingRoute.amount, failingRoute.clearHops, Sphinx.DecryptedFailurePacket(b, PermanentChannelFailure))))) // We retry ignoring the failing channel. router.expectMsg(RouteRequest(nodeParams.nodeId, e, finalAmount, maxFee, routeParams = routeParams.copy(randomize = true), allowMultiPart = true, ignore = Ignore(Set.empty, Set(ChannelDesc(channelId_be, b, e))), paymentContext = Some(cfg.paymentContext))) - router.send(payFsm, RouteResponse(Seq(Route(400000 msat, hop_ac_1 :: hop_ce :: Nil), Route(600000 msat, hop_ad :: hop_de :: Nil)))) + router.send(payFsm, RouteResponse(Seq(Route(400000 msat, hop_ac_1 :: hop_ce :: Nil, None), Route(600000 msat, hop_ad :: hop_de :: Nil, None)))) childPayFsm.expectMsgType[SendPaymentToRoute] childPayFsm.expectMsgType[SendPaymentToRoute] assert(!payFsm.stateData.asInstanceOf[PaymentProgress].pending.contains(childId)) @@ -214,27 +212,27 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 3, None, routeParams = routeParams) sender.send(payFsm, payment) router.expectMsgType[RouteRequest] - router.send(payFsm, RouteResponse(Seq(Route(400000 msat, hop_ab_1 :: hop_be :: Nil), Route(600000 msat, hop_ab_2 :: hop_be :: Nil)))) + router.send(payFsm, RouteResponse(Seq(Route(400000 msat, hop_ab_1 :: hop_be :: Nil, None), Route(600000 msat, hop_ab_2 :: hop_be :: Nil, None)))) childPayFsm.expectMsgType[SendPaymentToRoute] childPayFsm.expectMsgType[SendPaymentToRoute] childPayFsm.expectNoMessage(100 millis) val (failedId1, failedRoute1) :: (failedId2, failedRoute2) :: Nil = payFsm.stateData.asInstanceOf[PaymentProgress].pending.toSeq - childPayFsm.send(payFsm, PaymentFailed(failedId1, paymentHash, Seq(RemoteFailure(failedRoute1.amount, failedRoute1.hops, Sphinx.DecryptedFailurePacket(b, TemporaryNodeFailure))))) + childPayFsm.send(payFsm, PaymentFailed(failedId1, paymentHash, Seq(RemoteFailure(failedRoute1.amount, failedRoute1.clearHops, Sphinx.DecryptedFailurePacket(b, TemporaryNodeFailure))))) // When we retry, we ignore the failing node and we let the router know about the remaining pending route. router.expectMsg(RouteRequest(nodeParams.nodeId, e, failedRoute1.amount, maxFee - failedRoute1.fee(false), ignore = Ignore(Set(b), Set.empty), pendingPayments = Seq(failedRoute2), allowMultiPart = true, routeParams = routeParams.copy(randomize = true), paymentContext = Some(cfg.paymentContext))) // The second part fails while we're still waiting for new routes. - childPayFsm.send(payFsm, PaymentFailed(failedId2, paymentHash, Seq(RemoteFailure(failedRoute2.amount, failedRoute2.hops, Sphinx.DecryptedFailurePacket(b, TemporaryNodeFailure))))) + childPayFsm.send(payFsm, PaymentFailed(failedId2, paymentHash, Seq(RemoteFailure(failedRoute2.amount, failedRoute2.clearHops, Sphinx.DecryptedFailurePacket(b, TemporaryNodeFailure))))) // We receive a response to our first request, but it's now obsolete: we re-sent a new route request that takes into // account the latest failures. - router.send(payFsm, RouteResponse(Seq(Route(failedRoute1.amount, hop_ac_1 :: hop_ce :: Nil)))) + router.send(payFsm, RouteResponse(Seq(Route(failedRoute1.amount, hop_ac_1 :: hop_ce :: Nil, None)))) router.expectMsg(RouteRequest(nodeParams.nodeId, e, finalAmount, maxFee, ignore = Ignore(Set(b), Set.empty), allowMultiPart = true, routeParams = routeParams.copy(randomize = true), paymentContext = Some(cfg.paymentContext))) awaitCond(payFsm.stateData.asInstanceOf[PaymentProgress].pending.isEmpty) childPayFsm.expectNoMessage(100 millis) // We receive new routes that work. - router.send(payFsm, RouteResponse(Seq(Route(300000 msat, hop_ac_1 :: hop_ce :: Nil), Route(700000 msat, hop_ad :: hop_de :: Nil)))) + router.send(payFsm, RouteResponse(Seq(Route(300000 msat, hop_ac_1 :: hop_ce :: Nil, None), Route(700000 msat, hop_ad :: hop_de :: Nil, None)))) childPayFsm.expectMsgType[SendPaymentToRoute] childPayFsm.expectMsgType[SendPaymentToRoute] @@ -256,12 +254,12 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 3, None, routeParams = routeParams) sender.send(payFsm, payment) router.expectMsgType[RouteRequest] - router.send(payFsm, RouteResponse(Seq(Route(finalAmount, hop_ab_1 :: hop_be :: Nil)))) + router.send(payFsm, RouteResponse(Seq(Route(finalAmount, hop_ab_1 :: hop_be :: Nil, None)))) childPayFsm.expectMsgType[SendPaymentToRoute] childPayFsm.expectNoMessage(100 millis) val (failedId, failedRoute) :: Nil = payFsm.stateData.asInstanceOf[PaymentProgress].pending.toSeq - childPayFsm.send(payFsm, PaymentFailed(failedId, paymentHash, Seq(LocalFailure(failedRoute.amount, failedRoute.hops, RemoteCannotAffordFeesForNewHtlc(randomBytes32(), finalAmount, 15 sat, 0 sat, 15 sat))))) + childPayFsm.send(payFsm, PaymentFailed(failedId, paymentHash, Seq(LocalFailure(failedRoute.amount, failedRoute.clearHops, RemoteCannotAffordFeesForNewHtlc(randomBytes32(), finalAmount, 15 sat, 0 sat, 15 sat))))) // We retry without the failing channel. val expectedRouteRequest = RouteRequest( @@ -281,13 +279,13 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 3, None, routeParams = routeParams) sender.send(payFsm, payment) router.expectMsgType[RouteRequest] - router.send(payFsm, RouteResponse(Seq(Route(500000 msat, hop_ab_1 :: hop_be :: Nil), Route(500000 msat, hop_ab_1 :: hop_be :: Nil)))) + router.send(payFsm, RouteResponse(Seq(Route(500000 msat, hop_ab_1 :: hop_be :: Nil, None), Route(500000 msat, hop_ab_1 :: hop_be :: Nil, None)))) childPayFsm.expectMsgType[SendPaymentToRoute] childPayFsm.expectMsgType[SendPaymentToRoute] childPayFsm.expectNoMessage(100 millis) val (failedId, failedRoute) :: (_, pendingRoute) :: Nil = payFsm.stateData.asInstanceOf[PaymentProgress].pending.toSeq - childPayFsm.send(payFsm, PaymentFailed(failedId, paymentHash, Seq(LocalFailure(failedRoute.amount, failedRoute.hops, ChannelUnavailable(randomBytes32()))))) + childPayFsm.send(payFsm, PaymentFailed(failedId, paymentHash, Seq(LocalFailure(failedRoute.amount, failedRoute.clearHops, ChannelUnavailable(randomBytes32()))))) // If the router doesn't find routes, we will retry without ignoring the channel: it may work with a different split // of the amount to send. @@ -303,7 +301,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS router.send(payFsm, Status.Failure(RouteNotFound)) router.expectMsg(expectedRouteRequest.copy(ignore = Ignore.empty)) - router.send(payFsm, RouteResponse(Seq(Route(500000 msat, hop_ac_1 :: hop_ce :: Nil)))) + router.send(payFsm, RouteResponse(Seq(Route(500000 msat, hop_ac_1 :: hop_ce :: Nil, None)))) childPayFsm.expectMsgType[SendPaymentToRoute] val result = fulfillPendingPayments(f, 2) @@ -325,7 +323,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 3, None, routeParams = routeParams, extraEdges = List(extraEdge)) sender.send(payFsm, payment) assert(router.expectMsgType[RouteRequest].extraEdges.head == extraEdge) - val route = Route(finalAmount, hop_ab_1 :: hop_be :: Nil) + val route = Route(finalAmount, hop_ab_1 :: hop_be :: Nil, None) router.send(payFsm, RouteResponse(Seq(route))) childPayFsm.expectMsgType[SendPaymentToRoute] childPayFsm.expectNoMessage(100 millis) @@ -333,7 +331,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS // B changed his fees and expiry after the invoice was issued. val channelUpdate = hop_be.params.asInstanceOf[ChannelRelayParams.FromAnnouncement].channelUpdate.copy(feeBaseMsat = 250 msat, feeProportionalMillionths = 150, cltvExpiryDelta = CltvExpiryDelta(24)) val childId = payFsm.stateData.asInstanceOf[PaymentProgress].pending.keys.head - childPayFsm.send(payFsm, PaymentFailed(childId, paymentHash, Seq(RemoteFailure(route.amount, route.hops, Sphinx.DecryptedFailurePacket(b, FeeInsufficient(finalAmount, channelUpdate)))))) + childPayFsm.send(payFsm, PaymentFailed(childId, paymentHash, Seq(RemoteFailure(route.amount, route.clearHops, Sphinx.DecryptedFailurePacket(b, FeeInsufficient(finalAmount, channelUpdate)))))) // We update the routing hints accordingly before requesting a new route. val updatedExtraEdge = router.expectMsgType[RouteRequest].extraEdges.head assert(updatedExtraEdge == BasicEdge(b, e, hop_be.shortChannelId, channelUpdate.feeBaseMsat, channelUpdate.feeProportionalMillionths, channelUpdate.cltvExpiryDelta)) @@ -347,7 +345,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 3, None, routeParams = routeParams, extraEdges = List(extraEdge)) sender.send(payFsm, payment) assert(router.expectMsgType[RouteRequest].extraEdges.head == extraEdge) - val route = Route(finalAmount, hop_ab_1 :: hop_be :: Nil) + val route = Route(finalAmount, hop_ab_1 :: hop_be :: Nil, None) router.send(payFsm, RouteResponse(Seq(route))) childPayFsm.expectMsgType[SendPaymentToRoute] childPayFsm.expectNoMessage(100 millis) @@ -357,7 +355,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS val channelUpdateBE = hop_be.params.asInstanceOf[ChannelRelayParams.FromAnnouncement].channelUpdate val channelUpdateBE1 = Announcements.makeChannelUpdate(channelUpdateBE.chainHash, priv_b, e, channelUpdateBE.shortChannelId, channelUpdateBE.cltvExpiryDelta, channelUpdateBE.htlcMinimumMsat, channelUpdateBE.feeBaseMsat, channelUpdateBE.feeProportionalMillionths, channelUpdateBE.htlcMaximumMsat) val childId = payFsm.stateData.asInstanceOf[PaymentProgress].pending.keys.head - childPayFsm.send(payFsm, PaymentFailed(childId, paymentHash, Seq(RemoteFailure(route.amount, route.hops, Sphinx.DecryptedFailurePacket(b, TemporaryChannelFailure(channelUpdateBE1)))))) + childPayFsm.send(payFsm, PaymentFailed(childId, paymentHash, Seq(RemoteFailure(route.amount, route.clearHops, Sphinx.DecryptedFailurePacket(b, TemporaryChannelFailure(channelUpdateBE1)))))) // We update the routing hints accordingly before requesting a new route and ignore the channel. val routeRequest = router.expectMsgType[RouteRequest] assert(routeRequest.extraEdges.head == extraEdge) @@ -407,19 +405,19 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 2, None, routeParams = routeParams) sender.send(payFsm, payment) router.expectMsgType[RouteRequest] - router.send(payFsm, RouteResponse(Seq(Route(500000 msat, hop_ab_1 :: hop_be :: Nil), Route(500000 msat, hop_ac_1 :: hop_ce :: Nil)))) + router.send(payFsm, RouteResponse(Seq(Route(500000 msat, hop_ab_1 :: hop_be :: Nil, None), Route(500000 msat, hop_ac_1 :: hop_ce :: Nil, None)))) childPayFsm.expectMsgType[SendPaymentToRoute] childPayFsm.expectMsgType[SendPaymentToRoute] val (failedId1, failedRoute1) = payFsm.stateData.asInstanceOf[PaymentProgress].pending.head - childPayFsm.send(payFsm, PaymentFailed(failedId1, paymentHash, Seq(UnreadableRemoteFailure(failedRoute1.amount, failedRoute1.hops)))) + childPayFsm.send(payFsm, PaymentFailed(failedId1, paymentHash, Seq(UnreadableRemoteFailure(failedRoute1.amount, failedRoute1.clearHops)))) router.expectMsgType[RouteRequest] - router.send(payFsm, RouteResponse(Seq(Route(500000 msat, hop_ad :: hop_de :: Nil)))) + router.send(payFsm, RouteResponse(Seq(Route(500000 msat, hop_ad :: hop_de :: Nil, None)))) childPayFsm.expectMsgType[SendPaymentToRoute] assert(!payFsm.stateData.asInstanceOf[PaymentProgress].pending.contains(failedId1)) val (failedId2, failedRoute2) = payFsm.stateData.asInstanceOf[PaymentProgress].pending.head - val result = abortAfterFailure(f, PaymentFailed(failedId2, paymentHash, Seq(UnreadableRemoteFailure(failedRoute2.amount, failedRoute2.hops)))) + val result = abortAfterFailure(f, PaymentFailed(failedId2, paymentHash, Seq(UnreadableRemoteFailure(failedRoute2.amount, failedRoute2.clearHops)))) assert(result.failures.length >= 3) assert(result.failures.contains(LocalFailure(finalAmount, Nil, RetryExhausted))) @@ -468,11 +466,11 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 5, None, routeParams = routeParams) sender.send(payFsm, payment) router.expectMsgType[RouteRequest] - router.send(payFsm, RouteResponse(Seq(Route(finalAmount, hop_ab_1 :: hop_be :: Nil)))) + router.send(payFsm, RouteResponse(Seq(Route(finalAmount, hop_ab_1 :: hop_be :: Nil, None)))) childPayFsm.expectMsgType[SendPaymentToRoute] val (failedId, failedRoute) = payFsm.stateData.asInstanceOf[PaymentProgress].pending.head - val result = abortAfterFailure(f, PaymentFailed(failedId, paymentHash, Seq(RemoteFailure(failedRoute.amount, failedRoute.hops, Sphinx.DecryptedFailurePacket(e, IncorrectOrUnknownPaymentDetails(600000 msat, BlockHeight(0))))))) + val result = abortAfterFailure(f, PaymentFailed(failedId, paymentHash, Seq(RemoteFailure(failedRoute.amount, failedRoute.clearHops, Sphinx.DecryptedFailurePacket(e, IncorrectOrUnknownPaymentDetails(600000 msat, BlockHeight(0))))))) assert(result.failures.length == 1) val metrics = metricsListener.expectMsgType[PathFindingExperimentMetrics] @@ -489,11 +487,11 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 5, None, routeParams = routeParams) sender.send(payFsm, payment) router.expectMsgType[RouteRequest] - router.send(payFsm, RouteResponse(Seq(Route(finalAmount, hop_ab_1 :: hop_be :: Nil)))) + router.send(payFsm, RouteResponse(Seq(Route(finalAmount, hop_ab_1 :: hop_be :: Nil, None)))) childPayFsm.expectMsgType[SendPaymentToRoute] val (failedId, failedRoute) = payFsm.stateData.asInstanceOf[PaymentProgress].pending.head - val result = abortAfterFailure(f, PaymentFailed(failedId, paymentHash, Seq(LocalFailure(failedRoute.amount, failedRoute.hops, HtlcsTimedoutDownstream(channelId = ByteVector32.One, htlcs = Set.empty))))) + val result = abortAfterFailure(f, PaymentFailed(failedId, paymentHash, Seq(LocalFailure(failedRoute.amount, failedRoute.clearHops, HtlcsTimedoutDownstream(channelId = ByteVector32.One, htlcs = Set.empty))))) assert(result.failures.length == 1) } @@ -503,15 +501,15 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 5, None, routeParams = routeParams) sender.send(payFsm, payment) router.expectMsgType[RouteRequest] - router.send(payFsm, RouteResponse(Seq(Route(400000 msat, hop_ab_1 :: hop_be :: Nil), Route(600000 msat, hop_ac_1 :: hop_ce :: Nil)))) + router.send(payFsm, RouteResponse(Seq(Route(400000 msat, hop_ab_1 :: hop_be :: Nil, None), Route(600000 msat, hop_ac_1 :: hop_ce :: Nil, None)))) childPayFsm.expectMsgType[SendPaymentToRoute] childPayFsm.expectMsgType[SendPaymentToRoute] val (failedId1, failedRoute1) :: (failedId2, failedRoute2) :: Nil = payFsm.stateData.asInstanceOf[PaymentProgress].pending.toSeq - childPayFsm.send(payFsm, PaymentFailed(failedId1, paymentHash, Seq(UnreadableRemoteFailure(failedRoute1.amount, failedRoute1.hops)))) + childPayFsm.send(payFsm, PaymentFailed(failedId1, paymentHash, Seq(UnreadableRemoteFailure(failedRoute1.amount, failedRoute1.clearHops)))) router.expectMsgType[RouteRequest] - val result = abortAfterFailure(f, PaymentFailed(failedId2, paymentHash, Seq(RemoteFailure(failedRoute2.amount, failedRoute2.hops, Sphinx.DecryptedFailurePacket(e, PaymentTimeout))))) + val result = abortAfterFailure(f, PaymentFailed(failedId2, paymentHash, Seq(RemoteFailure(failedRoute2.amount, failedRoute2.clearHops, Sphinx.DecryptedFailurePacket(e, PaymentTimeout))))) assert(result.failures.length == 2) } @@ -521,12 +519,12 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 5, None, routeParams = routeParams) sender.send(payFsm, payment) router.expectMsgType[RouteRequest] - router.send(payFsm, RouteResponse(Seq(Route(400000 msat, hop_ab_1 :: hop_be :: Nil), Route(600000 msat, hop_ac_1 :: hop_ce :: Nil)))) + router.send(payFsm, RouteResponse(Seq(Route(400000 msat, hop_ab_1 :: hop_be :: Nil, None), Route(600000 msat, hop_ac_1 :: hop_ce :: Nil, None)))) childPayFsm.expectMsgType[SendPaymentToRoute] childPayFsm.expectMsgType[SendPaymentToRoute] val (failedId, failedRoute) :: (successId, successRoute) :: Nil = payFsm.stateData.asInstanceOf[PaymentProgress].pending.toSeq - childPayFsm.send(payFsm, PaymentFailed(failedId, paymentHash, Seq(UnreadableRemoteFailure(failedRoute.amount, failedRoute.hops)))) + childPayFsm.send(payFsm, PaymentFailed(failedId, paymentHash, Seq(UnreadableRemoteFailure(failedRoute.amount, failedRoute.clearHops)))) router.expectMsgType[RouteRequest] val result = fulfillPendingPayments(f, 1) @@ -541,16 +539,16 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 5, None, routeParams = routeParams) sender.send(payFsm, payment) router.expectMsgType[RouteRequest] - router.send(payFsm, RouteResponse(Seq(Route(400000 msat, hop_ab_1 :: hop_be :: Nil), Route(600000 msat, hop_ac_1 :: hop_ce :: Nil)))) + router.send(payFsm, RouteResponse(Seq(Route(400000 msat, hop_ab_1 :: hop_be :: Nil, None), Route(600000 msat, hop_ac_1 :: hop_ce :: Nil, None)))) childPayFsm.expectMsgType[SendPaymentToRoute] childPayFsm.expectMsgType[SendPaymentToRoute] val (failedId, failedRoute) :: (successId, successRoute) :: Nil = payFsm.stateData.asInstanceOf[PaymentProgress].pending.toSeq - childPayFsm.send(payFsm, PaymentFailed(failedId, paymentHash, Seq(RemoteFailure(failedRoute.amount, failedRoute.hops, Sphinx.DecryptedFailurePacket(e, PaymentTimeout))))) + childPayFsm.send(payFsm, PaymentFailed(failedId, paymentHash, Seq(RemoteFailure(failedRoute.amount, failedRoute.clearHops, Sphinx.DecryptedFailurePacket(e, PaymentTimeout))))) awaitCond(payFsm.stateName == PAYMENT_ABORTED) sender.watch(payFsm) - childPayFsm.send(payFsm, PaymentSent(cfg.id, paymentHash, paymentPreimage, finalAmount, e, Seq(PaymentSent.PartialPayment(successId, successRoute.amount, successRoute.fee(false), randomBytes32(), Some(successRoute.hops))))) + childPayFsm.send(payFsm, PaymentSent(cfg.id, paymentHash, paymentPreimage, finalAmount, e, Seq(PaymentSent.PartialPayment(successId, successRoute.amount, successRoute.fee(false), randomBytes32(), Some(successRoute.clearHops))))) sender.expectMsg(PreimageReceived(paymentHash, paymentPreimage)) val result = sender.expectMsgType[PaymentSent] assert(result.id == cfg.id) @@ -574,17 +572,17 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 5, None, routeParams = routeParams) sender.send(payFsm, payment) router.expectMsgType[RouteRequest] - router.send(payFsm, RouteResponse(Seq(Route(400000 msat, hop_ab_1 :: hop_be :: Nil), Route(600000 msat, hop_ac_1 :: hop_ce :: Nil)))) + router.send(payFsm, RouteResponse(Seq(Route(400000 msat, hop_ab_1 :: hop_be :: Nil, None), Route(600000 msat, hop_ac_1 :: hop_ce :: Nil, None)))) childPayFsm.expectMsgType[SendPaymentToRoute] childPayFsm.expectMsgType[SendPaymentToRoute] val (childId, route) :: (failedId, failedRoute) :: Nil = payFsm.stateData.asInstanceOf[PaymentProgress].pending.toSeq - childPayFsm.send(payFsm, PaymentSent(cfg.id, paymentHash, paymentPreimage, finalAmount, e, Seq(PaymentSent.PartialPayment(childId, route.amount, route.fee(false), randomBytes32(), Some(route.hops))))) + childPayFsm.send(payFsm, PaymentSent(cfg.id, paymentHash, paymentPreimage, finalAmount, e, Seq(PaymentSent.PartialPayment(childId, route.amount, route.fee(false), randomBytes32(), Some(route.clearHops))))) sender.expectMsg(PreimageReceived(paymentHash, paymentPreimage)) awaitCond(payFsm.stateName == PAYMENT_SUCCEEDED) sender.watch(payFsm) - childPayFsm.send(payFsm, PaymentFailed(failedId, paymentHash, Seq(RemoteFailure(failedRoute.amount, failedRoute.hops, Sphinx.DecryptedFailurePacket(e, PaymentTimeout))))) + childPayFsm.send(payFsm, PaymentFailed(failedId, paymentHash, Seq(RemoteFailure(failedRoute.amount, failedRoute.clearHops, Sphinx.DecryptedFailurePacket(e, PaymentTimeout))))) val result = sender.expectMsgType[PaymentSent] assert(result.parts.length == 1 && result.parts.head.id == childId) assert(result.amountWithFees < finalAmount) // we got the preimage without paying the full amount @@ -604,7 +602,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS assert(pending.size == childCount) val partialPayments = pending.map { - case (childId, route) => PaymentSent.PartialPayment(childId, route.amount, route.fee(false), randomBytes32(), Some(route.hops)) + case (childId, route) => PaymentSent.PartialPayment(childId, route.amount, route.fee(false), randomBytes32(), Some(route.clearHops)) } partialPayments.foreach(pp => childPayFsm.send(payFsm, PaymentSent(cfg.id, paymentHash, paymentPreimage, finalAmount, e, Seq(pp)))) sender.expectMsg(PreimageReceived(paymentHash, paymentPreimage)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala index 1adca621b4..900e3cccc4 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala @@ -34,7 +34,7 @@ import fr.acinq.eclair.payment.send.PaymentInitiator._ import fr.acinq.eclair.payment.send.{PaymentError, PaymentInitiator, PaymentLifecycle} import fr.acinq.eclair.router.RouteNotFound import fr.acinq.eclair.router.Router._ -import fr.acinq.eclair.wire.protocol.OnionPaymentPayloadTlv.{AmountToForward, KeySend, OutgoingCltv} +import fr.acinq.eclair.wire.protocol.OnionPaymentPayloadTlv.KeySend import fr.acinq.eclair.wire.protocol.PaymentOnion.{FinalPayload, IntermediatePayload} import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.{CltvExpiryDelta, Feature, Features, InvoiceFeature, MilliSatoshiLong, NodeParams, TestConstants, TestKitBaseClass, TimestampSecond, UnknownFeature, randomBytes32, randomKey} @@ -102,10 +102,10 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike sender.send(initiator, req) sender.expectMsgType[UUID] payFsm.expectMsgType[SendPaymentConfig] - val tlvs = payFsm.expectMsgType[PaymentLifecycle.SendPayment].finalPayload.records - assert(tlvs.get[AmountToForward].get.amount == finalAmount) - assert(tlvs.get[OutgoingCltv].get.cltv == req.invoice.minFinalCltvExpiryDelta.toCltvExpiry(nodeParams.currentBlockHeight + 1)) - assert(tlvs.unknown == customRecords) + val sendPayment = payFsm.expectMsgType[PaymentLifecycle.SendPayment] + assert(sendPayment.amount == finalAmount) + assert(sendPayment.targetExpiry == req.invoice.minFinalCltvExpiryDelta.toCltvExpiry(nodeParams.currentBlockHeight + 1)) + assert(sendPayment.userCustomTlvs == customRecords) } test("forward keysend payment") { f => @@ -114,11 +114,11 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike sender.send(initiator, req) sender.expectMsgType[UUID] payFsm.expectMsgType[SendPaymentConfig] - val tlvs = payFsm.expectMsgType[PaymentLifecycle.SendPayment].finalPayload.records - assert(tlvs.get[AmountToForward].get.amount == finalAmount) - assert(tlvs.get[OutgoingCltv].get.cltv == Channel.MIN_CLTV_EXPIRY_DELTA.toCltvExpiry(nodeParams.currentBlockHeight + 1)) - assert(tlvs.get[KeySend].get.paymentPreimage == paymentPreimage) - assert(tlvs.unknown.isEmpty) + val sendPayment = payFsm.expectMsgType[PaymentLifecycle.SendPayment] + assert(sendPayment.amount == finalAmount) + assert(sendPayment.targetExpiry == Channel.MIN_CLTV_EXPIRY_DELTA.toCltvExpiry(nodeParams.currentBlockHeight + 1)) + assert(sendPayment.additionalTlvs.contains(KeySend(paymentPreimage))) + assert(sendPayment.userCustomTlvs.isEmpty) } test("reject payment with unsupported mandatory feature") { f => @@ -155,7 +155,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike sender.send(initiator, request) val payment = sender.expectMsgType[SendPaymentToRouteResponse] payFsm.expectMsg(SendPaymentConfig(payment.paymentId, payment.parentId, None, paymentHash, finalAmount, c, Upstream.Local(payment.paymentId), Some(invoice), storeInDb = true, publishEvent = true, recordPathFindingMetrics = false, Nil)) - payFsm.expectMsg(PaymentLifecycle.SendPaymentToRoute(initiator, Left(route), FinalPayload.Standard.createSinglePartPayload(finalAmount, finalExpiryDelta.toCltvExpiry(nodeParams.currentBlockHeight + 1), invoice.paymentSecret.get, invoice.paymentMetadata))) + payFsm.expectMsg(PaymentLifecycle.SendPaymentToRoute(initiator, Left(route), finalAmount, finalAmount, finalExpiryDelta.toCltvExpiry(nodeParams.currentBlockHeight + 1), invoice.paymentSecret.get, invoice.paymentMetadata)) sender.send(initiator, GetPayment(Left(payment.paymentId))) sender.expectMsg(PaymentIsPending(payment.paymentId, invoice.paymentHash, PendingPaymentToRoute(sender.ref, request))) @@ -180,7 +180,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike sender.send(initiator, req) val id = sender.expectMsgType[UUID] payFsm.expectMsg(SendPaymentConfig(id, id, None, paymentHash, finalAmount, c, Upstream.Local(id), Some(invoice), storeInDb = true, publishEvent = true, recordPathFindingMetrics = true, Nil)) - payFsm.expectMsg(PaymentLifecycle.SendPaymentToNode(initiator, c, FinalPayload.Standard(TlvStream(OnionPaymentPayloadTlv.AmountToForward(finalAmount), OnionPaymentPayloadTlv.OutgoingCltv(req.finalExpiry(nodeParams.currentBlockHeight)), OnionPaymentPayloadTlv.PaymentData(invoice.paymentSecret.get, finalAmount))), 1, routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams)) + payFsm.expectMsg(PaymentLifecycle.SendPaymentToNode(initiator, c, finalAmount, finalAmount, req.finalExpiry(nodeParams.currentBlockHeight), invoice.paymentSecret.get, None, 1, routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams)) sender.send(initiator, GetPayment(Left(id))) sender.expectMsg(PaymentIsPending(id, invoice.paymentHash, PendingPaymentToNode(sender.ref, req))) @@ -230,11 +230,10 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val msg = payFsm.expectMsgType[PaymentLifecycle.SendPaymentToRoute] assert(msg.replyTo == initiator) assert(msg.route == Left(route)) - assert(msg.finalPayload.isInstanceOf[FinalPayload.Standard]) - assert(msg.finalPayload.amount == finalAmount / 2) - assert(msg.finalPayload.expiry == req.finalExpiry(nodeParams.currentBlockHeight)) - assert(msg.finalPayload.asInstanceOf[FinalPayload.Standard].paymentSecret == invoice.paymentSecret.get) - assert(msg.finalPayload.totalAmount == finalAmount) + assert(msg.amount == finalAmount / 2) + assert(msg.targetExpiry == req.finalExpiry(nodeParams.currentBlockHeight)) + assert(msg.paymentSecret == invoice.paymentSecret.get) + assert(msg.totalAmount == finalAmount) sender.send(initiator, GetPayment(Left(payment.paymentId))) sender.expectMsg(PaymentIsPending(payment.paymentId, invoice.paymentHash, PendingPaymentToRoute(sender.ref, req))) @@ -459,11 +458,10 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike payFsm.expectMsg(SendPaymentConfig(payment.paymentId, payment.parentId, None, paymentHash, finalAmount, c, Upstream.Local(payment.paymentId), Some(invoice), storeInDb = true, publishEvent = true, recordPathFindingMetrics = false, Seq(NodeHop(b, c, CltvExpiryDelta(0), 0 msat)))) val msg = payFsm.expectMsgType[PaymentLifecycle.SendPaymentToRoute] assert(msg.route == Left(route)) - assert(msg.finalPayload.isInstanceOf[FinalPayload.Standard]) - assert(msg.finalPayload.amount == finalAmount + trampolineFees) - assert(msg.finalPayload.asInstanceOf[FinalPayload.Standard].paymentSecret == payment.trampolineSecret.get) - assert(msg.finalPayload.totalAmount == finalAmount + trampolineFees) - val trampolineOnion = msg.finalPayload.records.get[OnionPaymentPayloadTlv.TrampolineOnion] + assert(msg.amount == finalAmount + trampolineFees) + assert(msg.paymentSecret == payment.trampolineSecret.get) + assert(msg.totalAmount == finalAmount + trampolineFees) + val trampolineOnion = msg.additionalTlvs.collectFirst{case t:OnionPaymentPayloadTlv.TrampolineOnion => t} assert(trampolineOnion.nonEmpty) // Verify that the trampoline node can correctly peel the trampoline onion. diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala index e889b9b1fd..1a640d2ff8 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala @@ -104,8 +104,8 @@ class PaymentLifecycleSpec extends BaseRouterSpec { import cfg._ // pre-computed route going from A to D - val route = Route(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: channelHopFromUpdate(b, c, update_bc) :: channelHopFromUpdate(c, d, update_cd) :: Nil) - val request = SendPaymentToRoute(sender.ref, Right(route), PaymentOnion.FinalPayload.Standard.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata)) + val route = Route(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: channelHopFromUpdate(b, c, update_bc) :: channelHopFromUpdate(c, d, update_cd) :: Nil, None) + val request = SendPaymentToRoute(sender.ref, Right(route), defaultAmountMsat, defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata) sender.send(paymentFSM, request) routerForwarder.expectNoMessage(100 millis) // we don't need the router, we have the pre-computed route val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]]) @@ -118,7 +118,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val ps = sender.expectMsgType[PaymentSent] assert(ps.id == parentId) - assert(ps.parts.head.route.contains(route.hops)) + assert(ps.parts.head.route.contains(route.clearHops)) awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status.isInstanceOf[OutgoingPaymentStatus.Succeeded])) metricsListener.expectNoMessage() @@ -133,7 +133,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { // pre-computed route going from A to D val route = PredefinedNodeRoute(Seq(a, b, c, d)) - val request = SendPaymentToRoute(sender.ref, Left(route), PaymentOnion.FinalPayload.Standard.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata)) + val request = SendPaymentToRoute(sender.ref, Left(route), defaultAmountMsat, defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata) sender.send(paymentFSM, request) routerForwarder.expectMsg(FinalizeRoute(defaultAmountMsat, route, paymentContext = Some(cfg.paymentContext))) @@ -152,14 +152,14 @@ class PaymentLifecycleSpec extends BaseRouterSpec { metricsListener.expectNoMessage() - assert(routerForwarder.expectMsgType[RouteDidRelay].route.hops.map(_.nodeId) === Seq(a, b, c)) + assert(routerForwarder.expectMsgType[RouteDidRelay].route.clearHops.map(_.nodeId) === Seq(a, b, c)) } test("send to route (nodes not found in the graph)") { routerFixture => val payFixture = createPaymentLifecycle(recordMetrics = false) import payFixture._ - val brokenRoute = SendPaymentToRoute(sender.ref, Left(PredefinedNodeRoute(Seq(randomKey().publicKey, randomKey().publicKey, randomKey().publicKey))), PaymentOnion.FinalPayload.Standard.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata)) + val brokenRoute = SendPaymentToRoute(sender.ref, Left(PredefinedNodeRoute(Seq(randomKey().publicKey, randomKey().publicKey, randomKey().publicKey))), defaultAmountMsat, defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata) sender.send(paymentFSM, brokenRoute) routerForwarder.expectMsgType[FinalizeRoute] routerForwarder.forward(routerFixture.router) @@ -176,7 +176,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val payFixture = createPaymentLifecycle(recordMetrics = false) import payFixture._ - val brokenRoute = SendPaymentToRoute(sender.ref, Left(PredefinedChannelRoute(randomKey().publicKey, Seq(ShortChannelId(1), ShortChannelId(2)))), PaymentOnion.FinalPayload.Standard.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata)) + val brokenRoute = SendPaymentToRoute(sender.ref, Left(PredefinedChannelRoute(randomKey().publicKey, Seq(ShortChannelId(1), ShortChannelId(2)))), defaultAmountMsat, defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata) sender.send(paymentFSM, brokenRoute) routerForwarder.expectMsgType[FinalizeRoute] routerForwarder.forward(routerFixture.router) @@ -197,7 +197,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val recipient = randomKey().publicKey val route = PredefinedNodeRoute(Seq(a, b, c, recipient)) val extraEdges = Seq(BasicEdge(c, recipient, ShortChannelId(561), 1 msat, 100, CltvExpiryDelta(144))) - val request = SendPaymentToRoute(sender.ref, Left(route), PaymentOnion.FinalPayload.Standard.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), extraEdges) + val request = SendPaymentToRoute(sender.ref, Left(route), defaultAmountMsat, defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata, extraEdges) sender.send(paymentFSM, request) routerForwarder.expectMsg(FinalizeRoute(defaultAmountMsat, route, extraEdges, paymentContext = Some(cfg.paymentContext))) @@ -215,7 +215,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { metricsListener.expectNoMessage() - assert(routerForwarder.expectMsgType[RouteDidRelay].route.hops.map(_.nodeId) === Seq(a, b, c)) + assert(routerForwarder.expectMsgType[RouteDidRelay].route.clearHops.map(_.nodeId) === Seq(a, b, c)) } test("payment failed (route not found)") { routerFixture => @@ -223,7 +223,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { import payFixture._ import cfg._ - val request = SendPaymentToNode(sender.ref, f, PaymentOnion.FinalPayload.Standard.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 5, routeParams = defaultRouteParams) + val request = SendPaymentToNode(sender.ref, f, defaultAmountMsat, defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata, 5, routeParams = defaultRouteParams) sender.send(paymentFSM, request) val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]]) val routeRequest = routerForwarder.expectMsgType[RouteRequest] @@ -256,7 +256,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { "my-test-experiment", experimentPercentage = 100 ).getDefaultRouteParams - val request = SendPaymentToNode(sender.ref, d, PaymentOnion.FinalPayload.Standard.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 5, routeParams = routeParams) + val request = SendPaymentToNode(sender.ref, d, defaultAmountMsat, defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata, 5, routeParams = routeParams) sender.send(paymentFSM, request) val routeRequest = routerForwarder.expectMsgType[RouteRequest] val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]]) @@ -281,7 +281,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { import cfg._ val paymentMetadataTooBig = ByteVector.fromValidHex("01" * 1300) - val request = SendPaymentToNode(sender.ref, d, PaymentOnion.FinalPayload.Standard.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, Some(paymentMetadataTooBig)), 5, routeParams = defaultRouteParams) + val request = SendPaymentToNode(sender.ref, d, defaultAmountMsat, defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, Some(paymentMetadataTooBig), 5, routeParams = defaultRouteParams) sender.send(paymentFSM, request) val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]]) val routeRequest = routerForwarder.expectMsgType[RouteRequest] @@ -300,7 +300,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { import payFixture._ import cfg._ - val request = SendPaymentToNode(sender.ref, d, PaymentOnion.FinalPayload.Standard.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 2, routeParams = defaultRouteParams) + val request = SendPaymentToNode(sender.ref, d, defaultAmountMsat, defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata, 2, routeParams = defaultRouteParams) sender.send(paymentFSM, request) routerForwarder.expectMsg(defaultRouteRequest(a, d, cfg)) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) @@ -327,7 +327,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFail(UpdateFailHtlc(ByteVector32.Zeroes, 0, defaultPaymentHash)))) // unparsable message // we allow 2 tries, so we send a 2nd request to the router - assert(sender.expectMsgType[PaymentFailed].failures == UnreadableRemoteFailure(route.amount, route.hops) :: UnreadableRemoteFailure(route.amount, route.hops) :: Nil) + assert(sender.expectMsgType[PaymentFailed].failures == UnreadableRemoteFailure(route.amount, route.clearHops) :: UnreadableRemoteFailure(route.amount, route.clearHops) :: Nil) awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status.isInstanceOf[OutgoingPaymentStatus.Failed])) // after last attempt the payment is failed val metrics = metricsListener.expectMsgType[PathFindingExperimentMetrics] @@ -345,7 +345,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { import payFixture._ import cfg._ - val request = SendPaymentToNode(sender.ref, d, PaymentOnion.FinalPayload.Standard.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 2, routeParams = defaultRouteParams) + val request = SendPaymentToNode(sender.ref, d, defaultAmountMsat, defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata, 2, routeParams = defaultRouteParams) sender.send(paymentFSM, request) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) routerForwarder.expectMsgType[RouteRequest] @@ -366,7 +366,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { import payFixture._ import cfg._ - val request = SendPaymentToNode(sender.ref, d, PaymentOnion.FinalPayload.Standard.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 2, routeParams = defaultRouteParams) + val request = SendPaymentToNode(sender.ref, d, defaultAmountMsat, defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata, 2, routeParams = defaultRouteParams) sender.send(paymentFSM, request) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) routerForwarder.expectMsgType[RouteRequest] @@ -386,7 +386,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { import payFixture._ import cfg._ - val request = SendPaymentToNode(sender.ref, d, PaymentOnion.FinalPayload.Standard.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 2, routeParams = defaultRouteParams) + val request = SendPaymentToNode(sender.ref, d, defaultAmountMsat, defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata, 2, routeParams = defaultRouteParams) sender.send(paymentFSM, request) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) @@ -409,7 +409,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { import payFixture._ import cfg._ - val request = SendPaymentToNode(sender.ref, d, PaymentOnion.FinalPayload.Standard.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 2, routeParams = defaultRouteParams) + val request = SendPaymentToNode(sender.ref, d, defaultAmountMsat, defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata, 2, routeParams = defaultRouteParams) sender.send(paymentFSM, request) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) @@ -432,7 +432,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { import payFixture._ import cfg._ - val request = SendPaymentToNode(sender.ref, d, PaymentOnion.FinalPayload.Standard.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 2, routeParams = defaultRouteParams) + val request = SendPaymentToNode(sender.ref, d, defaultAmountMsat, defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata, 2, routeParams = defaultRouteParams) sender.send(paymentFSM, request) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) @@ -455,7 +455,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val payFixture = createPaymentLifecycle() import payFixture._ - val request = SendPaymentToNode(sender.ref, d, PaymentOnion.FinalPayload.Standard.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 2, routeParams = defaultRouteParams) + val request = SendPaymentToNode(sender.ref, d, defaultAmountMsat, defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata, 2, routeParams = defaultRouteParams) sender.send(paymentFSM, request) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE) val WaitingForRoute(_, Nil, _) = paymentFSM.stateData @@ -478,7 +478,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { routerForwarder.expectMsg(defaultRouteRequest(a, d, cfg).copy(ignore = Ignore(Set.empty, Set(ChannelDesc(update_bc.shortChannelId, b, c))))) routerForwarder.forward(routerFixture.router) // we allow 2 tries, so we send a 2nd request to the router - assert(sender.expectMsgType[PaymentFailed].failures == RemoteFailure(route.amount, route.hops, Sphinx.DecryptedFailurePacket(b, failure)) :: LocalFailure(defaultAmountMsat, Nil, RouteNotFound) :: Nil) + assert(sender.expectMsgType[PaymentFailed].failures == RemoteFailure(route.amount, route.clearHops, Sphinx.DecryptedFailurePacket(b, failure)) :: LocalFailure(defaultAmountMsat, Nil, RouteNotFound) :: Nil) } test("payment failed (Update)") { routerFixture => @@ -486,7 +486,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { import payFixture._ import cfg._ - val request = SendPaymentToNode(sender.ref, d, PaymentOnion.FinalPayload.Standard.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 5, routeParams = defaultRouteParams) + val request = SendPaymentToNode(sender.ref, d, defaultAmountMsat, defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata, 5, routeParams = defaultRouteParams) sender.send(paymentFSM, request) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) @@ -530,7 +530,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { routerForwarder.forward(routerFixture.router) // this time the router can't find a route: game over - assert(sender.expectMsgType[PaymentFailed].failures == RemoteFailure(route1.amount, route1.hops, Sphinx.DecryptedFailurePacket(b, failure)) :: RemoteFailure(route2.amount, route2.hops, Sphinx.DecryptedFailurePacket(b, failure2)) :: LocalFailure(defaultAmountMsat, Nil, RouteNotFound) :: Nil) + assert(sender.expectMsgType[PaymentFailed].failures == RemoteFailure(route1.amount, route1.clearHops, Sphinx.DecryptedFailurePacket(b, failure)) :: RemoteFailure(route2.amount, route2.clearHops, Sphinx.DecryptedFailurePacket(b, failure2)) :: LocalFailure(defaultAmountMsat, Nil, RouteNotFound) :: Nil) awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status.isInstanceOf[OutgoingPaymentStatus.Failed])) routerForwarder.expectNoMessage(100 millis) @@ -540,7 +540,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val payFixture = createPaymentLifecycle() import payFixture._ - val request = SendPaymentToNode(sender.ref, d, PaymentOnion.FinalPayload.Standard.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 1, routeParams = defaultRouteParams) + val request = SendPaymentToNode(sender.ref, d, defaultAmountMsat, defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata, 1, routeParams = defaultRouteParams) sender.send(paymentFSM, request) routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d, cfg)) routerForwarder.forward(routerFixture.router) @@ -571,7 +571,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { BasicEdge(c, d, scid_cd, update_cd.feeBaseMsat, update_cd.feeProportionalMillionths, update_cd.cltvExpiryDelta) ) - val request = SendPaymentToNode(sender.ref, d, PaymentOnion.FinalPayload.Standard.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 5, extraEdges = extraEdges, routeParams = defaultRouteParams) + val request = SendPaymentToNode(sender.ref, d, defaultAmountMsat, defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata, 5, extraEdges = extraEdges, routeParams = defaultRouteParams) sender.send(paymentFSM, request) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) @@ -612,7 +612,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { // we build an assisted route for channel cd val extraEdges = Seq(BasicEdge(c, d, scid_cd, update_cd.feeBaseMsat, update_cd.feeProportionalMillionths, update_cd.cltvExpiryDelta)) - val request = SendPaymentToNode(sender.ref, d, PaymentOnion.FinalPayload.Standard.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 1, extraEdges = extraEdges, routeParams = defaultRouteParams) + val request = SendPaymentToNode(sender.ref, d, defaultAmountMsat, defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata, 1, extraEdges = extraEdges, routeParams = defaultRouteParams) sender.send(paymentFSM, request) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) @@ -628,7 +628,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val failureOnion = Sphinx.FailurePacket.wrap(Sphinx.FailurePacket.create(sharedSecrets1(1)._1, failure), sharedSecrets1.head._1) sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFail(UpdateFailHtlc(ByteVector32.Zeroes, 0, failureOnion)))) - assert(routerForwarder.expectMsgType[RouteCouldRelay].route.hops.map(_.shortChannelId) == Seq(update_ab, update_bc).map(_.shortChannelId)) + assert(routerForwarder.expectMsgType[RouteCouldRelay].route.clearHops.map(_.shortChannelId) == Seq(update_ab, update_bc).map(_.shortChannelId)) routerForwarder.expectMsg(ExcludeChannel(ChannelDesc(update_cd.shortChannelId, c, d), Some(nodeParams.routerConf.channelExcludeDuration))) routerForwarder.expectMsg(channelUpdate_cd_disabled) } @@ -638,7 +638,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { import payFixture._ import cfg._ - val request = SendPaymentToNode(sender.ref, d, PaymentOnion.FinalPayload.Standard.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 2, routeParams = defaultRouteParams) + val request = SendPaymentToNode(sender.ref, d, defaultAmountMsat, defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata, 2, routeParams = defaultRouteParams) sender.send(paymentFSM, request) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) @@ -657,7 +657,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { routerForwarder.forward(router) // we allow 2 tries, so we send a 2nd request to the router, which won't find another route - assert(sender.expectMsgType[PaymentFailed].failures == RemoteFailure(route1.amount, route1.hops, Sphinx.DecryptedFailurePacket(b, failure)) :: LocalFailure(defaultAmountMsat, Nil, RouteNotFound) :: Nil) + assert(sender.expectMsgType[PaymentFailed].failures == RemoteFailure(route1.amount, route1.clearHops, Sphinx.DecryptedFailurePacket(b, failure)) :: LocalFailure(defaultAmountMsat, Nil, RouteNotFound) :: Nil) awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status.isInstanceOf[OutgoingPaymentStatus.Failed])) } @@ -676,7 +676,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { import payFixture._ import cfg._ - val request = SendPaymentToNode(sender.ref, d, PaymentOnion.FinalPayload.Standard.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 5, routeParams = defaultRouteParams) + val request = SendPaymentToNode(sender.ref, d, defaultAmountMsat, defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata, 5, routeParams = defaultRouteParams) sender.send(paymentFSM, request) routerForwarder.expectMsgType[RouteRequest] val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]]) @@ -703,7 +703,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { assert(metrics.fees == 730.msat) metricsListener.expectNoMessage() - assert(routerForwarder.expectMsgType[RouteDidRelay].route.hops.map(_.shortChannelId) == Seq(update_ab, update_bc, update_cd).map(_.shortChannelId)) + assert(routerForwarder.expectMsgType[RouteDidRelay].route.clearHops.map(_.shortChannelId) == Seq(update_ab, update_bc, update_cd).map(_.shortChannelId)) } test("payment succeeded to a channel with fees=0") { routerFixture => @@ -732,7 +732,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { import payFixture._ // we send a payment to H - val request = SendPaymentToNode(sender.ref, h, PaymentOnion.FinalPayload.Standard.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 5, routeParams = defaultRouteParams) + val request = SendPaymentToNode(sender.ref, h, defaultAmountMsat, defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata, 5, routeParams = defaultRouteParams) sender.send(paymentFSM, request) routerForwarder.expectMsgType[RouteRequest] @@ -744,13 +744,13 @@ class PaymentLifecycleSpec extends BaseRouterSpec { sender.send(paymentFSM, addCompleted(HtlcResult.OnChainFulfill(defaultPaymentPreimage))) val paymentOK = sender.expectMsgType[PaymentSent] val PaymentSent(_, _, paymentOK.paymentPreimage, finalAmount, _, PartialPayment(_, partAmount, fee, ByteVector32.Zeroes, _, _) :: Nil) = eventListener.expectMsgType[PaymentSent] - assert(partAmount == request.finalPayload.amount) + assert(partAmount == request.amount) assert(finalAmount == defaultAmountMsat) // NB: A -> B doesn't pay fees because it's our direct neighbor // NB: B -> H doesn't asks for fees at all assert(fee == 0.msat) - assert(paymentOK.recipientAmount == request.finalPayload.amount) + assert(paymentOK.recipientAmount == request.amount) val metrics = metricsListener.expectMsgType[PathFindingExperimentMetrics] assert(metrics.status == "SUCCESS") @@ -759,7 +759,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { assert(metrics.fees == 0.msat) metricsListener.expectNoMessage() - assert(routerForwarder.expectMsgType[RouteDidRelay].route.hops.map(_.shortChannelId) == Seq(update_ab, channelUpdate_bh).map(_.shortChannelId)) + assert(routerForwarder.expectMsgType[RouteDidRelay].route.clearHops.map(_.shortChannelId) == Seq(update_ab, channelUpdate_bh).map(_.shortChannelId)) } test("filter errors properly") { () => @@ -819,7 +819,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { import payFixture._ import cfg._ - val request = SendPaymentToNode(sender.ref, d, PaymentOnion.FinalPayload.Standard.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 3, routeParams = defaultRouteParams) + val request = SendPaymentToNode(sender.ref, d, defaultAmountMsat, defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata, 3, routeParams = defaultRouteParams) sender.send(paymentFSM, request) routerForwarder.expectMsgType[RouteRequest] val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]]) @@ -832,7 +832,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { assert(nodeParams.db.payments.getOutgoingPayment(id).isEmpty) eventListener.expectNoMessage(100 millis) - assert(routerForwarder.expectMsgType[RouteDidRelay].route.hops.map(_.nextNodeId) == Seq(b, c, d)) + assert(routerForwarder.expectMsgType[RouteDidRelay].route.clearHops.map(_.nextNodeId) == Seq(b, c, d)) } test("send to route (no retry on error") { () => @@ -841,8 +841,8 @@ class PaymentLifecycleSpec extends BaseRouterSpec { import cfg._ // pre-computed route going from A to D - val route = Route(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: channelHopFromUpdate(b, c, update_bc) :: channelHopFromUpdate(c, d, update_cd) :: Nil) - val request = SendPaymentToRoute(sender.ref, Right(route), PaymentOnion.FinalPayload.Standard.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata)) + val route = Route(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: channelHopFromUpdate(b, c, update_bc) :: channelHopFromUpdate(c, d, update_cd) :: Nil, None) + val request = SendPaymentToRoute(sender.ref, Right(route), defaultAmountMsat, defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata) sender.send(paymentFSM, request) routerForwarder.expectNoMessage(100 millis) // we don't need the router, we have the pre-computed route val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]]) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala index 68b5b00152..8801ba455f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala @@ -63,8 +63,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { } def testBuildOnion(): Unit = { - val Right(finalPayload) = FinalPayload.Standard.validate(TlvStream(AmountToForward(finalAmount), OutgoingCltv(finalExpiry), PaymentData(paymentSecret, 0 msat))) - val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash, hops, finalPayload) + val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash, hops, None, finalAmount, 0 msat, finalExpiry, paymentSecret, None, Nil, Nil) assert(firstAmount == amount_ab) assert(firstExpiry == expiry_ab) assert(onion.packet.payload.length == PaymentOnionCodecs.paymentOnionPayloadLength) @@ -119,7 +118,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { } test("build a command including the onion") { - val Success((add, _)) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID), paymentHash, hops, FinalPayload.Standard.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret, None)) + val Success((add, _, _)) = buildCommand(randomKey(), ActorRef.noSender, Upstream.Local(UUID.randomUUID), paymentHash, hops, None, finalAmount, finalAmount, finalExpiry, paymentSecret, None, Nil, Nil) assert(add.amount > finalAmount) assert(add.cltvExpiry == finalExpiry + channelUpdate_de.cltvExpiryDelta + channelUpdate_cd.cltvExpiryDelta + channelUpdate_bc.cltvExpiryDelta) assert(add.paymentHash == paymentHash) @@ -130,7 +129,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { } test("build a command with no hops") { - val Success((add, _)) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, hops.take(1), FinalPayload.Standard.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret, Some(paymentMetadata))) + val Success((add, _, _)) = buildCommand(randomKey(), ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, hops.take(1), None, finalAmount, finalAmount, finalExpiry, paymentSecret, Some(paymentMetadata), Nil, Nil) assert(add.amount == finalAmount) assert(add.cltvExpiry == finalExpiry) assert(add.paymentHash == paymentHash) @@ -154,11 +153,11 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { // / \ / \ // a -> b -> c d e - val Success((amount_ac, expiry_ac, trampolineOnion)) = buildTrampolinePacket(paymentHash, trampolineHops, FinalPayload.Standard.createMultiPartPayload(finalAmount, finalAmount * 3, finalExpiry, paymentSecret, Some(hex"010203"))) + val Success((amount_ac, expiry_ac, trampolineOnion)) = buildTrampolinePacket(paymentHash, trampolineHops, finalAmount, finalAmount * 3, finalExpiry, paymentSecret, Some(hex"010203"), Nil, Nil) assert(amount_ac == amount_bc) assert(expiry_ac == expiry_bc) - val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash, trampolineChannelHops, FinalPayload.Standard.createTrampolinePayload(amount_ac, amount_ac, expiry_ac, randomBytes32(), trampolineOnion.packet)) + val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash, trampolineChannelHops, None, amount_ac, amount_ac, expiry_ac, randomBytes32(), None, Seq(OnionPaymentPayloadTlv.TrampolineOnion(trampolineOnion.packet)), Nil) assert(firstAmount == amount_ab) assert(firstExpiry == expiry_ab) @@ -182,7 +181,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { assert(inner_c.paymentMetadata.isEmpty) // c forwards the trampoline payment to d. - val Success((amount_d, expiry_d, onion_d)) = buildPaymentPacket(paymentHash, channelHopFromUpdate(c, d, channelUpdate_cd) :: Nil, FinalPayload.Standard.createTrampolinePayload(amount_cd, amount_cd, expiry_cd, randomBytes32(), packet_d)) + val Success((amount_d, expiry_d, onion_d)) = buildPaymentPacket(paymentHash, channelHopFromUpdate(c, d, channelUpdate_cd) :: Nil, None, amount_cd, amount_cd, expiry_cd, randomBytes32(), None, Seq(OnionPaymentPayloadTlv.TrampolineOnion(packet_d)), Nil) assert(amount_d == amount_cd) assert(expiry_d == expiry_cd) val add_d = UpdateAddHtlc(randomBytes32(), 3, amount_d, paymentHash, expiry_d, onion_d.packet, None) @@ -200,7 +199,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { assert(inner_d.paymentMetadata.isEmpty) // d forwards the trampoline payment to e. - val Success((amount_e, expiry_e, onion_e)) = buildPaymentPacket(paymentHash, channelHopFromUpdate(d, e, channelUpdate_de) :: Nil, FinalPayload.Standard.createTrampolinePayload(amount_de, amount_de, expiry_de, randomBytes32(), packet_e)) + val Success((amount_e, expiry_e, onion_e)) = buildPaymentPacket(paymentHash, channelHopFromUpdate(d, e, channelUpdate_de) :: Nil, None, amount_de, amount_de, expiry_de, randomBytes32(), None, Seq(OnionPaymentPayloadTlv.TrampolineOnion(packet_e)), Nil) assert(amount_e == amount_de) assert(expiry_e == expiry_de) val add_e = UpdateAddHtlc(randomBytes32(), 4, amount_e, paymentHash, expiry_e, onion_e.packet, None) @@ -218,11 +217,11 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { val routingHints = List(List(Bolt11Invoice.ExtraHop(randomKey().publicKey, ShortChannelId(42), 10 msat, 100, CltvExpiryDelta(144)))) val invoiceFeatures = Features[InvoiceFeature](VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional) val invoice = Bolt11Invoice(Block.RegtestGenesisBlock.hash, Some(finalAmount), paymentHash, priv_a.privateKey, Left("#reckless"), CltvExpiryDelta(18), None, None, routingHints, features = invoiceFeatures, paymentMetadata = Some(hex"010203")) - val Success((amount_ac, expiry_ac, trampolineOnion)) = buildTrampolineToLegacyPacket(invoice, trampolineHops, FinalPayload.Standard.createSinglePartPayload(finalAmount, finalExpiry, invoice.paymentSecret.get, None)) + val Success((amount_ac, expiry_ac, trampolineOnion)) = buildTrampolineToLegacyPacket(invoice, trampolineHops, finalAmount, finalExpiry) assert(amount_ac == amount_bc) assert(expiry_ac == expiry_bc) - val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash, trampolineChannelHops, FinalPayload.Standard.createTrampolinePayload(amount_ac, amount_ac, expiry_ac, randomBytes32(), trampolineOnion.packet)) + val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash, trampolineChannelHops, None, amount_ac, amount_ac, expiry_ac, randomBytes32(), None, Seq(OnionPaymentPayloadTlv.TrampolineOnion(trampolineOnion.packet)), Nil) assert(firstAmount == amount_ab) assert(firstExpiry == expiry_ab) @@ -243,7 +242,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { assert(inner_c.paymentSecret.isEmpty) // c forwards the trampoline payment to d. - val Success((amount_d, expiry_d, onion_d)) = buildPaymentPacket(paymentHash, channelHopFromUpdate(c, d, channelUpdate_cd) :: Nil, FinalPayload.Standard.createTrampolinePayload(amount_cd, amount_cd, expiry_cd, randomBytes32(), packet_d)) + val Success((amount_d, expiry_d, onion_d)) = buildPaymentPacket(paymentHash, channelHopFromUpdate(c, d, channelUpdate_cd) :: Nil, None, amount_cd, amount_cd, expiry_cd, randomBytes32(), None, Seq(OnionPaymentPayloadTlv.TrampolineOnion(packet_d)), Nil) assert(amount_d == amount_cd) assert(expiry_d == expiry_cd) val add_d = UpdateAddHtlc(randomBytes32(), 3, amount_d, paymentHash, expiry_d, onion_d.packet, None) @@ -265,19 +264,19 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { test("fail to build a trampoline payment when too much invoice data is provided") { val routingHintOverflow = List(List.fill(7)(Bolt11Invoice.ExtraHop(randomKey().publicKey, ShortChannelId(1), 10 msat, 100, CltvExpiryDelta(12)))) val invoice = Bolt11Invoice(Block.RegtestGenesisBlock.hash, Some(finalAmount), paymentHash, priv_a.privateKey, Left("#reckless"), CltvExpiryDelta(18), None, None, routingHintOverflow) - assert(buildTrampolineToLegacyPacket(invoice, trampolineHops, FinalPayload.Standard.createSinglePartPayload(finalAmount, finalExpiry, invoice.paymentSecret.get, invoice.paymentMetadata)).isFailure) + assert(buildTrampolineToLegacyPacket(invoice, trampolineHops, finalAmount, finalExpiry).isFailure) } test("fail to decrypt when the onion is invalid") { - val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash, hops, FinalPayload.Standard.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret, None)) + val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash, hops, None, finalAmount, finalAmount, finalExpiry, paymentSecret, None, Nil, Nil) val add = UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry, onion.packet.copy(payload = onion.packet.payload.reverse), None) val Left(failure) = decrypt(add, priv_b.privateKey, Features.empty) assert(failure.isInstanceOf[InvalidOnionHmac]) } test("fail to decrypt when the trampoline onion is invalid") { - val Success((amount_ac, expiry_ac, trampolineOnion)) = buildTrampolinePacket(paymentHash, trampolineHops, FinalPayload.Standard.createMultiPartPayload(finalAmount, finalAmount * 2, finalExpiry, paymentSecret, None)) - val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash, trampolineChannelHops, FinalPayload.Standard.createTrampolinePayload(amount_ac, amount_ac, expiry_ac, randomBytes32(), trampolineOnion.packet.copy(payload = trampolineOnion.packet.payload.reverse))) + val Success((amount_ac, expiry_ac, trampolineOnion)) = buildTrampolinePacket(paymentHash, trampolineHops, finalAmount, finalAmount * 2, finalExpiry, paymentSecret, None, Nil, Nil) + val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash, trampolineChannelHops, None, amount_ac, amount_ac, expiry_ac, randomBytes32(), None, Seq(OnionPaymentPayloadTlv.TrampolineOnion(trampolineOnion.packet.copy(payload = trampolineOnion.packet.payload.reverse))), Nil) val add_b = UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry, onion.packet, None) val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(add_b, priv_b.privateKey, Features.empty) val add_c = UpdateAddHtlc(randomBytes32(), 2, amount_bc, paymentHash, expiry_bc, packet_c, None) @@ -286,59 +285,59 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { } test("fail to decrypt when payment hash doesn't match associated data") { - val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash.reverse, hops, FinalPayload.Standard.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret, None)) + val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash.reverse, hops, None, finalAmount, finalAmount, finalExpiry, paymentSecret, None, Nil, Nil) val add = UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry, onion.packet, None) val Left(failure) = decrypt(add, priv_b.privateKey, Features.empty) assert(failure.isInstanceOf[InvalidOnionHmac]) } test("fail to decrypt at the final node when amount has been modified by next-to-last node") { - val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash, hops.take(1), FinalPayload.Standard.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret, None)) + val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash, hops.take(1), None, finalAmount, finalAmount, finalExpiry, paymentSecret, None, Nil, Nil) val add = UpdateAddHtlc(randomBytes32(), 1, firstAmount - 100.msat, paymentHash, firstExpiry, onion.packet, None) val Left(failure) = decrypt(add, priv_b.privateKey, Features.empty) assert(failure == FinalIncorrectHtlcAmount(firstAmount - 100.msat)) } test("fail to decrypt at the final node when expiry has been modified by next-to-last node") { - val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash, hops.take(1), FinalPayload.Standard.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret, None)) + val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash, hops.take(1), None, finalAmount, finalAmount, finalExpiry, paymentSecret, None, Nil, Nil) val add = UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry - CltvExpiryDelta(12), onion.packet, None) val Left(failure) = decrypt(add, priv_b.privateKey, Features.empty) assert(failure == FinalIncorrectCltvExpiry(firstExpiry - CltvExpiryDelta(12))) } test("fail to decrypt at the final trampoline node when amount has been modified by next-to-last trampoline") { - val Success((amount_ac, expiry_ac, trampolineOnion)) = buildTrampolinePacket(paymentHash, trampolineHops, FinalPayload.Standard.createMultiPartPayload(finalAmount, finalAmount, finalExpiry, paymentSecret, None)) - val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash, trampolineChannelHops, FinalPayload.Standard.createTrampolinePayload(amount_ac, amount_ac, expiry_ac, randomBytes32(), trampolineOnion.packet)) + val Success((amount_ac, expiry_ac, trampolineOnion)) = buildTrampolinePacket(paymentHash, trampolineHops, finalAmount, finalAmount, finalExpiry, paymentSecret, None, Nil, Nil) + val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash, trampolineChannelHops, None, amount_ac, amount_ac, expiry_ac, randomBytes32(), None, Seq(OnionPaymentPayloadTlv.TrampolineOnion(trampolineOnion.packet)), Nil) val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry, onion.packet, None), priv_b.privateKey, Features.empty) val Right(NodeRelayPacket(_, _, _, packet_d)) = decrypt(UpdateAddHtlc(randomBytes32(), 2, amount_bc, paymentHash, expiry_bc, packet_c, None), priv_c.privateKey, Features.empty) // c forwards the trampoline payment to d. - val Success((amount_d, expiry_d, onion_d)) = buildPaymentPacket(paymentHash, channelHopFromUpdate(c, d, channelUpdate_cd) :: Nil, FinalPayload.Standard.createTrampolinePayload(amount_cd, amount_cd, expiry_cd, randomBytes32(), packet_d)) + val Success((amount_d, expiry_d, onion_d)) = buildPaymentPacket(paymentHash, channelHopFromUpdate(c, d, channelUpdate_cd) :: Nil, None, amount_cd, amount_cd, expiry_cd, randomBytes32(), None, Seq(OnionPaymentPayloadTlv.TrampolineOnion(packet_d)), Nil) val Right(NodeRelayPacket(_, _, _, packet_e)) = decrypt(UpdateAddHtlc(randomBytes32(), 3, amount_d, paymentHash, expiry_d, onion_d.packet, None), priv_d.privateKey, Features.empty) // d forwards an invalid amount to e (the outer total amount doesn't match the inner amount). val invalidTotalAmount = amount_de + 100.msat - val Success((amount_e, expiry_e, onion_e)) = buildPaymentPacket(paymentHash, channelHopFromUpdate(d, e, channelUpdate_de) :: Nil, FinalPayload.Standard.createTrampolinePayload(amount_de, invalidTotalAmount, expiry_de, randomBytes32(), packet_e)) + val Success((amount_e, expiry_e, onion_e)) = buildPaymentPacket(paymentHash, channelHopFromUpdate(d, e, channelUpdate_de) :: Nil, None, amount_de, invalidTotalAmount, expiry_de, randomBytes32(), None, Seq(OnionPaymentPayloadTlv.TrampolineOnion(packet_e)), Nil) val Left(failure) = decrypt(UpdateAddHtlc(randomBytes32(), 4, amount_e, paymentHash, expiry_e, onion_e.packet, None), priv_e.privateKey, Features.empty) assert(failure == FinalIncorrectHtlcAmount(invalidTotalAmount)) } test("fail to decrypt at the final trampoline node when expiry has been modified by next-to-last trampoline") { - val Success((amount_ac, expiry_ac, trampolineOnion)) = buildTrampolinePacket(paymentHash, trampolineHops, FinalPayload.Standard.createMultiPartPayload(finalAmount, finalAmount, finalExpiry, paymentSecret, None)) - val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash, trampolineChannelHops, FinalPayload.Standard.createTrampolinePayload(amount_ac, amount_ac, expiry_ac, randomBytes32(), trampolineOnion.packet)) + val Success((amount_ac, expiry_ac, trampolineOnion)) = buildTrampolinePacket(paymentHash, trampolineHops, finalAmount, finalAmount, finalExpiry, paymentSecret, None, Nil, Nil) + val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash, trampolineChannelHops, None, amount_ac, amount_ac, expiry_ac, randomBytes32(), None, Seq(OnionPaymentPayloadTlv.TrampolineOnion(trampolineOnion.packet)), Nil) val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry, onion.packet, None), priv_b.privateKey, Features.empty) val Right(NodeRelayPacket(_, _, _, packet_d)) = decrypt(UpdateAddHtlc(randomBytes32(), 2, amount_bc, paymentHash, expiry_bc, packet_c, None), priv_c.privateKey, Features.empty) // c forwards the trampoline payment to d. - val Success((amount_d, expiry_d, onion_d)) = buildPaymentPacket(paymentHash, channelHopFromUpdate(c, d, channelUpdate_cd) :: Nil, FinalPayload.Standard.createTrampolinePayload(amount_cd, amount_cd, expiry_cd, randomBytes32(), packet_d)) + val Success((amount_d, expiry_d, onion_d)) = buildPaymentPacket(paymentHash, channelHopFromUpdate(c, d, channelUpdate_cd) :: Nil, None, amount_cd, amount_cd, expiry_cd, randomBytes32(), None, Seq(OnionPaymentPayloadTlv.TrampolineOnion(packet_d)), Nil) val Right(NodeRelayPacket(_, _, _, packet_e)) = decrypt(UpdateAddHtlc(randomBytes32(), 3, amount_d, paymentHash, expiry_d, onion_d.packet, None), priv_d.privateKey, Features.empty) // d forwards an invalid expiry to e (the outer expiry doesn't match the inner expiry). val invalidExpiry = expiry_de - CltvExpiryDelta(12) - val Success((amount_e, expiry_e, onion_e)) = buildPaymentPacket(paymentHash, channelHopFromUpdate(d, e, channelUpdate_de) :: Nil, FinalPayload.Standard.createTrampolinePayload(amount_de, amount_de, invalidExpiry, randomBytes32(), packet_e)) + val Success((amount_e, expiry_e, onion_e)) = buildPaymentPacket(paymentHash, channelHopFromUpdate(d, e, channelUpdate_de) :: Nil, None, amount_de, amount_de, invalidExpiry, randomBytes32(), None, Seq(OnionPaymentPayloadTlv.TrampolineOnion(packet_e)), Nil) val Left(failure) = decrypt(UpdateAddHtlc(randomBytes32(), 4, amount_e, paymentHash, expiry_e, onion_e.packet, None), priv_e.privateKey, Features.empty) assert(failure == FinalIncorrectCltvExpiry(invalidExpiry)) } test("fail to decrypt at intermediate trampoline node when amount is invalid") { - val Success((amount_ac, expiry_ac, trampolineOnion)) = buildTrampolinePacket(paymentHash, trampolineHops, FinalPayload.Standard.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret, None)) - val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash, trampolineChannelHops, FinalPayload.Standard.createTrampolinePayload(amount_ac, amount_ac, expiry_ac, randomBytes32(), trampolineOnion.packet)) + val Success((amount_ac, expiry_ac, trampolineOnion)) = buildTrampolinePacket(paymentHash, trampolineHops, finalAmount, finalAmount, finalExpiry, paymentSecret, None, Nil, Nil) + val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash, trampolineChannelHops, None, amount_ac, amount_ac, expiry_ac, randomBytes32(), None, Seq(OnionPaymentPayloadTlv.TrampolineOnion(trampolineOnion.packet)), Nil) val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry, onion.packet, None), priv_b.privateKey, Features.empty) // A trampoline relay is very similar to a final node: it can validate that the HTLC amount matches the onion outer amount. val Left(failure) = decrypt(UpdateAddHtlc(randomBytes32(), 2, amount_bc - 100.msat, paymentHash, expiry_bc, packet_c, None), priv_c.privateKey, Features.empty) @@ -346,8 +345,8 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { } test("fail to decrypt at intermediate trampoline node when expiry is invalid") { - val Success((amount_ac, expiry_ac, trampolineOnion)) = buildTrampolinePacket(paymentHash, trampolineHops, FinalPayload.Standard.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret, None)) - val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash, trampolineChannelHops, FinalPayload.Standard.createTrampolinePayload(amount_ac, amount_ac, expiry_ac, randomBytes32(), trampolineOnion.packet)) + val Success((amount_ac, expiry_ac, trampolineOnion)) = buildTrampolinePacket(paymentHash, trampolineHops, finalAmount, finalAmount, finalExpiry, paymentSecret, None, Nil, Nil) + val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash, trampolineChannelHops, None, amount_ac, amount_ac, expiry_ac, randomBytes32(), None, Seq(OnionPaymentPayloadTlv.TrampolineOnion(trampolineOnion.packet)), Nil) val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry, onion.packet, None), priv_b.privateKey, Features.empty) // A trampoline relay is very similar to a final node: it can validate that the HTLC expiry matches the onion outer expiry. val Left(failure) = decrypt(UpdateAddHtlc(randomBytes32(), 2, amount_bc, paymentHash, expiry_bc - CltvExpiryDelta(12), packet_c, None), priv_c.privateKey, Features.empty) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PostRestartHtlcCleanerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PostRestartHtlcCleanerSpec.scala index 92b304967b..99ab55727d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PostRestartHtlcCleanerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PostRestartHtlcCleanerSpec.scala @@ -720,7 +720,7 @@ object PostRestartHtlcCleanerSpec { val (paymentHash1, paymentHash2, paymentHash3) = (Crypto.sha256(preimage1), Crypto.sha256(preimage2), Crypto.sha256(preimage3)) def buildHtlc(htlcId: Long, channelId: ByteVector32, paymentHash: ByteVector32): UpdateAddHtlc = { - val Success((cmd, _)) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, hops, PaymentOnion.FinalPayload.Standard.createSinglePartPayload(finalAmount, finalExpiry, randomBytes32(), None)) + val Success((cmd, _, _)) = buildCommand(randomKey(), ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, hops, None, finalAmount, finalAmount, finalExpiry, randomBytes32(), None, Nil, Nil) UpdateAddHtlc(channelId, htlcId, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion, None) } @@ -729,7 +729,7 @@ object PostRestartHtlcCleanerSpec { def buildHtlcOut(htlcId: Long, channelId: ByteVector32, paymentHash: ByteVector32): DirectedHtlc = OutgoingHtlc(buildHtlc(htlcId, channelId, paymentHash)) def buildFinalHtlc(htlcId: Long, channelId: ByteVector32, paymentHash: ByteVector32): DirectedHtlc = { - val Success((cmd, _)) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, channelHopFromUpdate(a, TestConstants.Bob.nodeParams.nodeId, channelUpdate_ab) :: Nil, PaymentOnion.FinalPayload.Standard.createSinglePartPayload(finalAmount, finalExpiry, randomBytes32(), None)) + val Success((cmd, _, _)) = buildCommand(randomKey(), ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, channelHopFromUpdate(a, TestConstants.Bob.nodeParams.nodeId, channelUpdate_ab) :: Nil, None, finalAmount, finalAmount, finalExpiry, randomBytes32(), None, Nil, Nil) IncomingHtlc(UpdateAddHtlc(channelId, htlcId, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion, None)) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/NodeRelayerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/NodeRelayerSpec.scala index ca9a6463f6..69ddab3233 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/NodeRelayerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/NodeRelayerSpec.scala @@ -773,10 +773,9 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl val outgoingCfg = mockPayFSM.expectMessageType[SendPaymentConfig] validateOutgoingCfg(outgoingCfg, Upstream.Trampoline(incomingMultiPart.map(_.add))) val outgoingPayment = mockPayFSM.expectMessageType[SendPaymentToNode] - assert(outgoingPayment.finalPayload.isInstanceOf[FinalPayload.Standard]) - assert(outgoingPayment.finalPayload.amount == outgoingAmount) - assert(outgoingPayment.finalPayload.expiry == outgoingExpiry) - assert(outgoingPayment.finalPayload.asInstanceOf[FinalPayload.Standard].paymentMetadata == invoice.paymentMetadata) // we should use the provided metadata + assert(outgoingPayment.amount == outgoingAmount) + assert(outgoingPayment.targetExpiry == outgoingExpiry) + assert(outgoingPayment.paymentMetadata == invoice.paymentMetadata) // we should use the provided metadata assert(outgoingPayment.targetNodeId == outgoingNodeId) assert(outgoingPayment.extraEdges.head.asInstanceOf[BasicEdge].shortChannelId == ShortChannelId(42)) assert(outgoingPayment.extraEdges.head.asInstanceOf[BasicEdge].sourceNodeId == hints.head.nodeId) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/RelayerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/RelayerSpec.scala index d06d3f55fd..b6274e1bca 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/RelayerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/RelayerSpec.scala @@ -88,7 +88,7 @@ class RelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat } // we use this to build a valid onion - val Success((cmd, _)) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, hops, FinalPayload.Standard.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret, None)) + val Success((cmd, _, _)) = buildCommand(randomKey(), ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, hops, None, finalAmount, finalAmount, finalExpiry, paymentSecret, None, Nil, Nil) // and then manually build an htlc val add_ab = UpdateAddHtlc(channelId = randomBytes32(), id = 123456, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion, None) relayer ! RelayForward(add_ab) @@ -98,7 +98,7 @@ class RelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat test("relay an htlc-add at the final node to the payment handler") { f => import f._ - val Success((cmd, _)) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, hops.take(1), FinalPayload.Standard.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret, None)) + val Success((cmd, _, _)) = buildCommand(randomKey(), ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, hops.take(1), None, finalAmount, finalAmount, finalExpiry, paymentSecret, None, Nil, Nil) val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion, None) relayer ! RelayForward(add_ab) @@ -118,10 +118,10 @@ class RelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat // We simulate a payment split between multiple trampoline routes. val totalAmount = finalAmount * 3 val trampolineHops = NodeHop(a, b, channelUpdate_ab.cltvExpiryDelta, 0 msat) :: Nil - val Success((trampolineAmount, trampolineExpiry, trampolineOnion)) = OutgoingPaymentPacket.buildTrampolinePacket(paymentHash, trampolineHops, FinalPayload.Standard.createMultiPartPayload(finalAmount, totalAmount, finalExpiry, paymentSecret, None)) + val Success((trampolineAmount, trampolineExpiry, trampolineOnion)) = OutgoingPaymentPacket.buildTrampolinePacket(paymentHash, trampolineHops, finalAmount, totalAmount, finalExpiry, paymentSecret, None, Nil, Nil) assert(trampolineAmount == finalAmount) assert(trampolineExpiry == finalExpiry) - val Success((cmd, _)) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, channelHopFromUpdate(a, b, channelUpdate_ab) :: Nil, FinalPayload.Standard.createTrampolinePayload(trampolineAmount, trampolineAmount, trampolineExpiry, randomBytes32(), trampolineOnion.packet)) + val Success((cmd, _, _)) = buildCommand(randomKey(), ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, channelHopFromUpdate(a, b, channelUpdate_ab) :: Nil, None, trampolineAmount, trampolineAmount, trampolineExpiry, randomBytes32(), None, Seq(OnionPaymentPayloadTlv.TrampolineOnion(trampolineOnion.packet)), Nil) assert(cmd.amount == finalAmount) assert(cmd.cltvExpiry == finalExpiry) val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion, None) @@ -143,7 +143,7 @@ class RelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat import f._ // we use this to build a valid onion - val Success((cmd, _)) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, hops, FinalPayload.Standard.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret, None)) + val Success((cmd, _, _)) = buildCommand(randomKey(), ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, hops, None, finalAmount, finalAmount, finalExpiry, paymentSecret, None, Nil, Nil) // and then manually build an htlc with an invalid onion (hmac) val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion.copy(hmac = cmd.onion.hmac.reverse), None) @@ -164,8 +164,8 @@ class RelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat // we use this to build a valid trampoline onion inside a normal onion val trampolineHops = NodeHop(a, b, channelUpdate_ab.cltvExpiryDelta, 0 msat) :: NodeHop(b, c, channelUpdate_bc.cltvExpiryDelta, fee_b) :: Nil - val Success((trampolineAmount, trampolineExpiry, trampolineOnion)) = OutgoingPaymentPacket.buildTrampolinePacket(paymentHash, trampolineHops, FinalPayload.Standard.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret, None)) - val Success((cmd, _)) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, channelHopFromUpdate(a, b, channelUpdate_ab) :: Nil, FinalPayload.Standard.createTrampolinePayload(trampolineAmount, trampolineAmount, trampolineExpiry, randomBytes32(), trampolineOnion.packet)) + val Success((trampolineAmount, trampolineExpiry, trampolineOnion)) = OutgoingPaymentPacket.buildTrampolinePacket(paymentHash, trampolineHops, finalAmount, finalAmount, finalExpiry, paymentSecret, None, Nil, Nil) + val Success((cmd, _, _)) = buildCommand(randomKey(), ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, channelHopFromUpdate(a, b, channelUpdate_ab) :: Nil, None, trampolineAmount, trampolineAmount, trampolineExpiry, randomBytes32(), None, Seq(OnionPaymentPayloadTlv.TrampolineOnion(trampolineOnion.packet)), Nil) // and then manually build an htlc val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion, None) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala index 3783965545..23f89df0af 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala @@ -440,7 +440,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val g = DirectedGraph(edges) val Success(route :: Nil) = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(route.hops == channelHopFromUpdate(a, b, uab) :: channelHopFromUpdate(b, c, ubc) :: channelHopFromUpdate(c, d, ucd) :: channelHopFromUpdate(d, e, ude) :: Nil) + assert(route.clearHops == channelHopFromUpdate(a, b, uab) :: channelHopFromUpdate(b, c, ubc) :: channelHopFromUpdate(c, d, ucd) :: channelHopFromUpdate(d, e, ude) :: Nil) } test("blacklist routes") { @@ -505,12 +505,12 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val Success(route1 :: Nil) = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route2Ids(route1) == 1 :: 2 :: 3 :: 4 :: Nil) - assert(route1.hops(1).params.relayFees.feeBase == 10.msat) + assert(route1.clearHops(1).params.relayFees.feeBase == 10.msat) val extraGraphEdges = Set(makeEdge(2L, b, c, 5 msat, 5)) val Success(route2 :: Nil) = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, extraEdges = extraGraphEdges, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route2Ids(route2) == 1 :: 2 :: 3 :: 4 :: Nil) - assert(route2.hops(1).params.relayFees.feeBase == 5.msat) + assert(route2.clearHops(1).params.relayFees.feeBase == 5.msat) } test("compute ignored channels") { @@ -929,7 +929,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val Success(route :: Nil) = findRoute(g, thisNode, targetNode, amount, DEFAULT_MAX_FEE, 1, Set.empty, Set.empty, Set.empty, params, currentBlockHeight = BlockHeight(567634)) // simulate mainnet block for heuristic assert(route.length == 2) - assert(route.hops.last.nextNodeId == targetNode) + assert(route.clearHops.last.nextNodeId == targetNode) } test("validate path fees") { @@ -1073,7 +1073,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val amount = 50000 msat // These pending HTLCs will have already been taken into account in the edge's `balance_opt` field: findMultiPartRoute // should ignore this information. - val pendingHtlcs = Seq(Route(10000 msat, graphEdgeToHop(edge_ab_1) :: Nil), Route(5000 msat, graphEdgeToHop(edge_ab_2) :: Nil)) + val pendingHtlcs = Seq(Route(10000 msat, graphEdgeToHop(edge_ab_1) :: Nil, None), Route(5000 msat, graphEdgeToHop(edge_ab_2) :: Nil, None)) val Success(routes) = findMultiPartRoute(g, a, b, amount, 1 msat, pendingHtlcs = pendingHtlcs, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(routes.forall(_.length == 1), routes) checkRouteAmounts(routes, amount, 0 msat) @@ -1377,7 +1377,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { routes.foreach(route => { assert(route.length == 2) assert(route.amount <= 1_200_000.msat) - assert(!route.hops.flatMap(h => Seq(h.nodeId, h.nextNodeId)).contains(c)) + assert(!route.clearHops.flatMap(h => Seq(h.nodeId, h.nextNodeId)).contains(c)) }) } } @@ -1545,7 +1545,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(6L, d, e, 50 msat, 0, minHtlc = 100 msat, capacity = 25 sat), )) - val pendingHtlcs = Seq(Route(5000 msat, graphEdgeToHop(edge_ab) :: graphEdgeToHop(edge_be) :: Nil)) + val pendingHtlcs = Seq(Route(5000 msat, graphEdgeToHop(edge_ab) :: graphEdgeToHop(edge_be) :: Nil, None)) val Success(routes) = findMultiPartRoute(g, a, e, amount, maxFee, pendingHtlcs = pendingHtlcs, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(routes.forall(_.length == 2), routes) checkRouteAmounts(routes, amount, maxFee) @@ -1959,17 +1959,17 @@ object RouteCalculationSpec { def hops2Ids(hops: Seq[ChannelHop]): Seq[Long] = hops.map(hop => hop.shortChannelId.toLong) - def route2Ids(route: Route): Seq[Long] = hops2Ids(route.hops) + def route2Ids(route: Route): Seq[Long] = hops2Ids(route.clearHops) def routes2Ids(routes: Seq[Route]): Set[Seq[Long]] = routes.map(route2Ids).toSet - def route2Edges(route: Route): Seq[GraphEdge] = route.hops.map(hop => GraphEdge(ChannelDesc(hop.shortChannelId, hop.nodeId, hop.nextNodeId), hop.params, 0 sat, None)) + def route2Edges(route: Route): Seq[GraphEdge] = route.clearHops.map(hop => GraphEdge(ChannelDesc(hop.shortChannelId, hop.nodeId, hop.nextNodeId), hop.params, 0 sat, None)) - def route2Nodes(route: Route): Seq[(PublicKey, PublicKey)] = route.hops.map(hop => (hop.nodeId, hop.nextNodeId)) + def route2Nodes(route: Route): Seq[(PublicKey, PublicKey)] = route.clearHops.map(hop => (hop.nodeId, hop.nextNodeId)) def checkIgnoredChannels(routes: Seq[Route], shortChannelIds: Long*): Unit = { shortChannelIds.foreach(shortChannelId => routes.foreach(route => { - assert(route.hops.forall(_.shortChannelId.toLong != shortChannelId), route) + assert(route.clearHops.forall(_.shortChannelId.toLong != shortChannelId), route) })) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala index 5dd9b9d3cd..c0576920d0 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala @@ -355,13 +355,13 @@ class RouterSpec extends BaseRouterSpec { val sender = TestProbe() sender.send(router, RouteRequest(a, d, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, routeParams = DEFAULT_ROUTE_PARAMS)) val res = sender.expectMsgType[RouteResponse] - assert(res.routes.head.hops.map(_.nodeId).toList == a :: b :: c :: Nil) - assert(res.routes.head.hops.last.nextNodeId == d) + assert(res.routes.head.clearHops.map(_.nodeId).toList == a :: b :: c :: Nil) + assert(res.routes.head.clearHops.last.nextNodeId == d) sender.send(router, RouteRequest(a, h, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, routeParams = DEFAULT_ROUTE_PARAMS)) val res1 = sender.expectMsgType[RouteResponse] - assert(res1.routes.head.hops.map(_.nodeId).toList == a :: g :: Nil) - assert(res1.routes.head.hops.last.nextNodeId == h) + assert(res1.routes.head.clearHops.map(_.nodeId).toList == a :: g :: Nil) + assert(res1.routes.head.clearHops.last.nextNodeId == h) } test("route found (with extra routing info)") { fixture => @@ -375,8 +375,8 @@ class RouterSpec extends BaseRouterSpec { val extraHop_yz = ExtraHop(y, ShortChannelId(3), 20 msat, 21, CltvExpiryDelta(22)) sender.send(router, RouteRequest(a, z, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, extraEdges = Bolt11Invoice.toExtraEdges(extraHop_cx :: extraHop_xy :: extraHop_yz :: Nil, z), routeParams = DEFAULT_ROUTE_PARAMS)) val res = sender.expectMsgType[RouteResponse] - assert(res.routes.head.hops.map(_.nodeId).toList == a :: b :: c :: x :: y :: Nil) - assert(res.routes.head.hops.last.nextNodeId == z) + assert(res.routes.head.clearHops.map(_.nodeId).toList == a :: b :: c :: x :: y :: Nil) + assert(res.routes.head.clearHops.last.nextNodeId == z) } test("route not found (channel disabled)") { fixture => @@ -385,8 +385,8 @@ class RouterSpec extends BaseRouterSpec { val peerConnection = TestProbe() sender.send(router, RouteRequest(a, d, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, routeParams = DEFAULT_ROUTE_PARAMS)) val res = sender.expectMsgType[RouteResponse] - assert(res.routes.head.hops.map(_.nodeId).toList == a :: b :: c :: Nil) - assert(res.routes.head.hops.last.nextNodeId == d) + assert(res.routes.head.clearHops.map(_.nodeId).toList == a :: b :: c :: Nil) + assert(res.routes.head.clearHops.last.nextNodeId == d) val channelUpdate_cd1 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_c, d, scid_cd, CltvExpiryDelta(3), 0 msat, 153000 msat, 4, htlcMaximum, enable = false) peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, channelUpdate_cd1)) @@ -400,8 +400,8 @@ class RouterSpec extends BaseRouterSpec { val sender = TestProbe() sender.send(router, RouteRequest(a, h, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, routeParams = DEFAULT_ROUTE_PARAMS)) val res = sender.expectMsgType[RouteResponse] - assert(res.routes.head.hops.map(_.nodeId).toList == a :: g :: Nil) - assert(res.routes.head.hops.last.nextNodeId == h) + assert(res.routes.head.clearHops.map(_.nodeId).toList == a :: g :: Nil) + assert(res.routes.head.clearHops.last.nextNodeId == h) val channelUpdate_ag1 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, g, alias_ag_private, CltvExpiryDelta(7), 0 msat, 10 msat, 10, htlcMaximum, enable = false) sender.send(router, LocalChannelUpdate(sender.ref, channelId_ag_private, scids_ag_private, g, None, channelUpdate_ag1, CommitmentsSpec.makeCommitments(10000 msat, 15000 msat, a, g, announceChannel = false))) @@ -475,9 +475,9 @@ class RouterSpec extends BaseRouterSpec { val response = sender.expectMsgType[RouteResponse] // the route hasn't changed (nodes are the same) - assert(response.routes.head.hops.map(_.nodeId) == preComputedRoute.nodes.dropRight(1)) - assert(response.routes.head.hops.map(_.nextNodeId) == preComputedRoute.nodes.drop(1)) - assert(response.routes.head.hops.map(_.params) == Seq(ChannelRelayParams.FromAnnouncement(update_ab), ChannelRelayParams.FromAnnouncement(update_bc), ChannelRelayParams.FromAnnouncement(update_cd))) + assert(response.routes.head.clearHops.map(_.nodeId) == preComputedRoute.nodes.dropRight(1)) + assert(response.routes.head.clearHops.map(_.nextNodeId) == preComputedRoute.nodes.drop(1)) + assert(response.routes.head.clearHops.map(_.params) == Seq(ChannelRelayParams.FromAnnouncement(update_ab), ChannelRelayParams.FromAnnouncement(update_bc), ChannelRelayParams.FromAnnouncement(update_cd))) } test("given a pre-defined channels route add the proper channel updates") { fixture => @@ -489,9 +489,9 @@ class RouterSpec extends BaseRouterSpec { val response = sender.expectMsgType[RouteResponse] // the route hasn't changed (nodes are the same) - assert(response.routes.head.hops.map(_.nodeId) == Seq(a, b, c)) - assert(response.routes.head.hops.map(_.nextNodeId) == Seq(b, c, d)) - assert(response.routes.head.hops.map(_.params) == Seq(ChannelRelayParams.FromAnnouncement(update_ab), ChannelRelayParams.FromAnnouncement(update_bc), ChannelRelayParams.FromAnnouncement(update_cd))) + assert(response.routes.head.clearHops.map(_.nodeId) == Seq(a, b, c)) + assert(response.routes.head.clearHops.map(_.nextNodeId) == Seq(b, c, d)) + assert(response.routes.head.clearHops.map(_.params) == Seq(ChannelRelayParams.FromAnnouncement(update_ab), ChannelRelayParams.FromAnnouncement(update_bc), ChannelRelayParams.FromAnnouncement(update_cd))) } test("given a pre-defined private channels route add the proper channel updates") { fixture => @@ -505,9 +505,9 @@ class RouterSpec extends BaseRouterSpec { val response = sender.expectMsgType[RouteResponse] assert(response.routes.length == 1) val route = response.routes.head - assert(route.hops.map(_.params) == Seq(ChannelRelayParams.FromAnnouncement(update_ag_private))) - assert(route.hops.head.nodeId == a) - assert(route.hops.head.nextNodeId == g) + assert(route.clearHops.map(_.params) == Seq(ChannelRelayParams.FromAnnouncement(update_ag_private))) + assert(route.clearHops.head.nodeId == a) + assert(route.clearHops.head.nextNodeId == g) } { // using the real scid @@ -516,9 +516,9 @@ class RouterSpec extends BaseRouterSpec { val response = sender.expectMsgType[RouteResponse] assert(response.routes.length == 1) val route = response.routes.head - assert(route.hops.map(_.params) == Seq(ChannelRelayParams.FromAnnouncement(update_ag_private))) - assert(route.hops.head.nodeId == a) - assert(route.hops.head.nextNodeId == g) + assert(route.clearHops.map(_.params) == Seq(ChannelRelayParams.FromAnnouncement(update_ag_private))) + assert(route.clearHops.head.nodeId == a) + assert(route.clearHops.head.nextNodeId == g) } { val preComputedRoute = PredefinedChannelRoute(h, Seq(scid_ag_private, scid_gh)) @@ -526,9 +526,9 @@ class RouterSpec extends BaseRouterSpec { val response = sender.expectMsgType[RouteResponse] assert(response.routes.length == 1) val route = response.routes.head - assert(route.hops.map(_.nodeId) == Seq(a, g)) - assert(route.hops.map(_.nextNodeId) == Seq(g, h)) - assert(route.hops.map(_.params) == Seq(ChannelRelayParams.FromAnnouncement(update_ag_private), ChannelRelayParams.FromAnnouncement(update_gh))) + assert(route.clearHops.map(_.nodeId) == Seq(a, g)) + assert(route.clearHops.map(_.nextNodeId) == Seq(g, h)) + assert(route.clearHops.map(_.params) == Seq(ChannelRelayParams.FromAnnouncement(update_ag_private), ChannelRelayParams.FromAnnouncement(update_gh))) } } @@ -547,10 +547,10 @@ class RouterSpec extends BaseRouterSpec { val response = sender.expectMsgType[RouteResponse] assert(response.routes.length == 1) val route = response.routes.head - assert(route.hops.map(_.nodeId) == Seq(a, b)) - assert(route.hops.map(_.nextNodeId) == Seq(b, targetNodeId)) - assert(route.hops.head.params == ChannelRelayParams.FromAnnouncement(update_ab)) - assert(route.hops.last.params == ChannelRelayParams.FromHint(invoiceRoutingHint)) + assert(route.clearHops.map(_.nodeId) == Seq(a, b)) + assert(route.clearHops.map(_.nextNodeId) == Seq(b, targetNodeId)) + assert(route.clearHops.head.params == ChannelRelayParams.FromAnnouncement(update_ab)) + assert(route.clearHops.last.params == ChannelRelayParams.FromHint(invoiceRoutingHint)) } { val invoiceRoutingHint = Invoice.BasicEdge(h, targetNodeId, RealShortChannelId(BlockHeight(420000), 516, 1105), 10 msat, 150, CltvExpiryDelta(96)) @@ -562,10 +562,10 @@ class RouterSpec extends BaseRouterSpec { val response = sender.expectMsgType[RouteResponse] assert(response.routes.length == 1) val route = response.routes.head - assert(route.hops.map(_.nodeId) == Seq(a, g, h)) - assert(route.hops.map(_.nextNodeId) == Seq(g, h, targetNodeId)) - assert(route.hops.map(_.params).dropRight(1) == Seq(ChannelRelayParams.FromAnnouncement(update_ag_private), ChannelRelayParams.FromAnnouncement(update_gh))) - assert(route.hops.last.params == ChannelRelayParams.FromHint(invoiceRoutingHint)) + assert(route.clearHops.map(_.nodeId) == Seq(a, g, h)) + assert(route.clearHops.map(_.nextNodeId) == Seq(g, h, targetNodeId)) + assert(route.clearHops.map(_.params).dropRight(1) == Seq(ChannelRelayParams.FromAnnouncement(update_ag_private), ChannelRelayParams.FromAnnouncement(update_gh))) + assert(route.clearHops.last.params == ChannelRelayParams.FromHint(invoiceRoutingHint)) } } diff --git a/eclair-node/src/test/resources/api/findroute-full b/eclair-node/src/test/resources/api/findroute-full index ae20c38987..c2bfff5ffc 100644 --- a/eclair-node/src/test/resources/api/findroute-full +++ b/eclair-node/src/test/resources/api/findroute-full @@ -1 +1 @@ -{"routes":[{"amount":456,"hops":[{"nodeId":"03007e67dc5a8fd2b2ef21cb310ab6359ddb51f3f86a8b79b8b1e23bc3a6ea150a","nextNodeId":"026105f6cb4862810be989385d16f04b0f748f6f2a14040338b1a534d45b4be1c1","source":{"type":"announcement","channelUpdate":{"signature":"92cf3f12e161391986eb2cd7106ddab41a23c734f8f1ed120fb64f4b91f98f690ecf930388e62965f8aefbf1adafcd25a572669a125396dcfb83615208754679","chainHash":"024b7b3626554c44dcc2454ee3812458bfa68d9fced466edfab470844cb7ffe2","shortChannelId":"1x2x3","timestamp":{"iso":"1970-01-01T00:00:00Z","unix":0},"channelFlags":{"isEnabled":true,"isNode1":true},"cltvExpiryDelta":0,"htlcMinimumMsat":1,"feeBaseMsat":1,"feeProportionalMillionths":1,"htlcMaximumMsat":20000000,"tlvStream":{"records":[],"unknown":[]}}}},{"nodeId":"026105f6cb4862810be989385d16f04b0f748f6f2a14040338b1a534d45b4be1c1","nextNodeId":"038cfa2b5857843ee90cff91b06f692c0d8fe201921ee6387aee901d64f43699f0","source":{"type":"announcement","channelUpdate":{"signature":"92cf3f12e161391986eb2cd7106ddab41a23c734f8f1ed120fb64f4b91f98f690ecf930388e62965f8aefbf1adafcd25a572669a125396dcfb83615208754679","chainHash":"024b7b3626554c44dcc2454ee3812458bfa68d9fced466edfab470844cb7ffe2","shortChannelId":"1x2x4","timestamp":{"iso":"1970-01-01T00:00:00Z","unix":0},"channelFlags":{"isEnabled":true,"isNode1":true},"cltvExpiryDelta":0,"htlcMinimumMsat":1,"feeBaseMsat":1,"feeProportionalMillionths":1,"htlcMaximumMsat":20000000,"tlvStream":{"records":[],"unknown":[]}}}},{"nodeId":"038cfa2b5857843ee90cff91b06f692c0d8fe201921ee6387aee901d64f43699f0","nextNodeId":"02be60276e294c6921240daae33a361d214d02578656df0e74c61a09c3196e51df","source":{"type":"announcement","channelUpdate":{"signature":"92cf3f12e161391986eb2cd7106ddab41a23c734f8f1ed120fb64f4b91f98f690ecf930388e62965f8aefbf1adafcd25a572669a125396dcfb83615208754679","chainHash":"024b7b3626554c44dcc2454ee3812458bfa68d9fced466edfab470844cb7ffe2","shortChannelId":"1x2x5","timestamp":{"iso":"1970-01-01T00:00:00Z","unix":0},"channelFlags":{"isEnabled":true,"isNode1":true},"cltvExpiryDelta":0,"htlcMinimumMsat":1,"feeBaseMsat":1,"feeProportionalMillionths":1,"htlcMaximumMsat":20000000,"tlvStream":{"records":[],"unknown":[]}}}}]}]} \ No newline at end of file +{"routes":[{"amount":456,"clearHops":[{"nodeId":"03007e67dc5a8fd2b2ef21cb310ab6359ddb51f3f86a8b79b8b1e23bc3a6ea150a","nextNodeId":"026105f6cb4862810be989385d16f04b0f748f6f2a14040338b1a534d45b4be1c1","source":{"type":"announcement","channelUpdate":{"signature":"92cf3f12e161391986eb2cd7106ddab41a23c734f8f1ed120fb64f4b91f98f690ecf930388e62965f8aefbf1adafcd25a572669a125396dcfb83615208754679","chainHash":"024b7b3626554c44dcc2454ee3812458bfa68d9fced466edfab470844cb7ffe2","shortChannelId":"1x2x3","timestamp":{"iso":"1970-01-01T00:00:00Z","unix":0},"channelFlags":{"isEnabled":true,"isNode1":true},"cltvExpiryDelta":0,"htlcMinimumMsat":1,"feeBaseMsat":1,"feeProportionalMillionths":1,"htlcMaximumMsat":20000000,"tlvStream":{"records":[],"unknown":[]}}}},{"nodeId":"026105f6cb4862810be989385d16f04b0f748f6f2a14040338b1a534d45b4be1c1","nextNodeId":"038cfa2b5857843ee90cff91b06f692c0d8fe201921ee6387aee901d64f43699f0","source":{"type":"announcement","channelUpdate":{"signature":"92cf3f12e161391986eb2cd7106ddab41a23c734f8f1ed120fb64f4b91f98f690ecf930388e62965f8aefbf1adafcd25a572669a125396dcfb83615208754679","chainHash":"024b7b3626554c44dcc2454ee3812458bfa68d9fced466edfab470844cb7ffe2","shortChannelId":"1x2x4","timestamp":{"iso":"1970-01-01T00:00:00Z","unix":0},"channelFlags":{"isEnabled":true,"isNode1":true},"cltvExpiryDelta":0,"htlcMinimumMsat":1,"feeBaseMsat":1,"feeProportionalMillionths":1,"htlcMaximumMsat":20000000,"tlvStream":{"records":[],"unknown":[]}}}},{"nodeId":"038cfa2b5857843ee90cff91b06f692c0d8fe201921ee6387aee901d64f43699f0","nextNodeId":"02be60276e294c6921240daae33a361d214d02578656df0e74c61a09c3196e51df","source":{"type":"announcement","channelUpdate":{"signature":"92cf3f12e161391986eb2cd7106ddab41a23c734f8f1ed120fb64f4b91f98f690ecf930388e62965f8aefbf1adafcd25a572669a125396dcfb83615208754679","chainHash":"024b7b3626554c44dcc2454ee3812458bfa68d9fced466edfab470844cb7ffe2","shortChannelId":"1x2x5","timestamp":{"iso":"1970-01-01T00:00:00Z","unix":0},"channelFlags":{"isEnabled":true,"isNode1":true},"cltvExpiryDelta":0,"htlcMinimumMsat":1,"feeBaseMsat":1,"feeProportionalMillionths":1,"htlcMaximumMsat":20000000,"tlvStream":{"records":[],"unknown":[]}}}}]}]} \ No newline at end of file diff --git a/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala b/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala index 2b1d433199..354ebd63bf 100644 --- a/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala +++ b/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala @@ -1021,7 +1021,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM val eclair = mock[Eclair] val mockService = new MockService(eclair) - eclair.findRoute(any, any, any, any, any, any, any, any)(any[Timeout]) returns Future.successful(Router.RouteResponse(Seq(Router.Route(456.msat, mockHops)))) + eclair.findRoute(any, any, any, any, any, any, any, any)(any[Timeout]) returns Future.successful(Router.RouteResponse(Seq(Router.Route(456.msat, mockHops, None)))) // invalid format Post("/findroute", FormData("format" -> "invalid-output-format", "invoice" -> serializedInvoice, "amountMsat" -> "456")) ~> From b84f08929c2dbc0dcfd6a3fd5a2491068a44079b Mon Sep 17 00:00:00 2001 From: Thomas HUET Date: Tue, 20 Sep 2022 14:13:52 +0200 Subject: [PATCH 2/8] Use recipients instead of node ids --- .../main/scala/fr/acinq/eclair/Eclair.scala | 10 +- .../acinq/eclair/json/JsonSerializers.scala | 6 +- .../acinq/eclair/payment/Bolt11Invoice.scala | 6 +- .../acinq/eclair/payment/Bolt12Invoice.scala | 14 +- .../fr/acinq/eclair/payment/Invoice.scala | 4 +- .../acinq/eclair/payment/PaymentPacket.scala | 109 +++++------- .../fr/acinq/eclair/payment/Recipient.scala | 116 ++++++++++++ .../payment/receive/MultiPartHandler.scala | 4 +- .../eclair/payment/relay/NodeRelay.scala | 9 +- .../send/MultiPartPaymentLifecycle.scala | 23 +-- .../eclair/payment/send/PaymentError.scala | 8 +- .../payment/send/PaymentInitiator.scala | 167 +++++++++++------- .../payment/send/PaymentLifecycle.scala | 71 +++----- .../scala/fr/acinq/eclair/router/Graph.scala | 50 ++++-- .../eclair/router/RouteCalculation.scala | 82 +++++---- .../scala/fr/acinq/eclair/router/Router.scala | 22 ++- .../eclair/wire/protocol/PaymentOnion.scala | 6 +- .../integration/PaymentIntegrationSpec.scala | 16 +- 18 files changed, 449 insertions(+), 274 deletions(-) create mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/payment/Recipient.scala diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala index ffafb40226..293913af24 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -315,7 +315,7 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { for { ignoredChannels <- getChannelDescs(ignoreShortChannelIds.toSet) ignore = Ignore(ignoreNodeIds.toSet, ignoredChannels) - response <- (appKit.router ? RouteRequest(sourceNodeId, targetNodeId, amount, maxFee, extraEdges, ignore = ignore, routeParams = routeParams.copy(includeLocalChannelCost = includeLocalChannelCost))).mapTo[RouteResponse] + response <- (appKit.router ? RouteRequest(sourceNodeId, Seq(ClearRecipient(targetNodeId, randomBytes32(), None)), amount, maxFee, extraEdges, ignore = ignore, routeParams = routeParams.copy(includeLocalChannelCost = includeLocalChannelCost))).mapTo[RouteResponse] } yield response case Left(t) => Future.failed(t) } @@ -323,7 +323,12 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { override def sendToRoute(amount: MilliSatoshi, recipientAmount_opt: Option[MilliSatoshi], externalId_opt: Option[String], parentId_opt: Option[UUID], invoice: Bolt11Invoice, route: PredefinedRoute, trampolineSecret_opt: Option[ByteVector32], trampolineFees_opt: Option[MilliSatoshi], trampolineExpiryDelta_opt: Option[CltvExpiryDelta], trampolineNodes_opt: Seq[PublicKey])(implicit timeout: Timeout): Future[SendPaymentToRouteResponse] = { val recipientAmount = recipientAmount_opt.getOrElse(invoice.amount_opt.getOrElse(amount)) - val sendPayment = SendPaymentToRoute(amount, recipientAmount, invoice, route, externalId_opt, parentId_opt, trampolineSecret_opt, trampolineFees_opt.getOrElse(0 msat), trampolineExpiryDelta_opt.getOrElse(CltvExpiryDelta(0)), trampolineNodes_opt) + val sendPayment = + if(trampolineNodes_opt.nonEmpty){ + SendTrampolinePaymentToRoute(amount, recipientAmount, invoice, route, externalId_opt, parentId_opt, trampolineSecret_opt, trampolineFees_opt.getOrElse(0 msat), trampolineExpiryDelta_opt.getOrElse(CltvExpiryDelta(0)), trampolineNodes_opt) + }else{ + SendPaymentToRoute(amount, recipientAmount, invoice, route, externalId_opt, parentId_opt) + } if (invoice.isExpired()) { Future.failed(new IllegalArgumentException("invoice has expired")) } else if (route.isEmpty) { @@ -405,6 +410,7 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { case PendingPaymentToNode(_, r) => OutgoingPayment(paymentId, paymentId, r.externalId, paymentHash, paymentType, r.recipientAmount, r.recipientAmount, r.recipientNodeId, TimestampMilli.now(), Some(r.invoice), OutgoingPaymentStatus.Pending) case PendingPaymentToRoute(_, r) => OutgoingPayment(paymentId, paymentId, r.externalId, paymentHash, paymentType, r.recipientAmount, r.recipientAmount, r.recipientNodeId, TimestampMilli.now(), Some(r.invoice), OutgoingPaymentStatus.Pending) case PendingTrampolinePayment(_, _, r) => OutgoingPayment(paymentId, paymentId, None, paymentHash, paymentType, r.recipientAmount, r.recipientAmount, r.recipientNodeId, TimestampMilli.now(), Some(r.invoice), OutgoingPaymentStatus.Pending) + case PendingTrampolinePaymentToRoute(_, r) => OutgoingPayment(paymentId, paymentId, r.externalId, paymentHash, paymentType, r.recipientAmount, r.recipientAmount, r.recipientNodeId, TimestampMilli.now(), Some(r.invoice), OutgoingPaymentStatus.Pending) } dummyOutgoingPayment +: outgoingDbPayments } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala index f173ea3015..27b47cbba0 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala @@ -295,8 +295,8 @@ object ColorSerializer extends MinimalSerializer({ // @formatter:off private case class ChannelHopJson(nodeId: PublicKey, nextNodeId: PublicKey, source: ChannelRelayParams) -private case class RouteFullJson(amount: MilliSatoshi, clearHops: Seq[ChannelHopJson], blindedEnd: Option[BlindedPaymentRoute]) -object RouteFullSerializer extends ConvertClassSerializer[Route](route => RouteFullJson(route.amount, route.clearHops.map(h => ChannelHopJson(h.nodeId, h.nextNodeId, h.params)), route.blinded_opt)) +private case class RouteFullJson(amount: MilliSatoshi, clearHops: Seq[ChannelHopJson], recipient: Recipient) +object RouteFullSerializer extends ConvertClassSerializer[Route](route => RouteFullJson(route.amount, route.clearHops.map(h => ChannelHopJson(h.nodeId, h.nextNodeId, h.params)), route.recipient)) private case class RouteNodeIdsJson(amount: MilliSatoshi, nodeIds: Seq[PublicKey]) object RouteNodeIdsSerializer extends ConvertClassSerializer[Route](route => { @@ -404,7 +404,7 @@ object InvoiceSerializer extends MinimalSerializer({ FeatureSupportSerializer + UnknownFeatureSerializer )), - JField("blindedPaths", JArray(p.blindedPaymentRoutes.map(paymentRoute => { + JField("blindedPaths", JArray(p.recipients.map(paymentRoute => { JObject(List( JField("introductionNodeId", JString(paymentRoute.route.introductionNodeId.toString())), JField("blindedNodeIds", JArray(paymentRoute.route.blindedNodes.map(n => JString(n.blindedPublicKey.toString())).toList)) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt11Invoice.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt11Invoice.scala index 39dd731d37..fd291af443 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt11Invoice.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt11Invoice.scala @@ -45,6 +45,7 @@ case class Bolt11Invoice(prefix: String, amount_opt: Option[MilliSatoshi], creat amount_opt.foreach(a => require(a > 0.msat, s"amount is not valid")) require(tags.collect { case _: Bolt11Invoice.PaymentHash => }.size == 1, "there must be exactly one payment hash tag") + require(tags.collect { case _: Bolt11Invoice.PaymentSecret => }.size == 1, "there must be exactly one payment secret tag") require(tags.collect { case Bolt11Invoice.Description(_) | Bolt11Invoice.DescriptionHash(_) => }.size == 1, "there must be exactly one description tag or one description hash tag") { @@ -63,7 +64,7 @@ case class Bolt11Invoice(prefix: String, amount_opt: Option[MilliSatoshi], creat /** * @return the payment secret */ - lazy val paymentSecret = tags.collectFirst { case p: Bolt11Invoice.PaymentSecret => p.secret } + lazy val paymentSecret = tags.collectFirst { case p: Bolt11Invoice.PaymentSecret => p.secret }.get /** * @return the description of the payment, or its hash @@ -78,6 +79,9 @@ case class Bolt11Invoice(prefix: String, amount_opt: Option[MilliSatoshi], creat */ lazy val paymentMetadata: Option[ByteVector] = tags.collectFirst { case m: Bolt11Invoice.PaymentMetadata => m.data } + val recipient: ClearRecipient = ClearRecipient(nodeId, paymentSecret, paymentMetadata, features) + override val recipients: Seq[ClearRecipient] = Seq(recipient) + /** * @return the fallback address if any. It could be a script address, pubkey address, .. */ diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt12Invoice.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt12Invoice.scala index d361d7e1ec..9759a45cd8 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt12Invoice.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt12Invoice.scala @@ -20,7 +20,6 @@ import fr.acinq.bitcoin.Bech32 import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, ByteVector64, Crypto} import fr.acinq.eclair.crypto.Sphinx -import fr.acinq.eclair.crypto.Sphinx.RouteBlinding import fr.acinq.eclair.wire.protocol.OfferTypes._ import fr.acinq.eclair.wire.protocol.OnionRoutingCodecs.{InvalidTlvPayload, MissingRequiredTlv} import fr.acinq.eclair.wire.protocol.{OfferCodecs, OfferTypes, TlvStream} @@ -31,8 +30,6 @@ import java.util.concurrent.TimeUnit import scala.concurrent.duration.FiniteDuration import scala.util.{Failure, Try} -case class BlindedPaymentRoute(route: RouteBlinding.BlindedRoute, paymentInfo: PaymentInfo, capacity_opt: Option[MilliSatoshi]) - /** * Lightning Bolt 12 invoice * see https://github.com/lightning/bolts/blob/master/12-offer-encoding.md @@ -43,26 +40,25 @@ case class Bolt12Invoice(records: TlvStream[InvoiceTlv]) extends Invoice { val amount: MilliSatoshi = records.get[Amount].map(_.amount).get override val amount_opt: Option[MilliSatoshi] = Some(amount) - override val nodeId: Crypto.PublicKey = records.get[NodeId].get.publicKey + val nodeId: Crypto.PublicKey = records.get[NodeId].get.publicKey override val paymentHash: ByteVector32 = records.get[PaymentHash].get.hash - override val paymentSecret: Option[ByteVector32] = None override val paymentMetadata: Option[ByteVector] = None override val description: Either[String, ByteVector32] = Left(records.get[Description].get.description) - override val extraEdges: Seq[Invoice.ExtraEdge] = Seq.empty // TODO: the blinded paths need to be converted to graph edges + override val extraEdges: Seq[Invoice.ExtraEdge] = Seq.empty override val createdAt: TimestampSecond = records.get[CreatedAt].get.timestamp override val relativeExpiry: FiniteDuration = FiniteDuration(records.get[RelativeExpiry].map(_.seconds).getOrElse(DEFAULT_EXPIRY_SECONDS), TimeUnit.SECONDS) override val minFinalCltvExpiryDelta: CltvExpiryDelta = records.get[Cltv].map(_.minFinalCltvExpiry).getOrElse(DEFAULT_MIN_FINAL_EXPIRY_DELTA) override val features: Features[InvoiceFeature] = records.get[FeaturesTlv].map(_.features.invoiceFeatures()).getOrElse(Features.empty) val chain: ByteVector32 = records.get[Chain].map(_.hash).getOrElse(Block.LivenetGenesisBlock.hash) val offerId: Option[ByteVector32] = records.get[OfferId].map(_.offerId) - val blindedPaymentRoutes: Seq[BlindedPaymentRoute] = { + val recipients: Seq[BlindRecipient] = { val routesAndInfos = records.get[Paths].get.paths.zip(records.get[PaymentPathsInfo].get.paymentInfo) records.get[PaymentPathsCapacities] match { case Some(PaymentPathsCapacities(capacities)) => routesAndInfos.zip(capacities).map { - case ((route, payInfo), capacity) => BlindedPaymentRoute(route, payInfo, Some(capacity)) + case ((route, payInfo), capacity) => BlindRecipient(route, payInfo, Some(capacity), Nil, Nil) } case None => routesAndInfos.map { - case (route, payInfo) => BlindedPaymentRoute(route, payInfo, None) + case (route, payInfo) => BlindRecipient(route, payInfo, None, Nil, Nil) } } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Invoice.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Invoice.scala index b6f5bb78bb..db04c93e6f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Invoice.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Invoice.scala @@ -31,12 +31,10 @@ trait Invoice { val createdAt: TimestampSecond - val nodeId: PublicKey + val recipients: Seq[Recipient] val paymentHash: ByteVector32 - val paymentSecret: Option[ByteVector32] - val paymentMetadata: Option[ByteVector] val description: Either[String, ByteVector32] diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala index eed14f2bc8..335ed1b620 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala @@ -22,7 +22,7 @@ import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} import fr.acinq.eclair.channel.{CMD_ADD_HTLC, CMD_FAIL_HTLC, CannotExtractSharedSecret, Origin} import fr.acinq.eclair.crypto.Sphinx -import fr.acinq.eclair.router.Router.{ChannelHop, Hop, NodeHop} +import fr.acinq.eclair.router.Router.{ChannelHop, Hop, NodeHop, Route} import fr.acinq.eclair.wire.protocol.OnionPaymentPayloadTlv.EncryptedRecipientData import fr.acinq.eclair.wire.protocol.PaymentOnion.{FinalPayload, IntermediatePayload, PerHopPayload} import fr.acinq.eclair.wire.protocol.RouteBlindingEncryptedDataCodecs.RouteBlindingDecryptedData @@ -237,40 +237,25 @@ object OutgoingPaymentPacket { /** * Build the onion payloads for each hop. * - * @param clearHops the hops as computed by the router + extra routes from the invoice - * @param blindedEnd_opt blinded part of the route + * @param clearHops the hops as computed by the router + extra routes from the invoice + * @param recipient payment recipient + * @param amount amount to send to this route + * @param totalAmount total amount of the invoice + * @param expiry expiry for this route + * @param skipIntroduction if we are the introduction point of the blinded route, we should ignore the first blinded hop * @return a (firstAmount, firstExpiry, payloads) tuple where: * - firstAmount is the amount for the first htlc in the route * - firstExpiry is the cltv expiry for the first htlc in the route * - a sequence of payloads that will be used to build the onion */ def buildPayloads(clearHops: Seq[Hop], - blindedEnd_opt: Option[BlindedPaymentRoute], + recipient: Recipient, amount: MilliSatoshi, totalAmount: MilliSatoshi, expiry: CltvExpiry, - paymentSecret: ByteVector32, - paymentMetadata: Option[ByteVector], - additionalTlvs: Seq[OnionPaymentPayloadTlv], - userCustomTlvs: Seq[GenericTlv], skipIntroduction: Boolean): (MilliSatoshi, CltvExpiry, Seq[PerHopPayload]) = { - val (endAmount, endExpiry, endPayloads): (MilliSatoshi, CltvExpiry, Seq[PaymentOnion.PerHopPayload]) = blindedEnd_opt match { - case Some(blinded) => - val blindedPayloads = if (blinded.route.encryptedPayloads.length > 1) { - val middlePayloads = blinded.route.encryptedPayloads.drop(1).dropRight(1).map(IntermediatePayload.ChannelRelay.Blinded.create(_, None)) - val finalPayload = FinalPayload.Blinded.create(amount, expiry, blinded.route.encryptedPayloads.last, None, additionalTlvs, userCustomTlvs) - if (skipIntroduction) { - middlePayloads :+ finalPayload - } else { - val introductionPayload = IntermediatePayload.ChannelRelay.Blinded.create(blinded.route.encryptedPayloads.head, Some(blinded.route.blindingKey)) - introductionPayload +: middlePayloads :+ finalPayload - } - } else { - Seq(FinalPayload.Blinded.create(amount, expiry, blinded.route.encryptedPayloads.last, Some(blinded.route.blindingKey), additionalTlvs, userCustomTlvs)) - } - (amount + blinded.paymentInfo.fee(amount), expiry + blinded.paymentInfo.cltvExpiryDelta, blindedPayloads) - case None => (amount, expiry, Seq(FinalPayload.Standard.createMultiPartPayload(amount, totalAmount, expiry, paymentSecret, paymentMetadata, additionalTlvs, userCustomTlvs))) - } + val (endAmount, endExpiry, finalPayloads) = recipient.buildFinalPayloads(amount, totalAmount, expiry) + val endPayloads = if (skipIntroduction) finalPayloads.drop(1) else finalPayloads clearHops.reverse.foldLeft((endAmount, endExpiry, endPayloads)) { case ((amount, expiry, payloads), hop) => val payload = hop match { @@ -284,8 +269,11 @@ object OutgoingPaymentPacket { /** * Build an encrypted onion packet with the given final payload. * - * @param clearHops the hops as computed by the router + extra routes from the invoice, including ourselves in the first hop - * @param blindedEnd_opt blinded part of the route + * @param clearHops the hops as computed by the router + extra routes from the invoice, including ourselves in the first hop + * @param recipient payment recipient + * @param amount amount to send to this route + * @param totalAmount total amount of the invoice + * @param expiry expiry for this route * @return a (firstAmount, firstExpiry, onion) tuple where: * - firstAmount is the amount for the first htlc in the route * - firstExpiry is the cltv expiry for the first htlc in the route @@ -294,19 +282,15 @@ object OutgoingPaymentPacket { private def buildPacket(packetPayloadLength: Int, paymentHash: ByteVector32, clearHops: Seq[Hop], - blindedEnd_opt: Option[BlindedPaymentRoute], + recipient: Recipient, amount: MilliSatoshi, totalAmount: MilliSatoshi, - expiry: CltvExpiry, - paymentSecret: ByteVector32, - paymentMetadata: Option[ByteVector], - additionalTlvs: Seq[OnionPaymentPayloadTlv], - userCustomTlvs: Seq[GenericTlv]): Try[(MilliSatoshi, CltvExpiry, Sphinx.PacketAndSecrets)] = { - val (firstAmount, firstExpiry, payloads) = buildPayloads(clearHops.drop(1), blindedEnd_opt, amount, totalAmount, expiry, paymentSecret, paymentMetadata, additionalTlvs, userCustomTlvs, clearHops.isEmpty) + expiry: CltvExpiry): Try[(MilliSatoshi, CltvExpiry, Sphinx.PacketAndSecrets)] = { + val (firstAmount, firstExpiry, payloads) = buildPayloads(clearHops.drop(1), recipient, amount, totalAmount, expiry, clearHops.isEmpty) val clearNodes = clearHops.map(_.nextNodeId) - val nodes = blindedEnd_opt match { - case Some(blinded) => clearNodes ++ blinded.route.blindedNodeIds.drop(1) - case None => clearNodes + val nodes = recipient match { + case blinded: BlindRecipient => clearNodes ++ blinded.route.blindedNodeIds.drop(1) + case _: ClearRecipient => clearNodes } // BOLT 2 requires that associatedData == paymentHash buildOnion(packetPayloadLength, nodes, payloads, paymentHash).map(onion => (firstAmount, firstExpiry, onion)) @@ -314,26 +298,19 @@ object OutgoingPaymentPacket { def buildPaymentPacket(paymentHash: ByteVector32, clearHops: Seq[Hop], - blindedEnd_opt: Option[BlindedPaymentRoute], + recipient: Recipient, amount: MilliSatoshi, totalAmount: MilliSatoshi, - expiry: CltvExpiry, - paymentSecret: ByteVector32, - paymentMetadata: Option[ByteVector], - additionalTlvs: Seq[OnionPaymentPayloadTlv], - userCustomTlvs: Seq[GenericTlv]): Try[(MilliSatoshi, CltvExpiry, Sphinx.PacketAndSecrets)] = - buildPacket(PaymentOnionCodecs.paymentOnionPayloadLength, paymentHash, clearHops, blindedEnd_opt, amount, totalAmount, expiry, paymentSecret, paymentMetadata, additionalTlvs, userCustomTlvs) + expiry: CltvExpiry): Try[(MilliSatoshi, CltvExpiry, Sphinx.PacketAndSecrets)] = + buildPacket(PaymentOnionCodecs.paymentOnionPayloadLength, paymentHash, clearHops, recipient, amount, totalAmount, expiry) def buildTrampolinePacket(paymentHash: ByteVector32, hops: Seq[Hop], + recipient: Recipient, amount: MilliSatoshi, totalAmount: MilliSatoshi, - expiry: CltvExpiry, - paymentSecret: ByteVector32, - paymentMetadata: Option[ByteVector], - additionalTlvs: Seq[OnionPaymentPayloadTlv], - userCustomTlvs: Seq[GenericTlv]): Try[(MilliSatoshi, CltvExpiry, Sphinx.PacketAndSecrets)] = - buildPacket(PaymentOnionCodecs.trampolineOnionPayloadLength, paymentHash, hops, None, amount, totalAmount, expiry, paymentSecret, paymentMetadata, additionalTlvs, userCustomTlvs) + expiry: CltvExpiry): Try[(MilliSatoshi, CltvExpiry, Sphinx.PacketAndSecrets)] = + buildPacket(PaymentOnionCodecs.trampolineOnionPayloadLength, paymentHash, hops, recipient, amount, totalAmount, expiry) /** * Build an encrypted trampoline onion packet when the final recipient doesn't support trampoline. @@ -341,6 +318,8 @@ object OutgoingPaymentPacket { * * @param invoice Bolt 11 invoice (features and routing hints will be provided to the next-to-last node). * @param hops the trampoline hops (including ourselves in the first hop, and the non-trampoline final recipient in the last hop). + * @param amount amount to send to this route + * @param expiry expiry for this route * @return a (firstAmount, firstExpiry, onion) tuple where: * - firstAmount is the amount for the trampoline node in the route * - firstExpiry is the cltv expiry for the first trampoline node in the route @@ -378,44 +357,40 @@ object OutgoingPaymentPacket { /** * Build the command to add an HTLC with the given final payload and using the provided hops. * - * @return the command and the onion shared secrets (used to decrypt the error in case of payment failure) + * @return the command, the onion shared secrets (used to decrypt the error in case of payment failure) and the + * channel id to send the HTLC to */ def buildCommand(privateKey: PrivateKey, replyTo: ActorRef, upstream: Upstream, paymentHash: ByteVector32, - clearHops: Seq[ChannelHop], - blindedEnd_opt: Option[BlindedPaymentRoute], + route: Route, amount: MilliSatoshi, totalAmount: MilliSatoshi, - expiry: CltvExpiry, - paymentSecret: ByteVector32, - paymentMetadata: Option[ByteVector], - additionalTlvs: Seq[OnionPaymentPayloadTlv], - userCustomTlvs: Seq[GenericTlv]): Try[(CMD_ADD_HTLC, Seq[(ByteVector32, PublicKey)], ShortChannelId)] = { - val (shortChannelId, nextBlindingKey_opt, blindedRoute_opt) = if (clearHops.nonEmpty) { - (clearHops.head.shortChannelId, None, blindedEnd_opt) + expiry: CltvExpiry): Try[(CMD_ADD_HTLC, Seq[(ByteVector32, PublicKey)], ShortChannelId)] = { + val (shortChannelId, nextBlindingKey_opt, recipient) = if (route.clearHops.nonEmpty) { + (route.clearHops.head.shortChannelId, None, route.recipient) } else { - blindedEnd_opt match { - case Some(paymentRoute) if paymentRoute.route.introductionNodeId == privateKey.publicKey => + route.recipient match { + case recipient: BlindRecipient if recipient.route.introductionNodeId == privateKey.publicKey => // We assume that there is a next node that is not us, that should be checked before calling the router. - RouteBlindingEncryptedDataCodecs.decode(privateKey, paymentRoute.route.blindingKey, paymentRoute.route.encryptedPayloads.head) match { + RouteBlindingEncryptedDataCodecs.decode(privateKey, recipient.route.blindingKey, recipient.route.encryptedPayloads.head) match { case Left(e) => return Failure(e) case Right(RouteBlindingDecryptedData(encryptedDataTlvs, nextBlindingKey)) => IntermediatePayload.ChannelRelay.Blinded.validate(TlvStream(EncryptedRecipientData(ByteVector.empty)), encryptedDataTlvs, nextBlindingKey) match { case Left(invalidTlv) => return Failure(RouteBlindingEncryptedDataCodecs.CannotDecodeData(invalidTlv.failureMessage.message)) case Right(payload) => // We assume that fees were checked in the router. - val amountWithFees = amount + paymentRoute.paymentInfo.fee(amount) + val amountWithFees = recipient.amountToSend(amount) val remainingFee = amountWithFees - payload.amountToForward(amountWithFees) - val tailPaymentInfo = paymentRoute.paymentInfo.copy(feeBase = remainingFee, feeProportionalMillionths = 0, cltvExpiryDelta = paymentRoute.paymentInfo.cltvExpiryDelta - payload.cltvExpiryDelta) - (payload.outgoingChannelId, Some(nextBlindingKey), Some(paymentRoute.copy(paymentInfo = tailPaymentInfo))) + val tailPaymentInfo = recipient.paymentInfo.copy(feeBase = remainingFee, feeProportionalMillionths = 0, cltvExpiryDelta = recipient.paymentInfo.cltvExpiryDelta - payload.cltvExpiryDelta) + (payload.outgoingChannelId, Some(nextBlindingKey), recipient.copy(paymentInfo = tailPaymentInfo)) } } case _ => return Failure(new Exception("Invalid payment route")) } } - buildPaymentPacket(paymentHash, clearHops, blindedRoute_opt, amount, totalAmount, expiry, paymentSecret, paymentMetadata, additionalTlvs, userCustomTlvs).map { + buildPaymentPacket(paymentHash, route.clearHops, recipient, amount, totalAmount, expiry).map { case (firstAmount, firstExpiry, onion) => (CMD_ADD_HTLC(replyTo, firstAmount, paymentHash, firstExpiry, onion.packet, nextBlindingKey_opt, Origin.Hot(replyTo, upstream), commit = true), onion.sharedSecrets, shortChannelId) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Recipient.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Recipient.scala new file mode 100644 index 0000000000..52f83e313a --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Recipient.scala @@ -0,0 +1,116 @@ +/* + * Copyright 2022 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fr.acinq.eclair.payment + +import fr.acinq.bitcoin.scalacompat.ByteVector32 +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.eclair.crypto.Sphinx.RouteBlinding +import fr.acinq.eclair.wire.protocol.OfferTypes.PaymentInfo +import fr.acinq.eclair.wire.protocol.PaymentOnion.{FinalPayload, IntermediatePayload, PerHopPayload} +import fr.acinq.eclair.wire.protocol.{GenericTlv, OnionPaymentPayloadTlv, OnionRoutingPacket} +import fr.acinq.eclair.{CltvExpiry, Features, InvoiceFeature, MilliSatoshi, randomBytes32} +import scodec.bits.ByteVector + +sealed trait Recipient { + def nodeId: PublicKey + def introductionNodeId: PublicKey + + def features: Features[InvoiceFeature] + + def amountToSend(amount: MilliSatoshi): MilliSatoshi + + def additionalTlvs: Seq[OnionPaymentPayloadTlv] + + def userCustomTlvs: Seq[GenericTlv] + + def withCustomTlvs(customTlvs: Seq[GenericTlv]): Recipient + + def contains(id: PublicKey): Boolean + + def buildFinalPayloads(amount: MilliSatoshi, + totalAmount: MilliSatoshi, + expiry: CltvExpiry): (MilliSatoshi, CltvExpiry, Seq[PerHopPayload]) +} + +case class ClearRecipient(nodeId: PublicKey, + paymentSecret: ByteVector32, + paymentMetadata_opt: Option[ByteVector], + features: Features[InvoiceFeature] = Features.empty, + additionalTlvs: Seq[OnionPaymentPayloadTlv] = Nil, + userCustomTlvs: Seq[GenericTlv] = Nil) extends Recipient { + override def introductionNodeId: PublicKey = nodeId + + override def amountToSend(amount: MilliSatoshi): MilliSatoshi = amount + + override def withCustomTlvs(customTlvs: Seq[GenericTlv]): Recipient = copy(userCustomTlvs = customTlvs) + + override def contains(id: PublicKey): Boolean = id == nodeId + + override def buildFinalPayloads(amount: MilliSatoshi, + totalAmount: MilliSatoshi, + expiry: CltvExpiry): (MilliSatoshi, CltvExpiry, Seq[PerHopPayload]) = + (amount, expiry, Seq(FinalPayload.Standard.createMultiPartPayload(amount, totalAmount, expiry, paymentSecret, paymentMetadata_opt, additionalTlvs, userCustomTlvs))) +} + +object ClearRecipient { + def fromTrampolinePayload(payload : IntermediatePayload.NodeRelay.Standard): ClearRecipient = + ClearRecipient(payload.outgoingNodeId, payload.paymentSecret.get, payload.paymentMetadata, payload.invoiceFeatures.map(Features(_).invoiceFeatures()).getOrElse(Features.empty)) +} + +object KeySendRecipient { + def apply(nodeId: PublicKey, paymentPreimage: ByteVector32, userCustomTlvs: Seq[GenericTlv]): ClearRecipient = + ClearRecipient(nodeId, randomBytes32(), None, additionalTlvs = Seq(OnionPaymentPayloadTlv.KeySend(paymentPreimage)), userCustomTlvs = userCustomTlvs) +} + +object TrampolineRecipient { + def apply(trampolineNodeId: PublicKey, trampolineOnion: OnionRoutingPacket, paymentMetadata_opt: Option[ByteVector]): ClearRecipient = + ClearRecipient(trampolineNodeId, randomBytes32(), paymentMetadata_opt, additionalTlvs = Seq(OnionPaymentPayloadTlv.TrampolineOnion(trampolineOnion))) +} + +case class BlindRecipient(route: RouteBlinding.BlindedRoute, + paymentInfo: PaymentInfo, + capacity_opt: Option[MilliSatoshi], + additionalTlvs: Seq[OnionPaymentPayloadTlv] = Nil, + userCustomTlvs: Seq[GenericTlv] = Nil) extends Recipient { + override def nodeId: PublicKey = route.blindedNodeIds.last + + override def introductionNodeId: PublicKey = route.introductionNodeId + + override def features: Features[InvoiceFeature] = paymentInfo.allowedFeatures.invoiceFeatures() + + override def amountToSend(amount: MilliSatoshi): MilliSatoshi = amount + paymentInfo.fee(amount) + + override def withCustomTlvs(customTlvs: Seq[GenericTlv]): Recipient = copy(userCustomTlvs = customTlvs) + + override def contains(id: PublicKey): Boolean = + id == route.introductionNodeId || route.blindedNodeIds.contains(id) + + override def buildFinalPayloads(amount: MilliSatoshi, + totalAmount: MilliSatoshi, + expiry: CltvExpiry): (MilliSatoshi, CltvExpiry, Seq[PerHopPayload]) = { + val blindedPayloads = if (route.encryptedPayloads.length > 1) { + val middlePayloads = route.encryptedPayloads.drop(1).dropRight(1).map(IntermediatePayload.ChannelRelay.Blinded.create(_, None)) + val finalPayload = FinalPayload.Blinded.create(amount, totalAmount, expiry, route.encryptedPayloads.last, None, additionalTlvs, userCustomTlvs) + val introductionPayload = IntermediatePayload.ChannelRelay.Blinded.create(route.encryptedPayloads.head, Some(route.blindingKey)) + introductionPayload +: middlePayloads :+ finalPayload + } else { + Seq(FinalPayload.Blinded.create(amount, totalAmount, expiry, route.encryptedPayloads.last, Some(route.blindingKey), additionalTlvs, userCustomTlvs)) + } + (amount + paymentInfo.fee(amount), expiry + paymentInfo.cltvExpiryDelta, blindedPayloads) + + } +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala index 3548649da6..93824d9cf2 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala @@ -410,10 +410,10 @@ object MultiPartHandler { } private def validatePaymentSecret(add: UpdateAddHtlc, payload: FinalPayload.Standard, invoice: Bolt11Invoice)(implicit log: LoggingAdapter): Boolean = { - if (payload.amount < payload.totalAmount && !invoice.paymentSecret.contains(payload.paymentSecret)) { + if (payload.amount < payload.totalAmount && invoice.paymentSecret != payload.paymentSecret) { log.warning("received multi-part payment with invalid secret={} for amount={} totalAmount={}", payload.paymentSecret, add.amountMsat, payload.totalAmount) false - } else if (!invoice.paymentSecret.contains(payload.paymentSecret)) { + } else if (invoice.paymentSecret != payload.paymentSecret) { log.warning("received payment with invalid secret={} for amount={} totalAmount={}", payload.paymentSecret, add.amountMsat, payload.totalAmount) false } else { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelay.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelay.scala index 8362540770..186dd39aad 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelay.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelay.scala @@ -302,7 +302,7 @@ class NodeRelay private(nodeParams: NodeParams, }.toClassic private def relay(upstream: Upstream.Trampoline, payloadOut: IntermediatePayload.NodeRelay.Standard, packetOut: OnionRoutingPacket): ActorRef = { - val paymentCfg = SendPaymentConfig(relayId, relayId, None, paymentHash, payloadOut.amountToForward, payloadOut.outgoingNodeId, upstream, None, storeInDb = false, publishEvent = false, recordPathFindingMetrics = true, Nil) + val paymentCfg = SendPaymentConfig(relayId, relayId, None, paymentHash, payloadOut.amountToForward, Seq(ClearRecipient.fromTrampolinePayload(payloadOut)), upstream, None, storeInDb = false, publishEvent = false, recordPathFindingMetrics = true, Nil) val routeParams = computeRouteParams(nodeParams, upstream.amountIn, upstream.expiryIn, payloadOut.amountToForward, payloadOut.outgoingCltv) // If invoice features are provided in the onion, the sender is asking us to relay to a non-trampoline recipient. val payFSM = payloadOut.invoiceFeatures match { @@ -311,13 +311,13 @@ class NodeRelay private(nodeParams: NodeParams, val paymentSecret = payloadOut.paymentSecret.get // NB: we've verified that there was a payment secret in validateRelay if (Features(features).hasFeature(Features.BasicMultiPartPayment)) { context.log.debug("sending the payment to non-trampoline recipient using MPP") - val payment = SendMultiPartPayment(payFsmAdapters, paymentSecret, payloadOut.outgoingNodeId, payloadOut.amountToForward, payloadOut.outgoingCltv, nodeParams.maxPaymentAttempts, payloadOut.paymentMetadata, extraEdges, routeParams) + val payment = SendMultiPartPayment(payFsmAdapters, Seq(ClearRecipient(payloadOut.outgoingNodeId, paymentSecret, payloadOut.paymentMetadata)), payloadOut.amountToForward, payloadOut.outgoingCltv, nodeParams.maxPaymentAttempts, extraEdges, routeParams) val payFSM = outgoingPaymentFactory.spawnOutgoingPayFSM(context, paymentCfg, multiPart = true) payFSM ! payment payFSM } else { context.log.debug("sending the payment to non-trampoline recipient without MPP") - val payment = SendPaymentToNode(payFsmAdapters, payloadOut.outgoingNodeId, payloadOut.amountToForward, payloadOut.amountToForward, payloadOut.outgoingCltv, paymentSecret, payloadOut.paymentMetadata, nodeParams.maxPaymentAttempts, extraEdges, routeParams) + val payment = SendPaymentToNode(payFsmAdapters, Seq(ClearRecipient(payloadOut.outgoingNodeId, paymentSecret, payloadOut.paymentMetadata)), payloadOut.amountToForward, payloadOut.amountToForward, payloadOut.outgoingCltv, nodeParams.maxPaymentAttempts, extraEdges, routeParams) val payFSM = outgoingPaymentFactory.spawnOutgoingPayFSM(context, paymentCfg, multiPart = false) payFSM ! payment payFSM @@ -325,8 +325,7 @@ class NodeRelay private(nodeParams: NodeParams, case None => context.log.debug("sending the payment to the next trampoline node") val payFSM = outgoingPaymentFactory.spawnOutgoingPayFSM(context, paymentCfg, multiPart = true) - val paymentSecret = randomBytes32() // we generate a new secret to protect against probing attacks - val payment = SendMultiPartPayment(payFsmAdapters, paymentSecret, payloadOut.outgoingNodeId, payloadOut.amountToForward, payloadOut.outgoingCltv, nodeParams.maxPaymentAttempts, None, routeParams = routeParams, additionalTlvs = Seq(OnionPaymentPayloadTlv.TrampolineOnion(packetOut))) + val payment = SendMultiPartPayment(payFsmAdapters, Seq(TrampolineRecipient(payloadOut.outgoingNodeId, packetOut, None)), payloadOut.amountToForward, payloadOut.outgoingCltv, nodeParams.maxPaymentAttempts, routeParams = routeParams) payFSM ! payment payFSM } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala index 09ea7bb296..e465cac334 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala @@ -239,7 +239,7 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, val status = event match { case Right(_: PaymentSent) => "SUCCESS" case Left(f: PaymentFailed) => - if (f.failures.exists({ case r: RemoteFailure => r.e.originNode == cfg.recipientNodeId case _ => false })) { + if (f.failures.exists({ case r: RemoteFailure => cfg.recipients.exists(_.contains(r.e.originNode)) case _ => false })) { "RECIPIENT_FAILURE" } else { "FAILURE" @@ -303,29 +303,20 @@ object MultiPartPaymentLifecycle { * Send a payment to a given node. The payment may be split into multiple child payments, for which a path-finding * algorithm will run to find suitable payment routes. * - * @param paymentSecret payment secret to protect against probing (usually from a Bolt 11 invoice). - * @param targetNodeId target node (may be the final recipient when using source-routing, or the first trampoline - * node when using trampoline). + * @param recipients list of recipients to send the payment to. * @param totalAmount total amount to send to the target node. * @param targetExpiry expiry at the target node (CLTV for the target node's received HTLCs). * @param maxAttempts maximum number of retries. - * @param paymentMetadata payment metadata (usually from the Bolt 11 invoice). * @param extraEdges routing hints (usually from a Bolt 11 invoice). * @param routeParams parameters to fine-tune the routing algorithm. - * @param additionalTlvs when provided, additional tlvs that will be added to the onion sent to the target node. - * @param userCustomTlvs when provided, additional user-defined custom tlvs that will be added to the onion sent to the target node. */ case class SendMultiPartPayment(replyTo: ActorRef, - paymentSecret: ByteVector32, - targetNodeId: PublicKey, + recipients: Seq[Recipient], totalAmount: MilliSatoshi, targetExpiry: CltvExpiry, maxAttempts: Int, - paymentMetadata: Option[ByteVector], extraEdges: Seq[ExtraEdge] = Nil, - routeParams: RouteParams, - additionalTlvs: Seq[OnionPaymentPayloadTlv] = Nil, - userCustomTlvs: Seq[GenericTlv] = Nil) { + routeParams: RouteParams) { require(totalAmount > 0.msat, s"total amount must be > 0") } @@ -393,7 +384,7 @@ object MultiPartPaymentLifecycle { private def createRouteRequest(nodeParams: NodeParams, toSend: MilliSatoshi, maxFee: MilliSatoshi, routeParams: RouteParams, d: PaymentProgress, cfg: SendPaymentConfig): RouteRequest = RouteRequest( nodeParams.nodeId, - d.request.targetNodeId, + d.request.recipients, toSend, maxFee, d.request.extraEdges, @@ -404,12 +395,12 @@ object MultiPartPaymentLifecycle { Some(cfg.paymentContext)) private def createChildPayment(replyTo: ActorRef, route: Route, request: SendMultiPartPayment): SendPaymentToRoute = { - SendPaymentToRoute(replyTo, Right(route), route.amount, request.totalAmount, request.targetExpiry, request.paymentSecret, request.paymentMetadata, additionalTlvs = request.additionalTlvs, userCustomTlvs = request.userCustomTlvs) + SendPaymentToRoute(replyTo, Right(route), route.recipient, route.amount, request.totalAmount, request.targetExpiry) } /** When we receive an error from the final recipient or payment gets settled on chain, we should fail the whole payment, it's useless to retry. */ private def abortPayment(pf: PaymentFailed, d: PaymentProgress): Boolean = pf.failures.exists { - case f: RemoteFailure => f.e.originNode == d.request.targetNodeId + case f: RemoteFailure => d.request.recipients.exists(_.contains(f.e.originNode)) case LocalFailure(_, _, _: HtlcOverriddenByLocalCommit) => true case LocalFailure(_, _, _: HtlcsWillTimeoutUpstream) => true case LocalFailure(_, _, _: HtlcsTimedoutDownstream) => true diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentError.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentError.scala index 7248019cd3..b9f743bd7d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentError.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentError.scala @@ -16,6 +16,8 @@ package fr.acinq.eclair.payment.send +import fr.acinq.eclair.payment.Recipient +import fr.acinq.eclair.router.Router.PredefinedRoute import fr.acinq.eclair.{Features, InvoiceFeature} sealed trait PaymentError extends Throwable @@ -26,8 +28,10 @@ object PaymentError { sealed trait InvalidInvoice extends PaymentError /** The invoice contains a feature we don't support. */ case class UnsupportedFeatures(features: Features[InvoiceFeature]) extends InvalidInvoice { override def getMessage: String = s"unsupported invoice features: ${features.toByteVector.toHex}" } - /** The invoice is missing a payment secret. */ - case object PaymentSecretMissing extends InvalidInvoice { override def getMessage: String = "invalid invoice: payment secret is missing" } + /** The invoice recipient does not match the provided route. */ + case class InvalidRecipientForRoute(route: PredefinedRoute, recipients: Seq[Recipient]) extends InvalidInvoice { + override def getMessage: String = s"cannot use route for payment: route ends at ${route.targetNodeId} but payment must reach ${recipients.map(_.introductionNodeId).mkString(",")}" + } // @formatter:on // @formatter:off diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala index 793582bd9c..4f5ab8ccdf 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala @@ -27,7 +27,6 @@ import fr.acinq.eclair.payment._ import fr.acinq.eclair.payment.send.PaymentError._ import fr.acinq.eclair.router.RouteNotFound import fr.acinq.eclair.router.Router._ -import fr.acinq.eclair.wire.protocol.PaymentOnion.FinalPayload import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, Features, MilliSatoshi, MilliSatoshiLong, NodeParams, randomBytes32} @@ -50,30 +49,29 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn // Immediately return the paymentId sender() ! paymentId } - val paymentCfg = SendPaymentConfig(paymentId, paymentId, r.externalId, r.paymentHash, r.recipientAmount, r.recipientNodeId, Upstream.Local(paymentId), Some(r.invoice), storeInDb = true, publishEvent = true, recordPathFindingMetrics = true, Nil) + val recipients = r.recipients.map(_.withCustomTlvs(r.userCustomTlvs)) + val paymentCfg = SendPaymentConfig(paymentId, paymentId, r.externalId, r.paymentHash, r.recipientAmount, recipients, Upstream.Local(paymentId), Some(r.invoice), storeInDb = true, publishEvent = true, recordPathFindingMetrics = true, Nil) val finalExpiry = r.finalExpiry(nodeParams.currentBlockHeight) - r.invoice.paymentSecret match { - case _ if !nodeParams.features.invoiceFeatures().areSupported(r.invoice.features) => - sender() ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(r.recipientAmount, Nil, UnsupportedFeatures(r.invoice.features)) :: Nil) - case None => - sender() ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(r.recipientAmount, Nil, PaymentSecretMissing) :: Nil) - case Some(paymentSecret) if r.invoice.features.hasFeature(Features.BasicMultiPartPayment) && nodeParams.features.hasFeature(BasicMultiPartPayment) => - val fsm = outgoingPaymentFactory.spawnOutgoingMultiPartPayment(context, paymentCfg) - fsm ! MultiPartPaymentLifecycle.SendMultiPartPayment(self, paymentSecret, r.recipientNodeId, r.recipientAmount, finalExpiry, r.maxAttempts, r.invoice.paymentMetadata, r.invoice.extraEdges, r.routeParams, userCustomTlvs = r.userCustomTlvs) - context become main(pending + (paymentId -> PendingPaymentToNode(sender(), r))) - case Some(paymentSecret) => - val fsm = outgoingPaymentFactory.spawnOutgoingPayment(context, paymentCfg) - fsm ! PaymentLifecycle.SendPaymentToNode(self, r.recipientNodeId, r.recipientAmount, r.recipientAmount, finalExpiry, paymentSecret, r.invoice.paymentMetadata, r.maxAttempts, r.invoice.extraEdges, r.routeParams, userCustomTlvs = r.userCustomTlvs) - context become main(pending + (paymentId -> PendingPaymentToNode(sender(), r))) + if (!nodeParams.features.invoiceFeatures().areSupported(r.invoice.features)) { + sender() ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(r.recipientAmount, Nil, UnsupportedFeatures(r.invoice.features)) :: Nil) + } else if (r.invoice.features.hasFeature(Features.BasicMultiPartPayment) && nodeParams.features.hasFeature(BasicMultiPartPayment)) { + val fsm = outgoingPaymentFactory.spawnOutgoingMultiPartPayment(context, paymentCfg) + fsm ! MultiPartPaymentLifecycle.SendMultiPartPayment(self, recipients, r.recipientAmount, finalExpiry, r.maxAttempts, r.invoice.extraEdges, r.routeParams) + context become main(pending + (paymentId -> PendingPaymentToNode(sender(), r))) + } else { + val fsm = outgoingPaymentFactory.spawnOutgoingPayment(context, paymentCfg) + fsm ! PaymentLifecycle.SendPaymentToNode(self, recipients, r.recipientAmount, r.recipientAmount, finalExpiry, r.maxAttempts, r.invoice.extraEdges, r.routeParams) + context become main(pending + (paymentId -> PendingPaymentToNode(sender(), r))) } case r: SendSpontaneousPayment => val paymentId = UUID.randomUUID() sender() ! paymentId - val paymentCfg = SendPaymentConfig(paymentId, paymentId, r.externalId, r.paymentHash, r.recipientAmount, r.recipientNodeId, Upstream.Local(paymentId), None, storeInDb = true, publishEvent = true, recordPathFindingMetrics = r.recordPathFindingMetrics, Nil) + val recipients = Seq(KeySendRecipient(r.recipientNodeId, r.paymentPreimage, r.userCustomTlvs)) + val paymentCfg = SendPaymentConfig(paymentId, paymentId, r.externalId, r.paymentHash, r.recipientAmount, recipients, Upstream.Local(paymentId), None, storeInDb = true, publishEvent = true, recordPathFindingMetrics = r.recordPathFindingMetrics, Nil) val finalExpiry = Channel.MIN_CLTV_EXPIRY_DELTA.toCltvExpiry(nodeParams.currentBlockHeight + 1) val fsm = outgoingPaymentFactory.spawnOutgoingPayment(context, paymentCfg) - fsm ! PaymentLifecycle.SendPaymentToNode(self, r.recipientNodeId, r.recipientAmount, r.recipientAmount, finalExpiry, randomBytes32(), None, r.maxAttempts, routeParams = r.routeParams, additionalTlvs = Seq(OnionPaymentPayloadTlv.KeySend(r.paymentPreimage))) + fsm ! PaymentLifecycle.SendPaymentToNode(self, recipients, r.recipientAmount, r.recipientAmount, finalExpiry, r.maxAttempts, routeParams = r.routeParams) context become main(pending + (paymentId -> PendingSpontaneousPayment(sender(), r))) case r: SendTrampolinePayment => @@ -95,15 +93,12 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn } } - case r: SendPaymentToRoute => + case r: SendTrampolinePaymentToRoute => val paymentId = UUID.randomUUID() val parentPaymentId = r.parentId.getOrElse(UUID.randomUUID()) - val finalExpiry = r.finalExpiry(nodeParams.currentBlockHeight) val additionalHops = r.trampolineNodes.sliding(2).map(hop => NodeHop(hop.head, hop(1), CltvExpiryDelta(0), 0 msat)).toSeq - val paymentCfg = SendPaymentConfig(paymentId, parentPaymentId, r.externalId, r.paymentHash, r.recipientAmount, r.recipientNodeId, Upstream.Local(paymentId), Some(r.invoice), storeInDb = true, publishEvent = true, recordPathFindingMetrics = false, additionalHops) + val paymentCfg = SendPaymentConfig(paymentId, parentPaymentId, r.externalId, r.paymentHash, r.recipientAmount, r.recipients, Upstream.Local(paymentId), Some(r.invoice), storeInDb = true, publishEvent = true, recordPathFindingMetrics = false, additionalHops) r.trampolineNodes match { - case _ if r.invoice.paymentSecret.isEmpty => - sender() ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(r.recipientAmount, Nil, PaymentSecretMissing) :: Nil) case trampoline :: recipient :: Nil => log.info(s"sending trampoline payment to $recipient with trampoline=$trampoline, trampoline fees=${r.trampolineFees}, expiry delta=${r.trampolineExpiryDelta}") buildTrampolinePayment(r, trampoline, r.trampolineFees, r.trampolineExpiryDelta) match { @@ -112,26 +107,37 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn val trampolineSecret = r.trampolineSecret.getOrElse(randomBytes32()) sender() ! SendPaymentToRouteResponse(paymentId, parentPaymentId, Some(trampolineSecret)) val payFsm = outgoingPaymentFactory.spawnOutgoingPayment(context, paymentCfg) - payFsm ! PaymentLifecycle.SendPaymentToRoute(self, Left(r.route), r.amount, trampolineAmount, trampolineExpiry, trampolineSecret, r.invoice.paymentMetadata, r.invoice.extraEdges, additionalTlvs = Seq(OnionPaymentPayloadTlv.TrampolineOnion(trampolineOnion))) - context become main(pending + (paymentId -> PendingPaymentToRoute(sender(), r))) + payFsm ! PaymentLifecycle.SendPaymentToRoute(self, Left(r.route), TrampolineRecipient(trampoline, trampolineOnion, r.invoice.paymentMetadata), r.amount, trampolineAmount, trampolineExpiry, r.invoice.extraEdges) + context become main(pending + (paymentId -> PendingTrampolinePaymentToRoute(sender(), r))) case Failure(t) => log.warning("cannot send outgoing trampoline payment: {}", t.getMessage) sender() ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(r.recipientAmount, Nil, t) :: Nil) } - case Nil => - sender() ! SendPaymentToRouteResponse(paymentId, parentPaymentId, None) - val payFsm = outgoingPaymentFactory.spawnOutgoingPayment(context, paymentCfg) - payFsm ! PaymentLifecycle.SendPaymentToRoute(self, Left(r.route), r.amount, r.recipientAmount, finalExpiry, r.invoice.paymentSecret.get, r.invoice.paymentMetadata, r.invoice.extraEdges) - context become main(pending + (paymentId -> PendingPaymentToRoute(sender(), r))) case _ => sender() ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(r.recipientAmount, Nil, TrampolineMultiNodeNotSupported) :: Nil) } + case r: SendPaymentToRoute => + val paymentId = UUID.randomUUID() + val parentPaymentId = r.parentId.getOrElse(UUID.randomUUID()) + val finalExpiry = r.finalExpiry(nodeParams.currentBlockHeight) + val paymentCfg = SendPaymentConfig(paymentId, parentPaymentId, r.externalId, r.paymentHash, r.recipientAmount, r.recipients, Upstream.Local(paymentId), Some(r.invoice), storeInDb = true, publishEvent = true, recordPathFindingMetrics = false, Nil) + sender() ! SendPaymentToRouteResponse(paymentId, parentPaymentId, None) + val payFsm = outgoingPaymentFactory.spawnOutgoingPayment(context, paymentCfg) + r.recipients.find(_.introductionNodeId == r.route.targetNodeId) match { + case Some(recipient) => + payFsm ! PaymentLifecycle.SendPaymentToRoute(self, Left(r.route), recipient, r.amount, r.recipientAmount, finalExpiry, r.invoice.extraEdges) + context become main(pending + (paymentId -> PendingPaymentToRoute(sender(), r))) + case None => + log.warning("the provided route does not reach the correct recipient") + sender() ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(r.recipientAmount, Nil, InvalidRecipientForRoute(r.route, r.recipients)) :: Nil) + } + case pf: PaymentFailed => pending.get(pf.id).foreach { case pp: PendingTrampolinePayment => val trampolineRoute = Seq( NodeHop(nodeParams.nodeId, pp.r.trampolineNodeId, nodeParams.channelConf.expiryDelta, 0 msat), - NodeHop(pp.r.trampolineNodeId, pp.r.recipientNodeId, pp.r.trampolineAttempts.last._2, pp.r.trampolineAttempts.last._1) + NodeHop(pp.r.trampolineNodeId, pp.r.invoice.nodeId, pp.r.trampolineAttempts.last._2, pp.r.trampolineAttempts.last._1) ) val decryptedFailures = pf.failures.collect { case RemoteFailure(_, _, Sphinx.DecryptedFailurePacket(_, f)) => f } val shouldRetry = decryptedFailures.contains(TrampolineFeeInsufficient) || decryptedFailures.contains(TrampolineExpiryTooSoon) @@ -190,32 +196,32 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn } private def buildTrampolinePayment(r: SendRequestedPayment, trampolineNodeId: PublicKey, trampolineFees: MilliSatoshi, trampolineExpiryDelta: CltvExpiryDelta): Try[(MilliSatoshi, CltvExpiry, OnionRoutingPacket)] = { - val trampolineRoute = Seq( - NodeHop(nodeParams.nodeId, trampolineNodeId, nodeParams.channelConf.expiryDelta, 0 msat), - NodeHop(trampolineNodeId, r.recipientNodeId, trampolineExpiryDelta, trampolineFees) // for now we only use a single trampoline hop - ) - // We assume that the trampoline node supports multi-part payments (it should). - val trampolinePacket_opt = if (r.invoice.features.hasFeature(Features.TrampolinePaymentPrototype)) { - OutgoingPaymentPacket.buildTrampolinePacket(r.paymentHash, trampolineRoute, r.recipientAmount, r.recipientAmount, r.finalExpiry(nodeParams.currentBlockHeight), r.invoice.paymentSecret.get, r.invoice.paymentMetadata, Nil, Nil) - } else { - r.invoice match { - case invoice: Bolt11Invoice => OutgoingPaymentPacket.buildTrampolineToLegacyPacket(invoice, trampolineRoute, r.recipientAmount, r.finalExpiry(nodeParams.currentBlockHeight)) - case _ => Failure(new Exception("Trampoline to legacy is only supported for Bolt11 invoices.")) - } - } - trampolinePacket_opt.map { - case (trampolineAmount, trampolineExpiry, trampolineOnion) => (trampolineAmount, trampolineExpiry, trampolineOnion.packet) + r.invoice match { + case invoice: Bolt11Invoice => + val trampolineRoute = Seq( + NodeHop(nodeParams.nodeId, trampolineNodeId, nodeParams.channelConf.expiryDelta, 0 msat), + NodeHop(trampolineNodeId, invoice.nodeId, trampolineExpiryDelta, trampolineFees) // for now we only use a single trampoline hop + ) + // We assume that the trampoline node supports multi-part payments (it should). + val trampolinePacket_opt = if (r.invoice.features.hasFeature(Features.TrampolinePaymentPrototype)) { + OutgoingPaymentPacket.buildTrampolinePacket(r.paymentHash, trampolineRoute, invoice.recipient, r.recipientAmount, r.recipientAmount, r.finalExpiry(nodeParams.currentBlockHeight)) + } else { + OutgoingPaymentPacket.buildTrampolineToLegacyPacket(invoice, trampolineRoute, r.recipientAmount, r.finalExpiry(nodeParams.currentBlockHeight)) + } + trampolinePacket_opt.map { + case (trampolineAmount, trampolineExpiry, trampolineOnion) => (trampolineAmount, trampolineExpiry, trampolineOnion.packet) + } + case _ => Failure(new Exception("Trampoline to legacy is only supported for Bolt11 invoices.")) } } private def sendTrampolinePayment(paymentId: UUID, r: SendTrampolinePayment, trampolineFees: MilliSatoshi, trampolineExpiryDelta: CltvExpiryDelta): Try[Unit] = { - val paymentCfg = SendPaymentConfig(paymentId, paymentId, None, r.paymentHash, r.recipientAmount, r.recipientNodeId, Upstream.Local(paymentId), Some(r.invoice), storeInDb = true, publishEvent = false, recordPathFindingMetrics = true, Seq(NodeHop(r.trampolineNodeId, r.recipientNodeId, trampolineExpiryDelta, trampolineFees))) - // We generate a random secret for this payment to avoid leaking the invoice secret to the first trampoline node. - val trampolineSecret = randomBytes32() + val paymentCfg = SendPaymentConfig(paymentId, paymentId, None, r.paymentHash, r.recipientAmount, r.recipients, Upstream.Local(paymentId), Some(r.invoice), storeInDb = true, publishEvent = false, recordPathFindingMetrics = true, Seq(NodeHop(r.trampolineNodeId, r.invoice.nodeId, trampolineExpiryDelta, trampolineFees))) buildTrampolinePayment(r, r.trampolineNodeId, trampolineFees, trampolineExpiryDelta).map { case (trampolineAmount, trampolineExpiry, trampolineOnion) => val fsm = outgoingPaymentFactory.spawnOutgoingMultiPartPayment(context, paymentCfg) - fsm ! MultiPartPaymentLifecycle.SendMultiPartPayment(self, trampolineSecret, r.trampolineNodeId, trampolineAmount, trampolineExpiry, nodeParams.maxPaymentAttempts, r.invoice.paymentMetadata, r.invoice.extraEdges, r.routeParams, Seq(OnionPaymentPayloadTlv.TrampolineOnion(trampolineOnion))) + val trampolineRecipient = TrampolineRecipient(r.trampolineNodeId, trampolineOnion, r.invoice.paymentMetadata) + fsm ! MultiPartPaymentLifecycle.SendMultiPartPayment(self, Seq(trampolineRecipient), trampolineAmount, trampolineExpiry, nodeParams.maxPaymentAttempts, r.invoice.extraEdges, r.routeParams) } } @@ -251,6 +257,7 @@ object PaymentInitiator { case class PendingSpontaneousPayment(sender: ActorRef, request: SendSpontaneousPayment) extends PendingPayment { override def paymentHash: ByteVector32 = request.paymentHash } case class PendingPaymentToNode(sender: ActorRef, request: SendPaymentToNode) extends PendingPayment { override def paymentHash: ByteVector32 = request.paymentHash } case class PendingPaymentToRoute(sender: ActorRef, request: SendPaymentToRoute) extends PendingPayment { override def paymentHash: ByteVector32 = request.paymentHash } + case class PendingTrampolinePaymentToRoute(sender: ActorRef, request: SendTrampolinePaymentToRoute) extends PendingPayment { override def paymentHash: ByteVector32 = request.paymentHash } case class PendingTrampolinePayment(sender: ActorRef, remainingAttempts: Seq[(MilliSatoshi, CltvExpiryDelta)], r: SendTrampolinePayment) extends PendingPayment { override def paymentHash: ByteVector32 = r.paymentHash } // @formatter:on @@ -265,7 +272,8 @@ object PaymentInitiator { // @formatter:off def recipientAmount: MilliSatoshi def invoice: Invoice - def recipientNodeId: PublicKey = invoice.nodeId + def recipients: Seq[Recipient] = invoice.recipients + def recipientNodeId: PublicKey = recipients.head.nodeId def paymentHash: ByteVector32 = invoice.paymentHash // We add one block in order to not have our htlcs fail when a new block has just been found. def finalExpiry(currentBlockHeight: BlockHeight): CltvExpiry = invoice.minFinalCltvExpiryDelta.toCltvExpiry(currentBlockHeight + 1) @@ -287,7 +295,7 @@ object PaymentInitiator { * @param routeParams (optional) parameters to fine-tune the routing algorithm. */ case class SendTrampolinePayment(recipientAmount: MilliSatoshi, - invoice: Invoice, + invoice: Bolt11Invoice, trampolineNodeId: PublicKey, trampolineAttempts: Seq[(MilliSatoshi, CltvExpiryDelta)], routeParams: RouteParams) extends SendRequestedPayment @@ -330,6 +338,37 @@ object PaymentInitiator { val paymentHash = Crypto.sha256(paymentPreimage) } + /** + * The sender can skip the routing algorithm by specifying the route to use. + * When combining with MPP and Trampoline, extra-care must be taken to make sure payments are correctly grouped: only + * amount, route and trampolineNodes should be changing. + * + * Example 1: MPP containing two HTLCs for a 600 msat invoice: + * SendPaymentToRouteRequest(200 msat, 600 msat, None, parentId, invoice, CltvExpiryDelta(9), Seq(alice, bob, dave), None, 0 msat, CltvExpiryDelta(0), Nil) + * SendPaymentToRouteRequest(400 msat, 600 msat, None, parentId, invoice, CltvExpiryDelta(9), Seq(alice, carol, dave), None, 0 msat, CltvExpiryDelta(0), Nil) + * + * Example 2: Trampoline with MPP for a 600 msat invoice and 100 msat trampoline fees: + * SendPaymentToRouteRequest(250 msat, 600 msat, None, parentId, invoice, CltvExpiryDelta(9), Seq(alice, bob, dave), secret, 100 msat, CltvExpiryDelta(144), Seq(dave, peter)) + * SendPaymentToRouteRequest(450 msat, 600 msat, None, parentId, invoice, CltvExpiryDelta(9), Seq(alice, carol, dave), secret, 100 msat, CltvExpiryDelta(144), Seq(dave, peter)) + * + * @param amount amount that should be received by the last node in the route (should take trampoline + * fees into account). + * @param recipientAmount amount that should be received by the final recipient (usually from a Bolt 11 invoice). + * This amount may be split between multiple requests if using MPP. + * @param invoice Bolt 11 invoice. + * @param route route to use to reach either the final recipient or the first trampoline node. + * @param externalId (optional) externally-controlled identifier (to reconcile between application DB and eclair DB). + * @param parentId id of the whole payment. When manually sending a multi-part payment, you need to make + * sure all partial payments use the same parentId. If not provided, a random parentId will + * be generated that can be used for the remaining partial payments. + */ + case class SendPaymentToRoute(amount: MilliSatoshi, + recipientAmount: MilliSatoshi, + invoice: Invoice, + route: PredefinedRoute, + externalId: Option[String], + parentId: Option[UUID]) extends SendRequestedPayment + /** * The sender can skip the routing algorithm by specifying the route to use. * When combining with MPP and Trampoline, extra-care must be taken to make sure payments are correctly grouped: only @@ -363,16 +402,16 @@ object PaymentInitiator { * @param trampolineNodes if trampoline is used, list of trampoline nodes to use (we currently support only a * single trampoline node). */ - case class SendPaymentToRoute(amount: MilliSatoshi, - recipientAmount: MilliSatoshi, - invoice: Invoice, - route: PredefinedRoute, - externalId: Option[String], - parentId: Option[UUID], - trampolineSecret: Option[ByteVector32], - trampolineFees: MilliSatoshi, - trampolineExpiryDelta: CltvExpiryDelta, - trampolineNodes: Seq[PublicKey]) extends SendRequestedPayment + case class SendTrampolinePaymentToRoute(amount: MilliSatoshi, + recipientAmount: MilliSatoshi, + invoice: Bolt11Invoice, + route: PredefinedRoute, + externalId: Option[String], + parentId: Option[UUID], + trampolineSecret: Option[ByteVector32], + trampolineFees: MilliSatoshi, + trampolineExpiryDelta: CltvExpiryDelta, + trampolineNodes: Seq[PublicKey]) extends SendRequestedPayment /** * @param paymentId id of the outgoing payment (mapped to a single outgoing HTLC). @@ -409,18 +448,22 @@ object PaymentInitiator { externalId: Option[String], paymentHash: ByteVector32, recipientAmount: MilliSatoshi, - recipientNodeId: PublicKey, + recipients: Seq[Recipient], upstream: Upstream, invoice: Option[Invoice], storeInDb: Boolean, // e.g. for trampoline we don't want to store in the DB when we're relaying payments publishEvent: Boolean, recordPathFindingMetrics: Boolean, additionalHops: Seq[NodeHop]) { + val recipientNodeId: PublicKey = recipients.head.nodeId + def fullRoute(route: Route): Seq[Hop] = route.clearHops ++ additionalHops def createPaymentSent(preimage: ByteVector32, parts: Seq[PaymentSent.PartialPayment]) = PaymentSent(parentId, paymentHash, preimage, recipientAmount, recipientNodeId, parts) def paymentContext: PaymentContext = PaymentContext(id, parentId, paymentHash) + + def isRecipient(nodeId: PublicKey): Boolean = recipients.exists(_.contains(nodeId)) } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala index d2fef2750f..a27a5a6153 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala @@ -32,10 +32,10 @@ import fr.acinq.eclair.payment.PaymentSent.PartialPayment import fr.acinq.eclair.payment._ import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentConfig import fr.acinq.eclair.payment.send.PaymentLifecycle._ +import fr.acinq.eclair.router.Router.ChannelRelayParams.FromPaymentInfo import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.router._ import fr.acinq.eclair.wire.protocol._ -import scodec.bits.ByteVector import java.util.concurrent.TimeUnit import scala.util.{Failure, Success} @@ -57,19 +57,19 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A case Event(c: SendPaymentToRoute, WaitingForRequest) => log.debug("sending {} to route {}", c.amount, c.printRoute()) c.route.fold( - hops => router ! FinalizeRoute(c.amount, hops, c.extraEdges, paymentContext = Some(cfg.paymentContext)), + hops => router ! FinalizeRoute(c.amount, hops, c.targetRecipient, c.extraEdges, paymentContext = Some(cfg.paymentContext)), route => self ! RouteResponse(route :: Nil) ) if (cfg.storeInDb) { - paymentsDb.addOutgoingPayment(OutgoingPayment(id, cfg.parentId, cfg.externalId, paymentHash, PaymentType.Standard, c.amount, cfg.recipientAmount, cfg.recipientNodeId, TimestampMilli.now(), cfg.invoice, OutgoingPaymentStatus.Pending)) + paymentsDb.addOutgoingPayment(OutgoingPayment(id, cfg.parentId, cfg.externalId, paymentHash, PaymentType.Standard, c.amount, cfg.recipientAmount, cfg.recipients.head.nodeId, TimestampMilli.now(), cfg.invoice, OutgoingPaymentStatus.Pending)) } goto(WAITING_FOR_ROUTE) using WaitingForRoute(c, Nil, Ignore.empty) case Event(c: SendPaymentToNode, WaitingForRequest) => - log.debug("sending {} to {}", c.amount, c.targetNodeId) - router ! RouteRequest(nodeParams.nodeId, c.targetNodeId, c.amount, c.maxFee, c.extraEdges, routeParams = c.routeParams, paymentContext = Some(cfg.paymentContext)) + log.debug("sending {} to {}", c.amount, c.targetRecipients.head.nodeId) + router ! RouteRequest(nodeParams.nodeId, c.targetRecipients, c.amount, c.maxFee, routeParams = c.routeParams, paymentContext = Some(cfg.paymentContext)) if (cfg.storeInDb) { - paymentsDb.addOutgoingPayment(OutgoingPayment(id, cfg.parentId, cfg.externalId, paymentHash, PaymentType.Standard, c.amount, cfg.recipientAmount, cfg.recipientNodeId, TimestampMilli.now(), cfg.invoice, OutgoingPaymentStatus.Pending)) + paymentsDb.addOutgoingPayment(OutgoingPayment(id, cfg.parentId, cfg.externalId, paymentHash, PaymentType.Standard, c.amount, cfg.recipientAmount, cfg.recipients.head.nodeId, TimestampMilli.now(), cfg.invoice, OutgoingPaymentStatus.Pending)) } goto(WAITING_FOR_ROUTE) using WaitingForRoute(c, Nil, Ignore.empty) } @@ -77,7 +77,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A when(WAITING_FOR_ROUTE) { case Event(RouteResponse(route +: _), WaitingForRoute(c, failures, ignore)) => log.info(s"route found: attempt=${failures.size + 1}/${c.maxAttempts} route=${route.printNodes()} channels=${route.printChannels()}") - OutgoingPaymentPacket.buildCommand(nodeParams.privateKey, self, cfg.upstream, paymentHash, route.clearHops, route.blinded_opt, c.amount, c.totalAmount, c.targetExpiry, c.paymentSecret, c.paymentMetadata, c.additionalTlvs, c.userCustomTlvs) match { + OutgoingPaymentPacket.buildCommand(nodeParams.privateKey, self, cfg.upstream, paymentHash, route, c.amount, c.totalAmount, c.targetExpiry) match { case Success((cmd, sharedSecrets, shortChannelId: ShortChannelId)) => register ! Register.ForwardShortId(self.toTyped[Register.ForwardShortIdFailure[CMD_ADD_HTLC]], shortChannelId, cmd) goto(WAITING_FOR_PAYMENT_COMPLETE) using WaitingForComplete(c, cmd, failures, sharedSecrets, ignore, route) @@ -139,7 +139,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A data.c match { case sendPaymentToNode: SendPaymentToNode => val ignore1 = PaymentFailure.updateIgnored(failure, data.ignore) - router ! RouteRequest(nodeParams.nodeId, data.c.targetNodeId, data.c.amount, sendPaymentToNode.maxFee, data.c.extraEdges, ignore1, sendPaymentToNode.routeParams, paymentContext = Some(cfg.paymentContext)) + router ! RouteRequest(nodeParams.nodeId, data.c.targetRecipients, data.c.amount, sendPaymentToNode.maxFee, data.c.extraEdges, ignore1, sendPaymentToNode.routeParams, paymentContext = Some(cfg.paymentContext)) goto(WAITING_FOR_ROUTE) using WaitingForRoute(data.c, data.failures :+ failure, ignore1) case _: SendPaymentToRoute => log.error("unexpected retry during SendPaymentToRoute") @@ -194,7 +194,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A res case res => res }) match { - case Success(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage)) if nodeId == c.targetNodeId => + case Success(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage)) if c.targetRecipients.exists(_.contains(nodeId)) => // if destination node returns an error, we fail the payment immediately log.warning(s"received an error message from target nodeId=$nodeId, failing the payment (failure=$failureMessage)") myStop(c, Left(PaymentFailed(id, paymentHash, failures :+ RemoteFailure(d.c.amount, cfg.fullRoute(route), e)))) @@ -234,7 +234,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A log.error("unexpected retry during SendPaymentToRoute") stop(FSM.Normal) case c: SendPaymentToNode => - router ! RouteRequest(nodeParams.nodeId, c.targetNodeId, c.amount, c.maxFee, extraEdges1, ignore1, c.routeParams, paymentContext = Some(cfg.paymentContext)) + router ! RouteRequest(nodeParams.nodeId, c.targetRecipients, c.amount, c.maxFee, extraEdges1, ignore1, c.routeParams, paymentContext = Some(cfg.paymentContext)) goto(WAITING_FOR_ROUTE) using WaitingForRoute(c, failures :+ failure, ignore1) } } else { @@ -245,7 +245,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A log.error("unexpected retry during SendPaymentToRoute") stop(FSM.Normal) case c: SendPaymentToNode => - router ! RouteRequest(nodeParams.nodeId, c.targetNodeId, c.amount, c.maxFee, c.extraEdges, ignore + nodeId, c.routeParams, paymentContext = Some(cfg.paymentContext)) + router ! RouteRequest(nodeParams.nodeId, c.targetRecipients, c.amount, c.maxFee, c.extraEdges, ignore + nodeId, c.routeParams, paymentContext = Some(cfg.paymentContext)) goto(WAITING_FOR_ROUTE) using WaitingForRoute(c, failures :+ failure, ignore + nodeId) } } @@ -298,6 +298,9 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A // we remove this edge for our next payment attempt data.c.extraEdges.filterNot { case edge: BasicEdge => edge.sourceNodeId == nodeId && edge.targetNodeId == hop.nextNodeId } } + case _: FromPaymentInfo => + log.error(s"received an update from a blinded node=$nodeId, this should never happen") + data.c.extraEdges } case None => log.error(s"couldn't find node=$nodeId in the route, this should never happen") @@ -328,7 +331,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A val status = result match { case Right(_: PaymentSent) => "SUCCESS" case Left(f: PaymentFailed) => - if (f.failures.exists({ case r: RemoteFailure => r.e.originNode == cfg.recipientNodeId case _ => false })) { + if (f.failures.exists({ case r: RemoteFailure => cfg.isRecipient(r.e.originNode) case _ => false })) { "RECIPIENT_FAILURE" } else { "FAILURE" @@ -390,15 +393,11 @@ object PaymentLifecycle { sealed trait SendPayment { // @formatter:off def replyTo: ActorRef - def paymentSecret: ByteVector32 def amount: MilliSatoshi def totalAmount: MilliSatoshi def targetExpiry: CltvExpiry - def paymentMetadata: Option[ByteVector] - def additionalTlvs: Seq[OnionPaymentPayloadTlv] - def userCustomTlvs: Seq[GenericTlv] def extraEdges: Seq[ExtraEdge] - def targetNodeId: PublicKey + def targetRecipients: Seq[Recipient] def maxAttempts: Int // @formatter:on } @@ -407,24 +406,19 @@ object PaymentLifecycle { * Send a payment to a given route. * * @param route payment route to use. - * @param amount amount to send to the target node. - * @param targetExpiry expiry at the target node (CLTV for the target node's received HTLCs). - * @param paymentSecret payment secret to protect against probing (usually from a Bolt 11 invoice). - * @param paymentMetadata payment metadata (usually from the Bolt 11 invoice). + * @param amount amount to send to the target node. + * @param targetExpiry expiry at the target node (CLTV for the target node's received HTLCs). */ case class SendPaymentToRoute(replyTo: ActorRef, route: Either[PredefinedRoute, Route], + targetRecipient: Recipient, amount: MilliSatoshi, totalAmount: MilliSatoshi, targetExpiry: CltvExpiry, - paymentSecret: ByteVector32, - paymentMetadata: Option[ByteVector], - extraEdges: Seq[ExtraEdge] = Nil, - additionalTlvs: Seq[OnionPaymentPayloadTlv] = Nil, - userCustomTlvs: Seq[GenericTlv] = Nil) extends SendPayment { + extraEdges: Seq[ExtraEdge] = Nil) extends SendPayment { require(route.fold(!_.isEmpty, _.clearHops.nonEmpty), "payment route must not be empty") - val targetNodeId: PublicKey = route.fold(_.targetNodeId, _.clearHops.last.nextNodeId) + override def targetRecipients: Seq[Recipient] = Seq(targetRecipient) override def maxAttempts: Int = 1 @@ -438,30 +432,21 @@ object PaymentLifecycle { /** * Send a payment to a given node. A path-finding algorithm will run to find a suitable payment route. * - * @param targetNodeId target node (may be the final recipient when using source-routing, or the first trampoline - * node when using trampoline). - * @param paymentSecret payment secret to protect against probing (usually from a Bolt 11 invoice). - * @param amount amount to send to the target node. - * @param targetExpiry expiry at the target node (CLTV for the target node's received HTLCs). - * @param paymentMetadata payment metadata (usually from the Bolt 11 invoice). - * @param maxAttempts maximum number of retries. + * @param targetRecipients target recipients to send the payment to. + * @param amount amount to send to the target node. + * @param targetExpiry expiry at the target node (CLTV for the target node's received HTLCs). + * @param maxAttempts maximum number of retries. * @param extraEdges routing hints (usually from a Bolt 11 invoice). - * @param routeParams parameters to fine-tune the routing algorithm. - * @param additionalTlvs when provided, additional tlvs that will be added to the onion sent to the target node. - * @param userCustomTlvs when provided, additional user-defined custom tlvs that will be added to the onion sent to the target node. + * @param routeParams parameters to fine-tune the routing algorithm. */ case class SendPaymentToNode(replyTo: ActorRef, - targetNodeId: PublicKey, + targetRecipients: Seq[Recipient], amount: MilliSatoshi, totalAmount: MilliSatoshi, targetExpiry: CltvExpiry, - paymentSecret: ByteVector32, - paymentMetadata: Option[ByteVector], maxAttempts: Int, extraEdges: Seq[ExtraEdge] = Nil, - routeParams: RouteParams, - additionalTlvs: Seq[OnionPaymentPayloadTlv] = Nil, - userCustomTlvs: Seq[GenericTlv] = Nil) extends SendPayment { + routeParams: RouteParams) extends SendPayment { require(amount > 0.msat, s"total amount must be > 0") val maxFee: MilliSatoshi = routeParams.getMaxFee(amount) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala index 554a25dbea..4a3d0b0595 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala @@ -18,11 +18,13 @@ package fr.acinq.eclair.router import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.bitcoin.scalacompat.{Btc, BtcDouble, MilliBtc, Satoshi} +import fr.acinq.eclair.crypto.Sphinx.RouteBlinding import fr.acinq.eclair.payment.relay.Relayer.RelayFees -import fr.acinq.eclair.payment.Invoice +import fr.acinq.eclair.payment.{BlindRecipient, ClearRecipient, Invoice, Recipient} import fr.acinq.eclair.router.Graph.GraphStructure.{DirectedGraph, GraphEdge} import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.wire.protocol.ChannelUpdate +import fr.acinq.eclair.wire.protocol.OfferTypes.PaymentInfo import fr.acinq.eclair.{RealShortChannelId, _} import scala.annotation.tailrec @@ -99,7 +101,7 @@ object Graph { * * @param graph the graph on which will be performed the search * @param sourceNode the starting node of the path we're looking for (payer) - * @param targetNode the destination node of the path (recipient) + * @param targets the recipients * @param amount amount to send to the last node * @param ignoredEdges channels that should be avoided * @param ignoredVertices nodes that should be avoided @@ -112,7 +114,7 @@ object Graph { */ def yenKshortestPaths(graph: DirectedGraph, sourceNode: PublicKey, - targetNode: PublicKey, + targets: Seq[PublicKey], amount: MilliSatoshi, ignoredEdges: Set[ChannelDesc], ignoredVertices: Set[PublicKey], @@ -124,7 +126,7 @@ object Graph { includeLocalChannelCost: Boolean): Seq[WeightedPath] = { // find the shortest path (k = 0) val targetWeight = RichWeight(amount, 0, CltvExpiryDelta(0), 1.0, 0 msat, 0 msat, 0.0) - val shortestPath = dijkstraShortestPath(graph, sourceNode, targetNode, ignoredEdges, ignoredVertices, extraEdges, targetWeight, boundaries, currentBlockHeight, wr, includeLocalChannelCost) + val shortestPath = dijkstraShortestPath(graph, sourceNode, targets, ignoredEdges, ignoredVertices, extraEdges, targetWeight, boundaries, currentBlockHeight, wr, includeLocalChannelCost) if (shortestPath.isEmpty) { return Seq.empty // if we can't even find a single path, avoid returning a Seq(Seq.empty) } @@ -163,7 +165,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 - val spurPath = dijkstraShortestPath(graph, sourceNode, spurNode, ignoredEdges ++ alreadyExploredEdges, ignoredVertices ++ alreadyExploredVertices, extraEdges, rootPathWeight, boundaries, currentBlockHeight, wr, includeLocalChannelCost) + val spurPath = dijkstraShortestPath(graph, sourceNode, Seq(spurNode), ignoredEdges ++ alreadyExploredEdges, ignoredVertices ++ alreadyExploredVertices, extraEdges, rootPathWeight, boundaries, currentBlockHeight, wr, includeLocalChannelCost) if (spurPath.nonEmpty) { val completePath = spurPath ++ rootPathEdges val candidatePath = WeightedPath(completePath, pathWeight(sourceNode, completePath, amount, currentBlockHeight, wr, includeLocalChannelCost)) @@ -191,7 +193,7 @@ object Graph { * * @param g the graph on which will be performed the search * @param sourceNode the starting node of the path we're looking for (payer) - * @param targetNode the destination node of the path + * @param targets the destinations of the path * @param ignoredEdges channels that should be avoided * @param ignoredVertices nodes that should be avoided * @param extraEdges additional edges that can be used (e.g. private channels from invoices) @@ -203,7 +205,7 @@ object Graph { */ private def dijkstraShortestPath(g: DirectedGraph, sourceNode: PublicKey, - targetNode: PublicKey, + targets: Seq[PublicKey], ignoredEdges: Set[ChannelDesc], ignoredVertices: Set[PublicKey], extraEdges: Set[GraphEdge], @@ -214,8 +216,8 @@ object Graph { includeLocalChannelCost: Boolean): 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) - if (sourceNotInGraph || targetNotInGraph) { + val targetsNotInGraph = targets.forall(targetNode => !g.containsVertex(targetNode) && !extraEdges.exists(_.desc.b == targetNode)) + if (sourceNotInGraph || targetsNotInGraph) { return Seq.empty } @@ -228,9 +230,11 @@ object Graph { val toExplore = mutable.PriorityQueue.empty[WeightedNode](NodeComparator.reverse) val visitedNodes = mutable.HashSet[PublicKey]() - // initialize the queue and cost array with the initial weight - bestWeights.put(targetNode, initialWeight) - toExplore.enqueue(WeightedNode(targetNode, initialWeight)) + for (targetNode <- targets) { + // initialize the queue and cost array with the initial weight + bestWeights.put(targetNode, initialWeight) + toExplore.enqueue(WeightedNode(targetNode, initialWeight)) + } var targetFound = false while (toExplore.nonEmpty && !targetFound) { @@ -459,9 +463,10 @@ object Graph { balance_opt = pc.getBalanceSameSideAs(u) ) + private val maxBtc = 21e6.btc + def apply(e: Invoice.ExtraEdge): GraphEdge = e match { case e@Invoice.BasicEdge(sourceNodeId, targetNodeId, shortChannelId, _, _, _) => - val maxBtc = 21e6.btc GraphEdge( desc = ChannelDesc(shortChannelId, sourceNodeId, targetNodeId), params = ChannelRelayParams.FromHint(e), @@ -471,6 +476,15 @@ object Graph { balance_opt = Some(maxBtc.toMilliSatoshi) ) } + + def apply(route: RouteBlinding.BlindedRoute, + paymentInfo: PaymentInfo, + capacity_opt: Option[MilliSatoshi]): GraphEdge = GraphEdge( + desc = ChannelDesc(ShortChannelId.generateLocalAlias(), route.introductionNodeId, route.blindedNodeIds.last), + params = ChannelRelayParams.FromPaymentInfo(paymentInfo), + capacity = maxBtc.toSatoshi, + balance_opt = capacity_opt.orElse(Some(maxBtc.toMilliSatoshi)) + ) } /** A graph data structure that uses an adjacency list, stores the incoming edges of the neighbors */ @@ -642,6 +656,16 @@ object Graph { } def graphEdgeToHop(graphEdge: GraphEdge): ChannelHop = ChannelHop(graphEdge.desc.shortChannelId, graphEdge.desc.a, graphEdge.desc.b, graphEdge.params) + + def graphEdgesToRoute(amount: MilliSatoshi, graphEdges: Seq[GraphEdge], recipients: Seq[Recipient]): Route = { + val recipient = recipients.find(_.nodeId == graphEdges.last.desc.b).get + recipient match { + case _: ClearRecipient => Route(amount, graphEdges.map(graphEdgeToHop), recipient) + case _: BlindRecipient => + // The last edge is blinded and already included in the recipient. + Route(amount, graphEdges.dropRight(1).map(graphEdgeToHop), recipient) + } + } } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala index 4693369a90..825eaca4a3 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala @@ -22,7 +22,8 @@ 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.router.Graph.GraphStructure.DirectedGraph.graphEdgeToHop +import fr.acinq.eclair.payment.{BlindRecipient, ClearRecipient, Recipient} +import fr.acinq.eclair.router.Graph.GraphStructure.DirectedGraph.graphEdgesToRoute import fr.acinq.eclair.router.Graph.GraphStructure.{DirectedGraph, GraphEdge} import fr.acinq.eclair.router.Graph.{InfiniteLoop, NegativeProbability, RichWeight} import fr.acinq.eclair.router.Monitoring.{Metrics, Tags} @@ -53,7 +54,7 @@ object RouteCalculation { // select the largest edge (using balance when available, otherwise capacity). val selectedEdges = edges.map(es => es.maxBy(e => e.balance_opt.getOrElse(e.capacity.toMilliSatoshi))) val hops = selectedEdges.map(d => ChannelHop(d.desc.shortChannelId, d.desc.a, d.desc.b, d.params)) - ctx.sender() ! RouteResponse(Route(fr.amount, hops, None) :: Nil) + ctx.sender() ! RouteResponse(Route(fr.amount, hops, fr.recipient) :: Nil) case _ => // some nodes in the supplied route aren't connected in our graph ctx.sender() ! Status.Failure(new IllegalArgumentException("Not all the nodes in the supplied route are connected with public channels")) @@ -82,7 +83,7 @@ object RouteCalculation { if (end != targetNodeId || hops.length != shortChannelIds.length) { ctx.sender() ! Status.Failure(new IllegalArgumentException("The sequence of channels provided cannot be used to build a route to the target node")) } else { - ctx.sender() ! RouteResponse(Route(fr.amount, hops, None) :: Nil) + ctx.sender() ! RouteResponse(Route(fr.amount, hops, fr.recipient) :: Nil) } } @@ -103,15 +104,20 @@ object RouteCalculation { val params = r.routeParams val routesToFind = if (params.randomize) DEFAULT_ROUTES_COUNT else 1 - log.info(s"finding routes ${r.source}->${r.target} with assistedChannels={} ignoreNodes={} ignoreChannels={} excludedChannels={}", extraEdges.map(_.desc.shortChannelId).mkString(","), r.ignore.nodes.map(_.value).mkString(","), r.ignore.channels.mkString(","), d.excludedChannels.mkString(",")) + log.info(s"finding routes ${r.source}->${r.targets.head.nodeId} with assistedChannels={} ignoreNodes={} ignoreChannels={} excludedChannels={}", extraEdges.map(_.desc.shortChannelId).mkString(","), r.ignore.nodes.map(_.value).mkString(","), r.ignore.channels.mkString(","), d.excludedChannels.mkString(",")) log.info("finding routes with params={}, multiPart={}", params, r.allowMultiPart) - log.info("local channels to recipient: {}", d.graphWithBalances.graph.getEdgesBetween(r.source, r.target).map(e => s"${e.desc.shortChannelId} (${e.balance_opt}/${e.capacity})").mkString(", ")) + val clearTargetNodeIs = r.targets.map { + case ClearRecipient(nodeId, _, _, _, _, _) => nodeId + case BlindRecipient(route, _, _, _, _) => route.introductionNodeId + }.distinct + val directChannels = clearTargetNodeIs.flatMap(d.graphWithBalances.graph.getEdgesBetween(r.source, _)) + log.info("local channels to recipient: {}", directChannels.map(e => s"${e.desc.shortChannelId} (${e.balance_opt}/${e.capacity})").mkString(", ")) val tags = TagSet.Empty.withTag(Tags.MultiPart, r.allowMultiPart).withTag(Tags.Amount, Tags.amountBucket(r.amount)) KamonExt.time(Metrics.FindRouteDuration.withTags(tags.withTag(Tags.NumberOfRoutes, routesToFind.toLong))) { val result = if (r.allowMultiPart) { - findMultiPartRoute(d.graphWithBalances.graph, r.source, r.target, r.amount, r.maxFee, extraEdges, ignoredEdges, r.ignore.nodes, r.pendingPayments, params, currentBlockHeight) + findMultiPartRoute(d.graphWithBalances.graph, r.source, r.targets, r.amount, r.maxFee, extraEdges, ignoredEdges, r.ignore.nodes, r.pendingPayments, params, currentBlockHeight) } else { - findRoute(d.graphWithBalances.graph, r.source, r.target, r.amount, r.maxFee, routesToFind, extraEdges, ignoredEdges, r.ignore.nodes, params, currentBlockHeight) + findRoute(d.graphWithBalances.graph, r.source, r.targets, r.amount, r.maxFee, routesToFind, extraEdges, ignoredEdges, r.ignore.nodes, params, currentBlockHeight) } result match { case Success(routes) => @@ -176,7 +182,7 @@ object RouteCalculation { */ def findRoute(g: DirectedGraph, localNodeId: PublicKey, - targetNodeId: PublicKey, + recipients: Seq[Recipient], amount: MilliSatoshi, maxFee: MilliSatoshi, numRoutes: Int, @@ -185,8 +191,8 @@ object RouteCalculation { ignoredVertices: Set[PublicKey] = Set.empty, routeParams: RouteParams, currentBlockHeight: BlockHeight): Try[Seq[Route]] = Try { - findRouteInternal(g, localNodeId, targetNodeId, amount, maxFee, numRoutes, extraEdges, ignoredEdges, ignoredVertices, routeParams, currentBlockHeight) match { - case Right(routes) => routes.map(route => Route(amount, route.path.map(graphEdgeToHop), None)) + findRouteInternal(g, localNodeId, recipients, amount, maxFee, numRoutes, extraEdges, ignoredEdges, ignoredVertices, routeParams, currentBlockHeight) match { + case Right(routes) => routes.map(route => graphEdgesToRoute(amount, route.path, recipients)) case Left(ex) => return Failure(ex) } } @@ -194,7 +200,7 @@ object RouteCalculation { @tailrec private def findRouteInternal(g: DirectedGraph, localNodeId: PublicKey, - targetNodeId: PublicKey, + targets: Seq[Recipient], amount: MilliSatoshi, maxFee: MilliSatoshi, numRoutes: Int, @@ -205,7 +211,7 @@ object RouteCalculation { currentBlockHeight: BlockHeight): Either[RouterException, Seq[Graph.WeightedPath]] = { require(amount > 0.msat, "route amount must be strictly positive") - if (localNodeId == targetNodeId) return Left(CannotRouteToSelf) + if (targets.exists(_.introductionNodeId == localNodeId)) return Left(CannotRouteToSelf) def feeOk(fee: MilliSatoshi): Boolean = fee <= maxFee @@ -215,7 +221,10 @@ object RouteCalculation { val boundaries: RichWeight => Boolean = { weight => feeOk(weight.amount - amount) && lengthOk(weight.length) && cltvOk(weight.cltv) } - val foundRoutes: Seq[Graph.WeightedPath] = Graph.yenKshortestPaths(g, localNodeId, targetNodeId, amount, ignoredEdges, ignoredVertices, extraEdges, numRoutes, routeParams.heuristics, currentBlockHeight, boundaries, routeParams.includeLocalChannelCost) + val targetNodes = targets.map(_.nodeId) + val blindedEdges = targets.collect { case BlindRecipient(route, paymentInfo, capacity_opt, _, _) => GraphEdge(route, paymentInfo, capacity_opt) }.toSet + + val foundRoutes: Seq[Graph.WeightedPath] = Graph.yenKshortestPaths(g, localNodeId, targetNodes, amount, ignoredEdges, ignoredVertices, extraEdges ++ blindedEdges, numRoutes, routeParams.heuristics, currentBlockHeight, boundaries, routeParams.includeLocalChannelCost) if (foundRoutes.nonEmpty) { val (directRoutes, indirectRoutes) = foundRoutes.partition(_.path.length == 1) val routes = if (routeParams.randomize) { @@ -229,7 +238,7 @@ object RouteCalculation { val relaxedRouteParams = routeParams .modify(_.boundaries.maxRouteLength).setTo(ROUTE_MAX_LENGTH) .modify(_.boundaries.maxCltv).setTo(DEFAULT_ROUTE_MAX_CLTV) - findRouteInternal(g, localNodeId, targetNodeId, amount, maxFee, numRoutes, extraEdges, ignoredEdges, ignoredVertices, relaxedRouteParams, currentBlockHeight) + findRouteInternal(g, localNodeId, targets, amount, maxFee, numRoutes, extraEdges, ignoredEdges, ignoredVertices, relaxedRouteParams, currentBlockHeight) } else { Left(RouteNotFound) } @@ -240,7 +249,7 @@ object RouteCalculation { * * @param g graph of the whole network * @param localNodeId sender node (payer) - * @param targetNodeId target node (final recipient) + * @param targets target recipients * @param amount the amount that the target node should receive * @param maxFee the maximum fee of a resulting route * @param extraEdges a set of extra edges we want to CONSIDER during the search @@ -252,7 +261,7 @@ object RouteCalculation { */ def findMultiPartRoute(g: DirectedGraph, localNodeId: PublicKey, - targetNodeId: PublicKey, + targets: Seq[Recipient], amount: MilliSatoshi, maxFee: MilliSatoshi, extraEdges: Set[GraphEdge] = Set.empty, @@ -261,11 +270,11 @@ object RouteCalculation { pendingHtlcs: Seq[Route] = Nil, routeParams: RouteParams, currentBlockHeight: BlockHeight): Try[Seq[Route]] = Try { - val result = findMultiPartRouteInternal(g, localNodeId, targetNodeId, amount, maxFee, extraEdges, ignoredEdges, ignoredVertices, pendingHtlcs, routeParams, currentBlockHeight) match { + val result = findMultiPartRouteInternal(g, localNodeId, targets, amount, maxFee, extraEdges, ignoredEdges, ignoredVertices, pendingHtlcs, routeParams, currentBlockHeight) match { case Right(routes) => Right(routes) case Left(RouteNotFound) if routeParams.randomize => // If we couldn't find a randomized solution, fallback to a deterministic one. - findMultiPartRouteInternal(g, localNodeId, targetNodeId, amount, maxFee, extraEdges, ignoredEdges, ignoredVertices, pendingHtlcs, routeParams.copy(randomize = false), currentBlockHeight) + findMultiPartRouteInternal(g, localNodeId, targets, amount, maxFee, extraEdges, ignoredEdges, ignoredVertices, pendingHtlcs, routeParams.copy(randomize = false), currentBlockHeight) case Left(ex) => Left(ex) } result match { @@ -276,7 +285,7 @@ object RouteCalculation { private def findMultiPartRouteInternal(g: DirectedGraph, localNodeId: PublicKey, - targetNodeId: PublicKey, + targets: Seq[Recipient], amount: MilliSatoshi, maxFee: MilliSatoshi, extraEdges: Set[GraphEdge] = Set.empty, @@ -288,24 +297,23 @@ object RouteCalculation { // We use Yen's k-shortest paths to find many paths for chunks of the total amount. // When the recipient is a direct peer, we have complete visibility on our local channels so we can use more accurate MPP parameters. val routeParams1 = { - case class DirectChannel(balance: MilliSatoshi, isEmpty: Boolean) - val directChannels = g.getEdgesBetween(localNodeId, targetNodeId).collect { - // We should always have balance information available for local channels. - // NB: htlcMinimumMsat is set by our peer and may be 0 msat (even though it's not recommended). - case GraphEdge(_, params, _, Some(balance)) => DirectChannel(balance, balance <= 0.msat || balance < params.htlcMinimum) - } + val clearTargetNodeIs = targets.map { + case ClearRecipient(nodeId, _, _, _, _, _) => nodeId + case BlindRecipient(route, _, _, _, _) => route.introductionNodeId + }.distinct + val directChannelsCount = clearTargetNodeIs.map(g.getEdgesBetween(localNodeId, _).length).sum // If we have direct channels to the target, we can use them all. // We also count empty channels, which allows replacing them with a non-direct route (multiple hops). - val numRoutes = routeParams.mpp.maxParts.max(directChannels.length) + val numRoutes = routeParams.mpp.maxParts.max(directChannelsCount) // We want to ensure that the set of routes we find have enough capacity to allow sending the total amount, // without excluding routes with small capacity when the total amount is small. val minPartAmount = routeParams.mpp.minPartAmount.max(amount / numRoutes).min(amount) routeParams.copy(mpp = MultiPartParams(minPartAmount, numRoutes)) } - findRouteInternal(g, localNodeId, targetNodeId, routeParams1.mpp.minPartAmount, maxFee, routeParams1.mpp.maxParts, extraEdges, ignoredEdges, ignoredVertices, routeParams1, currentBlockHeight) match { + findRouteInternal(g, localNodeId, targets, routeParams1.mpp.minPartAmount, maxFee, routeParams1.mpp.maxParts, extraEdges, ignoredEdges, ignoredVertices, routeParams1, currentBlockHeight) match { case Right(routes) => // We use these shortest paths to find a set of non-conflicting HTLCs that send the total amount. - split(amount, mutable.Queue(routes: _*), initializeUsedCapacity(pendingHtlcs), routeParams1) match { + split(amount, mutable.Queue(routes: _*), initializeUsedCapacity(pendingHtlcs), routeParams1, targets) match { case Right(routes) if validateMultiPartRoute(amount, maxFee, routes, routeParams.includeLocalChannelCost) => Right(routes) case _ => Left(RouteNotFound) } @@ -314,17 +322,17 @@ object RouteCalculation { } @tailrec - private def split(amount: MilliSatoshi, paths: mutable.Queue[Graph.WeightedPath], usedCapacity: mutable.Map[ShortChannelId, MilliSatoshi], routeParams: RouteParams, selectedRoutes: Seq[Route] = Nil): Either[RouterException, Seq[Route]] = { + private def split(amount: MilliSatoshi, paths: mutable.Queue[Graph.WeightedPath], usedCapacity: mutable.Map[ShortChannelId, MilliSatoshi], routeParams: RouteParams, recipients: Seq[Recipient], selectedRoutes: Seq[Route] = Nil): Either[RouterException, Seq[Route]] = { if (amount == 0.msat) { Right(selectedRoutes) } else if (paths.isEmpty) { Left(RouteNotFound) } else { val current = paths.dequeue() - val candidate = computeRouteMaxAmount(current.path, usedCapacity) + val candidate = computeRouteMaxAmount(current.path, usedCapacity, recipients) if (candidate.amount < routeParams.mpp.minPartAmount.min(amount)) { // this route doesn't have enough capacity left: we remove it and continue. - split(amount, paths, usedCapacity, routeParams, selectedRoutes) + split(amount, paths, usedCapacity, routeParams, recipients, selectedRoutes) } else { val route = if (routeParams.randomize) { // randomly choose the amount to be between 20% and 100% of the available capacity. @@ -339,13 +347,13 @@ object RouteCalculation { } updateUsedCapacity(route, usedCapacity) // NB: we re-enqueue the current path, it may still have capacity for a second HTLC. - split(amount - route.amount, paths.enqueue(current), usedCapacity, routeParams, route +: selectedRoutes) + split(amount - route.amount, paths.enqueue(current), usedCapacity, routeParams, recipients, route +: selectedRoutes) } } } /** Compute the maximum amount that we can send through the given route. */ - private def computeRouteMaxAmount(route: Seq[GraphEdge], usedCapacity: mutable.Map[ShortChannelId, MilliSatoshi]): Route = { + private def computeRouteMaxAmount(route: Seq[GraphEdge], usedCapacity: mutable.Map[ShortChannelId, MilliSatoshi], recipients: Seq[Recipient]): Route = { val firstHopMaxAmount = route.head.maxHtlcAmount(usedCapacity.getOrElse(route.head.desc.shortChannelId, 0 msat)) val amount = route.drop(1).foldLeft(firstHopMaxAmount) { case (amount, edge) => // We compute fees going forward instead of backwards. That means we will slightly overestimate the fees of some @@ -354,7 +362,7 @@ object RouteCalculation { val edgeMaxAmount = edge.maxHtlcAmount(usedCapacity.getOrElse(edge.desc.shortChannelId, 0 msat)) amountMinusFees.min(edgeMaxAmount) } - Route(amount.max(0 msat), route.map(graphEdgeToHop), None) + graphEdgesToRoute(amount.max(0 msat), route, recipients) } /** Initialize known used capacity based on pending HTLCs. */ @@ -386,7 +394,11 @@ object RouteCalculation { * the target node it means we'd like to reach it via direct channels as much as possible. */ private def isNeighborBalanceTooLow(g: DirectedGraph, r: RouteRequest): Boolean = { - val neighborEdges = g.getEdgesBetween(r.source, r.target) + val clearTargetNodeIs = r.targets.map { + case ClearRecipient(nodeId, _, _, _, _, _) => nodeId + case BlindRecipient(route, _, _, _, _) => route.introductionNodeId + }.distinct + val neighborEdges = clearTargetNodeIs.flatMap(g.getEdgesBetween(r.source, _)) neighborEdges.nonEmpty && neighborEdges.map(e => e.balance_opt.getOrElse(e.capacity.toMilliSatoshi)).sum < r.amount } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala index d62efe4c23..f2c9e98bf7 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala @@ -33,11 +33,12 @@ import fr.acinq.eclair.db.NetworkDb import fr.acinq.eclair.io.Peer.PeerRoutingMessage import fr.acinq.eclair.payment.Invoice.ExtraEdge import fr.acinq.eclair.payment.relay.Relayer -import fr.acinq.eclair.payment.{BlindedPaymentRoute, Bolt11Invoice, Invoice} +import fr.acinq.eclair.payment.{Bolt11Invoice, Invoice, Recipient} import fr.acinq.eclair.remote.EclairInternalsSerializer.RemoteTypes import fr.acinq.eclair.router.Graph.GraphStructure.DirectedGraph import fr.acinq.eclair.router.Graph.{HeuristicsConstants, WeightRatios} import fr.acinq.eclair.router.Monitoring.Metrics +import fr.acinq.eclair.wire.protocol.OfferTypes.PaymentInfo import fr.acinq.eclair.wire.protocol._ import java.util.UUID @@ -448,6 +449,13 @@ object Router { override def htlcMinimum: MilliSatoshi = extraHop.htlcMinimum override def htlcMaximum_opt: Option[MilliSatoshi] = extraHop.htlcMaximum_opt } + /** It's a blinded route we learnt about from an invoice */ + case class FromPaymentInfo(paymentInfo: PaymentInfo) extends ChannelRelayParams { + override def cltvExpiryDelta: CltvExpiryDelta = paymentInfo.cltvExpiryDelta + override def relayFees: Relayer.RelayFees = Relayer.RelayFees(paymentInfo.feeBase, paymentInfo.feeProportionalMillionths) + override def htlcMinimum: MilliSatoshi = paymentInfo.minHtlc + override def htlcMaximum_opt: Option[MilliSatoshi] = Some(paymentInfo.maxHtlc) + } def areSame(a: ChannelRelayParams, b: ChannelRelayParams, ignoreHtlcSize: Boolean = false): Boolean = a.cltvExpiryDelta == b.cltvExpiryDelta && @@ -514,7 +522,7 @@ object Router { } case class RouteRequest(source: PublicKey, - target: PublicKey, + targets: Seq[payment.Recipient], amount: MilliSatoshi, maxFee: MilliSatoshi, extraEdges: Seq[ExtraEdge] = Nil, @@ -526,6 +534,7 @@ object Router { case class FinalizeRoute(amount: MilliSatoshi, route: PredefinedRoute, + recipient: Recipient, extraEdges: Seq[ExtraEdge] = Nil, paymentContext: Option[PaymentContext] = None) @@ -538,15 +547,14 @@ object Router { * There must be a next node to relay the payment to. If there are no clear hops, it must end with a blinded route for * which we are the introduction point and there must be a second blinded hop that is not us. */ - case class Route(amount: MilliSatoshi, clearHops: Seq[ChannelHop], blinded_opt: Option[BlindedPaymentRoute]) { - require(clearHops.nonEmpty || blinded_opt.nonEmpty, "route cannot be empty") + case class Route(amount: MilliSatoshi, clearHops: Seq[ChannelHop], recipient: payment.Recipient) { + require(clearHops.nonEmpty || recipient.isInstanceOf[payment.BlindRecipient], "route cannot be empty") val length: Int = clearHops.length def fee(includeLocalChannelCost: Boolean): MilliSatoshi = { val hopsToPay = if (includeLocalChannelCost) clearHops else clearHops.drop(1) - val amountBeforeBlinded = amount + blinded_opt.map(_.paymentInfo.fee(amount)).getOrElse(0 msat) - val amountToSend = hopsToPay.reverse.foldLeft(amountBeforeBlinded) { case (amount1, hop) => amount1 + hop.fee(amount1) } + val amountToSend = hopsToPay.reverse.foldLeft(recipient.amountToSend(amount)) { case (amount1, hop) => amount1 + hop.fee(amount1) } amountToSend - amount } @@ -556,7 +564,7 @@ object Router { def stopAt(nodeId: PublicKey): Route = { val amountAtStop = clearHops.reverse.takeWhile(_.nextNodeId != nodeId).foldLeft(amount) { case (amount1, hop) => amount1 + hop.fee(amount1) } - Route(amountAtStop, clearHops.takeWhile(_.nodeId != nodeId), None) + Route(amountAtStop, clearHops.takeWhile(_.nodeId != nodeId), recipient) } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/PaymentOnion.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/PaymentOnion.scala index 3b17b3c564..30801d99f3 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/PaymentOnion.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/PaymentOnion.scala @@ -18,7 +18,7 @@ package fr.acinq.eclair.wire.protocol import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey -import fr.acinq.eclair.payment.Bolt11Invoice +import fr.acinq.eclair.payment.{Bolt11Invoice, ClearRecipient, Recipient} import fr.acinq.eclair.wire.protocol.CommonCodecs._ import fr.acinq.eclair.wire.protocol.OnionRoutingCodecs.{ForbiddenTlv, InvalidTlvPayload, MissingRequiredTlv} import fr.acinq.eclair.wire.protocol.TlvCodecs._ @@ -335,7 +335,7 @@ object PaymentOnion { val tlvs = Seq( Some(AmountToForward(amount)), Some(OutgoingCltv(expiry)), - invoice.paymentSecret.map(s => PaymentData(s, totalAmount)), + Some(PaymentData(invoice.paymentSecret, totalAmount)), invoice.paymentMetadata.map(m => PaymentMetadata(m)), Some(OutgoingNodeId(targetNodeId)), Some(InvoiceFeatures(invoice.features.toByteVector)), @@ -437,6 +437,7 @@ object PaymentOnion { } def create(amount: MilliSatoshi, + totalAmount: MilliSatoshi, expiry: CltvExpiry, encryptedRecipientData: ByteVector, blinding_opt: Option[PublicKey], @@ -445,6 +446,7 @@ object PaymentOnion { val tlvs = Seq( blinding_opt.map(BlindingPoint), Some(AmountToForward(amount)), + Some(TotalAmount(totalAmount)), Some(OutgoingCltv(expiry)), Some(EncryptedRecipientData(encryptedRecipientData)), ).flatten diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala index e4a3e0a2a7..996849019a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala @@ -33,15 +33,16 @@ import fr.acinq.eclair.crypto.TransportHandler import fr.acinq.eclair.db._ import fr.acinq.eclair.io.Peer.PeerRoutingMessage import fr.acinq.eclair.payment._ -import fr.acinq.eclair.payment.receive.MultiPartHandler.ReceiveStandardPayment +import fr.acinq.eclair.payment.receive.MultiPartHandler.{ReceiveOfferPayment, ReceiveStandardPayment} import fr.acinq.eclair.payment.relay.Relayer import fr.acinq.eclair.payment.relay.Relayer.RelayFees import fr.acinq.eclair.payment.send.PaymentInitiator.{SendPaymentToNode, SendTrampolinePayment} import fr.acinq.eclair.router.Graph.WeightRatios import fr.acinq.eclair.router.Router.{GossipDecision, PublicChannel} import fr.acinq.eclair.router.{Announcements, AnnouncementsBatchValidationSpec, Router} +import fr.acinq.eclair.wire.protocol.OfferTypes.{InvoiceRequest, Offer} import fr.acinq.eclair.wire.protocol.{ChannelAnnouncement, ChannelUpdate, IncorrectOrUnknownPaymentDetails} -import fr.acinq.eclair.{CltvExpiryDelta, Features, Kit, MilliSatoshiLong, ShortChannelId, TimestampMilli, randomBytes32} +import fr.acinq.eclair.{CltvExpiryDelta, Features, Kit, MilliSatoshiLong, ShortChannelId, TimestampMilli, randomBytes32, randomKey} import org.json4s.JsonAST.{JString, JValue} import scodec.bits.ByteVector @@ -680,6 +681,17 @@ class PaymentIntegrationSpec extends IntegrationSpec { }, max = 120 seconds, interval = 1 second) } + test("blinded payment") { + val sender = TestProbe() + val amount = 1000_000 msat + val offer = Offer(Some(amount), "test offer", nodes("A").nodeParams.nodeId, Features.empty, nodes("A").nodeParams.chainHash) + val payerKey = randomKey() + val invoiceRequest = InvoiceRequest(offer, amount, 1, Features.empty, payerKey, nodes("A").nodeParams.chainHash) + sender.send(nodes("A").paymentHandler, ReceiveOfferPayment(nodes("A").nodeParams.privateKey, offer, invoiceRequest)) + val invoice = sender.expectMsgType[Invoice] + + } + /** Handy way to check what the channel balances are before adding new tests. */ def debugChannelBalances(): Unit = { val sender = TestProbe() From 42d7801d3ee7f16d146b74e5f139097e0a005449 Mon Sep 17 00:00:00 2001 From: Thomas HUET Date: Wed, 28 Sep 2022 15:51:59 +0200 Subject: [PATCH 3/8] Fix old tests --- .../acinq/eclair/payment/Bolt12Invoice.scala | 1 - .../fr/acinq/eclair/payment/Invoice.scala | 3 - .../fr/acinq/eclair/payment/Recipient.scala | 29 +- .../eclair/payment/relay/NodeRelay.scala | 2 +- .../send/MultiPartPaymentLifecycle.scala | 4 +- .../payment/send/PaymentInitiator.scala | 18 +- .../payment/send/PaymentLifecycle.scala | 8 +- .../fr/acinq/eclair/EclairImplSpec.scala | 4 +- .../fr/acinq/eclair/channel/FuzzySpec.scala | 10 +- .../ChannelStateTestsHelperMethods.scala | 6 +- .../channel/states/f/ShutdownStateSpec.scala | 6 +- .../integration/PaymentIntegrationSpec.scala | 12 +- .../eclair/payment/Bolt11InvoiceSpec.scala | 131 +++++---- .../eclair/payment/Bolt12InvoiceSpec.scala | 2 +- .../eclair/payment/MultiPartHandlerSpec.scala | 40 +-- .../MultiPartPaymentLifecycleSpec.scala | 109 ++++---- .../eclair/payment/PaymentInitiatorSpec.scala | 56 ++-- .../eclair/payment/PaymentLifecycleSpec.scala | 100 +++---- .../eclair/payment/PaymentPacketSpec.scala | 58 ++-- .../payment/PostRestartHtlcCleanerSpec.scala | 5 +- .../payment/relay/NodeRelayerSpec.scala | 18 +- .../eclair/payment/relay/RelayerSpec.scala | 18 +- .../fr/acinq/eclair/router/GraphSpec.scala | 8 +- .../eclair/router/RouteCalculationSpec.scala | 256 +++++++++--------- .../fr/acinq/eclair/router/RouterSpec.scala | 68 ++--- .../src/test/resources/api/findroute-full | 2 +- .../src/test/resources/api/received-expired | 2 +- .../src/test/resources/api/received-pending | 2 +- .../src/test/resources/api/received-success | 2 +- .../fr/acinq/eclair/api/ApiServiceSpec.scala | 36 +-- 30 files changed, 507 insertions(+), 509 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt12Invoice.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt12Invoice.scala index 9759a45cd8..a031464237 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt12Invoice.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt12Invoice.scala @@ -42,7 +42,6 @@ case class Bolt12Invoice(records: TlvStream[InvoiceTlv]) extends Invoice { override val amount_opt: Option[MilliSatoshi] = Some(amount) val nodeId: Crypto.PublicKey = records.get[NodeId].get.publicKey override val paymentHash: ByteVector32 = records.get[PaymentHash].get.hash - override val paymentMetadata: Option[ByteVector] = None override val description: Either[String, ByteVector32] = Left(records.get[Description].get.description) override val extraEdges: Seq[Invoice.ExtraEdge] = Seq.empty override val createdAt: TimestampSecond = records.get[CreatedAt].get.timestamp diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Invoice.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Invoice.scala index db04c93e6f..d346f5e619 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Invoice.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Invoice.scala @@ -21,7 +21,6 @@ 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.{CltvExpiryDelta, Features, InvoiceFeature, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TimestampSecond} -import scodec.bits.ByteVector import scala.concurrent.duration.FiniteDuration import scala.util.Try @@ -35,8 +34,6 @@ trait Invoice { val paymentHash: ByteVector32 - val paymentMetadata: Option[ByteVector] - val description: Either[String, ByteVector32] val extraEdges: Seq[Invoice.ExtraEdge] diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Recipient.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Recipient.scala index 52f83e313a..51a4203ba2 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Recipient.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Recipient.scala @@ -28,6 +28,7 @@ import scodec.bits.ByteVector sealed trait Recipient { def nodeId: PublicKey def introductionNodeId: PublicKey + def nodeIds: Seq[PublicKey] def features: Features[InvoiceFeature] @@ -39,8 +40,6 @@ sealed trait Recipient { def withCustomTlvs(customTlvs: Seq[GenericTlv]): Recipient - def contains(id: PublicKey): Boolean - def buildFinalPayloads(amount: MilliSatoshi, totalAmount: MilliSatoshi, expiry: CltvExpiry): (MilliSatoshi, CltvExpiry, Seq[PerHopPayload]) @@ -52,33 +51,28 @@ case class ClearRecipient(nodeId: PublicKey, features: Features[InvoiceFeature] = Features.empty, additionalTlvs: Seq[OnionPaymentPayloadTlv] = Nil, userCustomTlvs: Seq[GenericTlv] = Nil) extends Recipient { - override def introductionNodeId: PublicKey = nodeId + override val introductionNodeId: PublicKey = nodeId + + override val nodeIds: Seq[PublicKey] = Seq(nodeId) override def amountToSend(amount: MilliSatoshi): MilliSatoshi = amount override def withCustomTlvs(customTlvs: Seq[GenericTlv]): Recipient = copy(userCustomTlvs = customTlvs) - override def contains(id: PublicKey): Boolean = id == nodeId - override def buildFinalPayloads(amount: MilliSatoshi, totalAmount: MilliSatoshi, expiry: CltvExpiry): (MilliSatoshi, CltvExpiry, Seq[PerHopPayload]) = (amount, expiry, Seq(FinalPayload.Standard.createMultiPartPayload(amount, totalAmount, expiry, paymentSecret, paymentMetadata_opt, additionalTlvs, userCustomTlvs))) } -object ClearRecipient { - def fromTrampolinePayload(payload : IntermediatePayload.NodeRelay.Standard): ClearRecipient = - ClearRecipient(payload.outgoingNodeId, payload.paymentSecret.get, payload.paymentMetadata, payload.invoiceFeatures.map(Features(_).invoiceFeatures()).getOrElse(Features.empty)) -} - object KeySendRecipient { def apply(nodeId: PublicKey, paymentPreimage: ByteVector32, userCustomTlvs: Seq[GenericTlv]): ClearRecipient = ClearRecipient(nodeId, randomBytes32(), None, additionalTlvs = Seq(OnionPaymentPayloadTlv.KeySend(paymentPreimage)), userCustomTlvs = userCustomTlvs) } object TrampolineRecipient { - def apply(trampolineNodeId: PublicKey, trampolineOnion: OnionRoutingPacket, paymentMetadata_opt: Option[ByteVector]): ClearRecipient = - ClearRecipient(trampolineNodeId, randomBytes32(), paymentMetadata_opt, additionalTlvs = Seq(OnionPaymentPayloadTlv.TrampolineOnion(trampolineOnion))) + def apply(trampolineNodeId: PublicKey, trampolineOnion: OnionRoutingPacket, paymentMetadata_opt: Option[ByteVector], trampolineSecret: ByteVector32 = randomBytes32()): ClearRecipient = + ClearRecipient(trampolineNodeId, trampolineSecret, paymentMetadata_opt, additionalTlvs = Seq(OnionPaymentPayloadTlv.TrampolineOnion(trampolineOnion))) } case class BlindRecipient(route: RouteBlinding.BlindedRoute, @@ -86,19 +80,18 @@ case class BlindRecipient(route: RouteBlinding.BlindedRoute, capacity_opt: Option[MilliSatoshi], additionalTlvs: Seq[OnionPaymentPayloadTlv] = Nil, userCustomTlvs: Seq[GenericTlv] = Nil) extends Recipient { - override def nodeId: PublicKey = route.blindedNodeIds.last + override val nodeId: PublicKey = route.blindedNodeIds.last + + override val introductionNodeId: PublicKey = route.introductionNodeId - override def introductionNodeId: PublicKey = route.introductionNodeId + override val nodeIds: Seq[PublicKey] = (introductionNodeId +: route.blindedNodeIds).reverse - override def features: Features[InvoiceFeature] = paymentInfo.allowedFeatures.invoiceFeatures() + override val features: Features[InvoiceFeature] = paymentInfo.allowedFeatures.invoiceFeatures() override def amountToSend(amount: MilliSatoshi): MilliSatoshi = amount + paymentInfo.fee(amount) override def withCustomTlvs(customTlvs: Seq[GenericTlv]): Recipient = copy(userCustomTlvs = customTlvs) - override def contains(id: PublicKey): Boolean = - id == route.introductionNodeId || route.blindedNodeIds.contains(id) - override def buildFinalPayloads(amount: MilliSatoshi, totalAmount: MilliSatoshi, expiry: CltvExpiry): (MilliSatoshi, CltvExpiry, Seq[PerHopPayload]) = { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelay.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelay.scala index 186dd39aad..a7f1528908 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelay.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelay.scala @@ -302,7 +302,7 @@ class NodeRelay private(nodeParams: NodeParams, }.toClassic private def relay(upstream: Upstream.Trampoline, payloadOut: IntermediatePayload.NodeRelay.Standard, packetOut: OnionRoutingPacket): ActorRef = { - val paymentCfg = SendPaymentConfig(relayId, relayId, None, paymentHash, payloadOut.amountToForward, Seq(ClearRecipient.fromTrampolinePayload(payloadOut)), upstream, None, storeInDb = false, publishEvent = false, recordPathFindingMetrics = true, Nil) + val paymentCfg = SendPaymentConfig(relayId, relayId, None, paymentHash, payloadOut.amountToForward, Seq(payloadOut.outgoingNodeId), upstream, None, storeInDb = false, publishEvent = false, recordPathFindingMetrics = true, Nil) val routeParams = computeRouteParams(nodeParams, upstream.amountIn, upstream.expiryIn, payloadOut.amountToForward, payloadOut.outgoingCltv) // If invoice features are provided in the onion, the sender is asking us to relay to a non-trampoline recipient. val payFSM = payloadOut.invoiceFeatures match { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala index e465cac334..ffcd7d36f3 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala @@ -239,7 +239,7 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, val status = event match { case Right(_: PaymentSent) => "SUCCESS" case Left(f: PaymentFailed) => - if (f.failures.exists({ case r: RemoteFailure => cfg.recipients.exists(_.contains(r.e.originNode)) case _ => false })) { + if (f.failures.exists({ case r: RemoteFailure => cfg.isRecipient(r.e.originNode) case _ => false })) { "RECIPIENT_FAILURE" } else { "FAILURE" @@ -400,7 +400,7 @@ object MultiPartPaymentLifecycle { /** When we receive an error from the final recipient or payment gets settled on chain, we should fail the whole payment, it's useless to retry. */ private def abortPayment(pf: PaymentFailed, d: PaymentProgress): Boolean = pf.failures.exists { - case f: RemoteFailure => d.request.recipients.exists(_.contains(f.e.originNode)) + case f: RemoteFailure => d.request.recipients.flatMap(_.nodeIds).contains(f.e.originNode) case LocalFailure(_, _, _: HtlcOverriddenByLocalCommit) => true case LocalFailure(_, _, _: HtlcsWillTimeoutUpstream) => true case LocalFailure(_, _, _: HtlcsTimedoutDownstream) => true diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala index 4f5ab8ccdf..b2ab41a8ec 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala @@ -50,7 +50,7 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn sender() ! paymentId } val recipients = r.recipients.map(_.withCustomTlvs(r.userCustomTlvs)) - val paymentCfg = SendPaymentConfig(paymentId, paymentId, r.externalId, r.paymentHash, r.recipientAmount, recipients, Upstream.Local(paymentId), Some(r.invoice), storeInDb = true, publishEvent = true, recordPathFindingMetrics = true, Nil) + val paymentCfg = SendPaymentConfig(paymentId, paymentId, r.externalId, r.paymentHash, r.recipientAmount, recipients.flatMap(_.nodeIds), Upstream.Local(paymentId), Some(r.invoice), storeInDb = true, publishEvent = true, recordPathFindingMetrics = true, Nil) val finalExpiry = r.finalExpiry(nodeParams.currentBlockHeight) if (!nodeParams.features.invoiceFeatures().areSupported(r.invoice.features)) { sender() ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(r.recipientAmount, Nil, UnsupportedFeatures(r.invoice.features)) :: Nil) @@ -68,7 +68,7 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn val paymentId = UUID.randomUUID() sender() ! paymentId val recipients = Seq(KeySendRecipient(r.recipientNodeId, r.paymentPreimage, r.userCustomTlvs)) - val paymentCfg = SendPaymentConfig(paymentId, paymentId, r.externalId, r.paymentHash, r.recipientAmount, recipients, Upstream.Local(paymentId), None, storeInDb = true, publishEvent = true, recordPathFindingMetrics = r.recordPathFindingMetrics, Nil) + val paymentCfg = SendPaymentConfig(paymentId, paymentId, r.externalId, r.paymentHash, r.recipientAmount, recipients.flatMap(_.nodeIds), Upstream.Local(paymentId), None, storeInDb = true, publishEvent = true, recordPathFindingMetrics = r.recordPathFindingMetrics, Nil) val finalExpiry = Channel.MIN_CLTV_EXPIRY_DELTA.toCltvExpiry(nodeParams.currentBlockHeight + 1) val fsm = outgoingPaymentFactory.spawnOutgoingPayment(context, paymentCfg) fsm ! PaymentLifecycle.SendPaymentToNode(self, recipients, r.recipientAmount, r.recipientAmount, finalExpiry, r.maxAttempts, routeParams = r.routeParams) @@ -97,7 +97,7 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn val paymentId = UUID.randomUUID() val parentPaymentId = r.parentId.getOrElse(UUID.randomUUID()) val additionalHops = r.trampolineNodes.sliding(2).map(hop => NodeHop(hop.head, hop(1), CltvExpiryDelta(0), 0 msat)).toSeq - val paymentCfg = SendPaymentConfig(paymentId, parentPaymentId, r.externalId, r.paymentHash, r.recipientAmount, r.recipients, Upstream.Local(paymentId), Some(r.invoice), storeInDb = true, publishEvent = true, recordPathFindingMetrics = false, additionalHops) + val paymentCfg = SendPaymentConfig(paymentId, parentPaymentId, r.externalId, r.paymentHash, r.recipientAmount, r.recipients.flatMap(_.nodeIds), Upstream.Local(paymentId), Some(r.invoice), storeInDb = true, publishEvent = true, recordPathFindingMetrics = false, additionalHops) r.trampolineNodes match { case trampoline :: recipient :: Nil => log.info(s"sending trampoline payment to $recipient with trampoline=$trampoline, trampoline fees=${r.trampolineFees}, expiry delta=${r.trampolineExpiryDelta}") @@ -107,7 +107,7 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn val trampolineSecret = r.trampolineSecret.getOrElse(randomBytes32()) sender() ! SendPaymentToRouteResponse(paymentId, parentPaymentId, Some(trampolineSecret)) val payFsm = outgoingPaymentFactory.spawnOutgoingPayment(context, paymentCfg) - payFsm ! PaymentLifecycle.SendPaymentToRoute(self, Left(r.route), TrampolineRecipient(trampoline, trampolineOnion, r.invoice.paymentMetadata), r.amount, trampolineAmount, trampolineExpiry, r.invoice.extraEdges) + payFsm ! PaymentLifecycle.SendPaymentToRoute(self, Left(r.route), TrampolineRecipient(trampoline, trampolineOnion, r.invoice.paymentMetadata, trampolineSecret), r.amount, trampolineAmount, trampolineExpiry, r.invoice.extraEdges) context become main(pending + (paymentId -> PendingTrampolinePaymentToRoute(sender(), r))) case Failure(t) => log.warning("cannot send outgoing trampoline payment: {}", t.getMessage) @@ -121,7 +121,7 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn val paymentId = UUID.randomUUID() val parentPaymentId = r.parentId.getOrElse(UUID.randomUUID()) val finalExpiry = r.finalExpiry(nodeParams.currentBlockHeight) - val paymentCfg = SendPaymentConfig(paymentId, parentPaymentId, r.externalId, r.paymentHash, r.recipientAmount, r.recipients, Upstream.Local(paymentId), Some(r.invoice), storeInDb = true, publishEvent = true, recordPathFindingMetrics = false, Nil) + val paymentCfg = SendPaymentConfig(paymentId, parentPaymentId, r.externalId, r.paymentHash, r.recipientAmount, r.recipients.flatMap(_.nodeIds), Upstream.Local(paymentId), Some(r.invoice), storeInDb = true, publishEvent = true, recordPathFindingMetrics = false, Nil) sender() ! SendPaymentToRouteResponse(paymentId, parentPaymentId, None) val payFsm = outgoingPaymentFactory.spawnOutgoingPayment(context, paymentCfg) r.recipients.find(_.introductionNodeId == r.route.targetNodeId) match { @@ -216,7 +216,7 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn } private def sendTrampolinePayment(paymentId: UUID, r: SendTrampolinePayment, trampolineFees: MilliSatoshi, trampolineExpiryDelta: CltvExpiryDelta): Try[Unit] = { - val paymentCfg = SendPaymentConfig(paymentId, paymentId, None, r.paymentHash, r.recipientAmount, r.recipients, Upstream.Local(paymentId), Some(r.invoice), storeInDb = true, publishEvent = false, recordPathFindingMetrics = true, Seq(NodeHop(r.trampolineNodeId, r.invoice.nodeId, trampolineExpiryDelta, trampolineFees))) + val paymentCfg = SendPaymentConfig(paymentId, paymentId, None, r.paymentHash, r.recipientAmount, r.recipients.flatMap(_.nodeIds), Upstream.Local(paymentId), Some(r.invoice), storeInDb = true, publishEvent = false, recordPathFindingMetrics = true, Seq(NodeHop(r.trampolineNodeId, r.invoice.nodeId, trampolineExpiryDelta, trampolineFees))) buildTrampolinePayment(r, r.trampolineNodeId, trampolineFees, trampolineExpiryDelta).map { case (trampolineAmount, trampolineExpiry, trampolineOnion) => val fsm = outgoingPaymentFactory.spawnOutgoingMultiPartPayment(context, paymentCfg) @@ -448,14 +448,14 @@ object PaymentInitiator { externalId: Option[String], paymentHash: ByteVector32, recipientAmount: MilliSatoshi, - recipients: Seq[Recipient], + recipientNodeIds: Seq[PublicKey], upstream: Upstream, invoice: Option[Invoice], storeInDb: Boolean, // e.g. for trampoline we don't want to store in the DB when we're relaying payments publishEvent: Boolean, recordPathFindingMetrics: Boolean, additionalHops: Seq[NodeHop]) { - val recipientNodeId: PublicKey = recipients.head.nodeId + val recipientNodeId: PublicKey = recipientNodeIds.head def fullRoute(route: Route): Seq[Hop] = route.clearHops ++ additionalHops @@ -463,7 +463,7 @@ object PaymentInitiator { def paymentContext: PaymentContext = PaymentContext(id, parentId, paymentHash) - def isRecipient(nodeId: PublicKey): Boolean = recipients.exists(_.contains(nodeId)) + def isRecipient(nodeId: PublicKey): Boolean = recipientNodeIds.contains(nodeId) } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala index a27a5a6153..0116cc1035 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala @@ -61,15 +61,15 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A route => self ! RouteResponse(route :: Nil) ) if (cfg.storeInDb) { - paymentsDb.addOutgoingPayment(OutgoingPayment(id, cfg.parentId, cfg.externalId, paymentHash, PaymentType.Standard, c.amount, cfg.recipientAmount, cfg.recipients.head.nodeId, TimestampMilli.now(), cfg.invoice, OutgoingPaymentStatus.Pending)) + paymentsDb.addOutgoingPayment(OutgoingPayment(id, cfg.parentId, cfg.externalId, paymentHash, PaymentType.Standard, c.amount, cfg.recipientAmount, cfg.recipientNodeId, TimestampMilli.now(), cfg.invoice, OutgoingPaymentStatus.Pending)) } goto(WAITING_FOR_ROUTE) using WaitingForRoute(c, Nil, Ignore.empty) case Event(c: SendPaymentToNode, WaitingForRequest) => log.debug("sending {} to {}", c.amount, c.targetRecipients.head.nodeId) - router ! RouteRequest(nodeParams.nodeId, c.targetRecipients, c.amount, c.maxFee, routeParams = c.routeParams, paymentContext = Some(cfg.paymentContext)) + router ! RouteRequest(nodeParams.nodeId, c.targetRecipients, c.amount, c.maxFee, c.extraEdges, routeParams = c.routeParams, paymentContext = Some(cfg.paymentContext)) if (cfg.storeInDb) { - paymentsDb.addOutgoingPayment(OutgoingPayment(id, cfg.parentId, cfg.externalId, paymentHash, PaymentType.Standard, c.amount, cfg.recipientAmount, cfg.recipients.head.nodeId, TimestampMilli.now(), cfg.invoice, OutgoingPaymentStatus.Pending)) + paymentsDb.addOutgoingPayment(OutgoingPayment(id, cfg.parentId, cfg.externalId, paymentHash, PaymentType.Standard, c.amount, cfg.recipientAmount, cfg.recipientNodeId, TimestampMilli.now(), cfg.invoice, OutgoingPaymentStatus.Pending)) } goto(WAITING_FOR_ROUTE) using WaitingForRoute(c, Nil, Ignore.empty) } @@ -194,7 +194,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A res case res => res }) match { - case Success(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage)) if c.targetRecipients.exists(_.contains(nodeId)) => + case Success(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage)) if c.targetRecipients.flatMap(_.nodeIds).contains(nodeId) => // if destination node returns an error, we fail the payment immediately log.warning(s"received an error message from target nodeId=$nodeId, failing the payment (failure=$failureMessage)") myStop(c, Left(PaymentFailed(id, paymentHash, failures :+ RemoteFailure(d.c.amount, cfg.fullRoute(route), e)))) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala index 6b91e5ef12..8efe385a52 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala @@ -123,7 +123,7 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I // with finalCltvExpiry val externalId2 = "487da196-a4dc-4b1e-92b4-3e5e905e9f3f" - val invoice2 = Bolt11Invoice("lntb", Some(123 msat), TimestampSecond.now(), nodePrivKey.publicKey, List(Bolt11Invoice.MinFinalCltvExpiry(96), Bolt11Invoice.PaymentHash(ByteVector32.Zeroes), Bolt11Invoice.Description("description")), ByteVector.empty) + val invoice2 = Bolt11Invoice("lntb", Some(123 msat), TimestampSecond.now(), nodePrivKey.publicKey, List(Bolt11Invoice.MinFinalCltvExpiry(96), Bolt11Invoice.PaymentHash(ByteVector32.Zeroes), Bolt11Invoice.Description("description"), Bolt11Invoice.PaymentSecret(ByteVector32(hex"abababababababababababababababababababababababababababababababab"))), ByteVector.empty) eclair.send(Some(externalId2), 123 msat, invoice2) val send2 = paymentInitiator.expectMsgType[SendPaymentToNode] assert(send2.externalId.contains(externalId2)) @@ -314,7 +314,7 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I val secret = randomBytes32() val pr = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(1234 msat), ByteVector32.One, randomKey(), Right(randomBytes32()), CltvExpiryDelta(18)) eclair.sendToRoute(1000 msat, Some(1200 msat), Some("42"), Some(parentId), pr, route, Some(secret), Some(100 msat), Some(CltvExpiryDelta(144)), trampolines) - paymentInitiator.expectMsg(SendPaymentToRoute(1000 msat, 1200 msat, pr, route, Some("42"), Some(parentId), Some(secret), 100 msat, CltvExpiryDelta(144), trampolines)) + paymentInitiator.expectMsg(SendTrampolinePaymentToRoute(1000 msat, 1200 msat, pr, route, Some("42"), Some(parentId), Some(secret), 100 msat, CltvExpiryDelta(144), trampolines)) } test("call sendWithPreimage, which generates a random preimage, to perform a KeySend payment") { f => diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala index 2f42dfd8ff..724d7a3050 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala @@ -34,7 +34,7 @@ import fr.acinq.eclair.payment._ import fr.acinq.eclair.payment.receive.MultiPartHandler.ReceiveStandardPayment import fr.acinq.eclair.payment.receive.PaymentHandler import fr.acinq.eclair.payment.relay.Relayer -import fr.acinq.eclair.router.Router.ChannelHop +import fr.acinq.eclair.router.Router.{ChannelHop, Route} import fr.acinq.eclair.wire.protocol._ import grizzled.slf4j.Logging import org.scalatest.funsuite.FixtureAnyFunSuiteLike @@ -118,19 +118,19 @@ class FuzzySpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Channe // we don't want to be below htlcMinimumMsat val requiredAmount = 1000000 msat - def buildCmdAdd(paymentHash: ByteVector32, dest: PublicKey, paymentSecret: ByteVector32): CMD_ADD_HTLC = { + def buildCmdAdd(paymentHash: ByteVector32, recipient: ClearRecipient): CMD_ADD_HTLC = { // allow overpaying (no more than 2 times the required amount) val amount = requiredAmount + Random.nextInt(requiredAmount.toLong.toInt).msat val expiry = (Channel.MIN_CLTV_EXPIRY_DELTA + 1).toCltvExpiry(currentBlockHeight = BlockHeight(400000)) - OutgoingPaymentPacket.buildCommand(randomKey(), self, Upstream.Local(UUID.randomUUID()), paymentHash, ChannelHop(null, null, dest, null) :: Nil, None, amount, amount, expiry, paymentSecret, None, Nil, Nil).get._1 + OutgoingPaymentPacket.buildCommand(randomKey(), self, Upstream.Local(UUID.randomUUID()), paymentHash, Route(amount, ChannelHop(null, null, recipient.nodeId, null) :: Nil, recipient), amount, amount, expiry).get._1 } def initiatePaymentOrStop(remaining: Int): Unit = if (remaining > 0) { paymentHandler ! ReceiveStandardPayment(Some(requiredAmount), Left("One coffee")) context become { - case req: Invoice => - sendChannel ! buildCmdAdd(req.paymentHash, req.nodeId, req.paymentSecret.get) + case req: Bolt11Invoice => + sendChannel ! buildCmdAdd(req.paymentHash, req.recipient) context become { case RES_SUCCESS(_: CMD_ADD_HTLC, _) => () case RES_ADD_SETTLED(_, htlc, _: HtlcResult.Fulfill) => diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala index beacc1013c..212f3fdb19 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala @@ -33,9 +33,9 @@ import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.publish.TxPublisher import fr.acinq.eclair.channel.publish.TxPublisher.PublishReplaceableTx import fr.acinq.eclair.channel.states.ChannelStateTestsBase.FakeTxPublisherFactory -import fr.acinq.eclair.payment.OutgoingPaymentPacket +import fr.acinq.eclair.payment.{ClearRecipient, OutgoingPaymentPacket} import fr.acinq.eclair.payment.OutgoingPaymentPacket.Upstream -import fr.acinq.eclair.router.Router.ChannelHop +import fr.acinq.eclair.router.Router.{ChannelHop, Route} import fr.acinq.eclair.transactions.Transactions import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.wire.protocol._ @@ -357,7 +357,7 @@ trait ChannelStateTestsBase extends Assertions with Eventually { def makeCmdAdd(amount: MilliSatoshi, cltvExpiryDelta: CltvExpiryDelta, destination: PublicKey, paymentPreimage: ByteVector32, currentBlockHeight: BlockHeight, upstream: Upstream, replyTo: ActorRef = TestProbe().ref): (ByteVector32, CMD_ADD_HTLC) = { val paymentHash: ByteVector32 = Crypto.sha256(paymentPreimage) val expiry = cltvExpiryDelta.toCltvExpiry(currentBlockHeight) - val cmd = OutgoingPaymentPacket.buildCommand(randomKey(), replyTo, upstream, paymentHash, ChannelHop(null, null, destination, null) :: Nil, None, amount, amount, expiry, randomBytes32(), None, Nil, Nil).get._1.copy(commit = false) + val cmd = OutgoingPaymentPacket.buildCommand(randomKey(), replyTo, upstream, paymentHash, Route(amount, ChannelHop(null, null, destination, null) :: Nil, ClearRecipient(destination, randomBytes32(), None)), amount, amount, expiry).get._1.copy(commit = false) (paymentPreimage, cmd) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala index 4b51ad2f2d..1eb1d45724 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala @@ -29,7 +29,7 @@ import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsT import fr.acinq.eclair.payment.OutgoingPaymentPacket.Upstream import fr.acinq.eclair.payment._ import fr.acinq.eclair.payment.relay.Relayer._ -import fr.acinq.eclair.router.Router.ChannelHop +import fr.acinq.eclair.router.Router.{ChannelHop, Route} import fr.acinq.eclair.wire.protocol.{ClosingSigned, CommitSig, Error, FailureMessageCodecs, PaymentOnion, PermanentChannelFailure, RevokeAndAck, Shutdown, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFee, UpdateFulfillHtlc} import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, MilliSatoshiLong, TestConstants, TestKitBaseClass, randomBytes32} import org.scalatest.funsuite.FixtureAnyFunSuiteLike @@ -60,7 +60,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit val h1 = Crypto.sha256(r1) val amount1 = 300000000 msat val expiry1 = CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight) - val cmd1 = OutgoingPaymentPacket.buildCommand(TestConstants.Alice.nodeParams.privateKey, sender.ref, Upstream.Local(UUID.randomUUID), h1, ChannelHop(null, null, TestConstants.Bob.nodeParams.nodeId, null) :: Nil, None, amount1, amount1, expiry1, randomBytes32(), None, Nil, Nil).get._1.copy(commit = false) + val cmd1 = OutgoingPaymentPacket.buildCommand(TestConstants.Alice.nodeParams.privateKey, sender.ref, Upstream.Local(UUID.randomUUID), h1, Route(amount1, ChannelHop(null, null, TestConstants.Bob.nodeParams.nodeId, null) :: Nil, ClearRecipient(TestConstants.Bob.nodeParams.nodeId, randomBytes32(), None)), amount1, amount1, expiry1).get._1.copy(commit = false) alice ! cmd1 sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]] val htlc1 = alice2bob.expectMsgType[UpdateAddHtlc] @@ -70,7 +70,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit val h2 = Crypto.sha256(r2) val amount2 = 200000000 msat val expiry2 = CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight) - val cmd2 = OutgoingPaymentPacket.buildCommand(TestConstants.Alice.nodeParams.privateKey, sender.ref, Upstream.Local(UUID.randomUUID), h2, ChannelHop(null, null, TestConstants.Bob.nodeParams.nodeId, null) :: Nil, None, amount2, amount2, expiry2, randomBytes32(), None, Nil, Nil).get._1.copy(commit = false) + val cmd2 = OutgoingPaymentPacket.buildCommand(TestConstants.Alice.nodeParams.privateKey, sender.ref, Upstream.Local(UUID.randomUUID), h2, Route(amount2, ChannelHop(null, null, TestConstants.Bob.nodeParams.nodeId, null) :: Nil, ClearRecipient(TestConstants.Bob.nodeParams.nodeId, randomBytes32(), None)), amount2, amount2, expiry2).get._1.copy(commit = false) alice ! cmd2 sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]] val htlc2 = alice2bob.expectMsgType[UpdateAddHtlc] diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala index 996849019a..4c15f8643e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala @@ -160,7 +160,7 @@ class PaymentIntegrationSpec extends IntegrationSpec { // first we retrieve a payment hash from D val amountMsat = 4200000.msat sender.send(nodes("D").paymentHandler, ReceiveStandardPayment(Some(amountMsat), Left("1 coffee"))) - val invoice = sender.expectMsgType[Invoice] + val invoice = sender.expectMsgType[Bolt11Invoice] assert(invoice.paymentMetadata.nonEmpty) // then we make the actual payment @@ -462,7 +462,7 @@ class PaymentIntegrationSpec extends IntegrationSpec { val sender = TestProbe() val amount = 4000000000L.msat sender.send(nodes("F").paymentHandler, ReceiveStandardPayment(Some(amount), Left("like trampoline much?"))) - val invoice = sender.expectMsgType[Invoice] + val invoice = sender.expectMsgType[Bolt11Invoice] assert(invoice.features.hasFeature(Features.BasicMultiPartPayment)) assert(invoice.features.hasFeature(Features.TrampolinePaymentPrototype)) @@ -507,7 +507,7 @@ class PaymentIntegrationSpec extends IntegrationSpec { nodes("B").system.eventStream.subscribe(eventListener.ref, classOf[PaymentMetadataReceived]) val amount = 2500000000L.msat sender.send(nodes("B").paymentHandler, ReceiveStandardPayment(Some(amount), Left("trampoline-MPP is so #reckless"))) - val invoice = sender.expectMsgType[Invoice] + val invoice = sender.expectMsgType[Bolt11Invoice] assert(invoice.features.hasFeature(Features.BasicMultiPartPayment)) assert(invoice.features.hasFeature(Features.TrampolinePaymentPrototype)) assert(invoice.paymentMetadata.nonEmpty) @@ -563,7 +563,7 @@ class PaymentIntegrationSpec extends IntegrationSpec { val amount = 3000000000L.msat sender.send(nodes("A").paymentHandler, ReceiveStandardPayment(Some(amount), Left("trampoline to non-trampoline is so #vintage"), extraHops = routingHints)) - val invoice = sender.expectMsgType[Invoice] + val invoice = sender.expectMsgType[Bolt11Invoice] assert(invoice.features.hasFeature(Features.BasicMultiPartPayment)) assert(!invoice.features.hasFeature(Features.TrampolinePaymentPrototype)) assert(invoice.paymentMetadata.nonEmpty) @@ -611,7 +611,7 @@ class PaymentIntegrationSpec extends IntegrationSpec { // Now we try to send more than C's outgoing capacity to D. val amount = 2000000000L.msat sender.send(nodes("D").paymentHandler, ReceiveStandardPayment(Some(amount), Left("I iz Satoshi"))) - val invoice = sender.expectMsgType[Invoice] + val invoice = sender.expectMsgType[Bolt11Invoice] assert(invoice.features.hasFeature(Features.BasicMultiPartPayment)) assert(invoice.features.hasFeature(Features.TrampolinePaymentPrototype)) @@ -632,7 +632,7 @@ class PaymentIntegrationSpec extends IntegrationSpec { val sender = TestProbe() val amount = 2000000000L.msat // B can forward to C, but C doesn't have that much outgoing capacity to D sender.send(nodes("D").paymentHandler, ReceiveStandardPayment(Some(amount), Left("I iz not Satoshi"))) - val invoice = sender.expectMsgType[Invoice] + val invoice = sender.expectMsgType[Bolt11Invoice] assert(invoice.features.hasFeature(Features.BasicMultiPartPayment)) assert(invoice.features.hasFeature(Features.TrampolinePaymentPrototype)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt11InvoiceSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt11InvoiceSpec.scala index e7f734a834..341f15ae47 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt11InvoiceSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt11InvoiceSpec.scala @@ -137,7 +137,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite { assert(invoice.prefix == "lnbc") assert(invoice.amount_opt.isEmpty) assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") - assert(invoice.paymentSecret.map(_.bytes).contains(hex"1111111111111111111111111111111111111111111111111111111111111111")) + assert(invoice.paymentSecret.bytes == hex"1111111111111111111111111111111111111111111111111111111111111111") assert(invoice.features == Features(Features.VariableLengthOnion -> Mandatory, Features.PaymentSecret -> Mandatory)) assert(invoice.createdAt == TimestampSecond(1496314658L)) assert(invoice.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) @@ -317,7 +317,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite { assert(invoice.prefix == "lnbc") assert(invoice.amount_opt.contains(2500000000L msat)) assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") - assert(invoice.paymentSecret.contains(ByteVector32(hex"1111111111111111111111111111111111111111111111111111111111111111"))) + assert(invoice.paymentSecret.bytes == hex"1111111111111111111111111111111111111111111111111111111111111111") assert(invoice.createdAt == TimestampSecond(1496314658L)) assert(invoice.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) assert(invoice.description == Left("coffee beans")) @@ -336,7 +336,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite { assert(invoice.prefix == "lnbc") assert(invoice.amount_opt.contains(2500000000L msat)) assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") - assert(invoice.paymentSecret.contains(ByteVector32(hex"1111111111111111111111111111111111111111111111111111111111111111"))) + assert(invoice.paymentSecret.bytes == hex"1111111111111111111111111111111111111111111111111111111111111111") assert(invoice.createdAt == TimestampSecond(1496314658L)) assert(invoice.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) assert(invoice.description == Left("coffee beans")) @@ -376,7 +376,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite { assert(invoice.features == Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, PaymentMetadata -> Mandatory)) assert(invoice.createdAt == TimestampSecond(1496314658L)) assert(invoice.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) - assert(invoice.paymentSecret.contains(ByteVector32(hex"1111111111111111111111111111111111111111111111111111111111111111"))) + assert(invoice.paymentSecret.bytes == hex"1111111111111111111111111111111111111111111111111111111111111111") assert(invoice.description == Left("payment metadata inside")) assert(invoice.paymentMetadata.contains(hex"01fafaf0")) assert(invoice.tags.size == 5) @@ -430,6 +430,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite { nodeId = nodeId, tags = List( PaymentHash(ByteVector32(ByteVector.fill(32)(1))), + Bolt11Invoice.PaymentSecret(randomBytes32()), Description("description"), UnknownTag21(BitVector("some data we don't understand".getBytes)) ), @@ -525,16 +526,14 @@ class Bolt11InvoiceSpec extends AnyFunSuite { test("payment secret") { val invoice = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("Some invoice"), CltvExpiryDelta(18)) - assert(invoice.paymentSecret.isDefined) assert(invoice.features == Features(PaymentSecret -> Mandatory, VariableLengthOnion -> Mandatory)) assert(invoice.features.hasFeature(PaymentSecret, Some(Mandatory))) val Success(pr1) = Bolt11Invoice.fromString(invoice.toString) assert(pr1.paymentSecret == invoice.paymentSecret) - val Success(pr2) = Bolt11Invoice.fromString("lnbc40n1pw9qjvwpp5qq3w2ln6krepcslqszkrsfzwy49y0407hvks30ec6pu9s07jur3sdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdencqzysxqrrss7ju0s4dwx6w8a95a9p2xc5vudl09gjl0w2n02sjrvffde632nxwh2l4w35nqepj4j5njhh4z65wyfc724yj6dn9wajvajfn5j7em6wsq2elakl") - assert(!pr2.features.hasFeature(PaymentSecret, Some(Mandatory))) - assert(pr2.paymentSecret.isEmpty) + // An invoice must always provide a payment secret. + assert(Bolt11Invoice.fromString("lnbc40n1pw9qjvwpp5qq3w2ln6krepcslqszkrsfzwy49y0407hvks30ec6pu9s07jur3sdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdencqzysxqrrss7ju0s4dwx6w8a95a9p2xc5vudl09gjl0w2n02sjrvffde632nxwh2l4w35nqepj4j5njhh4z65wyfc724yj6dn9wajvajfn5j7em6wsq2elakl").isFailure) // An invoice that sets the payment secret feature bit must provide a payment secret. assert(Bolt11Invoice.fromString("lnbc1230p1pwljzn3pp5qyqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqdq52dhk6efqd9h8vmmfvdjs9qypqsqylvwhf7xlpy6xpecsnpcjjuuslmzzgeyv90mh7k7vs88k2dkxgrkt75qyfjv5ckygw206re7spga5zfd4agtdvtktxh5pkjzhn9dq2cqz9upw7").isFailure) @@ -557,69 +556,69 @@ class Bolt11InvoiceSpec extends AnyFunSuite { assert(pr2.features.hasFeature(BasicMultiPartPayment)) assert(pr2.features.hasFeature(TrampolinePaymentPrototype)) - val Success(pr3) = Bolt11Invoice.fromString("lnbc40n1pw9qjvwpp5qq3w2ln6krepcslqszkrsfzwy49y0407hvks30ec6pu9s07jur3sdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdencqzysxqrrss7ju0s4dwx6w8a95a9p2xc5vudl09gjl0w2n02sjrvffde632nxwh2l4w35nqepj4j5njhh4z65wyfc724yj6dn9wajvajfn5j7em6wsq2elakl") + val Success(pr3) = Bolt11Invoice.fromString("lnbc40n1pw9qjvwsp5ss9r82afylrju4zw8ttmpm29gku9szfxafr09g5x8tmhn5wdrekspp5qq3w2ln6krepcslqszkrsfzwy49y0407hvks30ec6pu9s07jur3sdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdencqzysxqrrss7ju0s4dwx6w8a95a9p2xc5vudl09gjl0w2n02sjrvffde632nxwh2l4w35nqepj4j5njhh4z65wyfc724yj6dn9wajvajfn5j7em6wsqunpauw") assert(!pr3.features.hasFeature(TrampolinePaymentPrototype)) } test("nonreg") { val requests = List( - "lnbc40n1pw9qjvwpp5qq3w2ln6krepcslqszkrsfzwy49y0407hvks30ec6pu9s07jur3sdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdencqzysxqrrss7ju0s4dwx6w8a95a9p2xc5vudl09gjl0w2n02sjrvffde632nxwh2l4w35nqepj4j5njhh4z65wyfc724yj6dn9wajvajfn5j7em6wsq2elakl" -> PublicKey(hex"02cda8c01b2303e91bec74c43093d5f1c4fd42a95671ae27bf853d7dfea9b78c06"), - "lnbc1500n1pwyvqwfpp5p5nxwpuk02nd2xtzwex97gtjlpdv0lxj5z08vdd0hes7a0h437qsdpa2fjkzep6yp8kumrfdejjqempd43xc6twvusxjueqd9kxcet8v9kzqct8v95kucqzysxqr23s8r9seqv6datylwtjcvlpdkukfep7g80hujz3w8t599saae7gap6j48gs97z4fvrx4t4ajra6pvdyf5ledw3tg7h2s3606qm79kk59zqpeygdhd" -> PublicKey(hex"03e50492eab4107a773141bb419e107bda3de3d55652e6e1a41225f06a0bbf2d56"), - "lnbc800n1pwykdmfpp5zqjae54l4ecmvm9v338vw2n07q2ehywvy4pvay53s7068t8yjvhqdqddpjkcmr0yysjzcqp27lya2lz7d80uxt6vevcwzy32227j3nsgyqlrxuwgs22u6728ldszlc70qgcs56wglrutn8jnnnelsk38d6yaqccmw8kmmdlfsyjd20qp69knex" -> PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), - "lnbc300n1pwzezrnpp5zgwqadf4zjygmhf3xms8m4dd8f4mdq26unr5mfxuyzgqcgc049tqdq9dpjhjcqp23gxhs2rawqxdvr7f7lmj46tdvkncnsz8q5jp2kge8ndfm4dpevxrg5xj4ufp36x89gmaw04lgpap7e3x9jcjydwhcj9l84wmts2lg6qquvpque" -> PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), - "lnbc10n1pdm2qaxpp5zlyxcc5dypurzyjamt6kk6a8rpad7je5r4w8fj79u6fktnqu085sdpl2pshjmt9de6zqen0wgsrzgrsd9ux2mrnypshggrnv96x7umgd9ejuurvv93k2tsxqzjccqp2e3nq4xh20prn9kx8etqgjjekzzjhep27mnqtyy62makh4gqc4akrzhe3nmj8lnwtd40ne5gn8myruvrt9p6vpuwmc4ghk7587erwqncpx9sds0" -> PublicKey(hex"024655b768ef40951b20053a5c4b951606d4d86085d51238f2c67c7dec29c792ca"), - "lnbc800n1pwp5uuhpp5y8aarm9j9x9cer0gah9ymfkcqq4j4rn3zr7y9xhsgar5pmaceaqqdqdvf5hgcm0d9hzzcqp2vf8ramzsgdznxd5yxhrxffuk43pst9ng7cqcez9p2zvykpcf039rp9vutpe6wfds744yr73ztyps2z58nkmflye9yt4v3d0qz8z3d9qqq3kv54" -> PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), - "lnbc1500n1pdl686hpp5y7mz3lgvrfccqnk9es6trumjgqdpjwcecycpkdggnx7h6cuup90sdpa2fjkzep6ypqkymm4wssycnjzf9rjqurjda4x2cm5ypskuepqv93x7at5ypek7cqzysxqr23s5e864m06fcfp3axsefy276d77tzp0xzzzdfl6p46wvstkeqhu50khm9yxea2d9efp7lvthrta0ktmhsv52hf3tvxm0unsauhmfmp27cqqx4xxe" -> PublicKey(hex"03e50492eab4107a773141bb419e107bda3de3d55652e6e1a41225f06a0bbf2d56"), - "lnbc80n1pwykw99pp5965lyj4uesussdrk0lfyd2qss9m23yjdjkpmhw0975zky2xlhdtsdpl2pshjmt9de6zqen0wgsrsgrsd9ux2mrnypshggrnv96x7umgd9ejuurvv93k2tsxqzjccqp27677yc44l22jxexewew7lzka7g5864gdpr6y5v6s6tqmn8xztltk9qnna2qwrsm7gfyrpqvhaz4u3egcalpx2gxef3kvqwd44hekfxcqr7nwhf" -> PublicKey(hex"024655b768ef40951b20053a5c4b951606d4d86085d51238f2c67c7dec29c792ca"), - "lnbc2200n1pwp4pwnpp5xy5f5kl83ytwuz0sgyypmqaqtjs68s3hrwgnnt445tqv7stu5kyqdpyvf5hgcm0d9hzqmn0wssxymr0vd4kx6rpd9hqcqp25y9w3wc3ztxhemsqch640g4u00szvvfk4vxr7klsakvn8cjcunjq8rwejzy6cfwj90ulycahnq43lff8m84xqf3tusslq2w69htwwlcpfqskmc" -> PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), - "lnbc300n1pwp50ggpp5x7x5a9zs26amr7rqngp2sjee2c3qc80ztvex00zxn7xkuzuhjkxqdq9dpjhjcqp2s464vnrpx7aynh26vsxx6s3m52x88dqen56pzxxnxmc9s7y5v0dprsdlv5q430zy33lcl5ll6uy60m7c9yrkjl8yxz7lgsqky3ka57qq4qeyz3" -> PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), - "lnbc10n1pd6jt93pp58vtzf4gup4vvqyknfakvh59avaek22hd0026snvpdnc846ypqrdsdp0tfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqcnqvscqzysxqyd9uq3sv9xkv2sgdf2nuvs97d2wkzj5g75rljnh5wy5wqhnauvqhxd9fpq898emtz8hul8cnxmc9wtj2777ehgnnyhcrs0y5zuhy8rs0jv6cqqe24tw" -> PublicKey(hex"03a9d79bcfab7feb0f24c3cd61a57f0f00de2225b6d31bce0bc4564efa3b1b5aaf"), - "lnbc890n1pwzu4uqpp5gy274lq0m5hzxuxy90vf65wchdszrazz9zxjdk30ed05kyjvwxrqdzq2pshjmt9de6zqen0wgsrswfqwp5hsetvwvsxzapqwdshgmmndp5hxtnsd3skxefwxqzjccqp2qjvlfyl4rmc56gerx70lxcrjjlnrjfz677ezw4lwzy6syqh4rnlql6t6n3pdfxkcal9jp98plgf2zqzz8jxfza9vjw3vd4t62ws8gkgqhv9x28" -> PublicKey(hex"024655b768ef40951b20053a5c4b951606d4d86085d51238f2c67c7dec29c792ca"), - "lnbc79760n1pd7cwyapp5gevl4mv968fs4le3tytzhr9r8tdk8cu3q7kfx348ut7xyntvnvmsdz92pskjepqw3hjqmrfva58gmnfdenjqumvda6zqmtpvd5xjmn9ypnx7u3qx5czq5msd9h8xcqzysxqrrssjzky68fdnhvee7aw089d5zltahfhy2ffa96pwf7fszjnm6mv0fzpv88jwaenm5qfg64pl768q8hf2vnvc5xsrpqd45nca2mewsv55wcpmhskah" -> PublicKey(hex"039f01ad62e5208940faff11d0bbc997582eafad7642aaf53de6a5f6551ab73400"), - "lnbc90n1pduns5qpp5f5h5ghga4cp7uj9de35ksk00a2ed9jf774zy7va37k5zet5cds8sdpl2pshjmt9de6zqen0wgsrjgrsd9ux2mrnypshggrnv96x7umgd9ejuurvv93k2tsxqzjccqp28ynysm3clcq865y9umys8t2f54anlsu2wfpyfxgq09ht3qfez9x9z9fpff8wzqwzua2t9vayzm4ek3vf4k4s5cdg3a6hp9vsgg9klpgpmafvnv" -> PublicKey(hex"024655b768ef40951b20053a5c4b951606d4d86085d51238f2c67c7dec29c792ca"), - "lnbc10u1pw9nehppp5tf0cpc3nx3wpk6j2n9teqwd8kuvryh69hv65w7p5u9cqhse3nmgsdzz2p6hycmgv9ek2gr0vcsrzgrxd3hhwetjyphkugzzd96xxmmfdcsywunpwejhjctjvscqp222vxxwq70temepf6n0xlzk0asr43ppqrt0mf6eclnfd5mxf6uhv5wvsqgdvht6uqxfw2vgdku5gfyhgguepvnjfu7s4kuthtnuxy0hsq6wwv9d" -> PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), - "lnbc30n1pw9qjwmpp5tcdc9wcr0avr5q96jlez09eax7djwmc475d5cylezsd652zvptjsdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdf4cqzysxqrrss7r8gn9d6klf2urzdjrq3x67a4u25wpeju5utusnc539aj5462y7kv9w56mndcx8jad7aa7qz8f8qpdw9qlx52feyemwd7afqxu45jxsqyzwns9" -> PublicKey(hex"02cda8c01b2303e91bec74c43093d5f1c4fd42a95671ae27bf853d7dfea9b78c06"), - "lnbc10u1pw9x36xpp5tlk00k0dftfx9vh40mtdlu844c9v65ad0kslnrvyuzfxqhdur46qdzz2p6hycmgv9ek2gr0vcsrzgrxd3hhwetjyphkugzzd96xxmmfdcsywunpwejhjctjvscqp2fpudmf4tt0crardf0k7vk5qs4mvys88el6e7pg62hgdt9t6ckf48l6jh4ckp87zpcnal6xnu33hxdd8k27vq2702688ww04kc065r7cqw3cqs3" -> PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), - "lnbc40n1pd6jttkpp5v8p97ezd3uz4ruw4w8w0gt4yr3ajtrmaeqe23ttxvpuh0cy79axqdp0tfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqcnqvscqzysxqyd9uq3r88ajpz77z6lg4wc7srhsk7m26guuvhdlpea6889m9jnc9a25sx7rdtryjukew86mtcngl6d8zqh9trtu60cmmwfx6845q08z06p6qpl3l55t" -> PublicKey(hex"03a9d79bcfab7feb0f24c3cd61a57f0f00de2225b6d31bce0bc4564efa3b1b5aaf"), - "lnbc1pwr7fqhpp5vhur3ahtumqz5mkramxr22597gaa9rnrjch8gxwr9h7r56umsjpqdpl235hqurfdcs9xct5daeks6tngask6etnyq58g6tswp5kutndv55jsaf3x5unj2gcqzysxqyz5vq88jysqvrwhq6qe38jdulefx0z9j7sfw85wqc6athfx9h77fjnjxjvprz76ayna0rcjllgu5ka960rul3qxvsrr9zth5plaerq96ursgpsshuee" -> PublicKey(hex"03c2abfa93eacec04721c019644584424aab2ba4dff3ac9bdab4e9c97007491dda"), - "lnbc10n1pw9rt5hpp5dsv5ux7xlmhmrpqnffgj6nf03mvx5zpns3578k2c5my3znnhz0gqdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwwp3cqzysxqrrssnrasvcr5ydng283zdfpw38qtqfxjnzhdmdx9wly9dsqmsxvksrkzkqrcenu6h36g4g55q56ejk429nm4zjfgssh8uhs7gs760z63ggcqp3gyd6" -> PublicKey(hex"02cda8c01b2303e91bec74c43093d5f1c4fd42a95671ae27bf853d7dfea9b78c06"), - "lnbc1500n1pd7u7p4pp5d54vffcehkcy79gm0fkqrthh3y576jy9flzpy9rf6syua0s5p0jqdpa2fjkzep6ypxhjgz90pcx2unfv4hxxefqdanzqargv5s9xetrdahxggzvd9nkscqzysxqr23sklptztnk25aqzwty35gk9q7jtfzjywdfx23d8a37g2eaejrv3d9nnt87m98s4eps87q87pzfd6hkd077emjupe0pcazpt9kaphehufqqu7k37h" -> PublicKey(hex"03e50492eab4107a773141bb419e107bda3de3d55652e6e1a41225f06a0bbf2d56"), - "lnbc10n1pdunsmgpp5wn90mffjvkd06pe84lpa6e370024wwv7xfw0tdxlt6qq8hc7d7rqdp0tfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqcngvscqzysxqyd9uqs0cqtrum6h7dct88nkjxwxvte7hjh9pusx64tp35u0m6qhqy5dgn9j27fs37mg0w3ruf7enxlsc9xmlasgjzyyaaxqdxu9x5w0md4fspgz8twv" -> PublicKey(hex"03a9d79bcfab7feb0f24c3cd61a57f0f00de2225b6d31bce0bc4564efa3b1b5aaf"), - "lnbc700n1pwp50wapp5w7eearwr7qjhz5vk5zq4g0t75f90mrekwnw4e795qfjxyaq27dxsdqvdp6kuar9wgeqcqp20gfw78vvasjm45l6zfxmfwn59ac9dukp36mf0y3gpquhp7rptddxy7d32ptmqukeghvamlkmve9n94sxmxglun4zwtkyhk43e6lw8qspc9y9ww" -> PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), - "lnbc10n1pd6jvy5pp50x9lymptter9najcdpgrcnqn34wq34f49vmnllc57ezyvtlg8ayqdpdtfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yq6rvcqzysxqyd9uqcejk56vfz3y80u3npefpx82f0tghua88a8x2d33gmxcjm45q6l5xwurwyp9aj2p59cr0lknpk0eujfdax32v4px4m22u6zr5z40zxvqp5m85cr" -> PublicKey(hex"03a9d79bcfab7feb0f24c3cd61a57f0f00de2225b6d31bce0bc4564efa3b1b5aaf"), - "lnbc10n1pw9pqz7pp50782e2u9s25gqacx7mvnuhg3xxwumum89dymdq3vlsrsmaeeqsxsdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwd3ccqzysxqrrsstxqhw2kvdfwsf7c27aaae45fheq9rzndesu4mph9dq08sawa0auz7e0z7jn9qf3zphegv2ermup0fgce0phqmf73j4zx88v3ksrgeeqq9yzzad" -> PublicKey(hex"02cda8c01b2303e91bec74c43093d5f1c4fd42a95671ae27bf853d7dfea9b78c06"), - "lnbc1300n1pwq4fx7pp5sqmq97yfxhhk7xv7u8cuc8jgv5drse45f5pmtx6f5ng2cqm332uqdq4e2279q9zux62tc5q5t9fgcqp29a662u3p2h4h4ucdav4xrlxz2rtwvvtward7htsrldpsc5erknkyxu0x2xt9qv0u766jadeetsz9pj4rljpjy0g8ayqqt2q8esewsrqpc8v4nw" -> PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), - "lnbc1u1pd7u7tnpp5s9he3ccpsmfdkzrsjns7p3wpz7veen6xxwxdca3khwqyh2ezk8kqdqdg9jxgg8sn7f27cqzysxqr23ssm4krdc4s0zqhfk97n0aclxsmaga208pa8c0hz3zyauqsjjxfj7kw6t29dkucp68s8s4zfdgp97kkmzgy25yuj0dcec85d9c50sgjqgq5jhl4e" -> PublicKey(hex"03e50492eab4107a773141bb419e107bda3de3d55652e6e1a41225f06a0bbf2d56"), - "lnbc1200n1pwq5kf2pp5snkm9kr0slgzfc806k4c8q93d4y57q3lz745v2hefx952rhuymrqdq509shjgrzd96xxmmfdcsscqp2w5ta9uwzhmxxp0mnhwwvnjdn6ev4huj3tha5d80ajv2p5phe8wk32yn7ch6lennx4zzawqtd34aqetataxjmrz39gzjl256walhw03gpxz79rr" -> PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), - "lnbc1500n1pd7u7v0pp5s6d0wqexag3aqzugaw3gs7hw7a2wrq6l8fh9s42ndqu8zu480m0sdqvg9jxgg8zn2sscqzysxqr23sm23myatjdsp3003rlasgzwg3rlr0ca8uqdt5d79lxmdwqptufr89r5rgk4np4ag0kcw7at6s6eqdany0k6m0ezjva0cyda5arpaw7lcqgzjl7u" -> PublicKey(hex"03e50492eab4107a773141bb419e107bda3de3d55652e6e1a41225f06a0bbf2d56"), - "lnbc100n1pd6jv8ypp53p6fdd954h3ffmyj6av4nzcnwfuyvn9rrsc2u6y22xnfs0l0cssqdpdtfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqerscqzysxqyd9uqyefde4la0qmglafzv8q34wqsf4mtwd8ausufavkp2e7paewd3mqsg0gsdmvrknw80t92cuvu9raevrnxtpsye0utklhpunsz68a9veqpkypx9j" -> PublicKey(hex"03a9d79bcfab7feb0f24c3cd61a57f0f00de2225b6d31bce0bc4564efa3b1b5aaf"), - "lnbc2300n1pwp50w8pp53030gw8rsqac6f3sqqa9exxwfvphsl4v4w484eynspwgv5v6vyrsdp9w35xjueqd9ejqmn0wssx67fqwpshxumhdaexgcqp2zmspcx992fvezxqkyf3rkcxc9dm2vr4ewfx42c0fccg4ea72fyd3pd6vn94tfy9t39y0hg0hupak2nv0n6pzy8culeceq8kzpwjy0tsp4fwqw5" -> PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), - "lnbc10n1pwykdlhpp53392ama65h3lnc4w55yqycp9v2ackexugl0ahz4jyc7fqtyuk85qdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwvejcqzysxqrrsszkwrx54an8lhr9h4h3d7lgpjrd370zucx0fdusaklqh2xgytr8hhgq5u0kvs56l8j53uktlmz3mqhhmn88kwwxfksnham9p6ws5pwxsqnpzyda" -> PublicKey(hex"03a9d79bcfab7feb0f24c3cd61a57f0f00de2225b6d31bce0bc4564efa3b1b5aaf"), - "lnbc10470n1pw9qf40pp535pels2faqwau2rmqkgzn0rgtsu9u6qaxe5y6ttgjx5qm4pg0kgsdzy2pshjmt9de6zqen0wgsrzvp5xus8q6tcv4k8xgrpwss8xct5daeks6tn9ecxcctrv5hqxqzjccqp27sp3m204a7d47at5jkkewa7rvewdmpwaqh2ss72cajafyf7dts9ne67hw9pps2ud69p4fw95y9cdk35aef43cv35s0zzj37qu7s395cp2vw5mu" -> PublicKey(hex"024655b768ef40951b20053a5c4b951606d4d86085d51238f2c67c7dec29c792ca"), - "lnbc100n1pwytlgspp5365rx7ell807x5jsr7ykf2k7p5z77qvxjx8x6pfhh5298xnr6d2sdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwvpscqzysxqrrssh9mphycg7e9lr58c267yerlcd9ka8lrljm8ygpnwu2v63jm7ax48y7qal25qy0ewpxw39r5whnqh93zw97gnnw64ss97n69975wh9gsqj7vudu" -> PublicKey(hex"03a9d79bcfab7feb0f24c3cd61a57f0f00de2225b6d31bce0bc4564efa3b1b5aaf"), - "lnbc210n1pdunsefpp5jxn3hlj86evlwgsz5d70hquy78k28ahdwjmlagx6qly9x29pu4uqdzq2pshjmt9de6zqen0wgsryvfqwp5hsetvwvsxzapqwdshgmmndp5hxtnsd3skxefwxqzjccqp2snr8trjcrr5xyy7g63uq7mewqyp9k3d0duznw23zhynaz6pj3uwk48yffqn8p0jugv2z03dxquc8azuwr8myjgwzh69a34fl2lnmq2sppac733" -> PublicKey(hex"024655b768ef40951b20053a5c4b951606d4d86085d51238f2c67c7dec29c792ca"), - "lnbc1700n1pwr7z98pp5j5r5q5c7syavjjz7czjvng4y95w0rd8zkl7q43sm7spg9ht2sjfqdquwf6kumnfdenjqmrfva58gmnfdenscqp2jrhlc758m734gw5td4gchcn9j5cp5p38zj3tcpvgkegxewat38d3h24kn0c2ac2pleuqp5dutvw5fmk4d2v3trcqhl5pdxqq8swnldcqtq0akh" -> PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), - "lnbc1500n1pdl05k5pp5nyd9netjpzn27slyj2np4slpmlz8dy69q7hygwm8ff4mey2jee5sdpa2fjkzep6ypxhjgz90pcx2unfv4hxxefqdanzqargv5s9xetrdahxggzvd9nkscqzysxqr23sqdd8t97qjc77pqa7jv7umc499jqkk0kwchapswj3xrukndr7g2nqna5x87n49uynty4pxexkt3fslyle7mwz708rs0rnnn44dnav9mgplf0aj7" -> PublicKey(hex"03e50492eab4107a773141bb419e107bda3de3d55652e6e1a41225f06a0bbf2d56"), - "lnbc1u1pwyvxrppp5nvm98wnqdee838wtfmhfjx9s49eduzu3rx0fqec2wenadth8pxqsdqdg9jxgg8sn7vgycqzysxqr23snuza3t8x0tvusu07epal9rqxh4cq22m64amuzd6x607s0w55a5xpefp2xlxmej9r6nktmwv5td3849y2sg7pckwk9r8vqqps8g4u66qq85mp3g" -> PublicKey(hex"03e50492eab4107a773141bb419e107bda3de3d55652e6e1a41225f06a0bbf2d56"), - "lnbc10n1pw9qjwppp55nx7xw3sytnfle67mh70dyukr4g4chyfmp4x4ag2hgjcts4kydnsdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwd3ccqzysxqrrss7t24v6w7dwtd65g64qcz77clgye7n8l0j67qh32q4jrw9d2dk2444vma7j6nedgx2ywel3e9ns4r257zprsn7t5uca045xxudz9pqzsqfena6v" -> PublicKey(hex"02cda8c01b2303e91bec74c43093d5f1c4fd42a95671ae27bf853d7dfea9b78c06"), - "lnbc10u1pw9x373pp549mpcznu3q0r4ml095kjg38pvsdptzja8vhpyvc2avatc2cegycsdzz2p6hycmgv9ek2gr0vcsrzgrxd3hhwetjyphkugzzd96xxmmfdcsywunpwejhjctjvscqp2tgqwhzyjmpfymrshnaw6rwmy4rgrtjmmp66dr9v54xp52rsyzqd5htc3lu3k52t06fqk8yj05nsw0nnssak3ywev4n3xs3jgz42urmspjeqyw0" -> PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), - "lnbc1500n1pd7u7vupp54jm8s8lmgnnru0ndwpxhm5qwllkrarasr9fy9zkunf49ct8mw9ssdqvg9jxgg8zn2sscqzysxqr23s4njradkzzaswlsgs0a6zc3cd28xc08t5car0k7su6q3u3vjvqt6xq2kpaadgt5x9suxx50rkevfw563fupzqzpc9m6dqsjcr8qt6k2sqelr838" -> PublicKey(hex"03e50492eab4107a773141bb419e107bda3de3d55652e6e1a41225f06a0bbf2d56"), - "lnbc720n1pwypj4epp5k2saqsjznpvevsm9mzqfan3d9fz967x5lp39g3nwsxdkusps73csdzq2pshjmt9de6zqen0wgsrwv3qwp5hsetvwvsxzapqwdshgmmndp5hxtnsd3skxefwxqzjccqp2d3ltxtq0r795emmp7yqjjmmzl55cgju004vw08f83e98d28xmw44t4styhfhgsrwxydf68m2kup7j358zdrmhevqwr0hlqwt2eceaxcq7hezhx" -> PublicKey(hex"024655b768ef40951b20053a5c4b951606d4d86085d51238f2c67c7dec29c792ca"), - "lnbc10n1pwykdacpp5kegv2kdkxmetm2tpnzfgt4640n7mgxl95jnpc6fkz6uyjdwahw8sdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdp5cqzysxqrrssjlny2skwtnnese9nmw99xlh7jwgtdxurhce2zcwsamevmj37kd5yzxzu55mt567seewmajra2hwyry5cv9kfzf02paerhs7tf9acdcgq24pqer" -> PublicKey(hex"03a9d79bcfab7feb0f24c3cd61a57f0f00de2225b6d31bce0bc4564efa3b1b5aaf"), - "lnbc3100n1pwp370spp5ku7y6tfz5up840v00vgc2vmmqtpsu5ly98h09vxv9d7k9xtq8mrsdpjd35kw6r5de5kueevypkxjemgw3hxjmn89ssxc6t8dp6xu6twvucqp2sunrt8slx2wmvjzdv3vvlls9gez7g2gd37g2pwa4pnlswuxzy0w3hd5kkqdrpl4ylcdhvkvuamwjsfh79nkn52dq0qpzj8c4rf57jmgqschvrr" -> PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), - "lnbc1500n1pwr7z8rpp5hyfkmnwwx7x902ys52du8pph6hdkarnqvj6fwhh9swfsg5lp94vsdpa2fjkzep6ypph2um5dajxjctvypmkzmrvv468xgrpwfjjqetkd9kzqctwvss8ycqzysxqr23s64a2h7gn25pchh8r6jpe236h925fylw2jcm4pd92w8hkmpflreph8r6s8jnnml0zu47qv6t2sj6frnle2cpanf6e027vsddgkl8hk7gpta89d0" -> PublicKey(hex"03e50492eab4107a773141bb419e107bda3de3d55652e6e1a41225f06a0bbf2d56"), - "lnbc1500n1pdl05v0pp5c4t5p3renelctlh0z4jpznyxna7lw9zhws868wktp8vtn8t5a8uqdpa2fjkzep6ypxxjemgw35kueeqfejhgam0wf4jqnrfw96kjerfw3ujq5r0dakq6cqzysxqr23s7k3ktaae69gpl2tfleyy2rsm0m6cy5yvf8uq7g4dmpyrwvfxzslnvryx5me4xh0fsp9jfjsqkuwpzx9ydwe6ndrm0eznarhdrfwn5gsp949n7x" -> PublicKey(hex"03e50492eab4107a773141bb419e107bda3de3d55652e6e1a41225f06a0bbf2d56"), - "lnbc1500n1pwyvxp3pp5ch8jx4g0ft0f6tzg008vr82wv92sredy07v46h7q3h3athx2nm2sdpa2fjkzep6ypyx7aeqfys8w6tndqsx67fqw35x2gzvv4jxwetjypvzqam0w4kxgcqzysxqr23s3hdgx90a6jcqgl84z36dv6kn6eg4klsaje2kdm84662rq7lzzzlycvne4l8d0steq5pctdp4ffeyhylgrt7ln92l8dyvrnsn9qg5qkgqrz2cra" -> PublicKey(hex"03e50492eab4107a773141bb419e107bda3de3d55652e6e1a41225f06a0bbf2d56"), - "lnbc1500n1pwr7z2ppp5cuzt0txjkkmpz6sgefdjjmdrsj9gl8fqyeu6hx7lj050f68yuceqdqvg9jxgg8zn2sscqzysxqr23s7442lgk6cj95qygw2hly9qw9zchhag5p5m3gyzrmws8namcsqh5nz2nm6a5sc2ln6jx59sln9a7t8vxtezels2exurr0gchz9gk0ufgpwczm3r" -> PublicKey(hex"03e50492eab4107a773141bb419e107bda3de3d55652e6e1a41225f06a0bbf2d56"), - "lnbc1500n1pd7u7g4pp5eam7uhxc0w4epnuflgkl62m64qu378nnhkg3vahkm7dhdcqnzl4sdqvg9jxgg8zn2sscqzysxqr23s870l2549nhsr2dfv9ehkl5z95p5rxpks5j2etr35e02z9r6haalrfjs7sz5y7wzenywp8t52w89c9u8taf9m76t2p0w0vxw243y7l4spqdue7w" -> PublicKey(hex"03e50492eab4107a773141bb419e107bda3de3d55652e6e1a41225f06a0bbf2d56"), - "lnbc5u1pwq2jqzpp56zhpjmfm72e8p8vmfssspe07u7zmnm5hhgynafe4y4lwz6ypusvqdzsd35kw6r5de5kuemwv468wmmjddehgmmjv4ejucm0d40n2vpsta6hqan0w3jhxhmnw3hhye2fgs7nywfhcqp2tqnqpewrz28yrvvvyzjyrvwahuy595t4w4ar3cvt5cq9jx3rmxd4p7vjgmeylfkgjrssc66a9q9hhnd4aj7gqv2zj0jr2zt0gahnv0sp9y675y" -> PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), - "lnbc10n1pw9pqp3pp562wg5n7atx369mt75feu233cnm5h508mx7j0d807lqe0w45gndnqdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdejcqzysxqrrsszfg9lfawdhnp2m785cqgzg4c85mvgct44xdzjea9t0vu4mc22u4prjjz5qd4y7uhgg3wm57muh5wfz8l04kgyq8juwql3vaffm23akspzkmj53" -> PublicKey(hex"02cda8c01b2303e91bec74c43093d5f1c4fd42a95671ae27bf853d7dfea9b78c06"), - "lnbc90n1pwypjnppp5m870lhg8qjrykj6hfegawaq0ukzc099ntfezhm8jr48cw5ywgpwqdpl2pshjmt9de6zqen0wgsrjgrsd9ux2mrnypshggrnv96x7umgd9ejuurvv93k2tsxqzjccqp2s0n2u7msmypy9dh96e6exfas434td6a7f5qy5shzyk4r9dxwv0zhyxcqjkmxgnnkjvqhthadhkqvvd66f8gxkdna3jqyzhnnhfs6w3qpme2zfz" -> PublicKey(hex"024655b768ef40951b20053a5c4b951606d4d86085d51238f2c67c7dec29c792ca"), - "lnbc100n1pdunsurpp5af2vzgyjtj2q48dxl8hpfv9cskwk7q5ahefzyy3zft6jyrc4uv2qdp0tfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqcnyvccqzysxqyd9uqpcp608auvkcr22672nhwqqtul0q6dqrxryfsstttlwyvkzttxt29mxyshley6u45gf0sxc0d9dxr5fk48tj4z2z0wh6asfxhlsea57qp45tfua" -> PublicKey(hex"03a9d79bcfab7feb0f24c3cd61a57f0f00de2225b6d31bce0bc4564efa3b1b5aaf"), - "lnbc100n1pd6hzfgpp5au2d4u2f2gm9wyz34e9rls66q77cmtlw3tzu8h67gcdcvj0dsjdqdp0tfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqcnqvscqzysxqyd9uqxg5n7462ykgs8a23l3s029dun9374xza88nlf2e34nupmc042lgps7tpwd0ue0he0gdcpfmc5mshmxkgw0hfztyg4j463ux28nh2gagqage30p" -> PublicKey(hex"03a9d79bcfab7feb0f24c3cd61a57f0f00de2225b6d31bce0bc4564efa3b1b5aaf"), - "lnbc50n1pdl052epp57549dnjwf2wqfz5hg8khu0wlkca8ggv72f9q7x76p0a7azkn3ljsdp0tfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqcnvvscqzysxqyd9uqa2z48kchpmnyafgq2qlt4pruwyjh93emh8cd5wczwy47pkx6qzarmvl28hrnqf98m2rnfa0gx4lnw2jvhlg9l4265240av6t9vdqpzsqntwwyx" -> PublicKey(hex"03a9d79bcfab7feb0f24c3cd61a57f0f00de2225b6d31bce0bc4564efa3b1b5aaf"), - "lnbc100n1pd7cwrypp57m4rft00sh6za2x0jwe7cqknj568k9xajtpnspql8dd38xmd7musdp0tfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqcngvscqzysxqyd9uqsxfmfv96q0d7r3qjymwsem02t5jhtq58a30q8lu5dy3jft7wahdq2f5vc5qqymgrrdyshff26ak7m7n0vqyf7t694vam4dcqkvnr65qp6wdch9" -> PublicKey(hex"03a9d79bcfab7feb0f24c3cd61a57f0f00de2225b6d31bce0bc4564efa3b1b5aaf"), - "lnbc100n1pw9qjdgpp5lmycszp7pzce0rl29s40fhkg02v7vgrxaznr6ys5cawg437h80nsdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdejcqzysxqrrss47kl34flydtmu2wnszuddrd0nwa6rnu4d339jfzje6hzk6an0uax3kteee2lgx5r0629wehjeseksz0uuakzwy47lmvy2g7hja7mnpsqjmdct9" -> PublicKey(hex"02cda8c01b2303e91bec74c43093d5f1c4fd42a95671ae27bf853d7dfea9b78c06"), + //"lnbc40n1pw9qjvwpp5qq3w2ln6krepcslqszkrsfzwy49y0407hvks30ec6pu9s07jur3sdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdencqzysxqrrss7ju0s4dwx6w8a95a9p2xc5vudl09gjl0w2n02sjrvffde632nxwh2l4w35nqepj4j5njhh4z65wyfc724yj6dn9wajvajfn5j7em6wsq2elakl" -> PublicKey(hex"02cda8c01b2303e91bec74c43093d5f1c4fd42a95671ae27bf853d7dfea9b78c06"), + //"lnbc1500n1pwyvqwfpp5p5nxwpuk02nd2xtzwex97gtjlpdv0lxj5z08vdd0hes7a0h437qsdpa2fjkzep6yp8kumrfdejjqempd43xc6twvusxjueqd9kxcet8v9kzqct8v95kucqzysxqr23s8r9seqv6datylwtjcvlpdkukfep7g80hujz3w8t599saae7gap6j48gs97z4fvrx4t4ajra6pvdyf5ledw3tg7h2s3606qm79kk59zqpeygdhd" -> PublicKey(hex"03e50492eab4107a773141bb419e107bda3de3d55652e6e1a41225f06a0bbf2d56"), + //"lnbc800n1pwykdmfpp5zqjae54l4ecmvm9v338vw2n07q2ehywvy4pvay53s7068t8yjvhqdqddpjkcmr0yysjzcqp27lya2lz7d80uxt6vevcwzy32227j3nsgyqlrxuwgs22u6728ldszlc70qgcs56wglrutn8jnnnelsk38d6yaqccmw8kmmdlfsyjd20qp69knex" -> PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), + //"lnbc300n1pwzezrnpp5zgwqadf4zjygmhf3xms8m4dd8f4mdq26unr5mfxuyzgqcgc049tqdq9dpjhjcqp23gxhs2rawqxdvr7f7lmj46tdvkncnsz8q5jp2kge8ndfm4dpevxrg5xj4ufp36x89gmaw04lgpap7e3x9jcjydwhcj9l84wmts2lg6qquvpque" -> PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), + //"lnbc10n1pdm2qaxpp5zlyxcc5dypurzyjamt6kk6a8rpad7je5r4w8fj79u6fktnqu085sdpl2pshjmt9de6zqen0wgsrzgrsd9ux2mrnypshggrnv96x7umgd9ejuurvv93k2tsxqzjccqp2e3nq4xh20prn9kx8etqgjjekzzjhep27mnqtyy62makh4gqc4akrzhe3nmj8lnwtd40ne5gn8myruvrt9p6vpuwmc4ghk7587erwqncpx9sds0" -> PublicKey(hex"024655b768ef40951b20053a5c4b951606d4d86085d51238f2c67c7dec29c792ca"), + //"lnbc800n1pwp5uuhpp5y8aarm9j9x9cer0gah9ymfkcqq4j4rn3zr7y9xhsgar5pmaceaqqdqdvf5hgcm0d9hzzcqp2vf8ramzsgdznxd5yxhrxffuk43pst9ng7cqcez9p2zvykpcf039rp9vutpe6wfds744yr73ztyps2z58nkmflye9yt4v3d0qz8z3d9qqq3kv54" -> PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), + //"lnbc1500n1pdl686hpp5y7mz3lgvrfccqnk9es6trumjgqdpjwcecycpkdggnx7h6cuup90sdpa2fjkzep6ypqkymm4wssycnjzf9rjqurjda4x2cm5ypskuepqv93x7at5ypek7cqzysxqr23s5e864m06fcfp3axsefy276d77tzp0xzzzdfl6p46wvstkeqhu50khm9yxea2d9efp7lvthrta0ktmhsv52hf3tvxm0unsauhmfmp27cqqx4xxe" -> PublicKey(hex"03e50492eab4107a773141bb419e107bda3de3d55652e6e1a41225f06a0bbf2d56"), + //"lnbc80n1pwykw99pp5965lyj4uesussdrk0lfyd2qss9m23yjdjkpmhw0975zky2xlhdtsdpl2pshjmt9de6zqen0wgsrsgrsd9ux2mrnypshggrnv96x7umgd9ejuurvv93k2tsxqzjccqp27677yc44l22jxexewew7lzka7g5864gdpr6y5v6s6tqmn8xztltk9qnna2qwrsm7gfyrpqvhaz4u3egcalpx2gxef3kvqwd44hekfxcqr7nwhf" -> PublicKey(hex"024655b768ef40951b20053a5c4b951606d4d86085d51238f2c67c7dec29c792ca"), + //"lnbc2200n1pwp4pwnpp5xy5f5kl83ytwuz0sgyypmqaqtjs68s3hrwgnnt445tqv7stu5kyqdpyvf5hgcm0d9hzqmn0wssxymr0vd4kx6rpd9hqcqp25y9w3wc3ztxhemsqch640g4u00szvvfk4vxr7klsakvn8cjcunjq8rwejzy6cfwj90ulycahnq43lff8m84xqf3tusslq2w69htwwlcpfqskmc" -> PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), + //"lnbc300n1pwp50ggpp5x7x5a9zs26amr7rqngp2sjee2c3qc80ztvex00zxn7xkuzuhjkxqdq9dpjhjcqp2s464vnrpx7aynh26vsxx6s3m52x88dqen56pzxxnxmc9s7y5v0dprsdlv5q430zy33lcl5ll6uy60m7c9yrkjl8yxz7lgsqky3ka57qq4qeyz3" -> PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), + //"lnbc10n1pd6jt93pp58vtzf4gup4vvqyknfakvh59avaek22hd0026snvpdnc846ypqrdsdp0tfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqcnqvscqzysxqyd9uq3sv9xkv2sgdf2nuvs97d2wkzj5g75rljnh5wy5wqhnauvqhxd9fpq898emtz8hul8cnxmc9wtj2777ehgnnyhcrs0y5zuhy8rs0jv6cqqe24tw" -> PublicKey(hex"03a9d79bcfab7feb0f24c3cd61a57f0f00de2225b6d31bce0bc4564efa3b1b5aaf"), + //"lnbc890n1pwzu4uqpp5gy274lq0m5hzxuxy90vf65wchdszrazz9zxjdk30ed05kyjvwxrqdzq2pshjmt9de6zqen0wgsrswfqwp5hsetvwvsxzapqwdshgmmndp5hxtnsd3skxefwxqzjccqp2qjvlfyl4rmc56gerx70lxcrjjlnrjfz677ezw4lwzy6syqh4rnlql6t6n3pdfxkcal9jp98plgf2zqzz8jxfza9vjw3vd4t62ws8gkgqhv9x28" -> PublicKey(hex"024655b768ef40951b20053a5c4b951606d4d86085d51238f2c67c7dec29c792ca"), + //"lnbc79760n1pd7cwyapp5gevl4mv968fs4le3tytzhr9r8tdk8cu3q7kfx348ut7xyntvnvmsdz92pskjepqw3hjqmrfva58gmnfdenjqumvda6zqmtpvd5xjmn9ypnx7u3qx5czq5msd9h8xcqzysxqrrssjzky68fdnhvee7aw089d5zltahfhy2ffa96pwf7fszjnm6mv0fzpv88jwaenm5qfg64pl768q8hf2vnvc5xsrpqd45nca2mewsv55wcpmhskah" -> PublicKey(hex"039f01ad62e5208940faff11d0bbc997582eafad7642aaf53de6a5f6551ab73400"), + //"lnbc90n1pduns5qpp5f5h5ghga4cp7uj9de35ksk00a2ed9jf774zy7va37k5zet5cds8sdpl2pshjmt9de6zqen0wgsrjgrsd9ux2mrnypshggrnv96x7umgd9ejuurvv93k2tsxqzjccqp28ynysm3clcq865y9umys8t2f54anlsu2wfpyfxgq09ht3qfez9x9z9fpff8wzqwzua2t9vayzm4ek3vf4k4s5cdg3a6hp9vsgg9klpgpmafvnv" -> PublicKey(hex"024655b768ef40951b20053a5c4b951606d4d86085d51238f2c67c7dec29c792ca"), + //"lnbc10u1pw9nehppp5tf0cpc3nx3wpk6j2n9teqwd8kuvryh69hv65w7p5u9cqhse3nmgsdzz2p6hycmgv9ek2gr0vcsrzgrxd3hhwetjyphkugzzd96xxmmfdcsywunpwejhjctjvscqp222vxxwq70temepf6n0xlzk0asr43ppqrt0mf6eclnfd5mxf6uhv5wvsqgdvht6uqxfw2vgdku5gfyhgguepvnjfu7s4kuthtnuxy0hsq6wwv9d" -> PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), + //"lnbc30n1pw9qjwmpp5tcdc9wcr0avr5q96jlez09eax7djwmc475d5cylezsd652zvptjsdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdf4cqzysxqrrss7r8gn9d6klf2urzdjrq3x67a4u25wpeju5utusnc539aj5462y7kv9w56mndcx8jad7aa7qz8f8qpdw9qlx52feyemwd7afqxu45jxsqyzwns9" -> PublicKey(hex"02cda8c01b2303e91bec74c43093d5f1c4fd42a95671ae27bf853d7dfea9b78c06"), + //"lnbc10u1pw9x36xpp5tlk00k0dftfx9vh40mtdlu844c9v65ad0kslnrvyuzfxqhdur46qdzz2p6hycmgv9ek2gr0vcsrzgrxd3hhwetjyphkugzzd96xxmmfdcsywunpwejhjctjvscqp2fpudmf4tt0crardf0k7vk5qs4mvys88el6e7pg62hgdt9t6ckf48l6jh4ckp87zpcnal6xnu33hxdd8k27vq2702688ww04kc065r7cqw3cqs3" -> PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), + //"lnbc40n1pd6jttkpp5v8p97ezd3uz4ruw4w8w0gt4yr3ajtrmaeqe23ttxvpuh0cy79axqdp0tfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqcnqvscqzysxqyd9uq3r88ajpz77z6lg4wc7srhsk7m26guuvhdlpea6889m9jnc9a25sx7rdtryjukew86mtcngl6d8zqh9trtu60cmmwfx6845q08z06p6qpl3l55t" -> PublicKey(hex"03a9d79bcfab7feb0f24c3cd61a57f0f00de2225b6d31bce0bc4564efa3b1b5aaf"), + //"lnbc1pwr7fqhpp5vhur3ahtumqz5mkramxr22597gaa9rnrjch8gxwr9h7r56umsjpqdpl235hqurfdcs9xct5daeks6tngask6etnyq58g6tswp5kutndv55jsaf3x5unj2gcqzysxqyz5vq88jysqvrwhq6qe38jdulefx0z9j7sfw85wqc6athfx9h77fjnjxjvprz76ayna0rcjllgu5ka960rul3qxvsrr9zth5plaerq96ursgpsshuee" -> PublicKey(hex"03c2abfa93eacec04721c019644584424aab2ba4dff3ac9bdab4e9c97007491dda"), + //"lnbc10n1pw9rt5hpp5dsv5ux7xlmhmrpqnffgj6nf03mvx5zpns3578k2c5my3znnhz0gqdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwwp3cqzysxqrrssnrasvcr5ydng283zdfpw38qtqfxjnzhdmdx9wly9dsqmsxvksrkzkqrcenu6h36g4g55q56ejk429nm4zjfgssh8uhs7gs760z63ggcqp3gyd6" -> PublicKey(hex"02cda8c01b2303e91bec74c43093d5f1c4fd42a95671ae27bf853d7dfea9b78c06"), + //"lnbc1500n1pd7u7p4pp5d54vffcehkcy79gm0fkqrthh3y576jy9flzpy9rf6syua0s5p0jqdpa2fjkzep6ypxhjgz90pcx2unfv4hxxefqdanzqargv5s9xetrdahxggzvd9nkscqzysxqr23sklptztnk25aqzwty35gk9q7jtfzjywdfx23d8a37g2eaejrv3d9nnt87m98s4eps87q87pzfd6hkd077emjupe0pcazpt9kaphehufqqu7k37h" -> PublicKey(hex"03e50492eab4107a773141bb419e107bda3de3d55652e6e1a41225f06a0bbf2d56"), + //"lnbc10n1pdunsmgpp5wn90mffjvkd06pe84lpa6e370024wwv7xfw0tdxlt6qq8hc7d7rqdp0tfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqcngvscqzysxqyd9uqs0cqtrum6h7dct88nkjxwxvte7hjh9pusx64tp35u0m6qhqy5dgn9j27fs37mg0w3ruf7enxlsc9xmlasgjzyyaaxqdxu9x5w0md4fspgz8twv" -> PublicKey(hex"03a9d79bcfab7feb0f24c3cd61a57f0f00de2225b6d31bce0bc4564efa3b1b5aaf"), + //"lnbc700n1pwp50wapp5w7eearwr7qjhz5vk5zq4g0t75f90mrekwnw4e795qfjxyaq27dxsdqvdp6kuar9wgeqcqp20gfw78vvasjm45l6zfxmfwn59ac9dukp36mf0y3gpquhp7rptddxy7d32ptmqukeghvamlkmve9n94sxmxglun4zwtkyhk43e6lw8qspc9y9ww" -> PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), + //"lnbc10n1pd6jvy5pp50x9lymptter9najcdpgrcnqn34wq34f49vmnllc57ezyvtlg8ayqdpdtfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yq6rvcqzysxqyd9uqcejk56vfz3y80u3npefpx82f0tghua88a8x2d33gmxcjm45q6l5xwurwyp9aj2p59cr0lknpk0eujfdax32v4px4m22u6zr5z40zxvqp5m85cr" -> PublicKey(hex"03a9d79bcfab7feb0f24c3cd61a57f0f00de2225b6d31bce0bc4564efa3b1b5aaf"), + //"lnbc10n1pw9pqz7pp50782e2u9s25gqacx7mvnuhg3xxwumum89dymdq3vlsrsmaeeqsxsdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwd3ccqzysxqrrsstxqhw2kvdfwsf7c27aaae45fheq9rzndesu4mph9dq08sawa0auz7e0z7jn9qf3zphegv2ermup0fgce0phqmf73j4zx88v3ksrgeeqq9yzzad" -> PublicKey(hex"02cda8c01b2303e91bec74c43093d5f1c4fd42a95671ae27bf853d7dfea9b78c06"), + //"lnbc1300n1pwq4fx7pp5sqmq97yfxhhk7xv7u8cuc8jgv5drse45f5pmtx6f5ng2cqm332uqdq4e2279q9zux62tc5q5t9fgcqp29a662u3p2h4h4ucdav4xrlxz2rtwvvtward7htsrldpsc5erknkyxu0x2xt9qv0u766jadeetsz9pj4rljpjy0g8ayqqt2q8esewsrqpc8v4nw" -> PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), + //"lnbc1u1pd7u7tnpp5s9he3ccpsmfdkzrsjns7p3wpz7veen6xxwxdca3khwqyh2ezk8kqdqdg9jxgg8sn7f27cqzysxqr23ssm4krdc4s0zqhfk97n0aclxsmaga208pa8c0hz3zyauqsjjxfj7kw6t29dkucp68s8s4zfdgp97kkmzgy25yuj0dcec85d9c50sgjqgq5jhl4e" -> PublicKey(hex"03e50492eab4107a773141bb419e107bda3de3d55652e6e1a41225f06a0bbf2d56"), + //"lnbc1200n1pwq5kf2pp5snkm9kr0slgzfc806k4c8q93d4y57q3lz745v2hefx952rhuymrqdq509shjgrzd96xxmmfdcsscqp2w5ta9uwzhmxxp0mnhwwvnjdn6ev4huj3tha5d80ajv2p5phe8wk32yn7ch6lennx4zzawqtd34aqetataxjmrz39gzjl256walhw03gpxz79rr" -> PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), + //"lnbc1500n1pd7u7v0pp5s6d0wqexag3aqzugaw3gs7hw7a2wrq6l8fh9s42ndqu8zu480m0sdqvg9jxgg8zn2sscqzysxqr23sm23myatjdsp3003rlasgzwg3rlr0ca8uqdt5d79lxmdwqptufr89r5rgk4np4ag0kcw7at6s6eqdany0k6m0ezjva0cyda5arpaw7lcqgzjl7u" -> PublicKey(hex"03e50492eab4107a773141bb419e107bda3de3d55652e6e1a41225f06a0bbf2d56"), + //"lnbc100n1pd6jv8ypp53p6fdd954h3ffmyj6av4nzcnwfuyvn9rrsc2u6y22xnfs0l0cssqdpdtfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqerscqzysxqyd9uqyefde4la0qmglafzv8q34wqsf4mtwd8ausufavkp2e7paewd3mqsg0gsdmvrknw80t92cuvu9raevrnxtpsye0utklhpunsz68a9veqpkypx9j" -> PublicKey(hex"03a9d79bcfab7feb0f24c3cd61a57f0f00de2225b6d31bce0bc4564efa3b1b5aaf"), + //"lnbc2300n1pwp50w8pp53030gw8rsqac6f3sqqa9exxwfvphsl4v4w484eynspwgv5v6vyrsdp9w35xjueqd9ejqmn0wssx67fqwpshxumhdaexgcqp2zmspcx992fvezxqkyf3rkcxc9dm2vr4ewfx42c0fccg4ea72fyd3pd6vn94tfy9t39y0hg0hupak2nv0n6pzy8culeceq8kzpwjy0tsp4fwqw5" -> PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), + //"lnbc10n1pwykdlhpp53392ama65h3lnc4w55yqycp9v2ackexugl0ahz4jyc7fqtyuk85qdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwvejcqzysxqrrsszkwrx54an8lhr9h4h3d7lgpjrd370zucx0fdusaklqh2xgytr8hhgq5u0kvs56l8j53uktlmz3mqhhmn88kwwxfksnham9p6ws5pwxsqnpzyda" -> PublicKey(hex"03a9d79bcfab7feb0f24c3cd61a57f0f00de2225b6d31bce0bc4564efa3b1b5aaf"), + //"lnbc10470n1pw9qf40pp535pels2faqwau2rmqkgzn0rgtsu9u6qaxe5y6ttgjx5qm4pg0kgsdzy2pshjmt9de6zqen0wgsrzvp5xus8q6tcv4k8xgrpwss8xct5daeks6tn9ecxcctrv5hqxqzjccqp27sp3m204a7d47at5jkkewa7rvewdmpwaqh2ss72cajafyf7dts9ne67hw9pps2ud69p4fw95y9cdk35aef43cv35s0zzj37qu7s395cp2vw5mu" -> PublicKey(hex"024655b768ef40951b20053a5c4b951606d4d86085d51238f2c67c7dec29c792ca"), + //"lnbc100n1pwytlgspp5365rx7ell807x5jsr7ykf2k7p5z77qvxjx8x6pfhh5298xnr6d2sdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwvpscqzysxqrrssh9mphycg7e9lr58c267yerlcd9ka8lrljm8ygpnwu2v63jm7ax48y7qal25qy0ewpxw39r5whnqh93zw97gnnw64ss97n69975wh9gsqj7vudu" -> PublicKey(hex"03a9d79bcfab7feb0f24c3cd61a57f0f00de2225b6d31bce0bc4564efa3b1b5aaf"), + //"lnbc210n1pdunsefpp5jxn3hlj86evlwgsz5d70hquy78k28ahdwjmlagx6qly9x29pu4uqdzq2pshjmt9de6zqen0wgsryvfqwp5hsetvwvsxzapqwdshgmmndp5hxtnsd3skxefwxqzjccqp2snr8trjcrr5xyy7g63uq7mewqyp9k3d0duznw23zhynaz6pj3uwk48yffqn8p0jugv2z03dxquc8azuwr8myjgwzh69a34fl2lnmq2sppac733" -> PublicKey(hex"024655b768ef40951b20053a5c4b951606d4d86085d51238f2c67c7dec29c792ca"), + //"lnbc1700n1pwr7z98pp5j5r5q5c7syavjjz7czjvng4y95w0rd8zkl7q43sm7spg9ht2sjfqdquwf6kumnfdenjqmrfva58gmnfdenscqp2jrhlc758m734gw5td4gchcn9j5cp5p38zj3tcpvgkegxewat38d3h24kn0c2ac2pleuqp5dutvw5fmk4d2v3trcqhl5pdxqq8swnldcqtq0akh" -> PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), + //"lnbc1500n1pdl05k5pp5nyd9netjpzn27slyj2np4slpmlz8dy69q7hygwm8ff4mey2jee5sdpa2fjkzep6ypxhjgz90pcx2unfv4hxxefqdanzqargv5s9xetrdahxggzvd9nkscqzysxqr23sqdd8t97qjc77pqa7jv7umc499jqkk0kwchapswj3xrukndr7g2nqna5x87n49uynty4pxexkt3fslyle7mwz708rs0rnnn44dnav9mgplf0aj7" -> PublicKey(hex"03e50492eab4107a773141bb419e107bda3de3d55652e6e1a41225f06a0bbf2d56"), + //"lnbc1u1pwyvxrppp5nvm98wnqdee838wtfmhfjx9s49eduzu3rx0fqec2wenadth8pxqsdqdg9jxgg8sn7vgycqzysxqr23snuza3t8x0tvusu07epal9rqxh4cq22m64amuzd6x607s0w55a5xpefp2xlxmej9r6nktmwv5td3849y2sg7pckwk9r8vqqps8g4u66qq85mp3g" -> PublicKey(hex"03e50492eab4107a773141bb419e107bda3de3d55652e6e1a41225f06a0bbf2d56"), + //"lnbc10n1pw9qjwppp55nx7xw3sytnfle67mh70dyukr4g4chyfmp4x4ag2hgjcts4kydnsdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwd3ccqzysxqrrss7t24v6w7dwtd65g64qcz77clgye7n8l0j67qh32q4jrw9d2dk2444vma7j6nedgx2ywel3e9ns4r257zprsn7t5uca045xxudz9pqzsqfena6v" -> PublicKey(hex"02cda8c01b2303e91bec74c43093d5f1c4fd42a95671ae27bf853d7dfea9b78c06"), + //"lnbc10u1pw9x373pp549mpcznu3q0r4ml095kjg38pvsdptzja8vhpyvc2avatc2cegycsdzz2p6hycmgv9ek2gr0vcsrzgrxd3hhwetjyphkugzzd96xxmmfdcsywunpwejhjctjvscqp2tgqwhzyjmpfymrshnaw6rwmy4rgrtjmmp66dr9v54xp52rsyzqd5htc3lu3k52t06fqk8yj05nsw0nnssak3ywev4n3xs3jgz42urmspjeqyw0" -> PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), + //"lnbc1500n1pd7u7vupp54jm8s8lmgnnru0ndwpxhm5qwllkrarasr9fy9zkunf49ct8mw9ssdqvg9jxgg8zn2sscqzysxqr23s4njradkzzaswlsgs0a6zc3cd28xc08t5car0k7su6q3u3vjvqt6xq2kpaadgt5x9suxx50rkevfw563fupzqzpc9m6dqsjcr8qt6k2sqelr838" -> PublicKey(hex"03e50492eab4107a773141bb419e107bda3de3d55652e6e1a41225f06a0bbf2d56"), + //"lnbc720n1pwypj4epp5k2saqsjznpvevsm9mzqfan3d9fz967x5lp39g3nwsxdkusps73csdzq2pshjmt9de6zqen0wgsrwv3qwp5hsetvwvsxzapqwdshgmmndp5hxtnsd3skxefwxqzjccqp2d3ltxtq0r795emmp7yqjjmmzl55cgju004vw08f83e98d28xmw44t4styhfhgsrwxydf68m2kup7j358zdrmhevqwr0hlqwt2eceaxcq7hezhx" -> PublicKey(hex"024655b768ef40951b20053a5c4b951606d4d86085d51238f2c67c7dec29c792ca"), + //"lnbc10n1pwykdacpp5kegv2kdkxmetm2tpnzfgt4640n7mgxl95jnpc6fkz6uyjdwahw8sdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdp5cqzysxqrrssjlny2skwtnnese9nmw99xlh7jwgtdxurhce2zcwsamevmj37kd5yzxzu55mt567seewmajra2hwyry5cv9kfzf02paerhs7tf9acdcgq24pqer" -> PublicKey(hex"03a9d79bcfab7feb0f24c3cd61a57f0f00de2225b6d31bce0bc4564efa3b1b5aaf"), + //"lnbc3100n1pwp370spp5ku7y6tfz5up840v00vgc2vmmqtpsu5ly98h09vxv9d7k9xtq8mrsdpjd35kw6r5de5kueevypkxjemgw3hxjmn89ssxc6t8dp6xu6twvucqp2sunrt8slx2wmvjzdv3vvlls9gez7g2gd37g2pwa4pnlswuxzy0w3hd5kkqdrpl4ylcdhvkvuamwjsfh79nkn52dq0qpzj8c4rf57jmgqschvrr" -> PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), + //"lnbc1500n1pwr7z8rpp5hyfkmnwwx7x902ys52du8pph6hdkarnqvj6fwhh9swfsg5lp94vsdpa2fjkzep6ypph2um5dajxjctvypmkzmrvv468xgrpwfjjqetkd9kzqctwvss8ycqzysxqr23s64a2h7gn25pchh8r6jpe236h925fylw2jcm4pd92w8hkmpflreph8r6s8jnnml0zu47qv6t2sj6frnle2cpanf6e027vsddgkl8hk7gpta89d0" -> PublicKey(hex"03e50492eab4107a773141bb419e107bda3de3d55652e6e1a41225f06a0bbf2d56"), + //"lnbc1500n1pdl05v0pp5c4t5p3renelctlh0z4jpznyxna7lw9zhws868wktp8vtn8t5a8uqdpa2fjkzep6ypxxjemgw35kueeqfejhgam0wf4jqnrfw96kjerfw3ujq5r0dakq6cqzysxqr23s7k3ktaae69gpl2tfleyy2rsm0m6cy5yvf8uq7g4dmpyrwvfxzslnvryx5me4xh0fsp9jfjsqkuwpzx9ydwe6ndrm0eznarhdrfwn5gsp949n7x" -> PublicKey(hex"03e50492eab4107a773141bb419e107bda3de3d55652e6e1a41225f06a0bbf2d56"), + //"lnbc1500n1pwyvxp3pp5ch8jx4g0ft0f6tzg008vr82wv92sredy07v46h7q3h3athx2nm2sdpa2fjkzep6ypyx7aeqfys8w6tndqsx67fqw35x2gzvv4jxwetjypvzqam0w4kxgcqzysxqr23s3hdgx90a6jcqgl84z36dv6kn6eg4klsaje2kdm84662rq7lzzzlycvne4l8d0steq5pctdp4ffeyhylgrt7ln92l8dyvrnsn9qg5qkgqrz2cra" -> PublicKey(hex"03e50492eab4107a773141bb419e107bda3de3d55652e6e1a41225f06a0bbf2d56"), + //"lnbc1500n1pwr7z2ppp5cuzt0txjkkmpz6sgefdjjmdrsj9gl8fqyeu6hx7lj050f68yuceqdqvg9jxgg8zn2sscqzysxqr23s7442lgk6cj95qygw2hly9qw9zchhag5p5m3gyzrmws8namcsqh5nz2nm6a5sc2ln6jx59sln9a7t8vxtezels2exurr0gchz9gk0ufgpwczm3r" -> PublicKey(hex"03e50492eab4107a773141bb419e107bda3de3d55652e6e1a41225f06a0bbf2d56"), + //"lnbc1500n1pd7u7g4pp5eam7uhxc0w4epnuflgkl62m64qu378nnhkg3vahkm7dhdcqnzl4sdqvg9jxgg8zn2sscqzysxqr23s870l2549nhsr2dfv9ehkl5z95p5rxpks5j2etr35e02z9r6haalrfjs7sz5y7wzenywp8t52w89c9u8taf9m76t2p0w0vxw243y7l4spqdue7w" -> PublicKey(hex"03e50492eab4107a773141bb419e107bda3de3d55652e6e1a41225f06a0bbf2d56"), + //"lnbc5u1pwq2jqzpp56zhpjmfm72e8p8vmfssspe07u7zmnm5hhgynafe4y4lwz6ypusvqdzsd35kw6r5de5kuemwv468wmmjddehgmmjv4ejucm0d40n2vpsta6hqan0w3jhxhmnw3hhye2fgs7nywfhcqp2tqnqpewrz28yrvvvyzjyrvwahuy595t4w4ar3cvt5cq9jx3rmxd4p7vjgmeylfkgjrssc66a9q9hhnd4aj7gqv2zj0jr2zt0gahnv0sp9y675y" -> PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), + //"lnbc10n1pw9pqp3pp562wg5n7atx369mt75feu233cnm5h508mx7j0d807lqe0w45gndnqdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdejcqzysxqrrsszfg9lfawdhnp2m785cqgzg4c85mvgct44xdzjea9t0vu4mc22u4prjjz5qd4y7uhgg3wm57muh5wfz8l04kgyq8juwql3vaffm23akspzkmj53" -> PublicKey(hex"02cda8c01b2303e91bec74c43093d5f1c4fd42a95671ae27bf853d7dfea9b78c06"), + //"lnbc90n1pwypjnppp5m870lhg8qjrykj6hfegawaq0ukzc099ntfezhm8jr48cw5ywgpwqdpl2pshjmt9de6zqen0wgsrjgrsd9ux2mrnypshggrnv96x7umgd9ejuurvv93k2tsxqzjccqp2s0n2u7msmypy9dh96e6exfas434td6a7f5qy5shzyk4r9dxwv0zhyxcqjkmxgnnkjvqhthadhkqvvd66f8gxkdna3jqyzhnnhfs6w3qpme2zfz" -> PublicKey(hex"024655b768ef40951b20053a5c4b951606d4d86085d51238f2c67c7dec29c792ca"), + //"lnbc100n1pdunsurpp5af2vzgyjtj2q48dxl8hpfv9cskwk7q5ahefzyy3zft6jyrc4uv2qdp0tfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqcnyvccqzysxqyd9uqpcp608auvkcr22672nhwqqtul0q6dqrxryfsstttlwyvkzttxt29mxyshley6u45gf0sxc0d9dxr5fk48tj4z2z0wh6asfxhlsea57qp45tfua" -> PublicKey(hex"03a9d79bcfab7feb0f24c3cd61a57f0f00de2225b6d31bce0bc4564efa3b1b5aaf"), + //"lnbc100n1pd6hzfgpp5au2d4u2f2gm9wyz34e9rls66q77cmtlw3tzu8h67gcdcvj0dsjdqdp0tfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqcnqvscqzysxqyd9uqxg5n7462ykgs8a23l3s029dun9374xza88nlf2e34nupmc042lgps7tpwd0ue0he0gdcpfmc5mshmxkgw0hfztyg4j463ux28nh2gagqage30p" -> PublicKey(hex"03a9d79bcfab7feb0f24c3cd61a57f0f00de2225b6d31bce0bc4564efa3b1b5aaf"), + //"lnbc50n1pdl052epp57549dnjwf2wqfz5hg8khu0wlkca8ggv72f9q7x76p0a7azkn3ljsdp0tfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqcnvvscqzysxqyd9uqa2z48kchpmnyafgq2qlt4pruwyjh93emh8cd5wczwy47pkx6qzarmvl28hrnqf98m2rnfa0gx4lnw2jvhlg9l4265240av6t9vdqpzsqntwwyx" -> PublicKey(hex"03a9d79bcfab7feb0f24c3cd61a57f0f00de2225b6d31bce0bc4564efa3b1b5aaf"), + //"lnbc100n1pd7cwrypp57m4rft00sh6za2x0jwe7cqknj568k9xajtpnspql8dd38xmd7musdp0tfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqcngvscqzysxqyd9uqsxfmfv96q0d7r3qjymwsem02t5jhtq58a30q8lu5dy3jft7wahdq2f5vc5qqymgrrdyshff26ak7m7n0vqyf7t694vam4dcqkvnr65qp6wdch9" -> PublicKey(hex"03a9d79bcfab7feb0f24c3cd61a57f0f00de2225b6d31bce0bc4564efa3b1b5aaf"), + //"lnbc100n1pw9qjdgpp5lmycszp7pzce0rl29s40fhkg02v7vgrxaznr6ys5cawg437h80nsdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdejcqzysxqrrss47kl34flydtmu2wnszuddrd0nwa6rnu4d339jfzje6hzk6an0uax3kteee2lgx5r0629wehjeseksz0uuakzwy47lmvy2g7hja7mnpsqjmdct9" -> PublicKey(hex"02cda8c01b2303e91bec74c43093d5f1c4fd42a95671ae27bf853d7dfea9b78c06"), "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q5sqqqqqqqqqqqqqqqpqsq67gye39hfg3zd8rgc80k32tvy9xk2xunwm5lzexnvpx6fd77en8qaq424dxgt56cag2dpt359k3ssyhetktkpqh24jqnjyw6uqd08sgptq44qu" -> PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad"), "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q4psqqqqqqqqqqqqqqqpqsqq40wa3khl49yue3zsgm26jrepqr2eghqlx86rttutve3ugd05em86nsefzh4pfurpd9ek9w2vp95zxqnfe2u7ckudyahsa52q66tgzcp6t2dyk" -> PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad"), "lnbc100n1pslczttpp5refxwyd5qvvnxsmswhqtqd50hdcwhk5edp02u3xpy6whf6eua3lqdq8w35hg6gsp56nrnqjjjj2g3wuhdwhy7r3sfu0wae603w9zme8wcq2f3myu3hm6qcqzrm9qrjgq7md5lu2hhkz657rs2a40xm2elaqda4krv6vy44my49x02azsqwr35puvgzjltd6dfth2awxcq49cx3srkl3zl34xhw7ppv840yf74wqq88rwr5" -> PublicKey(hex"036dc96e30210083a18762be096f13500004fc8af5bcca40f4872e18771ad58b4c"), diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt12InvoiceSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt12InvoiceSpec.scala index 86b175e2f9..149048f054 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt12InvoiceSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt12InvoiceSpec.scala @@ -340,7 +340,7 @@ class Bolt12InvoiceSpec extends AnyFunSuite { assert(codedDecoded.features == features) assert(codedDecoded.issuer.contains(issuer)) assert(codedDecoded.nodeId.value.drop(1) == nodeKey.publicKey.value.drop(1)) - assert(codedDecoded.blindedPaymentRoutes == Seq(BlindedPaymentRoute(path, payInfo, None))) + assert(codedDecoded.recipients == Seq(BlindRecipient(path, payInfo, None))) assert(codedDecoded.quantity.contains(quantity)) assert(codedDecoded.payerKey.contains(payerKey)) assert(codedDecoded.payerNote.contains(payerNote)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala index 694a504bcb..47eb9d55a8 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala @@ -111,7 +111,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(Crypto.sha256(incoming.get.paymentPreimage) == invoice.paymentHash) val add = UpdateAddHtlc(ByteVector32.One, 1, amountMsat, invoice.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket, None) - sender.send(handlerWithoutMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createSinglePartPayload(add.amountMsat, add.cltvExpiry, invoice.paymentSecret.get, invoice.paymentMetadata))) + sender.send(handlerWithoutMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createSinglePartPayload(add.amountMsat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) assert(register.expectMsgType[Register.Forward[CMD_FULFILL_HTLC]].message.id == add.id) val paymentReceived = eventListener.expectMsgType[PaymentReceived] @@ -129,7 +129,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status == IncomingPaymentStatus.Pending) val add = UpdateAddHtlc(ByteVector32.One, 2, amountMsat, invoice.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket, None) - sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createSinglePartPayload(add.amountMsat, add.cltvExpiry, invoice.paymentSecret.get, invoice.paymentMetadata))) + sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createSinglePartPayload(add.amountMsat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) assert(register.expectMsgType[Register.Forward[CMD_FULFILL_HTLC]].message.id == add.id) val paymentReceived = eventListener.expectMsgType[PaymentReceived] @@ -168,7 +168,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status == IncomingPaymentStatus.Pending) val add = UpdateAddHtlc(ByteVector32.One, 0, amountMsat, invoice.paymentHash, CltvExpiryDelta(3).toCltvExpiry(nodeParams.currentBlockHeight), TestConstants.emptyOnionPacket, None) - sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createSinglePartPayload(add.amountMsat, add.cltvExpiry, invoice.paymentSecret.get, invoice.paymentMetadata))) + sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createSinglePartPayload(add.amountMsat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(amountMsat, nodeParams.currentBlockHeight))) assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status == IncomingPaymentStatus.Pending) @@ -262,7 +262,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val invoice = sender.expectMsgType[Bolt12Invoice] assert(invoice.amount == 25_000.msat) assert(invoice.nodeId == privKey.publicKey) - assert(invoice.blindedPaymentRoutes.nonEmpty) + assert(invoice.recipients.nonEmpty) assert(invoice.features.hasFeature(RouteBlinding, Some(Mandatory))) assert(invoice.description == Left("a blinded coffee please")) assert(invoice.offerId.contains(offer.offerId)) @@ -301,7 +301,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(invoice.isExpired()) val add = UpdateAddHtlc(ByteVector32.One, 0, 1000 msat, invoice.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket, None) - sender.send(handlerWithoutMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createSinglePartPayload(add.amountMsat, add.cltvExpiry, invoice.paymentSecret.get, invoice.paymentMetadata))) + sender.send(handlerWithoutMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createSinglePartPayload(add.amountMsat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]] val Some(incoming) = nodeParams.db.payments.getIncomingPayment(invoice.paymentHash) assert(incoming.invoice.isExpired() && incoming.status == IncomingPaymentStatus.Expired) @@ -316,7 +316,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(invoice.isExpired()) val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, invoice.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket, None) - sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, invoice.paymentSecret.get, invoice.paymentMetadata))) + sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) val Some(incoming) = nodeParams.db.payments.getIncomingPayment(invoice.paymentHash) @@ -331,7 +331,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(!invoice.features.hasFeature(BasicMultiPartPayment)) val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, invoice.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket, None) - sender.send(handlerWithoutMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, invoice.paymentSecret.get, invoice.paymentMetadata))) + sender.send(handlerWithoutMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status == IncomingPaymentStatus.Pending) @@ -346,7 +346,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val lowCltvExpiry = nodeParams.channelConf.fulfillSafetyBeforeTimeout.toCltvExpiry(nodeParams.currentBlockHeight) val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, invoice.paymentHash, lowCltvExpiry, TestConstants.emptyOnionPacket, None) - sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, invoice.paymentSecret.get, invoice.paymentMetadata))) + sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status == IncomingPaymentStatus.Pending) @@ -360,7 +360,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(invoice.features.hasFeature(BasicMultiPartPayment)) val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, invoice.paymentHash.reverse, defaultExpiry, TestConstants.emptyOnionPacket, None) - sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, invoice.paymentSecret.get, invoice.paymentMetadata))) + sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status == IncomingPaymentStatus.Pending) @@ -374,7 +374,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(invoice.features.hasFeature(BasicMultiPartPayment)) val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, invoice.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket, None) - sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createMultiPartPayload(add.amountMsat, 999 msat, add.cltvExpiry, invoice.paymentSecret.get, invoice.paymentMetadata))) + sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createMultiPartPayload(add.amountMsat, 999 msat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(999 msat, nodeParams.currentBlockHeight))) assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status == IncomingPaymentStatus.Pending) @@ -388,7 +388,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(invoice.features.hasFeature(BasicMultiPartPayment)) val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, invoice.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket, None) - sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createMultiPartPayload(add.amountMsat, 2001 msat, add.cltvExpiry, invoice.paymentSecret.get, invoice.paymentMetadata))) + sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createMultiPartPayload(add.amountMsat, 2001 msat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(2001 msat, nodeParams.currentBlockHeight))) assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status == IncomingPaymentStatus.Pending) @@ -403,7 +403,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike // Invalid payment secret. val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, invoice.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket, None) - sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, invoice.paymentSecret.get.reverse, invoice.paymentMetadata))) + sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, invoice.paymentSecret.reverse, invoice.paymentMetadata))) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status == IncomingPaymentStatus.Pending) @@ -501,13 +501,13 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike f.sender.send(handler, ReceiveStandardPayment(Some(1000 msat), Left("1 slow coffee"))) val pr1 = f.sender.expectMsgType[Bolt11Invoice] val add1 = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr1.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket, None) - f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add1, FinalPayload.Standard.createMultiPartPayload(add1.amountMsat, 1000 msat, add1.cltvExpiry, pr1.paymentSecret.get, pr1.paymentMetadata))) + f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add1, FinalPayload.Standard.createMultiPartPayload(add1.amountMsat, 1000 msat, add1.cltvExpiry, pr1.paymentSecret, pr1.paymentMetadata))) // Partial payment exceeding the invoice amount, but incomplete because it promises to overpay. f.sender.send(handler, ReceiveStandardPayment(Some(1500 msat), Left("1 slow latte"))) val pr2 = f.sender.expectMsgType[Bolt11Invoice] val add2 = UpdateAddHtlc(ByteVector32.One, 1, 1600 msat, pr2.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket, None) - f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add2, FinalPayload.Standard.createMultiPartPayload(add2.amountMsat, 2000 msat, add2.cltvExpiry, pr2.paymentSecret.get, pr2.paymentMetadata))) + f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add2, FinalPayload.Standard.createMultiPartPayload(add2.amountMsat, 2000 msat, add2.cltvExpiry, pr2.paymentSecret, pr2.paymentMetadata))) awaitCond { f.sender.send(handler, GetPendingPayments) @@ -542,12 +542,12 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val invoice = f.sender.expectMsgType[Bolt11Invoice] val add1 = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, invoice.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket, None) - f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add1, FinalPayload.Standard.createMultiPartPayload(add1.amountMsat, 1000 msat, add1.cltvExpiry, invoice.paymentSecret.get, invoice.paymentMetadata))) + f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add1, FinalPayload.Standard.createMultiPartPayload(add1.amountMsat, 1000 msat, add1.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) // Invalid payment secret -> should be rejected. val add2 = UpdateAddHtlc(ByteVector32.Zeroes, 42, 200 msat, invoice.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket, None) - f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add2, FinalPayload.Standard.createMultiPartPayload(add2.amountMsat, 1000 msat, add2.cltvExpiry, invoice.paymentSecret.get.reverse, invoice.paymentMetadata))) + f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add2, FinalPayload.Standard.createMultiPartPayload(add2.amountMsat, 1000 msat, add2.cltvExpiry, invoice.paymentSecret.reverse, invoice.paymentMetadata))) val add3 = add2.copy(id = 43) - f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add3, FinalPayload.Standard.createMultiPartPayload(add3.amountMsat, 1000 msat, add3.cltvExpiry, invoice.paymentSecret.get, invoice.paymentMetadata))) + f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add3, FinalPayload.Standard.createMultiPartPayload(add3.amountMsat, 1000 msat, add3.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) f.register.expectMsgAllOf( Register.Forward(null, add2.channelId, CMD_FAIL_HTLC(add2.id, Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight)), commit = true)), @@ -587,7 +587,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(invoice.paymentHash == Crypto.sha256(preimage)) val add1 = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, invoice.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket, None) - f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add1, FinalPayload.Standard.createMultiPartPayload(add1.amountMsat, 1000 msat, add1.cltvExpiry, invoice.paymentSecret.get, invoice.paymentMetadata))) + f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add1, FinalPayload.Standard.createMultiPartPayload(add1.amountMsat, 1000 msat, add1.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) f.register.expectMsg(Register.Forward(null, ByteVector32.One, CMD_FAIL_HTLC(0, Right(PaymentTimeout), commit = true))) awaitCond({ f.sender.send(handler, GetPendingPayments) @@ -595,9 +595,9 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike }) val add2 = UpdateAddHtlc(ByteVector32.One, 2, 300 msat, invoice.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket, None) - f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add2, FinalPayload.Standard.createMultiPartPayload(add2.amountMsat, 1000 msat, add2.cltvExpiry, invoice.paymentSecret.get, invoice.paymentMetadata))) + f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add2, FinalPayload.Standard.createMultiPartPayload(add2.amountMsat, 1000 msat, add2.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) val add3 = UpdateAddHtlc(ByteVector32.Zeroes, 5, 700 msat, invoice.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket, None) - f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add3, FinalPayload.Standard.createMultiPartPayload(add3.amountMsat, 1000 msat, add3.cltvExpiry, invoice.paymentSecret.get, invoice.paymentMetadata))) + f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add3, FinalPayload.Standard.createMultiPartPayload(add3.amountMsat, 1000 msat, add3.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) // the fulfill are not necessarily in the same order as the commands f.register.expectMsgAllOf( diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala index 8e15b17ad6..be9c62bf8b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala @@ -35,7 +35,6 @@ import fr.acinq.eclair.router.BaseRouterSpec.channelHopFromUpdate import fr.acinq.eclair.router.Graph.WeightRatios import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.router.{Announcements, RouteNotFound} -import fr.acinq.eclair.wire.protocol.PaymentOnion.FinalPayload import fr.acinq.eclair.wire.protocol._ import org.scalatest.Outcome import org.scalatest.funsuite.FixtureAnyFunSuiteLike @@ -67,7 +66,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS override def withFixture(test: OneArgTest): Outcome = { val id = UUID.randomUUID() - val cfg = SendPaymentConfig(id, id, Some("42"), paymentHash, finalAmount, finalRecipient, Upstream.Local(id), None, storeInDb = true, publishEvent = true, recordPathFindingMetrics = true, Nil) + val cfg = SendPaymentConfig(id, id, Some("42"), paymentHash, finalAmount, Seq(finalRecipient), Upstream.Local(id), None, storeInDb = true, publishEvent = true, recordPathFindingMetrics = true, Nil) val nodeParams = TestConstants.Alice.nodeParams val (childPayFsm, router, sender, eventListener, metricsListener) = (TestProbe(), TestProbe(), TestProbe(), TestProbe(), TestProbe()) val paymentHandler = TestFSMRef(new MultiPartPaymentLifecycle(nodeParams, cfg, router.ref, FakePaymentFactory(childPayFsm))) @@ -80,18 +79,18 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS import f._ assert(payFsm.stateName == WAIT_FOR_PAYMENT_REQUEST) - val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 1, None, routeParams = routeParams.copy(randomize = true)) + val payment = SendMultiPartPayment(sender.ref, Seq(eRecipient), finalAmount, expiry, 1, routeParams = routeParams.copy(randomize = true)) sender.send(payFsm, payment) - router.expectMsg(RouteRequest(nodeParams.nodeId, e, finalAmount, maxFee, routeParams = routeParams.copy(randomize = false), allowMultiPart = true, paymentContext = Some(cfg.paymentContext))) + router.expectMsg(RouteRequest(nodeParams.nodeId, Seq(eRecipient), finalAmount, maxFee, routeParams = routeParams.copy(randomize = false), allowMultiPart = true, paymentContext = Some(cfg.paymentContext))) assert(payFsm.stateName == WAIT_FOR_ROUTES) - val singleRoute = Route(finalAmount, hop_ab_1 :: hop_be :: Nil, None) + val singleRoute = Route(finalAmount, hop_ab_1 :: hop_be :: Nil, eRecipient) router.send(payFsm, RouteResponse(Seq(singleRoute))) val childPayment = childPayFsm.expectMsgType[SendPaymentToRoute] assert(childPayment.route == Right(singleRoute)) assert(childPayment.targetExpiry == expiry) - assert(childPayment.paymentSecret == payment.paymentSecret) + assert(payment.recipients.contains(childPayment.targetRecipient)) assert(childPayment.amount == finalAmount) assert(childPayment.totalAmount == finalAmount) assert(payFsm.stateName == PAYMENT_IN_PROGRESS) @@ -113,22 +112,23 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS import f._ assert(payFsm.stateName == WAIT_FOR_PAYMENT_REQUEST) - val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, 1200000 msat, expiry, 1, Some(hex"012345"), routeParams = routeParams.copy(randomize = false)) + val recipient = eRecipient.copy(paymentMetadata_opt = Some(hex"012345")) + val payment = SendMultiPartPayment(sender.ref, Seq(recipient), 1200000 msat, expiry, 1, routeParams = routeParams.copy(randomize = false)) sender.send(payFsm, payment) - router.expectMsg(RouteRequest(nodeParams.nodeId, e, 1200000 msat, maxFee, routeParams = routeParams.copy(randomize = false), allowMultiPart = true, paymentContext = Some(cfg.paymentContext))) + router.expectMsg(RouteRequest(nodeParams.nodeId, Seq(recipient), 1200000 msat, maxFee, routeParams = routeParams.copy(randomize = false), allowMultiPart = true, paymentContext = Some(cfg.paymentContext))) assert(payFsm.stateName == WAIT_FOR_ROUTES) val routes = Seq( - Route(500000 msat, hop_ab_1 :: hop_be :: Nil, None), - Route(700000 msat, hop_ac_1 :: hop_ce :: Nil, None), + Route(500000 msat, hop_ab_1 :: hop_be :: Nil, recipient), + Route(700000 msat, hop_ac_1 :: hop_ce :: Nil, recipient), ) router.send(payFsm, RouteResponse(routes)) val childPayments = childPayFsm.expectMsgType[SendPaymentToRoute] :: childPayFsm.expectMsgType[SendPaymentToRoute] :: Nil assert(childPayments.map(_.route).toSet == routes.map(r => Right(r)).toSet) assert(childPayments.map(_.targetExpiry).toSet == Set(expiry)) - assert(childPayments.map(_.paymentSecret).toSet == Set(payment.paymentSecret)) - assert(childPayments.map(_.paymentMetadata).toSet == Set(Some(hex"012345"))) + assert(childPayments.map(_.targetRecipient.asInstanceOf[ClearRecipient].paymentSecret).toSet == Set(recipient.paymentSecret)) + assert(childPayments.map(_.targetRecipient.asInstanceOf[ClearRecipient].paymentMetadata_opt).toSet == Set(Some(hex"012345"))) assert(childPayments.map(_.amount).toSet == Set(500000 msat, 700000 msat)) assert(childPayments.map(_.totalAmount).toSet == Set(1200000 msat)) assert(payFsm.stateName == PAYMENT_IN_PROGRESS) @@ -152,14 +152,15 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS // We include a bunch of additional tlv records. val trampolineTlv = OnionPaymentPayloadTlv.TrampolineOnion(OnionRoutingPacket(0, ByteVector.fill(33)(0), ByteVector.fill(400)(0), randomBytes32())) val userCustomTlv = GenericTlv(UInt64(561), hex"deadbeef") - val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount + 1000.msat, expiry, 1, None, routeParams = routeParams, additionalTlvs = Seq(trampolineTlv), userCustomTlvs = Seq(userCustomTlv)) + val recipient = eRecipient.copy(additionalTlvs = Seq(trampolineTlv), userCustomTlvs = Seq(userCustomTlv)) + val payment = SendMultiPartPayment(sender.ref, Seq(recipient), finalAmount + 1000.msat, expiry, 1, routeParams = routeParams) sender.send(payFsm, payment) router.expectMsgType[RouteRequest] - router.send(payFsm, RouteResponse(Seq(Route(500000 msat, hop_ab_1 :: hop_be :: Nil, None), Route(501000 msat, hop_ac_1 :: hop_ce :: Nil, None)))) + router.send(payFsm, RouteResponse(Seq(Route(500000 msat, hop_ab_1 :: hop_be :: Nil, recipient), Route(501000 msat, hop_ac_1 :: hop_ce :: Nil, recipient)))) val childPayments = childPayFsm.expectMsgType[SendPaymentToRoute] :: childPayFsm.expectMsgType[SendPaymentToRoute] :: Nil childPayments.foreach(p => { - assert(p.additionalTlvs.contains(trampolineTlv)) - assert(p.userCustomTlvs == Seq(userCustomTlv)) + assert(p.targetRecipient.additionalTlvs.contains(trampolineTlv)) + assert(p.targetRecipient.userCustomTlvs == Seq(userCustomTlv)) }) val result = fulfillPendingPayments(f, 2) @@ -176,10 +177,10 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS test("successful retry") { f => import f._ - val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 3, None, routeParams = routeParams) + val payment = SendMultiPartPayment(sender.ref, Seq(eRecipient), finalAmount, expiry, 3, routeParams = routeParams) sender.send(payFsm, payment) router.expectMsgType[RouteRequest] - val failingRoute = Route(finalAmount, hop_ab_1 :: hop_be :: Nil, None) + val failingRoute = Route(finalAmount, hop_ab_1 :: hop_be :: Nil, eRecipient) router.send(payFsm, RouteResponse(Seq(failingRoute))) childPayFsm.expectMsgType[SendPaymentToRoute] childPayFsm.expectNoMessage(100 millis) @@ -187,8 +188,8 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS val childId = payFsm.stateData.asInstanceOf[PaymentProgress].pending.keys.head childPayFsm.send(payFsm, PaymentFailed(childId, paymentHash, Seq(RemoteFailure(failingRoute.amount, failingRoute.clearHops, Sphinx.DecryptedFailurePacket(b, PermanentChannelFailure))))) // We retry ignoring the failing channel. - router.expectMsg(RouteRequest(nodeParams.nodeId, e, finalAmount, maxFee, routeParams = routeParams.copy(randomize = true), allowMultiPart = true, ignore = Ignore(Set.empty, Set(ChannelDesc(channelId_be, b, e))), paymentContext = Some(cfg.paymentContext))) - router.send(payFsm, RouteResponse(Seq(Route(400000 msat, hop_ac_1 :: hop_ce :: Nil, None), Route(600000 msat, hop_ad :: hop_de :: Nil, None)))) + router.expectMsg(RouteRequest(nodeParams.nodeId, Seq(eRecipient), finalAmount, maxFee, routeParams = routeParams.copy(randomize = true), allowMultiPart = true, ignore = Ignore(Set.empty, Set(ChannelDesc(channelId_be, b, e))), paymentContext = Some(cfg.paymentContext))) + router.send(payFsm, RouteResponse(Seq(Route(400000 msat, hop_ac_1 :: hop_ce :: Nil, eRecipient), Route(600000 msat, hop_ad :: hop_de :: Nil, eRecipient)))) childPayFsm.expectMsgType[SendPaymentToRoute] childPayFsm.expectMsgType[SendPaymentToRoute] assert(!payFsm.stateData.asInstanceOf[PaymentProgress].pending.contains(childId)) @@ -209,10 +210,10 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS test("retry failures while waiting for routes") { f => import f._ - val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 3, None, routeParams = routeParams) + val payment = SendMultiPartPayment(sender.ref, Seq(eRecipient), finalAmount, expiry, 3, routeParams = routeParams) sender.send(payFsm, payment) router.expectMsgType[RouteRequest] - router.send(payFsm, RouteResponse(Seq(Route(400000 msat, hop_ab_1 :: hop_be :: Nil, None), Route(600000 msat, hop_ab_2 :: hop_be :: Nil, None)))) + router.send(payFsm, RouteResponse(Seq(Route(400000 msat, hop_ab_1 :: hop_be :: Nil, eRecipient), Route(600000 msat, hop_ab_2 :: hop_be :: Nil, eRecipient)))) childPayFsm.expectMsgType[SendPaymentToRoute] childPayFsm.expectMsgType[SendPaymentToRoute] childPayFsm.expectNoMessage(100 millis) @@ -221,18 +222,18 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS childPayFsm.send(payFsm, PaymentFailed(failedId1, paymentHash, Seq(RemoteFailure(failedRoute1.amount, failedRoute1.clearHops, Sphinx.DecryptedFailurePacket(b, TemporaryNodeFailure))))) // When we retry, we ignore the failing node and we let the router know about the remaining pending route. - router.expectMsg(RouteRequest(nodeParams.nodeId, e, failedRoute1.amount, maxFee - failedRoute1.fee(false), ignore = Ignore(Set(b), Set.empty), pendingPayments = Seq(failedRoute2), allowMultiPart = true, routeParams = routeParams.copy(randomize = true), paymentContext = Some(cfg.paymentContext))) + router.expectMsg(RouteRequest(nodeParams.nodeId, Seq(eRecipient), failedRoute1.amount, maxFee - failedRoute1.fee(false), ignore = Ignore(Set(b), Set.empty), pendingPayments = Seq(failedRoute2), allowMultiPart = true, routeParams = routeParams.copy(randomize = true), paymentContext = Some(cfg.paymentContext))) // The second part fails while we're still waiting for new routes. childPayFsm.send(payFsm, PaymentFailed(failedId2, paymentHash, Seq(RemoteFailure(failedRoute2.amount, failedRoute2.clearHops, Sphinx.DecryptedFailurePacket(b, TemporaryNodeFailure))))) // We receive a response to our first request, but it's now obsolete: we re-sent a new route request that takes into // account the latest failures. - router.send(payFsm, RouteResponse(Seq(Route(failedRoute1.amount, hop_ac_1 :: hop_ce :: Nil, None)))) - router.expectMsg(RouteRequest(nodeParams.nodeId, e, finalAmount, maxFee, ignore = Ignore(Set(b), Set.empty), allowMultiPart = true, routeParams = routeParams.copy(randomize = true), paymentContext = Some(cfg.paymentContext))) + router.send(payFsm, RouteResponse(Seq(Route(failedRoute1.amount, hop_ac_1 :: hop_ce :: Nil, eRecipient)))) + router.expectMsg(RouteRequest(nodeParams.nodeId, Seq(eRecipient), finalAmount, maxFee, ignore = Ignore(Set(b), Set.empty), allowMultiPart = true, routeParams = routeParams.copy(randomize = true), paymentContext = Some(cfg.paymentContext))) awaitCond(payFsm.stateData.asInstanceOf[PaymentProgress].pending.isEmpty) childPayFsm.expectNoMessage(100 millis) // We receive new routes that work. - router.send(payFsm, RouteResponse(Seq(Route(300000 msat, hop_ac_1 :: hop_ce :: Nil, None), Route(700000 msat, hop_ad :: hop_de :: Nil, None)))) + router.send(payFsm, RouteResponse(Seq(Route(300000 msat, hop_ac_1 :: hop_ce :: Nil, eRecipient), Route(700000 msat, hop_ad :: hop_de :: Nil, eRecipient)))) childPayFsm.expectMsgType[SendPaymentToRoute] childPayFsm.expectMsgType[SendPaymentToRoute] @@ -251,10 +252,10 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS test("retry local channel failures") { f => import f._ - val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 3, None, routeParams = routeParams) + val payment = SendMultiPartPayment(sender.ref, Seq(eRecipient), finalAmount, expiry, 3, routeParams = routeParams) sender.send(payFsm, payment) router.expectMsgType[RouteRequest] - router.send(payFsm, RouteResponse(Seq(Route(finalAmount, hop_ab_1 :: hop_be :: Nil, None)))) + router.send(payFsm, RouteResponse(Seq(Route(finalAmount, hop_ab_1 :: hop_be :: Nil, eRecipient)))) childPayFsm.expectMsgType[SendPaymentToRoute] childPayFsm.expectNoMessage(100 millis) @@ -263,7 +264,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS // We retry without the failing channel. val expectedRouteRequest = RouteRequest( - nodeParams.nodeId, e, + nodeParams.nodeId, Seq(eRecipient), failedRoute.amount, maxFee, ignore = Ignore(Set.empty, Set(ChannelDesc(channelId_ab_1, a, b))), pendingPayments = Nil, @@ -276,10 +277,10 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS test("retry without ignoring channels") { f => import f._ - val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 3, None, routeParams = routeParams) + val payment = SendMultiPartPayment(sender.ref, Seq(eRecipient), finalAmount, expiry, 3, routeParams = routeParams) sender.send(payFsm, payment) router.expectMsgType[RouteRequest] - router.send(payFsm, RouteResponse(Seq(Route(500000 msat, hop_ab_1 :: hop_be :: Nil, None), Route(500000 msat, hop_ab_1 :: hop_be :: Nil, None)))) + router.send(payFsm, RouteResponse(Seq(Route(500000 msat, hop_ab_1 :: hop_be :: Nil, eRecipient), Route(500000 msat, hop_ab_1 :: hop_be :: Nil, eRecipient)))) childPayFsm.expectMsgType[SendPaymentToRoute] childPayFsm.expectMsgType[SendPaymentToRoute] childPayFsm.expectNoMessage(100 millis) @@ -290,7 +291,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS // If the router doesn't find routes, we will retry without ignoring the channel: it may work with a different split // of the amount to send. val expectedRouteRequest = RouteRequest( - nodeParams.nodeId, e, + nodeParams.nodeId, Seq(eRecipient), failedRoute.amount, maxFee - failedRoute.fee(false), ignore = Ignore(Set.empty, Set(ChannelDesc(channelId_ab_1, a, b))), pendingPayments = Seq(pendingRoute), @@ -301,7 +302,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS router.send(payFsm, Status.Failure(RouteNotFound)) router.expectMsg(expectedRouteRequest.copy(ignore = Ignore.empty)) - router.send(payFsm, RouteResponse(Seq(Route(500000 msat, hop_ac_1 :: hop_ce :: Nil, None)))) + router.send(payFsm, RouteResponse(Seq(Route(500000 msat, hop_ac_1 :: hop_ce :: Nil, eRecipient)))) childPayFsm.expectMsgType[SendPaymentToRoute] val result = fulfillPendingPayments(f, 2) @@ -320,10 +321,10 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS // The B -> E channel is private and provided in the invoice routing hints. val extraEdge = Invoice.BasicEdge(b, e, hop_be.shortChannelId, hop_be.params.relayFees.feeBase, hop_be.params.relayFees.feeProportionalMillionths, hop_be.params.cltvExpiryDelta) - val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 3, None, routeParams = routeParams, extraEdges = List(extraEdge)) + val payment = SendMultiPartPayment(sender.ref, Seq(eRecipient), finalAmount, expiry, 3, routeParams = routeParams, extraEdges = List(extraEdge)) sender.send(payFsm, payment) assert(router.expectMsgType[RouteRequest].extraEdges.head == extraEdge) - val route = Route(finalAmount, hop_ab_1 :: hop_be :: Nil, None) + val route = Route(finalAmount, hop_ab_1 :: hop_be :: Nil, eRecipient) router.send(payFsm, RouteResponse(Seq(route))) childPayFsm.expectMsgType[SendPaymentToRoute] childPayFsm.expectNoMessage(100 millis) @@ -342,10 +343,10 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS // The B -> E channel is private and provided in the invoice routing hints. val extraEdge = Invoice.BasicEdge(b, e, hop_be.shortChannelId, hop_be.params.relayFees.feeBase, hop_be.params.relayFees.feeProportionalMillionths, hop_be.params.cltvExpiryDelta) - val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 3, None, routeParams = routeParams, extraEdges = List(extraEdge)) + val payment = SendMultiPartPayment(sender.ref, Seq(eRecipient), finalAmount, expiry, 3, routeParams = routeParams, extraEdges = List(extraEdge)) sender.send(payFsm, payment) assert(router.expectMsgType[RouteRequest].extraEdges.head == extraEdge) - val route = Route(finalAmount, hop_ab_1 :: hop_be :: Nil, None) + val route = Route(finalAmount, hop_ab_1 :: hop_be :: Nil, eRecipient) router.send(payFsm, RouteResponse(Seq(route))) childPayFsm.expectMsgType[SendPaymentToRoute] childPayFsm.expectNoMessage(100 millis) @@ -402,17 +403,17 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS test("abort after too many failed attempts") { f => import f._ - val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 2, None, routeParams = routeParams) + val payment = SendMultiPartPayment(sender.ref, Seq(eRecipient), finalAmount, expiry, 2, routeParams = routeParams) sender.send(payFsm, payment) router.expectMsgType[RouteRequest] - router.send(payFsm, RouteResponse(Seq(Route(500000 msat, hop_ab_1 :: hop_be :: Nil, None), Route(500000 msat, hop_ac_1 :: hop_ce :: Nil, None)))) + router.send(payFsm, RouteResponse(Seq(Route(500000 msat, hop_ab_1 :: hop_be :: Nil, eRecipient), Route(500000 msat, hop_ac_1 :: hop_ce :: Nil, eRecipient)))) childPayFsm.expectMsgType[SendPaymentToRoute] childPayFsm.expectMsgType[SendPaymentToRoute] val (failedId1, failedRoute1) = payFsm.stateData.asInstanceOf[PaymentProgress].pending.head childPayFsm.send(payFsm, PaymentFailed(failedId1, paymentHash, Seq(UnreadableRemoteFailure(failedRoute1.amount, failedRoute1.clearHops)))) router.expectMsgType[RouteRequest] - router.send(payFsm, RouteResponse(Seq(Route(500000 msat, hop_ad :: hop_de :: Nil, None)))) + router.send(payFsm, RouteResponse(Seq(Route(500000 msat, hop_ad :: hop_de :: Nil, eRecipient)))) childPayFsm.expectMsgType[SendPaymentToRoute] assert(!payFsm.stateData.asInstanceOf[PaymentProgress].pending.contains(failedId1)) @@ -433,7 +434,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS import f._ sender.watch(payFsm) - val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 5, None, routeParams = routeParams) + val payment = SendMultiPartPayment(sender.ref, Seq(eRecipient), finalAmount, expiry, 5, routeParams = routeParams) sender.send(payFsm, payment) router.expectMsgType[RouteRequest] router.send(payFsm, Status.Failure(RouteNotFound)) @@ -463,10 +464,10 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS test("abort if recipient sends error") { f => import f._ - val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 5, None, routeParams = routeParams) + val payment = SendMultiPartPayment(sender.ref, Seq(eRecipient), finalAmount, expiry, 5, routeParams = routeParams) sender.send(payFsm, payment) router.expectMsgType[RouteRequest] - router.send(payFsm, RouteResponse(Seq(Route(finalAmount, hop_ab_1 :: hop_be :: Nil, None)))) + router.send(payFsm, RouteResponse(Seq(Route(finalAmount, hop_ab_1 :: hop_be :: Nil, eRecipient)))) childPayFsm.expectMsgType[SendPaymentToRoute] val (failedId, failedRoute) = payFsm.stateData.asInstanceOf[PaymentProgress].pending.head @@ -484,10 +485,10 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS test("abort if payment gets settled on chain") { f => import f._ - val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 5, None, routeParams = routeParams) + val payment = SendMultiPartPayment(sender.ref, Seq(eRecipient), finalAmount, expiry, 5, routeParams = routeParams) sender.send(payFsm, payment) router.expectMsgType[RouteRequest] - router.send(payFsm, RouteResponse(Seq(Route(finalAmount, hop_ab_1 :: hop_be :: Nil, None)))) + router.send(payFsm, RouteResponse(Seq(Route(finalAmount, hop_ab_1 :: hop_be :: Nil, eRecipient)))) childPayFsm.expectMsgType[SendPaymentToRoute] val (failedId, failedRoute) = payFsm.stateData.asInstanceOf[PaymentProgress].pending.head @@ -498,10 +499,10 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS test("abort if recipient sends error during retry") { f => import f._ - val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 5, None, routeParams = routeParams) + val payment = SendMultiPartPayment(sender.ref, Seq(eRecipient), finalAmount, expiry, 5, routeParams = routeParams) sender.send(payFsm, payment) router.expectMsgType[RouteRequest] - router.send(payFsm, RouteResponse(Seq(Route(400000 msat, hop_ab_1 :: hop_be :: Nil, None), Route(600000 msat, hop_ac_1 :: hop_ce :: Nil, None)))) + router.send(payFsm, RouteResponse(Seq(Route(400000 msat, hop_ab_1 :: hop_be :: Nil, eRecipient), Route(600000 msat, hop_ac_1 :: hop_ce :: Nil, eRecipient)))) childPayFsm.expectMsgType[SendPaymentToRoute] childPayFsm.expectMsgType[SendPaymentToRoute] @@ -516,10 +517,10 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS test("receive partial success after retriable failure (recipient spec violation)") { f => import f._ - val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 5, None, routeParams = routeParams) + val payment = SendMultiPartPayment(sender.ref, Seq(eRecipient), finalAmount, expiry, 5, routeParams = routeParams) sender.send(payFsm, payment) router.expectMsgType[RouteRequest] - router.send(payFsm, RouteResponse(Seq(Route(400000 msat, hop_ab_1 :: hop_be :: Nil, None), Route(600000 msat, hop_ac_1 :: hop_ce :: Nil, None)))) + router.send(payFsm, RouteResponse(Seq(Route(400000 msat, hop_ab_1 :: hop_be :: Nil, eRecipient), Route(600000 msat, hop_ac_1 :: hop_ce :: Nil, eRecipient)))) childPayFsm.expectMsgType[SendPaymentToRoute] childPayFsm.expectMsgType[SendPaymentToRoute] @@ -536,10 +537,10 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS test("receive partial success after abort (recipient spec violation)") { f => import f._ - val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 5, None, routeParams = routeParams) + val payment = SendMultiPartPayment(sender.ref, Seq(eRecipient), finalAmount, expiry, 5, routeParams = routeParams) sender.send(payFsm, payment) router.expectMsgType[RouteRequest] - router.send(payFsm, RouteResponse(Seq(Route(400000 msat, hop_ab_1 :: hop_be :: Nil, None), Route(600000 msat, hop_ac_1 :: hop_ce :: Nil, None)))) + router.send(payFsm, RouteResponse(Seq(Route(400000 msat, hop_ab_1 :: hop_be :: Nil, eRecipient), Route(600000 msat, hop_ac_1 :: hop_ce :: Nil, eRecipient)))) childPayFsm.expectMsgType[SendPaymentToRoute] childPayFsm.expectMsgType[SendPaymentToRoute] @@ -569,10 +570,10 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS test("receive partial failure after success (recipient spec violation)") { f => import f._ - val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 5, None, routeParams = routeParams) + val payment = SendMultiPartPayment(sender.ref, Seq(eRecipient), finalAmount, expiry, 5, routeParams = routeParams) sender.send(payFsm, payment) router.expectMsgType[RouteRequest] - router.send(payFsm, RouteResponse(Seq(Route(400000 msat, hop_ab_1 :: hop_be :: Nil, None), Route(600000 msat, hop_ac_1 :: hop_ce :: Nil, None)))) + router.send(payFsm, RouteResponse(Seq(Route(400000 msat, hop_ab_1 :: hop_be :: Nil, eRecipient), Route(600000 msat, hop_ac_1 :: hop_ce :: Nil, eRecipient)))) childPayFsm.expectMsgType[SendPaymentToRoute] childPayFsm.expectMsgType[SendPaymentToRoute] @@ -711,4 +712,6 @@ object MultiPartPaymentLifecycleSpec { val hop_ad = channelHopFromUpdate(a, d, channelUpdate_ad) val hop_de = channelHopFromUpdate(d, e, channelUpdate_de) + + val eRecipient = ClearRecipient(e, randomBytes32(), None) } \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala index 900e3cccc4..a811af4f30 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala @@ -105,7 +105,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val sendPayment = payFsm.expectMsgType[PaymentLifecycle.SendPayment] assert(sendPayment.amount == finalAmount) assert(sendPayment.targetExpiry == req.invoice.minFinalCltvExpiryDelta.toCltvExpiry(nodeParams.currentBlockHeight + 1)) - assert(sendPayment.userCustomTlvs == customRecords) + assert(sendPayment.targetRecipients.head.userCustomTlvs == customRecords) } test("forward keysend payment") { f => @@ -117,8 +117,8 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val sendPayment = payFsm.expectMsgType[PaymentLifecycle.SendPayment] assert(sendPayment.amount == finalAmount) assert(sendPayment.targetExpiry == Channel.MIN_CLTV_EXPIRY_DELTA.toCltvExpiry(nodeParams.currentBlockHeight + 1)) - assert(sendPayment.additionalTlvs.contains(KeySend(paymentPreimage))) - assert(sendPayment.userCustomTlvs.isEmpty) + assert(sendPayment.targetRecipients.head.additionalTlvs.contains(KeySend(paymentPreimage))) + assert(sendPayment.targetRecipients.head.userCustomTlvs.isEmpty) } test("reject payment with unsupported mandatory feature") { f => @@ -151,11 +151,11 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val finalExpiryDelta = CltvExpiryDelta(36) val invoice = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some invoice"), finalExpiryDelta) val route = PredefinedNodeRoute(Seq(a, b, c)) - val request = SendPaymentToRoute(finalAmount, finalAmount, invoice, route, None, None, None, 0 msat, CltvExpiryDelta(0), Nil) + val request = SendPaymentToRoute(finalAmount, finalAmount, invoice, route, None, None) sender.send(initiator, request) val payment = sender.expectMsgType[SendPaymentToRouteResponse] - payFsm.expectMsg(SendPaymentConfig(payment.paymentId, payment.parentId, None, paymentHash, finalAmount, c, Upstream.Local(payment.paymentId), Some(invoice), storeInDb = true, publishEvent = true, recordPathFindingMetrics = false, Nil)) - payFsm.expectMsg(PaymentLifecycle.SendPaymentToRoute(initiator, Left(route), finalAmount, finalAmount, finalExpiryDelta.toCltvExpiry(nodeParams.currentBlockHeight + 1), invoice.paymentSecret.get, invoice.paymentMetadata)) + payFsm.expectMsg(SendPaymentConfig(payment.paymentId, payment.parentId, None, paymentHash, finalAmount, Seq(invoice.nodeId), Upstream.Local(payment.paymentId), Some(invoice), storeInDb = true, publishEvent = true, recordPathFindingMetrics = false, Nil)) + payFsm.expectMsg(PaymentLifecycle.SendPaymentToRoute(initiator, Left(route), invoice.recipient, finalAmount, finalAmount, finalExpiryDelta.toCltvExpiry(nodeParams.currentBlockHeight + 1))) sender.send(initiator, GetPayment(Left(payment.paymentId))) sender.expectMsg(PaymentIsPending(payment.paymentId, invoice.paymentHash, PendingPaymentToRoute(sender.ref, request))) @@ -179,8 +179,8 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(req.finalExpiry(nodeParams.currentBlockHeight) == (finalExpiryDelta + 1).toCltvExpiry(nodeParams.currentBlockHeight)) sender.send(initiator, req) val id = sender.expectMsgType[UUID] - payFsm.expectMsg(SendPaymentConfig(id, id, None, paymentHash, finalAmount, c, Upstream.Local(id), Some(invoice), storeInDb = true, publishEvent = true, recordPathFindingMetrics = true, Nil)) - payFsm.expectMsg(PaymentLifecycle.SendPaymentToNode(initiator, c, finalAmount, finalAmount, req.finalExpiry(nodeParams.currentBlockHeight), invoice.paymentSecret.get, None, 1, routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams)) + payFsm.expectMsg(SendPaymentConfig(id, id, None, paymentHash, finalAmount, Seq(invoice.nodeId), Upstream.Local(id), Some(invoice), storeInDb = true, publishEvent = true, recordPathFindingMetrics = true, Nil)) + payFsm.expectMsg(PaymentLifecycle.SendPaymentToNode(initiator, invoice.recipients, finalAmount, finalAmount, req.finalExpiry(nodeParams.currentBlockHeight), 1, routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams)) sender.send(initiator, GetPayment(Left(id))) sender.expectMsg(PaymentIsPending(id, invoice.paymentHash, PendingPaymentToNode(sender.ref, req))) @@ -202,8 +202,8 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val req = SendPaymentToNode(finalAmount + 100.msat, invoice, 1, routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) sender.send(initiator, req) val id = sender.expectMsgType[UUID] - multiPartPayFsm.expectMsg(SendPaymentConfig(id, id, None, paymentHash, finalAmount + 100.msat, c, Upstream.Local(id), Some(invoice), storeInDb = true, publishEvent = true, recordPathFindingMetrics = true, Nil)) - multiPartPayFsm.expectMsg(SendMultiPartPayment(initiator, invoice.paymentSecret.get, c, finalAmount + 100.msat, req.finalExpiry(nodeParams.currentBlockHeight), 1, invoice.paymentMetadata, routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams)) + multiPartPayFsm.expectMsg(SendPaymentConfig(id, id, None, paymentHash, finalAmount + 100.msat, Seq(invoice.nodeId), Upstream.Local(id), Some(invoice), storeInDb = true, publishEvent = true, recordPathFindingMetrics = true, Nil)) + multiPartPayFsm.expectMsg(SendMultiPartPayment(initiator, invoice.recipients, finalAmount + 100.msat, req.finalExpiry(nodeParams.currentBlockHeight), 1, routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams)) sender.send(initiator, GetPayment(Left(id))) sender.expectMsg(PaymentIsPending(id, invoice.paymentHash, PendingPaymentToNode(sender.ref, req))) @@ -223,16 +223,16 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike import f._ val invoice = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some invoice"), CltvExpiryDelta(18), features = featuresWithMpp) val route = PredefinedChannelRoute(c, Seq(channelUpdate_ab.shortChannelId, channelUpdate_bc.shortChannelId)) - val req = SendPaymentToRoute(finalAmount / 2, finalAmount, invoice, route, None, None, None, 0 msat, CltvExpiryDelta(0), Nil) + val req = SendPaymentToRoute(finalAmount / 2, finalAmount, invoice, route, None, None) sender.send(initiator, req) val payment = sender.expectMsgType[SendPaymentToRouteResponse] - payFsm.expectMsg(SendPaymentConfig(payment.paymentId, payment.parentId, None, paymentHash, finalAmount, c, Upstream.Local(payment.paymentId), Some(invoice), storeInDb = true, publishEvent = true, recordPathFindingMetrics = false, Nil)) + payFsm.expectMsg(SendPaymentConfig(payment.paymentId, payment.parentId, None, paymentHash, finalAmount, Seq(invoice.nodeId), Upstream.Local(payment.paymentId), Some(invoice), storeInDb = true, publishEvent = true, recordPathFindingMetrics = false, Nil)) val msg = payFsm.expectMsgType[PaymentLifecycle.SendPaymentToRoute] assert(msg.replyTo == initiator) assert(msg.route == Left(route)) assert(msg.amount == finalAmount / 2) assert(msg.targetExpiry == req.finalExpiry(nodeParams.currentBlockHeight)) - assert(msg.paymentSecret == invoice.paymentSecret.get) + assert(msg.targetRecipient.asInstanceOf[ClearRecipient].paymentSecret == invoice.paymentSecret) assert(msg.totalAmount == finalAmount) sender.send(initiator, GetPayment(Left(payment.paymentId))) @@ -265,15 +265,15 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike sender.expectMsg(PaymentIsPending(id, invoice.paymentHash, PendingTrampolinePayment(sender.ref, Nil, req))) val msg = multiPartPayFsm.expectMsgType[SendMultiPartPayment] - assert(msg.paymentSecret !== invoice.paymentSecret.get) // we should not leak the invoice secret to the trampoline node - assert(msg.targetNodeId == b) + assert(msg.recipients.head.asInstanceOf[ClearRecipient].paymentSecret !== invoice.paymentSecret) // we should not leak the invoice secret to the trampoline node + assert(msg.recipients.head.nodeId == b) assert(msg.targetExpiry.toLong == currentBlockCount + 9 + 12 + 1) assert(msg.totalAmount == finalAmount + trampolineFees) - assert(msg.additionalTlvs.head.isInstanceOf[OnionPaymentPayloadTlv.TrampolineOnion]) + assert(msg.recipients.head.asInstanceOf[ClearRecipient].additionalTlvs.head.isInstanceOf[OnionPaymentPayloadTlv.TrampolineOnion]) assert(msg.maxAttempts == nodeParams.maxPaymentAttempts) // Verify that the trampoline node can correctly peel the trampoline onion. - val trampolineOnion = msg.additionalTlvs.head.asInstanceOf[OnionPaymentPayloadTlv.TrampolineOnion].packet + val trampolineOnion = msg.recipients.head.asInstanceOf[ClearRecipient].additionalTlvs.head.asInstanceOf[OnionPaymentPayloadTlv.TrampolineOnion].packet val Right(decrypted) = Sphinx.peel(priv_b.privateKey, Some(invoice.paymentHash), trampolineOnion) assert(!decrypted.isLastPacket) val Right(trampolinePayload) = IntermediatePayload.NodeRelay.Standard.validate(PaymentOnionCodecs.perHopPayloadCodec.decode(decrypted.payload.bits).require.value) @@ -292,7 +292,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(finalPayload.amount == finalAmount) assert(finalPayload.totalAmount == finalAmount) assert(finalPayload.expiry.toLong == currentBlockCount + 9 + 1) - assert(finalPayload.paymentSecret == invoice.paymentSecret.get) + assert(finalPayload.paymentSecret == invoice.paymentSecret) } test("forward trampoline to legacy payment") { f => @@ -305,14 +305,14 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike multiPartPayFsm.expectMsgType[SendPaymentConfig] val msg = multiPartPayFsm.expectMsgType[SendMultiPartPayment] - assert(msg.paymentSecret !== invoice.paymentSecret.get) // we should not leak the invoice secret to the trampoline node - assert(msg.targetNodeId == b) + assert(msg.recipients.head.asInstanceOf[ClearRecipient].paymentSecret !== invoice.paymentSecret) // we should not leak the invoice secret to the trampoline node + assert(msg.recipients.head.nodeId == b) assert(msg.targetExpiry.toLong == currentBlockCount + 9 + 12 + 1) assert(msg.totalAmount == finalAmount + trampolineFees) - assert(msg.additionalTlvs.head.isInstanceOf[OnionPaymentPayloadTlv.TrampolineOnion]) + assert(msg.recipients.head.asInstanceOf[ClearRecipient].additionalTlvs.head.isInstanceOf[OnionPaymentPayloadTlv.TrampolineOnion]) // Verify that the trampoline node can correctly peel the trampoline onion. - val trampolineOnion = msg.additionalTlvs.head.asInstanceOf[OnionPaymentPayloadTlv.TrampolineOnion].packet + val trampolineOnion = msg.recipients.head.asInstanceOf[ClearRecipient].additionalTlvs.head.asInstanceOf[OnionPaymentPayloadTlv.TrampolineOnion].packet val Right(decrypted) = Sphinx.peel(priv_b.privateKey, Some(invoice.paymentHash), trampolineOnion) assert(!decrypted.isLastPacket) val Right(trampolinePayload) = IntermediatePayload.NodeRelay.Standard.validate(PaymentOnionCodecs.perHopPayloadCodec.decode(decrypted.payload.bits).require.value) @@ -320,7 +320,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(trampolinePayload.totalAmount == finalAmount) assert(trampolinePayload.outgoingCltv.toLong == currentBlockCount + 9 + 1) assert(trampolinePayload.outgoingNodeId == c) - assert(trampolinePayload.paymentSecret == invoice.paymentSecret) + assert(trampolinePayload.paymentSecret.get == invoice.paymentSecret) assert(trampolinePayload.invoiceFeatures.contains(hex"4100")) // var_onion_optin, payment_secret } @@ -451,17 +451,17 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val invoice = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some invoice"), CltvExpiryDelta(18)) val trampolineFees = 100 msat val route = PredefinedNodeRoute(Seq(a, b)) - val req = SendPaymentToRoute(finalAmount + trampolineFees, finalAmount, invoice, route, None, None, None, trampolineFees, CltvExpiryDelta(144), Seq(b, c)) + val req = SendTrampolinePaymentToRoute(finalAmount + trampolineFees, finalAmount, invoice, route, None, None, None, trampolineFees, CltvExpiryDelta(144), Seq(b, c)) sender.send(initiator, req) val payment = sender.expectMsgType[SendPaymentToRouteResponse] assert(payment.trampolineSecret.nonEmpty) - payFsm.expectMsg(SendPaymentConfig(payment.paymentId, payment.parentId, None, paymentHash, finalAmount, c, Upstream.Local(payment.paymentId), Some(invoice), storeInDb = true, publishEvent = true, recordPathFindingMetrics = false, Seq(NodeHop(b, c, CltvExpiryDelta(0), 0 msat)))) + payFsm.expectMsg(SendPaymentConfig(payment.paymentId, payment.parentId, None, paymentHash, finalAmount, Seq(invoice.nodeId), Upstream.Local(payment.paymentId), Some(invoice), storeInDb = true, publishEvent = true, recordPathFindingMetrics = false, Seq(NodeHop(b, c, CltvExpiryDelta(0), 0 msat)))) val msg = payFsm.expectMsgType[PaymentLifecycle.SendPaymentToRoute] assert(msg.route == Left(route)) assert(msg.amount == finalAmount + trampolineFees) - assert(msg.paymentSecret == payment.trampolineSecret.get) + assert(msg.targetRecipient.asInstanceOf[ClearRecipient].paymentSecret == payment.trampolineSecret.get) assert(msg.totalAmount == finalAmount + trampolineFees) - val trampolineOnion = msg.additionalTlvs.collectFirst{case t:OnionPaymentPayloadTlv.TrampolineOnion => t} + val trampolineOnion = msg.targetRecipient.additionalTlvs.collectFirst{case t:OnionPaymentPayloadTlv.TrampolineOnion => t} assert(trampolineOnion.nonEmpty) // Verify that the trampoline node can correctly peel the trampoline onion. @@ -471,7 +471,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(trampolinePayload.amountToForward == finalAmount) assert(trampolinePayload.totalAmount == finalAmount) assert(trampolinePayload.outgoingNodeId == c) - assert(trampolinePayload.paymentSecret == invoice.paymentSecret) + assert(trampolinePayload.paymentSecret.get == invoice.paymentSecret) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala index 1a640d2ff8..30cef8d07f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala @@ -66,7 +66,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val defaultInvoice = Bolt11Invoice(Block.RegtestGenesisBlock.hash, None, defaultPaymentHash, priv_d, Left("test"), Channel.MIN_CLTV_EXPIRY_DELTA) val defaultRouteParams = TestConstants.Alice.nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams - def defaultRouteRequest(source: PublicKey, target: PublicKey, cfg: SendPaymentConfig): RouteRequest = RouteRequest(source, target, defaultAmountMsat, defaultMaxFee, paymentContext = Some(cfg.paymentContext), routeParams = defaultRouteParams) + def defaultRouteRequest(source: PublicKey, target: Seq[Recipient], cfg: SendPaymentConfig): RouteRequest = RouteRequest(source, target, defaultAmountMsat, defaultMaxFee, paymentContext = Some(cfg.paymentContext), routeParams = defaultRouteParams) case class PaymentFixture(cfg: SendPaymentConfig, nodeParams: NodeParams, @@ -81,7 +81,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { def createPaymentLifecycle(storeInDb: Boolean = true, publishEvent: Boolean = true, recordMetrics: Boolean = true): PaymentFixture = { val (id, parentId) = (UUID.randomUUID(), UUID.randomUUID()) val nodeParams = TestConstants.Alice.nodeParams.copy(nodeKeyManager = testNodeKeyManager, channelKeyManager = testChannelKeyManager) - val cfg = SendPaymentConfig(id, parentId, Some(defaultExternalId), defaultPaymentHash, defaultAmountMsat, d, Upstream.Local(id), Some(defaultInvoice), storeInDb, publishEvent, recordMetrics, Nil) + val cfg = SendPaymentConfig(id, parentId, Some(defaultExternalId), defaultPaymentHash, defaultAmountMsat, Seq(defaultInvoice.nodeId), Upstream.Local(id), Some(defaultInvoice), storeInDb, publishEvent, recordMetrics, Nil) val (routerForwarder, register, sender, monitor, eventListener, metricsListener) = (TestProbe(), TestProbe(), TestProbe(), TestProbe(), TestProbe(), TestProbe()) val paymentFSM = TestFSMRef(new PaymentLifecycle(nodeParams, cfg, routerForwarder.ref, register.ref)) paymentFSM ! SubscribeTransitionCallBack(monitor.ref) @@ -104,8 +104,8 @@ class PaymentLifecycleSpec extends BaseRouterSpec { import cfg._ // pre-computed route going from A to D - val route = Route(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: channelHopFromUpdate(b, c, update_bc) :: channelHopFromUpdate(c, d, update_cd) :: Nil, None) - val request = SendPaymentToRoute(sender.ref, Right(route), defaultAmountMsat, defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata) + val route = Route(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: channelHopFromUpdate(b, c, update_bc) :: channelHopFromUpdate(c, d, update_cd) :: Nil, defaultInvoice.recipient) + val request = SendPaymentToRoute(sender.ref, Right(route), defaultInvoice.recipient, defaultAmountMsat, defaultAmountMsat, defaultExpiry) sender.send(paymentFSM, request) routerForwarder.expectNoMessage(100 millis) // we don't need the router, we have the pre-computed route val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]]) @@ -133,10 +133,10 @@ class PaymentLifecycleSpec extends BaseRouterSpec { // pre-computed route going from A to D val route = PredefinedNodeRoute(Seq(a, b, c, d)) - val request = SendPaymentToRoute(sender.ref, Left(route), defaultAmountMsat, defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata) + val request = SendPaymentToRoute(sender.ref, Left(route), defaultInvoice.recipient, defaultAmountMsat, defaultAmountMsat, defaultExpiry) sender.send(paymentFSM, request) - routerForwarder.expectMsg(FinalizeRoute(defaultAmountMsat, route, paymentContext = Some(cfg.paymentContext))) + routerForwarder.expectMsg(FinalizeRoute(defaultAmountMsat, route, defaultInvoice.recipient, paymentContext = Some(cfg.paymentContext))) val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]]) routerForwarder.forward(routerFixture.router) @@ -159,7 +159,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val payFixture = createPaymentLifecycle(recordMetrics = false) import payFixture._ - val brokenRoute = SendPaymentToRoute(sender.ref, Left(PredefinedNodeRoute(Seq(randomKey().publicKey, randomKey().publicKey, randomKey().publicKey))), defaultAmountMsat, defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata) + val brokenRoute = SendPaymentToRoute(sender.ref, Left(PredefinedNodeRoute(Seq(randomKey().publicKey, randomKey().publicKey, randomKey().publicKey))), defaultInvoice.recipient, defaultAmountMsat, defaultAmountMsat, defaultExpiry) sender.send(paymentFSM, brokenRoute) routerForwarder.expectMsgType[FinalizeRoute] routerForwarder.forward(routerFixture.router) @@ -176,7 +176,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val payFixture = createPaymentLifecycle(recordMetrics = false) import payFixture._ - val brokenRoute = SendPaymentToRoute(sender.ref, Left(PredefinedChannelRoute(randomKey().publicKey, Seq(ShortChannelId(1), ShortChannelId(2)))), defaultAmountMsat, defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata) + val brokenRoute = SendPaymentToRoute(sender.ref, Left(PredefinedChannelRoute(randomKey().publicKey, Seq(ShortChannelId(1), ShortChannelId(2)))), defaultInvoice.recipient, defaultAmountMsat, defaultAmountMsat, defaultExpiry) sender.send(paymentFSM, brokenRoute) routerForwarder.expectMsgType[FinalizeRoute] routerForwarder.forward(routerFixture.router) @@ -197,10 +197,10 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val recipient = randomKey().publicKey val route = PredefinedNodeRoute(Seq(a, b, c, recipient)) val extraEdges = Seq(BasicEdge(c, recipient, ShortChannelId(561), 1 msat, 100, CltvExpiryDelta(144))) - val request = SendPaymentToRoute(sender.ref, Left(route), defaultAmountMsat, defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata, extraEdges) + val request = SendPaymentToRoute(sender.ref, Left(route), defaultInvoice.recipient, defaultAmountMsat, defaultAmountMsat, defaultExpiry, extraEdges) sender.send(paymentFSM, request) - routerForwarder.expectMsg(FinalizeRoute(defaultAmountMsat, route, extraEdges, paymentContext = Some(cfg.paymentContext))) + routerForwarder.expectMsg(FinalizeRoute(defaultAmountMsat, route, defaultInvoice.recipient, extraEdges, paymentContext = Some(cfg.paymentContext))) val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]]) routerForwarder.forward(routerFixture.router) @@ -223,7 +223,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { import payFixture._ import cfg._ - val request = SendPaymentToNode(sender.ref, f, defaultAmountMsat, defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata, 5, routeParams = defaultRouteParams) + val request = SendPaymentToNode(sender.ref, Seq(defaultInvoice.recipient.copy(nodeId = f)), defaultAmountMsat, defaultAmountMsat, defaultExpiry, 5, routeParams = defaultRouteParams) sender.send(paymentFSM, request) val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]]) val routeRequest = routerForwarder.expectMsgType[RouteRequest] @@ -256,7 +256,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { "my-test-experiment", experimentPercentage = 100 ).getDefaultRouteParams - val request = SendPaymentToNode(sender.ref, d, defaultAmountMsat, defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata, 5, routeParams = routeParams) + val request = SendPaymentToNode(sender.ref, defaultInvoice.recipients, defaultAmountMsat, defaultAmountMsat, defaultExpiry, 5, routeParams = routeParams) sender.send(paymentFSM, request) val routeRequest = routerForwarder.expectMsgType[RouteRequest] val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]]) @@ -281,7 +281,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { import cfg._ val paymentMetadataTooBig = ByteVector.fromValidHex("01" * 1300) - val request = SendPaymentToNode(sender.ref, d, defaultAmountMsat, defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, Some(paymentMetadataTooBig), 5, routeParams = defaultRouteParams) + val request = SendPaymentToNode(sender.ref, Seq(defaultInvoice.recipient.copy(paymentMetadata_opt = Some(paymentMetadataTooBig))), defaultAmountMsat, defaultAmountMsat, defaultExpiry, 5, routeParams = defaultRouteParams) sender.send(paymentFSM, request) val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]]) val routeRequest = routerForwarder.expectMsgType[RouteRequest] @@ -300,9 +300,9 @@ class PaymentLifecycleSpec extends BaseRouterSpec { import payFixture._ import cfg._ - val request = SendPaymentToNode(sender.ref, d, defaultAmountMsat, defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata, 2, routeParams = defaultRouteParams) + val request = SendPaymentToNode(sender.ref, defaultInvoice.recipients, defaultAmountMsat, defaultAmountMsat, defaultExpiry, 2, routeParams = defaultRouteParams) sender.send(paymentFSM, request) - routerForwarder.expectMsg(defaultRouteRequest(a, d, cfg)) + routerForwarder.expectMsg(defaultRouteRequest(a, defaultInvoice.recipients, cfg)) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) val WaitingForRoute(_, Nil, _) = paymentFSM.stateData @@ -315,7 +315,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFail(UpdateFailHtlc(ByteVector32.Zeroes, 0, randomBytes32())))) // unparsable message // then the payment lifecycle will ask for a new route excluding all intermediate nodes - routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d, cfg).copy(ignore = Ignore(Set(c), Set.empty))) + routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, defaultInvoice.recipients, cfg).copy(ignore = Ignore(Set(c), Set.empty))) // let's simulate a response by the router with another route sender.send(paymentFSM, RouteResponse(route :: Nil)) @@ -345,7 +345,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { import payFixture._ import cfg._ - val request = SendPaymentToNode(sender.ref, d, defaultAmountMsat, defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata, 2, routeParams = defaultRouteParams) + val request = SendPaymentToNode(sender.ref, defaultInvoice.recipients, defaultAmountMsat, defaultAmountMsat, defaultExpiry, 2, routeParams = defaultRouteParams) sender.send(paymentFSM, request) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) routerForwarder.expectMsgType[RouteRequest] @@ -357,7 +357,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { sender.send(paymentFSM, RES_ADD_FAILED(cmd1, ChannelUnavailable(ByteVector32.Zeroes), None)) // then the payment lifecycle will ask for a new route excluding the channel - routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d, cfg).copy(ignore = Ignore(Set.empty, Set(ChannelDesc(scid_ab, a, b))))) + routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, defaultInvoice.recipients, cfg).copy(ignore = Ignore(Set.empty, Set(ChannelDesc(scid_ab, a, b))))) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) // payment is still pending because the error is recoverable } @@ -366,7 +366,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { import payFixture._ import cfg._ - val request = SendPaymentToNode(sender.ref, d, defaultAmountMsat, defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata, 2, routeParams = defaultRouteParams) + val request = SendPaymentToNode(sender.ref, defaultInvoice.recipients, defaultAmountMsat, defaultAmountMsat, defaultExpiry, 2, routeParams = defaultRouteParams) sender.send(paymentFSM, request) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) routerForwarder.expectMsgType[RouteRequest] @@ -377,7 +377,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { register.send(paymentFSM, ForwardShortIdFailure(fwd)) // then the payment lifecycle will ask for a new route excluding the channel - routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d, cfg).copy(ignore = Ignore(Set.empty, Set(ChannelDesc(scid_ab, a, b))))) + routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, defaultInvoice.recipients, cfg).copy(ignore = Ignore(Set.empty, Set(ChannelDesc(scid_ab, a, b))))) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) // payment is still pending because the error is recoverable } @@ -386,12 +386,12 @@ class PaymentLifecycleSpec extends BaseRouterSpec { import payFixture._ import cfg._ - val request = SendPaymentToNode(sender.ref, d, defaultAmountMsat, defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata, 2, routeParams = defaultRouteParams) + val request = SendPaymentToNode(sender.ref, defaultInvoice.recipients, defaultAmountMsat, defaultAmountMsat, defaultExpiry, 2, routeParams = defaultRouteParams) sender.send(paymentFSM, request) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) val WaitingForRoute(_, Nil, _) = paymentFSM.stateData - routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d, cfg)) + routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, defaultInvoice.recipients, cfg)) routerForwarder.forward(routerFixture.router) awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) val WaitingForComplete(_, cmd1, Nil, _, _, _) = paymentFSM.stateData @@ -400,7 +400,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFailMalformed(UpdateFailMalformedHtlc(ByteVector32.Zeroes, 0, randomBytes32(), FailureMessageCodecs.BADONION)))) // then the payment lifecycle will ask for a new route excluding the channel - routerForwarder.expectMsg(defaultRouteRequest(a, d, cfg).copy(ignore = Ignore(Set.empty, Set(ChannelDesc(scid_ab, a, b))))) + routerForwarder.expectMsg(defaultRouteRequest(a, defaultInvoice.recipients, cfg).copy(ignore = Ignore(Set.empty, Set(ChannelDesc(scid_ab, a, b))))) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) } @@ -409,12 +409,12 @@ class PaymentLifecycleSpec extends BaseRouterSpec { import payFixture._ import cfg._ - val request = SendPaymentToNode(sender.ref, d, defaultAmountMsat, defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata, 2, routeParams = defaultRouteParams) + val request = SendPaymentToNode(sender.ref, defaultInvoice.recipients, defaultAmountMsat, defaultAmountMsat, defaultExpiry, 2, routeParams = defaultRouteParams) sender.send(paymentFSM, request) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) val WaitingForRoute(_, Nil, _) = paymentFSM.stateData - routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d, cfg)) + routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, defaultInvoice.recipients, cfg)) routerForwarder.forward(routerFixture.router) awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) val WaitingForComplete(_, cmd1, Nil, _, _, _) = paymentFSM.stateData @@ -432,12 +432,12 @@ class PaymentLifecycleSpec extends BaseRouterSpec { import payFixture._ import cfg._ - val request = SendPaymentToNode(sender.ref, d, defaultAmountMsat, defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata, 2, routeParams = defaultRouteParams) + val request = SendPaymentToNode(sender.ref, defaultInvoice.recipients, defaultAmountMsat, defaultAmountMsat, defaultExpiry, 2, routeParams = defaultRouteParams) sender.send(paymentFSM, request) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) val WaitingForRoute(_, Nil, _) = paymentFSM.stateData - routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d, cfg)) + routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, defaultInvoice.recipients, cfg)) routerForwarder.forward(routerFixture.router) awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) val WaitingForComplete(_, cmd1, Nil, _, _, _) = paymentFSM.stateData @@ -447,7 +447,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { sender.send(paymentFSM, addCompleted(HtlcResult.DisconnectedBeforeSigned(update_bc_disabled))) // then the payment lifecycle will ask for a new route excluding the channel - routerForwarder.expectMsg(defaultRouteRequest(a, d, cfg).copy(ignore = Ignore(Set.empty, Set(ChannelDesc(scid_ab, a, b))))) + routerForwarder.expectMsg(defaultRouteRequest(a, defaultInvoice.recipients, cfg).copy(ignore = Ignore(Set.empty, Set(ChannelDesc(scid_ab, a, b))))) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) } @@ -455,11 +455,11 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val payFixture = createPaymentLifecycle() import payFixture._ - val request = SendPaymentToNode(sender.ref, d, defaultAmountMsat, defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata, 2, routeParams = defaultRouteParams) + val request = SendPaymentToNode(sender.ref, defaultInvoice.recipients, defaultAmountMsat, defaultAmountMsat, defaultExpiry, 2, routeParams = defaultRouteParams) sender.send(paymentFSM, request) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE) val WaitingForRoute(_, Nil, _) = paymentFSM.stateData - routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d, cfg)) + routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, defaultInvoice.recipients, cfg)) routerForwarder.forward(routerFixture.router) awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) val WaitingForComplete(_, cmd1, Nil, sharedSecrets1, _, route) = paymentFSM.stateData @@ -475,7 +475,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { // payment lifecycle forwards the embedded channelUpdate to the router routerForwarder.expectMsg(update_bc) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE) - routerForwarder.expectMsg(defaultRouteRequest(a, d, cfg).copy(ignore = Ignore(Set.empty, Set(ChannelDesc(update_bc.shortChannelId, b, c))))) + routerForwarder.expectMsg(defaultRouteRequest(a, defaultInvoice.recipients, cfg).copy(ignore = Ignore(Set.empty, Set(ChannelDesc(update_bc.shortChannelId, b, c))))) routerForwarder.forward(routerFixture.router) // we allow 2 tries, so we send a 2nd request to the router assert(sender.expectMsgType[PaymentFailed].failures == RemoteFailure(route.amount, route.clearHops, Sphinx.DecryptedFailurePacket(b, failure)) :: LocalFailure(defaultAmountMsat, Nil, RouteNotFound) :: Nil) @@ -486,12 +486,12 @@ class PaymentLifecycleSpec extends BaseRouterSpec { import payFixture._ import cfg._ - val request = SendPaymentToNode(sender.ref, d, defaultAmountMsat, defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata, 5, routeParams = defaultRouteParams) + val request = SendPaymentToNode(sender.ref, defaultInvoice.recipients, defaultAmountMsat, defaultAmountMsat, defaultExpiry, 5, routeParams = defaultRouteParams) sender.send(paymentFSM, request) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) val WaitingForRoute(_, Nil, _) = paymentFSM.stateData - routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d, cfg)) + routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, defaultInvoice.recipients, cfg)) routerForwarder.forward(routerFixture.router) awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) val WaitingForComplete(_, cmd1, Nil, sharedSecrets1, _, route1) = paymentFSM.stateData @@ -506,7 +506,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { // payment lifecycle forwards the embedded channelUpdate to the router routerForwarder.expectMsg(channelUpdate_bc_modified) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) // 1 failure but not final, the payment is still PENDING - routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d, cfg)) + routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, defaultInvoice.recipients, cfg)) routerForwarder.forward(routerFixture.router) // router answers with a new route, taking into account the new update @@ -526,7 +526,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { // but it will still forward the embedded channelUpdate to the router routerForwarder.expectMsg(channelUpdate_bc_modified_2) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE) - routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d, cfg)) + routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, defaultInvoice.recipients, cfg)) routerForwarder.forward(routerFixture.router) // this time the router can't find a route: game over @@ -540,9 +540,9 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val payFixture = createPaymentLifecycle() import payFixture._ - val request = SendPaymentToNode(sender.ref, d, defaultAmountMsat, defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata, 1, routeParams = defaultRouteParams) + val request = SendPaymentToNode(sender.ref, defaultInvoice.recipients, defaultAmountMsat, defaultAmountMsat, defaultExpiry, 1, routeParams = defaultRouteParams) sender.send(paymentFSM, request) - routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d, cfg)) + routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, defaultInvoice.recipients, cfg)) routerForwarder.forward(routerFixture.router) awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) val WaitingForComplete(_, cmd1, Nil, sharedSecrets1, _, _) = paymentFSM.stateData @@ -571,12 +571,12 @@ class PaymentLifecycleSpec extends BaseRouterSpec { BasicEdge(c, d, scid_cd, update_cd.feeBaseMsat, update_cd.feeProportionalMillionths, update_cd.cltvExpiryDelta) ) - val request = SendPaymentToNode(sender.ref, d, defaultAmountMsat, defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata, 5, extraEdges = extraEdges, routeParams = defaultRouteParams) + val request = SendPaymentToNode(sender.ref, defaultInvoice.recipients, defaultAmountMsat, defaultAmountMsat, defaultExpiry, 5, extraEdges = extraEdges, routeParams = defaultRouteParams) sender.send(paymentFSM, request) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) val WaitingForRoute(_, Nil, _) = paymentFSM.stateData - routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d, cfg).copy(extraEdges = extraEdges)) + routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, defaultInvoice.recipients, cfg).copy(extraEdges = extraEdges)) routerForwarder.forward(routerFixture.router) awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) val WaitingForComplete(_, cmd1, Nil, sharedSecrets1, _, _) = paymentFSM.stateData @@ -595,7 +595,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { extraEdges(0).update(channelUpdate_bc_modified), extraEdges(1) ) - routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d, cfg).copy(extraEdges = extraEdges1)) + routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, defaultInvoice.recipients, cfg).copy(extraEdges = extraEdges1)) routerForwarder.forward(routerFixture.router) // router answers with a new route, taking into account the new update @@ -612,11 +612,11 @@ class PaymentLifecycleSpec extends BaseRouterSpec { // we build an assisted route for channel cd val extraEdges = Seq(BasicEdge(c, d, scid_cd, update_cd.feeBaseMsat, update_cd.feeProportionalMillionths, update_cd.cltvExpiryDelta)) - val request = SendPaymentToNode(sender.ref, d, defaultAmountMsat, defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata, 1, extraEdges = extraEdges, routeParams = defaultRouteParams) + val request = SendPaymentToNode(sender.ref, defaultInvoice.recipients, defaultAmountMsat, defaultAmountMsat, defaultExpiry, 1, extraEdges = extraEdges, routeParams = defaultRouteParams) sender.send(paymentFSM, request) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) - routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d, cfg).copy(extraEdges = extraEdges)) + routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, defaultInvoice.recipients, cfg).copy(extraEdges = extraEdges)) routerForwarder.forward(routerFixture.router) awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) val WaitingForComplete(_, cmd1, Nil, sharedSecrets1, _, _) = paymentFSM.stateData @@ -638,12 +638,12 @@ class PaymentLifecycleSpec extends BaseRouterSpec { import payFixture._ import cfg._ - val request = SendPaymentToNode(sender.ref, d, defaultAmountMsat, defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata, 2, routeParams = defaultRouteParams) + val request = SendPaymentToNode(sender.ref, defaultInvoice.recipients, defaultAmountMsat, defaultAmountMsat, defaultExpiry, 2, routeParams = defaultRouteParams) sender.send(paymentFSM, request) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) val WaitingForRoute(_, Nil, _) = paymentFSM.stateData - routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d, cfg)) + routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, defaultInvoice.recipients, cfg)) routerForwarder.forward(router) awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) val WaitingForComplete(_, cmd1, Nil, sharedSecrets1, _, route1) = paymentFSM.stateData @@ -653,7 +653,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { // payment lifecycle forwards the embedded channelUpdate to the router awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE) - routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d, cfg).copy(ignore = Ignore(Set.empty, Set(ChannelDesc(scid_bc, b, c))))) + routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, defaultInvoice.recipients, cfg).copy(ignore = Ignore(Set.empty, Set(ChannelDesc(scid_bc, b, c))))) routerForwarder.forward(router) // we allow 2 tries, so we send a 2nd request to the router, which won't find another route @@ -676,7 +676,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { import payFixture._ import cfg._ - val request = SendPaymentToNode(sender.ref, d, defaultAmountMsat, defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata, 5, routeParams = defaultRouteParams) + val request = SendPaymentToNode(sender.ref, defaultInvoice.recipients, defaultAmountMsat, defaultAmountMsat, defaultExpiry, 5, routeParams = defaultRouteParams) sender.send(paymentFSM, request) routerForwarder.expectMsgType[RouteRequest] val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]]) @@ -732,7 +732,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { import payFixture._ // we send a payment to H - val request = SendPaymentToNode(sender.ref, h, defaultAmountMsat, defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata, 5, routeParams = defaultRouteParams) + val request = SendPaymentToNode(sender.ref, Seq(defaultInvoice.recipient.copy(nodeId = h)), defaultAmountMsat, defaultAmountMsat, defaultExpiry, 5, routeParams = defaultRouteParams) sender.send(paymentFSM, request) routerForwarder.expectMsgType[RouteRequest] @@ -819,7 +819,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { import payFixture._ import cfg._ - val request = SendPaymentToNode(sender.ref, d, defaultAmountMsat, defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata, 3, routeParams = defaultRouteParams) + val request = SendPaymentToNode(sender.ref, defaultInvoice.recipients, defaultAmountMsat, defaultAmountMsat, defaultExpiry, 3, routeParams = defaultRouteParams) sender.send(paymentFSM, request) routerForwarder.expectMsgType[RouteRequest] val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]]) @@ -841,8 +841,8 @@ class PaymentLifecycleSpec extends BaseRouterSpec { import cfg._ // pre-computed route going from A to D - val route = Route(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: channelHopFromUpdate(b, c, update_bc) :: channelHopFromUpdate(c, d, update_cd) :: Nil, None) - val request = SendPaymentToRoute(sender.ref, Right(route), defaultAmountMsat, defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata) + val route = Route(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: channelHopFromUpdate(b, c, update_bc) :: channelHopFromUpdate(c, d, update_cd) :: Nil, defaultInvoice.recipient) + val request = SendPaymentToRoute(sender.ref, Right(route), defaultInvoice.recipient, defaultAmountMsat, defaultAmountMsat, defaultExpiry) sender.send(paymentFSM, request) routerForwarder.expectNoMessage(100 millis) // we don't need the router, we have the pre-computed route val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]]) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala index 8801ba455f..4607df4a20 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala @@ -28,7 +28,7 @@ import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.payment.IncomingPaymentPacket.{ChannelRelayPacket, FinalPacket, NodeRelayPacket, decrypt} import fr.acinq.eclair.payment.OutgoingPaymentPacket._ import fr.acinq.eclair.router.BaseRouterSpec.channelHopFromUpdate -import fr.acinq.eclair.router.Router.NodeHop +import fr.acinq.eclair.router.Router.{NodeHop, Route} import fr.acinq.eclair.transactions.Transactions.InputInfo import fr.acinq.eclair.wire.protocol.OnionPaymentPayloadTlv.{AmountToForward, OutgoingCltv, PaymentData} import fr.acinq.eclair.wire.protocol.PaymentOnion.{FinalPayload, IntermediatePayload} @@ -63,7 +63,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { } def testBuildOnion(): Unit = { - val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash, hops, None, finalAmount, 0 msat, finalExpiry, paymentSecret, None, Nil, Nil) + val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash, hops, ClearRecipient(e , paymentSecret, None), finalAmount, 0 msat, finalExpiry) assert(firstAmount == amount_ab) assert(firstExpiry == expiry_ab) assert(onion.packet.payload.length == PaymentOnionCodecs.paymentOnionPayloadLength) @@ -118,7 +118,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { } test("build a command including the onion") { - val Success((add, _, _)) = buildCommand(randomKey(), ActorRef.noSender, Upstream.Local(UUID.randomUUID), paymentHash, hops, None, finalAmount, finalAmount, finalExpiry, paymentSecret, None, Nil, Nil) + val Success((add, _, _)) = buildCommand(randomKey(), ActorRef.noSender, Upstream.Local(UUID.randomUUID), paymentHash, Route(finalAmount, hops, ClearRecipient(e, paymentSecret, None)), finalAmount, finalAmount, finalExpiry) assert(add.amount > finalAmount) assert(add.cltvExpiry == finalExpiry + channelUpdate_de.cltvExpiryDelta + channelUpdate_cd.cltvExpiryDelta + channelUpdate_bc.cltvExpiryDelta) assert(add.paymentHash == paymentHash) @@ -129,7 +129,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { } test("build a command with no hops") { - val Success((add, _, _)) = buildCommand(randomKey(), ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, hops.take(1), None, finalAmount, finalAmount, finalExpiry, paymentSecret, Some(paymentMetadata), Nil, Nil) + val Success((add, _, _)) = buildCommand(randomKey(), ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, Route(finalAmount, hops.take(1), ClearRecipient(b, paymentSecret, Some(paymentMetadata))), finalAmount, finalAmount, finalExpiry) assert(add.amount == finalAmount) assert(add.cltvExpiry == finalExpiry) assert(add.paymentHash == paymentHash) @@ -153,11 +153,11 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { // / \ / \ // a -> b -> c d e - val Success((amount_ac, expiry_ac, trampolineOnion)) = buildTrampolinePacket(paymentHash, trampolineHops, finalAmount, finalAmount * 3, finalExpiry, paymentSecret, Some(hex"010203"), Nil, Nil) + val Success((amount_ac, expiry_ac, trampolineOnion)) = buildTrampolinePacket(paymentHash, trampolineHops, ClearRecipient(e, paymentSecret, Some(hex"010203")), finalAmount, finalAmount * 3, finalExpiry) assert(amount_ac == amount_bc) assert(expiry_ac == expiry_bc) - val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash, trampolineChannelHops, None, amount_ac, amount_ac, expiry_ac, randomBytes32(), None, Seq(OnionPaymentPayloadTlv.TrampolineOnion(trampolineOnion.packet)), Nil) + val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash, trampolineChannelHops, ClearRecipient(c, randomBytes32(), None, additionalTlvs = Seq(OnionPaymentPayloadTlv.TrampolineOnion(trampolineOnion.packet))), amount_ac, amount_ac, expiry_ac) assert(firstAmount == amount_ab) assert(firstExpiry == expiry_ab) @@ -181,7 +181,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { assert(inner_c.paymentMetadata.isEmpty) // c forwards the trampoline payment to d. - val Success((amount_d, expiry_d, onion_d)) = buildPaymentPacket(paymentHash, channelHopFromUpdate(c, d, channelUpdate_cd) :: Nil, None, amount_cd, amount_cd, expiry_cd, randomBytes32(), None, Seq(OnionPaymentPayloadTlv.TrampolineOnion(packet_d)), Nil) + val Success((amount_d, expiry_d, onion_d)) = buildPaymentPacket(paymentHash, channelHopFromUpdate(c, d, channelUpdate_cd) :: Nil, ClearRecipient(d, randomBytes32(), None, additionalTlvs = Seq(OnionPaymentPayloadTlv.TrampolineOnion(packet_d))), amount_cd, amount_cd, expiry_cd) assert(amount_d == amount_cd) assert(expiry_d == expiry_cd) val add_d = UpdateAddHtlc(randomBytes32(), 3, amount_d, paymentHash, expiry_d, onion_d.packet, None) @@ -199,7 +199,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { assert(inner_d.paymentMetadata.isEmpty) // d forwards the trampoline payment to e. - val Success((amount_e, expiry_e, onion_e)) = buildPaymentPacket(paymentHash, channelHopFromUpdate(d, e, channelUpdate_de) :: Nil, None, amount_de, amount_de, expiry_de, randomBytes32(), None, Seq(OnionPaymentPayloadTlv.TrampolineOnion(packet_e)), Nil) + val Success((amount_e, expiry_e, onion_e)) = buildPaymentPacket(paymentHash, channelHopFromUpdate(d, e, channelUpdate_de) :: Nil, ClearRecipient(e, randomBytes32(), None, additionalTlvs = Seq(OnionPaymentPayloadTlv.TrampolineOnion(packet_e))), amount_de, amount_de, expiry_de) assert(amount_e == amount_de) assert(expiry_e == expiry_de) val add_e = UpdateAddHtlc(randomBytes32(), 4, amount_e, paymentHash, expiry_e, onion_e.packet, None) @@ -221,7 +221,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { assert(amount_ac == amount_bc) assert(expiry_ac == expiry_bc) - val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash, trampolineChannelHops, None, amount_ac, amount_ac, expiry_ac, randomBytes32(), None, Seq(OnionPaymentPayloadTlv.TrampolineOnion(trampolineOnion.packet)), Nil) + val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash, trampolineChannelHops, ClearRecipient(c, randomBytes32(), None, additionalTlvs = Seq(OnionPaymentPayloadTlv.TrampolineOnion(trampolineOnion.packet))), amount_ac, amount_ac, expiry_ac) assert(firstAmount == amount_ab) assert(firstExpiry == expiry_ab) @@ -242,7 +242,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { assert(inner_c.paymentSecret.isEmpty) // c forwards the trampoline payment to d. - val Success((amount_d, expiry_d, onion_d)) = buildPaymentPacket(paymentHash, channelHopFromUpdate(c, d, channelUpdate_cd) :: Nil, None, amount_cd, amount_cd, expiry_cd, randomBytes32(), None, Seq(OnionPaymentPayloadTlv.TrampolineOnion(packet_d)), Nil) + val Success((amount_d, expiry_d, onion_d)) = buildPaymentPacket(paymentHash, channelHopFromUpdate(c, d, channelUpdate_cd) :: Nil, ClearRecipient(d, randomBytes32(), None, additionalTlvs = Seq(OnionPaymentPayloadTlv.TrampolineOnion(packet_d))), amount_cd, amount_cd, expiry_cd) assert(amount_d == amount_cd) assert(expiry_d == expiry_cd) val add_d = UpdateAddHtlc(randomBytes32(), 3, amount_d, paymentHash, expiry_d, onion_d.packet, None) @@ -255,7 +255,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { assert(inner_d.outgoingCltv == expiry_de) assert(inner_d.outgoingNodeId == e) assert(inner_d.totalAmount == finalAmount) - assert(inner_d.paymentSecret == invoice.paymentSecret) + assert(inner_d.paymentSecret.get == invoice.paymentSecret) assert(inner_d.paymentMetadata.contains(hex"010203")) assert(inner_d.invoiceFeatures.contains(hex"024100")) // var_onion_optin, payment_secret, basic_mpp assert(inner_d.invoiceRoutingInfo.contains(routingHints)) @@ -268,15 +268,15 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { } test("fail to decrypt when the onion is invalid") { - val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash, hops, None, finalAmount, finalAmount, finalExpiry, paymentSecret, None, Nil, Nil) + val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash, hops, ClearRecipient(e, paymentSecret, None), finalAmount, finalAmount, finalExpiry) val add = UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry, onion.packet.copy(payload = onion.packet.payload.reverse), None) val Left(failure) = decrypt(add, priv_b.privateKey, Features.empty) assert(failure.isInstanceOf[InvalidOnionHmac]) } test("fail to decrypt when the trampoline onion is invalid") { - val Success((amount_ac, expiry_ac, trampolineOnion)) = buildTrampolinePacket(paymentHash, trampolineHops, finalAmount, finalAmount * 2, finalExpiry, paymentSecret, None, Nil, Nil) - val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash, trampolineChannelHops, None, amount_ac, amount_ac, expiry_ac, randomBytes32(), None, Seq(OnionPaymentPayloadTlv.TrampolineOnion(trampolineOnion.packet.copy(payload = trampolineOnion.packet.payload.reverse))), Nil) + val Success((amount_ac, expiry_ac, trampolineOnion)) = buildTrampolinePacket(paymentHash, trampolineHops, ClearRecipient(e, paymentSecret, None), finalAmount, finalAmount * 2, finalExpiry) + val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash, trampolineChannelHops, ClearRecipient(c, randomBytes32(), None, additionalTlvs = Seq(OnionPaymentPayloadTlv.TrampolineOnion(trampolineOnion.packet.copy(payload = trampolineOnion.packet.payload.reverse)))), amount_ac, amount_ac, expiry_ac) val add_b = UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry, onion.packet, None) val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(add_b, priv_b.privateKey, Features.empty) val add_c = UpdateAddHtlc(randomBytes32(), 2, amount_bc, paymentHash, expiry_bc, packet_c, None) @@ -285,59 +285,59 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { } test("fail to decrypt when payment hash doesn't match associated data") { - val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash.reverse, hops, None, finalAmount, finalAmount, finalExpiry, paymentSecret, None, Nil, Nil) + val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash.reverse, hops, ClearRecipient(e, paymentSecret, None), finalAmount, finalAmount, finalExpiry) val add = UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry, onion.packet, None) val Left(failure) = decrypt(add, priv_b.privateKey, Features.empty) assert(failure.isInstanceOf[InvalidOnionHmac]) } test("fail to decrypt at the final node when amount has been modified by next-to-last node") { - val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash, hops.take(1), None, finalAmount, finalAmount, finalExpiry, paymentSecret, None, Nil, Nil) + val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash, hops.take(1), ClearRecipient(b, paymentSecret, None), finalAmount, finalAmount, finalExpiry) val add = UpdateAddHtlc(randomBytes32(), 1, firstAmount - 100.msat, paymentHash, firstExpiry, onion.packet, None) val Left(failure) = decrypt(add, priv_b.privateKey, Features.empty) assert(failure == FinalIncorrectHtlcAmount(firstAmount - 100.msat)) } test("fail to decrypt at the final node when expiry has been modified by next-to-last node") { - val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash, hops.take(1), None, finalAmount, finalAmount, finalExpiry, paymentSecret, None, Nil, Nil) + val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash, hops.take(1), ClearRecipient(b, paymentSecret, None), finalAmount, finalAmount, finalExpiry) val add = UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry - CltvExpiryDelta(12), onion.packet, None) val Left(failure) = decrypt(add, priv_b.privateKey, Features.empty) assert(failure == FinalIncorrectCltvExpiry(firstExpiry - CltvExpiryDelta(12))) } test("fail to decrypt at the final trampoline node when amount has been modified by next-to-last trampoline") { - val Success((amount_ac, expiry_ac, trampolineOnion)) = buildTrampolinePacket(paymentHash, trampolineHops, finalAmount, finalAmount, finalExpiry, paymentSecret, None, Nil, Nil) - val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash, trampolineChannelHops, None, amount_ac, amount_ac, expiry_ac, randomBytes32(), None, Seq(OnionPaymentPayloadTlv.TrampolineOnion(trampolineOnion.packet)), Nil) + val Success((amount_ac, expiry_ac, trampolineOnion)) = buildTrampolinePacket(paymentHash, trampolineHops, ClearRecipient(e, paymentSecret, None), finalAmount, finalAmount, finalExpiry) + val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash, trampolineChannelHops, ClearRecipient(c, randomBytes32(), None, additionalTlvs = Seq(OnionPaymentPayloadTlv.TrampolineOnion(trampolineOnion.packet))), amount_ac, amount_ac, expiry_ac) val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry, onion.packet, None), priv_b.privateKey, Features.empty) val Right(NodeRelayPacket(_, _, _, packet_d)) = decrypt(UpdateAddHtlc(randomBytes32(), 2, amount_bc, paymentHash, expiry_bc, packet_c, None), priv_c.privateKey, Features.empty) // c forwards the trampoline payment to d. - val Success((amount_d, expiry_d, onion_d)) = buildPaymentPacket(paymentHash, channelHopFromUpdate(c, d, channelUpdate_cd) :: Nil, None, amount_cd, amount_cd, expiry_cd, randomBytes32(), None, Seq(OnionPaymentPayloadTlv.TrampolineOnion(packet_d)), Nil) + val Success((amount_d, expiry_d, onion_d)) = buildPaymentPacket(paymentHash, channelHopFromUpdate(c, d, channelUpdate_cd) :: Nil, ClearRecipient(d, randomBytes32(), None, additionalTlvs = Seq(OnionPaymentPayloadTlv.TrampolineOnion(packet_d))), amount_cd, amount_cd, expiry_cd) val Right(NodeRelayPacket(_, _, _, packet_e)) = decrypt(UpdateAddHtlc(randomBytes32(), 3, amount_d, paymentHash, expiry_d, onion_d.packet, None), priv_d.privateKey, Features.empty) // d forwards an invalid amount to e (the outer total amount doesn't match the inner amount). val invalidTotalAmount = amount_de + 100.msat - val Success((amount_e, expiry_e, onion_e)) = buildPaymentPacket(paymentHash, channelHopFromUpdate(d, e, channelUpdate_de) :: Nil, None, amount_de, invalidTotalAmount, expiry_de, randomBytes32(), None, Seq(OnionPaymentPayloadTlv.TrampolineOnion(packet_e)), Nil) + val Success((amount_e, expiry_e, onion_e)) = buildPaymentPacket(paymentHash, channelHopFromUpdate(d, e, channelUpdate_de) :: Nil, ClearRecipient(e, randomBytes32(), None, additionalTlvs = Seq(OnionPaymentPayloadTlv.TrampolineOnion(packet_e))), amount_de, invalidTotalAmount, expiry_de) val Left(failure) = decrypt(UpdateAddHtlc(randomBytes32(), 4, amount_e, paymentHash, expiry_e, onion_e.packet, None), priv_e.privateKey, Features.empty) assert(failure == FinalIncorrectHtlcAmount(invalidTotalAmount)) } test("fail to decrypt at the final trampoline node when expiry has been modified by next-to-last trampoline") { - val Success((amount_ac, expiry_ac, trampolineOnion)) = buildTrampolinePacket(paymentHash, trampolineHops, finalAmount, finalAmount, finalExpiry, paymentSecret, None, Nil, Nil) - val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash, trampolineChannelHops, None, amount_ac, amount_ac, expiry_ac, randomBytes32(), None, Seq(OnionPaymentPayloadTlv.TrampolineOnion(trampolineOnion.packet)), Nil) + val Success((amount_ac, expiry_ac, trampolineOnion)) = buildTrampolinePacket(paymentHash, trampolineHops, ClearRecipient(e, paymentSecret, None), finalAmount, finalAmount, finalExpiry) + val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash, trampolineChannelHops, ClearRecipient(c, randomBytes32(), None, additionalTlvs = Seq(OnionPaymentPayloadTlv.TrampolineOnion(trampolineOnion.packet))), amount_ac, amount_ac, expiry_ac) val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry, onion.packet, None), priv_b.privateKey, Features.empty) val Right(NodeRelayPacket(_, _, _, packet_d)) = decrypt(UpdateAddHtlc(randomBytes32(), 2, amount_bc, paymentHash, expiry_bc, packet_c, None), priv_c.privateKey, Features.empty) // c forwards the trampoline payment to d. - val Success((amount_d, expiry_d, onion_d)) = buildPaymentPacket(paymentHash, channelHopFromUpdate(c, d, channelUpdate_cd) :: Nil, None, amount_cd, amount_cd, expiry_cd, randomBytes32(), None, Seq(OnionPaymentPayloadTlv.TrampolineOnion(packet_d)), Nil) + val Success((amount_d, expiry_d, onion_d)) = buildPaymentPacket(paymentHash, channelHopFromUpdate(c, d, channelUpdate_cd) :: Nil, ClearRecipient(d, randomBytes32(), None, additionalTlvs = Seq(OnionPaymentPayloadTlv.TrampolineOnion(packet_d))), amount_cd, amount_cd, expiry_cd) val Right(NodeRelayPacket(_, _, _, packet_e)) = decrypt(UpdateAddHtlc(randomBytes32(), 3, amount_d, paymentHash, expiry_d, onion_d.packet, None), priv_d.privateKey, Features.empty) // d forwards an invalid expiry to e (the outer expiry doesn't match the inner expiry). val invalidExpiry = expiry_de - CltvExpiryDelta(12) - val Success((amount_e, expiry_e, onion_e)) = buildPaymentPacket(paymentHash, channelHopFromUpdate(d, e, channelUpdate_de) :: Nil, None, amount_de, amount_de, invalidExpiry, randomBytes32(), None, Seq(OnionPaymentPayloadTlv.TrampolineOnion(packet_e)), Nil) + val Success((amount_e, expiry_e, onion_e)) = buildPaymentPacket(paymentHash, channelHopFromUpdate(d, e, channelUpdate_de) :: Nil, ClearRecipient(e, randomBytes32(), None, additionalTlvs = Seq(OnionPaymentPayloadTlv.TrampolineOnion(packet_e))), amount_de, amount_de, invalidExpiry) val Left(failure) = decrypt(UpdateAddHtlc(randomBytes32(), 4, amount_e, paymentHash, expiry_e, onion_e.packet, None), priv_e.privateKey, Features.empty) assert(failure == FinalIncorrectCltvExpiry(invalidExpiry)) } test("fail to decrypt at intermediate trampoline node when amount is invalid") { - val Success((amount_ac, expiry_ac, trampolineOnion)) = buildTrampolinePacket(paymentHash, trampolineHops, finalAmount, finalAmount, finalExpiry, paymentSecret, None, Nil, Nil) - val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash, trampolineChannelHops, None, amount_ac, amount_ac, expiry_ac, randomBytes32(), None, Seq(OnionPaymentPayloadTlv.TrampolineOnion(trampolineOnion.packet)), Nil) + val Success((amount_ac, expiry_ac, trampolineOnion)) = buildTrampolinePacket(paymentHash, trampolineHops, ClearRecipient(e, paymentSecret, None), finalAmount, finalAmount, finalExpiry) + val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash, trampolineChannelHops, ClearRecipient(c, randomBytes32(), None, additionalTlvs = Seq(OnionPaymentPayloadTlv.TrampolineOnion(trampolineOnion.packet))), amount_ac, amount_ac, expiry_ac) val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry, onion.packet, None), priv_b.privateKey, Features.empty) // A trampoline relay is very similar to a final node: it can validate that the HTLC amount matches the onion outer amount. val Left(failure) = decrypt(UpdateAddHtlc(randomBytes32(), 2, amount_bc - 100.msat, paymentHash, expiry_bc, packet_c, None), priv_c.privateKey, Features.empty) @@ -345,8 +345,8 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { } test("fail to decrypt at intermediate trampoline node when expiry is invalid") { - val Success((amount_ac, expiry_ac, trampolineOnion)) = buildTrampolinePacket(paymentHash, trampolineHops, finalAmount, finalAmount, finalExpiry, paymentSecret, None, Nil, Nil) - val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash, trampolineChannelHops, None, amount_ac, amount_ac, expiry_ac, randomBytes32(), None, Seq(OnionPaymentPayloadTlv.TrampolineOnion(trampolineOnion.packet)), Nil) + val Success((amount_ac, expiry_ac, trampolineOnion)) = buildTrampolinePacket(paymentHash, trampolineHops, ClearRecipient(e, paymentSecret, None), finalAmount, finalAmount, finalExpiry) + val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash, trampolineChannelHops, ClearRecipient(c, randomBytes32(), None, additionalTlvs = Seq(OnionPaymentPayloadTlv.TrampolineOnion(trampolineOnion.packet))), amount_ac, amount_ac, expiry_ac) val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry, onion.packet, None), priv_b.privateKey, Features.empty) // A trampoline relay is very similar to a final node: it can validate that the HTLC expiry matches the onion outer expiry. val Left(failure) = decrypt(UpdateAddHtlc(randomBytes32(), 2, amount_bc, paymentHash, expiry_bc - CltvExpiryDelta(12), packet_c, None), priv_c.privateKey, Features.empty) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PostRestartHtlcCleanerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PostRestartHtlcCleanerSpec.scala index 99ab55727d..4d6ec887c0 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PostRestartHtlcCleanerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PostRestartHtlcCleanerSpec.scala @@ -31,6 +31,7 @@ import fr.acinq.eclair.payment.OutgoingPaymentPacket.{Upstream, buildCommand} import fr.acinq.eclair.payment.PaymentPacketSpec._ import fr.acinq.eclair.payment.relay.{PostRestartHtlcCleaner, Relayer} import fr.acinq.eclair.router.BaseRouterSpec.channelHopFromUpdate +import fr.acinq.eclair.router.Router.Route import fr.acinq.eclair.transactions.Transactions.{ClaimRemoteDelayedOutputTx, InputInfo} import fr.acinq.eclair.transactions.{DirectedHtlc, IncomingHtlc, OutgoingHtlc} import fr.acinq.eclair.wire.internal.channel.ChannelCodecsSpec @@ -720,7 +721,7 @@ object PostRestartHtlcCleanerSpec { val (paymentHash1, paymentHash2, paymentHash3) = (Crypto.sha256(preimage1), Crypto.sha256(preimage2), Crypto.sha256(preimage3)) def buildHtlc(htlcId: Long, channelId: ByteVector32, paymentHash: ByteVector32): UpdateAddHtlc = { - val Success((cmd, _, _)) = buildCommand(randomKey(), ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, hops, None, finalAmount, finalAmount, finalExpiry, randomBytes32(), None, Nil, Nil) + val Success((cmd, _, _)) = buildCommand(randomKey(), ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, Route(finalAmount, hops, ClearRecipient(e, randomBytes32(), None)), finalAmount, finalAmount, finalExpiry) UpdateAddHtlc(channelId, htlcId, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion, None) } @@ -729,7 +730,7 @@ object PostRestartHtlcCleanerSpec { def buildHtlcOut(htlcId: Long, channelId: ByteVector32, paymentHash: ByteVector32): DirectedHtlc = OutgoingHtlc(buildHtlc(htlcId, channelId, paymentHash)) def buildFinalHtlc(htlcId: Long, channelId: ByteVector32, paymentHash: ByteVector32): DirectedHtlc = { - val Success((cmd, _, _)) = buildCommand(randomKey(), ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, channelHopFromUpdate(a, TestConstants.Bob.nodeParams.nodeId, channelUpdate_ab) :: Nil, None, finalAmount, finalAmount, finalExpiry, randomBytes32(), None, Nil, Nil) + val Success((cmd, _, _)) = buildCommand(randomKey(), ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, Route(finalAmount, channelHopFromUpdate(a, TestConstants.Bob.nodeParams.nodeId, channelUpdate_ab) :: Nil, ClearRecipient(b, randomBytes32(), None)), finalAmount, finalAmount, finalExpiry) IncomingHtlc(UpdateAddHtlc(channelId, htlcId, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion, None)) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/NodeRelayerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/NodeRelayerSpec.scala index 69ddab3233..5350b421fc 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/NodeRelayerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/NodeRelayerSpec.scala @@ -726,12 +726,12 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl val outgoingCfg = mockPayFSM.expectMessageType[SendPaymentConfig] validateOutgoingCfg(outgoingCfg, Upstream.Trampoline(incomingMultiPart.map(_.add))) val outgoingPayment = mockPayFSM.expectMessageType[SendMultiPartPayment] - assert(outgoingPayment.paymentSecret == invoice.paymentSecret.get) // we should use the provided secret - assert(outgoingPayment.paymentMetadata == invoice.paymentMetadata) // we should use the provided metadata + assert(outgoingPayment.recipients.head.asInstanceOf[ClearRecipient].paymentSecret == invoice.paymentSecret) // we should use the provided secret + assert(outgoingPayment.recipients.head.asInstanceOf[ClearRecipient].paymentMetadata_opt == invoice.paymentMetadata) // we should use the provided metadata assert(outgoingPayment.totalAmount == outgoingAmount) assert(outgoingPayment.targetExpiry == outgoingExpiry) - assert(outgoingPayment.targetNodeId == outgoingNodeId) - assert(outgoingPayment.additionalTlvs == Nil) + assert(outgoingPayment.recipients.head.nodeId == outgoingNodeId) + assert(outgoingPayment.recipients.head.additionalTlvs == Nil) assert(outgoingPayment.extraEdges.head.asInstanceOf[BasicEdge].shortChannelId == ShortChannelId(42)) assert(outgoingPayment.extraEdges.head.asInstanceOf[BasicEdge].sourceNodeId == hints.head.nodeId) assert(outgoingPayment.extraEdges.head.asInstanceOf[BasicEdge].targetNodeId == outgoingNodeId) @@ -775,8 +775,8 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl val outgoingPayment = mockPayFSM.expectMessageType[SendPaymentToNode] assert(outgoingPayment.amount == outgoingAmount) assert(outgoingPayment.targetExpiry == outgoingExpiry) - assert(outgoingPayment.paymentMetadata == invoice.paymentMetadata) // we should use the provided metadata - assert(outgoingPayment.targetNodeId == outgoingNodeId) + assert(outgoingPayment.targetRecipients.head.asInstanceOf[ClearRecipient].paymentMetadata_opt == invoice.paymentMetadata) // we should use the provided metadata + assert(outgoingPayment.targetRecipients.head.nodeId == outgoingNodeId) assert(outgoingPayment.extraEdges.head.asInstanceOf[BasicEdge].shortChannelId == ShortChannelId(42)) assert(outgoingPayment.extraEdges.head.asInstanceOf[BasicEdge].sourceNodeId == hints.head.nodeId) assert(outgoingPayment.extraEdges.head.asInstanceOf[BasicEdge].targetNodeId == outgoingNodeId) @@ -834,11 +834,11 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl } def validateOutgoingPayment(outgoingPayment: SendMultiPartPayment): Unit = { - assert(outgoingPayment.paymentSecret !== incomingSecret) // we should generate a new outgoing secret + assert(outgoingPayment.recipients.head.asInstanceOf[ClearRecipient].paymentSecret !== incomingSecret) // we should generate a new outgoing secret assert(outgoingPayment.totalAmount == outgoingAmount) assert(outgoingPayment.targetExpiry == outgoingExpiry) - assert(outgoingPayment.targetNodeId == outgoingNodeId) - assert(outgoingPayment.additionalTlvs == Seq(OnionPaymentPayloadTlv.TrampolineOnion(nextTrampolinePacket))) + assert(outgoingPayment.recipients.head.nodeId == outgoingNodeId) + assert(outgoingPayment.recipients.head.additionalTlvs == Seq(OnionPaymentPayloadTlv.TrampolineOnion(nextTrampolinePacket))) assert(outgoingPayment.extraEdges == Nil) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/RelayerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/RelayerSpec.scala index b6274e1bca..9a7fed615a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/RelayerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/RelayerSpec.scala @@ -29,9 +29,9 @@ import fr.acinq.eclair.payment.IncomingPaymentPacket.FinalPacket import fr.acinq.eclair.payment.OutgoingPaymentPacket.{Upstream, buildCommand} import fr.acinq.eclair.payment.PaymentPacketSpec._ import fr.acinq.eclair.payment.relay.Relayer._ -import fr.acinq.eclair.payment.{OutgoingPaymentPacket, PaymentPacketSpec} +import fr.acinq.eclair.payment.{ClearRecipient, OutgoingPaymentPacket, PaymentPacketSpec} import fr.acinq.eclair.router.BaseRouterSpec.channelHopFromUpdate -import fr.acinq.eclair.router.Router.NodeHop +import fr.acinq.eclair.router.Router.{NodeHop, Route} import fr.acinq.eclair.wire.protocol.PaymentOnion.FinalPayload import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.{NodeParams, TestConstants, randomBytes32, _} @@ -88,7 +88,7 @@ class RelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat } // we use this to build a valid onion - val Success((cmd, _, _)) = buildCommand(randomKey(), ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, hops, None, finalAmount, finalAmount, finalExpiry, paymentSecret, None, Nil, Nil) + val Success((cmd, _, _)) = buildCommand(randomKey(), ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, Route(finalAmount, hops, ClearRecipient(e, paymentSecret, None)), finalAmount, finalAmount, finalExpiry) // and then manually build an htlc val add_ab = UpdateAddHtlc(channelId = randomBytes32(), id = 123456, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion, None) relayer ! RelayForward(add_ab) @@ -98,7 +98,7 @@ class RelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat test("relay an htlc-add at the final node to the payment handler") { f => import f._ - val Success((cmd, _, _)) = buildCommand(randomKey(), ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, hops.take(1), None, finalAmount, finalAmount, finalExpiry, paymentSecret, None, Nil, Nil) + val Success((cmd, _, _)) = buildCommand(randomKey(), ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, Route(finalAmount, hops.take(1), ClearRecipient(b, paymentSecret, None)), finalAmount, finalAmount, finalExpiry) val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion, None) relayer ! RelayForward(add_ab) @@ -118,10 +118,10 @@ class RelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat // We simulate a payment split between multiple trampoline routes. val totalAmount = finalAmount * 3 val trampolineHops = NodeHop(a, b, channelUpdate_ab.cltvExpiryDelta, 0 msat) :: Nil - val Success((trampolineAmount, trampolineExpiry, trampolineOnion)) = OutgoingPaymentPacket.buildTrampolinePacket(paymentHash, trampolineHops, finalAmount, totalAmount, finalExpiry, paymentSecret, None, Nil, Nil) + val Success((trampolineAmount, trampolineExpiry, trampolineOnion)) = OutgoingPaymentPacket.buildTrampolinePacket(paymentHash, trampolineHops, ClearRecipient(b, paymentSecret, None), finalAmount, totalAmount, finalExpiry) assert(trampolineAmount == finalAmount) assert(trampolineExpiry == finalExpiry) - val Success((cmd, _, _)) = buildCommand(randomKey(), ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, channelHopFromUpdate(a, b, channelUpdate_ab) :: Nil, None, trampolineAmount, trampolineAmount, trampolineExpiry, randomBytes32(), None, Seq(OnionPaymentPayloadTlv.TrampolineOnion(trampolineOnion.packet)), Nil) + val Success((cmd, _, _)) = buildCommand(randomKey(), ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, Route(trampolineAmount, channelHopFromUpdate(a, b, channelUpdate_ab) :: Nil, ClearRecipient(b, randomBytes32(), None, additionalTlvs = Seq(OnionPaymentPayloadTlv.TrampolineOnion(trampolineOnion.packet)))), trampolineAmount, trampolineAmount, trampolineExpiry) assert(cmd.amount == finalAmount) assert(cmd.cltvExpiry == finalExpiry) val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion, None) @@ -143,7 +143,7 @@ class RelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat import f._ // we use this to build a valid onion - val Success((cmd, _, _)) = buildCommand(randomKey(), ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, hops, None, finalAmount, finalAmount, finalExpiry, paymentSecret, None, Nil, Nil) + val Success((cmd, _, _)) = buildCommand(randomKey(), ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, Route(finalAmount, hops, ClearRecipient(e, paymentSecret, None)), finalAmount, finalAmount, finalExpiry) // and then manually build an htlc with an invalid onion (hmac) val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion.copy(hmac = cmd.onion.hmac.reverse), None) @@ -164,8 +164,8 @@ class RelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat // we use this to build a valid trampoline onion inside a normal onion val trampolineHops = NodeHop(a, b, channelUpdate_ab.cltvExpiryDelta, 0 msat) :: NodeHop(b, c, channelUpdate_bc.cltvExpiryDelta, fee_b) :: Nil - val Success((trampolineAmount, trampolineExpiry, trampolineOnion)) = OutgoingPaymentPacket.buildTrampolinePacket(paymentHash, trampolineHops, finalAmount, finalAmount, finalExpiry, paymentSecret, None, Nil, Nil) - val Success((cmd, _, _)) = buildCommand(randomKey(), ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, channelHopFromUpdate(a, b, channelUpdate_ab) :: Nil, None, trampolineAmount, trampolineAmount, trampolineExpiry, randomBytes32(), None, Seq(OnionPaymentPayloadTlv.TrampolineOnion(trampolineOnion.packet)), Nil) + val Success((trampolineAmount, trampolineExpiry, trampolineOnion)) = OutgoingPaymentPacket.buildTrampolinePacket(paymentHash, trampolineHops, ClearRecipient(c, paymentSecret, None), finalAmount, finalAmount, finalExpiry) + val Success((cmd, _, _)) = buildCommand(randomKey(), ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, Route(trampolineAmount, channelHopFromUpdate(a, b, channelUpdate_ab) :: Nil, ClearRecipient(b, randomBytes32(), None, additionalTlvs = Seq(OnionPaymentPayloadTlv.TrampolineOnion(trampolineOnion.packet)))), trampolineAmount, trampolineAmount, trampolineExpiry) // and then manually build an htlc val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion, None) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/GraphSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/GraphSpec.scala index a62561bb47..b51958f0e5 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/GraphSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/GraphSpec.scala @@ -257,7 +257,7 @@ class GraphSpec extends AnyFunSuite { val edgeDE = makeEdge(6L, d, e, 9 msat, 0, capacity = 200000 sat) val graph = DirectedGraph(Seq(edgeAB, edgeBC, edgeCD, edgeDC, edgeCE, edgeDE)) - val path :: Nil = yenKshortestPaths(graph, a, e, 100000000 msat, + val path :: Nil = yenKshortestPaths(graph, a, Seq(e), 100000000 msat, Set.empty, Set.empty, Set.empty, 1, Right(HeuristicsConstants(1.0E-8, RelayFees(2000 msat, 500), RelayFees(50 msat, 20), useLogProbability = true)), BlockHeight(714930), _ => true, includeLocalChannelCost = true) @@ -281,7 +281,7 @@ class GraphSpec extends AnyFunSuite { val edgeDE = makeEdge(6L, d, e, 1 msat, 0, capacity = 200000 sat) val graph = DirectedGraph(Seq(edgeAB, edgeBC, edgeCD, edgeDC, edgeCE, edgeDE)) - val paths = yenKshortestPaths(graph, a, e, 90000000 msat, + val paths = yenKshortestPaths(graph, a, Seq(e), 90000000 msat, Set.empty, Set.empty, Set.empty, 2, Left(WeightRatios(1, 0, 0, 0, RelayFees(0 msat, 0))), BlockHeight(714930), _ => true, includeLocalChannelCost = true) @@ -307,7 +307,7 @@ class GraphSpec extends AnyFunSuite { val edgeDE = makeEdge(6L, d, e, 1 msat, 0, capacity = 200000 sat) val graph = DirectedGraph(Seq(edgeAB, edgeBC, edgeCD, edgeDC, edgeCE, edgeDE)) - val paths = yenKshortestPaths(graph, a, e, 90000000 msat, + val paths = yenKshortestPaths(graph, a, Seq(e), 90000000 msat, Set.empty, Set.empty, Set.empty, 2, Left(WeightRatios(1, 0, 0, 0, RelayFees(0 msat, 0))), BlockHeight(714930), _ => true, includeLocalChannelCost = true) @@ -340,7 +340,7 @@ class GraphSpec extends AnyFunSuite { val edgeGH = makeEdge(9L, g, h, 2 msat, 0, capacity = 100000 sat, minHtlc = 1000 msat) val graph = DirectedGraph(Seq(edgeCD, edgeDF, edgeCE, edgeED, edgeEF, edgeFG, edgeFH, edgeEG, edgeGH)) - val paths = yenKshortestPaths(graph, c, h, 10000000 msat, + val paths = yenKshortestPaths(graph, c, Seq(h), 10000000 msat, Set.empty, Set.empty, Set.empty, 3, Left(WeightRatios(1, 0, 0, 0, RelayFees(0 msat, 0))), BlockHeight(714930), _ => true, includeLocalChannelCost = true) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala index 23f89df0af..54294f4124 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala @@ -19,6 +19,7 @@ package fr.acinq.eclair.router import com.softwaremill.quicklens.ModifyPimp import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, ByteVector64, Satoshi, SatoshiLong} +import fr.acinq.eclair.payment.ClearRecipient import fr.acinq.eclair.payment.relay.Relayer.RelayFees import fr.acinq.eclair.router.BaseRouterSpec.channelHopFromUpdate import fr.acinq.eclair.router.Graph.GraphStructure.DirectedGraph.graphEdgeToHop @@ -28,7 +29,7 @@ import fr.acinq.eclair.router.RouteCalculation._ import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.transactions.Transactions import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{BlockHeight, CltvExpiryDelta, Features, MilliSatoshi, MilliSatoshiLong, RealShortChannelId, ShortChannelId, TimestampSecond, TimestampSecondLong, ToMilliSatoshiConversion, randomKey} +import fr.acinq.eclair.{BlockHeight, CltvExpiryDelta, Features, MilliSatoshi, MilliSatoshiLong, RealShortChannelId, ShortChannelId, TimestampSecond, TimestampSecondLong, ToMilliSatoshiConversion, randomBytes32, randomKey} import org.scalatest.TryValues.convertTryToSuccessOrFailure import org.scalatest.funsuite.AnyFunSuite import org.scalatest.{ParallelTestExecution, Tag} @@ -56,7 +57,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(4L, d, e, 1 msat, 10, cltvDelta = CltvExpiryDelta(1)) )) - val Success(route :: Nil) = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) + val Success(route :: Nil) = findRoute(g, a, makeRecipients(e), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route2Ids(route) == 1 :: 2 :: 3 :: 4 :: Nil) } @@ -77,7 +78,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(4L, d, e, 10 msat, 10, cltvDelta = CltvExpiryDelta(1)) )) - val Success(route :: Nil) = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, maxFee, numRoutes = 1, routeParams = routeParams, currentBlockHeight = BlockHeight(400000)) + val Success(route :: Nil) = findRoute(g, a, makeRecipients(e), DEFAULT_AMOUNT_MSAT, maxFee, numRoutes = 1, routeParams = routeParams, currentBlockHeight = BlockHeight(400000)) assert(route2Ids(route) == 1 :: 2 :: 3 :: 4 :: Nil) } @@ -120,7 +121,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(6L, f, d, feeBase = 1 msat, feeProportionalMillionth = 100, minHtlc = 0 msat) )) - val Success(route :: Nil) = findRoute(graph, a, d, amount, maxFee = 7 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) + val Success(route :: Nil) = findRoute(graph, a, makeRecipients(d), amount, maxFee = 7 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) val weightedPath = Graph.pathWeight(a, route2Edges(route), amount, BlockHeight(0), Left(NO_WEIGHT_RATIOS), includeLocalChannelCost = false) assert(route2Ids(route) == 4 :: 5 :: 6 :: Nil) assert(weightedPath.length == 3) @@ -131,7 +132,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val graph2 = graph.addEdge(makeEdge(5L, e, f, feeBase = 1 msat, feeProportionalMillionth = 400, minHtlc = 0 msat, capacity = 10 sat)) val graph3 = graph.addEdge(makeEdge(5L, e, f, feeBase = 1 msat, feeProportionalMillionth = 400, minHtlc = 0 msat, balance_opt = Some(10001 msat))) for (g <- Seq(graph1, graph2, graph3)) { - val Success(route1 :: Nil) = findRoute(g, a, d, amount, maxFee = 10 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) + val Success(route1 :: Nil) = findRoute(g, a, makeRecipients(d), amount, maxFee = 10 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route2Ids(route1) == 1 :: 2 :: 3 :: Nil) } } @@ -145,7 +146,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(5L, d, e, 5 msat, 0) // d -> e )) - val Success(route :: Nil) = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) + val Success(route :: Nil) = findRoute(g, a, makeRecipients(e), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route2Ids(route) == 2 :: 5 :: Nil) } @@ -157,11 +158,11 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(4L, d, e, 0 msat, 0) )) - val Success(route1 :: Nil) = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) + val Success(route1 :: Nil) = findRoute(g, a, makeRecipients(e), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route2Ids(route1) == 1 :: 2 :: 3 :: 4 :: Nil) val graphWithRemovedEdge = g.removeEdge(ChannelDesc(ShortChannelId(3L), c, d)) - val route2 = findRoute(graphWithRemovedEdge, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) + val route2 = findRoute(graphWithRemovedEdge, a, makeRecipients(e), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route2 == Failure(RouteNotFound)) } @@ -180,7 +181,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(4L, f, h, 50 msat, 0) // more expensive but fee will be ignored since f is the payer )) - val Success(route :: Nil) = findRoute(graph, f, i, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) + val Success(route :: Nil) = findRoute(graph, f, makeRecipients(i), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route2Ids(route) == 4 :: 3 :: Nil) } @@ -199,7 +200,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(3L, h, i, 0 msat, 0) )) - val Success(route1 :: route2 :: Nil) = findRoute(graph, f, i, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 2, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) + val Success(route1 :: route2 :: Nil) = findRoute(graph, f, makeRecipients(i), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 2, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route2Ids(route1) == 4 :: Nil) assert(route2Ids(route2) == 1 :: 2 :: 3 :: Nil) } @@ -219,7 +220,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(3L, h, i, 1 msat, 0) )) - val Success(route :: Nil) = findRoute(graph, f, i, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) + val Success(route :: Nil) = findRoute(graph, f, makeRecipients(i), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route2Ids(route) == 1 :: 2 :: 3 :: Nil) } @@ -238,7 +239,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(3L, h, i, 1 msat, 0) )) - val route = findRoute(graph, f, i, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) + val route = findRoute(graph, f, makeRecipients(i), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route == Failure(RouteNotFound)) } @@ -257,7 +258,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(3L, h, i, 0 msat, 0) )) - val Success(route :: Nil) = findRoute(graph, f, i, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) + val Success(route :: Nil) = findRoute(graph, f, makeRecipients(i), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route2Ids(route) == 1 :: 6 :: 3 :: Nil) } @@ -276,7 +277,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(3L, h, i, 0 msat, 0) )) - val Success(route :: Nil) = findRoute(graph, f, i, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) + val Success(route :: Nil) = findRoute(graph, f, makeRecipients(i), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route2Ids(route) == 1 :: 2 :: 3 :: Nil) } @@ -289,7 +290,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(5L, b, e, 10 msat, 10) )) - val Success(route :: Nil) = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) + val Success(route :: Nil) = findRoute(g, a, makeRecipients(e), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route2Ids(route) == 1 :: 2 :: 3 :: 4 :: Nil) } @@ -299,7 +300,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(4L, d, e, 0 msat, 0) )) - val route = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) + val route = findRoute(g, a, makeRecipients(e), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route == Failure(RouteNotFound)) } @@ -310,7 +311,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(4L, d, e, 0 msat, 0) )) - val route = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) + val route = findRoute(g, a, makeRecipients(e), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route == Failure(RouteNotFound)) } @@ -320,8 +321,8 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(4L, c, d, 0 msat, 0) )).addVertex(a).addVertex(e) - assert(findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) == Failure(RouteNotFound)) - assert(findRoute(g, b, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) == Failure(RouteNotFound)) + assert(findRoute(g, a, makeRecipients(d), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) == Failure(RouteNotFound)) + assert(findRoute(g, b, makeRecipients(e), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) == Failure(RouteNotFound)) } test("route not found (amount too high OR too low)") { @@ -343,8 +344,8 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val g = DirectedGraph(edgesHi) val g1 = DirectedGraph(edgesLo) - assert(findRoute(g, a, d, highAmount, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) == Failure(RouteNotFound)) - assert(findRoute(g1, a, d, lowAmount, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) == Failure(RouteNotFound)) + assert(findRoute(g, a, makeRecipients(d), highAmount, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) == Failure(RouteNotFound)) + assert(findRoute(g1, a, makeRecipients(d), lowAmount, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) == Failure(RouteNotFound)) } test("route not found (balance too low)") { @@ -353,7 +354,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(2L, b, c, 1 msat, 2, minHtlc = 10000 msat), makeEdge(3L, c, d, 1 msat, 2, minHtlc = 10000 msat) )) - assert(findRoute(g, a, d, 15000 msat, 100 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)).isSuccess) + assert(findRoute(g, a, makeRecipients(d), 15000 msat, 100 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)).isSuccess) // not enough balance on the last edge val g1 = DirectedGraph(List( @@ -373,7 +374,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(2L, b, c, 1 msat, 2, minHtlc = 10000 msat), makeEdge(3L, c, d, 1 msat, 2, minHtlc = 10000 msat) )) - Seq(g1, g2, g3).foreach(g => assert(findRoute(g, a, d, 15000 msat, 100 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) == Failure(RouteNotFound))) + Seq(g1, g2, g3).foreach(g => assert(findRoute(g, a, makeRecipients(d), 15000 msat, 100 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) == Failure(RouteNotFound))) } test("route to self") { @@ -383,7 +384,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(3L, c, d, 0 msat, 0) )) - val route = findRoute(g, a, a, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) + val route = findRoute(g, a, makeRecipients(a), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route == Failure(CannotRouteToSelf)) } @@ -395,7 +396,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(4L, d, e, 0 msat, 0) )) - val Success(route :: Nil) = findRoute(g, a, b, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) + val Success(route :: Nil) = findRoute(g, a, makeRecipients(b), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route2Ids(route) == 1 :: Nil) } @@ -408,10 +409,10 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(4L, d, e, 0 msat, 0) )) - val Success(route1 :: Nil) = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) + val Success(route1 :: Nil) = findRoute(g, a, makeRecipients(e), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route2Ids(route1) == 1 :: 2 :: 3 :: 4 :: Nil) - val route2 = findRoute(g, e, a, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) + val route2 = findRoute(g, e, makeRecipients(a), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route2 == Failure(RouteNotFound)) } @@ -439,7 +440,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { ) val g = DirectedGraph(edges) - val Success(route :: Nil) = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) + val Success(route :: Nil) = findRoute(g, a, makeRecipients(e), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route.clearHops == channelHopFromUpdate(a, b, uab) :: channelHopFromUpdate(b, c, ubc) :: channelHopFromUpdate(c, d, ucd) :: channelHopFromUpdate(d, e, ude) :: Nil) } @@ -451,7 +452,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(4L, d, e, 0 msat, 0) )) - val route1 = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, ignoredEdges = Set(ChannelDesc(ShortChannelId(3L), c, d)), routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) + val route1 = findRoute(g, a, makeRecipients(e), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, ignoredEdges = Set(ChannelDesc(ShortChannelId(3L), c, d)), routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route1 == Failure(RouteNotFound)) // verify that we left the graph untouched @@ -460,7 +461,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { assert(g.containsVertex(d)) // make sure we can find a route if without the blacklist - val Success(route2 :: Nil) = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) + val Success(route2 :: Nil) = findRoute(g, a, makeRecipients(e), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route2Ids(route2) == 1 :: 2 :: 3 :: 4 :: Nil) } @@ -471,12 +472,12 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(3L, c, d, 10 msat, 10) )) - val route = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) + val route = findRoute(g, a, makeRecipients(e), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route == Failure(RouteNotFound)) // now we add the missing edge to reach the destination val extraGraphEdges = Set(makeEdge(4L, d, e, 5 msat, 5)) - val Success(route1 :: Nil) = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, extraEdges = extraGraphEdges, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) + val Success(route1 :: Nil) = findRoute(g, a, makeRecipients(e), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, extraEdges = extraGraphEdges, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route2Ids(route1) == 1 :: 2 :: 3 :: 4 :: Nil) } @@ -486,12 +487,12 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(3L, c, d, 10 msat, 10) )) - val route = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) + val route = findRoute(g, a, makeRecipients(d), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route == Failure(RouteNotFound)) // now we add the missing starting edge val extraGraphEdges = Set(makeEdge(1L, a, b, 5 msat, 5)) - val Success(route1 :: Nil) = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, extraEdges = extraGraphEdges, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) + val Success(route1 :: Nil) = findRoute(g, a, makeRecipients(d), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, extraEdges = extraGraphEdges, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route2Ids(route1) == 1 :: 2 :: 3 :: Nil) } @@ -503,12 +504,12 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(4L, d, e, 10 msat, 10) )) - val Success(route1 :: Nil) = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) + val Success(route1 :: Nil) = findRoute(g, a, makeRecipients(e), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route2Ids(route1) == 1 :: 2 :: 3 :: 4 :: Nil) assert(route1.clearHops(1).params.relayFees.feeBase == 10.msat) val extraGraphEdges = Set(makeEdge(2L, b, c, 5 msat, 5)) - val Success(route2 :: Nil) = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, extraEdges = extraGraphEdges, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) + val Success(route2 :: Nil) = findRoute(g, a, makeRecipients(e), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, extraEdges = extraGraphEdges, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route2Ids(route2) == 1 :: 2 :: 3 :: 4 :: Nil) assert(route2.clearHops(1).params.relayFees.feeBase == 5.msat) } @@ -566,10 +567,10 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val g = DirectedGraph(edges) - assert(findRoute(g, nodes(0), nodes(18), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)).map(r => route2Ids(r.head)) == Success(0 until 18)) - assert(findRoute(g, nodes(0), nodes(19), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)).map(r => route2Ids(r.head)) == Success(0 until 19)) - assert(findRoute(g, nodes(0), nodes(20), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)).map(r => route2Ids(r.head)) == Success(0 until 20)) - assert(findRoute(g, nodes(0), nodes(21), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) == Failure(RouteNotFound)) + assert(findRoute(g, nodes(0), makeRecipients(nodes(18)), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)).map(r => route2Ids(r.head)) == Success(0 until 18)) + assert(findRoute(g, nodes(0), makeRecipients(nodes(19)), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)).map(r => route2Ids(r.head)) == Success(0 until 19)) + assert(findRoute(g, nodes(0), makeRecipients(nodes(20)), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)).map(r => route2Ids(r.head)) == Success(0 until 20)) + assert(findRoute(g, nodes(0), makeRecipients(nodes(21)), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) == Failure(RouteNotFound)) } test("ignore cheaper route when it has more than 20 hops") { @@ -584,7 +585,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val g = DirectedGraph(expensiveShortEdge :: edges) - val Success(route :: Nil) = findRoute(g, nodes(0), nodes(49), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) + val Success(route :: Nil) = findRoute(g, nodes(0), makeRecipients(nodes(49)), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route2Ids(route) == 0 :: 1 :: 99 :: 48 :: Nil) } @@ -599,7 +600,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(6, f, d, feeBase = 5 msat, 0, minHtlc = 0 msat, maxHtlc = None, CltvExpiryDelta(9)) )) - val Success(route :: Nil) = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.modify(_.boundaries.maxCltv).setTo(CltvExpiryDelta(28)), currentBlockHeight = BlockHeight(400000)) + val Success(route :: Nil) = findRoute(g, a, makeRecipients(d), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.modify(_.boundaries.maxCltv).setTo(CltvExpiryDelta(28)), currentBlockHeight = BlockHeight(400000)) assert(route2Ids(route) == 4 :: 5 :: 6 :: Nil) } @@ -614,7 +615,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(6, b, f, feeBase = 5 msat, 0, minHtlc = 0 msat, maxHtlc = None, CltvExpiryDelta(9)) )) - val Success(route :: Nil) = findRoute(g, a, f, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.modify(_.boundaries.maxRouteLength).setTo(3), currentBlockHeight = BlockHeight(400000)) + val Success(route :: Nil) = findRoute(g, a, makeRecipients(f), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.modify(_.boundaries.maxRouteLength).setTo(3), currentBlockHeight = BlockHeight(400000)) assert(route2Ids(route) == 1 :: 6 :: Nil) } @@ -627,7 +628,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(5L, d, e, 10 msat, 10) )) - val Success(route :: Nil) = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) + val Success(route :: Nil) = findRoute(g, a, makeRecipients(e), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route2Ids(route) == 1 :: 2 :: 4 :: 5 :: Nil) } @@ -643,7 +644,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(5L, e, d, 0 msat, 0) // e -> d )) - val Success(route :: Nil) = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) + val Success(route :: Nil) = findRoute(g, a, makeRecipients(d), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route2Ids(route) == 1 :: 3 :: 5 :: Nil) } @@ -676,7 +677,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(7L, c, f, 1 msat, 0, balance_opt = Some(DEFAULT_AMOUNT_MSAT)) )) - val fourShortestPaths = Graph.yenKshortestPaths(g1, d, f, DEFAULT_AMOUNT_MSAT, Set.empty, Set.empty, Set.empty, pathsToFind = 4, Left(NO_WEIGHT_RATIOS), BlockHeight(0), noopBoundaries, includeLocalChannelCost = false) + val fourShortestPaths = Graph.yenKshortestPaths(g1, d, Seq(f), DEFAULT_AMOUNT_MSAT, Set.empty, Set.empty, Set.empty, pathsToFind = 4, Left(NO_WEIGHT_RATIOS), BlockHeight(0), noopBoundaries, includeLocalChannelCost = false) assert(fourShortestPaths.size == 4) assert(hops2Ids(fourShortestPaths(0).path.map(graphEdgeToHop)) == 2 :: 5 :: Nil) // D -> E -> F assert(hops2Ids(fourShortestPaths(1).path.map(graphEdgeToHop)) == 1 :: 3 :: 5 :: Nil) // D -> A -> E -> F @@ -685,7 +686,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { // Update balance D -> A to evict the last path (balance too low) val g2 = g1.addEdge(makeEdge(1L, d, a, 1 msat, 0, balance_opt = Some(DEFAULT_AMOUNT_MSAT + 3.msat))) - val threeShortestPaths = Graph.yenKshortestPaths(g2, d, f, DEFAULT_AMOUNT_MSAT, Set.empty, Set.empty, Set.empty, pathsToFind = 4, Left(NO_WEIGHT_RATIOS), BlockHeight(0), noopBoundaries, includeLocalChannelCost = false) + val threeShortestPaths = Graph.yenKshortestPaths(g2, d, Seq(f), DEFAULT_AMOUNT_MSAT, Set.empty, Set.empty, Set.empty, pathsToFind = 4, Left(NO_WEIGHT_RATIOS), BlockHeight(0), noopBoundaries, includeLocalChannelCost = false) assert(threeShortestPaths.size == 3) assert(hops2Ids(threeShortestPaths(0).path.map(graphEdgeToHop)) == 2 :: 5 :: Nil) // D -> E -> F assert(hops2Ids(threeShortestPaths(1).path.map(graphEdgeToHop)) == 1 :: 3 :: 5 :: Nil) // D -> A -> E -> F @@ -714,7 +715,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(90L, g, h, 2 msat, 0) )) - val twoShortestPaths = Graph.yenKshortestPaths(graph, c, h, DEFAULT_AMOUNT_MSAT, Set.empty, Set.empty, Set.empty, pathsToFind = 2, Left(NO_WEIGHT_RATIOS), BlockHeight(0), noopBoundaries, includeLocalChannelCost = false) + val twoShortestPaths = Graph.yenKshortestPaths(graph, c, Seq(h), DEFAULT_AMOUNT_MSAT, Set.empty, Set.empty, Set.empty, pathsToFind = 2, Left(NO_WEIGHT_RATIOS), BlockHeight(0), noopBoundaries, includeLocalChannelCost = false) assert(twoShortestPaths.size == 2) val shortest = twoShortestPaths(0) @@ -745,7 +746,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) // we ask for 3 shortest paths but only 2 can be found - val foundPaths = Graph.yenKshortestPaths(graph, a, f, DEFAULT_AMOUNT_MSAT, Set.empty, Set.empty, Set.empty, pathsToFind = 3, Left(NO_WEIGHT_RATIOS), BlockHeight(0), noopBoundaries, includeLocalChannelCost = false) + val foundPaths = Graph.yenKshortestPaths(graph, a, Seq(f), DEFAULT_AMOUNT_MSAT, Set.empty, Set.empty, Set.empty, pathsToFind = 3, Left(NO_WEIGHT_RATIOS), BlockHeight(0), noopBoundaries, includeLocalChannelCost = false) assert(foundPaths.size == 2) assert(hops2Ids(foundPaths(0).path.map(graphEdgeToHop)) == 1 :: 2 :: 3 :: Nil) // A -> B -> C -> F assert(hops2Ids(foundPaths(1).path.map(graphEdgeToHop)) == 1 :: 2 :: 4 :: 5 :: 6 :: Nil) // A -> B -> C -> D -> E -> F @@ -773,7 +774,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) for (_ <- 0 to 10) { - val Success(routes) = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, strictFee, numRoutes = 3, routeParams = strictFeeParams, currentBlockHeight = BlockHeight(400000)) + val Success(routes) = findRoute(g, a, makeRecipients(d), DEFAULT_AMOUNT_MSAT, strictFee, numRoutes = 3, routeParams = strictFeeParams, currentBlockHeight = BlockHeight(400000)) assert(routes.length == 2, routes) val weightedPath = Graph.pathWeight(a, route2Edges(routes.head), DEFAULT_AMOUNT_MSAT, BlockHeight(400000), Left(NO_WEIGHT_RATIOS), includeLocalChannelCost = false) val totalFees = weightedPath.amount - DEFAULT_AMOUNT_MSAT @@ -800,10 +801,10 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(7L, e, c, feeBase = 2 msat, 1000, minHtlc = 0 msat, capacity = largeCapacity, cltvDelta = CltvExpiryDelta(12)) )) - val Success(routeFeeOptimized :: Nil) = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) + val Success(routeFeeOptimized :: Nil) = findRoute(g, a, makeRecipients(d), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route2Nodes(routeFeeOptimized) == (a, b) :: (b, c) :: (c, d) :: Nil) - val Success(routeCltvOptimized :: Nil) = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = Left(WeightRatios( + val Success(routeCltvOptimized :: Nil) = findRoute(g, a, makeRecipients(d), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = Left(WeightRatios( baseFactor = 0, cltvDeltaFactor = 1, ageFactor = 0, @@ -812,7 +813,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { ))), currentBlockHeight = BlockHeight(400000)) assert(route2Nodes(routeCltvOptimized) == (a, e) :: (e, f) :: (f, d) :: Nil) - val Success(routeCapacityOptimized :: Nil) = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = Left(WeightRatios( + val Success(routeCapacityOptimized :: Nil) = findRoute(g, a, makeRecipients(d), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = Left(WeightRatios( baseFactor = 0, cltvDeltaFactor = 0, ageFactor = 0, @@ -834,7 +835,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(ShortChannelId.fromCoordinates(s"${currentBlockHeight.toLong}x0x6").success.value.toLong, f, d, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(144)) )) - val Success(routeScoreOptimized :: Nil) = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT / 2, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = Left(WeightRatios( + val Success(routeScoreOptimized :: Nil) = findRoute(g, a, makeRecipients(d), DEFAULT_AMOUNT_MSAT / 2, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = Left(WeightRatios( baseFactor = 0.01, ageFactor = 0.33, cltvDeltaFactor = 0.33, @@ -855,7 +856,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(6, f, d, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(12)) )) - val Success(routeScoreOptimized :: Nil) = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = Left(WeightRatios( + val Success(routeScoreOptimized :: Nil) = findRoute(g, a, makeRecipients(d), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = Left(WeightRatios( baseFactor = 0.01, ageFactor = 0.33, cltvDeltaFactor = 0.33, @@ -878,7 +879,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(6, f, d, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(144)) )) - val Success(routeScoreOptimized :: Nil) = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT / 2, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = Left(WeightRatios( + val Success(routeScoreOptimized :: Nil) = findRoute(g, a, makeRecipients(d), DEFAULT_AMOUNT_MSAT / 2, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = Left(WeightRatios( baseFactor = 0.01, ageFactor = 0.33, cltvDeltaFactor = 0.33, @@ -927,7 +928,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val targetNode = PublicKey(hex"024655b768ef40951b20053a5c4b951606d4d86085d51238f2c67c7dec29c792ca") val amount = 351000 msat - val Success(route :: Nil) = findRoute(g, thisNode, targetNode, amount, DEFAULT_MAX_FEE, 1, Set.empty, Set.empty, Set.empty, params, currentBlockHeight = BlockHeight(567634)) // simulate mainnet block for heuristic + val Success(route :: Nil) = findRoute(g, thisNode, makeRecipients(targetNode), amount, DEFAULT_MAX_FEE, 1, Set.empty, Set.empty, Set.empty, params, currentBlockHeight = BlockHeight(567634)) // simulate mainnet block for heuristic assert(route.length == 2) assert(route.clearHops.last.nextNodeId == targetNode) } @@ -964,20 +965,20 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val routeParams = DEFAULT_ROUTE_PARAMS.copy(mpp = MultiPartParams(2500 msat, 3)) { - val Success(routes) = findMultiPartRoute(g, a, b, amount, 1 msat, routeParams = routeParams, currentBlockHeight = BlockHeight(400000)) + val Success(routes) = findMultiPartRoute(g, a, makeRecipients(b), amount, 1 msat, routeParams = routeParams, currentBlockHeight = BlockHeight(400000)) assert(routes.length == 4, routes) assert(routes.forall(_.length == 1), routes) checkRouteAmounts(routes, amount, 0 msat) } { - val Success(routes) = findMultiPartRoute(g, a, b, amount, 1 msat, routeParams = routeParams.copy(randomize = true), currentBlockHeight = BlockHeight(400000)) + val Success(routes) = findMultiPartRoute(g, a, makeRecipients(b), amount, 1 msat, routeParams = routeParams.copy(randomize = true), currentBlockHeight = BlockHeight(400000)) assert(routes.length >= 4, routes) assert(routes.forall(_.length == 1), routes) checkRouteAmounts(routes, amount, 0 msat) } { // We set min-part-amount to a value that excludes channels 1 and 4. - val failure = findMultiPartRoute(g, a, b, amount, 1 msat, routeParams = routeParams.copy(mpp = MultiPartParams(16500 msat, 3)), currentBlockHeight = BlockHeight(400000)) + val failure = findMultiPartRoute(g, a, makeRecipients(b), amount, 1 msat, routeParams = routeParams.copy(mpp = MultiPartParams(16500 msat, 3)), currentBlockHeight = BlockHeight(400000)) assert(failure == Failure(RouteNotFound)) } } @@ -991,7 +992,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) val amount = 25000 msat - val Success(routes) = findMultiPartRoute(g, a, b, amount, 1 msat, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) + val Success(routes) = findMultiPartRoute(g, a, makeRecipients(b), amount, 1 msat, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(routes.length == 1, routes) checkRouteAmounts(routes, amount, 0 msat) assert(route2Ids(routes.head) == 1L :: Nil) @@ -1007,7 +1008,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) val amount = 65000 msat - val Success(routes) = findMultiPartRoute(g, a, b, amount, 1 msat, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) + val Success(routes) = findMultiPartRoute(g, a, makeRecipients(b), amount, 1 msat, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(routes.length == 4, routes) assert(routes.forall(_.length == 1), routes) checkRouteAmounts(routes, amount, 0 msat) @@ -1026,14 +1027,14 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) { - val Success(routes) = findMultiPartRoute(g, a, b, amount, 1 msat, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) + val Success(routes) = findMultiPartRoute(g, a, makeRecipients(b), amount, 1 msat, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(routes.length == 3, routes) assert(routes.forall(_.length == 1), routes) checkIgnoredChannels(routes, 2L) checkRouteAmounts(routes, amount, 0 msat) } { - val Success(routes) = findMultiPartRoute(g, a, b, amount, 1 msat, routeParams = DEFAULT_ROUTE_PARAMS.copy(randomize = true), currentBlockHeight = BlockHeight(400000)) + val Success(routes) = findMultiPartRoute(g, a, makeRecipients(b), amount, 1 msat, routeParams = DEFAULT_ROUTE_PARAMS.copy(randomize = true), currentBlockHeight = BlockHeight(400000)) assert(routes.length >= 3, routes) assert(routes.forall(_.length == 1), routes) checkIgnoredChannels(routes, 2L) @@ -1053,7 +1054,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val amount = 20000 msat val ignoredEdges = Set(ChannelDesc(ShortChannelId(2L), a, b), ChannelDesc(ShortChannelId(3L), a, b)) - val Success(routes) = findMultiPartRoute(g, a, b, amount, 1 msat, ignoredEdges = ignoredEdges, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) + val Success(routes) = findMultiPartRoute(g, a, makeRecipients(b), amount, 1 msat, ignoredEdges = ignoredEdges, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(routes.forall(_.length == 1), routes) checkIgnoredChannels(routes, 2L, 3L) checkRouteAmounts(routes, amount, 0 msat) @@ -1073,8 +1074,8 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val amount = 50000 msat // These pending HTLCs will have already been taken into account in the edge's `balance_opt` field: findMultiPartRoute // should ignore this information. - val pendingHtlcs = Seq(Route(10000 msat, graphEdgeToHop(edge_ab_1) :: Nil, None), Route(5000 msat, graphEdgeToHop(edge_ab_2) :: Nil, None)) - val Success(routes) = findMultiPartRoute(g, a, b, amount, 1 msat, pendingHtlcs = pendingHtlcs, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) + val pendingHtlcs = Seq(Route(10000 msat, graphEdgeToHop(edge_ab_1) :: Nil, makeRecipient(b)), Route(5000 msat, graphEdgeToHop(edge_ab_2) :: Nil, makeRecipient(b))) + val Success(routes) = findMultiPartRoute(g, a, makeRecipients(b), amount, 1 msat, pendingHtlcs = pendingHtlcs, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(routes.forall(_.length == 1), routes) checkRouteAmounts(routes, amount, 0 msat) } @@ -1088,7 +1089,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) val amount = 50000 msat - val Success(routes) = findMultiPartRoute(g, a, b, amount, 1 msat, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) + val Success(routes) = findMultiPartRoute(g, a, makeRecipients(b), amount, 1 msat, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(routes.forall(_.length == 1), routes) assert(routes.length >= 10, routes) assert(routes.forall(_.amount <= 5000.msat), routes) @@ -1105,7 +1106,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val amount = 30000 msat val routeParams = DEFAULT_ROUTE_PARAMS.copy(mpp = MultiPartParams(2500 msat, 5)) - val Success(routes) = findMultiPartRoute(g, a, b, amount, 1 msat, routeParams = routeParams, currentBlockHeight = BlockHeight(400000)) + val Success(routes) = findMultiPartRoute(g, a, makeRecipients(b), amount, 1 msat, routeParams = routeParams, currentBlockHeight = BlockHeight(400000)) assert(routes.forall(_.length == 1), routes) assert(routes.length == 3, routes) checkRouteAmounts(routes, amount, 0 msat) @@ -1121,10 +1122,10 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) val amount = 30000 msat - val maxFeeTooLow = findMultiPartRoute(g, a, b, amount, 1 msat, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) + val maxFeeTooLow = findMultiPartRoute(g, a, makeRecipients(b), amount, 1 msat, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(maxFeeTooLow == Failure(RouteNotFound)) - val Success(routes) = findMultiPartRoute(g, a, b, amount, 20 msat, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) + val Success(routes) = findMultiPartRoute(g, a, makeRecipients(b), amount, 20 msat, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(routes.forall(_.length <= 2), routes) assert(routes.length == 3, routes) checkRouteAmounts(routes, amount, 20 msat) @@ -1139,11 +1140,11 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) { - val result = findMultiPartRoute(g, a, b, 40000 msat, 1 msat, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) + val result = findMultiPartRoute(g, a, makeRecipients(b), 40000 msat, 1 msat, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(result == Failure(RouteNotFound)) } { - val result = findMultiPartRoute(g, a, b, 40000 msat, 1 msat, routeParams = DEFAULT_ROUTE_PARAMS.copy(randomize = true), currentBlockHeight = BlockHeight(400000)) + val result = findMultiPartRoute(g, a, makeRecipients(b), 40000 msat, 1 msat, routeParams = DEFAULT_ROUTE_PARAMS.copy(randomize = true), currentBlockHeight = BlockHeight(400000)) assert(result == Failure(RouteNotFound)) } } @@ -1156,7 +1157,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(4L, a, d, 0 msat, 0, minHtlc = 1 msat, capacity = 4500 sat), )) - val result = findMultiPartRoute(g, a, b, 5000000 msat, 1 msat, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) + val result = findMultiPartRoute(g, a, makeRecipients(b), 5000000 msat, 1 msat, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(result == Failure(RouteNotFound)) } @@ -1168,11 +1169,11 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) { - val result = findMultiPartRoute(g, a, b, 10000 msat, 1 msat, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) + val result = findMultiPartRoute(g, a, makeRecipients(b), 10000 msat, 1 msat, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(result == Failure(RouteNotFound)) } { - val result = findMultiPartRoute(g, a, b, 10000 msat, 1 msat, routeParams = DEFAULT_ROUTE_PARAMS.copy(randomize = true), currentBlockHeight = BlockHeight(400000)) + val result = findMultiPartRoute(g, a, makeRecipients(b), 10000 msat, 1 msat, routeParams = DEFAULT_ROUTE_PARAMS.copy(randomize = true), currentBlockHeight = BlockHeight(400000)) assert(result == Failure(RouteNotFound)) } } @@ -1195,38 +1196,38 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) { - val Success(routes) = findMultiPartRoute(g, a, e, amount, maxFee, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) + val Success(routes) = findMultiPartRoute(g, a, makeRecipients(e), amount, maxFee, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) checkRouteAmounts(routes, amount, maxFee) assert(routes2Ids(routes) == Set(Seq(1L, 2L, 3L), Seq(4L, 6L), Seq(5L, 6L))) } { // Update A - B with unknown balance, capacity should be used instead. val g1 = g.addEdge(edge_ab.copy(capacity = 15 sat, balance_opt = None)) - val Success(routes) = findMultiPartRoute(g1, a, e, amount, maxFee, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) + val Success(routes) = findMultiPartRoute(g1, a, makeRecipients(e), amount, maxFee, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) checkRouteAmounts(routes, amount, maxFee) assert(routes2Ids(routes) == Set(Seq(1L, 2L, 3L), Seq(4L, 6L), Seq(5L, 6L))) } { // Randomize routes. - val Success(routes) = findMultiPartRoute(g, a, e, amount, maxFee, routeParams = DEFAULT_ROUTE_PARAMS.copy(randomize = true), currentBlockHeight = BlockHeight(400000)) + val Success(routes) = findMultiPartRoute(g, a, makeRecipients(e), amount, maxFee, routeParams = DEFAULT_ROUTE_PARAMS.copy(randomize = true), currentBlockHeight = BlockHeight(400000)) checkRouteAmounts(routes, amount, maxFee) } { // Update balance A - B to be too low. val g1 = g.addEdge(edge_ab.copy(balance_opt = Some(2000 msat))) - val failure = findMultiPartRoute(g1, a, e, amount, maxFee, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) + val failure = findMultiPartRoute(g1, a, makeRecipients(e), amount, maxFee, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(failure == Failure(RouteNotFound)) } { // Update capacity A - B to be too low. val g1 = g.addEdge(edge_ab.copy(capacity = 5 sat, balance_opt = None)) - val failure = findMultiPartRoute(g1, a, e, amount, maxFee, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) + val failure = findMultiPartRoute(g1, a, makeRecipients(e), amount, maxFee, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(failure == Failure(RouteNotFound)) } { // Try to find a route with a maxFee that's too low. val maxFeeTooLow = 100 msat - val failure = findMultiPartRoute(g, a, e, amount, maxFeeTooLow, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) + val failure = findMultiPartRoute(g, a, makeRecipients(e), amount, maxFeeTooLow, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(failure == Failure(RouteNotFound)) } } @@ -1248,13 +1249,13 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { { // We can send single-part tiny payments. val (amount, maxFee) = (1400 msat, 30 msat) - val Success(routes) = findMultiPartRoute(g, a, e, amount, maxFee, routeParams = routeParams, currentBlockHeight = BlockHeight(400000)) + val Success(routes) = findMultiPartRoute(g, a, makeRecipients(e), amount, maxFee, routeParams = routeParams, currentBlockHeight = BlockHeight(400000)) checkRouteAmounts(routes, amount, maxFee) } { // But we don't want to split such tiny amounts. val (amount, maxFee) = (2000 msat, 150 msat) - val failure = findMultiPartRoute(g, a, e, amount, maxFee, routeParams = routeParams, currentBlockHeight = BlockHeight(400000)) + val failure = findMultiPartRoute(g, a, makeRecipients(e), amount, maxFee, routeParams = routeParams, currentBlockHeight = BlockHeight(400000)) assert(failure == Failure(RouteNotFound)) } } @@ -1267,7 +1268,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(3L, c, d, 15 msat, 50, minHtlc = 1 msat, capacity = 150 sat), )) - val Success(routes) = findMultiPartRoute(g, a, d, amount, maxFee, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) + val Success(routes) = findMultiPartRoute(g, a, makeRecipients(d), amount, maxFee, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) checkRouteAmounts(routes, amount, maxFee) assert(routes.length == 1, "payment shouldn't be split when we have one path with enough capacity") assert(routes2Ids(routes) == Set(Seq(1L, 2L, 3L))) @@ -1292,38 +1293,38 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) { - val Success(routes) = findMultiPartRoute(g, a, f, amount, maxFee, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) + val Success(routes) = findMultiPartRoute(g, a, makeRecipients(f), amount, maxFee, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) checkRouteAmounts(routes, amount, maxFee) assert(routes2Ids(routes) == Set(Seq(1L, 2L, 3L, 5L), Seq(1L, 4L, 5L), Seq(1L, 6L, 7L))) } { // Randomize routes. - val Success(routes) = findMultiPartRoute(g, a, f, amount, maxFee, routeParams = DEFAULT_ROUTE_PARAMS.copy(randomize = true), currentBlockHeight = BlockHeight(400000)) + val Success(routes) = findMultiPartRoute(g, a, makeRecipients(f), amount, maxFee, routeParams = DEFAULT_ROUTE_PARAMS.copy(randomize = true), currentBlockHeight = BlockHeight(400000)) checkRouteAmounts(routes, amount, maxFee) } { // Update A - B with unknown balance, capacity should be used instead. val g1 = g.addEdge(edge_ab.copy(capacity = 500 sat, balance_opt = None)) - val Success(routes) = findMultiPartRoute(g1, a, f, amount, maxFee, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) + val Success(routes) = findMultiPartRoute(g1, a, makeRecipients(f), amount, maxFee, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) checkRouteAmounts(routes, amount, maxFee) assert(routes2Ids(routes) == Set(Seq(1L, 2L, 3L, 5L), Seq(1L, 4L, 5L), Seq(1L, 6L, 7L))) } { // Update balance A - B to be too low to cover fees. val g1 = g.addEdge(edge_ab.copy(balance_opt = Some(400000 msat))) - val failure = findMultiPartRoute(g1, a, f, amount, maxFee, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) + val failure = findMultiPartRoute(g1, a, makeRecipients(f), amount, maxFee, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(failure == Failure(RouteNotFound)) } { // Update capacity A - B to be too low to cover fees. val g1 = g.addEdge(edge_ab.copy(capacity = 400 sat, balance_opt = None)) - val failure = findMultiPartRoute(g1, a, f, amount, maxFee, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) + val failure = findMultiPartRoute(g1, a, makeRecipients(f), amount, maxFee, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(failure == Failure(RouteNotFound)) } { // Try to find a route with a maxFee that's too low. val maxFeeTooLow = 100 msat - val failure = findMultiPartRoute(g, a, f, amount, maxFeeTooLow, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) + val failure = findMultiPartRoute(g, a, makeRecipients(f), amount, maxFeeTooLow, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(failure == Failure(RouteNotFound)) } } @@ -1357,7 +1358,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val amount = 15_000_000 msat val maxFee = 50_000 msat // this fee is enough to go through the preferred route val routeParams = DEFAULT_ROUTE_PARAMS.copy(randomize = false, mpp = MultiPartParams(50_000 msat, 5)) - val Success(routes) = findMultiPartRoute(g, a, d, amount, maxFee, routeParams = routeParams, currentBlockHeight = BlockHeight(400000)) + val Success(routes) = findMultiPartRoute(g, a, makeRecipients(d), amount, maxFee, routeParams = routeParams, currentBlockHeight = BlockHeight(400000)) checkRouteAmounts(routes, amount, maxFee) assert(routes2Ids(routes) == Set(Seq(100L, 101L))) } @@ -1365,14 +1366,14 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val amount = 15_000_000 msat val maxFee = 10_000 msat // this fee is too low to go through the preferred route val routeParams = DEFAULT_ROUTE_PARAMS.copy(randomize = false, mpp = MultiPartParams(50_000 msat, 5)) - val failure = findMultiPartRoute(g, a, d, amount, maxFee, routeParams = routeParams, currentBlockHeight = BlockHeight(400000)) + val failure = findMultiPartRoute(g, a, makeRecipients(d), amount, maxFee, routeParams = routeParams, currentBlockHeight = BlockHeight(400000)) assert(failure == Failure(RouteNotFound)) } { val amount = 5_000_000 msat val maxFee = 10_000 msat // this fee is enough to go through the preferred route, but the cheaper ones can handle it val routeParams = DEFAULT_ROUTE_PARAMS.copy(randomize = false, mpp = MultiPartParams(50_000 msat, 5)) - val Success(routes) = findMultiPartRoute(g, a, d, amount, maxFee, routeParams = routeParams, currentBlockHeight = BlockHeight(400000)) + val Success(routes) = findMultiPartRoute(g, a, makeRecipients(d), amount, maxFee, routeParams = routeParams, currentBlockHeight = BlockHeight(400000)) assert(routes.length == 5) routes.foreach(route => { assert(route.length == 2) @@ -1408,7 +1409,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val ignoredNodes = Set(d) val ignoredChannels = Set(ChannelDesc(ShortChannelId(2L), b, c)) - val Success(routes) = findMultiPartRoute(g, a, f, amount, maxFee, ignoredEdges = ignoredChannels, ignoredVertices = ignoredNodes, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) + val Success(routes) = findMultiPartRoute(g, a, makeRecipients(f), amount, maxFee, ignoredEdges = ignoredChannels, ignoredVertices = ignoredNodes, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) checkRouteAmounts(routes, amount, maxFee) assert(routes2Ids(routes) == Set(Seq(8L), Seq(9L, 10L))) } @@ -1430,7 +1431,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(6L, d, e, 1 msat, 0, minHtlc = 500 msat, maxHtlc = Some(4000 msat), capacity = 50 sat), )) - val Success(routes) = findMultiPartRoute(g, a, e, amount, maxFee, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) + val Success(routes) = findMultiPartRoute(g, a, makeRecipients(e), amount, maxFee, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) checkRouteAmounts(routes, amount, maxFee) assert(routes.length >= 4, routes) assert(routes.forall(_.amount <= 4000.msat), routes) @@ -1438,7 +1439,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { checkIgnoredChannels(routes, 1L, 2L) val maxFeeTooLow = 3 msat - val failure = findMultiPartRoute(g, a, e, amount, maxFeeTooLow, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) + val failure = findMultiPartRoute(g, a, makeRecipients(e), amount, maxFeeTooLow, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(failure == Failure(RouteNotFound)) } @@ -1465,32 +1466,32 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { { val (amount, maxFee) = (15000 msat, 50 msat) - val Success(routes) = findMultiPartRoute(g, d, f, amount, maxFee, routeParams = routeParams, currentBlockHeight = BlockHeight(400000)) + val Success(routes) = findMultiPartRoute(g, d, makeRecipients(f), amount, maxFee, routeParams = routeParams, currentBlockHeight = BlockHeight(400000)) checkRouteAmounts(routes, amount, maxFee) } { val (amount, maxFee) = (25000 msat, 100 msat) - val Success(routes) = findMultiPartRoute(g, d, f, amount, maxFee, routeParams = routeParams, currentBlockHeight = BlockHeight(400000)) + val Success(routes) = findMultiPartRoute(g, d, makeRecipients(f), amount, maxFee, routeParams = routeParams, currentBlockHeight = BlockHeight(400000)) checkRouteAmounts(routes, amount, maxFee) } { val (amount, maxFee) = (25000 msat, 50 msat) - val failure = findMultiPartRoute(g, d, f, amount, maxFee, routeParams = routeParams, currentBlockHeight = BlockHeight(400000)) + val failure = findMultiPartRoute(g, d, makeRecipients(f), amount, maxFee, routeParams = routeParams, currentBlockHeight = BlockHeight(400000)) assert(failure == Failure(RouteNotFound)) } { val (amount, maxFee) = (40000 msat, 100 msat) - val Success(routes) = findMultiPartRoute(g, d, f, amount, maxFee, routeParams = routeParams, currentBlockHeight = BlockHeight(400000)) + val Success(routes) = findMultiPartRoute(g, d, makeRecipients(f), amount, maxFee, routeParams = routeParams, currentBlockHeight = BlockHeight(400000)) checkRouteAmounts(routes, amount, maxFee) } { val (amount, maxFee) = (40000 msat, 100 msat) - val Success(routes) = findMultiPartRoute(g, d, f, amount, maxFee, routeParams = routeParams.copy(randomize = true), currentBlockHeight = BlockHeight(400000)) + val Success(routes) = findMultiPartRoute(g, d, makeRecipients(f), amount, maxFee, routeParams = routeParams.copy(randomize = true), currentBlockHeight = BlockHeight(400000)) checkRouteAmounts(routes, amount, maxFee) } { val (amount, maxFee) = (40000 msat, 50 msat) - val failure = findMultiPartRoute(g, d, f, amount, maxFee, routeParams = routeParams, currentBlockHeight = BlockHeight(400000)) + val failure = findMultiPartRoute(g, d, makeRecipients(f), amount, maxFee, routeParams = routeParams, currentBlockHeight = BlockHeight(400000)) assert(failure == Failure(RouteNotFound)) } } @@ -1511,18 +1512,18 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(11L, e, f, 5 msat, 100, minHtlc = 500 msat, capacity = 10 sat), ) - val Success(routes1) = findMultiPartRoute(g, a, e, amount, maxFeeE, extraEdges = extraEdges, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) + val Success(routes1) = findMultiPartRoute(g, a, makeRecipients(e), amount, maxFeeE, extraEdges = extraEdges, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) checkRouteAmounts(routes1, amount, maxFeeE) assert(routes1.length >= 3, routes1) assert(routes1.forall(_.amount <= 4000.msat), routes1) - val Success(routes2) = findMultiPartRoute(g, a, f, amount, maxFeeF, extraEdges = extraEdges, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) + val Success(routes2) = findMultiPartRoute(g, a, makeRecipients(f), amount, maxFeeF, extraEdges = extraEdges, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) checkRouteAmounts(routes2, amount, maxFeeF) assert(routes2.length >= 3, routes2) assert(routes2.forall(_.amount <= 4000.msat), routes2) val maxFeeTooLow = 40 msat - val failure = findMultiPartRoute(g, a, f, amount, maxFeeTooLow, extraEdges = extraEdges, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) + val failure = findMultiPartRoute(g, a, makeRecipients(f), amount, maxFeeTooLow, extraEdges = extraEdges, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(failure == Failure(RouteNotFound)) } @@ -1545,8 +1546,8 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(6L, d, e, 50 msat, 0, minHtlc = 100 msat, capacity = 25 sat), )) - val pendingHtlcs = Seq(Route(5000 msat, graphEdgeToHop(edge_ab) :: graphEdgeToHop(edge_be) :: Nil, None)) - val Success(routes) = findMultiPartRoute(g, a, e, amount, maxFee, pendingHtlcs = pendingHtlcs, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) + val pendingHtlcs = Seq(Route(5000 msat, graphEdgeToHop(edge_ab) :: graphEdgeToHop(edge_be) :: Nil, makeRecipient(b))) + val Success(routes) = findMultiPartRoute(g, a, makeRecipients(e), amount, maxFee, pendingHtlcs = pendingHtlcs, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(routes.forall(_.length == 2), routes) checkRouteAmounts(routes, amount, maxFee) checkIgnoredChannels(routes, 1L, 2L) @@ -1584,7 +1585,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(9L, c, f, Random.nextLong(250).msat, Random.nextInt(10000), minHtlc = Random.nextLong(100).msat, maxHtlc = Some((20000 + Random.nextLong(80000)).msat), CltvExpiryDelta(Random.nextInt(288)), capacity = (10 + Random.nextLong(100)).sat) )) - findMultiPartRoute(g, d, f, amount, maxFee, routeParams = DEFAULT_ROUTE_PARAMS.copy(randomize = true), currentBlockHeight = BlockHeight(400000)) match { + findMultiPartRoute(g, d, makeRecipients(f), amount, maxFee, routeParams = DEFAULT_ROUTE_PARAMS.copy(randomize = true), currentBlockHeight = BlockHeight(400000)) match { case Success(routes) => checkRouteAmounts(routes, amount, maxFee) case Failure(ex) => assert(ex == RouteNotFound) } @@ -1609,7 +1610,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(7L, f, b, 1000 msat, 1000), )) - val Success(routes) = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 3, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) + val Success(routes) = findRoute(g, a, makeRecipients(e), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 3, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(routes.length == 2) val route1 :: route2 :: Nil = routes assert(route2Ids(route1) == 1 :: 5 :: Nil) @@ -1634,7 +1635,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(7L, b, f, 1000 msat, 1000), )) - val Success(routes) = findRoute(g, e, a, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 3, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) + val Success(routes) = findRoute(g, e, makeRecipients(a), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 3, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(routes.length == 2) val route1 :: route2 :: Nil = routes assert(route2Ids(route1) == 5 :: 1 :: Nil) @@ -1670,7 +1671,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val g = DirectedGraph(makeEdges(10)) - val Success(routes) = findRoute(g, a, b, DEFAULT_AMOUNT_MSAT, 100000000 msat, numRoutes = 10, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) + val Success(routes) = findRoute(g, a, makeRecipients(b), DEFAULT_AMOUNT_MSAT, 100000000 msat, numRoutes = 10, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(routes.distinct.length == 10) } @@ -1702,7 +1703,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val g = DirectedGraph(makeEdges(10)) - val Success(routes) = findRoute(g, a, b, DEFAULT_AMOUNT_MSAT, 100000000 msat, numRoutes = 10, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) + val Success(routes) = findRoute(g, a, makeRecipients(b), DEFAULT_AMOUNT_MSAT, 100000000 msat, numRoutes = 10, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(routes.distinct.length == 10) val fees = routes.map(_.fee(false)) assert(fees.forall(_ == fees.head)) @@ -1713,8 +1714,8 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(1L, a, b, 1000 msat, 7000), )) - assert(findRoute(g, a, b, 10000000 msat, 10000 msat, numRoutes = 3, routeParams = DEFAULT_ROUTE_PARAMS.copy(includeLocalChannelCost = true), currentBlockHeight = BlockHeight(400000)) == Failure(RouteNotFound)) - assert(findRoute(g, a, b, 10000000 msat, 100000 msat, numRoutes = 3, routeParams = DEFAULT_ROUTE_PARAMS.copy(includeLocalChannelCost = true), currentBlockHeight = BlockHeight(400000)).isSuccess) + assert(findRoute(g, a, makeRecipients(b), 10000000 msat, 10000 msat, numRoutes = 3, routeParams = DEFAULT_ROUTE_PARAMS.copy(includeLocalChannelCost = true), currentBlockHeight = BlockHeight(400000)) == Failure(RouteNotFound)) + assert(findRoute(g, a, makeRecipients(b), 10000000 msat, 100000 msat, numRoutes = 3, routeParams = DEFAULT_ROUTE_PARAMS.copy(includeLocalChannelCost = true), currentBlockHeight = BlockHeight(400000)).isSuccess) } test("penalty per hop") { @@ -1738,19 +1739,19 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) { // No hop cost - val Success(routes) = findRoute(g, start, b, DEFAULT_AMOUNT_MSAT, 100000000 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) + val Success(routes) = findRoute(g, start, makeRecipients(b), DEFAULT_AMOUNT_MSAT, 100000000 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(routes.distinct.length == 1) val route :: Nil = routes assert(route2Ids(route) == 0 :: 2 :: 5 :: 6 :: 7 :: 4 :: Nil) } { // small base hop cost - val Success(routes) = findRoute(g, start, b, DEFAULT_AMOUNT_MSAT, 100000000 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = Left(WeightRatios(1, 0, 0, 0, RelayFees(100 msat, 0)))), currentBlockHeight = BlockHeight(400000)) + val Success(routes) = findRoute(g, start, makeRecipients(b), DEFAULT_AMOUNT_MSAT, 100000000 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = Left(WeightRatios(1, 0, 0, 0, RelayFees(100 msat, 0)))), currentBlockHeight = BlockHeight(400000)) assert(routes.distinct.length == 1) val route :: Nil = routes assert(route2Ids(route) == 0 :: 2 :: 3 :: 4 :: Nil) } { // large proportional hop cost - val Success(routes) = findRoute(g, start, b, DEFAULT_AMOUNT_MSAT, 100000000 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = Left(WeightRatios(1, 0, 0, 0, RelayFees(0 msat, 200)))), currentBlockHeight = BlockHeight(400000)) + val Success(routes) = findRoute(g, start, makeRecipients(b), DEFAULT_AMOUNT_MSAT, 100000000 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = Left(WeightRatios(1, 0, 0, 0, RelayFees(0 msat, 200)))), currentBlockHeight = BlockHeight(400000)) assert(routes.distinct.length == 1) val route :: Nil = routes assert(route2Ids(route) == 0 :: 1 :: Nil) @@ -1778,7 +1779,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { hopCost = RelayFees(0 msat, 0), useLogProbability = false, ) - val Success(routes) = findRoute(g, start, b, DEFAULT_AMOUNT_MSAT, 100000000 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = Right(hc)), currentBlockHeight = BlockHeight(400000)) + val Success(routes) = findRoute(g, start, makeRecipients(b), DEFAULT_AMOUNT_MSAT, 100000000 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = Right(hc)), currentBlockHeight = BlockHeight(400000)) assert(routes.distinct.length == 1) val route :: Nil = routes assert(route2Ids(route) == 0 :: 2 :: 3 :: 4 :: Nil) @@ -1792,7 +1793,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { hopCost = RelayFees(0 msat, 0), useLogProbability = true, ) - val Success(routes) = findRoute(g, start, b, DEFAULT_AMOUNT_MSAT, 100000000 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = Right(hc)), currentBlockHeight = BlockHeight(400000)) + val Success(routes) = findRoute(g, start, makeRecipients(b), DEFAULT_AMOUNT_MSAT, 100000000 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = Right(hc)), currentBlockHeight = BlockHeight(400000)) assert(routes.distinct.length == 1) val route :: Nil = routes assert(route2Ids(route) == 0 :: 2 :: 3 :: 4 :: Nil) @@ -1819,7 +1820,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { hopCost = RelayFees(0 msat, 0), useLogProbability = true, ) - val Success(routes) = findRoute(g, start, b, DEFAULT_AMOUNT_MSAT, 100000000 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = Right(hc)), currentBlockHeight = BlockHeight(400000)) + val Success(routes) = findRoute(g, start, makeRecipients(b), DEFAULT_AMOUNT_MSAT, 100000000 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = Right(hc)), currentBlockHeight = BlockHeight(400000)) assert(routes.distinct.length == 1) val route :: Nil = routes assert(route2Ids(route) == 0 :: 2 :: 3 :: 4 :: Nil) @@ -1839,7 +1840,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { hopCost = RelayFees(0 msat, 0), useLogProbability = true, ) - val Success(routes) = findRoute(g, a, c, DEFAULT_AMOUNT_MSAT, 100000000 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = Right(hc)), currentBlockHeight = BlockHeight(400000)) + val Success(routes) = findRoute(g, a, makeRecipients(c), DEFAULT_AMOUNT_MSAT, 100000000 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = Right(hc)), currentBlockHeight = BlockHeight(400000)) assert(routes.distinct.length == 1) val route :: Nil = routes assert(route2Ids(route) == 1 :: 2 :: Nil) @@ -1862,7 +1863,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { capacityFactor = 0.5, hopCost = RelayFees(500 msat, 200), ) - val Success(routes) = findRoute(g, a, c, DEFAULT_AMOUNT_MSAT, 100_000_000 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = Left(wr)), currentBlockHeight = BlockHeight(400000)) + val Success(routes) = findRoute(g, a, makeRecipients(c), DEFAULT_AMOUNT_MSAT, 100_000_000 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = Left(wr)), currentBlockHeight = BlockHeight(400000)) assert(routes.distinct.length == 1) val route :: Nil = routes assert(route2Ids(route) == recentChannelId :: Nil) @@ -1874,11 +1875,11 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { { val routeParams = DEFAULT_ROUTE_PARAMS.copy(includeLocalChannelCost = true, boundaries = SearchBoundaries(100_999 msat, 0.0, 6, CltvExpiryDelta(576))) - assert(findMultiPartRoute(g, a, b, amount, 100_999 msat, Set.empty, Set.empty, Set.empty, Nil, routeParams = routeParams, currentBlockHeight = BlockHeight(400000)) == Failure(RouteNotFound)) + assert(findMultiPartRoute(g, a, makeRecipients(b), amount, 100_999 msat, Set.empty, Set.empty, Set.empty, Nil, routeParams = routeParams, currentBlockHeight = BlockHeight(400000)) == Failure(RouteNotFound)) } { val routeParams = DEFAULT_ROUTE_PARAMS.copy(includeLocalChannelCost = true, boundaries = SearchBoundaries(101_000 msat, 0.0, 6, CltvExpiryDelta(576))) - assert(findMultiPartRoute(g, a, b, amount, 101_000 msat, Set.empty, Set.empty, Set.empty, Nil, routeParams = routeParams, currentBlockHeight = BlockHeight(400000)).isSuccess) + assert(findMultiPartRoute(g, a, makeRecipients(b), amount, 101_000 msat, Set.empty, Set.empty, Set.empty, Nil, routeParams = routeParams, currentBlockHeight = BlockHeight(400000)).isSuccess) } } @@ -1899,7 +1900,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { capacityFactor = 1, hopCost = RelayFees(500 msat, 200), ) - val Success(routes) = findRoute(g, a, d, 50000 msat, 100000000 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = Left(wr), includeLocalChannelCost = true), currentBlockHeight = BlockHeight(400000)) + val Success(routes) = findRoute(g, a, makeRecipients(d), 50000 msat, 100000000 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = Left(wr), includeLocalChannelCost = true), currentBlockHeight = BlockHeight(400000)) val route :: Nil = routes assert(route2Ids(route) == 3 :: 4 :: Nil) } @@ -1978,4 +1979,7 @@ object RouteCalculationSpec { assert(routes.map(_.fee(false)).sum <= maxFee, routes) } + def makeRecipient(nodeId: PublicKey): ClearRecipient = ClearRecipient(nodeId, randomBytes32(), None) + def makeRecipients(nodeId: PublicKey): Seq[ClearRecipient] = Seq(makeRecipient(nodeId)) + } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala index c0576920d0..a7c1cd9bdb 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala @@ -31,7 +31,7 @@ import fr.acinq.eclair.payment.{Bolt11Invoice, Invoice} import fr.acinq.eclair.router.Announcements.{makeChannelUpdate, makeNodeAnnouncement} import fr.acinq.eclair.router.BaseRouterSpec.channelAnnouncement import fr.acinq.eclair.router.Graph.RoutingHeuristics -import fr.acinq.eclair.router.RouteCalculationSpec.{DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, DEFAULT_ROUTE_PARAMS} +import fr.acinq.eclair.router.RouteCalculationSpec.{DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, DEFAULT_ROUTE_PARAMS, makeRecipient, makeRecipients} import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.transactions.Scripts import fr.acinq.eclair.wire.protocol._ @@ -330,7 +330,7 @@ class RouterSpec extends BaseRouterSpec { import fixture._ val sender = TestProbe() // no route a->f - sender.send(router, RouteRequest(a, f, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, routeParams = DEFAULT_ROUTE_PARAMS)) + sender.send(router, RouteRequest(a, makeRecipients(f), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, routeParams = DEFAULT_ROUTE_PARAMS)) sender.expectMsg(Failure(RouteNotFound)) } @@ -338,7 +338,7 @@ class RouterSpec extends BaseRouterSpec { import fixture._ val sender = TestProbe() // no route a->f - sender.send(router, RouteRequest(randomKey().publicKey, f, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, routeParams = DEFAULT_ROUTE_PARAMS)) + sender.send(router, RouteRequest(randomKey().publicKey, makeRecipients(f), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, routeParams = DEFAULT_ROUTE_PARAMS)) sender.expectMsg(Failure(RouteNotFound)) } @@ -346,19 +346,19 @@ class RouterSpec extends BaseRouterSpec { import fixture._ val sender = TestProbe() // no route a->f - sender.send(router, RouteRequest(a, randomKey().publicKey, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, routeParams = DEFAULT_ROUTE_PARAMS)) + sender.send(router, RouteRequest(a, makeRecipients(randomKey().publicKey), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, routeParams = DEFAULT_ROUTE_PARAMS)) sender.expectMsg(Failure(RouteNotFound)) } test("route found") { fixture => import fixture._ val sender = TestProbe() - sender.send(router, RouteRequest(a, d, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, routeParams = DEFAULT_ROUTE_PARAMS)) + sender.send(router, RouteRequest(a, makeRecipients(d), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, routeParams = DEFAULT_ROUTE_PARAMS)) val res = sender.expectMsgType[RouteResponse] assert(res.routes.head.clearHops.map(_.nodeId).toList == a :: b :: c :: Nil) assert(res.routes.head.clearHops.last.nextNodeId == d) - sender.send(router, RouteRequest(a, h, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, routeParams = DEFAULT_ROUTE_PARAMS)) + sender.send(router, RouteRequest(a, makeRecipients(h), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, routeParams = DEFAULT_ROUTE_PARAMS)) val res1 = sender.expectMsgType[RouteResponse] assert(res1.routes.head.clearHops.map(_.nodeId).toList == a :: g :: Nil) assert(res1.routes.head.clearHops.last.nextNodeId == h) @@ -373,7 +373,7 @@ class RouterSpec extends BaseRouterSpec { val extraHop_cx = ExtraHop(c, ShortChannelId(1), 10 msat, 11, CltvExpiryDelta(12)) val extraHop_xy = ExtraHop(x, ShortChannelId(2), 10 msat, 11, CltvExpiryDelta(12)) val extraHop_yz = ExtraHop(y, ShortChannelId(3), 20 msat, 21, CltvExpiryDelta(22)) - sender.send(router, RouteRequest(a, z, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, extraEdges = Bolt11Invoice.toExtraEdges(extraHop_cx :: extraHop_xy :: extraHop_yz :: Nil, z), routeParams = DEFAULT_ROUTE_PARAMS)) + sender.send(router, RouteRequest(a, makeRecipients(z), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, extraEdges = Bolt11Invoice.toExtraEdges(extraHop_cx :: extraHop_xy :: extraHop_yz :: Nil, z), routeParams = DEFAULT_ROUTE_PARAMS)) val res = sender.expectMsgType[RouteResponse] assert(res.routes.head.clearHops.map(_.nodeId).toList == a :: b :: c :: x :: y :: Nil) assert(res.routes.head.clearHops.last.nextNodeId == z) @@ -383,7 +383,7 @@ class RouterSpec extends BaseRouterSpec { import fixture._ val sender = TestProbe() val peerConnection = TestProbe() - sender.send(router, RouteRequest(a, d, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, routeParams = DEFAULT_ROUTE_PARAMS)) + sender.send(router, RouteRequest(a, makeRecipients(d), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, routeParams = DEFAULT_ROUTE_PARAMS)) val res = sender.expectMsgType[RouteResponse] assert(res.routes.head.clearHops.map(_.nodeId).toList == a :: b :: c :: Nil) assert(res.routes.head.clearHops.last.nextNodeId == d) @@ -391,21 +391,21 @@ class RouterSpec extends BaseRouterSpec { val channelUpdate_cd1 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_c, d, scid_cd, CltvExpiryDelta(3), 0 msat, 153000 msat, 4, htlcMaximum, enable = false) peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, channelUpdate_cd1)) peerConnection.expectMsg(TransportHandler.ReadAck(channelUpdate_cd1)) - sender.send(router, RouteRequest(a, d, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, routeParams = DEFAULT_ROUTE_PARAMS)) + sender.send(router, RouteRequest(a, makeRecipients(d), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, routeParams = DEFAULT_ROUTE_PARAMS)) sender.expectMsg(Failure(RouteNotFound)) } test("route not found (private channel disabled)") { fixture => import fixture._ val sender = TestProbe() - sender.send(router, RouteRequest(a, h, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, routeParams = DEFAULT_ROUTE_PARAMS)) + sender.send(router, RouteRequest(a, makeRecipients(h), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, routeParams = DEFAULT_ROUTE_PARAMS)) val res = sender.expectMsgType[RouteResponse] assert(res.routes.head.clearHops.map(_.nodeId).toList == a :: g :: Nil) assert(res.routes.head.clearHops.last.nextNodeId == h) val channelUpdate_ag1 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, g, alias_ag_private, CltvExpiryDelta(7), 0 msat, 10 msat, 10, htlcMaximum, enable = false) sender.send(router, LocalChannelUpdate(sender.ref, channelId_ag_private, scids_ag_private, g, None, channelUpdate_ag1, CommitmentsSpec.makeCommitments(10000 msat, 15000 msat, a, g, announceChannel = false))) - sender.send(router, RouteRequest(a, h, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, routeParams = DEFAULT_ROUTE_PARAMS)) + sender.send(router, RouteRequest(a, makeRecipients(h), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, routeParams = DEFAULT_ROUTE_PARAMS)) sender.expectMsg(Failure(RouteNotFound)) } @@ -414,44 +414,44 @@ class RouterSpec extends BaseRouterSpec { val sender = TestProbe() // Via private channels. - sender.send(router, RouteRequest(a, g, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, routeParams = DEFAULT_ROUTE_PARAMS)) + sender.send(router, RouteRequest(a, makeRecipients(g), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, routeParams = DEFAULT_ROUTE_PARAMS)) sender.expectMsgType[RouteResponse] - sender.send(router, RouteRequest(a, g, 50000000 msat, Long.MaxValue.msat, routeParams = DEFAULT_ROUTE_PARAMS)) + sender.send(router, RouteRequest(a, makeRecipients(g), 50000000 msat, Long.MaxValue.msat, routeParams = DEFAULT_ROUTE_PARAMS)) sender.expectMsg(Failure(BalanceTooLow)) - sender.send(router, RouteRequest(a, g, 50000000 msat, Long.MaxValue.msat, allowMultiPart = true, routeParams = DEFAULT_ROUTE_PARAMS)) + sender.send(router, RouteRequest(a, makeRecipients(g), 50000000 msat, Long.MaxValue.msat, allowMultiPart = true, routeParams = DEFAULT_ROUTE_PARAMS)) sender.expectMsg(Failure(BalanceTooLow)) // Via public channels. - sender.send(router, RouteRequest(a, b, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, routeParams = DEFAULT_ROUTE_PARAMS)) + sender.send(router, RouteRequest(a, makeRecipients(b), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, routeParams = DEFAULT_ROUTE_PARAMS)) sender.expectMsgType[RouteResponse] val commitments1 = CommitmentsSpec.makeCommitments(10000000 msat, 20000000 msat, a, b, announceChannel = true) sender.send(router, LocalChannelUpdate(sender.ref, null, scids_ab, b, Some(chan_ab), update_ab, commitments1)) - sender.send(router, RouteRequest(a, b, 12000000 msat, Long.MaxValue.msat, routeParams = DEFAULT_ROUTE_PARAMS)) + sender.send(router, RouteRequest(a, makeRecipients(b), 12000000 msat, Long.MaxValue.msat, routeParams = DEFAULT_ROUTE_PARAMS)) sender.expectMsg(Failure(BalanceTooLow)) - sender.send(router, RouteRequest(a, b, 12000000 msat, Long.MaxValue.msat, allowMultiPart = true, routeParams = DEFAULT_ROUTE_PARAMS)) + sender.send(router, RouteRequest(a, makeRecipients(b), 12000000 msat, Long.MaxValue.msat, allowMultiPart = true, routeParams = DEFAULT_ROUTE_PARAMS)) sender.expectMsg(Failure(BalanceTooLow)) - sender.send(router, RouteRequest(a, b, 5000000 msat, Long.MaxValue.msat, routeParams = DEFAULT_ROUTE_PARAMS)) + sender.send(router, RouteRequest(a, makeRecipients(b), 5000000 msat, Long.MaxValue.msat, routeParams = DEFAULT_ROUTE_PARAMS)) sender.expectMsgType[RouteResponse] - sender.send(router, RouteRequest(a, b, 5000000 msat, Long.MaxValue.msat, allowMultiPart = true, routeParams = DEFAULT_ROUTE_PARAMS)) + sender.send(router, RouteRequest(a, makeRecipients(b), 5000000 msat, Long.MaxValue.msat, allowMultiPart = true, routeParams = DEFAULT_ROUTE_PARAMS)) sender.expectMsgType[RouteResponse] } test("temporary channel exclusion") { fixture => import fixture._ val sender = TestProbe() - sender.send(router, RouteRequest(a, d, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, routeParams = DEFAULT_ROUTE_PARAMS)) + sender.send(router, RouteRequest(a, makeRecipients(d), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, routeParams = DEFAULT_ROUTE_PARAMS)) sender.expectMsgType[RouteResponse] val bc = ChannelDesc(scid_bc, b, c) // let's exclude channel b->c sender.send(router, ExcludeChannel(bc, Some(1 hour))) - sender.send(router, RouteRequest(a, d, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, routeParams = DEFAULT_ROUTE_PARAMS)) + sender.send(router, RouteRequest(a, makeRecipients(d), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, routeParams = DEFAULT_ROUTE_PARAMS)) sender.expectMsg(Failure(RouteNotFound)) // note that cb is still available! - sender.send(router, RouteRequest(d, a, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, routeParams = DEFAULT_ROUTE_PARAMS)) + sender.send(router, RouteRequest(d, makeRecipients(a), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, routeParams = DEFAULT_ROUTE_PARAMS)) sender.expectMsgType[RouteResponse] // let's remove the exclusion sender.send(router, LiftChannelExclusion(bc)) - sender.send(router, RouteRequest(a, d, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, routeParams = DEFAULT_ROUTE_PARAMS)) + sender.send(router, RouteRequest(a, makeRecipients(d), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, routeParams = DEFAULT_ROUTE_PARAMS)) sender.expectMsgType[RouteResponse] } @@ -471,7 +471,7 @@ class RouterSpec extends BaseRouterSpec { val sender = TestProbe() val preComputedRoute = PredefinedNodeRoute(Seq(a, b, c, d)) - sender.send(router, FinalizeRoute(10000 msat, preComputedRoute)) + sender.send(router, FinalizeRoute(10000 msat, preComputedRoute, makeRecipient(d))) val response = sender.expectMsgType[RouteResponse] // the route hasn't changed (nodes are the same) @@ -485,7 +485,7 @@ class RouterSpec extends BaseRouterSpec { val sender = TestProbe() val preComputedRoute = PredefinedChannelRoute(d, Seq(scid_ab, scid_bc, scid_cd)) - sender.send(router, FinalizeRoute(10000 msat, preComputedRoute)) + sender.send(router, FinalizeRoute(10000 msat, preComputedRoute, makeRecipient(d))) val response = sender.expectMsgType[RouteResponse] // the route hasn't changed (nodes are the same) @@ -501,7 +501,7 @@ class RouterSpec extends BaseRouterSpec { { // using the channel alias val preComputedRoute = PredefinedChannelRoute(g, Seq(alias_ag_private)) - sender.send(router, FinalizeRoute(10000 msat, preComputedRoute)) + sender.send(router, FinalizeRoute(10000 msat, preComputedRoute, makeRecipient(g))) val response = sender.expectMsgType[RouteResponse] assert(response.routes.length == 1) val route = response.routes.head @@ -512,7 +512,7 @@ class RouterSpec extends BaseRouterSpec { { // using the real scid val preComputedRoute = PredefinedChannelRoute(g, Seq(scid_ag_private)) - sender.send(router, FinalizeRoute(10000 msat, preComputedRoute)) + sender.send(router, FinalizeRoute(10000 msat, preComputedRoute, makeRecipient(g))) val response = sender.expectMsgType[RouteResponse] assert(response.routes.length == 1) val route = response.routes.head @@ -522,7 +522,7 @@ class RouterSpec extends BaseRouterSpec { } { val preComputedRoute = PredefinedChannelRoute(h, Seq(scid_ag_private, scid_gh)) - sender.send(router, FinalizeRoute(10000 msat, preComputedRoute)) + sender.send(router, FinalizeRoute(10000 msat, preComputedRoute, makeRecipient(h))) val response = sender.expectMsgType[RouteResponse] assert(response.routes.length == 1) val route = response.routes.head @@ -543,7 +543,7 @@ class RouterSpec extends BaseRouterSpec { val amount = 10_000.msat // the amount affects the way we estimate the channel capacity of the hinted channel assert(amount < RoutingHeuristics.CAPACITY_CHANNEL_LOW) - sender.send(router, FinalizeRoute(amount, preComputedRoute, extraEdges = Seq(invoiceRoutingHint))) + sender.send(router, FinalizeRoute(amount, preComputedRoute, makeRecipient(targetNodeId), extraEdges = Seq(invoiceRoutingHint))) val response = sender.expectMsgType[RouteResponse] assert(response.routes.length == 1) val route = response.routes.head @@ -558,7 +558,7 @@ class RouterSpec extends BaseRouterSpec { val amount = RoutingHeuristics.CAPACITY_CHANNEL_LOW * 2 // the amount affects the way we estimate the channel capacity of the hinted channel assert(amount > RoutingHeuristics.CAPACITY_CHANNEL_LOW) - sender.send(router, FinalizeRoute(amount, preComputedRoute, extraEdges = Seq(invoiceRoutingHint))) + sender.send(router, FinalizeRoute(amount, preComputedRoute, makeRecipient(targetNodeId), extraEdges = Seq(invoiceRoutingHint))) val response = sender.expectMsgType[RouteResponse] assert(response.routes.length == 1) val route = response.routes.head @@ -575,22 +575,22 @@ class RouterSpec extends BaseRouterSpec { { val preComputedRoute = PredefinedChannelRoute(d, Seq(scid_ab, scid_cd)) - sender.send(router, FinalizeRoute(10000 msat, preComputedRoute)) + sender.send(router, FinalizeRoute(10000 msat, preComputedRoute, makeRecipient(d))) sender.expectMsgType[Status.Failure] } { val preComputedRoute = PredefinedChannelRoute(d, Seq(scid_ab, scid_bc)) - sender.send(router, FinalizeRoute(10000 msat, preComputedRoute)) + sender.send(router, FinalizeRoute(10000 msat, preComputedRoute, makeRecipient(d))) sender.expectMsgType[Status.Failure] } { val preComputedRoute = PredefinedChannelRoute(d, Seq(scid_bc, scid_cd)) - sender.send(router, FinalizeRoute(10000 msat, preComputedRoute)) + sender.send(router, FinalizeRoute(10000 msat, preComputedRoute, makeRecipient(d))) sender.expectMsgType[Status.Failure] } { val preComputedRoute = PredefinedChannelRoute(d, Seq(scid_ab, ShortChannelId(1105), scid_cd)) - sender.send(router, FinalizeRoute(10000 msat, preComputedRoute)) + sender.send(router, FinalizeRoute(10000 msat, preComputedRoute, makeRecipient(d))) sender.expectMsgType[Status.Failure] } } diff --git a/eclair-node/src/test/resources/api/findroute-full b/eclair-node/src/test/resources/api/findroute-full index c2bfff5ffc..b3681611e3 100644 --- a/eclair-node/src/test/resources/api/findroute-full +++ b/eclair-node/src/test/resources/api/findroute-full @@ -1 +1 @@ -{"routes":[{"amount":456,"clearHops":[{"nodeId":"03007e67dc5a8fd2b2ef21cb310ab6359ddb51f3f86a8b79b8b1e23bc3a6ea150a","nextNodeId":"026105f6cb4862810be989385d16f04b0f748f6f2a14040338b1a534d45b4be1c1","source":{"type":"announcement","channelUpdate":{"signature":"92cf3f12e161391986eb2cd7106ddab41a23c734f8f1ed120fb64f4b91f98f690ecf930388e62965f8aefbf1adafcd25a572669a125396dcfb83615208754679","chainHash":"024b7b3626554c44dcc2454ee3812458bfa68d9fced466edfab470844cb7ffe2","shortChannelId":"1x2x3","timestamp":{"iso":"1970-01-01T00:00:00Z","unix":0},"channelFlags":{"isEnabled":true,"isNode1":true},"cltvExpiryDelta":0,"htlcMinimumMsat":1,"feeBaseMsat":1,"feeProportionalMillionths":1,"htlcMaximumMsat":20000000,"tlvStream":{"records":[],"unknown":[]}}}},{"nodeId":"026105f6cb4862810be989385d16f04b0f748f6f2a14040338b1a534d45b4be1c1","nextNodeId":"038cfa2b5857843ee90cff91b06f692c0d8fe201921ee6387aee901d64f43699f0","source":{"type":"announcement","channelUpdate":{"signature":"92cf3f12e161391986eb2cd7106ddab41a23c734f8f1ed120fb64f4b91f98f690ecf930388e62965f8aefbf1adafcd25a572669a125396dcfb83615208754679","chainHash":"024b7b3626554c44dcc2454ee3812458bfa68d9fced466edfab470844cb7ffe2","shortChannelId":"1x2x4","timestamp":{"iso":"1970-01-01T00:00:00Z","unix":0},"channelFlags":{"isEnabled":true,"isNode1":true},"cltvExpiryDelta":0,"htlcMinimumMsat":1,"feeBaseMsat":1,"feeProportionalMillionths":1,"htlcMaximumMsat":20000000,"tlvStream":{"records":[],"unknown":[]}}}},{"nodeId":"038cfa2b5857843ee90cff91b06f692c0d8fe201921ee6387aee901d64f43699f0","nextNodeId":"02be60276e294c6921240daae33a361d214d02578656df0e74c61a09c3196e51df","source":{"type":"announcement","channelUpdate":{"signature":"92cf3f12e161391986eb2cd7106ddab41a23c734f8f1ed120fb64f4b91f98f690ecf930388e62965f8aefbf1adafcd25a572669a125396dcfb83615208754679","chainHash":"024b7b3626554c44dcc2454ee3812458bfa68d9fced466edfab470844cb7ffe2","shortChannelId":"1x2x5","timestamp":{"iso":"1970-01-01T00:00:00Z","unix":0},"channelFlags":{"isEnabled":true,"isNode1":true},"cltvExpiryDelta":0,"htlcMinimumMsat":1,"feeBaseMsat":1,"feeProportionalMillionths":1,"htlcMaximumMsat":20000000,"tlvStream":{"records":[],"unknown":[]}}}}]}]} \ No newline at end of file +{"routes":[{"amount":456,"clearHops":[{"nodeId":"03007e67dc5a8fd2b2ef21cb310ab6359ddb51f3f86a8b79b8b1e23bc3a6ea150a","nextNodeId":"026105f6cb4862810be989385d16f04b0f748f6f2a14040338b1a534d45b4be1c1","source":{"type":"announcement","channelUpdate":{"signature":"92cf3f12e161391986eb2cd7106ddab41a23c734f8f1ed120fb64f4b91f98f690ecf930388e62965f8aefbf1adafcd25a572669a125396dcfb83615208754679","chainHash":"024b7b3626554c44dcc2454ee3812458bfa68d9fced466edfab470844cb7ffe2","shortChannelId":"1x2x3","timestamp":{"iso":"1970-01-01T00:00:00Z","unix":0},"channelFlags":{"isEnabled":true,"isNode1":true},"cltvExpiryDelta":0,"htlcMinimumMsat":1,"feeBaseMsat":1,"feeProportionalMillionths":1,"htlcMaximumMsat":20000000,"tlvStream":{"records":[],"unknown":[]}}}},{"nodeId":"026105f6cb4862810be989385d16f04b0f748f6f2a14040338b1a534d45b4be1c1","nextNodeId":"038cfa2b5857843ee90cff91b06f692c0d8fe201921ee6387aee901d64f43699f0","source":{"type":"announcement","channelUpdate":{"signature":"92cf3f12e161391986eb2cd7106ddab41a23c734f8f1ed120fb64f4b91f98f690ecf930388e62965f8aefbf1adafcd25a572669a125396dcfb83615208754679","chainHash":"024b7b3626554c44dcc2454ee3812458bfa68d9fced466edfab470844cb7ffe2","shortChannelId":"1x2x4","timestamp":{"iso":"1970-01-01T00:00:00Z","unix":0},"channelFlags":{"isEnabled":true,"isNode1":true},"cltvExpiryDelta":0,"htlcMinimumMsat":1,"feeBaseMsat":1,"feeProportionalMillionths":1,"htlcMaximumMsat":20000000,"tlvStream":{"records":[],"unknown":[]}}}},{"nodeId":"038cfa2b5857843ee90cff91b06f692c0d8fe201921ee6387aee901d64f43699f0","nextNodeId":"02be60276e294c6921240daae33a361d214d02578656df0e74c61a09c3196e51df","source":{"type":"announcement","channelUpdate":{"signature":"92cf3f12e161391986eb2cd7106ddab41a23c734f8f1ed120fb64f4b91f98f690ecf930388e62965f8aefbf1adafcd25a572669a125396dcfb83615208754679","chainHash":"024b7b3626554c44dcc2454ee3812458bfa68d9fced466edfab470844cb7ffe2","shortChannelId":"1x2x5","timestamp":{"iso":"1970-01-01T00:00:00Z","unix":0},"channelFlags":{"isEnabled":true,"isNode1":true},"cltvExpiryDelta":0,"htlcMinimumMsat":1,"feeBaseMsat":1,"feeProportionalMillionths":1,"htlcMaximumMsat":20000000,"tlvStream":{"records":[],"unknown":[]}}}}],"recipient":{"nodeId":"02b5e21cd29b915e6039529d64b2a27a72c20d08910cac9702bef2dd1b1c88ee5f","paymentSecret":"64c383f6df992da265d8222e3d0ba5f0a0e0bdb2b16568e37c22c13a058237c6","features":{"activated":{},"unknown":[]},"additionalTlvs":[],"userCustomTlvs":[]}}]} \ No newline at end of file diff --git a/eclair-node/src/test/resources/api/received-expired b/eclair-node/src/test/resources/api/received-expired index d1d4b57414..99b22eec47 100644 --- a/eclair-node/src/test/resources/api/received-expired +++ b/eclair-node/src/test/resources/api/received-expired @@ -1 +1 @@ -{"invoice":{"prefix":"lnbc","timestamp":1496314658,"nodeId":"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad","serialized":"lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp","description":"1 cup coffee","paymentHash":"0001020304050607080900010203040506070809000102030405060708090102","expiry":60,"amount":250000000,"features":{"activated":{},"unknown":[]},"routingInfo":[]},"paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","paymentType":"Standard","createdAt":{"iso":"1970-01-01T00:00:00.042Z","unix":0},"status":{"type":"expired"}} \ No newline at end of file +{"invoice":{"prefix":"lnbc","timestamp":1496314658,"nodeId":"03094af7f02c8e08d9999829e378b6156c89f2577b912b3bf7b7723864686d0055","serialized":"lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpusp5sqzvk0wus00t7pjsatzvd46t6g3dpg9j6q2mnn0es9wzvx2ynzasaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rsp9nsq2q","description":"1 cup coffee","paymentHash":"0001020304050607080900010203040506070809000102030405060708090102","expiry":60,"amount":250000000,"features":{"activated":{},"unknown":[]},"routingInfo":[]},"paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","paymentType":"Standard","createdAt":{"iso":"1970-01-01T00:00:00.042Z","unix":0},"status":{"type":"expired"}} \ No newline at end of file diff --git a/eclair-node/src/test/resources/api/received-pending b/eclair-node/src/test/resources/api/received-pending index abc7fdb9f7..2ff8e94c9d 100644 --- a/eclair-node/src/test/resources/api/received-pending +++ b/eclair-node/src/test/resources/api/received-pending @@ -1 +1 @@ -{"invoice":{"prefix":"lnbc","timestamp":1496314658,"nodeId":"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad","serialized":"lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp","description":"1 cup coffee","paymentHash":"0001020304050607080900010203040506070809000102030405060708090102","expiry":60,"amount":250000000,"features":{"activated":{},"unknown":[]},"routingInfo":[]},"paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","paymentType":"Standard","createdAt":{"iso":"1970-01-01T00:00:00.042Z","unix":0},"status":{"type":"pending"}} \ No newline at end of file +{"invoice":{"prefix":"lnbc","timestamp":1496314658,"nodeId":"0397e1041191f81ac6260335033d1eccaca224bbc375cab6f4f6e39718da500d78","serialized":"lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpusp52w8wy2g82dcr0qzcrregmf7emvkdetc0xz8wkn2e0hrqpa6h88asaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspmq6uta","description":"1 cup coffee","paymentHash":"0001020304050607080900010203040506070809000102030405060708090102","expiry":60,"amount":250000000,"features":{"activated":{},"unknown":[]},"routingInfo":[]},"paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","paymentType":"Standard","createdAt":{"iso":"1970-01-01T00:00:00.042Z","unix":0},"status":{"type":"pending"}} \ No newline at end of file diff --git a/eclair-node/src/test/resources/api/received-success b/eclair-node/src/test/resources/api/received-success index 6fdc44e62d..d2af2ec5c7 100644 --- a/eclair-node/src/test/resources/api/received-success +++ b/eclair-node/src/test/resources/api/received-success @@ -1 +1 @@ -{"invoice":{"prefix":"lnbc","timestamp":1496314658,"nodeId":"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad","serialized":"lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp","description":"1 cup coffee","paymentHash":"0001020304050607080900010203040506070809000102030405060708090102","expiry":60,"amount":250000000,"features":{"activated":{},"unknown":[]},"routingInfo":[]},"paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","paymentType":"Standard","createdAt":{"iso":"1970-01-01T00:00:00.042Z","unix":0},"status":{"type":"received","amount":42,"receivedAt":{"iso":"2021-10-05T13:12:23.777Z","unix":1633439543}}} \ No newline at end of file +{"invoice":{"prefix":"lnbc","timestamp":1496314658,"nodeId":"0384f391c31dcdc11f9eaa05237d3899f56fbcf0ed280afe02c602bc2cef956d0c","serialized":"lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpusp554nypxne5a24ut584fnqjwu38jg86w6wksnxw9fnlnztfzl0ax3qaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rsppdtsrm","description":"1 cup coffee","paymentHash":"0001020304050607080900010203040506070809000102030405060708090102","expiry":60,"amount":250000000,"features":{"activated":{},"unknown":[]},"routingInfo":[]},"paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","paymentType":"Standard","createdAt":{"iso":"1970-01-01T00:00:00.042Z","unix":0},"status":{"type":"received","amount":42,"receivedAt":{"iso":"2021-10-05T13:12:23.777Z","unix":1633439543}}} \ No newline at end of file diff --git a/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala b/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala index 354ebd63bf..533bd4fad6 100644 --- a/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala +++ b/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala @@ -611,7 +611,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM eclair.send(any, any, any, any, any, any, any)(any[Timeout]) returns Future.failed(new IllegalArgumentException("invoice has expired")) val mockService = new MockService(eclair) - val invoice = "lnbc12580n1pw2ywztpp554ganw404sh4yjkwnysgn3wjcxfcq7gtx53gxczkjr9nlpc3hzvqdq2wpskwctddyxqr4rqrzjqwryaup9lh50kkranzgcdnn2fgvx390wgj5jd07rwr3vxeje0glc7z9rtvqqwngqqqqqqqlgqqqqqeqqjqrrt8smgjvfj7sg38dwtr9kc9gg3era9k3t2hvq3cup0jvsrtrxuplevqgfhd3rzvhulgcxj97yjuj8gdx8mllwj4wzjd8gdjhpz3lpqqvk2plh" + val invoice = "lnbc12580n1pw2ywztpp554ganw404sh4yjkwnysgn3wjcxfcq7gtx53gxczkjr9nlpc3hzvqdq2wpskwctddyxqr4rqrzjqwryaup9lh50kkranzgcdnn2fgvx390wgj5jd07rwr3vxeje0glc7z9rtvqqwngqqqqqqqlgqqqqqeqqjqsp535dwtpeqfugfmslrnq6lm6hnlernf84ue38t6nf77t86lkhnhx4srrt8smgjvfj7sg38dwtr9kc9gg3era9k3t2hvq3cup0jvsrtrxuplevqgfhd3rzvhulgcxj97yjuj8gdx8mllwj4wzjd8gdjhpz3lpqqz08fh9" Post("/payinvoice", FormData("invoice" -> invoice).toEntity) ~> addCredentials(BasicHttpCredentials("", mockApi().password)) ~> @@ -626,7 +626,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM } test("'send' method should allow blocking until payment completes") { - val invoice = "lnbc12580n1pw2ywztpp554ganw404sh4yjkwnysgn3wjcxfcq7gtx53gxczkjr9nlpc3hzvqdq2wpskwctddyxqr4rqrzjqwryaup9lh50kkranzgcdnn2fgvx390wgj5jd07rwr3vxeje0glc7z9rtvqqwngqqqqqqqlgqqqqqeqqjqrrt8smgjvfj7sg38dwtr9kc9gg3era9k3t2hvq3cup0jvsrtrxuplevqgfhd3rzvhulgcxj97yjuj8gdx8mllwj4wzjd8gdjhpz3lpqqvk2plh" + val invoice = "lnbc12580n1pw2ywztpp554ganw404sh4yjkwnysgn3wjcxfcq7gtx53gxczkjr9nlpc3hzvqdq2wpskwctddyxqr4rqrzjqwryaup9lh50kkranzgcdnn2fgvx390wgj5jd07rwr3vxeje0glc7z9rtvqqwngqqqqqqqlgqqqqqeqqjqsp5r5daw4nv6naqjy5m44uqk6yhutkjpzq4eg84zzx22f3ru9r6yueqrrt8smgjvfj7sg38dwtr9kc9gg3era9k3t2hvq3cup0jvsrtrxuplevqgfhd3rzvhulgcxj97yjuj8gdx8mllwj4wzjd8gdjhpz3lpqq4zj6z8" val eclair = mock[Eclair] val mockService = new MockService(eclair) @@ -671,7 +671,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM } test("'send' method should correctly forward amount parameters to EclairImpl 1") { - val invoice = "lnbc12580n1pw2ywztpp554ganw404sh4yjkwnysgn3wjcxfcq7gtx53gxczkjr9nlpc3hzvqdq2wpskwctddyxqr4rqrzjqwryaup9lh50kkranzgcdnn2fgvx390wgj5jd07rwr3vxeje0glc7z9rtvqqwngqqqqqqqlgqqqqqeqqjqrrt8smgjvfj7sg38dwtr9kc9gg3era9k3t2hvq3cup0jvsrtrxuplevqgfhd3rzvhulgcxj97yjuj8gdx8mllwj4wzjd8gdjhpz3lpqqvk2plh" + val invoice = "lnbc12580n1pw2ywztpp554ganw404sh4yjkwnysgn3wjcxfcq7gtx53gxczkjr9nlpc3hzvqdq2wpskwctddyxqr4rqrzjqwryaup9lh50kkranzgcdnn2fgvx390wgj5jd07rwr3vxeje0glc7z9rtvqqwngqqqqqqqlgqqqqqeqqjqsp5slwhqzqd3lxkc0xh44hczdfg52tzkz39mjslzxkyfpvn3a6ymd3qrrt8smgjvfj7sg38dwtr9kc9gg3era9k3t2hvq3cup0jvsrtrxuplevqgfhd3rzvhulgcxj97yjuj8gdx8mllwj4wzjd8gdjhpz3lpqq4vzvdc" val eclair = mock[Eclair] eclair.send(any, any, any, any, any, any, any)(any[Timeout]) returns Future.successful(UUID.randomUUID()) @@ -688,7 +688,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM } test("'send' method should correctly forward amount parameters to EclairImpl 2") { - val invoice = "lnbc12580n1pw2ywztpp554ganw404sh4yjkwnysgn3wjcxfcq7gtx53gxczkjr9nlpc3hzvqdq2wpskwctddyxqr4rqrzjqwryaup9lh50kkranzgcdnn2fgvx390wgj5jd07rwr3vxeje0glc7z9rtvqqwngqqqqqqqlgqqqqqeqqjqrrt8smgjvfj7sg38dwtr9kc9gg3era9k3t2hvq3cup0jvsrtrxuplevqgfhd3rzvhulgcxj97yjuj8gdx8mllwj4wzjd8gdjhpz3lpqqvk2plh" + val invoice = "lnbc12580n1pw2ywztpp554ganw404sh4yjkwnysgn3wjcxfcq7gtx53gxczkjr9nlpc3hzvqdq2wpskwctddyxqr4rqrzjqwryaup9lh50kkranzgcdnn2fgvx390wgj5jd07rwr3vxeje0glc7z9rtvqqwngqqqqqqqlgqqqqqeqqjqsp5m5rpq3xhpzy5r986a4gdm0mtn6tumzf8l2pdtud93u68wdq6r59qrrt8smgjvfj7sg38dwtr9kc9gg3era9k3t2hvq3cup0jvsrtrxuplevqgfhd3rzvhulgcxj97yjuj8gdx8mllwj4wzjd8gdjhpz3lpqqvup35u" val eclair = mock[Eclair] eclair.send(any, any, any, any, any, any, any)(any[Timeout]) returns Future.successful(UUID.randomUUID()) @@ -705,7 +705,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM } test("'send' method should correctly forward amount parameters to EclairImpl 3") { - val invoice = "lnbc12580n1pw2ywztpp554ganw404sh4yjkwnysgn3wjcxfcq7gtx53gxczkjr9nlpc3hzvqdq2wpskwctddyxqr4rqrzjqwryaup9lh50kkranzgcdnn2fgvx390wgj5jd07rwr3vxeje0glc7z9rtvqqwngqqqqqqqlgqqqqqeqqjqrrt8smgjvfj7sg38dwtr9kc9gg3era9k3t2hvq3cup0jvsrtrxuplevqgfhd3rzvhulgcxj97yjuj8gdx8mllwj4wzjd8gdjhpz3lpqqvk2plh" + val invoice = "lnbc12580n1pw2ywztpp554ganw404sh4yjkwnysgn3wjcxfcq7gtx53gxczkjr9nlpc3hzvqdq2wpskwctddyxqr4rqrzjqwryaup9lh50kkranzgcdnn2fgvx390wgj5jd07rwr3vxeje0glc7z9rtvqqwngqqqqqqqlgqqqqqeqqjqsp5jkl3al88afhthda4cx09a36tuaq6rxt54kk9j9j2gq0m5truwmjqrrt8smgjvfj7sg38dwtr9kc9gg3era9k3t2hvq3cup0jvsrtrxuplevqgfhd3rzvhulgcxj97yjuj8gdx8mllwj4wzjd8gdjhpz3lpqqzlgzlr" val eclair = mock[Eclair] eclair.send(any, any, any, any, any, any, any)(any[Timeout]) returns Future.successful(UUID.randomUUID()) @@ -722,7 +722,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM } test("'send' method should correctly forward amount parameters to EclairImpl 4") { - val invoice = "lnbc12580n1pw2ywztpp554ganw404sh4yjkwnysgn3wjcxfcq7gtx53gxczkjr9nlpc3hzvqdq2wpskwctddyxqr4rqrzjqwryaup9lh50kkranzgcdnn2fgvx390wgj5jd07rwr3vxeje0glc7z9rtvqqwngqqqqqqqlgqqqqqeqqjqrrt8smgjvfj7sg38dwtr9kc9gg3era9k3t2hvq3cup0jvsrtrxuplevqgfhd3rzvhulgcxj97yjuj8gdx8mllwj4wzjd8gdjhpz3lpqqvk2plh" + val invoice = "lnbc12580n1pw2ywztpp554ganw404sh4yjkwnysgn3wjcxfcq7gtx53gxczkjr9nlpc3hzvqdq2wpskwctddyxqr4rqrzjqwryaup9lh50kkranzgcdnn2fgvx390wgj5jd07rwr3vxeje0glc7z9rtvqqwngqqqqqqqlgqqqqqeqqjqsp50zu2v2ax6xem8hre2ftw7034twtfmgfg9fy2lrl0ew6u0nl05ycsrrt8smgjvfj7sg38dwtr9kc9gg3era9k3t2hvq3cup0jvsrtrxuplevqgfhd3rzvhulgcxj97yjuj8gdx8mllwj4wzjd8gdjhpz3lpqqkk7der" val eclair = mock[Eclair] eclair.send(any, any, any, any, any, any, any)(any[Timeout]) returns Future.successful(UUID.randomUUID()) @@ -831,7 +831,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM } test("'getreceivedinfo' 2") { - val invoice = "lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp" + val invoice = "lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpusp52w8wy2g82dcr0qzcrregmf7emvkdetc0xz8wkn2e0hrqpa6h88asaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspmq6uta" val defaultPayment = IncomingStandardPayment(Bolt11Invoice.fromString(invoice).get, ByteVector32.One, PaymentType.Standard, 42 unixms, IncomingPaymentStatus.Pending) val eclair = mock[Eclair] val pending = randomBytes32() @@ -851,7 +851,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM } test("'getreceivedinfo' 3") { - val invoice = "lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp" + val invoice = "lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpusp5sqzvk0wus00t7pjsatzvd46t6g3dpg9j6q2mnn0es9wzvx2ynzasaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rsp9nsq2q" val defaultPayment = IncomingStandardPayment(Bolt11Invoice.fromString(invoice).get, ByteVector32.One, PaymentType.Standard, 42 unixms, IncomingPaymentStatus.Pending) val eclair = mock[Eclair] val expired = randomBytes32() @@ -871,7 +871,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM } test("'getreceivedinfo' 4") { - val invoice = "lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp" + val invoice = "lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpusp554nypxne5a24ut584fnqjwu38jg86w6wksnxw9fnlnztfzl0ax3qaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rsppdtsrm" val defaultPayment = IncomingStandardPayment(Bolt11Invoice.fromString(invoice).get, ByteVector32.One, PaymentType.Standard, 42 unixms, IncomingPaymentStatus.Pending) val eclair = mock[Eclair] val received = randomBytes32() @@ -996,8 +996,8 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM } test("'findroute' method response should support nodeId, shortChannelId and full formats") { - val serializedInvoice = "lnbc12580n1pw2ywztpp554ganw404sh4yjkwnysgn3wjcxfcq7gtx53gxczkjr9nlpc3hzvqdq2wpskwctddyxqr4rqrzjqwryaup9lh50kkranzgcdnn2fgvx390wgj5jd07rwr3vxeje0glc7z9rtvqqwngqqqqqqqlgqqqqqeqqjqrrt8smgjvfj7sg38dwtr9kc9gg3era9k3t2hvq3cup0jvsrtrxuplevqgfhd3rzvhulgcxj97yjuj8gdx8mllwj4wzjd8gdjhpz3lpqqvk2plh" - val invoice = Invoice.fromString(serializedInvoice).get + val serializedInvoice = "lnbc12580n1pw2ywztpp554ganw404sh4yjkwnysgn3wjcxfcq7gtx53gxczkjr9nlpc3hzvqdq2wpskwctddyxqr4rqrzjqwryaup9lh50kkranzgcdnn2fgvx390wgj5jd07rwr3vxeje0glc7z9rtvqqwngqqqqqqqlgqqqqqeqqjqsp5vnpc8aklnyk6yewcyghr6za97zswp0djk9jk3cmuytqn5pvzxlrqrrt8smgjvfj7sg38dwtr9kc9gg3era9k3t2hvq3cup0jvsrtrxuplevqgfhd3rzvhulgcxj97yjuj8gdx8mllwj4wzjd8gdjhpz3lpqq64rhq2" + val invoice = Bolt11Invoice.fromString(serializedInvoice).get val mockChannelUpdate1 = ChannelUpdate( signature = ByteVector64.fromValidHex("92cf3f12e161391986eb2cd7106ddab41a23c734f8f1ed120fb64f4b91f98f690ecf930388e62965f8aefbf1adafcd25a572669a125396dcfb83615208754679"), @@ -1021,7 +1021,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM val eclair = mock[Eclair] val mockService = new MockService(eclair) - eclair.findRoute(any, any, any, any, any, any, any, any)(any[Timeout]) returns Future.successful(Router.RouteResponse(Seq(Router.Route(456.msat, mockHops, None)))) + eclair.findRoute(any, any, any, any, any, any, any, any)(any[Timeout]) returns Future.successful(Router.RouteResponse(Seq(Router.Route(456.msat, mockHops, invoice.recipient)))) // invalid format Post("/findroute", FormData("format" -> "invalid-output-format", "invoice" -> serializedInvoice, "amountMsat" -> "456")) ~> @@ -1031,7 +1031,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM check { assert(handled) assert(status == BadRequest) - eclair.findRoute(invoice.nodeId, 456.msat, any, any, any, any, any, any)(any[Timeout]).wasNever(called) + eclair.findRoute(invoice.recipient.nodeId, 456.msat, any, any, any, any, any, any)(any[Timeout]).wasNever(called) } // default format @@ -1044,7 +1044,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM assert(status == OK) val response = entityAs[String] matchTestJson("findroute-nodeid", response) - eclair.findRoute(invoice.nodeId, 456.msat, any, any, any, any, any, any)(any[Timeout]).wasCalled(once) + eclair.findRoute(invoice.recipient.nodeId, 456.msat, any, any, any, any, any, any)(any[Timeout]).wasCalled(once) } Post("/findroute", FormData("format" -> "nodeId", "invoice" -> serializedInvoice, "amountMsat" -> "456")) ~> @@ -1056,7 +1056,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM assert(status == OK) val response = entityAs[String] matchTestJson("findroute-nodeid", response) - eclair.findRoute(invoice.nodeId, 456.msat, any, any, any, any, any, any)(any[Timeout]).wasCalled(twice) + eclair.findRoute(invoice.recipient.nodeId, 456.msat, any, any, any, any, any, any)(any[Timeout]).wasCalled(twice) } Post("/findroute", FormData("format" -> "shortChannelId", "invoice" -> serializedInvoice, "amountMsat" -> "456")) ~> @@ -1068,7 +1068,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM assert(status == OK) val response = entityAs[String] matchTestJson("findroute-scid", response) - eclair.findRoute(invoice.nodeId, 456.msat, any, any, any, any, any, any)(any[Timeout]).wasCalled(threeTimes) + eclair.findRoute(invoice.recipient.nodeId, 456.msat, any, any, any, any, any, any)(any[Timeout]).wasCalled(threeTimes) } Post("/findroute", FormData("format" -> "full", "invoice" -> serializedInvoice, "amountMsat" -> "456")) ~> @@ -1080,7 +1080,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM assert(status == OK) val response = entityAs[String] matchTestJson("findroute-full", response) - eclair.findRoute(invoice.nodeId, 456.msat, any, any, any, any, any, any)(any[Timeout]).wasCalled(fourTimes) + eclair.findRoute(invoice.recipient.nodeId, 456.msat, any, any, any, any, any, any)(any[Timeout]).wasCalled(fourTimes) } } @@ -1213,6 +1213,8 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM } private def matchTestJson(apiName: String, response: String) = { + println(apiName) + println(response) val resource = getClass.getResourceAsStream(s"/api/$apiName") val expectedResponse = Try(Source.fromInputStream(resource).mkString).getOrElse { throw new IllegalArgumentException(s"Mock file for $apiName not found") From 139e6e26426384bcfde282290a2f8f280d658653 Mon Sep 17 00:00:00 2001 From: Thomas HUET Date: Thu, 29 Sep 2022 10:46:13 +0200 Subject: [PATCH 4/8] Add new tests for blinded payments --- .../BlindPaymentIntegrationSpec.scala | 221 ++++++++++++++++++ .../integration/PaymentIntegrationSpec.scala | 11 - 2 files changed, 221 insertions(+), 11 deletions(-) create mode 100644 eclair-core/src/test/scala/fr/acinq/eclair/integration/BlindPaymentIntegrationSpec.scala diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/BlindPaymentIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/BlindPaymentIntegrationSpec.scala new file mode 100644 index 0000000000..18736e9ee9 --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/BlindPaymentIntegrationSpec.scala @@ -0,0 +1,221 @@ +/* + * Copyright 2022 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fr.acinq.eclair.integration + +import akka.actor.ActorRef +import akka.actor.typed.scaladsl.adapter.{ClassicActorRefOps, actorRefAdapter} +import akka.testkit.TestProbe +import com.typesafe.config.ConfigFactory +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, Crypto, SatoshiLong} +import fr.acinq.eclair.blockchain.bitcoind.BitcoindService.BitcoinReq +import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher +import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{Watch, WatchFundingConfirmed} +import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient +import fr.acinq.eclair.channel._ +import fr.acinq.eclair.channel.fsm.Channel.{BroadcastChannelUpdate, PeriodicRefresh} +import fr.acinq.eclair.crypto.Sphinx.DecryptedFailurePacket +import fr.acinq.eclair.crypto.TransportHandler +import fr.acinq.eclair.db._ +import fr.acinq.eclair.io.Peer.PeerRoutingMessage +import fr.acinq.eclair.payment._ +import fr.acinq.eclair.payment.receive.MultiPartHandler.{ReceiveOfferPayment, ReceiveStandardPayment} +import fr.acinq.eclair.payment.relay.Relayer +import fr.acinq.eclair.payment.relay.Relayer.RelayFees +import fr.acinq.eclair.payment.send.PaymentInitiator.{SendPaymentToNode, SendTrampolinePayment} +import fr.acinq.eclair.router.Graph.WeightRatios +import fr.acinq.eclair.router.Router.{GossipDecision, PublicChannel} +import fr.acinq.eclair.router.{Announcements, AnnouncementsBatchValidationSpec, CannotRouteToSelf, Router} +import fr.acinq.eclair.wire.protocol.OfferTypes.{InvoiceRequest, Offer} +import fr.acinq.eclair.wire.protocol.{ChannelAnnouncement, ChannelUpdate, IncorrectOrUnknownPaymentDetails} +import fr.acinq.eclair.{CltvExpiryDelta, Features, Kit, MilliSatoshiLong, ShortChannelId, TimestampMilli, randomBytes32, randomKey} +import org.json4s.JsonAST.{JString, JValue} +import scodec.bits.ByteVector + +import java.util.UUID +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.duration._ +import scala.jdk.CollectionConverters._ + +class BlindPaymentIntegrationSpec extends IntegrationSpec { + + test("start eclair nodes") { + instantiateEclairNode("A", ConfigFactory.parseMap(Map("eclair.node-alias" -> "A", "eclair.channel.expiry-delta-blocks" -> 130, "eclair.server.port" -> 29730, "eclair.api.port" -> 28080, "eclair.features.option_route_blinding" -> "optional", "eclair.channel.channel-flags.announce-channel" -> false).asJava).withFallback(withDefaultCommitment).withFallback(commonConfig)) // A's channels are private + instantiateEclairNode("B", ConfigFactory.parseMap(Map("eclair.node-alias" -> "B", "eclair.channel.expiry-delta-blocks" -> 131, "eclair.server.port" -> 29731, "eclair.api.port" -> 28081, "eclair.features.option_route_blinding" -> "optional").asJava).withFallback(withDefaultCommitment).withFallback(commonConfig)) + instantiateEclairNode("C", ConfigFactory.parseMap(Map("eclair.node-alias" -> "C", "eclair.channel.expiry-delta-blocks" -> 132, "eclair.server.port" -> 29732, "eclair.api.port" -> 28082, "eclair.features.option_route_blinding" -> "optional").asJava).withFallback(withDefaultCommitment).withFallback(commonConfig)) + instantiateEclairNode("D", ConfigFactory.parseMap(Map("eclair.node-alias" -> "D", "eclair.channel.expiry-delta-blocks" -> 133, "eclair.server.port" -> 29733, "eclair.api.port" -> 28083, "eclair.features.option_route_blinding" -> "optional").asJava).withFallback(withDefaultCommitment).withFallback(commonConfig)) + instantiateEclairNode("E", ConfigFactory.parseMap(Map("eclair.node-alias" -> "E", "eclair.channel.expiry-delta-blocks" -> 134, "eclair.server.port" -> 29734, "eclair.api.port" -> 28084).asJava).withFallback(withDefaultCommitment).withFallback(commonConfig)) + instantiateEclairNode("F", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F", "eclair.channel.expiry-delta-blocks" -> 135, "eclair.server.port" -> 29735, "eclair.api.port" -> 28085, "eclair.features.option_route_blinding" -> "optional").asJava).withFallback(commonConfig)) + instantiateEclairNode("G", ConfigFactory.parseMap(Map("eclair.node-alias" -> "G", "eclair.channel.expiry-delta-blocks" -> 136, "eclair.server.port" -> 29736, "eclair.api.port" -> 28086, "eclair.relay.fees.public-channels.fee-base-msat" -> 1010, "eclair.relay.fees.public-channels.fee-proportional-millionths" -> 102, "eclair.trampoline-payments-enable" -> true).asJava).withFallback(commonConfig)) + } + + test("connect nodes") { + // ,--G--, + // / \ + // A---B ------- C ==== D + // \ / \ + // '--E--' '--F + // + // All channels have fees 1 sat + 200 millionths, except for G that have fees 1010 msat + 102 millionths + + val sender = TestProbe() + val eventListener = TestProbe() + nodes.values.foreach(_.system.eventStream.subscribe(eventListener.ref, classOf[ChannelStateChanged])) + + connect(nodes("A"), nodes("B"), 11000000 sat, 0 msat) + connect(nodes("B"), nodes("C"), 2000000 sat, 0 msat) + connect(nodes("C"), nodes("D"), 5000000 sat, 0 msat) + connect(nodes("C"), nodes("D"), 5000000 sat, 0 msat) + connect(nodes("C"), nodes("F"), 16000000 sat, 0 msat) + connect(nodes("B"), nodes("E"), 10000000 sat, 0 msat) + connect(nodes("E"), nodes("C"), 10000000 sat, 0 msat) + connect(nodes("B"), nodes("G"), 16000000 sat, 0 msat) + connect(nodes("G"), nodes("C"), 16000000 sat, 0 msat) + + val numberOfChannels = 9 + val channelEndpointsCount = 2 * numberOfChannels + + // we make sure all channels have set up their WatchConfirmed for the funding tx + awaitCond({ + val watches = nodes.values.foldLeft(Set.empty[Watch[_]]) { + case (watches, setup) => + setup.watcher ! ZmqWatcher.ListWatches(sender.ref) + watches ++ sender.expectMsgType[Set[Watch[_]]] + } + watches.count(_.isInstanceOf[WatchFundingConfirmed]) == channelEndpointsCount + }, max = 20 seconds, interval = 1 second) + + // confirming the funding tx + generateBlocks(2) + + within(60 seconds) { + var count = 0 + while (count < channelEndpointsCount) { + if (eventListener.expectMsgType[ChannelStateChanged](60 seconds).currentState == NORMAL) count = count + 1 + } + } + } + + def awaitAnnouncements(subset: Map[String, Kit], nodes: Int, privateChannels: Int, publicChannels: Int, privateUpdates: Int, publicUpdates: Int): Unit = { + val sender = TestProbe() + subset.foreach { + case (node, setup) => + withClue(node) { + awaitAssert({ + sender.send(setup.router, Router.GetRouterData) + val data = sender.expectMsgType[Router.Data] + assert(data.nodes.size == nodes) + assert(data.privateChannels.size == privateChannels) + assert(data.channels.size == publicChannels) + assert(data.privateChannels.values.flatMap(pc => pc.update_1_opt.toSeq ++ pc.update_2_opt.toSeq).size == privateUpdates) + assert(data.channels.values.flatMap(pc => pc.update_1_opt.toSeq ++ pc.update_2_opt.toSeq).size == publicUpdates) + }, max = 10 seconds, interval = 1 second) + } + } + } + + test("wait for network announcements") { + // generating more blocks so that all funding txes are buried under at least 6 blocks + generateBlocks(4) + // A requires private channels, as a consequence: + // - only A and B know about channel A-B (and there is no channel_announcement) + // - A is not announced (no node_announcement) + awaitAnnouncements(nodes.view.filterKeys(key => List("A", "B").contains(key)).toMap, nodes = 6, privateChannels = 1, publicChannels = 8, privateUpdates = 2, publicUpdates = 16) + awaitAnnouncements(nodes.view.filterKeys(key => List("C", "D", "E", "G").contains(key)).toMap, nodes = 6, privateChannels = 0, publicChannels = 8, privateUpdates = 0, publicUpdates = 16) + } + + test("wait for channels balance") { + // Channels balance should now be available in the router + val sender = TestProbe() + val nodeId = nodes("C").nodeParams.nodeId + sender.send(nodes("C").router, Router.GetRoutingState) + val routingState = sender.expectMsgType[Router.RoutingState] + val publicChannels = routingState.channels.filter(pc => Set(pc.ann.nodeId1, pc.ann.nodeId2).contains(nodeId)) + assert(publicChannels.nonEmpty) + publicChannels.foreach(pc => assert(pc.meta_opt.exists(m => m.balance1 > 0.msat || m.balance2 > 0.msat), pc)) + } + + test("send an HTLC A->D") { + val (sender, eventListener) = (TestProbe(), TestProbe()) + nodes("D").system.eventStream.subscribe(eventListener.ref, classOf[PaymentMetadataReceived]) + + val recipientKey = randomKey() + val payerKey = randomKey() + + // first we retrieve an invoice from D + val amount = 4200000 msat + val chain = nodes("D").nodeParams.chainHash + val offer = Offer(Some(amount), "test offer", recipientKey.publicKey, nodes("D").nodeParams.features.invoiceFeatures(), chain) + val invoiceRequest = InvoiceRequest(offer, amount, 1, nodes("A").nodeParams.features.invoiceFeatures(), payerKey, chain) + + sender.send(nodes("D").paymentHandler, ReceiveOfferPayment(recipientKey, offer, invoiceRequest)) + val invoice = sender.expectMsgType[Bolt12Invoice] + + // then we make the actual payment + sender.send(nodes("A").paymentInitiator, SendPaymentToNode(amount, invoice, routeParams = integrationTestRouteParams, maxAttempts = 1)) + val paymentId = sender.expectMsgType[UUID] + val ps = sender.expectMsgType[PaymentSent] + assert(ps.id == paymentId) + assert(Crypto.sha256(ps.paymentPreimage) == invoice.paymentHash) + } + + test("send an HTLC D->D") { + val (sender, eventListener) = (TestProbe(), TestProbe()) + nodes("D").system.eventStream.subscribe(eventListener.ref, classOf[PaymentMetadataReceived]) + + val recipientKey = randomKey() + val payerKey = randomKey() + + // first we retrieve an invoice from D + val amount = 4200000 msat + val chain = nodes("D").nodeParams.chainHash + val offer = Offer(Some(amount), "test offer", recipientKey.publicKey, nodes("D").nodeParams.features.invoiceFeatures(), chain) + val invoiceRequest = InvoiceRequest(offer, amount, 1, nodes("D").nodeParams.features.invoiceFeatures(), payerKey, chain) + + sender.send(nodes("D").paymentHandler, ReceiveOfferPayment(recipientKey, offer, invoiceRequest)) + val invoice = sender.expectMsgType[Bolt12Invoice] + + // then we make the actual payment + sender.send(nodes("D").paymentInitiator, SendPaymentToNode(amount, invoice, routeParams = integrationTestRouteParams, maxAttempts = 1)) + val paymentId = sender.expectMsgType[UUID] + val pf = sender.expectMsgType[PaymentFailed] + assert(pf.id == paymentId) + assert(pf.failures.head.asInstanceOf[LocalFailure].t == CannotRouteToSelf) + } + + /** Handy way to check what the channel balances are before adding new tests. */ + def debugChannelBalances(): Unit = { + val sender = TestProbe() + sender.send(nodes("B").relayer, Relayer.GetOutgoingChannels()) + sender.send(nodes("C").relayer, Relayer.GetOutgoingChannels()) + + logger.info(s"A -> ${nodes("A").nodeParams.nodeId}") + logger.info(s"B -> ${nodes("B").nodeParams.nodeId}") + logger.info(s"C -> ${nodes("C").nodeParams.nodeId}") + logger.info(s"D -> ${nodes("D").nodeParams.nodeId}") + logger.info(s"E -> ${nodes("E").nodeParams.nodeId}") + logger.info(s"F -> ${nodes("F").nodeParams.nodeId}") + logger.info(s"G -> ${nodes("G").nodeParams.nodeId}") + + val channels1 = sender.expectMsgType[Relayer.OutgoingChannels] + val channels2 = sender.expectMsgType[Relayer.OutgoingChannels] + + logger.info(channels1.channels.map(_.toChannelBalance)) + logger.info(channels2.channels.map(_.toChannelBalance)) + } + +} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala index 4c15f8643e..c26872b678 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala @@ -681,17 +681,6 @@ class PaymentIntegrationSpec extends IntegrationSpec { }, max = 120 seconds, interval = 1 second) } - test("blinded payment") { - val sender = TestProbe() - val amount = 1000_000 msat - val offer = Offer(Some(amount), "test offer", nodes("A").nodeParams.nodeId, Features.empty, nodes("A").nodeParams.chainHash) - val payerKey = randomKey() - val invoiceRequest = InvoiceRequest(offer, amount, 1, Features.empty, payerKey, nodes("A").nodeParams.chainHash) - sender.send(nodes("A").paymentHandler, ReceiveOfferPayment(nodes("A").nodeParams.privateKey, offer, invoiceRequest)) - val invoice = sender.expectMsgType[Invoice] - - } - /** Handy way to check what the channel balances are before adding new tests. */ def debugChannelBalances(): Unit = { val sender = TestProbe() From 73d5e252494cdad38b226c19d9fee56912b9c2e8 Mon Sep 17 00:00:00 2001 From: Thomas HUET Date: Thu, 29 Sep 2022 15:11:45 +0200 Subject: [PATCH 5/8] Beautify --- .../main/scala/fr/acinq/eclair/Eclair.scala | 4 +- .../acinq/eclair/json/JsonSerializers.scala | 2 +- .../acinq/eclair/payment/PaymentPacket.scala | 2 +- .../fr/acinq/eclair/payment/Recipient.scala | 32 ++++++++-- .../payment/receive/MultiPartHandler.scala | 2 +- .../eclair/payment/relay/NodeRelay.scala | 2 +- .../send/MultiPartPaymentLifecycle.scala | 21 +++---- .../payment/send/PaymentInitiator.scala | 61 ++++++++----------- .../payment/send/PaymentLifecycle.scala | 20 +++--- .../eclair/router/RouteCalculation.scala | 18 ++---- .../scala/fr/acinq/eclair/router/Router.scala | 12 ++-- .../eclair/wire/protocol/PaymentOnion.scala | 2 +- .../BlindPaymentIntegrationSpec.scala | 2 + .../payment/relay/NodeRelayerSpec.scala | 2 +- 14 files changed, 91 insertions(+), 91 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala index 293913af24..207aac96bd 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -324,9 +324,9 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { override def sendToRoute(amount: MilliSatoshi, recipientAmount_opt: Option[MilliSatoshi], externalId_opt: Option[String], parentId_opt: Option[UUID], invoice: Bolt11Invoice, route: PredefinedRoute, trampolineSecret_opt: Option[ByteVector32], trampolineFees_opt: Option[MilliSatoshi], trampolineExpiryDelta_opt: Option[CltvExpiryDelta], trampolineNodes_opt: Seq[PublicKey])(implicit timeout: Timeout): Future[SendPaymentToRouteResponse] = { val recipientAmount = recipientAmount_opt.getOrElse(invoice.amount_opt.getOrElse(amount)) val sendPayment = - if(trampolineNodes_opt.nonEmpty){ + if (trampolineNodes_opt.nonEmpty) { SendTrampolinePaymentToRoute(amount, recipientAmount, invoice, route, externalId_opt, parentId_opt, trampolineSecret_opt, trampolineFees_opt.getOrElse(0 msat), trampolineExpiryDelta_opt.getOrElse(CltvExpiryDelta(0)), trampolineNodes_opt) - }else{ + } else { SendPaymentToRoute(amount, recipientAmount, invoice, route, externalId_opt, parentId_opt) } if (invoice.isExpired()) { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala index 27b47cbba0..58b7f0e55e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala @@ -316,7 +316,7 @@ private case class PaymentFailureSummaryJson(amount: MilliSatoshi, route: Seq[Pu private case class PaymentFailedSummaryJson(paymentHash: ByteVector32, destination: PublicKey, totalAmount: MilliSatoshi, pathFindingExperiment: String, failures: Seq[PaymentFailureSummaryJson]) object PaymentFailedSummarySerializer extends ConvertClassSerializer[PaymentFailedSummary](p => PaymentFailedSummaryJson( p.cfg.paymentHash, - p.cfg.recipientNodeId, + p.cfg.recipientNodeIds.head, p.cfg.recipientAmount, p.pathFindingExperiment, p.paymentFailed.failures.map(f => { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala index 335ed1b620..3591347c1d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala @@ -380,7 +380,7 @@ object OutgoingPaymentPacket { IntermediatePayload.ChannelRelay.Blinded.validate(TlvStream(EncryptedRecipientData(ByteVector.empty)), encryptedDataTlvs, nextBlindingKey) match { case Left(invalidTlv) => return Failure(RouteBlindingEncryptedDataCodecs.CannotDecodeData(invalidTlv.failureMessage.message)) case Right(payload) => - // We assume that fees were checked in the router. + // We assume that fees and CLTV were checked in the router. val amountWithFees = recipient.amountToSend(amount) val remainingFee = amountWithFees - payload.amountToForward(amountWithFees) val tailPaymentInfo = recipient.paymentInfo.copy(feeBase = remainingFee, feeProportionalMillionths = 0, cltvExpiryDelta = recipient.paymentInfo.cltvExpiryDelta - payload.cltvExpiryDelta) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Recipient.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Recipient.scala index 51a4203ba2..bc3308533c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Recipient.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Recipient.scala @@ -26,25 +26,45 @@ import fr.acinq.eclair.{CltvExpiry, Features, InvoiceFeature, MilliSatoshi, rand import scodec.bits.ByteVector sealed trait Recipient { + /** Id of the final receiving node. */ def nodeId: PublicKey + + /** Id of the node to compute the route to. */ def introductionNodeId: PublicKey + + /** All node ids of the route. The first one is `nodeId`. */ def nodeIds: Seq[PublicKey] def features: Features[InvoiceFeature] + /** Computes the amount to send to the introduction node taking into account potential fees for the blinded route. + * + * @param amount amount to send to the recipient + * @return amount to send to the introduction node + */ def amountToSend(amount: MilliSatoshi): MilliSatoshi + /** Additional TLVs to add to the final payload (for keysend and trampoline). */ def additionalTlvs: Seq[OnionPaymentPayloadTlv] + /** Additional, user-supplied TLVs to add to the final payload. */ def userCustomTlvs: Seq[GenericTlv] def withCustomTlvs(customTlvs: Seq[GenericTlv]): Recipient + /** Builds the HTLC payloads to send to this recipient. + * + * @param amount amount to send on this route + * @param totalAmount total amount to send to the recipient + * @param expiry CLTV expiry for this route + * @return amount to send to the introduction node, CLTV expiry for the introduction node, HTLC payloads + */ def buildFinalPayloads(amount: MilliSatoshi, totalAmount: MilliSatoshi, expiry: CltvExpiry): (MilliSatoshi, CltvExpiry, Seq[PerHopPayload]) } +/** A classic node id recipient. */ case class ClearRecipient(nodeId: PublicKey, paymentSecret: ByteVector32, paymentMetadata_opt: Option[ByteVector], @@ -60,8 +80,8 @@ case class ClearRecipient(nodeId: PublicKey, override def withCustomTlvs(customTlvs: Seq[GenericTlv]): Recipient = copy(userCustomTlvs = customTlvs) override def buildFinalPayloads(amount: MilliSatoshi, - totalAmount: MilliSatoshi, - expiry: CltvExpiry): (MilliSatoshi, CltvExpiry, Seq[PerHopPayload]) = + totalAmount: MilliSatoshi, + expiry: CltvExpiry): (MilliSatoshi, CltvExpiry, Seq[PerHopPayload]) = (amount, expiry, Seq(FinalPayload.Standard.createMultiPartPayload(amount, totalAmount, expiry, paymentSecret, paymentMetadata_opt, additionalTlvs, userCustomTlvs))) } @@ -75,6 +95,7 @@ object TrampolineRecipient { ClearRecipient(trampolineNodeId, trampolineSecret, paymentMetadata_opt, additionalTlvs = Seq(OnionPaymentPayloadTlv.TrampolineOnion(trampolineOnion))) } +/** A recipient hidden behind a blinded route. */ case class BlindRecipient(route: RouteBlinding.BlindedRoute, paymentInfo: PaymentInfo, capacity_opt: Option[MilliSatoshi], @@ -93,17 +114,16 @@ case class BlindRecipient(route: RouteBlinding.BlindedRoute, override def withCustomTlvs(customTlvs: Seq[GenericTlv]): Recipient = copy(userCustomTlvs = customTlvs) override def buildFinalPayloads(amount: MilliSatoshi, - totalAmount: MilliSatoshi, - expiry: CltvExpiry): (MilliSatoshi, CltvExpiry, Seq[PerHopPayload]) = { + totalAmount: MilliSatoshi, + expiry: CltvExpiry): (MilliSatoshi, CltvExpiry, Seq[PerHopPayload]) = { val blindedPayloads = if (route.encryptedPayloads.length > 1) { + val introductionPayload = IntermediatePayload.ChannelRelay.Blinded.create(route.encryptedPayloads.head, Some(route.blindingKey)) val middlePayloads = route.encryptedPayloads.drop(1).dropRight(1).map(IntermediatePayload.ChannelRelay.Blinded.create(_, None)) val finalPayload = FinalPayload.Blinded.create(amount, totalAmount, expiry, route.encryptedPayloads.last, None, additionalTlvs, userCustomTlvs) - val introductionPayload = IntermediatePayload.ChannelRelay.Blinded.create(route.encryptedPayloads.head, Some(route.blindingKey)) introductionPayload +: middlePayloads :+ finalPayload } else { Seq(FinalPayload.Blinded.create(amount, totalAmount, expiry, route.encryptedPayloads.last, Some(route.blindingKey), additionalTlvs, userCustomTlvs)) } (amount + paymentInfo.fee(amount), expiry + paymentInfo.cltvExpiryDelta, blindedPayloads) - } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala index 93824d9cf2..e6914e5d20 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala @@ -34,7 +34,7 @@ import fr.acinq.eclair.payment._ import fr.acinq.eclair.wire.protocol.OfferTypes.{InvoiceRequest, Offer} import fr.acinq.eclair.wire.protocol.PaymentOnion.FinalPayload import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, FeatureSupport, Features, InvoiceFeature, Logs, MilliSatoshi, MilliSatoshiLong, NodeParams, ShortChannelId, TimestampMilli, randomBytes, randomBytes32, randomKey} +import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, FeatureSupport, Features, InvoiceFeature, Logs, MilliSatoshi, MilliSatoshiLong, NodeParams, ShortChannelId, TimestampMilli, randomBytes32, randomKey} import scodec.bits.HexStringSyntax import scala.util.{Failure, Success, Try} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelay.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelay.scala index a7f1528908..3256e52e05 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelay.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelay.scala @@ -40,7 +40,7 @@ import fr.acinq.eclair.router.Router.RouteParams import fr.acinq.eclair.router.{BalanceTooLow, RouteNotFound} import fr.acinq.eclair.wire.protocol.PaymentOnion.IntermediatePayload import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{BlockHeight, CltvExpiry, Features, Logs, MilliSatoshi, NodeParams, UInt64, nodeFee, randomBytes32} +import fr.acinq.eclair.{BlockHeight, CltvExpiry, Features, Logs, MilliSatoshi, NodeParams, UInt64, nodeFee} import java.util.UUID import scala.collection.immutable.Queue diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala index ffcd7d36f3..d0f9254a05 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala @@ -19,7 +19,6 @@ package fr.acinq.eclair.payment.send import akka.actor.{ActorRef, FSM, Props, Status} import akka.event.Logging.MDC import fr.acinq.bitcoin.scalacompat.ByteVector32 -import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.channel.{HtlcOverriddenByLocalCommit, HtlcsTimedoutDownstream, HtlcsWillTimeoutUpstream} import fr.acinq.eclair.db.{OutgoingPayment, OutgoingPaymentStatus, PaymentType} import fr.acinq.eclair.payment.Invoice.ExtraEdge @@ -30,9 +29,7 @@ import fr.acinq.eclair.payment._ import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentConfig import fr.acinq.eclair.payment.send.PaymentLifecycle.SendPaymentToRoute import fr.acinq.eclair.router.Router._ -import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.{CltvExpiry, FSMDiagnosticActorLogging, Logs, MilliSatoshi, MilliSatoshiLong, NodeParams, TimestampMilli} -import scodec.bits.ByteVector import java.util.UUID import java.util.concurrent.TimeUnit @@ -105,7 +102,7 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, if (cfg.storeInDb && d.pending.isEmpty && d.failures.isEmpty) { // In cases where we fail early (router error during the first attempt), the DB won't have an entry for that // payment, which may be confusing for users. - val dummyPayment = OutgoingPayment(id, cfg.parentId, cfg.externalId, paymentHash, PaymentType.Standard, cfg.recipientAmount, cfg.recipientAmount, cfg.recipientNodeId, TimestampMilli.now(), cfg.invoice, OutgoingPaymentStatus.Pending) + val dummyPayment = OutgoingPayment(id, cfg.parentId, cfg.externalId, paymentHash, PaymentType.Standard, cfg.recipientAmount, cfg.recipientAmount, cfg.recipientNodeIds.head, TimestampMilli.now(), cfg.invoice, OutgoingPaymentStatus.Pending) nodeParams.db.payments.addOutgoingPayment(dummyPayment) nodeParams.db.payments.updateOutgoingPayment(PaymentFailed(id, paymentHash, failure :: Nil)) } @@ -264,7 +261,7 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, } paymentSent.feesPaid + localFees } - context.system.eventStream.publish(PathFindingExperimentMetrics(cfg.paymentHash, cfg.recipientAmount, fees, status, duration, now, isMultiPart = true, request.routeParams.experimentName, cfg.recipientNodeId, request.extraEdges)) + context.system.eventStream.publish(PathFindingExperimentMetrics(cfg.paymentHash, cfg.recipientAmount, fees, status, duration, now, isMultiPart = true, request.routeParams.experimentName, cfg.recipientNodeIds.head, request.extraEdges)) } Metrics.SentPaymentDuration .withTag(Tags.MultiPart, Tags.MultiPartType.Parent) @@ -287,7 +284,7 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, parentPaymentId_opt = Some(cfg.parentId), paymentId_opt = Some(id), paymentHash_opt = Some(paymentHash), - remoteNodeId_opt = Some(cfg.recipientNodeId), + remoteNodeId_opt = Some(cfg.recipientNodeIds.head), nodeAlias_opt = Some(nodeParams.alias)) } @@ -303,12 +300,12 @@ object MultiPartPaymentLifecycle { * Send a payment to a given node. The payment may be split into multiple child payments, for which a path-finding * algorithm will run to find suitable payment routes. * - * @param recipients list of recipients to send the payment to. - * @param totalAmount total amount to send to the target node. - * @param targetExpiry expiry at the target node (CLTV for the target node's received HTLCs). - * @param maxAttempts maximum number of retries. - * @param extraEdges routing hints (usually from a Bolt 11 invoice). - * @param routeParams parameters to fine-tune the routing algorithm. + * @param recipients list of recipients to send the payment to. + * @param totalAmount total amount to send to the target node. + * @param targetExpiry expiry at the target node (CLTV for the target node's received HTLCs). + * @param maxAttempts maximum number of retries. + * @param extraEdges routing hints (usually from a Bolt 11 invoice). + * @param routeParams parameters to fine-tune the routing algorithm. */ case class SendMultiPartPayment(replyTo: ActorRef, recipients: Seq[Recipient], diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala index b2ab41a8ec..65d3eea7d7 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala @@ -68,7 +68,7 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn val paymentId = UUID.randomUUID() sender() ! paymentId val recipients = Seq(KeySendRecipient(r.recipientNodeId, r.paymentPreimage, r.userCustomTlvs)) - val paymentCfg = SendPaymentConfig(paymentId, paymentId, r.externalId, r.paymentHash, r.recipientAmount, recipients.flatMap(_.nodeIds), Upstream.Local(paymentId), None, storeInDb = true, publishEvent = true, recordPathFindingMetrics = r.recordPathFindingMetrics, Nil) + val paymentCfg = SendPaymentConfig(paymentId, paymentId, r.externalId, r.paymentHash, r.recipientAmount, Seq(r.recipientNodeId), Upstream.Local(paymentId), None, storeInDb = true, publishEvent = true, recordPathFindingMetrics = r.recordPathFindingMetrics, Nil) val finalExpiry = Channel.MIN_CLTV_EXPIRY_DELTA.toCltvExpiry(nodeParams.currentBlockHeight + 1) val fsm = outgoingPaymentFactory.spawnOutgoingPayment(context, paymentCfg) fsm ! PaymentLifecycle.SendPaymentToNode(self, recipients, r.recipientAmount, r.recipientAmount, finalExpiry, r.maxAttempts, routeParams = r.routeParams) @@ -97,7 +97,7 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn val paymentId = UUID.randomUUID() val parentPaymentId = r.parentId.getOrElse(UUID.randomUUID()) val additionalHops = r.trampolineNodes.sliding(2).map(hop => NodeHop(hop.head, hop(1), CltvExpiryDelta(0), 0 msat)).toSeq - val paymentCfg = SendPaymentConfig(paymentId, parentPaymentId, r.externalId, r.paymentHash, r.recipientAmount, r.recipients.flatMap(_.nodeIds), Upstream.Local(paymentId), Some(r.invoice), storeInDb = true, publishEvent = true, recordPathFindingMetrics = false, additionalHops) + val paymentCfg = SendPaymentConfig(paymentId, parentPaymentId, r.externalId, r.paymentHash, r.recipientAmount, Seq(r.invoice.nodeId), Upstream.Local(paymentId), Some(r.invoice), storeInDb = true, publishEvent = true, recordPathFindingMetrics = false, additionalHops) r.trampolineNodes match { case trampoline :: recipient :: Nil => log.info(s"sending trampoline payment to $recipient with trampoline=$trampoline, trampoline fees=${r.trampolineFees}, expiry delta=${r.trampolineExpiryDelta}") @@ -211,12 +211,12 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn trampolinePacket_opt.map { case (trampolineAmount, trampolineExpiry, trampolineOnion) => (trampolineAmount, trampolineExpiry, trampolineOnion.packet) } - case _ => Failure(new Exception("Trampoline to legacy is only supported for Bolt11 invoices.")) + case _ => Failure(new Exception("Trampoline is only supported for Bolt11 invoices.")) } } private def sendTrampolinePayment(paymentId: UUID, r: SendTrampolinePayment, trampolineFees: MilliSatoshi, trampolineExpiryDelta: CltvExpiryDelta): Try[Unit] = { - val paymentCfg = SendPaymentConfig(paymentId, paymentId, None, r.paymentHash, r.recipientAmount, r.recipients.flatMap(_.nodeIds), Upstream.Local(paymentId), Some(r.invoice), storeInDb = true, publishEvent = false, recordPathFindingMetrics = true, Seq(NodeHop(r.trampolineNodeId, r.invoice.nodeId, trampolineExpiryDelta, trampolineFees))) + val paymentCfg = SendPaymentConfig(paymentId, paymentId, None, r.paymentHash, r.recipientAmount, Seq(r.invoice.nodeId), Upstream.Local(paymentId), Some(r.invoice), storeInDb = true, publishEvent = false, recordPathFindingMetrics = true, Seq(NodeHop(r.trampolineNodeId, r.invoice.nodeId, trampolineExpiryDelta, trampolineFees))) buildTrampolinePayment(r, r.trampolineNodeId, trampolineFees, trampolineExpiryDelta).map { case (trampolineAmount, trampolineExpiry, trampolineOnion) => val fsm = outgoingPaymentFactory.spawnOutgoingMultiPartPayment(context, paymentCfg) @@ -340,23 +340,18 @@ object PaymentInitiator { /** * The sender can skip the routing algorithm by specifying the route to use. - * When combining with MPP and Trampoline, extra-care must be taken to make sure payments are correctly grouped: only - * amount, route and trampolineNodes should be changing. + * When combining with MPP, extra-care must be taken to make sure payments are correctly grouped: only amount and + * route should be changing. * * Example 1: MPP containing two HTLCs for a 600 msat invoice: - * SendPaymentToRouteRequest(200 msat, 600 msat, None, parentId, invoice, CltvExpiryDelta(9), Seq(alice, bob, dave), None, 0 msat, CltvExpiryDelta(0), Nil) - * SendPaymentToRouteRequest(400 msat, 600 msat, None, parentId, invoice, CltvExpiryDelta(9), Seq(alice, carol, dave), None, 0 msat, CltvExpiryDelta(0), Nil) - * - * Example 2: Trampoline with MPP for a 600 msat invoice and 100 msat trampoline fees: - * SendPaymentToRouteRequest(250 msat, 600 msat, None, parentId, invoice, CltvExpiryDelta(9), Seq(alice, bob, dave), secret, 100 msat, CltvExpiryDelta(144), Seq(dave, peter)) - * SendPaymentToRouteRequest(450 msat, 600 msat, None, parentId, invoice, CltvExpiryDelta(9), Seq(alice, carol, dave), secret, 100 msat, CltvExpiryDelta(144), Seq(dave, peter)) + * SendPaymentToRoute(200 msat, 600 msat, invoice, Seq(alice, bob, dave), None, Some(parentId)) + * SendPaymentToRoute(400 msat, 600 msat, invoice, Seq(alice, carol, dave), None, Some(parentId)) * - * @param amount amount that should be received by the last node in the route (should take trampoline - * fees into account). - * @param recipientAmount amount that should be received by the final recipient (usually from a Bolt 11 invoice). + * @param amount amount to send through this route + * @param recipientAmount amount that should be received by the final recipient. * This amount may be split between multiple requests if using MPP. - * @param invoice Bolt 11 invoice. - * @param route route to use to reach either the final recipient or the first trampoline node. + * @param invoice invoice. + * @param route route to use to reach the recipient. * @param externalId (optional) externally-controlled identifier (to reconcile between application DB and eclair DB). * @param parentId id of the whole payment. When manually sending a multi-part payment, you need to make * sure all partial payments use the same parentId. If not provided, a random parentId will @@ -374,13 +369,9 @@ object PaymentInitiator { * When combining with MPP and Trampoline, extra-care must be taken to make sure payments are correctly grouped: only * amount, route and trampolineNodes should be changing. * - * Example 1: MPP containing two HTLCs for a 600 msat invoice: - * SendPaymentToRouteRequest(200 msat, 600 msat, None, parentId, invoice, CltvExpiryDelta(9), Seq(alice, bob, dave), None, 0 msat, CltvExpiryDelta(0), Nil) - * SendPaymentToRouteRequest(400 msat, 600 msat, None, parentId, invoice, CltvExpiryDelta(9), Seq(alice, carol, dave), None, 0 msat, CltvExpiryDelta(0), Nil) - * * Example 2: Trampoline with MPP for a 600 msat invoice and 100 msat trampoline fees: - * SendPaymentToRouteRequest(250 msat, 600 msat, None, parentId, invoice, CltvExpiryDelta(9), Seq(alice, bob, dave), secret, 100 msat, CltvExpiryDelta(144), Seq(dave, peter)) - * SendPaymentToRouteRequest(450 msat, 600 msat, None, parentId, invoice, CltvExpiryDelta(9), Seq(alice, carol, dave), secret, 100 msat, CltvExpiryDelta(144), Seq(dave, peter)) + * SendPaymentToRouteRequest(250 msat, 600 msat, invoice, Seq(alice, bob, dave), None, Some(parentId), secret, 100 msat, CltvExpiryDelta(144), Seq(dave, peter)) + * SendPaymentToRouteRequest(450 msat, 600 msat, invoice, Seq(alice, carol, dave), None, Some(parentId), secret, 100 msat, CltvExpiryDelta(144), Seq(dave, peter)) * * @param amount amount that should be received by the last node in the route (should take trampoline * fees into account). @@ -392,15 +383,15 @@ object PaymentInitiator { * @param parentId id of the whole payment. When manually sending a multi-part payment, you need to make * sure all partial payments use the same parentId. If not provided, a random parentId will * be generated that can be used for the remaining partial payments. - * @param trampolineSecret if trampoline is used, this is a secret to protect the payment to the first trampoline - * node against probing. When manually sending a multi-part payment, you need to make sure - * all partial payments use the same trampolineSecret. - * @param trampolineFees if trampoline is used, fees for the first trampoline node. This value must be the same - * for all partial payments in the set. - * @param trampolineExpiryDelta if trampoline is used, expiry delta for the first trampoline node. This value must be - * the same for all partial payments in the set. - * @param trampolineNodes if trampoline is used, list of trampoline nodes to use (we currently support only a - * single trampoline node). + * @param trampolineSecret this is a secret to protect the payment to the first trampoline node against probing. + * When manually sending a multi-part payment, you need to make sure all partial payments + * use the same trampolineSecret. + * @param trampolineFees fees for the first trampoline node. This value must be the same for all partial + * payments in the set. + * @param trampolineExpiryDelta expiry delta for the first trampoline node. This value must be the same for all + * partial payments in the set. + * @param trampolineNodes list of trampoline nodes to use (we currently support only a single trampoline node). + * The last one must be the recipient. */ case class SendTrampolinePaymentToRoute(amount: MilliSatoshi, recipientAmount: MilliSatoshi, @@ -432,7 +423,7 @@ object PaymentInitiator { * @param externalId externally-controlled identifier (to reconcile between application DB and eclair DB). * @param paymentHash payment hash. * @param recipientAmount amount that should be received by the final recipient (usually from a Bolt 11 invoice). - * @param recipientNodeId id of the final recipient. + * @param recipientNodeIds ids of the final recipients. Used to check if an error was returned by a recipient. * @param upstream information about the payment origin (to link upstream to downstream when relaying a payment). * @param invoice Bolt 11 invoice. * @param storeInDb whether to store data in the payments DB (e.g. when we're relaying a trampoline payment, we @@ -455,11 +446,9 @@ object PaymentInitiator { publishEvent: Boolean, recordPathFindingMetrics: Boolean, additionalHops: Seq[NodeHop]) { - val recipientNodeId: PublicKey = recipientNodeIds.head - def fullRoute(route: Route): Seq[Hop] = route.clearHops ++ additionalHops - def createPaymentSent(preimage: ByteVector32, parts: Seq[PaymentSent.PartialPayment]) = PaymentSent(parentId, paymentHash, preimage, recipientAmount, recipientNodeId, parts) + def createPaymentSent(preimage: ByteVector32, parts: Seq[PaymentSent.PartialPayment]) = PaymentSent(parentId, paymentHash, preimage, recipientAmount, recipientNodeIds.head, parts) def paymentContext: PaymentContext = PaymentContext(id, parentId, paymentHash) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala index 0116cc1035..dc18ed2129 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala @@ -61,15 +61,15 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A route => self ! RouteResponse(route :: Nil) ) if (cfg.storeInDb) { - paymentsDb.addOutgoingPayment(OutgoingPayment(id, cfg.parentId, cfg.externalId, paymentHash, PaymentType.Standard, c.amount, cfg.recipientAmount, cfg.recipientNodeId, TimestampMilli.now(), cfg.invoice, OutgoingPaymentStatus.Pending)) + paymentsDb.addOutgoingPayment(OutgoingPayment(id, cfg.parentId, cfg.externalId, paymentHash, PaymentType.Standard, c.amount, cfg.recipientAmount, cfg.recipientNodeIds.head, TimestampMilli.now(), cfg.invoice, OutgoingPaymentStatus.Pending)) } goto(WAITING_FOR_ROUTE) using WaitingForRoute(c, Nil, Ignore.empty) case Event(c: SendPaymentToNode, WaitingForRequest) => - log.debug("sending {} to {}", c.amount, c.targetRecipients.head.nodeId) + log.debug("sending {} to {}", c.amount, c.targetRecipients.map(_.nodeId).mkString(",")) router ! RouteRequest(nodeParams.nodeId, c.targetRecipients, c.amount, c.maxFee, c.extraEdges, routeParams = c.routeParams, paymentContext = Some(cfg.paymentContext)) if (cfg.storeInDb) { - paymentsDb.addOutgoingPayment(OutgoingPayment(id, cfg.parentId, cfg.externalId, paymentHash, PaymentType.Standard, c.amount, cfg.recipientAmount, cfg.recipientNodeId, TimestampMilli.now(), cfg.invoice, OutgoingPaymentStatus.Pending)) + paymentsDb.addOutgoingPayment(OutgoingPayment(id, cfg.parentId, cfg.externalId, paymentHash, PaymentType.Standard, c.amount, cfg.recipientAmount, cfg.recipientNodeIds.head, TimestampMilli.now(), cfg.invoice, OutgoingPaymentStatus.Pending)) } goto(WAITING_FOR_ROUTE) using WaitingForRoute(c, Nil, Ignore.empty) } @@ -362,7 +362,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A } request match { case request: SendPaymentToNode => - context.system.eventStream.publish(PathFindingExperimentMetrics(cfg.paymentHash, request.amount, fees, status, duration, now, isMultiPart = false, request.routeParams.experimentName, cfg.recipientNodeId, request.extraEdges)) + context.system.eventStream.publish(PathFindingExperimentMetrics(cfg.paymentHash, request.amount, fees, status, duration, now, isMultiPart = false, request.routeParams.experimentName, cfg.recipientNodeIds.head, request.extraEdges)) case _: SendPaymentToRoute => () } } @@ -379,7 +379,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A parentPaymentId_opt = Some(cfg.parentId), paymentId_opt = Some(id), paymentHash_opt = Some(paymentHash), - remoteNodeId_opt = Some(cfg.recipientNodeId), + remoteNodeId_opt = Some(cfg.recipientNodeIds.head), nodeAlias_opt = Some(nodeParams.alias)) } @@ -406,7 +406,8 @@ object PaymentLifecycle { * Send a payment to a given route. * * @param route payment route to use. - * @param amount amount to send to the target node. + * @param amount amount to send through this route. + * @param totalAmount amount to send to the recipient. * @param targetExpiry expiry at the target node (CLTV for the target node's received HTLCs). */ case class SendPaymentToRoute(replyTo: ActorRef, @@ -433,10 +434,11 @@ object PaymentLifecycle { * Send a payment to a given node. A path-finding algorithm will run to find a suitable payment route. * * @param targetRecipients target recipients to send the payment to. - * @param amount amount to send to the target node. + * @param amount amount to send through this route. + * @param totalAmount amount to send to the recipient. * @param targetExpiry expiry at the target node (CLTV for the target node's received HTLCs). * @param maxAttempts maximum number of retries. - * @param extraEdges routing hints (usually from a Bolt 11 invoice). + * @param extraEdges routing hints (usually from a Bolt 11 invoice). * @param routeParams parameters to fine-tune the routing algorithm. */ case class SendPaymentToNode(replyTo: ActorRef, @@ -447,7 +449,7 @@ object PaymentLifecycle { maxAttempts: Int, extraEdges: Seq[ExtraEdge] = Nil, routeParams: RouteParams) extends SendPayment { - require(amount > 0.msat, s"total amount must be > 0") + require(amount > 0.msat, s"amount must be > 0") val maxFee: MilliSatoshi = routeParams.getMaxFee(amount) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala index 825eaca4a3..1a77d4da74 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala @@ -104,12 +104,9 @@ object RouteCalculation { val params = r.routeParams val routesToFind = if (params.randomize) DEFAULT_ROUTES_COUNT else 1 - log.info(s"finding routes ${r.source}->${r.targets.head.nodeId} with assistedChannels={} ignoreNodes={} ignoreChannels={} excludedChannels={}", extraEdges.map(_.desc.shortChannelId).mkString(","), r.ignore.nodes.map(_.value).mkString(","), r.ignore.channels.mkString(","), d.excludedChannels.mkString(",")) + log.info(s"finding routes ${r.source}->${r.targets.map(_.nodeId).mkString(",")} with assistedChannels={} ignoreNodes={} ignoreChannels={} excludedChannels={}", extraEdges.map(_.desc.shortChannelId).mkString(","), r.ignore.nodes.map(_.value).mkString(","), r.ignore.channels.mkString(","), d.excludedChannels.mkString(",")) log.info("finding routes with params={}, multiPart={}", params, r.allowMultiPart) - val clearTargetNodeIs = r.targets.map { - case ClearRecipient(nodeId, _, _, _, _, _) => nodeId - case BlindRecipient(route, _, _, _, _) => route.introductionNodeId - }.distinct + val clearTargetNodeIs = r.targets.map(_.introductionNodeId).distinct val directChannels = clearTargetNodeIs.flatMap(d.graphWithBalances.graph.getEdgesBetween(r.source, _)) log.info("local channels to recipient: {}", directChannels.map(e => s"${e.desc.shortChannelId} (${e.balance_opt}/${e.capacity})").mkString(", ")) val tags = TagSet.Empty.withTag(Tags.MultiPart, r.allowMultiPart).withTag(Tags.Amount, Tags.amountBucket(r.amount)) @@ -223,7 +220,6 @@ object RouteCalculation { val targetNodes = targets.map(_.nodeId) val blindedEdges = targets.collect { case BlindRecipient(route, paymentInfo, capacity_opt, _, _) => GraphEdge(route, paymentInfo, capacity_opt) }.toSet - val foundRoutes: Seq[Graph.WeightedPath] = Graph.yenKshortestPaths(g, localNodeId, targetNodes, amount, ignoredEdges, ignoredVertices, extraEdges ++ blindedEdges, numRoutes, routeParams.heuristics, currentBlockHeight, boundaries, routeParams.includeLocalChannelCost) if (foundRoutes.nonEmpty) { val (directRoutes, indirectRoutes) = foundRoutes.partition(_.path.length == 1) @@ -297,10 +293,7 @@ object RouteCalculation { // We use Yen's k-shortest paths to find many paths for chunks of the total amount. // When the recipient is a direct peer, we have complete visibility on our local channels so we can use more accurate MPP parameters. val routeParams1 = { - val clearTargetNodeIs = targets.map { - case ClearRecipient(nodeId, _, _, _, _, _) => nodeId - case BlindRecipient(route, _, _, _, _) => route.introductionNodeId - }.distinct + val clearTargetNodeIs = targets.map(_.introductionNodeId).distinct val directChannelsCount = clearTargetNodeIs.map(g.getEdgesBetween(localNodeId, _).length).sum // If we have direct channels to the target, we can use them all. // We also count empty channels, which allows replacing them with a non-direct route (multiple hops). @@ -394,10 +387,7 @@ object RouteCalculation { * the target node it means we'd like to reach it via direct channels as much as possible. */ private def isNeighborBalanceTooLow(g: DirectedGraph, r: RouteRequest): Boolean = { - val clearTargetNodeIs = r.targets.map { - case ClearRecipient(nodeId, _, _, _, _, _) => nodeId - case BlindRecipient(route, _, _, _, _) => route.introductionNodeId - }.distinct + val clearTargetNodeIs = r.targets.map(_.introductionNodeId).distinct val neighborEdges = clearTargetNodeIs.flatMap(g.getEdgesBetween(r.source, _)) neighborEdges.nonEmpty && neighborEdges.map(e => e.balance_opt.getOrElse(e.capacity.toMilliSatoshi)).sum < r.amount } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala index f2c9e98bf7..489a4a587c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala @@ -33,7 +33,7 @@ import fr.acinq.eclair.db.NetworkDb import fr.acinq.eclair.io.Peer.PeerRoutingMessage import fr.acinq.eclair.payment.Invoice.ExtraEdge import fr.acinq.eclair.payment.relay.Relayer -import fr.acinq.eclair.payment.{Bolt11Invoice, Invoice, Recipient} +import fr.acinq.eclair.payment.{Bolt11Invoice, ClearRecipient, Invoice, Recipient} import fr.acinq.eclair.remote.EclairInternalsSerializer.RemoteTypes import fr.acinq.eclair.router.Graph.GraphStructure.DirectedGraph import fr.acinq.eclair.router.Graph.{HeuristicsConstants, WeightRatios} @@ -522,7 +522,7 @@ object Router { } case class RouteRequest(source: PublicKey, - targets: Seq[payment.Recipient], + targets: Seq[Recipient], amount: MilliSatoshi, maxFee: MilliSatoshi, extraEdges: Seq[ExtraEdge] = Nil, @@ -544,8 +544,8 @@ object Router { case class PaymentContext(id: UUID, parentId: UUID, paymentHash: ByteVector32) /* A route is composed of zero or more hops chosen by us, optionally followed by blinded hops chosen by someone else. - * There must be a next node to relay the payment to. If there are no clear hops, it must end with a blinded route for - * which we are the introduction point and there must be a second blinded hop that is not us. + * There must be a next node to relay the payment to. If there are no clear hops, the recipient must be a blinded + * route for which we are the introduction point and there must be a second blinded hop that is not us. */ case class Route(amount: MilliSatoshi, clearHops: Seq[ChannelHop], recipient: payment.Recipient) { require(clearHops.nonEmpty || recipient.isInstanceOf[payment.BlindRecipient], "route cannot be empty") @@ -563,8 +563,8 @@ object Router { def printChannels(): String = clearHops.map(_.shortChannelId).mkString("->") def stopAt(nodeId: PublicKey): Route = { - val amountAtStop = clearHops.reverse.takeWhile(_.nextNodeId != nodeId).foldLeft(amount) { case (amount1, hop) => amount1 + hop.fee(amount1) } - Route(amountAtStop, clearHops.takeWhile(_.nodeId != nodeId), recipient) + val amountAtStop = clearHops.reverse.takeWhile(_.nextNodeId != nodeId).foldLeft(recipient.amountToSend(amount)) { case (amount1, hop) => amount1 + hop.fee(amount1) } + Route(amountAtStop, clearHops.takeWhile(_.nodeId != nodeId), ClearRecipient(nodeId, randomBytes32(), None)) } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/PaymentOnion.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/PaymentOnion.scala index 30801d99f3..827ce8c567 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/PaymentOnion.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/PaymentOnion.scala @@ -18,7 +18,7 @@ package fr.acinq.eclair.wire.protocol import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey -import fr.acinq.eclair.payment.{Bolt11Invoice, ClearRecipient, Recipient} +import fr.acinq.eclair.payment.Bolt11Invoice import fr.acinq.eclair.wire.protocol.CommonCodecs._ import fr.acinq.eclair.wire.protocol.OnionRoutingCodecs.{ForbiddenTlv, InvalidTlvPayload, MissingRequiredTlv} import fr.acinq.eclair.wire.protocol.TlvCodecs._ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/BlindPaymentIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/BlindPaymentIntegrationSpec.scala index 18736e9ee9..ca9471c961 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/BlindPaymentIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/BlindPaymentIntegrationSpec.scala @@ -197,6 +197,8 @@ class BlindPaymentIntegrationSpec extends IntegrationSpec { assert(pf.failures.head.asInstanceOf[LocalFailure].t == CannotRouteToSelf) } + // TODO: Add more tests with more cases of blinded routes and with MPP. + /** Handy way to check what the channel balances are before adding new tests. */ def debugChannelBalances(): Unit = { val sender = TestProbe() diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/NodeRelayerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/NodeRelayerSpec.scala index 5350b421fc..fcb88db86b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/NodeRelayerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/NodeRelayerSpec.scala @@ -829,7 +829,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl assert(outgoingCfg.paymentHash == paymentHash) assert(outgoingCfg.invoice.isEmpty) assert(outgoingCfg.recipientAmount == outgoingAmount) - assert(outgoingCfg.recipientNodeId == outgoingNodeId) + assert(outgoingCfg.recipientNodeIds.contains(outgoingNodeId)) assert(outgoingCfg.upstream == upstream) } From 7fb2ffaf7498fd2b7fbb4bacd1b707d6a958130f Mon Sep 17 00:00:00 2001 From: Thomas HUET Date: Thu, 29 Sep 2022 17:56:32 +0200 Subject: [PATCH 6/8] Fix Yen's k shortest paths --- .../scala/fr/acinq/eclair/router/Graph.scala | 31 +++++++++------- .../eclair/router/RouteCalculation.scala | 35 +++++++++++++++---- .../fr/acinq/eclair/router/GraphSpec.scala | 8 ++--- .../eclair/router/RouteCalculationSpec.scala | 8 ++--- 4 files changed, 54 insertions(+), 28 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala index 4a3d0b0595..b6d4b6d7aa 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala @@ -101,7 +101,7 @@ object Graph { * * @param graph the graph on which will be performed the search * @param sourceNode the starting node of the path we're looking for (payer) - * @param targets the recipients + * @param targetNode the destination node of the path (recipient) * @param amount amount to send to the last node * @param ignoredEdges channels that should be avoided * @param ignoredVertices nodes that should be avoided @@ -114,7 +114,7 @@ object Graph { */ def yenKshortestPaths(graph: DirectedGraph, sourceNode: PublicKey, - targets: Seq[PublicKey], + targetNode: PublicKey, amount: MilliSatoshi, ignoredEdges: Set[ChannelDesc], ignoredVertices: Set[PublicKey], @@ -126,7 +126,7 @@ object Graph { includeLocalChannelCost: Boolean): Seq[WeightedPath] = { // find the shortest path (k = 0) val targetWeight = RichWeight(amount, 0, CltvExpiryDelta(0), 1.0, 0 msat, 0 msat, 0.0) - val shortestPath = dijkstraShortestPath(graph, sourceNode, targets, ignoredEdges, ignoredVertices, extraEdges, targetWeight, boundaries, currentBlockHeight, wr, includeLocalChannelCost) + val shortestPath = dijkstraShortestPath(graph, sourceNode, targetNode, ignoredEdges, ignoredVertices, extraEdges, targetWeight, boundaries, currentBlockHeight, wr, includeLocalChannelCost) if (shortestPath.isEmpty) { return Seq.empty // if we can't even find a single path, avoid returning a Seq(Seq.empty) } @@ -165,7 +165,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 - val spurPath = dijkstraShortestPath(graph, sourceNode, Seq(spurNode), ignoredEdges ++ alreadyExploredEdges, ignoredVertices ++ alreadyExploredVertices, extraEdges, rootPathWeight, boundaries, currentBlockHeight, wr, includeLocalChannelCost) + val spurPath = dijkstraShortestPath(graph, sourceNode, spurNode, ignoredEdges ++ alreadyExploredEdges, ignoredVertices ++ alreadyExploredVertices, extraEdges, rootPathWeight, boundaries, currentBlockHeight, wr, includeLocalChannelCost) if (spurPath.nonEmpty) { val completePath = spurPath ++ rootPathEdges val candidatePath = WeightedPath(completePath, pathWeight(sourceNode, completePath, amount, currentBlockHeight, wr, includeLocalChannelCost)) @@ -193,7 +193,7 @@ object Graph { * * @param g the graph on which will be performed the search * @param sourceNode the starting node of the path we're looking for (payer) - * @param targets the destinations of the path + * @param targetNode the destination node of the path * @param ignoredEdges channels that should be avoided * @param ignoredVertices nodes that should be avoided * @param extraEdges additional edges that can be used (e.g. private channels from invoices) @@ -205,7 +205,7 @@ object Graph { */ private def dijkstraShortestPath(g: DirectedGraph, sourceNode: PublicKey, - targets: Seq[PublicKey], + targetNode: PublicKey, ignoredEdges: Set[ChannelDesc], ignoredVertices: Set[PublicKey], extraEdges: Set[GraphEdge], @@ -216,8 +216,8 @@ object Graph { includeLocalChannelCost: Boolean): Seq[GraphEdge] = { // the graph does not contain source/destination nodes val sourceNotInGraph = !g.containsVertex(sourceNode) && !extraEdges.exists(_.desc.a == sourceNode) - val targetsNotInGraph = targets.forall(targetNode => !g.containsVertex(targetNode) && !extraEdges.exists(_.desc.b == targetNode)) - if (sourceNotInGraph || targetsNotInGraph) { + val targetNotInGraph = !g.containsVertex(targetNode) && !extraEdges.exists(_.desc.b == targetNode) + if (sourceNotInGraph || targetNotInGraph) { return Seq.empty } @@ -230,11 +230,9 @@ object Graph { val toExplore = mutable.PriorityQueue.empty[WeightedNode](NodeComparator.reverse) val visitedNodes = mutable.HashSet[PublicKey]() - for (targetNode <- targets) { - // initialize the queue and cost array with the initial weight - bestWeights.put(targetNode, initialWeight) - toExplore.enqueue(WeightedNode(targetNode, initialWeight)) - } + // initialize the queue and cost array with the initial weight + bestWeights.put(targetNode, initialWeight) + toExplore.enqueue(WeightedNode(targetNode, initialWeight)) var targetFound = false while (toExplore.nonEmpty && !targetFound) { @@ -485,6 +483,13 @@ object Graph { capacity = maxBtc.toSatoshi, balance_opt = capacity_opt.orElse(Some(maxBtc.toMilliSatoshi)) ) + + def apply(a: PublicKey, b: PublicKey): GraphEdge = GraphEdge( + desc = ChannelDesc(ShortChannelId.generateLocalAlias(), a, b), + params = ChannelRelayParams.FromPaymentInfo(PaymentInfo(0 msat, 0, CltvExpiryDelta(0), 0 msat, maxBtc.toMilliSatoshi, Features.empty)), + capacity = maxBtc.toSatoshi, + balance_opt = Some(maxBtc.toMilliSatoshi) + ) } /** A graph data structure that uses an adjacency list, stores the incoming edges of the neighbors */ diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala index 1a77d4da74..0436fa459b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala @@ -22,7 +22,7 @@ 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.payment.{BlindRecipient, ClearRecipient, Recipient} +import fr.acinq.eclair.payment.{BlindRecipient, Recipient} import fr.acinq.eclair.router.Graph.GraphStructure.DirectedGraph.graphEdgesToRoute import fr.acinq.eclair.router.Graph.GraphStructure.{DirectedGraph, GraphEdge} import fr.acinq.eclair.router.Graph.{InfiniteLoop, NegativeProbability, RichWeight} @@ -189,7 +189,7 @@ object RouteCalculation { routeParams: RouteParams, currentBlockHeight: BlockHeight): Try[Seq[Route]] = Try { findRouteInternal(g, localNodeId, recipients, amount, maxFee, numRoutes, extraEdges, ignoredEdges, ignoredVertices, routeParams, currentBlockHeight) match { - case Right(routes) => routes.map(route => graphEdgesToRoute(amount, route.path, recipients)) + case Right(routes) => routes.map(route => graphEdgesToRoute(amount, route, recipients)) case Left(ex) => return Failure(ex) } } @@ -205,7 +205,7 @@ object RouteCalculation { ignoredEdges: Set[ChannelDesc] = Set.empty, ignoredVertices: Set[PublicKey] = Set.empty, routeParams: RouteParams, - currentBlockHeight: BlockHeight): Either[RouterException, Seq[Graph.WeightedPath]] = { + currentBlockHeight: BlockHeight): Either[RouterException, Seq[Seq[GraphEdge]]] = { require(amount > 0.msat, "route amount must be strictly positive") if (targets.exists(_.introductionNodeId == localNodeId)) return Left(CannotRouteToSelf) @@ -220,9 +220,30 @@ object RouteCalculation { val targetNodes = targets.map(_.nodeId) val blindedEdges = targets.collect { case BlindRecipient(route, paymentInfo, capacity_opt, _, _) => GraphEdge(route, paymentInfo, capacity_opt) }.toSet - val foundRoutes: Seq[Graph.WeightedPath] = Graph.yenKshortestPaths(g, localNodeId, targetNodes, amount, ignoredEdges, ignoredVertices, extraEdges ++ blindedEdges, numRoutes, routeParams.heuristics, currentBlockHeight, boundaries, routeParams.includeLocalChannelCost) + val foundRoutes: Seq[Seq[GraphEdge]] = + if (targetNodes.length > 1) { + // We need to add a dummy node that connects to all the targets. + val dummyTarget = randomKey().publicKey + val dummyLinks = targetNodes.map(GraphEdge(_, dummyTarget)).toSet + val routes = + Graph.yenKshortestPaths(g, + localNodeId, dummyTarget, + amount, + ignoredEdges, ignoredVertices, + extraEdges ++ blindedEdges ++ dummyLinks, + numRoutes, routeParams.heuristics, currentBlockHeight, boundaries, routeParams.includeLocalChannelCost).map(_.path) + // We drop the last dummy hop. + routes.map(_.dropRight(1)) + } else { + Graph.yenKshortestPaths(g, + localNodeId, targetNodes.head, + amount, + ignoredEdges, ignoredVertices, + extraEdges ++ blindedEdges, + numRoutes, routeParams.heuristics, currentBlockHeight, boundaries, routeParams.includeLocalChannelCost).map(_.path) + } if (foundRoutes.nonEmpty) { - val (directRoutes, indirectRoutes) = foundRoutes.partition(_.path.length == 1) + val (directRoutes, indirectRoutes) = foundRoutes.partition(_.length == 1) val routes = if (routeParams.randomize) { Random.shuffle(directRoutes) ++ Random.shuffle(indirectRoutes) } else { @@ -315,14 +336,14 @@ object RouteCalculation { } @tailrec - private def split(amount: MilliSatoshi, paths: mutable.Queue[Graph.WeightedPath], usedCapacity: mutable.Map[ShortChannelId, MilliSatoshi], routeParams: RouteParams, recipients: Seq[Recipient], selectedRoutes: Seq[Route] = Nil): Either[RouterException, Seq[Route]] = { + private def split(amount: MilliSatoshi, paths: mutable.Queue[Seq[GraphEdge]], usedCapacity: mutable.Map[ShortChannelId, MilliSatoshi], routeParams: RouteParams, recipients: Seq[Recipient], selectedRoutes: Seq[Route] = Nil): Either[RouterException, Seq[Route]] = { if (amount == 0.msat) { Right(selectedRoutes) } else if (paths.isEmpty) { Left(RouteNotFound) } else { val current = paths.dequeue() - val candidate = computeRouteMaxAmount(current.path, usedCapacity, recipients) + val candidate = computeRouteMaxAmount(current, usedCapacity, recipients) if (candidate.amount < routeParams.mpp.minPartAmount.min(amount)) { // this route doesn't have enough capacity left: we remove it and continue. split(amount, paths, usedCapacity, routeParams, recipients, selectedRoutes) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/GraphSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/GraphSpec.scala index b51958f0e5..a62561bb47 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/GraphSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/GraphSpec.scala @@ -257,7 +257,7 @@ class GraphSpec extends AnyFunSuite { val edgeDE = makeEdge(6L, d, e, 9 msat, 0, capacity = 200000 sat) val graph = DirectedGraph(Seq(edgeAB, edgeBC, edgeCD, edgeDC, edgeCE, edgeDE)) - val path :: Nil = yenKshortestPaths(graph, a, Seq(e), 100000000 msat, + val path :: Nil = yenKshortestPaths(graph, a, e, 100000000 msat, Set.empty, Set.empty, Set.empty, 1, Right(HeuristicsConstants(1.0E-8, RelayFees(2000 msat, 500), RelayFees(50 msat, 20), useLogProbability = true)), BlockHeight(714930), _ => true, includeLocalChannelCost = true) @@ -281,7 +281,7 @@ class GraphSpec extends AnyFunSuite { val edgeDE = makeEdge(6L, d, e, 1 msat, 0, capacity = 200000 sat) val graph = DirectedGraph(Seq(edgeAB, edgeBC, edgeCD, edgeDC, edgeCE, edgeDE)) - val paths = yenKshortestPaths(graph, a, Seq(e), 90000000 msat, + val paths = yenKshortestPaths(graph, a, e, 90000000 msat, Set.empty, Set.empty, Set.empty, 2, Left(WeightRatios(1, 0, 0, 0, RelayFees(0 msat, 0))), BlockHeight(714930), _ => true, includeLocalChannelCost = true) @@ -307,7 +307,7 @@ class GraphSpec extends AnyFunSuite { val edgeDE = makeEdge(6L, d, e, 1 msat, 0, capacity = 200000 sat) val graph = DirectedGraph(Seq(edgeAB, edgeBC, edgeCD, edgeDC, edgeCE, edgeDE)) - val paths = yenKshortestPaths(graph, a, Seq(e), 90000000 msat, + val paths = yenKshortestPaths(graph, a, e, 90000000 msat, Set.empty, Set.empty, Set.empty, 2, Left(WeightRatios(1, 0, 0, 0, RelayFees(0 msat, 0))), BlockHeight(714930), _ => true, includeLocalChannelCost = true) @@ -340,7 +340,7 @@ class GraphSpec extends AnyFunSuite { val edgeGH = makeEdge(9L, g, h, 2 msat, 0, capacity = 100000 sat, minHtlc = 1000 msat) val graph = DirectedGraph(Seq(edgeCD, edgeDF, edgeCE, edgeED, edgeEF, edgeFG, edgeFH, edgeEG, edgeGH)) - val paths = yenKshortestPaths(graph, c, Seq(h), 10000000 msat, + val paths = yenKshortestPaths(graph, c, h, 10000000 msat, Set.empty, Set.empty, Set.empty, 3, Left(WeightRatios(1, 0, 0, 0, RelayFees(0 msat, 0))), BlockHeight(714930), _ => true, includeLocalChannelCost = true) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala index 54294f4124..43f816a93c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala @@ -677,7 +677,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(7L, c, f, 1 msat, 0, balance_opt = Some(DEFAULT_AMOUNT_MSAT)) )) - val fourShortestPaths = Graph.yenKshortestPaths(g1, d, Seq(f), DEFAULT_AMOUNT_MSAT, Set.empty, Set.empty, Set.empty, pathsToFind = 4, Left(NO_WEIGHT_RATIOS), BlockHeight(0), noopBoundaries, includeLocalChannelCost = false) + val fourShortestPaths = Graph.yenKshortestPaths(g1, d, f, DEFAULT_AMOUNT_MSAT, Set.empty, Set.empty, Set.empty, pathsToFind = 4, Left(NO_WEIGHT_RATIOS), BlockHeight(0), noopBoundaries, includeLocalChannelCost = false) assert(fourShortestPaths.size == 4) assert(hops2Ids(fourShortestPaths(0).path.map(graphEdgeToHop)) == 2 :: 5 :: Nil) // D -> E -> F assert(hops2Ids(fourShortestPaths(1).path.map(graphEdgeToHop)) == 1 :: 3 :: 5 :: Nil) // D -> A -> E -> F @@ -686,7 +686,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { // Update balance D -> A to evict the last path (balance too low) val g2 = g1.addEdge(makeEdge(1L, d, a, 1 msat, 0, balance_opt = Some(DEFAULT_AMOUNT_MSAT + 3.msat))) - val threeShortestPaths = Graph.yenKshortestPaths(g2, d, Seq(f), DEFAULT_AMOUNT_MSAT, Set.empty, Set.empty, Set.empty, pathsToFind = 4, Left(NO_WEIGHT_RATIOS), BlockHeight(0), noopBoundaries, includeLocalChannelCost = false) + val threeShortestPaths = Graph.yenKshortestPaths(g2, d, f, DEFAULT_AMOUNT_MSAT, Set.empty, Set.empty, Set.empty, pathsToFind = 4, Left(NO_WEIGHT_RATIOS), BlockHeight(0), noopBoundaries, includeLocalChannelCost = false) assert(threeShortestPaths.size == 3) assert(hops2Ids(threeShortestPaths(0).path.map(graphEdgeToHop)) == 2 :: 5 :: Nil) // D -> E -> F assert(hops2Ids(threeShortestPaths(1).path.map(graphEdgeToHop)) == 1 :: 3 :: 5 :: Nil) // D -> A -> E -> F @@ -715,7 +715,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(90L, g, h, 2 msat, 0) )) - val twoShortestPaths = Graph.yenKshortestPaths(graph, c, Seq(h), DEFAULT_AMOUNT_MSAT, Set.empty, Set.empty, Set.empty, pathsToFind = 2, Left(NO_WEIGHT_RATIOS), BlockHeight(0), noopBoundaries, includeLocalChannelCost = false) + val twoShortestPaths = Graph.yenKshortestPaths(graph, c, h, DEFAULT_AMOUNT_MSAT, Set.empty, Set.empty, Set.empty, pathsToFind = 2, Left(NO_WEIGHT_RATIOS), BlockHeight(0), noopBoundaries, includeLocalChannelCost = false) assert(twoShortestPaths.size == 2) val shortest = twoShortestPaths(0) @@ -746,7 +746,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) // we ask for 3 shortest paths but only 2 can be found - val foundPaths = Graph.yenKshortestPaths(graph, a, Seq(f), DEFAULT_AMOUNT_MSAT, Set.empty, Set.empty, Set.empty, pathsToFind = 3, Left(NO_WEIGHT_RATIOS), BlockHeight(0), noopBoundaries, includeLocalChannelCost = false) + val foundPaths = Graph.yenKshortestPaths(graph, a, f, DEFAULT_AMOUNT_MSAT, Set.empty, Set.empty, Set.empty, pathsToFind = 3, Left(NO_WEIGHT_RATIOS), BlockHeight(0), noopBoundaries, includeLocalChannelCost = false) assert(foundPaths.size == 2) assert(hops2Ids(foundPaths(0).path.map(graphEdgeToHop)) == 1 :: 2 :: 3 :: Nil) // A -> B -> C -> F assert(hops2Ids(foundPaths(1).path.map(graphEdgeToHop)) == 1 :: 2 :: 4 :: 5 :: 6 :: Nil) // A -> B -> C -> D -> E -> F From 4a3d736763647ddcafba0eb309131f3d8713b6d6 Mon Sep 17 00:00:00 2001 From: Thomas HUET Date: Fri, 30 Sep 2022 13:45:18 +0200 Subject: [PATCH 7/8] Test routing with multiple targets --- .../scala/fr/acinq/eclair/router/Graph.scala | 4 +- .../eclair/router/RouteCalculation.scala | 37 +++++++------------ .../eclair/router/RouteCalculationSpec.scala | 24 ++++++++++++ 3 files changed, 40 insertions(+), 25 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala index b6d4b6d7aa..4584b3aaab 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala @@ -276,12 +276,12 @@ object Graph { } if (targetFound) { - val edgePath = new mutable.ArrayBuffer[GraphEdge](RouteCalculation.ROUTE_MAX_LENGTH) + val edgePath = new mutable.ArrayBuffer[GraphEdge](RouteCalculation.ROUTE_MAX_LENGTH + 1) var current = bestEdges.get(sourceNode) while (current.isDefined) { edgePath += current.get current = bestEdges.get(current.get.desc.b) - if (edgePath.length > RouteCalculation.ROUTE_MAX_LENGTH) { + if (edgePath.length > RouteCalculation.ROUTE_MAX_LENGTH + 1) { throw InfiniteLoop(edgePath.toSeq) } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala index 0436fa459b..41077c3c0b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala @@ -212,7 +212,8 @@ object RouteCalculation { def feeOk(fee: MilliSatoshi): Boolean = fee <= maxFee - def lengthOk(length: Int): Boolean = length <= routeParams.boundaries.maxRouteLength && length <= ROUTE_MAX_LENGTH + // We use (length - 1) to ignore the dummy edge at the end. + def lengthOk(length: Int): Boolean = (length - 1) <= routeParams.boundaries.maxRouteLength && (length - 1) <= ROUTE_MAX_LENGTH def cltvOk(cltv: CltvExpiryDelta): Boolean = cltv <= routeParams.boundaries.maxCltv @@ -220,28 +221,18 @@ object RouteCalculation { val targetNodes = targets.map(_.nodeId) val blindedEdges = targets.collect { case BlindRecipient(route, paymentInfo, capacity_opt, _, _) => GraphEdge(route, paymentInfo, capacity_opt) }.toSet - val foundRoutes: Seq[Seq[GraphEdge]] = - if (targetNodes.length > 1) { - // We need to add a dummy node that connects to all the targets. - val dummyTarget = randomKey().publicKey - val dummyLinks = targetNodes.map(GraphEdge(_, dummyTarget)).toSet - val routes = - Graph.yenKshortestPaths(g, - localNodeId, dummyTarget, - amount, - ignoredEdges, ignoredVertices, - extraEdges ++ blindedEdges ++ dummyLinks, - numRoutes, routeParams.heuristics, currentBlockHeight, boundaries, routeParams.includeLocalChannelCost).map(_.path) - // We drop the last dummy hop. - routes.map(_.dropRight(1)) - } else { - Graph.yenKshortestPaths(g, - localNodeId, targetNodes.head, - amount, - ignoredEdges, ignoredVertices, - extraEdges ++ blindedEdges, - numRoutes, routeParams.heuristics, currentBlockHeight, boundaries, routeParams.includeLocalChannelCost).map(_.path) - } + // In case there are multiple targets, we need to add a dummy node that connects to all the targets. + val dummyTarget = randomKey().publicKey + val dummyLinks = targetNodes.map(GraphEdge(_, dummyTarget)).toSet + val routes = + Graph.yenKshortestPaths(g, + localNodeId, dummyTarget, + amount, + ignoredEdges, ignoredVertices, + extraEdges ++ blindedEdges ++ dummyLinks, + numRoutes, routeParams.heuristics, currentBlockHeight, boundaries, routeParams.includeLocalChannelCost).map(_.path) + // We drop the last dummy hop. + val foundRoutes: Seq[Seq[GraphEdge]] = routes.map(_.dropRight(1)) if (foundRoutes.nonEmpty) { val (directRoutes, indirectRoutes) = foundRoutes.partition(_.length == 1) val routes = if (routeParams.randomize) { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala index 43f816a93c..9628b27e58 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala @@ -1904,6 +1904,30 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val route :: Nil = routes assert(route2Ids(route) == 3 :: 4 :: Nil) } + + test("several recipients"){ + /* + B + / + A D + \ / + C + \ + E + */ + val g = DirectedGraph(List( + makeEdge(1L, a, b, 100 msat, 100, minHtlc = 1000 msat), + makeEdge(2L, a, c, 100 msat, 100, minHtlc = 1000 msat), + makeEdge(3L, c, d, 100 msat, 100, minHtlc = 1000 msat), + makeEdge(4L, c, e, 200 msat, 200, minHtlc = 1000 msat), + )) + + val Success(routes) = findRoute(g, a, Seq(makeRecipient(b), makeRecipient(d), makeRecipient(e)), 50000 msat, 100000000 msat, numRoutes = 3, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) + val route1 :: route2 :: route3 :: Nil = routes + assert(route2Ids(route1) == 1 :: Nil) + assert(route2Ids(route2) == 2 :: 3 :: Nil) + assert(route2Ids(route3) == 2 :: 4 :: Nil) + } } object RouteCalculationSpec { From 974548e4e941341dbeee2825db103fb13954ae07 Mon Sep 17 00:00:00 2001 From: Thomas HUET Date: Wed, 5 Oct 2022 13:25:02 +0200 Subject: [PATCH 8/8] FullRoute Make it work when we are the introduction node --- .../scala/fr/acinq/eclair/db/PaymentsDb.scala | 6 +- .../fr/acinq/eclair/db/pg/PgPaymentsDb.scala | 2 +- .../eclair/db/sqlite/SqlitePaymentsDb.scala | 2 +- .../acinq/eclair/json/JsonSerializers.scala | 2 +- .../acinq/eclair/payment/PaymentEvents.scala | 36 +++++++----- .../acinq/eclair/payment/PaymentPacket.scala | 10 ++-- .../send/MultiPartPaymentLifecycle.scala | 7 ++- .../payment/send/PaymentInitiator.scala | 26 +++++---- .../payment/send/PaymentLifecycle.scala | 17 +++--- .../eclair/router/RouteCalculation.scala | 2 +- .../fr/acinq/eclair/db/PaymentsDbSpec.scala | 4 +- .../BlindPaymentIntegrationSpec.scala | 52 ++--------------- .../integration/PaymentIntegrationSpec.scala | 2 +- .../MultiPartPaymentLifecycleSpec.scala | 58 +++++++++---------- .../eclair/payment/PaymentInitiatorSpec.scala | 12 ++-- .../eclair/payment/PaymentLifecycleSpec.scala | 56 +++++++++--------- .../payment/relay/NodeRelayerSpec.scala | 6 +- 17 files changed, 137 insertions(+), 163 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala index e4e7002f27..fa466049b6 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala @@ -246,9 +246,9 @@ object FailureType extends Enumeration { object FailureSummary { def apply(f: PaymentFailure): FailureSummary = f match { - case LocalFailure(_, route, t) => FailureSummary(FailureType.LOCAL, t.getMessage, route.map(h => HopSummary(h)).toList, route.headOption.map(_.nodeId)) - case RemoteFailure(_, route, e) => FailureSummary(FailureType.REMOTE, e.failureMessage.message, route.map(h => HopSummary(h)).toList, Some(e.originNode)) - case UnreadableRemoteFailure(_, route) => FailureSummary(FailureType.UNREADABLE_REMOTE, "could not decrypt failure onion", route.map(h => HopSummary(h)).toList, None) + case LocalFailure(_, route, t) => FailureSummary(FailureType.LOCAL, t.getMessage, route.hops.map(h => HopSummary(h)).toList, route.hops.headOption.map(_.nodeId)) + case RemoteFailure(_, route, e) => FailureSummary(FailureType.REMOTE, e.failureMessage.message, route.hops.map(h => HopSummary(h)).toList, Some(e.originNode)) + case UnreadableRemoteFailure(_, route) => FailureSummary(FailureType.UNREADABLE_REMOTE, "could not decrypt failure onion", route.hops.map(h => HopSummary(h)).toList, None) } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPaymentsDb.scala index ae19ab51d2..d6efe6bcd1 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPaymentsDb.scala @@ -126,7 +126,7 @@ class PgPaymentsDb(implicit ds: DataSource, lock: PgLock) extends PaymentsDb wit statement.setTimestamp(1, p.timestamp.toSqlTimestamp) statement.setString(2, paymentResult.paymentPreimage.toHex) statement.setLong(3, p.feesPaid.toLong) - statement.setBytes(4, encodeRoute(p.route.getOrElse(Nil).map(h => HopSummary(h)).toList)) + statement.setBytes(4, encodeRoute(p.route.map(_.hops).getOrElse(Nil).map(h => HopSummary(h)).toList)) statement.setString(5, p.id.toString) statement.addBatch() }) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala index bce1640c93..842f830e49 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala @@ -155,7 +155,7 @@ class SqlitePaymentsDb(val sqlite: Connection) extends PaymentsDb with Logging { statement.setLong(1, p.timestamp.toLong) statement.setBytes(2, paymentResult.paymentPreimage.toArray) statement.setLong(3, p.feesPaid.toLong) - statement.setBytes(4, encodeRoute(p.route.getOrElse(Nil).map(h => HopSummary(h)).toList)) + statement.setBytes(4, encodeRoute(p.route.map(_.hops).getOrElse(Nil).map(h => HopSummary(h)).toList)) statement.setString(5, p.id.toString) statement.addBatch() }) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala index 58b7f0e55e..19de032a3c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala @@ -320,7 +320,7 @@ object PaymentFailedSummarySerializer extends ConvertClassSerializer[PaymentFail p.cfg.recipientAmount, p.pathFindingExperiment, p.paymentFailed.failures.map(f => { - val route = f.route.map(_.nodeId) ++ f.route.lastOption.map(_.nextNodeId) + val route = f.route.hops.map(_.nodeId) ++ f.route.hops.lastOption.map(_.nextNodeId) val message = f match { case LocalFailure(_, _, t) => t.getMessage case RemoteFailure(_, _, Sphinx.DecryptedFailurePacket(origin, failureMessage)) => s"$origin returned: ${failureMessage.message}" diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala index 925445618c..88ebf32bec 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala @@ -23,7 +23,7 @@ import fr.acinq.eclair.payment.Invoice.{BasicEdge, ExtraEdge} import fr.acinq.eclair.payment.send.PaymentError.RetryExhausted import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentConfig import fr.acinq.eclair.router.Announcements -import fr.acinq.eclair.router.Router.{ChannelDesc, ChannelHop, Hop, Ignore} +import fr.acinq.eclair.router.Router.{ChannelDesc, ChannelHop, Hop, Ignore, Route} import fr.acinq.eclair.wire.protocol.{ChannelDisabled, ChannelUpdate, Node, TemporaryChannelFailure} import fr.acinq.eclair.{MilliSatoshi, ShortChannelId, TimestampMilli} import scodec.bits.ByteVector @@ -41,6 +41,12 @@ sealed trait PaymentEvent { val timestamp: TimestampMilli } +case class FullRoute(hops: Seq[Hop], blindedEnd_opt: Option[BlindRecipient] = None) + +object FullRoute { + val empty: FullRoute = FullRoute(Nil, None) +} + /** * A payment was successfully sent and fulfilled. * @@ -73,8 +79,8 @@ object PaymentSent { * @param route payment route used. * @param timestamp absolute time in milli-seconds since UNIX epoch when the payment was fulfilled. */ - case class PartialPayment(id: UUID, amount: MilliSatoshi, feesPaid: MilliSatoshi, toChannelId: ByteVector32, route: Option[Seq[Hop]], timestamp: TimestampMilli = TimestampMilli.now()) { - require(route.isEmpty || route.get.nonEmpty, "route must be None or contain at least one hop") + case class PartialPayment(id: UUID, amount: MilliSatoshi, feesPaid: MilliSatoshi, toChannelId: ByteVector32, route: Option[FullRoute], timestamp: TimestampMilli = TimestampMilli.now()) { + require(route.isEmpty || route.get.hops.nonEmpty || route.get.blindedEnd_opt.nonEmpty, "route must be None or contain at least one hop") val amountWithFees: MilliSatoshi = amount + feesPaid } @@ -125,18 +131,18 @@ case class WaitingToRelayPayment(remoteNodeId: PublicKey, paymentHash: ByteVecto sealed trait PaymentFailure { // @formatter:off def amount: MilliSatoshi - def route: Seq[Hop] + def route: FullRoute // @formatter:on } /** A failure happened locally, preventing the payment from being sent (e.g. no route found). */ -case class LocalFailure(amount: MilliSatoshi, route: Seq[Hop], t: Throwable) extends PaymentFailure +case class LocalFailure(amount: MilliSatoshi, route: FullRoute, t: Throwable) extends PaymentFailure /** A remote node failed the payment and we were able to decrypt the onion failure packet. */ -case class RemoteFailure(amount: MilliSatoshi, route: Seq[Hop], e: Sphinx.DecryptedFailurePacket) extends PaymentFailure +case class RemoteFailure(amount: MilliSatoshi, route: FullRoute, e: Sphinx.DecryptedFailurePacket) extends PaymentFailure /** A remote node failed the payment but we couldn't decrypt the failure (e.g. a malicious node tampered with the message). */ -case class UnreadableRemoteFailure(amount: MilliSatoshi, route: Seq[Hop]) extends PaymentFailure +case class UnreadableRemoteFailure(amount: MilliSatoshi, route: FullRoute) extends PaymentFailure object PaymentFailure { @@ -193,12 +199,12 @@ object PaymentFailure { /** Update the set of nodes and channels to ignore in retries depending on the failure we received. */ def updateIgnored(failure: PaymentFailure, ignore: Ignore): Ignore = failure match { - case RemoteFailure(_, hops, Sphinx.DecryptedFailurePacket(nodeId, _)) if nodeId == hops.last.nextNodeId => + case RemoteFailure(_, fullRoute, Sphinx.DecryptedFailurePacket(nodeId, _)) if fullRoute.hops.lastOption.exists(_.nextNodeId == nodeId) => // The failure came from the final recipient: the payment should be aborted without penalizing anyone in the route. ignore case RemoteFailure(_, _, Sphinx.DecryptedFailurePacket(nodeId, _: Node)) => ignore + nodeId - case RemoteFailure(_, hops, Sphinx.DecryptedFailurePacket(nodeId, failureMessage: Update)) => + case RemoteFailure(_, fullRoute, Sphinx.DecryptedFailurePacket(nodeId, failureMessage: Update)) => if (Announcements.checkSig(failureMessage.update, nodeId)) { val shouldIgnore = failureMessage match { case _: TemporaryChannelFailure => true @@ -206,7 +212,7 @@ object PaymentFailure { case _ => false } if (shouldIgnore) { - ignoreNodeOutgoingChannel(nodeId, hops, ignore) + ignoreNodeOutgoingChannel(nodeId, fullRoute.hops, ignore) } else { // We were using an outdated channel update, we should retry with the new one and nobody should be penalized. ignore @@ -215,13 +221,13 @@ object PaymentFailure { // This node is fishy, it gave us a bad signature, so let's filter it out. ignore + nodeId } - case RemoteFailure(_, hops, Sphinx.DecryptedFailurePacket(nodeId, _)) => - ignoreNodeOutgoingChannel(nodeId, hops, ignore) - case UnreadableRemoteFailure(_, hops) => + case RemoteFailure(_, fullRoute, Sphinx.DecryptedFailurePacket(nodeId, _)) => + ignoreNodeOutgoingChannel(nodeId, fullRoute.hops, ignore) + case UnreadableRemoteFailure(_, fullRoute) => // We don't know which node is sending garbage, let's blacklist all nodes except the one we are directly connected to and the final recipient. - val blacklist = hops.map(_.nextNodeId).drop(1).dropRight(1).toSet + val blacklist = fullRoute.hops.map(_.nextNodeId).drop(1).dropRight(1).toSet ignore ++ blacklist - case LocalFailure(_, hops, _) => hops.headOption match { + case LocalFailure(_, fullRoute, _) => fullRoute.hops.headOption match { case Some(hop: ChannelHop) => val faultyChannel = ChannelDesc(hop.shortChannelId, hop.nodeId, hop.nextNodeId) ignore + faultyChannel diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala index 3591347c1d..3baf673724 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala @@ -297,7 +297,7 @@ object OutgoingPaymentPacket { } def buildPaymentPacket(paymentHash: ByteVector32, - clearHops: Seq[Hop], + clearHops: Seq[ChannelHop], recipient: Recipient, amount: MilliSatoshi, totalAmount: MilliSatoshi, @@ -305,7 +305,7 @@ object OutgoingPaymentPacket { buildPacket(PaymentOnionCodecs.paymentOnionPayloadLength, paymentHash, clearHops, recipient, amount, totalAmount, expiry) def buildTrampolinePacket(paymentHash: ByteVector32, - hops: Seq[Hop], + hops: Seq[NodeHop], recipient: Recipient, amount: MilliSatoshi, totalAmount: MilliSatoshi, @@ -373,16 +373,16 @@ object OutgoingPaymentPacket { } else { route.recipient match { case recipient: BlindRecipient if recipient.route.introductionNodeId == privateKey.publicKey => - // We assume that there is a next node that is not us, that should be checked before calling the router. + // TODO(trampoline-to-blind): Check that we are not the final recipient. RouteBlindingEncryptedDataCodecs.decode(privateKey, recipient.route.blindingKey, recipient.route.encryptedPayloads.head) match { case Left(e) => return Failure(e) case Right(RouteBlindingDecryptedData(encryptedDataTlvs, nextBlindingKey)) => IntermediatePayload.ChannelRelay.Blinded.validate(TlvStream(EncryptedRecipientData(ByteVector.empty)), encryptedDataTlvs, nextBlindingKey) match { case Left(invalidTlv) => return Failure(RouteBlindingEncryptedDataCodecs.CannotDecodeData(invalidTlv.failureMessage.message)) case Right(payload) => - // We assume that fees and CLTV were checked in the router. + // TODO(trampoline-to-blind): Check fees and CLTV. As long as we are the sender it's fine but it is needed if we trampoline the payment for someone else. val amountWithFees = recipient.amountToSend(amount) - val remainingFee = amountWithFees - payload.amountToForward(amountWithFees) + val remainingFee = payload.amountToForward(amountWithFees) - amount val tailPaymentInfo = recipient.paymentInfo.copy(feeBase = remainingFee, feeProportionalMillionths = 0, cltvExpiryDelta = recipient.paymentInfo.cltvExpiryDelta - payload.cltvExpiryDelta) (payload.outgoingChannelId, Some(nextBlindingKey), recipient.copy(paymentInfo = tailPaymentInfo)) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala index d0f9254a05..c6008895cb 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala @@ -97,7 +97,7 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, retriedFailedChannels = true stay() using d.copy(remainingAttempts = (d.remainingAttempts - 1).max(0), ignore = d.ignore.emptyChannels()) } else { - val failure = LocalFailure(toSend, Nil, t) + val failure = LocalFailure(toSend, FullRoute.empty, t) Metrics.PaymentError.withTag(Tags.Failure, Tags.FailureType(failure)).increment() if (cfg.storeInDb && d.pending.isEmpty && d.failures.isEmpty) { // In cases where we fail early (router error during the first attempt), the DB won't have an entry for that @@ -131,7 +131,7 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, if (abortPayment(pf, d)) { gotoAbortedOrStop(PaymentAborted(d.request, d.failures ++ pf.failures, d.pending.keySet - pf.id)) } else if (d.remainingAttempts == 0) { - val failure = LocalFailure(d.request.totalAmount, Nil, PaymentError.RetryExhausted) + val failure = LocalFailure(d.request.totalAmount, FullRoute.empty, PaymentError.RetryExhausted) Metrics.PaymentError.withTag(Tags.Failure, Tags.FailureType(failure)).increment() gotoAbortedOrStop(PaymentAborted(d.request, d.failures ++ pf.failures :+ failure, d.pending.keySet - pf.id)) } else { @@ -256,7 +256,8 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, // in case of a relayed payment, we need to take into account the fee of the first channels paymentSent.parts.collect { // NB: the route attribute will always be defined here - case p@PartialPayment(_, _, _, _, Some(route), _) => route.head.fee(p.amountWithFees) + // TODO(trampoline-to-blind): fullRoute.hops may be empty if we are the introduction point of a blinded route. + case p@PartialPayment(_, _, _, _, Some(fullRoute), _) => fullRoute.hops.head.fee(p.amountWithFees) }.sum } paymentSent.feesPaid + localFees diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala index 65d3eea7d7..0eca22ce05 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala @@ -53,7 +53,7 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn val paymentCfg = SendPaymentConfig(paymentId, paymentId, r.externalId, r.paymentHash, r.recipientAmount, recipients.flatMap(_.nodeIds), Upstream.Local(paymentId), Some(r.invoice), storeInDb = true, publishEvent = true, recordPathFindingMetrics = true, Nil) val finalExpiry = r.finalExpiry(nodeParams.currentBlockHeight) if (!nodeParams.features.invoiceFeatures().areSupported(r.invoice.features)) { - sender() ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(r.recipientAmount, Nil, UnsupportedFeatures(r.invoice.features)) :: Nil) + sender() ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(r.recipientAmount, FullRoute.empty, UnsupportedFeatures(r.invoice.features)) :: Nil) } else if (r.invoice.features.hasFeature(Features.BasicMultiPartPayment) && nodeParams.features.hasFeature(BasicMultiPartPayment)) { val fsm = outgoingPaymentFactory.spawnOutgoingMultiPartPayment(context, paymentCfg) fsm ! MultiPartPaymentLifecycle.SendMultiPartPayment(self, recipients, r.recipientAmount, finalExpiry, r.maxAttempts, r.invoice.extraEdges, r.routeParams) @@ -79,9 +79,9 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn sender() ! paymentId r.trampolineAttempts match { case Nil => - sender() ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(r.recipientAmount, Nil, TrampolineFeesMissing) :: Nil) + sender() ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(r.recipientAmount, FullRoute.empty, TrampolineFeesMissing) :: Nil) case _ if !r.invoice.features.hasFeature(Features.TrampolinePaymentPrototype) && r.invoice.amount_opt.isEmpty => - sender() ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(r.recipientAmount, Nil, TrampolineLegacyAmountLessInvoice) :: Nil) + sender() ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(r.recipientAmount, FullRoute.empty, TrampolineLegacyAmountLessInvoice) :: Nil) case (trampolineFees, trampolineExpiryDelta) :: remainingAttempts => log.info(s"sending trampoline payment with trampoline fees=$trampolineFees and expiry delta=$trampolineExpiryDelta") sendTrampolinePayment(paymentId, r, trampolineFees, trampolineExpiryDelta) match { @@ -89,7 +89,7 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn context become main(pending + (paymentId -> PendingTrampolinePayment(sender(), remainingAttempts, r))) case Failure(t) => log.warning("cannot send outgoing trampoline payment: {}", t.getMessage) - sender() ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(r.recipientAmount, Nil, t) :: Nil) + sender() ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(r.recipientAmount, FullRoute.empty, t) :: Nil) } } @@ -111,10 +111,10 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn context become main(pending + (paymentId -> PendingTrampolinePaymentToRoute(sender(), r))) case Failure(t) => log.warning("cannot send outgoing trampoline payment: {}", t.getMessage) - sender() ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(r.recipientAmount, Nil, t) :: Nil) + sender() ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(r.recipientAmount, FullRoute.empty, t) :: Nil) } case _ => - sender() ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(r.recipientAmount, Nil, TrampolineMultiNodeNotSupported) :: Nil) + sender() ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(r.recipientAmount, FullRoute.empty, TrampolineMultiNodeNotSupported) :: Nil) } case r: SendPaymentToRoute => @@ -130,7 +130,7 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn context become main(pending + (paymentId -> PendingPaymentToRoute(sender(), r))) case None => log.warning("the provided route does not reach the correct recipient") - sender() ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(r.recipientAmount, Nil, InvalidRecipientForRoute(r.route, r.recipients)) :: Nil) + sender() ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(r.recipientAmount, FullRoute.empty, InvalidRecipientForRoute(r.route, r.recipients)) :: Nil) } case pf: PaymentFailed => pending.get(pf.id).foreach { @@ -150,14 +150,14 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn context become main(pending + (pf.id -> pp.copy(remainingAttempts = remaining))) case Failure(t) => log.warning("cannot send outgoing trampoline payment: {}", t.getMessage) - val localFailure = pf.copy(failures = Seq(LocalFailure(pp.r.recipientAmount, trampolineRoute, t))) + val localFailure = pf.copy(failures = Seq(LocalFailure(pp.r.recipientAmount, FullRoute(trampolineRoute, None), t))) pp.sender ! localFailure context.system.eventStream.publish(localFailure) context become main(pending - pf.id) } case Nil => log.info("trampoline node couldn't find a route after all retries") - val localFailure = pf.copy(failures = Seq(LocalFailure(pp.r.recipientAmount, trampolineRoute, RouteNotFound))) + val localFailure = pf.copy(failures = Seq(LocalFailure(pp.r.recipientAmount, FullRoute(trampolineRoute, None), RouteNotFound))) pp.sender ! localFailure context.system.eventStream.publish(localFailure) context become main(pending - pf.id) @@ -446,7 +446,13 @@ object PaymentInitiator { publishEvent: Boolean, recordPathFindingMetrics: Boolean, additionalHops: Seq[NodeHop]) { - def fullRoute(route: Route): Seq[Hop] = route.clearHops ++ additionalHops + def fullRoute(route: Route): FullRoute = + route.recipient match { + case _:ClearRecipient => FullRoute(route.clearHops ++ additionalHops, None) + case b:BlindRecipient => + require(additionalHops.isEmpty, "Can't add hops after a blinded route.") + FullRoute(route.clearHops, Some(b)) + } def createPaymentSent(preimage: ByteVector32, parts: Seq[PaymentSent.PartialPayment]) = PaymentSent(parentId, paymentHash, preimage, recipientAmount, recipientNodeIds.head, parts) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala index dc18ed2129..cf3f1f844c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala @@ -83,14 +83,14 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A goto(WAITING_FOR_PAYMENT_COMPLETE) using WaitingForComplete(c, cmd, failures, sharedSecrets, ignore, route) case Failure(t) => log.warning("cannot send outgoing payment: {}", t.getMessage) - Metrics.PaymentError.withTag(Tags.Failure, Tags.FailureType(LocalFailure(c.amount, Nil, t))).increment() - myStop(c, Left(PaymentFailed(id, paymentHash, failures :+ LocalFailure(c.amount, Nil, t)))) + Metrics.PaymentError.withTag(Tags.Failure, Tags.FailureType(LocalFailure(c.amount, FullRoute.empty, t))).increment() + myStop(c, Left(PaymentFailed(id, paymentHash, failures :+ LocalFailure(c.amount, FullRoute.empty, t)))) } case Event(Status.Failure(t), WaitingForRoute(c, failures, _)) => log.warning("router error: {}", t.getMessage) - Metrics.PaymentError.withTag(Tags.Failure, Tags.FailureType(LocalFailure(c.amount, Nil, t))).increment() - myStop(c, Left(PaymentFailed(id, paymentHash, failures :+ LocalFailure(c.amount, Nil, t)))) + Metrics.PaymentError.withTag(Tags.Failure, Tags.FailureType(LocalFailure(c.amount, FullRoute.empty, t))).increment() + myStop(c, Left(PaymentFailed(id, paymentHash, failures :+ LocalFailure(c.amount, FullRoute.empty, t)))) } when(WAITING_FOR_PAYMENT_COMPLETE) { @@ -170,10 +170,10 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A import d._ ((Sphinx.FailurePacket.decrypt(fail.reason, sharedSecrets) match { case success@Success(e) => - Metrics.PaymentError.withTag(Tags.Failure, Tags.FailureType(RemoteFailure(d.c.amount, Nil, e))).increment() + Metrics.PaymentError.withTag(Tags.Failure, Tags.FailureType(RemoteFailure(d.c.amount, FullRoute.empty, e))).increment() success case failure@Failure(_) => - Metrics.PaymentError.withTag(Tags.Failure, Tags.FailureType(UnreadableRemoteFailure(d.c.amount, Nil))).increment() + Metrics.PaymentError.withTag(Tags.Failure, Tags.FailureType(UnreadableRemoteFailure(d.c.amount, FullRoute.empty))).increment() failure }) match { case res@Success(Sphinx.DecryptedFailurePacket(nodeId, failureMessage)) => @@ -348,7 +348,8 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A // in case of a relayed payment, we need to take into account the fee of the first channels paymentSent.parts.collect { // NB: the route attribute will always be defined here - case p@PartialPayment(_, _, _, _, Some(route), _) => route.head.fee(p.amountWithFees) + // TODO(trampoline-to-blind): fullRoute.hops may be empty if we are the introduction point of a blinded route. + case p@PartialPayment(_, _, _, _, Some(fullRoute), _) => fullRoute.hops.head.fee(p.amountWithFees) }.sum } paymentSent.feesPaid + localFees @@ -417,7 +418,7 @@ object PaymentLifecycle { totalAmount: MilliSatoshi, targetExpiry: CltvExpiry, extraEdges: Seq[ExtraEdge] = Nil) extends SendPayment { - require(route.fold(!_.isEmpty, _.clearHops.nonEmpty), "payment route must not be empty") + require(route.fold(!_.isEmpty, _ => true), "payment route must not be empty") override def targetRecipients: Seq[Recipient] = Seq(targetRecipient) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala index 41077c3c0b..ef1a2dcf94 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala @@ -208,7 +208,7 @@ object RouteCalculation { currentBlockHeight: BlockHeight): Either[RouterException, Seq[Seq[GraphEdge]]] = { require(amount > 0.msat, "route amount must be strictly positive") - if (targets.exists(_.introductionNodeId == localNodeId)) return Left(CannotRouteToSelf) + if (targets.exists(_.nodeId == localNodeId)) return Left(CannotRouteToSelf) def feeOk(fee: MilliSatoshi): Boolean = fee <= maxFee diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/PaymentsDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/PaymentsDbSpec.scala index 39ffa96e64..c33055fd4a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/PaymentsDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/PaymentsDbSpec.scala @@ -622,7 +622,7 @@ class PaymentsDbSpec extends AnyFunSuite { db.updateOutgoingPayment(PaymentFailed(s3.id, s3.paymentHash, Nil, 310 unixms)) val ss3 = s3.copy(status = OutgoingPaymentStatus.Failed(Nil, 310 unixms)) assert(db.getOutgoingPayment(s3.id).contains(ss3)) - db.updateOutgoingPayment(PaymentFailed(s4.id, s4.paymentHash, Seq(LocalFailure(s4.amount, Seq(hop_ab), new RuntimeException("woops")), RemoteFailure(s4.amount, Seq(hop_ab, hop_bc), Sphinx.DecryptedFailurePacket(carol, UnknownNextPeer))), 320 unixms)) + db.updateOutgoingPayment(PaymentFailed(s4.id, s4.paymentHash, Seq(LocalFailure(s4.amount, FullRoute(Seq(hop_ab)), new RuntimeException("woops")), RemoteFailure(s4.amount, FullRoute(Seq(hop_ab, hop_bc)), Sphinx.DecryptedFailurePacket(carol, UnknownNextPeer))), 320 unixms)) val ss4 = s4.copy(status = OutgoingPaymentStatus.Failed(Seq(FailureSummary(FailureType.LOCAL, "woops", List(HopSummary(alice, bob, Some(ShortChannelId(42)))), Some(alice)), FailureSummary(FailureType.REMOTE, "processing node does not know the next peer in the route", List(HopSummary(alice, bob, Some(ShortChannelId(42))), HopSummary(bob, carol, None)), Some(carol))), 320 unixms)) assert(db.getOutgoingPayment(s4.id).contains(ss4)) @@ -631,7 +631,7 @@ class PaymentsDbSpec extends AnyFunSuite { val paymentSent = PaymentSent(parentId, paymentHash1, preimage1, 600 msat, carol, Seq( PaymentSent.PartialPayment(s1.id, s1.amount, 15 msat, randomBytes32(), None, 400 unixms), - PaymentSent.PartialPayment(s2.id, s2.amount, 20 msat, randomBytes32(), Some(Seq(hop_ab, hop_bc)), 410 unixms) + PaymentSent.PartialPayment(s2.id, s2.amount, 20 msat, randomBytes32(), Some(FullRoute(Seq(hop_ab, hop_bc))), 410 unixms) )) val ss1 = s1.copy(status = OutgoingPaymentStatus.Succeeded(preimage1, 15 msat, Nil, 400 unixms)) val ss2 = s2.copy(status = OutgoingPaymentStatus.Succeeded(preimage1, 20 msat, Seq(HopSummary(alice, bob, Some(ShortChannelId(42))), HopSummary(bob, carol, None)), 410 unixms)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/BlindPaymentIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/BlindPaymentIntegrationSpec.scala index ca9471c961..4ced56a783 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/BlindPaymentIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/BlindPaymentIntegrationSpec.scala @@ -16,38 +16,22 @@ package fr.acinq.eclair.integration -import akka.actor.ActorRef -import akka.actor.typed.scaladsl.adapter.{ClassicActorRefOps, actorRefAdapter} +import akka.actor.typed.scaladsl.adapter.actorRefAdapter import akka.testkit.TestProbe import com.typesafe.config.ConfigFactory -import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, Crypto, SatoshiLong} -import fr.acinq.eclair.blockchain.bitcoind.BitcoindService.BitcoinReq +import fr.acinq.bitcoin.scalacompat.{Crypto, SatoshiLong} import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{Watch, WatchFundingConfirmed} -import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient import fr.acinq.eclair.channel._ -import fr.acinq.eclair.channel.fsm.Channel.{BroadcastChannelUpdate, PeriodicRefresh} -import fr.acinq.eclair.crypto.Sphinx.DecryptedFailurePacket -import fr.acinq.eclair.crypto.TransportHandler -import fr.acinq.eclair.db._ -import fr.acinq.eclair.io.Peer.PeerRoutingMessage import fr.acinq.eclair.payment._ -import fr.acinq.eclair.payment.receive.MultiPartHandler.{ReceiveOfferPayment, ReceiveStandardPayment} +import fr.acinq.eclair.payment.receive.MultiPartHandler.ReceiveOfferPayment import fr.acinq.eclair.payment.relay.Relayer -import fr.acinq.eclair.payment.relay.Relayer.RelayFees -import fr.acinq.eclair.payment.send.PaymentInitiator.{SendPaymentToNode, SendTrampolinePayment} -import fr.acinq.eclair.router.Graph.WeightRatios -import fr.acinq.eclair.router.Router.{GossipDecision, PublicChannel} -import fr.acinq.eclair.router.{Announcements, AnnouncementsBatchValidationSpec, CannotRouteToSelf, Router} +import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentToNode +import fr.acinq.eclair.router.{CannotRouteToSelf, Router} import fr.acinq.eclair.wire.protocol.OfferTypes.{InvoiceRequest, Offer} -import fr.acinq.eclair.wire.protocol.{ChannelAnnouncement, ChannelUpdate, IncorrectOrUnknownPaymentDetails} -import fr.acinq.eclair.{CltvExpiryDelta, Features, Kit, MilliSatoshiLong, ShortChannelId, TimestampMilli, randomBytes32, randomKey} -import org.json4s.JsonAST.{JString, JValue} -import scodec.bits.ByteVector +import fr.acinq.eclair.{Kit, MilliSatoshiLong, randomKey} import java.util.UUID -import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.duration._ import scala.jdk.CollectionConverters._ @@ -173,30 +157,6 @@ class BlindPaymentIntegrationSpec extends IntegrationSpec { assert(Crypto.sha256(ps.paymentPreimage) == invoice.paymentHash) } - test("send an HTLC D->D") { - val (sender, eventListener) = (TestProbe(), TestProbe()) - nodes("D").system.eventStream.subscribe(eventListener.ref, classOf[PaymentMetadataReceived]) - - val recipientKey = randomKey() - val payerKey = randomKey() - - // first we retrieve an invoice from D - val amount = 4200000 msat - val chain = nodes("D").nodeParams.chainHash - val offer = Offer(Some(amount), "test offer", recipientKey.publicKey, nodes("D").nodeParams.features.invoiceFeatures(), chain) - val invoiceRequest = InvoiceRequest(offer, amount, 1, nodes("D").nodeParams.features.invoiceFeatures(), payerKey, chain) - - sender.send(nodes("D").paymentHandler, ReceiveOfferPayment(recipientKey, offer, invoiceRequest)) - val invoice = sender.expectMsgType[Bolt12Invoice] - - // then we make the actual payment - sender.send(nodes("D").paymentInitiator, SendPaymentToNode(amount, invoice, routeParams = integrationTestRouteParams, maxAttempts = 1)) - val paymentId = sender.expectMsgType[UUID] - val pf = sender.expectMsgType[PaymentFailed] - assert(pf.id == paymentId) - assert(pf.failures.head.asInstanceOf[LocalFailure].t == CannotRouteToSelf) - } - // TODO: Add more tests with more cases of blinded routes and with MPP. /** Handy way to check what the channel balances are before adding new tests. */ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala index c26872b678..4d9698bd6c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala @@ -333,7 +333,7 @@ class PaymentIntegrationSpec extends IntegrationSpec { sender.send(nodes("A").paymentInitiator, SendPaymentToNode(amountMsat, invoice, maxAttempts = 1, routeParams = integrationTestRouteParams.copy(heuristics = Left(WeightRatios(0, 0, 0, 1, RelayFees(0 msat, 0)))))) sender.expectMsgType[UUID] val ps = sender.expectMsgType[PaymentSent] - ps.parts.foreach(part => assert(part.route.getOrElse(Nil).exists(_.nodeId == nodes("G").nodeParams.nodeId))) + ps.parts.foreach(part => assert(part.route.map(_.hops).getOrElse(Nil).exists(_.nodeId == nodes("G").nodeParams.nodeId))) } test("send a multi-part payment B->D") { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala index be9c62bf8b..1e5c09d1c0 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala @@ -186,7 +186,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS childPayFsm.expectNoMessage(100 millis) val childId = payFsm.stateData.asInstanceOf[PaymentProgress].pending.keys.head - childPayFsm.send(payFsm, PaymentFailed(childId, paymentHash, Seq(RemoteFailure(failingRoute.amount, failingRoute.clearHops, Sphinx.DecryptedFailurePacket(b, PermanentChannelFailure))))) + childPayFsm.send(payFsm, PaymentFailed(childId, paymentHash, Seq(RemoteFailure(failingRoute.amount, FullRoute(failingRoute.clearHops), Sphinx.DecryptedFailurePacket(b, PermanentChannelFailure))))) // We retry ignoring the failing channel. router.expectMsg(RouteRequest(nodeParams.nodeId, Seq(eRecipient), finalAmount, maxFee, routeParams = routeParams.copy(randomize = true), allowMultiPart = true, ignore = Ignore(Set.empty, Set(ChannelDesc(channelId_be, b, e))), paymentContext = Some(cfg.paymentContext))) router.send(payFsm, RouteResponse(Seq(Route(400000 msat, hop_ac_1 :: hop_ce :: Nil, eRecipient), Route(600000 msat, hop_ad :: hop_de :: Nil, eRecipient)))) @@ -219,12 +219,12 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS childPayFsm.expectNoMessage(100 millis) val (failedId1, failedRoute1) :: (failedId2, failedRoute2) :: Nil = payFsm.stateData.asInstanceOf[PaymentProgress].pending.toSeq - childPayFsm.send(payFsm, PaymentFailed(failedId1, paymentHash, Seq(RemoteFailure(failedRoute1.amount, failedRoute1.clearHops, Sphinx.DecryptedFailurePacket(b, TemporaryNodeFailure))))) + childPayFsm.send(payFsm, PaymentFailed(failedId1, paymentHash, Seq(RemoteFailure(failedRoute1.amount, FullRoute(failedRoute1.clearHops), Sphinx.DecryptedFailurePacket(b, TemporaryNodeFailure))))) // When we retry, we ignore the failing node and we let the router know about the remaining pending route. router.expectMsg(RouteRequest(nodeParams.nodeId, Seq(eRecipient), failedRoute1.amount, maxFee - failedRoute1.fee(false), ignore = Ignore(Set(b), Set.empty), pendingPayments = Seq(failedRoute2), allowMultiPart = true, routeParams = routeParams.copy(randomize = true), paymentContext = Some(cfg.paymentContext))) // The second part fails while we're still waiting for new routes. - childPayFsm.send(payFsm, PaymentFailed(failedId2, paymentHash, Seq(RemoteFailure(failedRoute2.amount, failedRoute2.clearHops, Sphinx.DecryptedFailurePacket(b, TemporaryNodeFailure))))) + childPayFsm.send(payFsm, PaymentFailed(failedId2, paymentHash, Seq(RemoteFailure(failedRoute2.amount, FullRoute(failedRoute2.clearHops), Sphinx.DecryptedFailurePacket(b, TemporaryNodeFailure))))) // We receive a response to our first request, but it's now obsolete: we re-sent a new route request that takes into // account the latest failures. router.send(payFsm, RouteResponse(Seq(Route(failedRoute1.amount, hop_ac_1 :: hop_ce :: Nil, eRecipient)))) @@ -260,7 +260,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS childPayFsm.expectNoMessage(100 millis) val (failedId, failedRoute) :: Nil = payFsm.stateData.asInstanceOf[PaymentProgress].pending.toSeq - childPayFsm.send(payFsm, PaymentFailed(failedId, paymentHash, Seq(LocalFailure(failedRoute.amount, failedRoute.clearHops, RemoteCannotAffordFeesForNewHtlc(randomBytes32(), finalAmount, 15 sat, 0 sat, 15 sat))))) + childPayFsm.send(payFsm, PaymentFailed(failedId, paymentHash, Seq(LocalFailure(failedRoute.amount, FullRoute(failedRoute.clearHops), RemoteCannotAffordFeesForNewHtlc(randomBytes32(), finalAmount, 15 sat, 0 sat, 15 sat))))) // We retry without the failing channel. val expectedRouteRequest = RouteRequest( @@ -286,7 +286,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS childPayFsm.expectNoMessage(100 millis) val (failedId, failedRoute) :: (_, pendingRoute) :: Nil = payFsm.stateData.asInstanceOf[PaymentProgress].pending.toSeq - childPayFsm.send(payFsm, PaymentFailed(failedId, paymentHash, Seq(LocalFailure(failedRoute.amount, failedRoute.clearHops, ChannelUnavailable(randomBytes32()))))) + childPayFsm.send(payFsm, PaymentFailed(failedId, paymentHash, Seq(LocalFailure(failedRoute.amount, FullRoute(failedRoute.clearHops), ChannelUnavailable(randomBytes32()))))) // If the router doesn't find routes, we will retry without ignoring the channel: it may work with a different split // of the amount to send. @@ -332,7 +332,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS // B changed his fees and expiry after the invoice was issued. val channelUpdate = hop_be.params.asInstanceOf[ChannelRelayParams.FromAnnouncement].channelUpdate.copy(feeBaseMsat = 250 msat, feeProportionalMillionths = 150, cltvExpiryDelta = CltvExpiryDelta(24)) val childId = payFsm.stateData.asInstanceOf[PaymentProgress].pending.keys.head - childPayFsm.send(payFsm, PaymentFailed(childId, paymentHash, Seq(RemoteFailure(route.amount, route.clearHops, Sphinx.DecryptedFailurePacket(b, FeeInsufficient(finalAmount, channelUpdate)))))) + childPayFsm.send(payFsm, PaymentFailed(childId, paymentHash, Seq(RemoteFailure(route.amount, FullRoute(route.clearHops), Sphinx.DecryptedFailurePacket(b, FeeInsufficient(finalAmount, channelUpdate)))))) // We update the routing hints accordingly before requesting a new route. val updatedExtraEdge = router.expectMsgType[RouteRequest].extraEdges.head assert(updatedExtraEdge == BasicEdge(b, e, hop_be.shortChannelId, channelUpdate.feeBaseMsat, channelUpdate.feeProportionalMillionths, channelUpdate.cltvExpiryDelta)) @@ -356,7 +356,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS val channelUpdateBE = hop_be.params.asInstanceOf[ChannelRelayParams.FromAnnouncement].channelUpdate val channelUpdateBE1 = Announcements.makeChannelUpdate(channelUpdateBE.chainHash, priv_b, e, channelUpdateBE.shortChannelId, channelUpdateBE.cltvExpiryDelta, channelUpdateBE.htlcMinimumMsat, channelUpdateBE.feeBaseMsat, channelUpdateBE.feeProportionalMillionths, channelUpdateBE.htlcMaximumMsat) val childId = payFsm.stateData.asInstanceOf[PaymentProgress].pending.keys.head - childPayFsm.send(payFsm, PaymentFailed(childId, paymentHash, Seq(RemoteFailure(route.amount, route.clearHops, Sphinx.DecryptedFailurePacket(b, TemporaryChannelFailure(channelUpdateBE1)))))) + childPayFsm.send(payFsm, PaymentFailed(childId, paymentHash, Seq(RemoteFailure(route.amount, FullRoute(route.clearHops), Sphinx.DecryptedFailurePacket(b, TemporaryChannelFailure(channelUpdateBE1)))))) // We update the routing hints accordingly before requesting a new route and ignore the channel. val routeRequest = router.expectMsgType[RouteRequest] assert(routeRequest.extraEdges.head == extraEdge) @@ -375,9 +375,9 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS { val failures = Seq( - LocalFailure(finalAmount, Nil, ChannelUnavailable(randomBytes32())), - RemoteFailure(finalAmount, Nil, Sphinx.DecryptedFailurePacket(b, FeeInsufficient(100 msat, makeChannelUpdate(ShortChannelId(2), 15 msat, 150, CltvExpiryDelta(48))))), - UnreadableRemoteFailure(finalAmount, Nil) + LocalFailure(finalAmount, FullRoute.empty, ChannelUnavailable(randomBytes32())), + RemoteFailure(finalAmount, FullRoute.empty, Sphinx.DecryptedFailurePacket(b, FeeInsufficient(100 msat, makeChannelUpdate(ShortChannelId(2), 15 msat, 150, CltvExpiryDelta(48))))), + UnreadableRemoteFailure(finalAmount, FullRoute.empty) ) val extraEdges1 = Seq( BasicEdge(a, b, ShortChannelId(1), 10 msat, 0, CltvExpiryDelta(12)), BasicEdge(b, c, ShortChannelId(2), 15 msat, 150, CltvExpiryDelta(48)), @@ -387,10 +387,10 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS } { val failures = Seq( - RemoteFailure(finalAmount, Nil, Sphinx.DecryptedFailurePacket(a, FeeInsufficient(100 msat, makeChannelUpdate(ShortChannelId(1), 20 msat, 20, CltvExpiryDelta(20))))), - RemoteFailure(finalAmount, Nil, Sphinx.DecryptedFailurePacket(b, FeeInsufficient(100 msat, makeChannelUpdate(ShortChannelId(2), 21 msat, 21, CltvExpiryDelta(21))))), - RemoteFailure(finalAmount, Nil, Sphinx.DecryptedFailurePacket(a, FeeInsufficient(100 msat, makeChannelUpdate(ShortChannelId(3), 22 msat, 22, CltvExpiryDelta(22))))), - RemoteFailure(finalAmount, Nil, Sphinx.DecryptedFailurePacket(a, FeeInsufficient(100 msat, makeChannelUpdate(ShortChannelId(1), 23 msat, 23, CltvExpiryDelta(23))))), + RemoteFailure(finalAmount, FullRoute.empty, Sphinx.DecryptedFailurePacket(a, FeeInsufficient(100 msat, makeChannelUpdate(ShortChannelId(1), 20 msat, 20, CltvExpiryDelta(20))))), + RemoteFailure(finalAmount, FullRoute.empty, Sphinx.DecryptedFailurePacket(b, FeeInsufficient(100 msat, makeChannelUpdate(ShortChannelId(2), 21 msat, 21, CltvExpiryDelta(21))))), + RemoteFailure(finalAmount, FullRoute.empty, Sphinx.DecryptedFailurePacket(a, FeeInsufficient(100 msat, makeChannelUpdate(ShortChannelId(3), 22 msat, 22, CltvExpiryDelta(22))))), + RemoteFailure(finalAmount, FullRoute.empty, Sphinx.DecryptedFailurePacket(a, FeeInsufficient(100 msat, makeChannelUpdate(ShortChannelId(1), 23 msat, 23, CltvExpiryDelta(23))))), ) val extraEdges1 = Seq( BasicEdge(a, b, ShortChannelId(1), 23 msat, 23, CltvExpiryDelta(23)), BasicEdge(b, c, ShortChannelId(2), 21 msat, 21, CltvExpiryDelta(21)), @@ -411,16 +411,16 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS childPayFsm.expectMsgType[SendPaymentToRoute] val (failedId1, failedRoute1) = payFsm.stateData.asInstanceOf[PaymentProgress].pending.head - childPayFsm.send(payFsm, PaymentFailed(failedId1, paymentHash, Seq(UnreadableRemoteFailure(failedRoute1.amount, failedRoute1.clearHops)))) + childPayFsm.send(payFsm, PaymentFailed(failedId1, paymentHash, Seq(UnreadableRemoteFailure(failedRoute1.amount, FullRoute(failedRoute1.clearHops))))) router.expectMsgType[RouteRequest] router.send(payFsm, RouteResponse(Seq(Route(500000 msat, hop_ad :: hop_de :: Nil, eRecipient)))) childPayFsm.expectMsgType[SendPaymentToRoute] assert(!payFsm.stateData.asInstanceOf[PaymentProgress].pending.contains(failedId1)) val (failedId2, failedRoute2) = payFsm.stateData.asInstanceOf[PaymentProgress].pending.head - val result = abortAfterFailure(f, PaymentFailed(failedId2, paymentHash, Seq(UnreadableRemoteFailure(failedRoute2.amount, failedRoute2.clearHops)))) + val result = abortAfterFailure(f, PaymentFailed(failedId2, paymentHash, Seq(UnreadableRemoteFailure(failedRoute2.amount, FullRoute(failedRoute2.clearHops))))) assert(result.failures.length >= 3) - assert(result.failures.contains(LocalFailure(finalAmount, Nil, RetryExhausted))) + assert(result.failures.contains(LocalFailure(finalAmount, FullRoute.empty, RetryExhausted))) val metrics = metricsListener.expectMsgType[PathFindingExperimentMetrics] assert(metrics.status == "FAILURE") @@ -442,7 +442,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS val result = sender.expectMsgType[PaymentFailed] assert(result.id == cfg.id) assert(result.paymentHash == paymentHash) - assert(result.failures == Seq(LocalFailure(finalAmount, Nil, RouteNotFound))) + assert(result.failures == Seq(LocalFailure(finalAmount, FullRoute.empty, RouteNotFound))) val Some(outgoing) = nodeParams.db.payments.getOutgoingPayment(cfg.id) assert(outgoing.status.isInstanceOf[OutgoingPaymentStatus.Failed]) @@ -471,7 +471,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS childPayFsm.expectMsgType[SendPaymentToRoute] val (failedId, failedRoute) = payFsm.stateData.asInstanceOf[PaymentProgress].pending.head - val result = abortAfterFailure(f, PaymentFailed(failedId, paymentHash, Seq(RemoteFailure(failedRoute.amount, failedRoute.clearHops, Sphinx.DecryptedFailurePacket(e, IncorrectOrUnknownPaymentDetails(600000 msat, BlockHeight(0))))))) + val result = abortAfterFailure(f, PaymentFailed(failedId, paymentHash, Seq(RemoteFailure(failedRoute.amount, FullRoute(failedRoute.clearHops), Sphinx.DecryptedFailurePacket(e, IncorrectOrUnknownPaymentDetails(600000 msat, BlockHeight(0))))))) assert(result.failures.length == 1) val metrics = metricsListener.expectMsgType[PathFindingExperimentMetrics] @@ -492,7 +492,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS childPayFsm.expectMsgType[SendPaymentToRoute] val (failedId, failedRoute) = payFsm.stateData.asInstanceOf[PaymentProgress].pending.head - val result = abortAfterFailure(f, PaymentFailed(failedId, paymentHash, Seq(LocalFailure(failedRoute.amount, failedRoute.clearHops, HtlcsTimedoutDownstream(channelId = ByteVector32.One, htlcs = Set.empty))))) + val result = abortAfterFailure(f, PaymentFailed(failedId, paymentHash, Seq(LocalFailure(failedRoute.amount, FullRoute(failedRoute.clearHops), HtlcsTimedoutDownstream(channelId = ByteVector32.One, htlcs = Set.empty))))) assert(result.failures.length == 1) } @@ -507,10 +507,10 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS childPayFsm.expectMsgType[SendPaymentToRoute] val (failedId1, failedRoute1) :: (failedId2, failedRoute2) :: Nil = payFsm.stateData.asInstanceOf[PaymentProgress].pending.toSeq - childPayFsm.send(payFsm, PaymentFailed(failedId1, paymentHash, Seq(UnreadableRemoteFailure(failedRoute1.amount, failedRoute1.clearHops)))) + childPayFsm.send(payFsm, PaymentFailed(failedId1, paymentHash, Seq(UnreadableRemoteFailure(failedRoute1.amount, FullRoute(failedRoute1.clearHops))))) router.expectMsgType[RouteRequest] - val result = abortAfterFailure(f, PaymentFailed(failedId2, paymentHash, Seq(RemoteFailure(failedRoute2.amount, failedRoute2.clearHops, Sphinx.DecryptedFailurePacket(e, PaymentTimeout))))) + val result = abortAfterFailure(f, PaymentFailed(failedId2, paymentHash, Seq(RemoteFailure(failedRoute2.amount, FullRoute(failedRoute2.clearHops), Sphinx.DecryptedFailurePacket(e, PaymentTimeout))))) assert(result.failures.length == 2) } @@ -525,7 +525,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS childPayFsm.expectMsgType[SendPaymentToRoute] val (failedId, failedRoute) :: (successId, successRoute) :: Nil = payFsm.stateData.asInstanceOf[PaymentProgress].pending.toSeq - childPayFsm.send(payFsm, PaymentFailed(failedId, paymentHash, Seq(UnreadableRemoteFailure(failedRoute.amount, failedRoute.clearHops)))) + childPayFsm.send(payFsm, PaymentFailed(failedId, paymentHash, Seq(UnreadableRemoteFailure(failedRoute.amount, FullRoute(failedRoute.clearHops))))) router.expectMsgType[RouteRequest] val result = fulfillPendingPayments(f, 1) @@ -545,11 +545,11 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS childPayFsm.expectMsgType[SendPaymentToRoute] val (failedId, failedRoute) :: (successId, successRoute) :: Nil = payFsm.stateData.asInstanceOf[PaymentProgress].pending.toSeq - childPayFsm.send(payFsm, PaymentFailed(failedId, paymentHash, Seq(RemoteFailure(failedRoute.amount, failedRoute.clearHops, Sphinx.DecryptedFailurePacket(e, PaymentTimeout))))) + childPayFsm.send(payFsm, PaymentFailed(failedId, paymentHash, Seq(RemoteFailure(failedRoute.amount, FullRoute(failedRoute.clearHops), Sphinx.DecryptedFailurePacket(e, PaymentTimeout))))) awaitCond(payFsm.stateName == PAYMENT_ABORTED) sender.watch(payFsm) - childPayFsm.send(payFsm, PaymentSent(cfg.id, paymentHash, paymentPreimage, finalAmount, e, Seq(PaymentSent.PartialPayment(successId, successRoute.amount, successRoute.fee(false), randomBytes32(), Some(successRoute.clearHops))))) + childPayFsm.send(payFsm, PaymentSent(cfg.id, paymentHash, paymentPreimage, finalAmount, e, Seq(PaymentSent.PartialPayment(successId, successRoute.amount, successRoute.fee(false), randomBytes32(), Some(FullRoute(successRoute.clearHops)))))) sender.expectMsg(PreimageReceived(paymentHash, paymentPreimage)) val result = sender.expectMsgType[PaymentSent] assert(result.id == cfg.id) @@ -578,12 +578,12 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS childPayFsm.expectMsgType[SendPaymentToRoute] val (childId, route) :: (failedId, failedRoute) :: Nil = payFsm.stateData.asInstanceOf[PaymentProgress].pending.toSeq - childPayFsm.send(payFsm, PaymentSent(cfg.id, paymentHash, paymentPreimage, finalAmount, e, Seq(PaymentSent.PartialPayment(childId, route.amount, route.fee(false), randomBytes32(), Some(route.clearHops))))) + childPayFsm.send(payFsm, PaymentSent(cfg.id, paymentHash, paymentPreimage, finalAmount, e, Seq(PaymentSent.PartialPayment(childId, route.amount, route.fee(false), randomBytes32(), Some(FullRoute(route.clearHops)))))) sender.expectMsg(PreimageReceived(paymentHash, paymentPreimage)) awaitCond(payFsm.stateName == PAYMENT_SUCCEEDED) sender.watch(payFsm) - childPayFsm.send(payFsm, PaymentFailed(failedId, paymentHash, Seq(RemoteFailure(failedRoute.amount, failedRoute.clearHops, Sphinx.DecryptedFailurePacket(e, PaymentTimeout))))) + childPayFsm.send(payFsm, PaymentFailed(failedId, paymentHash, Seq(RemoteFailure(failedRoute.amount, FullRoute(failedRoute.clearHops), Sphinx.DecryptedFailurePacket(e, PaymentTimeout))))) val result = sender.expectMsgType[PaymentSent] assert(result.parts.length == 1 && result.parts.head.id == childId) assert(result.amountWithFees < finalAmount) // we got the preimage without paying the full amount @@ -603,7 +603,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS assert(pending.size == childCount) val partialPayments = pending.map { - case (childId, route) => PaymentSent.PartialPayment(childId, route.amount, route.fee(false), randomBytes32(), Some(route.clearHops)) + case (childId, route) => PaymentSent.PartialPayment(childId, route.amount, route.fee(false), randomBytes32(), Some(FullRoute(route.clearHops))) } partialPayments.foreach(pp => childPayFsm.send(payFsm, PaymentSent(cfg.id, paymentHash, paymentPreimage, finalAmount, e, Seq(pp)))) sender.expectMsg(PreimageReceived(paymentHash, paymentPreimage)) @@ -634,7 +634,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS assert(payFsm.stateData.asInstanceOf[PaymentAborted].pending.size == pending.size - 1) // Fail all remaining child payments. payFsm.stateData.asInstanceOf[PaymentAborted].pending.foreach(childId => - childPayFsm.send(payFsm, PaymentFailed(childId, paymentHash, Seq(RemoteFailure(pending(childId).amount, hop_ab_1 :: hop_be :: Nil, Sphinx.DecryptedFailurePacket(e, PaymentTimeout))))) + childPayFsm.send(payFsm, PaymentFailed(childId, paymentHash, Seq(RemoteFailure(pending(childId).amount, FullRoute(hop_ab_1 :: hop_be :: Nil), Sphinx.DecryptedFailurePacket(e, PaymentTimeout))))) ) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala index a811af4f30..c44541cfab 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala @@ -335,7 +335,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val id = sender.expectMsgType[UUID] val fail = sender.expectMsgType[PaymentFailed] assert(fail.id == id) - assert(fail.failures == LocalFailure(finalAmount, Nil, PaymentError.TrampolineLegacyAmountLessInvoice) :: Nil) + assert(fail.failures == LocalFailure(finalAmount, FullRoute.empty, PaymentError.TrampolineLegacyAmountLessInvoice) :: Nil) multiPartPayFsm.expectNoMessage(50 millis) payFsm.expectNoMessage(50 millis) @@ -373,7 +373,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike sender.expectMsgType[PaymentIsPending] // Simulate a failure which should trigger a retry. - multiPartPayFsm.send(initiator, PaymentFailed(cfg.parentId, invoice.paymentHash, Seq(RemoteFailure(msg1.totalAmount, Nil, Sphinx.DecryptedFailurePacket(b, TrampolineFeeInsufficient))))) + multiPartPayFsm.send(initiator, PaymentFailed(cfg.parentId, invoice.paymentHash, Seq(RemoteFailure(msg1.totalAmount, FullRoute.empty, Sphinx.DecryptedFailurePacket(b, TrampolineFeeInsufficient))))) multiPartPayFsm.expectMsgType[SendPaymentConfig] val msg2 = multiPartPayFsm.expectMsgType[SendMultiPartPayment] assert(msg2.totalAmount == finalAmount + 25000.msat) @@ -405,13 +405,13 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(msg1.totalAmount == finalAmount + 21000.msat) // Simulate a failure which should trigger a retry. - multiPartPayFsm.send(initiator, PaymentFailed(cfg.parentId, invoice.paymentHash, Seq(RemoteFailure(msg1.totalAmount, Nil, Sphinx.DecryptedFailurePacket(b, TrampolineFeeInsufficient))))) + multiPartPayFsm.send(initiator, PaymentFailed(cfg.parentId, invoice.paymentHash, Seq(RemoteFailure(msg1.totalAmount, FullRoute.empty, Sphinx.DecryptedFailurePacket(b, TrampolineFeeInsufficient))))) multiPartPayFsm.expectMsgType[SendPaymentConfig] val msg2 = multiPartPayFsm.expectMsgType[SendMultiPartPayment] assert(msg2.totalAmount == finalAmount + 25000.msat) // Simulate a failure that exhausts payment attempts. - val failed = PaymentFailed(cfg.parentId, invoice.paymentHash, Seq(RemoteFailure(msg2.totalAmount, Nil, Sphinx.DecryptedFailurePacket(b, TemporaryNodeFailure)))) + val failed = PaymentFailed(cfg.parentId, invoice.paymentHash, Seq(RemoteFailure(msg2.totalAmount, FullRoute.empty, Sphinx.DecryptedFailurePacket(b, TemporaryNodeFailure)))) multiPartPayFsm.send(initiator, failed) sender.expectMsg(failed) eventListener.expectMsg(failed) @@ -431,7 +431,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val msg1 = multiPartPayFsm.expectMsgType[SendMultiPartPayment] assert(msg1.totalAmount == finalAmount + 21000.msat) // Trampoline node couldn't find a route for the given fee. - val failed = PaymentFailed(cfg.parentId, invoice.paymentHash, Seq(RemoteFailure(msg1.totalAmount, Nil, Sphinx.DecryptedFailurePacket(b, TrampolineFeeInsufficient)))) + val failed = PaymentFailed(cfg.parentId, invoice.paymentHash, Seq(RemoteFailure(msg1.totalAmount, FullRoute.empty, Sphinx.DecryptedFailurePacket(b, TrampolineFeeInsufficient)))) multiPartPayFsm.send(initiator, failed) multiPartPayFsm.expectMsgType[SendPaymentConfig] val msg2 = multiPartPayFsm.expectMsgType[SendMultiPartPayment] @@ -440,7 +440,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike multiPartPayFsm.send(initiator, failed) val failure = sender.expectMsgType[PaymentFailed] - assert(failure.failures == Seq(LocalFailure(finalAmount, Seq(NodeHop(nodeParams.nodeId, b, nodeParams.channelConf.expiryDelta, 0 msat), NodeHop(b, c, CltvExpiryDelta(24), 25000 msat)), RouteNotFound))) + assert(failure.failures == Seq(LocalFailure(finalAmount, FullRoute(Seq(NodeHop(nodeParams.nodeId, b, nodeParams.channelConf.expiryDelta, 0 msat), NodeHop(b, c, CltvExpiryDelta(24), 25000 msat))), RouteNotFound))) eventListener.expectMsg(failure) sender.expectNoMessage(100 millis) eventListener.expectNoMessage(100 millis) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala index 30cef8d07f..8e2efbcaa7 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala @@ -118,7 +118,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val ps = sender.expectMsgType[PaymentSent] assert(ps.id == parentId) - assert(ps.parts.head.route.contains(route.clearHops)) + assert(ps.parts.head.route.contains(FullRoute(route.clearHops))) awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status.isInstanceOf[OutgoingPaymentStatus.Succeeded])) metricsListener.expectNoMessage() @@ -230,7 +230,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) routerForwarder.forward(routerFixture.router, routeRequest) - assert(sender.expectMsgType[PaymentFailed].failures == LocalFailure(defaultAmountMsat, Nil, RouteNotFound) :: Nil) + assert(sender.expectMsgType[PaymentFailed].failures == LocalFailure(defaultAmountMsat, FullRoute.empty, RouteNotFound) :: Nil) awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status.isInstanceOf[OutgoingPaymentStatus.Failed])) val metrics = metricsListener.expectMsgType[PathFindingExperimentMetrics] @@ -262,7 +262,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]]) routerForwarder.forward(routerFixture.router, routeRequest) - val Seq(LocalFailure(_, Nil, RouteNotFound)) = sender.expectMsgType[PaymentFailed].failures + val Seq(LocalFailure(_, FullRoute.empty, RouteNotFound)) = sender.expectMsgType[PaymentFailed].failures awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status.isInstanceOf[OutgoingPaymentStatus.Failed])) val metrics = metricsListener.expectMsgType[PathFindingExperimentMetrics] @@ -327,7 +327,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFail(UpdateFailHtlc(ByteVector32.Zeroes, 0, defaultPaymentHash)))) // unparsable message // we allow 2 tries, so we send a 2nd request to the router - assert(sender.expectMsgType[PaymentFailed].failures == UnreadableRemoteFailure(route.amount, route.clearHops) :: UnreadableRemoteFailure(route.amount, route.clearHops) :: Nil) + assert(sender.expectMsgType[PaymentFailed].failures == UnreadableRemoteFailure(route.amount, FullRoute(route.clearHops)) :: UnreadableRemoteFailure(route.amount, FullRoute(route.clearHops)) :: Nil) awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status.isInstanceOf[OutgoingPaymentStatus.Failed])) // after last attempt the payment is failed val metrics = metricsListener.expectMsgType[PathFindingExperimentMetrics] @@ -478,7 +478,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { routerForwarder.expectMsg(defaultRouteRequest(a, defaultInvoice.recipients, cfg).copy(ignore = Ignore(Set.empty, Set(ChannelDesc(update_bc.shortChannelId, b, c))))) routerForwarder.forward(routerFixture.router) // we allow 2 tries, so we send a 2nd request to the router - assert(sender.expectMsgType[PaymentFailed].failures == RemoteFailure(route.amount, route.clearHops, Sphinx.DecryptedFailurePacket(b, failure)) :: LocalFailure(defaultAmountMsat, Nil, RouteNotFound) :: Nil) + assert(sender.expectMsgType[PaymentFailed].failures == RemoteFailure(route.amount, FullRoute(route.clearHops), Sphinx.DecryptedFailurePacket(b, failure)) :: LocalFailure(defaultAmountMsat, FullRoute.empty, RouteNotFound) :: Nil) } test("payment failed (Update)") { routerFixture => @@ -530,7 +530,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { routerForwarder.forward(routerFixture.router) // this time the router can't find a route: game over - assert(sender.expectMsgType[PaymentFailed].failures == RemoteFailure(route1.amount, route1.clearHops, Sphinx.DecryptedFailurePacket(b, failure)) :: RemoteFailure(route2.amount, route2.clearHops, Sphinx.DecryptedFailurePacket(b, failure2)) :: LocalFailure(defaultAmountMsat, Nil, RouteNotFound) :: Nil) + assert(sender.expectMsgType[PaymentFailed].failures == RemoteFailure(route1.amount, FullRoute(route1.clearHops), Sphinx.DecryptedFailurePacket(b, failure)) :: RemoteFailure(route2.amount, FullRoute(route2.clearHops), Sphinx.DecryptedFailurePacket(b, failure2)) :: LocalFailure(defaultAmountMsat, FullRoute.empty, RouteNotFound) :: Nil) awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status.isInstanceOf[OutgoingPaymentStatus.Failed])) routerForwarder.expectNoMessage(100 millis) @@ -657,7 +657,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { routerForwarder.forward(router) // we allow 2 tries, so we send a 2nd request to the router, which won't find another route - assert(sender.expectMsgType[PaymentFailed].failures == RemoteFailure(route1.amount, route1.clearHops, Sphinx.DecryptedFailurePacket(b, failure)) :: LocalFailure(defaultAmountMsat, Nil, RouteNotFound) :: Nil) + assert(sender.expectMsgType[PaymentFailed].failures == RemoteFailure(route1.amount, FullRoute(route1.clearHops), Sphinx.DecryptedFailurePacket(b, failure)) :: LocalFailure(defaultAmountMsat, FullRoute.empty, RouteNotFound) :: Nil) awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status.isInstanceOf[OutgoingPaymentStatus.Failed])) } @@ -764,16 +764,16 @@ class PaymentLifecycleSpec extends BaseRouterSpec { test("filter errors properly") { () => val failures = Seq( - LocalFailure(defaultAmountMsat, Nil, RouteNotFound), - RemoteFailure(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: Nil, Sphinx.DecryptedFailurePacket(a, TemporaryNodeFailure)), - LocalFailure(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: Nil, ChannelUnavailable(ByteVector32.Zeroes)), - LocalFailure(defaultAmountMsat, Nil, RouteNotFound) + LocalFailure(defaultAmountMsat, FullRoute.empty, RouteNotFound), + RemoteFailure(defaultAmountMsat, FullRoute(channelHopFromUpdate(a, b, update_ab) :: Nil), Sphinx.DecryptedFailurePacket(a, TemporaryNodeFailure)), + LocalFailure(defaultAmountMsat, FullRoute(channelHopFromUpdate(a, b, update_ab) :: Nil), ChannelUnavailable(ByteVector32.Zeroes)), + LocalFailure(defaultAmountMsat, FullRoute.empty, RouteNotFound) ) val filtered = PaymentFailure.transformForUser(failures) val expected = Seq( - LocalFailure(defaultAmountMsat, Nil, RouteNotFound), - RemoteFailure(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: Nil, Sphinx.DecryptedFailurePacket(a, TemporaryNodeFailure)), - LocalFailure(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: Nil, ChannelUnavailable(ByteVector32.Zeroes)) + LocalFailure(defaultAmountMsat, FullRoute.empty, RouteNotFound), + RemoteFailure(defaultAmountMsat, FullRoute(channelHopFromUpdate(a, b, update_ab) :: Nil), Sphinx.DecryptedFailurePacket(a, TemporaryNodeFailure)), + LocalFailure(defaultAmountMsat, FullRoute(channelHopFromUpdate(a, b, update_ab) :: Nil), ChannelUnavailable(ByteVector32.Zeroes)) ) assert(filtered == expected) } @@ -782,20 +782,20 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val route_abcd = channelHopFromUpdate(a, b, update_ab) :: channelHopFromUpdate(b, c, update_bc) :: channelHopFromUpdate(c, d, update_cd) :: Nil val testCases = Seq( // local failures -> ignore first channel if there is one - (LocalFailure(defaultAmountMsat, Nil, RouteNotFound), Set.empty, Set.empty), - (LocalFailure(defaultAmountMsat, NodeHop(a, b, CltvExpiryDelta(144), 0 msat) :: NodeHop(b, c, CltvExpiryDelta(144), 0 msat) :: Nil, RouteNotFound), Set.empty, Set.empty), - (LocalFailure(defaultAmountMsat, route_abcd, new RuntimeException("fatal")), Set.empty, Set(ChannelDesc(scid_ab, a, b))), + (LocalFailure(defaultAmountMsat, FullRoute.empty, RouteNotFound), Set.empty, Set.empty), + (LocalFailure(defaultAmountMsat, FullRoute(NodeHop(a, b, CltvExpiryDelta(144), 0 msat) :: NodeHop(b, c, CltvExpiryDelta(144), 0 msat) :: Nil), RouteNotFound), Set.empty, Set.empty), + (LocalFailure(defaultAmountMsat, FullRoute(route_abcd), new RuntimeException("fatal")), Set.empty, Set(ChannelDesc(scid_ab, a, b))), // remote failure from final recipient -> all intermediate nodes behaved correctly - (RemoteFailure(defaultAmountMsat, route_abcd, Sphinx.DecryptedFailurePacket(d, IncorrectOrUnknownPaymentDetails(100 msat, BlockHeight(42)))), Set.empty, Set.empty), + (RemoteFailure(defaultAmountMsat, FullRoute(route_abcd), Sphinx.DecryptedFailurePacket(d, IncorrectOrUnknownPaymentDetails(100 msat, BlockHeight(42)))), Set.empty, Set.empty), // remote failures from intermediate nodes -> depending on the failure, ignore either the failing node or its outgoing channel - (RemoteFailure(defaultAmountMsat, route_abcd, Sphinx.DecryptedFailurePacket(b, PermanentNodeFailure)), Set(b), Set.empty), - (RemoteFailure(defaultAmountMsat, route_abcd, Sphinx.DecryptedFailurePacket(c, TemporaryNodeFailure)), Set(c), Set.empty), - (RemoteFailure(defaultAmountMsat, route_abcd, Sphinx.DecryptedFailurePacket(b, PermanentChannelFailure)), Set.empty, Set(ChannelDesc(scid_bc, b, c))), - (RemoteFailure(defaultAmountMsat, route_abcd, Sphinx.DecryptedFailurePacket(c, UnknownNextPeer)), Set.empty, Set(ChannelDesc(scid_cd, c, d))), - (RemoteFailure(defaultAmountMsat, route_abcd, Sphinx.DecryptedFailurePacket(b, FeeInsufficient(100 msat, update_bc))), Set.empty, Set.empty), + (RemoteFailure(defaultAmountMsat, FullRoute(route_abcd), Sphinx.DecryptedFailurePacket(b, PermanentNodeFailure)), Set(b), Set.empty), + (RemoteFailure(defaultAmountMsat, FullRoute(route_abcd), Sphinx.DecryptedFailurePacket(c, TemporaryNodeFailure)), Set(c), Set.empty), + (RemoteFailure(defaultAmountMsat, FullRoute(route_abcd), Sphinx.DecryptedFailurePacket(b, PermanentChannelFailure)), Set.empty, Set(ChannelDesc(scid_bc, b, c))), + (RemoteFailure(defaultAmountMsat, FullRoute(route_abcd), Sphinx.DecryptedFailurePacket(c, UnknownNextPeer)), Set.empty, Set(ChannelDesc(scid_cd, c, d))), + (RemoteFailure(defaultAmountMsat, FullRoute(route_abcd), Sphinx.DecryptedFailurePacket(b, FeeInsufficient(100 msat, update_bc))), Set.empty, Set.empty), // unreadable remote failures -> blacklist all nodes except our direct peer and the final recipient - (UnreadableRemoteFailure(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: Nil), Set.empty, Set.empty), - (UnreadableRemoteFailure(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: channelHopFromUpdate(b, c, update_bc) :: channelHopFromUpdate(c, d, update_cd) :: ChannelHop(ShortChannelId(5656986L), d, e, null) :: Nil), Set(c, d), Set.empty) + (UnreadableRemoteFailure(defaultAmountMsat, FullRoute(channelHopFromUpdate(a, b, update_ab) :: Nil)), Set.empty, Set.empty), + (UnreadableRemoteFailure(defaultAmountMsat, FullRoute(channelHopFromUpdate(a, b, update_ab) :: channelHopFromUpdate(b, c, update_bc) :: channelHopFromUpdate(c, d, update_cd) :: ChannelHop(ShortChannelId(5656986L), d, e, null) :: Nil)), Set(c, d), Set.empty) ) for ((failure, expectedNodes, expectedChannels) <- testCases) { @@ -805,9 +805,9 @@ class PaymentLifecycleSpec extends BaseRouterSpec { } val failures = Seq( - RemoteFailure(defaultAmountMsat, route_abcd, Sphinx.DecryptedFailurePacket(c, TemporaryNodeFailure)), - RemoteFailure(defaultAmountMsat, route_abcd, Sphinx.DecryptedFailurePacket(b, UnknownNextPeer)), - LocalFailure(defaultAmountMsat, route_abcd, new RuntimeException("fatal")) + RemoteFailure(defaultAmountMsat, FullRoute(route_abcd), Sphinx.DecryptedFailurePacket(c, TemporaryNodeFailure)), + RemoteFailure(defaultAmountMsat, FullRoute(route_abcd), Sphinx.DecryptedFailurePacket(b, UnknownNextPeer)), + LocalFailure(defaultAmountMsat, FullRoute(route_abcd), new RuntimeException("fatal")) ) val ignore = PaymentFailure.updateIgnored(failures, Ignore.empty) assert(ignore.nodes == Set(c)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/NodeRelayerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/NodeRelayerSpec.scala index fcb88db86b..5730e8b388 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/NodeRelayerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/NodeRelayerSpec.scala @@ -547,7 +547,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl val nodeRelayerAdapters = mockPayFSM.expectMessageType[SendMultiPartPayment].replyTo // The proposed fees are low, so we ask the sender to raise them. - nodeRelayerAdapters ! PaymentFailed(relayId, paymentHash, LocalFailure(outgoingAmount, Nil, BalanceTooLow) :: Nil) + nodeRelayerAdapters ! PaymentFailed(relayId, paymentHash, LocalFailure(outgoingAmount, FullRoute.empty, BalanceTooLow) :: Nil) incomingMultiPart.foreach { p => val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] assert(fwd.channelId == p.add.channelId) @@ -593,7 +593,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl router.expectMessageType[RouteRequest] // If we're having a hard time finding routes, raising the fee/cltv will likely help. - val failures = LocalFailure(outgoingAmount, Nil, RouteNotFound) :: RemoteFailure(outgoingAmount, Nil, Sphinx.DecryptedFailurePacket(outgoingNodeId, PermanentNodeFailure)) :: LocalFailure(outgoingAmount, Nil, RouteNotFound) :: Nil + val failures = LocalFailure(outgoingAmount, FullRoute.empty, RouteNotFound) :: RemoteFailure(outgoingAmount, FullRoute.empty, Sphinx.DecryptedFailurePacket(outgoingNodeId, PermanentNodeFailure)) :: LocalFailure(outgoingAmount, FullRoute.empty, RouteNotFound) :: Nil payFSM ! PaymentFailed(relayId, incomingMultiPart.head.add.paymentHash, failures) incomingMultiPart.foreach { p => @@ -615,7 +615,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl val payFSM = mockPayFSM.expectMessageType[akka.actor.ActorRef] router.expectMessageType[RouteRequest] - val failures = RemoteFailure(outgoingAmount, Nil, Sphinx.DecryptedFailurePacket(outgoingNodeId, FinalIncorrectHtlcAmount(42 msat))) :: UnreadableRemoteFailure(outgoingAmount, Nil) :: Nil + val failures = RemoteFailure(outgoingAmount, FullRoute.empty, Sphinx.DecryptedFailurePacket(outgoingNodeId, FinalIncorrectHtlcAmount(42 msat))) :: UnreadableRemoteFailure(outgoingAmount, FullRoute.empty) :: Nil payFSM ! PaymentFailed(relayId, incomingMultiPart.head.add.paymentHash, failures) incomingMultiPart.foreach { p =>