1
0
Fork 0
mirror of https://github.com/ACINQ/eclair.git synced 2025-03-04 09:58:02 +01:00

Trampoline/MPP API changes (#1297)

Let a sender manually split a payment and specify a trampoline route.

Fix two flaky tests where the order of payment parts could be
different, resulting in a failed equality test.

If we're relaying multiple HTLCs for the same payment_hash,
we need to list all of those.
The previous code only handled that when Trampoline was used.
This commit is contained in:
Bastien Teinturier 2020-01-31 11:52:15 +01:00 committed by GitHub
parent 78e6cdbec9
commit 48ad9b30e6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 282 additions and 170 deletions

View file

@ -33,7 +33,7 @@ import fr.acinq.eclair.io.{NodeURI, Peer}
import fr.acinq.eclair.payment._ import fr.acinq.eclair.payment._
import fr.acinq.eclair.payment.receive.MultiPartHandler.ReceivePayment import fr.acinq.eclair.payment.receive.MultiPartHandler.ReceivePayment
import fr.acinq.eclair.payment.relay.Relayer.{GetOutgoingChannels, OutgoingChannels, UsableBalance} import fr.acinq.eclair.payment.relay.Relayer.{GetOutgoingChannels, OutgoingChannels, UsableBalance}
import fr.acinq.eclair.payment.send.PaymentInitiator.{SendPaymentRequest, SendTrampolinePaymentRequest} import fr.acinq.eclair.payment.send.PaymentInitiator.{SendPaymentRequest, SendPaymentToRouteRequest, SendPaymentToRouteResponse}
import fr.acinq.eclair.router._ import fr.acinq.eclair.router._
import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate, NodeAddress, NodeAnnouncement} import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate, NodeAddress, NodeAnnouncement}
import scodec.bits.ByteVector import scodec.bits.ByteVector
@ -88,13 +88,11 @@ trait Eclair {
def send(externalId_opt: Option[String], recipientNodeId: PublicKey, amount: MilliSatoshi, paymentHash: ByteVector32, invoice_opt: Option[PaymentRequest] = None, maxAttempts_opt: Option[Int] = None, feeThresholdSat_opt: Option[Satoshi] = None, maxFeePct_opt: Option[Double] = None)(implicit timeout: Timeout): Future[UUID] def send(externalId_opt: Option[String], recipientNodeId: PublicKey, amount: MilliSatoshi, paymentHash: ByteVector32, invoice_opt: Option[PaymentRequest] = None, maxAttempts_opt: Option[Int] = None, feeThresholdSat_opt: Option[Satoshi] = None, maxFeePct_opt: Option[Double] = None)(implicit timeout: Timeout): Future[UUID]
def sendToTrampoline(invoice: PaymentRequest, trampolineId: PublicKey, trampolineFees: MilliSatoshi, trampolineExpiryDelta: CltvExpiryDelta)(implicit timeout: Timeout): Future[UUID]
def sentInfo(id: Either[UUID, ByteVector32])(implicit timeout: Timeout): Future[Seq[OutgoingPayment]] def sentInfo(id: Either[UUID, ByteVector32])(implicit timeout: Timeout): Future[Seq[OutgoingPayment]]
def findRoute(targetNodeId: PublicKey, amount: MilliSatoshi, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty)(implicit timeout: Timeout): Future[RouteResponse] def findRoute(targetNodeId: PublicKey, amount: MilliSatoshi, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty)(implicit timeout: Timeout): Future[RouteResponse]
def sendToRoute(externalId_opt: Option[String], route: Seq[PublicKey], amount: MilliSatoshi, paymentHash: ByteVector32, finalCltvExpiryDelta: CltvExpiryDelta, invoice_opt: Option[PaymentRequest] = None)(implicit timeout: Timeout): Future[UUID] def sendToRoute(amount: MilliSatoshi, recipientAmount_opt: Option[MilliSatoshi], externalId_opt: Option[String], parentId_opt: Option[UUID], invoice: PaymentRequest, finalCltvExpiryDelta: CltvExpiryDelta, route: Seq[PublicKey], trampolineSecret_opt: Option[ByteVector32] = None, trampolineFees_opt: Option[MilliSatoshi] = None, trampolineExpiryDelta_opt: Option[CltvExpiryDelta] = None, trampolineNodes_opt: Seq[PublicKey] = Nil)(implicit timeout: Timeout): Future[SendPaymentToRouteResponse]
def audit(from_opt: Option[Long], to_opt: Option[Long])(implicit timeout: Timeout): Future[AuditResponse] def audit(from_opt: Option[Long], to_opt: Option[Long])(implicit timeout: Timeout): Future[AuditResponse]
@ -214,10 +212,21 @@ class EclairImpl(appKit: Kit) extends Eclair {
(appKit.router ? RouteRequest(appKit.nodeParams.nodeId, targetNodeId, amount, assistedRoutes)).mapTo[RouteResponse] (appKit.router ? RouteRequest(appKit.nodeParams.nodeId, targetNodeId, amount, assistedRoutes)).mapTo[RouteResponse]
} }
override def sendToRoute(externalId_opt: Option[String], route: Seq[PublicKey], amount: MilliSatoshi, paymentHash: ByteVector32, finalCltvExpiryDelta: CltvExpiryDelta, invoice_opt: Option[PaymentRequest] = None)(implicit timeout: Timeout): Future[UUID] = { override def sendToRoute(amount: MilliSatoshi, recipientAmount_opt: Option[MilliSatoshi], externalId_opt: Option[String], parentId_opt: Option[UUID], invoice: PaymentRequest, finalCltvExpiryDelta: CltvExpiryDelta, route: Seq[PublicKey], trampolineSecret_opt: Option[ByteVector32], trampolineFees_opt: Option[MilliSatoshi], trampolineExpiryDelta_opt: Option[CltvExpiryDelta], trampolineNodes_opt: Seq[PublicKey])(implicit timeout: Timeout): Future[SendPaymentToRouteResponse] = {
externalId_opt match { val recipientAmount = recipientAmount_opt.getOrElse(invoice.amount.getOrElse(amount))
case Some(externalId) if externalId.length > externalIdMaxLength => Future.failed(new IllegalArgumentException("externalId is too long: cannot exceed 66 characters")) val sendPayment = SendPaymentToRouteRequest(amount, recipientAmount, externalId_opt, parentId_opt, invoice, finalCltvExpiryDelta, route, trampolineSecret_opt, trampolineFees_opt.getOrElse(0 msat), trampolineExpiryDelta_opt.getOrElse(CltvExpiryDelta(0)), trampolineNodes_opt)
case _ => (appKit.paymentInitiator ? SendPaymentRequest(amount, paymentHash, route.last, 1, finalCltvExpiryDelta, invoice_opt, externalId_opt, route)).mapTo[UUID] if (invoice.isExpired) {
Future.failed(new IllegalArgumentException("invoice has expired"))
} else if (route.isEmpty) {
Future.failed(new IllegalArgumentException("missing payment route"))
} else if (externalId_opt.exists(_.length > externalIdMaxLength)) {
Future.failed(new IllegalArgumentException(s"externalId is too long: cannot exceed $externalIdMaxLength characters"))
} else if (trampolineNodes_opt.nonEmpty && (trampolineFees_opt.isEmpty || trampolineExpiryDelta_opt.isEmpty)) {
Future.failed(new IllegalArgumentException("trampoline payments must specify a trampoline fee and cltv delta"))
} else if (trampolineNodes_opt.nonEmpty && trampolineNodes_opt.length != 2) {
Future.failed(new IllegalArgumentException("trampoline payments currently only support paying a trampoline node via a single other trampoline node"))
} else {
(appKit.paymentInitiator ? sendPayment).mapTo[SendPaymentToRouteResponse]
} }
} }
@ -230,7 +239,7 @@ class EclairImpl(appKit: Kit) extends Eclair {
) )
externalId_opt match { externalId_opt match {
case Some(externalId) if externalId.length > externalIdMaxLength => Future.failed(new IllegalArgumentException("externalId is too long: cannot exceed 66 characters")) case Some(externalId) if externalId.length > externalIdMaxLength => Future.failed(new IllegalArgumentException(s"externalId is too long: cannot exceed $externalIdMaxLength characters"))
case _ => invoice_opt match { case _ => invoice_opt match {
case Some(invoice) if invoice.isExpired => Future.failed(new IllegalArgumentException("invoice has expired")) case Some(invoice) if invoice.isExpired => Future.failed(new IllegalArgumentException("invoice has expired"))
case Some(invoice) => case Some(invoice) =>
@ -246,12 +255,6 @@ class EclairImpl(appKit: Kit) extends Eclair {
} }
} }
override def sendToTrampoline(invoice: PaymentRequest, trampolineId: PublicKey, trampolineFees: MilliSatoshi, trampolineExpiryDelta: CltvExpiryDelta)(implicit timeout: Timeout): Future[UUID] = {
val defaultRouteParams = Router.getDefaultRouteParams(appKit.nodeParams.routerConf)
val sendPayment = SendTrampolinePaymentRequest(invoice.amount.get, invoice, trampolineId, Seq((trampolineFees, trampolineExpiryDelta)), invoice.minFinalCltvExpiryDelta.getOrElse(Channel.MIN_CLTV_EXPIRY_DELTA), Some(defaultRouteParams))
(appKit.paymentInitiator ? sendPayment).mapTo[UUID]
}
override def sentInfo(id: Either[UUID, ByteVector32])(implicit timeout: Timeout): Future[Seq[OutgoingPayment]] = Future { override def sentInfo(id: Either[UUID, ByteVector32])(implicit timeout: Timeout): Future[Seq[OutgoingPayment]] = Future {
id match { id match {
case Left(uuid) => appKit.nodeParams.db.payments.listOutgoingPayments(uuid) case Left(uuid) => appKit.nodeParams.db.payments.listOutgoingPayments(uuid)

View file

@ -278,17 +278,19 @@ class SqliteAuditDb(sqlite: Connection) extends AuditDb with Logging {
rs.getLong("timestamp")) rs.getLong("timestamp"))
relayedByHash = relayedByHash + (paymentHash -> (relayedByHash.getOrElse(paymentHash, Nil) :+ part)) relayedByHash = relayedByHash + (paymentHash -> (relayedByHash.getOrElse(paymentHash, Nil) :+ part))
} }
relayedByHash.map { relayedByHash.flatMap {
case (paymentHash, parts) => parts.head.relayType match { case (paymentHash, parts) =>
case "channel" => parts.foldLeft(ChannelPaymentRelayed(0 msat, 0 msat, paymentHash, ByteVector32.Zeroes, ByteVector32.Zeroes, parts.head.timestamp)) { // We may have been routing multiple payments for the same payment_hash (MPP) in both cases (trampoline and channel).
case (e, part) if part.direction == "IN" => e.copy(amountIn = part.amount, fromChannelId = part.channelId) // NB: we may link the wrong in-out parts, but the overall sum will be correct: we sort by amounts to minimize the risk of mismatch.
case (e, part) if part.direction == "OUT" => e.copy(amountOut = part.amount, toChannelId = part.channelId) val incoming = parts.filter(_.direction == "IN").map(p => PaymentRelayed.Part(p.amount, p.channelId)).sortBy(_.amount)
val outgoing = parts.filter(_.direction == "OUT").map(p => PaymentRelayed.Part(p.amount, p.channelId)).sortBy(_.amount)
parts.headOption match {
case Some(RelayedPart(_, _, _, "channel", timestamp)) => incoming.zip(outgoing).map {
case (in, out) => ChannelPaymentRelayed(in.amount, out.amount, paymentHash, in.channelId, out.channelId, timestamp)
}
case Some(RelayedPart(_, _, _, "trampoline", timestamp)) => TrampolinePaymentRelayed(paymentHash, incoming, outgoing, timestamp) :: Nil
case _ => Nil
} }
case "trampoline" => parts.foldLeft(TrampolinePaymentRelayed(paymentHash, Nil, Nil, parts.head.timestamp)) {
case (e, part) if part.direction == "IN" => e.copy(incoming = e.incoming :+ PaymentRelayed.Part(part.amount, part.channelId))
case (e, part) if part.direction == "OUT" => e.copy(outgoing = e.outgoing :+ PaymentRelayed.Part(part.amount, part.channelId))
}
}
}.toSeq.sortBy(_.timestamp) }.toSeq.sortBy(_.timestamp)
} }

View file

@ -29,8 +29,8 @@ import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle.SendMultiPartPayme
import fr.acinq.eclair.payment.send.PaymentLifecycle.{SendPayment, SendPaymentToRoute} import fr.acinq.eclair.payment.send.PaymentLifecycle.{SendPayment, SendPaymentToRoute}
import fr.acinq.eclair.router.{ChannelHop, Hop, NodeHop, RouteParams} import fr.acinq.eclair.router.{ChannelHop, Hop, NodeHop, RouteParams}
import fr.acinq.eclair.wire.Onion.FinalLegacyPayload import fr.acinq.eclair.wire.Onion.FinalLegacyPayload
import fr.acinq.eclair.wire.{Onion, OnionTlv, TrampolineExpiryTooSoon, TrampolineFeeInsufficient} import fr.acinq.eclair.wire._
import fr.acinq.eclair.{CltvExpiryDelta, Features, LongToBtcAmount, MilliSatoshi, NodeParams, randomBytes32} import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Features, LongToBtcAmount, MilliSatoshi, NodeParams, randomBytes32}
/** /**
* Created by PM on 29/08/2016. * Created by PM on 29/08/2016.
@ -52,19 +52,14 @@ class PaymentInitiator(nodeParams: NodeParams, router: ActorRef, relayer: ActorR
sender ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(InvalidInvoice(s"unknown invoice features (${invoice.features})")) :: Nil) sender ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(InvalidInvoice(s"unknown invoice features (${invoice.features})")) :: Nil)
case Some(invoice) if invoice.features.allowMultiPart && Features.hasFeature(nodeParams.features, Features.BasicMultiPartPayment) => case Some(invoice) if invoice.features.allowMultiPart && Features.hasFeature(nodeParams.features, Features.BasicMultiPartPayment) =>
invoice.paymentSecret match { invoice.paymentSecret match {
case Some(paymentSecret) => r.predefinedRoute match { case Some(paymentSecret) =>
case Nil => spawnMultiPartPaymentFsm(paymentCfg) forward SendMultiPartPayment(paymentSecret, r.recipientNodeId, r.recipientAmount, finalExpiry, r.maxAttempts, r.assistedRoutes, r.routeParams) spawnMultiPartPaymentFsm(paymentCfg) forward SendMultiPartPayment(paymentSecret, r.recipientNodeId, r.recipientAmount, finalExpiry, r.maxAttempts, r.assistedRoutes, r.routeParams)
case hops => spawnPaymentFsm(paymentCfg) forward SendPaymentToRoute(hops, Onion.createMultiPartPayload(r.recipientAmount, invoice.amount.getOrElse(r.recipientAmount), finalExpiry, paymentSecret)) case None =>
} sender ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(InvalidInvoice("multi-part invoice is missing a payment secret")) :: Nil)
case None => sender ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(InvalidInvoice("multi-part invoice is missing a payment secret")) :: Nil)
} }
case _ => case _ =>
val payFsm = spawnPaymentFsm(paymentCfg)
// NB: we only generate legacy payment onions for now for maximum compatibility. // NB: we only generate legacy payment onions for now for maximum compatibility.
r.predefinedRoute match { spawnPaymentFsm(paymentCfg) forward SendPayment(r.recipientNodeId, FinalLegacyPayload(r.recipientAmount, finalExpiry), r.maxAttempts, r.assistedRoutes, r.routeParams)
case Nil => payFsm forward SendPayment(r.recipientNodeId, FinalLegacyPayload(r.recipientAmount, finalExpiry), r.maxAttempts, r.assistedRoutes, r.routeParams)
case hops => payFsm forward SendPaymentToRoute(hops, FinalLegacyPayload(r.recipientAmount, finalExpiry))
}
} }
case r: SendTrampolinePaymentRequest => case r: SendTrampolinePaymentRequest =>
@ -101,18 +96,42 @@ class PaymentInitiator(nodeParams: NodeParams, router: ActorRef, relayer: ActorR
context.system.eventStream.publish(ps) context.system.eventStream.publish(ps)
context become main(pending - ps.id) context become main(pending - ps.id)
}) })
case r: SendPaymentToRouteRequest =>
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.paymentRequest), storeInDb = true, publishEvent = true, additionalHops)
val payFsm = spawnPaymentFsm(paymentCfg)
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}")
// We generate a random secret for the payment to the first trampoline node.
val trampolineSecret = r.trampolineSecret.getOrElse(randomBytes32)
sender ! SendPaymentToRouteResponse(paymentId, parentPaymentId, Some(trampolineSecret))
val (trampolineAmount, trampolineExpiry, trampolineOnion) = buildTrampolinePayment(SendTrampolinePaymentRequest(r.recipientAmount, r.paymentRequest, trampoline, Seq((r.trampolineFees, r.trampolineExpiryDelta)), r.finalExpiryDelta), r.trampolineFees, r.trampolineExpiryDelta)
payFsm forward SendPaymentToRoute(r.route, Onion.createMultiPartPayload(r.amount, trampolineAmount, trampolineExpiry, trampolineSecret, Seq(OnionTlv.TrampolineOnion(trampolineOnion))))
case Nil =>
sender ! SendPaymentToRouteResponse(paymentId, parentPaymentId, None)
r.paymentRequest.paymentSecret match {
case Some(paymentSecret) => payFsm forward SendPaymentToRoute(r.route, Onion.createMultiPartPayload(r.amount, r.recipientAmount, finalExpiry, paymentSecret))
case None => payFsm forward SendPaymentToRoute(r.route, FinalLegacyPayload(r.recipientAmount, finalExpiry))
}
case _ =>
sender ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(new IllegalArgumentException(s"unsupported number of trampoline nodes: ${r.trampolineNodes}")) :: Nil)
}
} }
def spawnPaymentFsm(paymentCfg: SendPaymentConfig): ActorRef = context.actorOf(PaymentLifecycle.props(nodeParams, paymentCfg, router, register)) def spawnPaymentFsm(paymentCfg: SendPaymentConfig): ActorRef = context.actorOf(PaymentLifecycle.props(nodeParams, paymentCfg, router, register))
def spawnMultiPartPaymentFsm(paymentCfg: SendPaymentConfig): ActorRef = context.actorOf(MultiPartPaymentLifecycle.props(nodeParams, paymentCfg, relayer, router, register)) def spawnMultiPartPaymentFsm(paymentCfg: SendPaymentConfig): ActorRef = context.actorOf(MultiPartPaymentLifecycle.props(nodeParams, paymentCfg, relayer, router, register))
private def sendTrampolinePayment(paymentId: UUID, r: SendTrampolinePaymentRequest, trampolineFees: MilliSatoshi, trampolineExpiryDelta: CltvExpiryDelta): Unit = { private def buildTrampolinePayment(r: SendTrampolinePaymentRequest, trampolineFees: MilliSatoshi, trampolineExpiryDelta: CltvExpiryDelta): (MilliSatoshi, CltvExpiry, OnionRoutingPacket) = {
val trampolineRoute = Seq( val trampolineRoute = Seq(
NodeHop(nodeParams.nodeId, r.trampolineNodeId, nodeParams.expiryDeltaBlocks, 0 msat), NodeHop(nodeParams.nodeId, r.trampolineNodeId, nodeParams.expiryDeltaBlocks, 0 msat),
NodeHop(r.trampolineNodeId, r.recipientNodeId, trampolineExpiryDelta, trampolineFees) // for now we only use a single trampoline hop NodeHop(r.trampolineNodeId, r.recipientNodeId, trampolineExpiryDelta, trampolineFees) // for now we only use a single trampoline hop
) )
val paymentCfg = SendPaymentConfig(paymentId, paymentId, None, r.paymentHash, r.recipientAmount, r.recipientNodeId, Upstream.Local(paymentId), Some(r.paymentRequest), storeInDb = true, publishEvent = false, trampolineRoute.tail)
val finalPayload = if (r.paymentRequest.features.allowMultiPart) { val finalPayload = if (r.paymentRequest.features.allowMultiPart) {
Onion.createMultiPartPayload(r.recipientAmount, r.recipientAmount, r.finalExpiry(nodeParams.currentBlockHeight), r.paymentRequest.paymentSecret.get) Onion.createMultiPartPayload(r.recipientAmount, r.recipientAmount, r.finalExpiry(nodeParams.currentBlockHeight), r.paymentRequest.paymentSecret.get)
} else { } else {
@ -124,9 +143,15 @@ class PaymentInitiator(nodeParams: NodeParams, router: ActorRef, relayer: ActorR
} else { } else {
OutgoingPacket.buildTrampolineToLegacyPacket(r.paymentRequest, trampolineRoute, finalPayload) OutgoingPacket.buildTrampolineToLegacyPacket(r.paymentRequest, trampolineRoute, finalPayload)
} }
(trampolineAmount, trampolineExpiry, trampolineOnion.packet)
}
private def sendTrampolinePayment(paymentId: UUID, r: SendTrampolinePaymentRequest, trampolineFees: MilliSatoshi, trampolineExpiryDelta: CltvExpiryDelta): Unit = {
val paymentCfg = SendPaymentConfig(paymentId, paymentId, None, r.paymentHash, r.recipientAmount, r.recipientNodeId, Upstream.Local(paymentId), Some(r.paymentRequest), storeInDb = true, publishEvent = false, 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. // We generate a random secret for this payment to avoid leaking the invoice secret to the first trampoline node.
val trampolineSecret = randomBytes32 val trampolineSecret = randomBytes32
spawnMultiPartPaymentFsm(paymentCfg) ! SendMultiPartPayment(trampolineSecret, r.trampolineNodeId, trampolineAmount, trampolineExpiry, 1, r.paymentRequest.routingInfo, r.routeParams, Seq(OnionTlv.TrampolineOnion(trampolineOnion.packet))) val (trampolineAmount, trampolineExpiry, trampolineOnion) = buildTrampolinePayment(r, trampolineFees, trampolineExpiryDelta)
spawnMultiPartPaymentFsm(paymentCfg) ! SendMultiPartPayment(trampolineSecret, r.trampolineNodeId, trampolineAmount, trampolineExpiry, 1, r.paymentRequest.routingInfo, r.routeParams, Seq(OnionTlv.TrampolineOnion(trampolineOnion)))
} }
} }
@ -173,7 +198,6 @@ object PaymentInitiator {
* @param finalExpiryDelta expiry delta for the final recipient. * @param finalExpiryDelta expiry delta for the final recipient.
* @param paymentRequest (optional) Bolt 11 invoice. * @param paymentRequest (optional) Bolt 11 invoice.
* @param externalId (optional) externally-controlled identifier (to reconcile between application DB and eclair DB). * @param externalId (optional) externally-controlled identifier (to reconcile between application DB and eclair DB).
* @param predefinedRoute (optional) route to use for the payment.
* @param assistedRoutes (optional) routing hints (usually from a Bolt 11 invoice). * @param assistedRoutes (optional) routing hints (usually from a Bolt 11 invoice).
* @param routeParams (optional) parameters to fine-tune the routing algorithm. * @param routeParams (optional) parameters to fine-tune the routing algorithm.
*/ */
@ -184,13 +208,74 @@ object PaymentInitiator {
finalExpiryDelta: CltvExpiryDelta = Channel.MIN_CLTV_EXPIRY_DELTA, finalExpiryDelta: CltvExpiryDelta = Channel.MIN_CLTV_EXPIRY_DELTA,
paymentRequest: Option[PaymentRequest] = None, paymentRequest: Option[PaymentRequest] = None,
externalId: Option[String] = None, externalId: Option[String] = None,
predefinedRoute: Seq[PublicKey] = Nil,
assistedRoutes: Seq[Seq[ExtraHop]] = Nil, assistedRoutes: Seq[Seq[ExtraHop]] = Nil,
routeParams: Option[RouteParams] = None) { routeParams: Option[RouteParams] = None) {
// We add one block in order to not have our htlcs fail when a new block has just been found. // We add one block in order to not have our htlcs fail when a new block has just been found.
def finalExpiry(currentBlockHeight: Long) = finalExpiryDelta.toCltvExpiry(currentBlockHeight + 1) def finalExpiry(currentBlockHeight: Long) = finalExpiryDelta.toCltvExpiry(currentBlockHeight + 1)
} }
/**
* 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 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.
* @param paymentRequest Bolt 11 invoice.
* @param finalExpiryDelta expiry delta for the final recipient.
* @param route route to use to reach either the final recipient or the first trampoline node.
* @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).
*/
case class SendPaymentToRouteRequest(amount: MilliSatoshi,
recipientAmount: MilliSatoshi,
externalId: Option[String],
parentId: Option[UUID],
paymentRequest: PaymentRequest,
finalExpiryDelta: CltvExpiryDelta = Channel.MIN_CLTV_EXPIRY_DELTA,
route: Seq[PublicKey],
trampolineSecret: Option[ByteVector32],
trampolineFees: MilliSatoshi,
trampolineExpiryDelta: CltvExpiryDelta,
trampolineNodes: Seq[PublicKey]) {
val recipientNodeId = paymentRequest.nodeId
val paymentHash = paymentRequest.paymentHash
// We add one block in order to not have our htlcs fail when a new block has just been found.
def finalExpiry(currentBlockHeight: Long) = finalExpiryDelta.toCltvExpiry(currentBlockHeight + 1)
}
/**
* @param paymentId id of the outgoing payment (mapped to a single outgoing HTLC).
* @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.
* @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.
*/
case class SendPaymentToRouteResponse(paymentId: UUID, parentId: UUID, trampolineSecret: Option[ByteVector32])
/** /**
* Configuration for an instance of a payment state machine. * Configuration for an instance of a payment state machine.
* *

View file

@ -16,6 +16,8 @@
package fr.acinq.eclair package fr.acinq.eclair
import java.util.UUID
import akka.actor.ActorSystem import akka.actor.ActorSystem
import akka.testkit.{TestKit, TestProbe} import akka.testkit.{TestKit, TestProbe}
import akka.util.Timeout import akka.util.Timeout
@ -30,7 +32,7 @@ import fr.acinq.eclair.payment.PaymentRequest
import fr.acinq.eclair.payment.PaymentRequest.ExtraHop import fr.acinq.eclair.payment.PaymentRequest.ExtraHop
import fr.acinq.eclair.payment.receive.MultiPartHandler.ReceivePayment import fr.acinq.eclair.payment.receive.MultiPartHandler.ReceivePayment
import fr.acinq.eclair.payment.receive.PaymentHandler import fr.acinq.eclair.payment.receive.PaymentHandler
import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentRequest import fr.acinq.eclair.payment.send.PaymentInitiator.{SendPaymentRequest, SendPaymentToRouteRequest}
import fr.acinq.eclair.router.RouteCalculationSpec.makeUpdate import fr.acinq.eclair.router.RouteCalculationSpec.makeUpdate
import fr.acinq.eclair.router.{Announcements, PublicChannel, Router, GetNetworkStats, NetworkStats, Stats} import fr.acinq.eclair.router.{Announcements, PublicChannel, Router, GetNetworkStats, NetworkStats, Stats}
import org.mockito.Mockito import org.mockito.Mockito
@ -313,18 +315,15 @@ class EclairImplSpec extends TestKit(ActorSystem("test")) with fixture.FunSuiteL
test("sendtoroute should pass the parameters correctly") { f => test("sendtoroute should pass the parameters correctly") { f =>
import f._ import f._
val route = Seq(PublicKey(hex"030bb6a5e0c6b203c7e2180fb78c7ba4bdce46126761d8201b91ddac089cdecc87"))
val eclair = new EclairImpl(kit) val eclair = new EclairImpl(kit)
val route = Seq(randomKey.publicKey)
val trampolines = Seq(randomKey.publicKey, randomKey.publicKey)
val parentId = UUID.randomUUID()
val secret = randomBytes32
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(1234 msat), ByteVector32.One, randomKey, "Some invoice") val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(1234 msat), ByteVector32.One, randomKey, "Some invoice")
eclair.sendToRoute(Some("42"), route, 1234 msat, ByteVector32.One, CltvExpiryDelta(123), Some(pr)) eclair.sendToRoute(1000 msat, Some(1200 msat), Some("42"), Some(parentId), pr, CltvExpiryDelta(123), route, Some(secret), Some(100 msat), Some(CltvExpiryDelta(144)), trampolines)
val send = paymentInitiator.expectMsgType[SendPaymentRequest] paymentInitiator.expectMsg(SendPaymentToRouteRequest(1000 msat, 1200 msat, Some("42"), Some(parentId), pr, CltvExpiryDelta(123), route, Some(secret), 100 msat, CltvExpiryDelta(144), trampolines))
assert(send.externalId === Some("42"))
assert(send.predefinedRoute === route)
assert(send.recipientAmount === 1234.msat)
assert(send.finalExpiryDelta === CltvExpiryDelta(123))
assert(send.paymentHash === ByteVector32.One)
assert(send.paymentRequest === Some(pr))
} }
} }

View file

@ -61,7 +61,10 @@ class SqliteAuditDbSpec extends FunSuite {
val e8 = ChannelLifecycleEvent(randomBytes32, randomKey.publicKey, 456123000 sat, isFunder = true, isPrivate = false, "mutual") val e8 = ChannelLifecycleEvent(randomBytes32, randomKey.publicKey, 456123000 sat, isFunder = true, isPrivate = false, "mutual")
val e9 = ChannelErrorOccurred(null, randomBytes32, randomKey.publicKey, null, LocalError(new RuntimeException("oops")), isFatal = true) val e9 = ChannelErrorOccurred(null, randomBytes32, randomKey.publicKey, null, LocalError(new RuntimeException("oops")), isFatal = true)
val e10 = ChannelErrorOccurred(null, randomBytes32, randomKey.publicKey, null, RemoteError(wire.Error(randomBytes32, "remote oops")), isFatal = true) val e10 = ChannelErrorOccurred(null, randomBytes32, randomKey.publicKey, null, RemoteError(wire.Error(randomBytes32, "remote oops")), isFatal = true)
val e11 = TrampolinePaymentRelayed(randomBytes32, Seq(PaymentRelayed.Part(20000 msat, randomBytes32), PaymentRelayed.Part(22000 msat, randomBytes32)), Seq(PaymentRelayed.Part(20000 msat, randomBytes32), PaymentRelayed.Part(10000 msat, randomBytes32), PaymentRelayed.Part(10000 msat, randomBytes32))) val e11 = TrampolinePaymentRelayed(randomBytes32, Seq(PaymentRelayed.Part(20000 msat, randomBytes32), PaymentRelayed.Part(22000 msat, randomBytes32)), Seq(PaymentRelayed.Part(10000 msat, randomBytes32), PaymentRelayed.Part(12000 msat, randomBytes32), PaymentRelayed.Part(15000 msat, randomBytes32)))
val multiPartPaymentHash = randomBytes32
val e12 = ChannelPaymentRelayed(13000 msat, 11000 msat, multiPartPaymentHash, randomBytes32, randomBytes32)
val e13 = ChannelPaymentRelayed(15000 msat, 12500 msat, multiPartPaymentHash, randomBytes32, randomBytes32)
db.add(e1) db.add(e1)
db.add(e2) db.add(e2)
@ -74,11 +77,13 @@ class SqliteAuditDbSpec extends FunSuite {
db.add(e9) db.add(e9)
db.add(e10) db.add(e10)
db.add(e11) db.add(e11)
db.add(e12)
db.add(e13)
assert(db.listSent(from = 0L, to = (Platform.currentTime.milliseconds + 15.minute).toMillis).toSet === Set(e1, e5, e6)) assert(db.listSent(from = 0L, to = (Platform.currentTime.milliseconds + 15.minute).toMillis).toSet === Set(e1, e5, e6))
assert(db.listSent(from = 100000L, to = (Platform.currentTime.milliseconds + 1.minute).toMillis).toList === List(e1)) assert(db.listSent(from = 100000L, to = (Platform.currentTime.milliseconds + 1.minute).toMillis).toList === List(e1))
assert(db.listReceived(from = 0L, to = (Platform.currentTime.milliseconds + 1.minute).toMillis).toList === List(e2)) assert(db.listReceived(from = 0L, to = (Platform.currentTime.milliseconds + 1.minute).toMillis).toList === List(e2))
assert(db.listRelayed(from = 0L, to = (Platform.currentTime.milliseconds + 1.minute).toMillis).toList === List(e3, e11)) assert(db.listRelayed(from = 0L, to = (Platform.currentTime.milliseconds + 1.minute).toMillis).toList === List(e3, e11, e12, e13))
assert(db.listNetworkFees(from = 0L, to = (Platform.currentTime.milliseconds + 1.minute).toMillis).size === 1) assert(db.listNetworkFees(from = 0L, to = (Platform.currentTime.milliseconds + 1.minute).toMillis).size === 1)
assert(db.listNetworkFees(from = 0L, to = (Platform.currentTime.milliseconds + 1.minute).toMillis).head.txType === "mutual") assert(db.listNetworkFees(from = 0L, to = (Platform.currentTime.milliseconds + 1.minute).toMillis).head.txType === "mutual")
} }
@ -343,7 +348,7 @@ class SqliteAuditDbSpec extends FunSuite {
PaymentSent.PartialPayment(UUID.randomUUID(), 500 msat, 10 msat, randomBytes32, None, 160), PaymentSent.PartialPayment(UUID.randomUUID(), 500 msat, 10 msat, randomBytes32, None, 160),
PaymentSent.PartialPayment(UUID.randomUUID(), 600 msat, 5 msat, randomBytes32, None, 165) PaymentSent.PartialPayment(UUID.randomUUID(), 600 msat, 5 msat, randomBytes32, None, 165)
)) ))
val relayed3 = TrampolinePaymentRelayed(randomBytes32, Seq(PaymentRelayed.Part(500 msat, randomBytes32), PaymentRelayed.Part(450 msat, randomBytes32)), Seq(PaymentRelayed.Part(800 msat, randomBytes32)), 150) val relayed3 = TrampolinePaymentRelayed(randomBytes32, Seq(PaymentRelayed.Part(450 msat, randomBytes32), PaymentRelayed.Part(500 msat, randomBytes32)), Seq(PaymentRelayed.Part(800 msat, randomBytes32)), 150)
postMigrationDb.add(ps2) postMigrationDb.add(ps2)
assert(postMigrationDb.listSent(155, 200) === Seq(ps2)) assert(postMigrationDb.listSent(155, 200) === Seq(ps2))
@ -351,7 +356,7 @@ class SqliteAuditDbSpec extends FunSuite {
assert(postMigrationDb.listRelayed(100, 160) === Seq(relayed1, relayed2, relayed3)) assert(postMigrationDb.listRelayed(100, 160) === Seq(relayed1, relayed2, relayed3))
} }
test("fails if the DB contains invalid values") { test("ignore invalid values in the DB") {
val sqlite = TestConstants.sqliteInMemory() val sqlite = TestConstants.sqliteInMemory()
val db = new SqliteAuditDb(sqlite) val db = new SqliteAuditDb(sqlite)
@ -365,8 +370,6 @@ class SqliteAuditDbSpec extends FunSuite {
statement.executeUpdate() statement.executeUpdate()
} }
assertThrows[MatchError](db.listRelayed(5, 15))
using(sqlite.prepareStatement("INSERT INTO relayed (payment_hash, amount_msat, channel_id, direction, relay_type, timestamp) VALUES (?, ?, ?, ?, ?, ?)")) { statement => using(sqlite.prepareStatement("INSERT INTO relayed (payment_hash, amount_msat, channel_id, direction, relay_type, timestamp) VALUES (?, ?, ?, ?, ?, ?)")) { statement =>
statement.setBytes(1, randomBytes32.toArray) statement.setBytes(1, randomBytes32.toArray)
statement.setLong(2, 51) statement.setLong(2, 51)
@ -377,8 +380,6 @@ class SqliteAuditDbSpec extends FunSuite {
statement.executeUpdate() statement.executeUpdate()
} }
assertThrows[MatchError](db.listRelayed(15, 25))
val paymentHash = randomBytes32 val paymentHash = randomBytes32
val channelId = randomBytes32 val channelId = randomBytes32
@ -386,13 +387,13 @@ class SqliteAuditDbSpec extends FunSuite {
statement.setBytes(1, paymentHash.toArray) statement.setBytes(1, paymentHash.toArray)
statement.setLong(2, 65) statement.setLong(2, 65)
statement.setBytes(3, channelId.toArray) statement.setBytes(3, channelId.toArray)
statement.setString(4, "IN") statement.setString(4, "IN") // missing a corresponding OUT
statement.setString(5, "channel") statement.setString(5, "channel")
statement.setLong(6, 30) statement.setLong(6, 30)
statement.executeUpdate() statement.executeUpdate()
} }
assert(db.listRelayed(25, 35) === Seq(ChannelPaymentRelayed(65 msat, 0 msat, paymentHash, channelId, ByteVector32.Zeroes, 30))) assert(db.listRelayed(0, 40) === Nil)
} }
} }

View file

@ -451,24 +451,26 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService
sender.send(nodes("B").paymentInitiator, SendPaymentRequest(amount, pr.paymentHash, nodes("D").nodeParams.nodeId, 5, paymentRequest = Some(pr))) sender.send(nodes("B").paymentInitiator, SendPaymentRequest(amount, pr.paymentHash, nodes("D").nodeParams.nodeId, 5, paymentRequest = Some(pr)))
val paymentId = sender.expectMsgType[UUID](30 seconds) val paymentId = sender.expectMsgType[UUID](30 seconds)
val paymentSent = sender.expectMsgType[PaymentSent](30 seconds) val paymentSent = sender.expectMsgType[PaymentSent](30 seconds)
assert(paymentSent.id === paymentId) assert(paymentSent.id === paymentId, paymentSent)
assert(paymentSent.paymentHash === pr.paymentHash) assert(paymentSent.paymentHash === pr.paymentHash, paymentSent)
assert(paymentSent.parts.length > 1) assert(paymentSent.parts.length > 1, paymentSent)
assert(paymentSent.recipientNodeId === nodes("D").nodeParams.nodeId) assert(paymentSent.recipientNodeId === nodes("D").nodeParams.nodeId, paymentSent)
assert(paymentSent.recipientAmount === amount) assert(paymentSent.recipientAmount === amount, paymentSent)
assert(paymentSent.feesPaid > 0.msat) assert(paymentSent.feesPaid > 0.msat, paymentSent)
assert(paymentSent.parts.forall(p => p.id != paymentSent.id)) assert(paymentSent.parts.forall(p => p.id != paymentSent.id), paymentSent)
assert(paymentSent.parts.forall(p => p.route.isDefined)) assert(paymentSent.parts.forall(p => p.route.isDefined), paymentSent)
val paymentParts = nodes("B").nodeParams.db.payments.listOutgoingPayments(paymentId).filter(_.status.isInstanceOf[OutgoingPaymentStatus.Succeeded]) val paymentParts = nodes("B").nodeParams.db.payments.listOutgoingPayments(paymentId).filter(_.status.isInstanceOf[OutgoingPaymentStatus.Succeeded])
assert(paymentParts.length == paymentSent.parts.length) assert(paymentParts.length == paymentSent.parts.length, paymentParts)
assert(paymentParts.map(_.amount).sum === amount) assert(paymentParts.map(_.amount).sum === amount, paymentParts)
assert(paymentParts.forall(p => p.parentId == paymentId)) assert(paymentParts.forall(p => p.parentId == paymentId), paymentParts)
assert(paymentParts.forall(p => p.parentId != p.id)) assert(paymentParts.forall(p => p.parentId != p.id), paymentParts)
assert(paymentParts.forall(p => p.status.asInstanceOf[OutgoingPaymentStatus.Succeeded].feesPaid > 0.msat)) assert(paymentParts.forall(p => p.status.asInstanceOf[OutgoingPaymentStatus.Succeeded].feesPaid > 0.msat), paymentParts)
awaitCond(nodes("B").nodeParams.db.audit.listSent(start, Platform.currentTime).nonEmpty) awaitCond(nodes("B").nodeParams.db.audit.listSent(start, Platform.currentTime).nonEmpty)
assert(nodes("B").nodeParams.db.audit.listSent(start, Platform.currentTime) === Seq(paymentSent.copy(parts = paymentSent.parts.map(_.copy(route = None))))) val sent = nodes("B").nodeParams.db.audit.listSent(start, Platform.currentTime)
assert(sent.length === 1, sent)
assert(sent.head.copy(parts = sent.head.parts.sortBy(_.timestamp)) === paymentSent.copy(parts = paymentSent.parts.map(_.copy(route = None)).sortBy(_.timestamp)), sent)
awaitCond(nodes("D").nodeParams.db.payments.getIncomingPayment(pr.paymentHash).exists(_.status.isInstanceOf[IncomingPaymentStatus.Received])) awaitCond(nodes("D").nodeParams.db.payments.getIncomingPayment(pr.paymentHash).exists(_.status.isInstanceOf[IncomingPaymentStatus.Received]))
val Some(IncomingPayment(_, _, _, _, IncomingPaymentStatus.Received(receivedAmount, _))) = nodes("D").nodeParams.db.payments.getIncomingPayment(pr.paymentHash) val Some(IncomingPayment(_, _, _, _, IncomingPaymentStatus.Received(receivedAmount, _))) = nodes("D").nodeParams.db.payments.getIncomingPayment(pr.paymentHash)
@ -492,9 +494,9 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService
sender.send(nodes("B").paymentInitiator, SendPaymentRequest(amount, pr.paymentHash, nodes("D").nodeParams.nodeId, 1, paymentRequest = Some(pr))) sender.send(nodes("B").paymentInitiator, SendPaymentRequest(amount, pr.paymentHash, nodes("D").nodeParams.nodeId, 1, paymentRequest = Some(pr)))
val paymentId = sender.expectMsgType[UUID](30 seconds) val paymentId = sender.expectMsgType[UUID](30 seconds)
val paymentFailed = sender.expectMsgType[PaymentFailed](45 seconds) val paymentFailed = sender.expectMsgType[PaymentFailed](45 seconds)
assert(paymentFailed.id === paymentId) assert(paymentFailed.id === paymentId, paymentFailed)
assert(paymentFailed.paymentHash === pr.paymentHash) assert(paymentFailed.paymentHash === pr.paymentHash, paymentFailed)
assert(paymentFailed.failures.length > 1) assert(paymentFailed.failures.length > 1, paymentFailed)
assert(nodes("D").nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending) assert(nodes("D").nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending)
@ -515,17 +517,17 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService
sender.send(nodes("D").paymentInitiator, SendPaymentRequest(amount, pr.paymentHash, nodes("C").nodeParams.nodeId, 3, paymentRequest = Some(pr))) sender.send(nodes("D").paymentInitiator, SendPaymentRequest(amount, pr.paymentHash, nodes("C").nodeParams.nodeId, 3, paymentRequest = Some(pr)))
val paymentId = sender.expectMsgType[UUID](30 seconds) val paymentId = sender.expectMsgType[UUID](30 seconds)
val paymentSent = sender.expectMsgType[PaymentSent](30 seconds) val paymentSent = sender.expectMsgType[PaymentSent](30 seconds)
assert(paymentSent.id === paymentId) assert(paymentSent.id === paymentId, paymentSent)
assert(paymentSent.paymentHash === pr.paymentHash) assert(paymentSent.paymentHash === pr.paymentHash, paymentSent)
assert(paymentSent.parts.length > 1) assert(paymentSent.parts.length > 1, paymentSent)
assert(paymentSent.recipientAmount === amount) assert(paymentSent.recipientAmount === amount, paymentSent)
assert(paymentSent.feesPaid === 0.msat) // no fees when using direct channels assert(paymentSent.feesPaid === 0.msat, paymentSent) // no fees when using direct channels
val paymentParts = nodes("D").nodeParams.db.payments.listOutgoingPayments(paymentId).filter(_.status.isInstanceOf[OutgoingPaymentStatus.Succeeded]) val paymentParts = nodes("D").nodeParams.db.payments.listOutgoingPayments(paymentId).filter(_.status.isInstanceOf[OutgoingPaymentStatus.Succeeded])
assert(paymentParts.map(_.amount).sum === amount) assert(paymentParts.map(_.amount).sum === amount, paymentParts)
assert(paymentParts.forall(p => p.parentId == paymentId)) assert(paymentParts.forall(p => p.parentId == paymentId), paymentParts)
assert(paymentParts.forall(p => p.parentId != p.id)) assert(paymentParts.forall(p => p.parentId != p.id), paymentParts)
assert(paymentParts.forall(p => p.status.asInstanceOf[OutgoingPaymentStatus.Succeeded].feesPaid == 0.msat)) assert(paymentParts.forall(p => p.status.asInstanceOf[OutgoingPaymentStatus.Succeeded].feesPaid == 0.msat), paymentParts)
awaitCond(nodes("C").nodeParams.db.payments.getIncomingPayment(pr.paymentHash).exists(_.status.isInstanceOf[IncomingPaymentStatus.Received])) awaitCond(nodes("C").nodeParams.db.payments.getIncomingPayment(pr.paymentHash).exists(_.status.isInstanceOf[IncomingPaymentStatus.Received]))
val Some(IncomingPayment(_, _, _, _, IncomingPaymentStatus.Received(receivedAmount, _))) = nodes("C").nodeParams.db.payments.getIncomingPayment(pr.paymentHash) val Some(IncomingPayment(_, _, _, _, IncomingPaymentStatus.Received(receivedAmount, _))) = nodes("C").nodeParams.db.payments.getIncomingPayment(pr.paymentHash)
@ -547,10 +549,11 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService
sender.send(nodes("D").paymentInitiator, SendPaymentRequest(amount, pr.paymentHash, nodes("C").nodeParams.nodeId, 1, paymentRequest = Some(pr))) sender.send(nodes("D").paymentInitiator, SendPaymentRequest(amount, pr.paymentHash, nodes("C").nodeParams.nodeId, 1, paymentRequest = Some(pr)))
val paymentId = sender.expectMsgType[UUID](30 seconds) val paymentId = sender.expectMsgType[UUID](30 seconds)
val paymentFailed = sender.expectMsgType[PaymentFailed](30 seconds) val paymentFailed = sender.expectMsgType[PaymentFailed](30 seconds)
assert(paymentFailed.id === paymentId) assert(paymentFailed.id === paymentId, paymentFailed)
assert(paymentFailed.paymentHash === pr.paymentHash) assert(paymentFailed.paymentHash === pr.paymentHash, paymentFailed)
assert(nodes("C").nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending) val incoming = nodes("C").nodeParams.db.payments.getIncomingPayment(pr.paymentHash)
assert(incoming.get.status === IncomingPaymentStatus.Pending, incoming)
sender.send(nodes("D").relayer, GetOutgoingChannels()) sender.send(nodes("D").relayer, GetOutgoingChannels())
val canSend2 = sender.expectMsgType[OutgoingChannels].channels.map(_.commitments.availableBalanceForSend).sum val canSend2 = sender.expectMsgType[OutgoingChannels].channels.map(_.commitments.availableBalanceForSend).sum
@ -573,12 +576,12 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService
sender.send(nodes("B").paymentInitiator, payment) sender.send(nodes("B").paymentInitiator, payment)
val paymentId = sender.expectMsgType[UUID](30 seconds) val paymentId = sender.expectMsgType[UUID](30 seconds)
val paymentSent = sender.expectMsgType[PaymentSent](30 seconds) val paymentSent = sender.expectMsgType[PaymentSent](30 seconds)
assert(paymentSent.id === paymentId) assert(paymentSent.id === paymentId, paymentSent)
assert(paymentSent.paymentHash === pr.paymentHash) assert(paymentSent.paymentHash === pr.paymentHash, paymentSent)
assert(paymentSent.recipientNodeId === nodes("F3").nodeParams.nodeId) assert(paymentSent.recipientNodeId === nodes("F3").nodeParams.nodeId, paymentSent)
assert(paymentSent.recipientAmount === amount) assert(paymentSent.recipientAmount === amount, paymentSent)
assert(paymentSent.feesPaid === 1000000.msat) assert(paymentSent.feesPaid === 1000000.msat, paymentSent)
assert(paymentSent.nonTrampolineFees === 0.msat) assert(paymentSent.nonTrampolineFees === 0.msat, paymentSent)
awaitCond(nodes("F3").nodeParams.db.payments.getIncomingPayment(pr.paymentHash).exists(_.status.isInstanceOf[IncomingPaymentStatus.Received])) awaitCond(nodes("F3").nodeParams.db.payments.getIncomingPayment(pr.paymentHash).exists(_.status.isInstanceOf[IncomingPaymentStatus.Received]))
val Some(IncomingPayment(_, _, _, _, IncomingPaymentStatus.Received(receivedAmount, _))) = nodes("F3").nodeParams.db.payments.getIncomingPayment(pr.paymentHash) val Some(IncomingPayment(_, _, _, _, IncomingPaymentStatus.Received(receivedAmount, _))) = nodes("F3").nodeParams.db.payments.getIncomingPayment(pr.paymentHash)
@ -586,15 +589,15 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService
awaitCond(nodes("G").nodeParams.db.audit.listRelayed(start, Platform.currentTime).exists(_.paymentHash == pr.paymentHash)) awaitCond(nodes("G").nodeParams.db.audit.listRelayed(start, Platform.currentTime).exists(_.paymentHash == pr.paymentHash))
val relayed = nodes("G").nodeParams.db.audit.listRelayed(start, Platform.currentTime).filter(_.paymentHash == pr.paymentHash).head val relayed = nodes("G").nodeParams.db.audit.listRelayed(start, Platform.currentTime).filter(_.paymentHash == pr.paymentHash).head
assert(relayed.amountIn - relayed.amountOut > 0.msat) assert(relayed.amountIn - relayed.amountOut > 0.msat, relayed)
assert(relayed.amountIn - relayed.amountOut < 1000000.msat) assert(relayed.amountIn - relayed.amountOut < 1000000.msat, relayed)
val outgoingSuccess = nodes("B").nodeParams.db.payments.listOutgoingPayments(paymentId).filter(p => p.status.isInstanceOf[OutgoingPaymentStatus.Succeeded]) val outgoingSuccess = nodes("B").nodeParams.db.payments.listOutgoingPayments(paymentId).filter(p => p.status.isInstanceOf[OutgoingPaymentStatus.Succeeded])
outgoingSuccess.foreach { case OutgoingPayment(_, _, _, _, _, _, _, recipientNodeId, _, _, OutgoingPaymentStatus.Succeeded(_, _, route, _)) => outgoingSuccess.foreach { case p@OutgoingPayment(_, _, _, _, _, _, _, recipientNodeId, _, _, OutgoingPaymentStatus.Succeeded(_, _, route, _)) =>
assert(recipientNodeId === nodes("F3").nodeParams.nodeId) assert(recipientNodeId === nodes("F3").nodeParams.nodeId, p)
assert(route.lastOption === Some(HopSummary(nodes("G").nodeParams.nodeId, nodes("F3").nodeParams.nodeId))) assert(route.lastOption === Some(HopSummary(nodes("G").nodeParams.nodeId, nodes("F3").nodeParams.nodeId)), p)
} }
assert(outgoingSuccess.map(_.amount).sum === amount + 1000000.msat) assert(outgoingSuccess.map(_.amount).sum === amount + 1000000.msat, outgoingSuccess)
} }
test("send a trampoline payment D->B (via trampoline C)") { test("send a trampoline payment D->B (via trampoline C)") {
@ -610,11 +613,11 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService
sender.send(nodes("D").paymentInitiator, payment) sender.send(nodes("D").paymentInitiator, payment)
val paymentId = sender.expectMsgType[UUID](30 seconds) val paymentId = sender.expectMsgType[UUID](30 seconds)
val paymentSent = sender.expectMsgType[PaymentSent](30 seconds) val paymentSent = sender.expectMsgType[PaymentSent](30 seconds)
assert(paymentSent.id === paymentId) assert(paymentSent.id === paymentId, paymentSent)
assert(paymentSent.paymentHash === pr.paymentHash) assert(paymentSent.paymentHash === pr.paymentHash, paymentSent)
assert(paymentSent.recipientAmount === amount) assert(paymentSent.recipientAmount === amount, paymentSent)
assert(paymentSent.feesPaid === 300000.msat) assert(paymentSent.feesPaid === 300000.msat, paymentSent)
assert(paymentSent.nonTrampolineFees === 0.msat) assert(paymentSent.nonTrampolineFees === 0.msat, paymentSent)
awaitCond(nodes("B").nodeParams.db.payments.getIncomingPayment(pr.paymentHash).exists(_.status.isInstanceOf[IncomingPaymentStatus.Received])) awaitCond(nodes("B").nodeParams.db.payments.getIncomingPayment(pr.paymentHash).exists(_.status.isInstanceOf[IncomingPaymentStatus.Received]))
val Some(IncomingPayment(_, _, _, _, IncomingPaymentStatus.Received(receivedAmount, _))) = nodes("B").nodeParams.db.payments.getIncomingPayment(pr.paymentHash) val Some(IncomingPayment(_, _, _, _, IncomingPaymentStatus.Received(receivedAmount, _))) = nodes("B").nodeParams.db.payments.getIncomingPayment(pr.paymentHash)
@ -622,18 +625,20 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService
awaitCond(nodes("C").nodeParams.db.audit.listRelayed(start, Platform.currentTime).exists(_.paymentHash == pr.paymentHash)) awaitCond(nodes("C").nodeParams.db.audit.listRelayed(start, Platform.currentTime).exists(_.paymentHash == pr.paymentHash))
val relayed = nodes("C").nodeParams.db.audit.listRelayed(start, Platform.currentTime).filter(_.paymentHash == pr.paymentHash).head val relayed = nodes("C").nodeParams.db.audit.listRelayed(start, Platform.currentTime).filter(_.paymentHash == pr.paymentHash).head
assert(relayed.amountIn - relayed.amountOut > 0.msat) assert(relayed.amountIn - relayed.amountOut > 0.msat, relayed)
assert(relayed.amountIn - relayed.amountOut < 300000.msat) assert(relayed.amountIn - relayed.amountOut < 300000.msat, relayed)
val outgoingSuccess = nodes("D").nodeParams.db.payments.listOutgoingPayments(paymentId).filter(p => p.status.isInstanceOf[OutgoingPaymentStatus.Succeeded]) val outgoingSuccess = nodes("D").nodeParams.db.payments.listOutgoingPayments(paymentId).filter(p => p.status.isInstanceOf[OutgoingPaymentStatus.Succeeded])
outgoingSuccess.foreach { case OutgoingPayment(_, _, _, _, _, _, _, recipientNodeId, _, _, OutgoingPaymentStatus.Succeeded(_, _, route, _)) => outgoingSuccess.foreach { case p@OutgoingPayment(_, _, _, _, _, _, _, recipientNodeId, _, _, OutgoingPaymentStatus.Succeeded(_, _, route, _)) =>
assert(recipientNodeId === nodes("B").nodeParams.nodeId) assert(recipientNodeId === nodes("B").nodeParams.nodeId, p)
assert(route.lastOption === Some(HopSummary(nodes("C").nodeParams.nodeId, nodes("B").nodeParams.nodeId))) assert(route.lastOption === Some(HopSummary(nodes("C").nodeParams.nodeId, nodes("B").nodeParams.nodeId)), p)
} }
assert(outgoingSuccess.map(_.amount).sum === amount + 300000.msat) assert(outgoingSuccess.map(_.amount).sum === amount + 300000.msat, outgoingSuccess)
awaitCond(nodes("D").nodeParams.db.audit.listSent(start, Platform.currentTime).nonEmpty) awaitCond(nodes("D").nodeParams.db.audit.listSent(start, Platform.currentTime).nonEmpty)
assert(nodes("D").nodeParams.db.audit.listSent(start, Platform.currentTime) === Seq(paymentSent.copy(parts = paymentSent.parts.map(_.copy(route = None))))) val sent = nodes("D").nodeParams.db.audit.listSent(start, Platform.currentTime)
assert(sent.length === 1, sent)
assert(sent.head.copy(parts = sent.head.parts.sortBy(_.timestamp)) === paymentSent.copy(parts = paymentSent.parts.map(_.copy(route = None)).sortBy(_.timestamp)), sent)
} }
test("send a trampoline payment F3->A (via trampoline C, non-trampoline recipient)") { test("send a trampoline payment F3->A (via trampoline C, non-trampoline recipient)") {
@ -654,10 +659,10 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService
sender.send(nodes("F3").paymentInitiator, payment) sender.send(nodes("F3").paymentInitiator, payment)
val paymentId = sender.expectMsgType[UUID](30 seconds) val paymentId = sender.expectMsgType[UUID](30 seconds)
val paymentSent = sender.expectMsgType[PaymentSent](30 seconds) val paymentSent = sender.expectMsgType[PaymentSent](30 seconds)
assert(paymentSent.id === paymentId) assert(paymentSent.id === paymentId, paymentSent)
assert(paymentSent.paymentHash === pr.paymentHash) assert(paymentSent.paymentHash === pr.paymentHash, paymentSent)
assert(paymentSent.recipientAmount === amount) assert(paymentSent.recipientAmount === amount, paymentSent)
assert(paymentSent.trampolineFees === 1000000.msat) assert(paymentSent.trampolineFees === 1000000.msat, paymentSent)
awaitCond(nodes("A").nodeParams.db.payments.getIncomingPayment(pr.paymentHash).exists(_.status.isInstanceOf[IncomingPaymentStatus.Received])) awaitCond(nodes("A").nodeParams.db.payments.getIncomingPayment(pr.paymentHash).exists(_.status.isInstanceOf[IncomingPaymentStatus.Received]))
val Some(IncomingPayment(_, _, _, _, IncomingPaymentStatus.Received(receivedAmount, _))) = nodes("A").nodeParams.db.payments.getIncomingPayment(pr.paymentHash) val Some(IncomingPayment(_, _, _, _, IncomingPaymentStatus.Received(receivedAmount, _))) = nodes("A").nodeParams.db.payments.getIncomingPayment(pr.paymentHash)
@ -665,15 +670,15 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService
awaitCond(nodes("C").nodeParams.db.audit.listRelayed(start, Platform.currentTime).exists(_.paymentHash == pr.paymentHash)) awaitCond(nodes("C").nodeParams.db.audit.listRelayed(start, Platform.currentTime).exists(_.paymentHash == pr.paymentHash))
val relayed = nodes("C").nodeParams.db.audit.listRelayed(start, Platform.currentTime).filter(_.paymentHash == pr.paymentHash).head val relayed = nodes("C").nodeParams.db.audit.listRelayed(start, Platform.currentTime).filter(_.paymentHash == pr.paymentHash).head
assert(relayed.amountIn - relayed.amountOut > 0.msat) assert(relayed.amountIn - relayed.amountOut > 0.msat, relayed)
assert(relayed.amountIn - relayed.amountOut < 1000000.msat) assert(relayed.amountIn - relayed.amountOut < 1000000.msat, relayed)
val outgoingSuccess = nodes("F3").nodeParams.db.payments.listOutgoingPayments(paymentId).filter(p => p.status.isInstanceOf[OutgoingPaymentStatus.Succeeded]) val outgoingSuccess = nodes("F3").nodeParams.db.payments.listOutgoingPayments(paymentId).filter(p => p.status.isInstanceOf[OutgoingPaymentStatus.Succeeded])
outgoingSuccess.foreach { case OutgoingPayment(_, _, _, _, _, _, _, recipientNodeId, _, _, OutgoingPaymentStatus.Succeeded(_, _, route, _)) => outgoingSuccess.foreach { case p@OutgoingPayment(_, _, _, _, _, _, _, recipientNodeId, _, _, OutgoingPaymentStatus.Succeeded(_, _, route, _)) =>
assert(recipientNodeId === nodes("A").nodeParams.nodeId) assert(recipientNodeId === nodes("A").nodeParams.nodeId, p)
assert(route.lastOption === Some(HopSummary(nodes("C").nodeParams.nodeId, nodes("A").nodeParams.nodeId))) assert(route.lastOption === Some(HopSummary(nodes("C").nodeParams.nodeId, nodes("A").nodeParams.nodeId)), p)
} }
assert(outgoingSuccess.map(_.amount).sum === amount + 1000000.msat) assert(outgoingSuccess.map(_.amount).sum === amount + 1000000.msat, outgoingSuccess)
} }
test("send a trampoline payment B->D (temporary local failure at trampoline)") { test("send a trampoline payment B->D (temporary local failure at trampoline)") {
@ -697,13 +702,13 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService
sender.send(nodes("B").paymentInitiator, payment) sender.send(nodes("B").paymentInitiator, payment)
val paymentId = sender.expectMsgType[UUID](30 seconds) val paymentId = sender.expectMsgType[UUID](30 seconds)
val paymentFailed = sender.expectMsgType[PaymentFailed](30 seconds) val paymentFailed = sender.expectMsgType[PaymentFailed](30 seconds)
assert(paymentFailed.id === paymentId) assert(paymentFailed.id === paymentId, paymentFailed)
assert(paymentFailed.paymentHash === pr.paymentHash) assert(paymentFailed.paymentHash === pr.paymentHash, paymentFailed)
assert(nodes("D").nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending) assert(nodes("D").nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending)
val outgoingPayments = nodes("B").nodeParams.db.payments.listOutgoingPayments(paymentId) val outgoingPayments = nodes("B").nodeParams.db.payments.listOutgoingPayments(paymentId)
assert(outgoingPayments.nonEmpty) assert(outgoingPayments.nonEmpty, outgoingPayments)
assert(outgoingPayments.forall(p => p.status.isInstanceOf[OutgoingPaymentStatus.Failed])) assert(outgoingPayments.forall(p => p.status.isInstanceOf[OutgoingPaymentStatus.Failed]), outgoingPayments)
} }
test("send a trampoline payment A->D (temporary remote failure at trampoline)") { test("send a trampoline payment A->D (temporary remote failure at trampoline)") {
@ -718,13 +723,13 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService
sender.send(nodes("A").paymentInitiator, payment) sender.send(nodes("A").paymentInitiator, payment)
val paymentId = sender.expectMsgType[UUID](30 seconds) val paymentId = sender.expectMsgType[UUID](30 seconds)
val paymentFailed = sender.expectMsgType[PaymentFailed](30 seconds) val paymentFailed = sender.expectMsgType[PaymentFailed](30 seconds)
assert(paymentFailed.id === paymentId) assert(paymentFailed.id === paymentId, paymentFailed)
assert(paymentFailed.paymentHash === pr.paymentHash) assert(paymentFailed.paymentHash === pr.paymentHash, paymentFailed)
assert(nodes("D").nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending) assert(nodes("D").nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending)
val outgoingPayments = nodes("A").nodeParams.db.payments.listOutgoingPayments(paymentId) val outgoingPayments = nodes("A").nodeParams.db.payments.listOutgoingPayments(paymentId)
assert(outgoingPayments.nonEmpty) assert(outgoingPayments.nonEmpty, outgoingPayments)
assert(outgoingPayments.forall(p => p.status.isInstanceOf[OutgoingPaymentStatus.Failed])) assert(outgoingPayments.forall(p => p.status.isInstanceOf[OutgoingPaymentStatus.Failed]), outgoingPayments)
} }
/** /**

View file

@ -28,11 +28,11 @@ import fr.acinq.eclair.payment.PaymentPacketSpec._
import fr.acinq.eclair.payment.PaymentRequest.{ExtraHop, Features} import fr.acinq.eclair.payment.PaymentRequest.{ExtraHop, Features}
import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle.SendMultiPartPayment import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle.SendMultiPartPayment
import fr.acinq.eclair.payment.send.PaymentInitiator import fr.acinq.eclair.payment.send.PaymentInitiator
import fr.acinq.eclair.payment.send.PaymentInitiator.{SendPaymentConfig, SendPaymentRequest, SendTrampolinePaymentRequest, TrampolineLegacyAmountLessInvoice} import fr.acinq.eclair.payment.send.PaymentInitiator._
import fr.acinq.eclair.payment.send.PaymentLifecycle.{SendPayment, SendPaymentToRoute} import fr.acinq.eclair.payment.send.PaymentLifecycle.{SendPayment, SendPaymentToRoute}
import fr.acinq.eclair.router.RouteParams import fr.acinq.eclair.router.{NodeHop, RouteParams}
import fr.acinq.eclair.wire.Onion.FinalLegacyPayload import fr.acinq.eclair.wire.Onion.FinalLegacyPayload
import fr.acinq.eclair.wire.{OnionCodecs, OnionTlv, TrampolineFeeInsufficient} import fr.acinq.eclair.wire.{Onion, OnionCodecs, OnionTlv, TrampolineFeeInsufficient}
import fr.acinq.eclair.{CltvExpiryDelta, LongToBtcAmount, NodeParams, TestConstants, randomBytes32, randomKey} import fr.acinq.eclair.{CltvExpiryDelta, LongToBtcAmount, NodeParams, TestConstants, randomBytes32, randomKey}
import org.scalatest.{Outcome, Tag, fixture} import org.scalatest.{Outcome, Tag, fixture}
import scodec.bits.HexStringSyntax import scodec.bits.HexStringSyntax
@ -83,9 +83,10 @@ class PaymentInitiatorSpec extends TestKit(ActorSystem("test")) with fixture.Fun
test("forward payment with pre-defined route") { f => test("forward payment with pre-defined route") { f =>
import f._ import f._
sender.send(initiator, SendPaymentRequest(finalAmount, paymentHash, c, 1, predefinedRoute = Seq(a, b, c))) val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, "Some invoice", features = None)
val paymentId = sender.expectMsgType[UUID] sender.send(initiator, SendPaymentToRouteRequest(finalAmount, finalAmount, None, None, pr, Channel.MIN_CLTV_EXPIRY_DELTA, Seq(a, b, c), None, 0 msat, CltvExpiryDelta(0), Nil))
payFsm.expectMsg(SendPaymentConfig(paymentId, paymentId, None, paymentHash, finalAmount, c, Upstream.Local(paymentId), None, storeInDb = true, publishEvent = true, Nil)) val payment = sender.expectMsgType[SendPaymentToRouteResponse]
payFsm.expectMsg(SendPaymentConfig(payment.paymentId, payment.parentId, None, paymentHash, finalAmount, c, Upstream.Local(payment.paymentId), Some(pr), storeInDb = true, publishEvent = true, Nil))
payFsm.expectMsg(SendPaymentToRoute(Seq(a, b, c), FinalLegacyPayload(finalAmount, Channel.MIN_CLTV_EXPIRY_DELTA.toCltvExpiry(nodeParams.currentBlockHeight + 1)))) payFsm.expectMsg(SendPaymentToRoute(Seq(a, b, c), FinalLegacyPayload(finalAmount, Channel.MIN_CLTV_EXPIRY_DELTA.toCltvExpiry(nodeParams.currentBlockHeight + 1))))
} }
@ -126,11 +127,11 @@ class PaymentInitiatorSpec extends TestKit(ActorSystem("test")) with fixture.Fun
test("forward multi-part payment with pre-defined route") { f => test("forward multi-part payment with pre-defined route") { f =>
import f._ import f._
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, randomKey, "Some invoice", features = Some(Features(VariableLengthOnion.optional, PaymentSecret.optional, BasicMultiPartPayment.optional))) val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, "Some invoice", features = Some(Features(VariableLengthOnion.optional, PaymentSecret.optional, BasicMultiPartPayment.optional)))
val req = SendPaymentRequest(finalAmount / 2, paymentHash, c, 1, paymentRequest = Some(pr), predefinedRoute = Seq(a, b, c)) val req = SendPaymentToRouteRequest(finalAmount / 2, finalAmount, None, None, pr, Channel.MIN_CLTV_EXPIRY_DELTA, Seq(a, b, c), None, 0 msat, CltvExpiryDelta(0), Nil)
sender.send(initiator, req) sender.send(initiator, req)
val id = sender.expectMsgType[UUID] val payment = sender.expectMsgType[SendPaymentToRouteResponse]
payFsm.expectMsg(SendPaymentConfig(id, id, None, paymentHash, finalAmount / 2, c, Upstream.Local(id), Some(pr), storeInDb = true, publishEvent = true, Nil)) payFsm.expectMsg(SendPaymentConfig(payment.paymentId, payment.parentId, None, paymentHash, finalAmount, c, Upstream.Local(payment.paymentId), Some(pr), storeInDb = true, publishEvent = true, Nil))
val msg = payFsm.expectMsgType[SendPaymentToRoute] val msg = payFsm.expectMsgType[SendPaymentToRoute]
assert(msg.hops === Seq(a, b, c)) assert(msg.hops === Seq(a, b, c))
assert(msg.finalPayload.amount === finalAmount / 2) assert(msg.finalPayload.amount === finalAmount / 2)
@ -286,4 +287,32 @@ class PaymentInitiatorSpec extends TestKit(ActorSystem("test")) with fixture.Fun
eventListener.expectNoMsg(100 millis) eventListener.expectNoMsg(100 millis)
} }
test("forward trampoline payment with pre-defined route") { f =>
import f._
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, "Some invoice")
val trampolineFees = 100 msat
val req = SendPaymentToRouteRequest(finalAmount + trampolineFees, finalAmount, None, None, pr, Channel.MIN_CLTV_EXPIRY_DELTA, Seq(a, b), 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(pr), storeInDb = true, publishEvent = true, Seq(NodeHop(b, c, CltvExpiryDelta(0), 0 msat))))
val msg = payFsm.expectMsgType[SendPaymentToRoute]
assert(msg.hops === Seq(a, b))
assert(msg.finalPayload.amount === finalAmount + trampolineFees)
assert(msg.finalPayload.paymentSecret === payment.trampolineSecret)
assert(msg.finalPayload.totalAmount === finalAmount + trampolineFees)
assert(msg.finalPayload.isInstanceOf[Onion.FinalTlvPayload])
val trampolineOnion = msg.finalPayload.asInstanceOf[Onion.FinalTlvPayload].records.get[OnionTlv.TrampolineOnion]
assert(trampolineOnion.nonEmpty)
// Verify that the trampoline node can correctly peel the trampoline onion.
val Right(decrypted) = Sphinx.TrampolinePacket.peel(priv_b.privateKey, pr.paymentHash, trampolineOnion.get.packet)
assert(!decrypted.isLastPacket)
val trampolinePayload = OnionCodecs.nodeRelayPerHopPayloadCodec.decode(decrypted.payload.bits).require.value
assert(trampolinePayload.amountToForward === finalAmount)
assert(trampolinePayload.totalAmount === finalAmount)
assert(trampolinePayload.outgoingNodeId === c)
assert(trampolinePayload.paymentSecret === pr.paymentSecret)
}
} }

View file

@ -261,7 +261,6 @@ object CustomTypeHints {
classOf[OutgoingPaymentStatus.Succeeded] -> "sent" classOf[OutgoingPaymentStatus.Succeeded] -> "sent"
)) ))
// TODO: @t-bast: don't forget to update slate-doc
val paymentEvent = CustomTypeHints(Map( val paymentEvent = CustomTypeHints(Map(
classOf[PaymentSent] -> "payment-sent", classOf[PaymentSent] -> "payment-sent",
classOf[ChannelPaymentRelayed] -> "payment-relayed", classOf[ChannelPaymentRelayed] -> "payment-relayed",

View file

@ -221,18 +221,6 @@ trait Service extends ExtraDirectives with Logging {
case _ => reject(MalformedFormFieldRejection("invoice", "The invoice must have an amount or you need to specify one using the field 'amountMsat'")) case _ => reject(MalformedFormFieldRejection("invoice", "The invoice must have an amount or you need to specify one using the field 'amountMsat'"))
} }
} ~ } ~
// TODO: @t-bast: remove this API once stabilized: should re-work the payment APIs to integrate Trampoline nicely
// - payinvoice: should stay the same, it's the easy flow where the node does its magic
// - sendtonode: exactly like payinvoice but without an invoice: let the node do its magic
// - sendtoroute: no path-finding, lets the user control exactly how to send (provide multiple routes, with trampoline or not, etc) -> maybe doesn't go through normal PayFSM (avoid retries)
// -> maybe somehow make one call per partial HTLC (allows easier failure reporting and out-of-node retry logic)?
// -> needs both trampolineRoute and routeToTrampoline arguments?
path("sendtotrampoline") {
formFields(invoiceFormParam, "trampolineId".as[PublicKey], "trampolineFeesMsat".as[MilliSatoshi], "trampolineExpiryDelta".as[Int]) {
(invoice, trampolineId, trampolineFees, trampolineExpiryDelta) =>
complete(eclairApi.sendToTrampoline(invoice, trampolineId, trampolineFees, CltvExpiryDelta(trampolineExpiryDelta)))
}
} ~
path("sendtonode") { path("sendtonode") {
formFields(amountMsatFormParam, paymentHashFormParam, nodeIdFormParam, "maxAttempts".as[Int].?, "feeThresholdSat".as[Satoshi].?, "maxFeePct".as[Double].?, "externalId".?) { formFields(amountMsatFormParam, paymentHashFormParam, nodeIdFormParam, "maxAttempts".as[Int].?, "feeThresholdSat".as[Satoshi].?, "maxFeePct".as[Double].?, "externalId".?) {
(amountMsat, paymentHash, nodeId, maxAttempts_opt, feeThresholdSat_opt, maxFeePct_opt, externalId_opt) => (amountMsat, paymentHash, nodeId, maxAttempts_opt, feeThresholdSat_opt, maxFeePct_opt, externalId_opt) =>
@ -240,9 +228,9 @@ trait Service extends ExtraDirectives with Logging {
} }
} ~ } ~
path("sendtoroute") { path("sendtoroute") {
formFields(amountMsatFormParam, paymentHashFormParam, "finalCltvExpiry".as[Int], "route".as[List[PublicKey]](pubkeyListUnmarshaller), "externalId".?, invoiceFormParam.?) { formFields(amountMsatFormParam, "recipientAmountMsat".as[MilliSatoshi].?, invoiceFormParam, "finalCltvExpiry".as[Int], "route".as[List[PublicKey]](pubkeyListUnmarshaller), "externalId".?, "parentId".as[UUID].?, "trampolineSecret".as[ByteVector32].?, "trampolineFeesMsat".as[MilliSatoshi].?, "trampolineCltvExpiry".as[Int].?, "trampolineNodes".as[List[PublicKey]](pubkeyListUnmarshaller).?) {
(amountMsat, paymentHash, finalCltvExpiry, route, externalId_opt, invoice_opt) => (amountMsat, recipientAmountMsat_opt, invoice, finalCltvExpiry, route, externalId_opt, parentId_opt, trampolineSecret_opt, trampolineFeesMsat_opt, trampolineCltvExpiry_opt, trampolineNodes_opt) =>
complete(eclairApi.sendToRoute(externalId_opt, route, amountMsat, paymentHash, CltvExpiryDelta(finalCltvExpiry), invoice_opt)) complete(eclairApi.sendToRoute(amountMsat, recipientAmountMsat_opt, externalId_opt, parentId_opt, invoice, CltvExpiryDelta(finalCltvExpiry), route, trampolineSecret_opt, trampolineFeesMsat_opt, trampolineCltvExpiry_opt.map(CltvExpiryDelta), trampolineNodes_opt.getOrElse(Nil)))
} }
} ~ } ~
path("getsentinfo") { path("getsentinfo") {

View file

@ -30,10 +30,11 @@ import de.heikoseeberger.akkahttpjson4s.Json4sSupport
import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.bitcoin.{Block, ByteVector32} import fr.acinq.bitcoin.{Block, ByteVector32}
import fr.acinq.eclair._ import fr.acinq.eclair._
import fr.acinq.eclair.db.{IncomingPayment, IncomingPaymentStatus, OutgoingPayment, OutgoingPaymentStatus, PaymentType} import fr.acinq.eclair.db._
import fr.acinq.eclair.io.NodeURI import fr.acinq.eclair.io.NodeURI
import fr.acinq.eclair.io.Peer.PeerInfo import fr.acinq.eclair.io.Peer.PeerInfo
import fr.acinq.eclair.payment.relay.Relayer.UsableBalance import fr.acinq.eclair.payment.relay.Relayer.UsableBalance
import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentToRouteResponse
import fr.acinq.eclair.payment.{PaymentFailed, _} import fr.acinq.eclair.payment.{PaymentFailed, _}
import fr.acinq.eclair.router.{NetworkStats, Stats} import fr.acinq.eclair.router.{NetworkStats, Stats}
import fr.acinq.eclair.wire.NodeAddress import fr.acinq.eclair.wire.NodeAddress
@ -400,8 +401,8 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest with IdiomaticMock
} }
test("'sendtoroute' method should accept a both a json-encoded AND comma separated list of pubkeys") { test("'sendtoroute' method should accept a both a json-encoded AND comma separated list of pubkeys") {
val rawUUID = "487da196-a4dc-4b1e-92b4-3e5e905e9f3f" val payment = SendPaymentToRouteResponse(UUID.fromString("487da196-a4dc-4b1e-92b4-3e5e905e9f3f"), UUID.fromString("2ad8c6d7-99cb-4238-8f67-89024b8eed0d"), None)
val paymentUUID = UUID.fromString(rawUUID) val expected = """{"paymentId":"487da196-a4dc-4b1e-92b4-3e5e905e9f3f","parentId":"2ad8c6d7-99cb-4238-8f67-89024b8eed0d"}"""
val externalId = UUID.randomUUID().toString val externalId = UUID.randomUUID().toString
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(1234 msat), ByteVector32.Zeroes, randomKey, "Some invoice") val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(1234 msat), ByteVector32.Zeroes, randomKey, "Some invoice")
val expectedRoute = List(PublicKey(hex"0217eb8243c95f5a3b7d4c5682d10de354b7007eb59b6807ae407823963c7547a9"), PublicKey(hex"0242a4ae0c5bef18048fbecf995094b74bfb0f7391418d71ed394784373f41e4f3"), PublicKey(hex"026ac9fcd64fb1aa1c491fc490634dc33da41d4a17b554e0adf1b32fee88ee9f28")) val expectedRoute = List(PublicKey(hex"0217eb8243c95f5a3b7d4c5682d10de354b7007eb59b6807ae407823963c7547a9"), PublicKey(hex"0242a4ae0c5bef18048fbecf995094b74bfb0f7391418d71ed394784373f41e4f3"), PublicKey(hex"026ac9fcd64fb1aa1c491fc490634dc33da41d4a17b554e0adf1b32fee88ee9f28"))
@ -409,30 +410,30 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest with IdiomaticMock
val jsonNodes = serialization.write(expectedRoute) val jsonNodes = serialization.write(expectedRoute)
val eclair = mock[Eclair] val eclair = mock[Eclair]
eclair.sendToRoute(any[Option[String]], any[List[PublicKey]], any[MilliSatoshi], any[ByteVector32], any[CltvExpiryDelta], any[Option[PaymentRequest]])(any[Timeout]) returns Future.successful(paymentUUID) eclair.sendToRoute(any[MilliSatoshi], any[Option[MilliSatoshi]], any[Option[String]], any[Option[UUID]], any[PaymentRequest], any[CltvExpiryDelta], any[List[PublicKey]], any[Option[ByteVector32]], any[Option[MilliSatoshi]], any[Option[CltvExpiryDelta]], any[List[PublicKey]])(any[Timeout]) returns Future.successful(payment)
val mockService = new MockService(eclair) val mockService = new MockService(eclair)
Post("/sendtoroute", FormData("route" -> jsonNodes, "amountMsat" -> "1234", "paymentHash" -> ByteVector32.Zeroes.toHex, "finalCltvExpiry" -> "190", "externalId" -> externalId.toString, "invoice" -> PaymentRequest.write(pr)).toEntity) ~> Post("/sendtoroute", FormData("route" -> jsonNodes, "amountMsat" -> "1234", "finalCltvExpiry" -> "190", "externalId" -> externalId.toString, "invoice" -> PaymentRequest.write(pr)).toEntity) ~>
addCredentials(BasicHttpCredentials("", mockService.password)) ~> addCredentials(BasicHttpCredentials("", mockService.password)) ~>
addHeader("Content-Type", "application/json") ~> addHeader("Content-Type", "application/json") ~>
Route.seal(mockService.route) ~> Route.seal(mockService.route) ~>
check { check {
assert(handled) assert(handled)
assert(status == OK) assert(status == OK)
assert(entityAs[String] == "\"" + rawUUID + "\"") assert(entityAs[String] == expected)
eclair.sendToRoute(Some(externalId), expectedRoute, 1234 msat, ByteVector32.Zeroes, CltvExpiryDelta(190), Some(pr))(any[Timeout]).wasCalled(once) eclair.sendToRoute(1234 msat, None, Some(externalId), None, pr, CltvExpiryDelta(190), expectedRoute, None, None, None, Nil)(any[Timeout]).wasCalled(once)
} }
// this test uses CSV encoded route // this test uses CSV encoded route
Post("/sendtoroute", FormData("route" -> csvNodes, "amountMsat" -> "1234", "paymentHash" -> ByteVector32.One.toHex, "finalCltvExpiry" -> "190").toEntity) ~> Post("/sendtoroute", FormData("route" -> csvNodes, "amountMsat" -> "1234", "finalCltvExpiry" -> "190", "invoice" -> PaymentRequest.write(pr)).toEntity) ~>
addCredentials(BasicHttpCredentials("", mockService.password)) ~> addCredentials(BasicHttpCredentials("", mockService.password)) ~>
addHeader("Content-Type", "application/json") ~> addHeader("Content-Type", "application/json") ~>
Route.seal(mockService.route) ~> Route.seal(mockService.route) ~>
check { check {
assert(handled) assert(handled)
assert(status == OK) assert(status == OK)
assert(entityAs[String] == "\"" + rawUUID + "\"") assert(entityAs[String] == expected)
eclair.sendToRoute(None, expectedRoute, 1234 msat, ByteVector32.One, CltvExpiryDelta(190), None)(any[Timeout]).wasCalled(once) eclair.sendToRoute(1234 msat, None, None, None, pr, CltvExpiryDelta(190), expectedRoute, None, None, None, Nil)(any[Timeout]).wasCalled(once)
} }
} }
@ -440,7 +441,7 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest with IdiomaticMock
val capStat = Stats(30 sat, 12 sat, 14 sat, 20 sat, 40 sat, 46 sat, 48 sat) val capStat = Stats(30 sat, 12 sat, 14 sat, 20 sat, 40 sat, 46 sat, 48 sat)
val cltvStat = Stats(CltvExpiryDelta(32), CltvExpiryDelta(11), CltvExpiryDelta(13), CltvExpiryDelta(22), CltvExpiryDelta(42), CltvExpiryDelta(51), CltvExpiryDelta(53)) val cltvStat = Stats(CltvExpiryDelta(32), CltvExpiryDelta(11), CltvExpiryDelta(13), CltvExpiryDelta(22), CltvExpiryDelta(42), CltvExpiryDelta(51), CltvExpiryDelta(53))
val feeBaseStat = Stats(32 msat, 11 msat, 13 msat, 22 msat, 42 msat, 51 msat, 53 msat) val feeBaseStat = Stats(32 msat, 11 msat, 13 msat, 22 msat, 42 msat, 51 msat, 53 msat)
val feePropStat = Stats(32l, 11l, 13l, 22l, 42l, 51l, 53l) val feePropStat = Stats(32L, 11L, 13L, 22L, 42L, 51L, 53L)
val networkStats = new NetworkStats(1, 2, capStat, cltvStat, feeBaseStat, feePropStat) val networkStats = new NetworkStats(1, 2, capStat, cltvStat, feeBaseStat, feePropStat)
val eclair = mock[Eclair] val eclair = mock[Eclair]