1
0
Fork 0
mirror of https://github.com/ACINQ/eclair.git synced 2025-02-23 22:46:44 +01:00

Validate payment secret when decoding (#1840)

The `payment_secret` feature was made mandatory in #1810 and is the default
in other implementations as well. We can thus force it to be available when
decoding onion payloads, which simplifies downstream components (no need
to handle the case where a `payment_secret` may be missing anymore).

We also rename messages in `PaymentInitiator` to remove the confusion with
Bolt 11 payment requests.
This commit is contained in:
Bastien Teinturier 2021-06-11 18:11:37 +02:00 committed by GitHub
parent e750474c72
commit bbfbad5975
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 556 additions and 618 deletions

View file

@ -35,7 +35,7 @@ import fr.acinq.eclair.payment._
import fr.acinq.eclair.payment.receive.MultiPartHandler.ReceivePayment
import fr.acinq.eclair.payment.relay.Relayer.{GetOutgoingChannels, OutgoingChannels, UsableBalance}
import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle.PreimageReceived
import fr.acinq.eclair.payment.send.PaymentInitiator.{SendPaymentRequest, SendPaymentToRouteRequest, SendPaymentToRouteResponse}
import fr.acinq.eclair.payment.send.PaymentInitiator.{SendPayment, SendPaymentToRoute, SendPaymentToRouteResponse, SendSpontaneousPayment}
import fr.acinq.eclair.router.Router._
import fr.acinq.eclair.router.{NetworkStats, RouteCalculation, Router}
import fr.acinq.eclair.wire.protocol._
@ -107,9 +107,9 @@ trait Eclair {
def receivedInfo(paymentHash: ByteVector32)(implicit timeout: Timeout): Future[Option[IncomingPayment]]
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], amount: MilliSatoshi, invoice: PaymentRequest, maxAttempts_opt: Option[Int] = None, feeThresholdSat_opt: Option[Satoshi] = None, maxFeePct_opt: Option[Double] = None)(implicit timeout: Timeout): Future[UUID]
def sendBlocking(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[Either[PreimageReceived, PaymentEvent]]
def sendBlocking(externalId_opt: Option[String], amount: MilliSatoshi, invoice: PaymentRequest, maxAttempts_opt: Option[Int] = None, feeThresholdSat_opt: Option[Satoshi] = None, maxFeePct_opt: Option[Double] = None)(implicit timeout: Timeout): Future[Either[PreimageReceived, PaymentEvent]]
def sendWithPreimage(externalId_opt: Option[String], recipientNodeId: PublicKey, amount: MilliSatoshi, paymentPreimage: ByteVector32 = randomBytes32(), maxAttempts_opt: Option[Int] = None, feeThresholdSat_opt: Option[Satoshi] = None, maxFeePct_opt: Option[Double] = None)(implicit timeout: Timeout): Future[UUID]
@ -272,7 +272,6 @@ class EclairImpl(appKit: Kit) extends Eclair {
override def findRoute(targetNodeId: PublicKey, amount: MilliSatoshi, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty)(implicit timeout: Timeout): Future[RouteResponse] =
findRouteBetween(appKit.nodeParams.nodeId, targetNodeId, amount, assistedRoutes)
override def findRouteBetween(sourceNodeId: PublicKey, targetNodeId: PublicKey, amount: MilliSatoshi, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty)(implicit timeout: Timeout): Future[RouteResponse] = {
val maxFee = RouteCalculation.getDefaultRouteParams(appKit.nodeParams.routerConf).getMaxFee(amount)
(appKit.router ? RouteRequest(sourceNodeId, targetNodeId, amount, maxFee, assistedRoutes)).mapTo[RouteResponse]
@ -280,7 +279,7 @@ class EclairImpl(appKit: Kit) extends Eclair {
override def sendToRoute(amount: MilliSatoshi, recipientAmount_opt: Option[MilliSatoshi], externalId_opt: Option[String], parentId_opt: Option[UUID], invoice: PaymentRequest, finalCltvExpiryDelta: CltvExpiryDelta, 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.getOrElse(amount))
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)
val sendPayment = SendPaymentToRoute(amount, recipientAmount, invoice, finalCltvExpiryDelta, route, externalId_opt, parentId_opt, trampolineSecret_opt, trampolineFees_opt.getOrElse(0 msat), trampolineExpiryDelta_opt.getOrElse(CltvExpiryDelta(0)), trampolineNodes_opt)
if (invoice.isExpired) {
Future.failed(new IllegalArgumentException("invoice has expired"))
} else if (route.isEmpty) {
@ -296,7 +295,7 @@ class EclairImpl(appKit: Kit) extends Eclair {
}
}
private def createPaymentRequest(externalId_opt: Option[String], recipientNodeId: PublicKey, amount: MilliSatoshi, paymentHash: ByteVector32, invoice_opt: Option[PaymentRequest], maxAttempts_opt: Option[Int], feeThreshold_opt: Option[Satoshi], maxFeePct_opt: Option[Double]): Either[IllegalArgumentException, SendPaymentRequest] = {
private def createPaymentRequest(externalId_opt: Option[String], amount: MilliSatoshi, invoice: PaymentRequest, maxAttempts_opt: Option[Int], feeThreshold_opt: Option[Satoshi], maxFeePct_opt: Option[Double]): Either[IllegalArgumentException, SendPayment] = {
val maxAttempts = maxAttempts_opt.getOrElse(appKit.nodeParams.maxPaymentAttempts)
val defaultRouteParams = RouteCalculation.getDefaultRouteParams(appKit.nodeParams.routerConf)
val routeParams = defaultRouteParams.copy(
@ -306,26 +305,23 @@ class EclairImpl(appKit: Kit) extends Eclair {
externalId_opt match {
case Some(externalId) if externalId.length > externalIdMaxLength => Left(new IllegalArgumentException(s"externalId is too long: cannot exceed $externalIdMaxLength characters"))
case _ => invoice_opt match {
case Some(invoice) if invoice.isExpired => Left(new IllegalArgumentException("invoice has expired"))
case Some(invoice) => invoice.minFinalCltvExpiryDelta match {
case Some(minFinalCltvExpiryDelta) => Right(SendPaymentRequest(amount, paymentHash, recipientNodeId, maxAttempts, minFinalCltvExpiryDelta, invoice_opt, externalId_opt, assistedRoutes = invoice.routingInfo, routeParams = Some(routeParams)))
case None => Right(SendPaymentRequest(amount, paymentHash, recipientNodeId, maxAttempts, paymentRequest = invoice_opt, externalId = externalId_opt, assistedRoutes = invoice.routingInfo, routeParams = Some(routeParams)))
}
case None => Right(SendPaymentRequest(amount, paymentHash, recipientNodeId, maxAttempts = maxAttempts, externalId = externalId_opt, routeParams = Some(routeParams)))
case _ if invoice.isExpired => Left(new IllegalArgumentException("invoice has expired"))
case _ => invoice.minFinalCltvExpiryDelta match {
case Some(minFinalCltvExpiryDelta) => Right(SendPayment(amount, invoice, maxAttempts, minFinalCltvExpiryDelta, externalId_opt, assistedRoutes = invoice.routingInfo, routeParams = Some(routeParams)))
case None => Right(SendPayment(amount, invoice, maxAttempts, externalId = externalId_opt, assistedRoutes = invoice.routingInfo, routeParams = Some(routeParams)))
}
}
}
override def send(externalId_opt: Option[String], recipientNodeId: PublicKey, amount: MilliSatoshi, paymentHash: ByteVector32, invoice_opt: Option[PaymentRequest], maxAttempts_opt: Option[Int], feeThreshold_opt: Option[Satoshi], maxFeePct_opt: Option[Double])(implicit timeout: Timeout): Future[UUID] = {
createPaymentRequest(externalId_opt, recipientNodeId, amount, paymentHash, invoice_opt, maxAttempts_opt, feeThreshold_opt, maxFeePct_opt) match {
override def send(externalId_opt: Option[String], amount: MilliSatoshi, invoice: PaymentRequest, maxAttempts_opt: Option[Int], feeThreshold_opt: Option[Satoshi], maxFeePct_opt: Option[Double])(implicit timeout: Timeout): Future[UUID] = {
createPaymentRequest(externalId_opt, amount, invoice, maxAttempts_opt, feeThreshold_opt, maxFeePct_opt) match {
case Left(ex) => Future.failed(ex)
case Right(req) => (appKit.paymentInitiator ? req).mapTo[UUID]
}
}
override def sendBlocking(externalId_opt: Option[String], recipientNodeId: PublicKey, amount: MilliSatoshi, paymentHash: ByteVector32, invoice_opt: Option[PaymentRequest], maxAttempts_opt: Option[Int], feeThreshold_opt: Option[Satoshi], maxFeePct_opt: Option[Double])(implicit timeout: Timeout): Future[Either[PreimageReceived, PaymentEvent]] = {
createPaymentRequest(externalId_opt, recipientNodeId, amount, paymentHash, invoice_opt, maxAttempts_opt, feeThreshold_opt, maxFeePct_opt) match {
override def sendBlocking(externalId_opt: Option[String], amount: MilliSatoshi, invoice: PaymentRequest, maxAttempts_opt: Option[Int], feeThreshold_opt: Option[Satoshi], maxFeePct_opt: Option[Double])(implicit timeout: Timeout): Future[Either[PreimageReceived, PaymentEvent]] = {
createPaymentRequest(externalId_opt, amount, invoice, maxAttempts_opt, feeThreshold_opt, maxFeePct_opt) match {
case Left(ex) => Future.failed(ex)
case Right(req) => (appKit.paymentInitiator ? req.copy(blockUntilComplete = true)).map {
case e: PreimageReceived => Left(e)
@ -334,6 +330,17 @@ class EclairImpl(appKit: Kit) extends Eclair {
}
}
override def sendWithPreimage(externalId_opt: Option[String], recipientNodeId: PublicKey, amount: MilliSatoshi, paymentPreimage: ByteVector32, maxAttempts_opt: Option[Int], feeThreshold_opt: Option[Satoshi], maxFeePct_opt: Option[Double])(implicit timeout: Timeout): Future[UUID] = {
val maxAttempts = maxAttempts_opt.getOrElse(appKit.nodeParams.maxPaymentAttempts)
val defaultRouteParams = RouteCalculation.getDefaultRouteParams(appKit.nodeParams.routerConf)
val routeParams = defaultRouteParams.copy(
maxFeePct = maxFeePct_opt.getOrElse(defaultRouteParams.maxFeePct),
maxFeeBase = feeThreshold_opt.map(_.toMilliSatoshi).getOrElse(defaultRouteParams.maxFeeBase)
)
val sendPayment = SendSpontaneousPayment(amount, recipientNodeId, paymentPreimage, maxAttempts, externalId_opt, Some(routeParams))
(appKit.paymentInitiator ? sendPayment).mapTo[UUID]
}
override def sentInfo(id: Either[UUID, ByteVector32])(implicit timeout: Timeout): Future[Seq[OutgoingPayment]] = Future {
id match {
case Left(uuid) => appKit.nodeParams.db.payments.listOutgoingPayments(uuid)
@ -421,19 +428,6 @@ class EclairImpl(appKit: Kit) extends Eclair {
override def usableBalances()(implicit timeout: Timeout): Future[Iterable[UsableBalance]] =
(appKit.relayer ? GetOutgoingChannels()).mapTo[OutgoingChannels].map(_.channels.map(_.toUsableBalance))
override def sendWithPreimage(externalId_opt: Option[String], recipientNodeId: PublicKey, amount: MilliSatoshi, paymentPreimage: ByteVector32, maxAttempts_opt: Option[Int], feeThreshold_opt: Option[Satoshi], maxFeePct_opt: Option[Double])(implicit timeout: Timeout): Future[UUID] = {
val maxAttempts = maxAttempts_opt.getOrElse(appKit.nodeParams.maxPaymentAttempts)
val defaultRouteParams = RouteCalculation.getDefaultRouteParams(appKit.nodeParams.routerConf)
val routeParams = defaultRouteParams.copy(
maxFeePct = maxFeePct_opt.getOrElse(defaultRouteParams.maxFeePct),
maxFeeBase = feeThreshold_opt.map(_.toMilliSatoshi).getOrElse(defaultRouteParams.maxFeeBase)
)
val paymentHash: ByteVector32 = Crypto.sha256(paymentPreimage)
val keySendTlvRecords = Seq(GenericTlv(UInt64(5482373484L), paymentPreimage))
val sendPayment = SendPaymentRequest(amount, paymentHash, recipientNodeId, maxAttempts, externalId = externalId_opt, routeParams = Some(routeParams), userCustomTlvs = keySendTlvRecords)
(appKit.paymentInitiator ? sendPayment).mapTo[UUID]
}
override def signMessage(message: ByteVector): SignedMessage = {
val bytesToSign = SignedMessage.signedBytes(message)
val (signature, recoveryId) = appKit.nodeParams.nodeKeyManager.signDigest(bytesToSign)
@ -445,6 +439,6 @@ class EclairImpl(appKit: Kit) extends Eclair {
val signature = ByteVector64(recoverableSignature.tail)
val recoveryId = recoverableSignature.head.toInt - 31
val pubKeyFromSignature = Crypto.recoverPublicKey(signature, signedBytes, recoveryId)
VerifiedMessage(true, pubKeyFromSignature)
VerifiedMessage(valid = true, pubKeyFromSignature)
}
}

View file

@ -16,8 +16,6 @@
package fr.acinq.eclair.payment
import java.util.UUID
import akka.actor.ActorRef
import akka.event.LoggingAdapter
import fr.acinq.bitcoin.ByteVector32
@ -30,6 +28,7 @@ import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, MilliSatoshi, UInt64, rando
import scodec.bits.ByteVector
import scodec.{Attempt, DecodeResult}
import java.util.UUID
import scala.reflect.ClassTag
/**
@ -86,7 +85,6 @@ object IncomingPacket {
case Left(failure) => Left(failure)
// NB: we don't validate the ChannelRelayPacket here because its fees and cltv depend on what channel we'll choose to use.
case Right(DecodedOnionPacket(payload: Onion.ChannelRelayPayload, next)) => Right(ChannelRelayPacket(add, payload, next))
case Right(DecodedOnionPacket(payload: Onion.FinalLegacyPayload, _)) => validateFinal(add, payload)
case Right(DecodedOnionPacket(payload: Onion.FinalTlvPayload, _)) => payload.records.get[OnionTlv.TrampolineOnion] match {
case Some(OnionTlv.TrampolineOnion(trampolinePacket)) => decryptOnion(add, privateKey)(trampolinePacket, Sphinx.TrampolinePacket) match {
case Left(failure) => Left(failure)
@ -117,12 +115,10 @@ object IncomingPacket {
Left(FinalIncorrectCltvExpiry(add.cltvExpiry)) // previous trampoline didn't forward the right expiry
} else if (outerPayload.totalAmount != innerPayload.amount) {
Left(FinalIncorrectHtlcAmount(outerPayload.totalAmount)) // previous trampoline didn't forward the right amount
} else if (innerPayload.paymentSecret.isEmpty) {
Left(InvalidOnionPayload(UInt64(8), 0)) // trampoline recipients always provide a payment secret in the invoice
} else {
// We merge contents from the outer and inner payloads.
// We must use the inner payload's total amount and payment secret because the payment may be split between multiple trampoline payments (#reckless).
Right(FinalPacket(add, Onion.createMultiPartPayload(outerPayload.amount, innerPayload.totalAmount, outerPayload.expiry, innerPayload.paymentSecret.get)))
Right(FinalPacket(add, Onion.createMultiPartPayload(outerPayload.amount, innerPayload.totalAmount, outerPayload.expiry, innerPayload.paymentSecret)))
}
}
@ -174,8 +170,7 @@ object OutgoingPacket {
hops.reverse.foldLeft((finalPayload.amount, finalPayload.expiry, Seq[Onion.PerHopPayload](finalPayload))) {
case ((amount, expiry, payloads), hop) =>
val payload = hop match {
// Since we don't have any scenario where we add tlv data for intermediate hops, we use legacy payloads.
case hop: ChannelHop => Onion.RelayLegacyPayload(hop.lastUpdate.shortChannelId, amount, expiry)
case hop: ChannelHop => Onion.ChannelRelayTlvPayload(hop.lastUpdate.shortChannelId, amount, expiry)
case hop: NodeHop => Onion.createNodeRelayPayload(amount, expiry, hop.nextNodeId)
}
(amount + hop.fee(amount), expiry + hop.cltvExpiryDelta, payload +: payloads)

View file

@ -123,7 +123,8 @@ object PaymentRequest {
val prefixes = Map(
Block.RegtestGenesisBlock.hash -> "lnbcrt",
Block.TestnetGenesisBlock.hash -> "lntb",
Block.LivenetGenesisBlock.hash -> "lnbc")
Block.LivenetGenesisBlock.hash -> "lnbc"
)
def apply(chainHash: ByteVector32,
amount: Option[MilliSatoshi],
@ -135,30 +136,31 @@ object PaymentRequest {
expirySeconds: Option[Long] = None,
extraHops: List[List[ExtraHop]] = Nil,
timestamp: Long = System.currentTimeMillis() / 1000L,
features: Option[PaymentRequestFeatures] = Some(PaymentRequestFeatures(Features.VariableLengthOnion.mandatory, Features.PaymentSecret.mandatory))): PaymentRequest = {
paymentSecret: ByteVector32 = randomBytes32(),
features: PaymentRequestFeatures = PaymentRequestFeatures(Features.VariableLengthOnion.mandatory, Features.PaymentSecret.mandatory)): PaymentRequest = {
require(features.requirePaymentSecret, "invoices must require a payment secret")
val prefix = prefixes(chainHash)
val tags = {
val defaultTags = List(
Some(PaymentHash(paymentHash)),
Some(Description(description)),
Some(PaymentSecret(paymentSecret)),
fallbackAddress.map(FallbackAddress(_)),
expirySeconds.map(Expiry(_)),
Some(MinFinalCltvExpiry(minFinalCltvExpiryDelta.toInt)),
features).flatten
val paymentSecretTag = if (features.exists(_.allowPaymentSecret)) PaymentSecret(randomBytes32()) :: Nil else Nil
Some(features)
).flatten
val routingInfoTags = extraHops.map(RoutingInfo)
defaultTags ++ paymentSecretTag ++ routingInfoTags
defaultTags ++ routingInfoTags
}
PaymentRequest(
prefix = prefix,
amount = amount,
timestamp = timestamp,
nodeId = privateKey.publicKey,
tags = tags,
signature = ByteVector.empty)
.sign(privateKey)
signature = ByteVector.empty
).sign(privateKey)
}
case class Bolt11Data(timestamp: Long, taggedFields: List[TaggedField], signature: ByteVector)
@ -485,7 +487,7 @@ object PaymentRequest {
}
// char -> 5 bits value
val charToint5: Map[Char, BitVector] = Bech32.alphabet.zipWithIndex.toMap.mapValues(BitVector.fromInt(_, size = 5, ordering = ByteOrdering.BigEndian)).toMap
val charToint5: Map[Char, BitVector] = Bech32.alphabet.zipWithIndex.toMap.view.mapValues(BitVector.fromInt(_, size = 5, ordering = ByteOrdering.BigEndian)).toMap
// TODO: could be optimized by preallocating the resulting buffer
def string2Bits(data: String): BitVector = data.map(charToint5).foldLeft(BitVector.empty)(_ ++ _)

View file

@ -62,7 +62,7 @@ class MultiPartHandler(nodeParams: NodeParams, register: ActorRef, db: IncomingP
val allowMultiPart = nodeParams.features.hasFeature(Features.BasicMultiPartPayment)
val f2 = if (allowMultiPart) Seq(Features.BasicMultiPartPayment.optional) else Nil
val f3 = if (nodeParams.enableTrampolinePayment) Seq(Features.TrampolinePayment.optional) else Nil
Some(PaymentRequest.PaymentRequestFeatures(f1 ++ f2 ++ f3: _*))
PaymentRequest.PaymentRequestFeatures(f1 ++ f2 ++ f3: _*)
}
val paymentRequest = PaymentRequest(nodeParams.chainHash, amount_opt, paymentHash, nodeParams.privateKey, desc, nodeParams.minFinalExpiryDelta, fallbackAddress_opt, expirySeconds = Some(expirySeconds), extraHops = extraHops, features = features)
log.debug("generated payment request={} from amount={}", PaymentRequest.write(paymentRequest), amount_opt)
@ -101,9 +101,8 @@ class MultiPartHandler(nodeParams: NodeParams, register: ActorRef, db: IncomingP
} else {
PaymentRequestFeatures(Features.PaymentSecret.mandatory, Features.VariableLengthOnion.mandatory)
}
// Insert a fake invoice and then restart the incoming payment handler
val paymentRequest = PaymentRequest(nodeParams.chainHash, amount, paymentHash, nodeParams.privateKey, desc, nodeParams.minFinalExpiryDelta, features = Some(features))
val paymentRequest = PaymentRequest(nodeParams.chainHash, amount, paymentHash, nodeParams.privateKey, desc, nodeParams.minFinalExpiryDelta, paymentSecret = p.payload.paymentSecret, features = features)
log.debug("generated fake payment request={} from amount={} (KeySend)", PaymentRequest.write(paymentRequest), amount)
db.addIncomingPayment(paymentRequest, paymentPreimage, paymentType = PaymentType.KeySend)
ctx.self ! p
@ -245,10 +244,10 @@ object MultiPartHandler {
if (payment.payload.amount < payment.payload.totalAmount && !pr.features.allowMultiPart) {
log.warning("received multi-part payment but invoice doesn't support it for amount={} totalAmount={}", payment.add.amountMsat, payment.payload.totalAmount)
false
} else if (payment.payload.amount < payment.payload.totalAmount && pr.paymentSecret != payment.payload.paymentSecret) {
} else if (payment.payload.amount < payment.payload.totalAmount && !pr.paymentSecret.contains(payment.payload.paymentSecret)) {
log.warning("received multi-part payment with invalid secret={} for amount={} totalAmount={}", payment.payload.paymentSecret, payment.add.amountMsat, payment.payload.totalAmount)
false
} else if (payment.payload.paymentSecret.isDefined && pr.paymentSecret != payment.payload.paymentSecret) {
} else if (!pr.paymentSecret.contains(payment.payload.paymentSecret)) {
log.warning("received payment with invalid secret={} for amount={} totalAmount={}", payment.payload.paymentSecret, payment.add.amountMsat, payment.payload.totalAmount)
false
} else {

View file

@ -37,7 +37,7 @@ import fr.acinq.eclair.payment.send.{MultiPartPaymentLifecycle, PaymentInitiator
import fr.acinq.eclair.router.Router.RouteParams
import fr.acinq.eclair.router.{BalanceTooLow, RouteCalculation, RouteNotFound}
import fr.acinq.eclair.wire.protocol._
import fr.acinq.eclair.{CltvExpiry, Features, Logs, MilliSatoshi, NodeParams, nodeFee, randomBytes32}
import fr.acinq.eclair.{CltvExpiry, Features, Logs, MilliSatoshi, NodeParams, UInt64, nodeFee, randomBytes32}
import java.util.UUID
import scala.collection.immutable.Queue
@ -81,7 +81,6 @@ object NodeRelay {
register: ActorRef,
relayId: UUID,
nodeRelayPacket: NodeRelayPacket,
paymentSecret: ByteVector32,
outgoingPaymentFactory: OutgoingPaymentFactory): Behavior[Command] =
Behaviors.setup { context =>
val paymentHash = nodeRelayPacket.add.paymentHash
@ -97,7 +96,7 @@ object NodeRelay {
context.messageAdapter[MultiPartPaymentFSM.MultiPartPaymentSucceeded](WrappedMultiPartPaymentSucceeded)
}.toClassic
val incomingPaymentHandler = context.actorOf(MultiPartPaymentFSM.props(nodeParams, paymentHash, totalAmountIn, mppFsmAdapters))
new NodeRelay(nodeParams, parent, register, relayId, paymentHash, paymentSecret, context, outgoingPaymentFactory)
new NodeRelay(nodeParams, parent, register, relayId, paymentHash, nodeRelayPacket.outerPayload.paymentSecret, context, outgoingPaymentFactory)
.receiving(Queue.empty, nodeRelayPacket.innerPayload, nodeRelayPacket.nextPacket, incomingPaymentHandler)
}
}
@ -110,6 +109,8 @@ object NodeRelay {
Some(TrampolineExpiryTooSoon)
} else if (payloadOut.outgoingCltv <= CltvExpiry(nodeParams.currentBlockHeight)) {
Some(TrampolineExpiryTooSoon)
} else if (payloadOut.invoiceFeatures.isDefined && payloadOut.paymentSecret.isEmpty) {
Some(InvalidOnionPayload(UInt64(8), 0)) // payment secret field is missing
} else {
None
}
@ -178,21 +179,11 @@ class NodeRelay private(nodeParams: NodeParams,
*/
private def receiving(htlcs: Queue[UpdateAddHtlc], nextPayload: Onion.NodeRelayPayload, nextPacket: OnionRoutingPacket, handler: ActorRef): Behavior[Command] =
Behaviors.receiveMessagePartial {
case Relay(IncomingPacket.NodeRelayPacket(add, outer, _, _)) => outer.paymentSecret match {
// TODO: @pm: maybe those checks should be done by the mpp FSM?
case None =>
context.log.warn("rejecting htlc #{} from channel {}: missing payment secret", add.id, add.channelId)
rejectHtlc(add.id, add.channelId, add.amountMsat)
Behaviors.same
case Some(incomingSecret) if incomingSecret != paymentSecret =>
context.log.warn("rejecting htlc #{} from channel {}: payment secret doesn't match other HTLCs in the set", add.id, add.channelId)
rejectHtlc(add.id, add.channelId, add.amountMsat)
Behaviors.same
case Some(incomingSecret) if incomingSecret == paymentSecret =>
context.log.debug("forwarding incoming htlc #{} from channel {} to the payment FSM", add.id, add.channelId)
handler ! MultiPartPaymentFSM.HtlcPart(outer.totalAmount, add)
receiving(htlcs :+ add, nextPayload, nextPacket, handler)
}
case Relay(IncomingPacket.NodeRelayPacket(add, outer, _, _)) =>
require(outer.paymentSecret == paymentSecret, "payment secret mismatch")
context.log.debug("forwarding incoming htlc #{} from channel {} to the payment FSM", add.id, add.channelId)
handler ! MultiPartPaymentFSM.HtlcPart(outer.totalAmount, add)
receiving(htlcs :+ add, nextPayload, nextPacket, handler)
case WrappedMultiPartPaymentFailed(MultiPartPaymentFSM.MultiPartPaymentFailed(_, failure, parts)) =>
context.log.warn("could not complete incoming multi-part payment (parts={} paidAmount={} failure={})", parts.size, parts.map(_.amount).sum, failure)
Metrics.recordPaymentRelayFailed(failure.getClass.getSimpleName, Tags.RelayType.Trampoline)
@ -276,20 +267,20 @@ class NodeRelay private(nodeParams: NodeParams,
val payFSM = payloadOut.invoiceFeatures match {
case Some(features) =>
val routingHints = payloadOut.invoiceRoutingInfo.map(_.map(_.toSeq).toSeq).getOrElse(Nil)
payloadOut.paymentSecret match {
case Some(paymentSecret) 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, routingHints, Some(routeParams))
val payFSM = outgoingPaymentFactory.spawnOutgoingPayFSM(context, paymentCfg, multiPart = true)
payFSM ! payment
payFSM
case _ =>
context.log.debug("sending the payment to non-trampoline recipient without MPP")
val finalPayload = Onion.createSinglePartPayload(payloadOut.amountToForward, payloadOut.outgoingCltv, payloadOut.paymentSecret)
val payment = SendPayment(payFsmAdapters, payloadOut.outgoingNodeId, finalPayload, nodeParams.maxPaymentAttempts, routingHints, Some(routeParams))
val payFSM = outgoingPaymentFactory.spawnOutgoingPayFSM(context, paymentCfg, multiPart = false)
payFSM ! payment
payFSM
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, routingHints, Some(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 finalPayload = Onion.createSinglePartPayload(payloadOut.amountToForward, payloadOut.outgoingCltv, paymentSecret)
val payment = SendPayment(payFsmAdapters, payloadOut.outgoingNodeId, finalPayload, nodeParams.maxPaymentAttempts, routingHints, Some(routeParams))
val payFSM = outgoingPaymentFactory.spawnOutgoingPayFSM(context, paymentCfg, multiPart = false)
payFSM ! payment
payFSM
}
case None =>
context.log.debug("sending the payment to the next trampoline node")

View file

@ -19,10 +19,7 @@ package fr.acinq.eclair.payment.relay
import akka.actor.typed.scaladsl.Behaviors
import akka.actor.typed.{ActorRef, Behavior}
import fr.acinq.bitcoin.ByteVector32
import fr.acinq.eclair.channel.CMD_FAIL_HTLC
import fr.acinq.eclair.db.PendingCommandsDb
import fr.acinq.eclair.payment._
import fr.acinq.eclair.wire.protocol.IncorrectOrUnknownPaymentDetails
import fr.acinq.eclair.{Logs, NodeParams}
import java.util.UUID
@ -65,28 +62,19 @@ object NodeRelayer {
Behaviors.receiveMessage {
case Relay(nodeRelayPacket) =>
val htlcIn = nodeRelayPacket.add
nodeRelayPacket.outerPayload.paymentSecret match {
case Some(paymentSecret) =>
val childKey = PaymentKey(htlcIn.paymentHash, paymentSecret)
children.get(childKey) match {
case Some(handler) =>
context.log.debug("forwarding incoming htlc #{} from channel {} to existing handler", htlcIn.id, htlcIn.channelId)
handler ! NodeRelay.Relay(nodeRelayPacket)
Behaviors.same
case None =>
val relayId = UUID.randomUUID()
context.log.debug(s"spawning a new handler with relayId=$relayId")
val handler = context.spawn(NodeRelay.apply(nodeParams, context.self, register, relayId, nodeRelayPacket, childKey.paymentSecret, outgoingPaymentFactory), relayId.toString)
context.log.debug("forwarding incoming htlc #{} from channel {} to new handler", htlcIn.id, htlcIn.channelId)
handler ! NodeRelay.Relay(nodeRelayPacket)
apply(nodeParams, register, outgoingPaymentFactory, children + (childKey -> handler))
}
case None =>
context.log.warn("rejecting htlc #{} from channel {}: missing payment secret", htlcIn.id, htlcIn.channelId)
val failureMessage = IncorrectOrUnknownPaymentDetails(htlcIn.amountMsat, nodeParams.currentBlockHeight)
val cmd = CMD_FAIL_HTLC(htlcIn.id, Right(failureMessage), commit = true)
PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, htlcIn.channelId, cmd)
val childKey = PaymentKey(htlcIn.paymentHash, nodeRelayPacket.outerPayload.paymentSecret)
children.get(childKey) match {
case Some(handler) =>
context.log.debug("forwarding incoming htlc #{} from channel {} to existing handler", htlcIn.id, htlcIn.channelId)
handler ! NodeRelay.Relay(nodeRelayPacket)
Behaviors.same
case None =>
val relayId = UUID.randomUUID()
context.log.debug(s"spawning a new handler with relayId=$relayId")
val handler = context.spawn(NodeRelay.apply(nodeParams, context.self, register, relayId, nodeRelayPacket, outgoingPaymentFactory), relayId.toString)
context.log.debug("forwarding incoming htlc #{} from channel {} to new handler", htlcIn.id, htlcIn.channelId)
handler ! NodeRelay.Relay(nodeRelayPacket)
apply(nodeParams, register, outgoingPaymentFactory, children + (childKey -> handler))
}
case RelayComplete(childHandler, paymentHash, paymentSecret) =>
// we do a back-and-forth between parent and child before stopping the child to prevent a race condition

View file

@ -19,10 +19,11 @@ package fr.acinq.eclair.payment.send
import akka.actor.{Actor, ActorLogging, ActorRef, Props}
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.eclair.crypto.Sphinx.DecryptedFailurePacket
import fr.acinq.eclair.payment.{PaymentEvent, PaymentFailed, RemoteFailure}
import fr.acinq.eclair.payment.{PaymentEvent, PaymentFailed, PaymentRequest, RemoteFailure}
import fr.acinq.eclair.router.{Announcements, Router}
import fr.acinq.eclair.wire.protocol.IncorrectOrUnknownPaymentDetails
import fr.acinq.eclair.{MilliSatoshiLong, NodeParams, randomBytes32, randomLong}
import scodec.bits.ByteVector
import scala.concurrent.duration._
@ -52,9 +53,18 @@ class Autoprobe(nodeParams: NodeParams, router: ActorRef, paymentInitiator: Acto
case TickProbe =>
pickPaymentDestination(nodeParams.nodeId, routingData) match {
case Some(targetNodeId) =>
val paymentHash = randomBytes32() // we don't even know the preimage (this needs to be a secure random!)
log.info(s"sending payment probe to node=$targetNodeId payment_hash=$paymentHash")
paymentInitiator ! PaymentInitiator.SendPaymentRequest(PAYMENT_AMOUNT_MSAT, paymentHash, targetNodeId, maxAttempts = 1)
val fakeInvoice = PaymentRequest(
PaymentRequest.prefixes(nodeParams.chainHash),
Some(PAYMENT_AMOUNT_MSAT),
System.currentTimeMillis(),
targetNodeId,
List(
PaymentRequest.PaymentHash(randomBytes32()), // we don't even know the preimage (this needs to be a secure random!)
PaymentRequest.Description("ignored"),
),
ByteVector.empty)
log.info(s"sending payment probe to node=$targetNodeId payment_hash=${fakeInvoice.paymentHash}")
paymentInitiator ! PaymentInitiator.SendPayment(PAYMENT_AMOUNT_MSAT, fakeInvoice, maxAttempts = 1)
case None =>
log.info(s"could not find a destination, re-scheduling")
scheduleProbe()
@ -76,7 +86,7 @@ class Autoprobe(nodeParams: NodeParams, router: ActorRef, paymentInitiator: Acto
object Autoprobe {
def props(nodeParams: NodeParams, router: ActorRef, paymentInitiator: ActorRef) = Props(classOf[Autoprobe], nodeParams, router, paymentInitiator)
def props(nodeParams: NodeParams, router: ActorRef, paymentInitiator: ActorRef) = Props(new Autoprobe(nodeParams, router, paymentInitiator))
val ROUTING_TABLE_REFRESH_INTERVAL = 10 minutes

View file

@ -17,8 +17,8 @@
package fr.acinq.eclair.payment.send
import akka.actor.{Actor, ActorContext, ActorLogging, ActorRef, Props}
import fr.acinq.bitcoin.ByteVector32
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.bitcoin.{ByteVector32, Crypto}
import fr.acinq.eclair.Features.BasicMultiPartPayment
import fr.acinq.eclair.channel.Channel
import fr.acinq.eclair.crypto.Sphinx
@ -27,10 +27,8 @@ import fr.acinq.eclair.payment.PaymentRequest.ExtraHop
import fr.acinq.eclair.payment._
import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle.{PreimageReceived, SendMultiPartPayment}
import fr.acinq.eclair.payment.send.PaymentError._
import fr.acinq.eclair.payment.send.PaymentLifecycle.{SendPayment, SendPaymentToRoute}
import fr.acinq.eclair.router.RouteNotFound
import fr.acinq.eclair.router.Router._
import fr.acinq.eclair.wire.protocol.Onion.FinalLegacyPayload
import fr.acinq.eclair.wire.protocol._
import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, MilliSatoshi, MilliSatoshiLong, NodeParams, randomBytes32}
@ -46,33 +44,38 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn
override def receive: Receive = main(Map.empty)
def main(pending: Map[UUID, PendingPayment]): Receive = {
case r: SendPaymentRequest =>
case r: SendPayment =>
val paymentId = UUID.randomUUID()
if (!r.blockUntilComplete) {
// Immediately return the paymentId
sender ! paymentId
}
val paymentCfg = SendPaymentConfig(paymentId, paymentId, r.externalId, r.paymentHash, r.recipientAmount, r.recipientNodeId, Upstream.Local(paymentId), r.paymentRequest, storeInDb = true, publishEvent = true, Nil)
val paymentCfg = SendPaymentConfig(paymentId, paymentId, r.externalId, r.paymentHash, r.recipientAmount, r.recipientNodeId, Upstream.Local(paymentId), Some(r.paymentRequest), storeInDb = true, publishEvent = true, Nil)
val finalExpiry = r.finalExpiry(nodeParams.currentBlockHeight)
r.paymentRequest match {
case Some(invoice) if !invoice.features.areSupported(nodeParams) =>
sender ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(Nil, UnsupportedFeatures(invoice.features.features)) :: Nil)
case Some(invoice) if invoice.features.allowMultiPart && nodeParams.features.hasFeature(BasicMultiPartPayment) =>
invoice.paymentSecret match {
case Some(paymentSecret) =>
val fsm = outgoingPaymentFactory.spawnOutgoingMultiPartPayment(context, paymentCfg)
fsm ! SendMultiPartPayment(sender, paymentSecret, r.recipientNodeId, r.recipientAmount, finalExpiry, r.maxAttempts, r.assistedRoutes, r.routeParams, userCustomTlvs = r.userCustomTlvs)
case None =>
sender ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(Nil, PaymentSecretMissing) :: Nil)
}
case _ =>
val paymentSecret = r.paymentRequest.flatMap(_.paymentSecret)
r.paymentRequest.paymentSecret match {
case _ if !r.paymentRequest.features.areSupported(nodeParams) =>
sender ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(Nil, UnsupportedFeatures(r.paymentRequest.features.features)) :: Nil)
case None =>
sender ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(Nil, PaymentSecretMissing) :: Nil)
case Some(paymentSecret) if r.paymentRequest.features.allowMultiPart && nodeParams.features.hasFeature(BasicMultiPartPayment) =>
val fsm = outgoingPaymentFactory.spawnOutgoingMultiPartPayment(context, paymentCfg)
fsm ! SendMultiPartPayment(sender, paymentSecret, r.recipientNodeId, r.recipientAmount, finalExpiry, r.maxAttempts, r.assistedRoutes, r.routeParams, userCustomTlvs = r.userCustomTlvs)
case Some(paymentSecret) =>
val finalPayload = Onion.createSinglePartPayload(r.recipientAmount, finalExpiry, paymentSecret, r.userCustomTlvs)
val fsm = outgoingPaymentFactory.spawnOutgoingPayment(context, paymentCfg)
fsm ! SendPayment(sender, r.recipientNodeId, finalPayload, r.maxAttempts, r.assistedRoutes, r.routeParams)
fsm ! PaymentLifecycle.SendPayment(sender, r.recipientNodeId, finalPayload, r.maxAttempts, r.assistedRoutes, r.routeParams)
}
case r: SendTrampolinePaymentRequest =>
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, Nil)
val finalExpiry = Channel.MIN_CLTV_EXPIRY_DELTA.toCltvExpiry(nodeParams.currentBlockHeight + 1)
val finalPayload = Onion.FinalTlvPayload(TlvStream(Seq(OnionTlv.AmountToForward(r.recipientAmount), OnionTlv.OutgoingCltv(finalExpiry), OnionTlv.PaymentData(randomBytes32(), r.recipientAmount), OnionTlv.KeySend(r.paymentPreimage)), r.userCustomTlvs))
val fsm = outgoingPaymentFactory.spawnOutgoingPayment(context, paymentCfg)
fsm ! PaymentLifecycle.SendPayment(sender, r.recipientNodeId, finalPayload, r.maxAttempts, routeParams = r.routeParams)
case r: SendTrampolinePayment =>
val paymentId = UUID.randomUUID()
sender ! paymentId
r.trampolineAttempts match {
@ -121,33 +124,33 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn
context become main(pending - ps.id)
})
case r: SendPaymentToRouteRequest =>
case r: SendPaymentToRoute =>
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 = outgoingPaymentFactory.spawnOutgoingPayment(context, paymentCfg)
r.trampolineNodes match {
case _ if r.paymentRequest.paymentSecret.isEmpty =>
sender ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(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}")
// 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.fallbackFinalExpiryDelta), r.trampolineFees, r.trampolineExpiryDelta)
payFsm ! SendPaymentToRoute(sender, Left(r.route), Onion.createMultiPartPayload(r.amount, trampolineAmount, trampolineExpiry, trampolineSecret, Seq(OnionTlv.TrampolineOnion(trampolineOnion))), r.paymentRequest.routingInfo)
val payFsm = outgoingPaymentFactory.spawnOutgoingPayment(context, paymentCfg)
val (trampolineAmount, trampolineExpiry, trampolineOnion) = buildTrampolinePayment(SendTrampolinePayment(r.recipientAmount, r.paymentRequest, trampoline, Seq((r.trampolineFees, r.trampolineExpiryDelta)), r.fallbackFinalExpiryDelta), r.trampolineFees, r.trampolineExpiryDelta)
payFsm ! PaymentLifecycle.SendPaymentToRoute(sender, Left(r.route), Onion.createMultiPartPayload(r.amount, trampolineAmount, trampolineExpiry, trampolineSecret, Seq(OnionTlv.TrampolineOnion(trampolineOnion))), r.paymentRequest.routingInfo)
case Nil =>
sender ! SendPaymentToRouteResponse(paymentId, parentPaymentId, None)
r.paymentRequest.paymentSecret match {
case Some(paymentSecret) => payFsm ! SendPaymentToRoute(sender, Left(r.route), Onion.createMultiPartPayload(r.amount, r.recipientAmount, finalExpiry, paymentSecret), r.paymentRequest.routingInfo)
case None => payFsm ! SendPaymentToRoute(sender, Left(r.route), FinalLegacyPayload(r.recipientAmount, finalExpiry), r.paymentRequest.routingInfo)
}
val payFsm = outgoingPaymentFactory.spawnOutgoingPayment(context, paymentCfg)
payFsm ! PaymentLifecycle.SendPaymentToRoute(sender, Left(r.route), Onion.createMultiPartPayload(r.amount, r.recipientAmount, finalExpiry, r.paymentRequest.paymentSecret.get), r.paymentRequest.routingInfo)
case _ =>
sender ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(Nil, TrampolineMultiNodeNotSupported) :: Nil)
}
}
private def buildTrampolinePayment(r: SendTrampolinePaymentRequest, trampolineFees: MilliSatoshi, trampolineExpiryDelta: CltvExpiryDelta): (MilliSatoshi, CltvExpiry, OnionRoutingPacket) = {
private def buildTrampolinePayment(r: SendTrampolinePayment, trampolineFees: MilliSatoshi, trampolineExpiryDelta: CltvExpiryDelta): (MilliSatoshi, CltvExpiry, OnionRoutingPacket) = {
val trampolineRoute = Seq(
NodeHop(nodeParams.nodeId, r.trampolineNodeId, nodeParams.expiryDelta, 0 msat),
NodeHop(r.trampolineNodeId, r.recipientNodeId, trampolineExpiryDelta, trampolineFees) // for now we only use a single trampoline hop
@ -155,7 +158,7 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn
val finalPayload = if (r.paymentRequest.features.allowMultiPart) {
Onion.createMultiPartPayload(r.recipientAmount, r.recipientAmount, r.finalExpiry(nodeParams.currentBlockHeight), r.paymentRequest.paymentSecret.get)
} else {
Onion.createSinglePartPayload(r.recipientAmount, r.finalExpiry(nodeParams.currentBlockHeight), r.paymentRequest.paymentSecret)
Onion.createSinglePartPayload(r.recipientAmount, r.finalExpiry(nodeParams.currentBlockHeight), r.paymentRequest.paymentSecret.get)
}
// We assume that the trampoline node supports multi-part payments (it should).
val (trampolineAmount, trampolineExpiry, trampolineOnion) = if (r.paymentRequest.features.allowTrampoline) {
@ -166,7 +169,7 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn
(trampolineAmount, trampolineExpiry, trampolineOnion.packet)
}
private def sendTrampolinePayment(paymentId: UUID, r: SendTrampolinePaymentRequest, trampolineFees: MilliSatoshi, trampolineExpiryDelta: CltvExpiryDelta): Unit = {
private def sendTrampolinePayment(paymentId: UUID, r: SendTrampolinePayment, 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.
val trampolineSecret = randomBytes32()
@ -199,7 +202,7 @@ object PaymentInitiator {
def props(nodeParams: NodeParams, outgoingPaymentFactory: MultiPartPaymentFactory) = Props(new PaymentInitiator(nodeParams, outgoingPaymentFactory))
case class PendingPayment(sender: ActorRef, remainingAttempts: Seq[(MilliSatoshi, CltvExpiryDelta)], r: SendTrampolinePaymentRequest)
case class PendingPayment(sender: ActorRef, remainingAttempts: Seq[(MilliSatoshi, CltvExpiryDelta)], r: SendTrampolinePayment)
/**
* We temporarily let the caller decide to use Trampoline (instead of a normal payment) and set the fees/cltv.
@ -216,12 +219,12 @@ object PaymentInitiator {
* @param fallbackFinalExpiryDelta expiry delta for the final recipient when the [[paymentRequest]] doesn't specify it.
* @param routeParams (optional) parameters to fine-tune the routing algorithm.
*/
case class SendTrampolinePaymentRequest(recipientAmount: MilliSatoshi,
paymentRequest: PaymentRequest,
trampolineNodeId: PublicKey,
trampolineAttempts: Seq[(MilliSatoshi, CltvExpiryDelta)],
fallbackFinalExpiryDelta: CltvExpiryDelta = Channel.MIN_CLTV_EXPIRY_DELTA,
routeParams: Option[RouteParams] = None) {
case class SendTrampolinePayment(recipientAmount: MilliSatoshi,
paymentRequest: PaymentRequest,
trampolineNodeId: PublicKey,
trampolineAttempts: Seq[(MilliSatoshi, CltvExpiryDelta)],
fallbackFinalExpiryDelta: CltvExpiryDelta = Channel.MIN_CLTV_EXPIRY_DELTA,
routeParams: Option[RouteParams] = None) {
val recipientNodeId = paymentRequest.nodeId
val paymentHash = paymentRequest.paymentHash
@ -231,30 +234,48 @@ object PaymentInitiator {
/**
* @param recipientAmount amount that should be received by the final recipient (usually from a Bolt 11 invoice).
* @param paymentHash payment hash.
* @param recipientNodeId id of the final recipient.
* @param paymentRequest Bolt 11 invoice.
* @param maxAttempts maximum number of retries.
* @param fallbackFinalExpiryDelta expiry delta for the final recipient when the [[paymentRequest]] doesn't specify it.
* @param paymentRequest (optional) Bolt 11 invoice.
* @param externalId (optional) externally-controlled identifier (to reconcile between application DB and eclair DB).
* @param assistedRoutes (optional) routing hints (usually from a Bolt 11 invoice).
* @param routeParams (optional) parameters to fine-tune the routing algorithm.
* @param userCustomTlvs (optional) user-defined custom tlvs that will be added to the onion sent to the target node.
* @param blockUntilComplete (optional) if true, wait until the payment completes before returning a result.
*/
case class SendPaymentRequest(recipientAmount: MilliSatoshi,
paymentHash: ByteVector32,
recipientNodeId: PublicKey,
maxAttempts: Int,
fallbackFinalExpiryDelta: CltvExpiryDelta = Channel.MIN_CLTV_EXPIRY_DELTA,
paymentRequest: Option[PaymentRequest] = None,
externalId: Option[String] = None,
assistedRoutes: Seq[Seq[ExtraHop]] = Nil,
routeParams: Option[RouteParams] = None,
userCustomTlvs: Seq[GenericTlv] = Nil,
blockUntilComplete: Boolean = false) {
case class SendPayment(recipientAmount: MilliSatoshi,
paymentRequest: PaymentRequest,
maxAttempts: Int,
fallbackFinalExpiryDelta: CltvExpiryDelta = Channel.MIN_CLTV_EXPIRY_DELTA,
externalId: Option[String] = None,
assistedRoutes: Seq[Seq[ExtraHop]] = Nil,
routeParams: Option[RouteParams] = None,
userCustomTlvs: Seq[GenericTlv] = Nil,
blockUntilComplete: Boolean = false) {
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) = paymentRequest.flatMap(_.minFinalCltvExpiryDelta).getOrElse(fallbackFinalExpiryDelta).toCltvExpiry(currentBlockHeight + 1)
def finalExpiry(currentBlockHeight: Long) = paymentRequest.minFinalCltvExpiryDelta.getOrElse(fallbackFinalExpiryDelta).toCltvExpiry(currentBlockHeight + 1)
}
/**
* @param recipientAmount amount that should be received by the final recipient.
* @param recipientNodeId id of the final recipient.
* @param paymentPreimage payment preimage.
* @param maxAttempts maximum number of retries.
* @param externalId (optional) externally-controlled identifier (to reconcile between application DB and eclair DB).
* @param routeParams (optional) parameters to fine-tune the routing algorithm.
* @param userCustomTlvs (optional) user-defined custom tlvs that will be added to the onion sent to the target node.
*/
case class SendSpontaneousPayment(recipientAmount: MilliSatoshi,
recipientNodeId: PublicKey,
paymentPreimage: ByteVector32,
maxAttempts: Int,
externalId: Option[String] = None,
routeParams: Option[RouteParams] = None,
userCustomTlvs: Seq[GenericTlv] = Nil) {
val paymentHash = Crypto.sha256(paymentPreimage)
}
/**
@ -274,13 +295,13 @@ object PaymentInitiator {
* 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 paymentRequest Bolt 11 invoice.
* @param fallbackFinalExpiryDelta expiry delta for the final recipient when the [[paymentRequest]] doesn't specify it.
* @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.
* @param paymentRequest Bolt 11 invoice.
* @param fallbackFinalExpiryDelta expiry delta for the final recipient when the [[paymentRequest]] doesn't specify it.
* @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.
@ -291,17 +312,17 @@ object PaymentInitiator {
* @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,
fallbackFinalExpiryDelta: CltvExpiryDelta = Channel.MIN_CLTV_EXPIRY_DELTA,
route: PredefinedRoute,
trampolineSecret: Option[ByteVector32],
trampolineFees: MilliSatoshi,
trampolineExpiryDelta: CltvExpiryDelta,
trampolineNodes: Seq[PublicKey]) {
case class SendPaymentToRoute(amount: MilliSatoshi,
recipientAmount: MilliSatoshi,
paymentRequest: PaymentRequest,
fallbackFinalExpiryDelta: CltvExpiryDelta = Channel.MIN_CLTV_EXPIRY_DELTA,
route: PredefinedRoute,
externalId: Option[String],
parentId: Option[UUID],
trampolineSecret: Option[ByteVector32],
trampolineFees: MilliSatoshi,
trampolineExpiryDelta: CltvExpiryDelta,
trampolineNodes: Seq[PublicKey]) {
val recipientNodeId = paymentRequest.nodeId
val paymentHash = paymentRequest.paymentHash

View file

@ -175,19 +175,18 @@ object Onion {
* We use the following architecture for onion payloads:
*
* PerHopPayload
* _______________________/\__________________________
* / \
* RelayPayload FinalPayload
* _______________/\_________________ ____/\______
* / \ / \
* ChannelRelayPayload \ / \
* ________/\______________ \ / \
* / \ \ / \
* RelayLegacyPayload ChannelRelayTlvPayload NodeRelayPayload FinalLegacyPayload FinalTlvPayload
* _______________________/\_______________
* / \
* RelayPayload FinalPayload
* _______________/\_________________ \______
* / \ \
* ChannelRelayPayload \ \
* ________/\______________ \ \
* / \ \ \
* RelayLegacyPayload ChannelRelayTlvPayload NodeRelayPayload FinalTlvPayload
*
* We also introduce additional traits to separate payloads based on their encoding (PerHopPayloadFormat) and on the
* type of onion packet they can be used with (PacketType).
*
*/
sealed trait PerHopPayloadFormat
@ -229,29 +228,29 @@ object Onion {
sealed trait FinalPayload extends PerHopPayload with PerHopPayloadFormat with TrampolinePacket with PaymentPacket {
val amount: MilliSatoshi
val expiry: CltvExpiry
val paymentSecret: Option[ByteVector32]
val paymentSecret: ByteVector32
val totalAmount: MilliSatoshi
val paymentPreimage: Option[ByteVector32]
}
case class RelayLegacyPayload(outgoingChannelId: ShortChannelId, amountToForward: MilliSatoshi, outgoingCltv: CltvExpiry) extends ChannelRelayPayload with LegacyFormat
case class FinalLegacyPayload(amount: MilliSatoshi, expiry: CltvExpiry) extends FinalPayload with LegacyFormat {
override val paymentSecret = None
override val totalAmount = amount
override val paymentPreimage = None
}
case class ChannelRelayTlvPayload(records: TlvStream[OnionTlv]) extends ChannelRelayPayload with TlvFormat {
override val amountToForward = records.get[AmountToForward].get.amount
override val outgoingCltv = records.get[OutgoingCltv].get.cltv
override val outgoingChannelId = records.get[OutgoingChannelId].get.shortChannelId
}
object ChannelRelayTlvPayload {
def apply(outgoingChannelId: ShortChannelId, amountToForward: MilliSatoshi, outgoingCltv: CltvExpiry): ChannelRelayTlvPayload =
ChannelRelayTlvPayload(TlvStream(OnionTlv.AmountToForward(amountToForward), OnionTlv.OutgoingCltv(outgoingCltv), OnionTlv.OutgoingChannelId(outgoingChannelId)))
}
case class NodeRelayPayload(records: TlvStream[OnionTlv]) extends RelayPayload with TlvFormat with TrampolinePacket {
val amountToForward = records.get[AmountToForward].get.amount
val outgoingCltv = records.get[OutgoingCltv].get.cltv
val outgoingNodeId = records.get[OutgoingNodeId].get.nodeId
// The following fields are only included in the trampoline-to-legacy case.
val totalAmount = records.get[PaymentData].map(_.totalAmount match {
case MilliSatoshi(0) => amountToForward
case totalAmount => totalAmount
@ -264,7 +263,7 @@ object Onion {
case class FinalTlvPayload(records: TlvStream[OnionTlv]) extends FinalPayload with TlvFormat {
override val amount = records.get[AmountToForward].get.amount
override val expiry = records.get[OutgoingCltv].get.cltv
override val paymentSecret = records.get[PaymentData].map(_.secret)
override val paymentSecret = records.get[PaymentData].get.secret
override val totalAmount = records.get[PaymentData].map(_.totalAmount match {
case MilliSatoshi(0) => amount
case totalAmount => totalAmount
@ -282,11 +281,8 @@ object Onion {
NodeRelayPayload(TlvStream(tlvs2))
}
def createSinglePartPayload(amount: MilliSatoshi, expiry: CltvExpiry, paymentSecret: Option[ByteVector32] = None, userCustomTlvs: Seq[GenericTlv] = Nil): FinalPayload = paymentSecret match {
case Some(paymentSecret) => FinalTlvPayload(TlvStream(Seq(AmountToForward(amount), OutgoingCltv(expiry), PaymentData(paymentSecret, amount)), userCustomTlvs))
case None if userCustomTlvs.nonEmpty => FinalTlvPayload(TlvStream(Seq(AmountToForward(amount), OutgoingCltv(expiry)), userCustomTlvs))
case None => FinalLegacyPayload(amount, expiry)
}
def createSinglePartPayload(amount: MilliSatoshi, expiry: CltvExpiry, paymentSecret: ByteVector32, userCustomTlvs: Seq[GenericTlv] = Nil): FinalPayload =
FinalTlvPayload(TlvStream(Seq(AmountToForward(amount), OutgoingCltv(expiry), PaymentData(paymentSecret, amount)), userCustomTlvs))
def createMultiPartPayload(amount: MilliSatoshi, totalAmount: MilliSatoshi, expiry: CltvExpiry, paymentSecret: ByteVector32, additionalTlvs: Seq[OnionTlv] = Nil, userCustomTlvs: Seq[GenericTlv] = Nil): FinalPayload =
FinalTlvPayload(TlvStream(AmountToForward(amount) +: OutgoingCltv(expiry) +: PaymentData(paymentSecret, totalAmount) +: additionalTlvs, userCustomTlvs))
@ -362,13 +358,6 @@ object OnionCodecs {
("outgoing_cltv_value" | cltvExpiry) ::
("unused_with_v0_version_on_header" | ignore(8 * 12))).as[RelayLegacyPayload]
private val legacyFinalPerHopPayloadCodec: Codec[FinalLegacyPayload] = (
("realm" | constant(ByteVector.fromByte(0))) ::
("short_channel_id" | ignore(8 * 8)) ::
("amount" | millisatoshi) ::
("expiry" | cltvExpiry) ::
("unused_with_v0_version_on_header" | ignore(8 * 12))).as[FinalLegacyPayload]
case class MissingRequiredTlv(tag: UInt64) extends Err {
// @formatter:off
val failureMessage: FailureMessage = InvalidOnionPayload(tag, 0)
@ -398,14 +387,13 @@ object OnionCodecs {
case NodeRelayPayload(tlvs) => tlvs
})
val finalPerHopPayloadCodec: Codec[FinalPayload] = fallback(tlvPerHopPayloadCodec, legacyFinalPerHopPayloadCodec).narrow({
case Left(tlvs) if tlvs.get[AmountToForward].isEmpty => Attempt.failure(MissingRequiredTlv(UInt64(2)))
case Left(tlvs) if tlvs.get[OutgoingCltv].isEmpty => Attempt.failure(MissingRequiredTlv(UInt64(4)))
case Left(tlvs) => Attempt.successful(FinalTlvPayload(tlvs))
case Right(legacy) => Attempt.successful(legacy)
val finalPerHopPayloadCodec: Codec[FinalPayload] = tlvPerHopPayloadCodec.narrow({
case tlvs if tlvs.get[AmountToForward].isEmpty => Attempt.failure(MissingRequiredTlv(UInt64(2)))
case tlvs if tlvs.get[OutgoingCltv].isEmpty => Attempt.failure(MissingRequiredTlv(UInt64(4)))
case tlvs if tlvs.get[PaymentData].isEmpty => Attempt.failure(MissingRequiredTlv(UInt64(8)))
case tlvs => Attempt.successful(FinalTlvPayload(tlvs))
}, {
case legacy: FinalLegacyPayload => Right(legacy)
case FinalTlvPayload(tlvs) => Left(tlvs)
case FinalTlvPayload(tlvs) => tlvs
})
def perHopPayloadCodecByPacketType[T <: PacketType](packetType: Sphinx.OnionRoutingPacket[T], isLastPacket: Boolean): Codec[PacketType] = packetType match {

View file

@ -16,8 +16,8 @@
package fr.acinq.eclair
import akka.actor.typed.scaladsl.adapter.actorRefAdapter
import akka.actor.ActorRef
import akka.actor.typed.scaladsl.adapter.actorRefAdapter
import akka.testkit.TestProbe
import akka.util.Timeout
import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey}
@ -32,7 +32,7 @@ import fr.acinq.eclair.payment.PaymentRequest
import fr.acinq.eclair.payment.PaymentRequest.ExtraHop
import fr.acinq.eclair.payment.receive.MultiPartHandler.ReceivePayment
import fr.acinq.eclair.payment.receive.PaymentHandler
import fr.acinq.eclair.payment.send.PaymentInitiator.{SendPaymentRequest, SendPaymentToRouteRequest}
import fr.acinq.eclair.payment.send.PaymentInitiator.{SendPayment, SendPaymentToRoute, SendSpontaneousPayment}
import fr.acinq.eclair.router.RouteCalculationSpec.makeUpdateShort
import fr.acinq.eclair.router.Router.{GetNetworkStats, GetNetworkStatsResponse, PredefinedNodeRoute, PublicChannel}
import fr.acinq.eclair.router.{Announcements, NetworkStats, Router, Stats}
@ -100,57 +100,57 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I
import f._
val eclair = new EclairImpl(kit)
val nodeId = PublicKey(hex"030bb6a5e0c6b203c7e2180fb78c7ba4bdce46126761d8201b91ddac089cdecc87")
eclair.send(None, nodeId, 123 msat, ByteVector32.Zeroes, invoice_opt = None)
val send = paymentInitiator.expectMsgType[SendPaymentRequest]
val nodePrivKey = randomKey()
val invoice0 = PaymentRequest(Block.RegtestGenesisBlock.hash, Some(123 msat), ByteVector32.Zeroes, nodePrivKey, "description", CltvExpiryDelta(18))
eclair.send(None, 123 msat, invoice0)
val send = paymentInitiator.expectMsgType[SendPayment]
assert(send.externalId === None)
assert(send.recipientNodeId === nodeId)
assert(send.recipientNodeId === nodePrivKey.publicKey)
assert(send.recipientAmount === 123.msat)
assert(send.paymentHash === ByteVector32.Zeroes)
assert(send.paymentRequest === None)
assert(send.paymentRequest === invoice0)
assert(send.assistedRoutes === Seq.empty)
// with assisted routes
val externalId1 = "030bb6a5e0c6b203c7e2180fb78c7ba4bdce46126761d8201b91ddac089cdecc87"
val hints = List(List(ExtraHop(Bob.nodeParams.nodeId, ShortChannelId("569178x2331x1"), feeBase = 10 msat, feeProportionalMillionths = 1, cltvExpiryDelta = CltvExpiryDelta(12))))
val invoice1 = PaymentRequest(Block.RegtestGenesisBlock.hash, Some(123 msat), ByteVector32.Zeroes, randomKey(), "description", CltvExpiryDelta(18), None, None, hints)
eclair.send(Some(externalId1), nodeId, 123 msat, ByteVector32.Zeroes, invoice_opt = Some(invoice1))
val send1 = paymentInitiator.expectMsgType[SendPaymentRequest]
val invoice1 = PaymentRequest(Block.RegtestGenesisBlock.hash, Some(123 msat), ByteVector32.Zeroes, nodePrivKey, "description", CltvExpiryDelta(18), None, None, hints)
eclair.send(Some(externalId1), 123 msat, invoice1)
val send1 = paymentInitiator.expectMsgType[SendPayment]
assert(send1.externalId === Some(externalId1))
assert(send1.recipientNodeId === nodeId)
assert(send1.recipientNodeId === nodePrivKey.publicKey)
assert(send1.recipientAmount === 123.msat)
assert(send1.paymentHash === ByteVector32.Zeroes)
assert(send1.paymentRequest === Some(invoice1))
assert(send1.paymentRequest === invoice1)
assert(send1.assistedRoutes === hints)
// with finalCltvExpiry
val externalId2 = "487da196-a4dc-4b1e-92b4-3e5e905e9f3f"
val invoice2 = PaymentRequest("lntb", Some(123 msat), System.currentTimeMillis() / 1000L, nodeId, List(PaymentRequest.MinFinalCltvExpiry(96), PaymentRequest.PaymentHash(ByteVector32.Zeroes), PaymentRequest.Description("description")), ByteVector.empty)
eclair.send(Some(externalId2), nodeId, 123 msat, ByteVector32.Zeroes, invoice_opt = Some(invoice2))
val send2 = paymentInitiator.expectMsgType[SendPaymentRequest]
val invoice2 = PaymentRequest("lntb", Some(123 msat), System.currentTimeMillis() / 1000L, nodePrivKey.publicKey, List(PaymentRequest.MinFinalCltvExpiry(96), PaymentRequest.PaymentHash(ByteVector32.Zeroes), PaymentRequest.Description("description")), ByteVector.empty)
eclair.send(Some(externalId2), 123 msat, invoice2)
val send2 = paymentInitiator.expectMsgType[SendPayment]
assert(send2.externalId === Some(externalId2))
assert(send2.recipientNodeId === nodeId)
assert(send2.recipientNodeId === nodePrivKey.publicKey)
assert(send2.recipientAmount === 123.msat)
assert(send2.paymentHash === ByteVector32.Zeroes)
assert(send2.paymentRequest === Some(invoice2))
assert(send2.paymentRequest === invoice2)
assert(send2.fallbackFinalExpiryDelta === CltvExpiryDelta(96))
// with custom route fees parameters
eclair.send(None, nodeId, 123 msat, ByteVector32.Zeroes, invoice_opt = None, feeThreshold_opt = Some(123 sat), maxFeePct_opt = Some(4.20))
val send3 = paymentInitiator.expectMsgType[SendPaymentRequest]
eclair.send(None, 123 msat, invoice0, feeThreshold_opt = Some(123 sat), maxFeePct_opt = Some(4.20))
val send3 = paymentInitiator.expectMsgType[SendPayment]
assert(send3.externalId === None)
assert(send3.recipientNodeId === nodeId)
assert(send3.recipientNodeId === nodePrivKey.publicKey)
assert(send3.recipientAmount === 123.msat)
assert(send3.paymentHash === ByteVector32.Zeroes)
assert(send3.routeParams.get.maxFeeBase === 123000.msat) // conversion sat -> msat
assert(send3.routeParams.get.maxFeePct === 4.20)
val invalidExternalId = "Robert'); DROP TABLE received_payments; DROP TABLE sent_payments; DROP TABLE payments;"
assertThrows[IllegalArgumentException](Await.result(eclair.send(Some(invalidExternalId), nodeId, 123 msat, ByteVector32.Zeroes), 50 millis))
assertThrows[IllegalArgumentException](Await.result(eclair.send(Some(invalidExternalId), 123 msat, invoice0), 50 millis))
val expiredInvoice = invoice2.copy(timestamp = 0L)
assertThrows[IllegalArgumentException](Await.result(eclair.send(None, nodeId, 123 msat, ByteVector32.Zeroes, invoice_opt = Some(expiredInvoice)), 50 millis))
assertThrows[IllegalArgumentException](Await.result(eclair.send(None, 123 msat, expiredInvoice), 50 millis))
}
test("return node announcements") { f =>
@ -386,27 +386,22 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(1234 msat), ByteVector32.One, randomKey(), "Some invoice", CltvExpiryDelta(18))
eclair.sendToRoute(1000 msat, Some(1200 msat), Some("42"), Some(parentId), pr, CltvExpiryDelta(123), route, Some(secret), Some(100 msat), Some(CltvExpiryDelta(144)), trampolines)
paymentInitiator.expectMsg(SendPaymentToRouteRequest(1000 msat, 1200 msat, Some("42"), Some(parentId), pr, CltvExpiryDelta(123), route, Some(secret), 100 msat, CltvExpiryDelta(144), trampolines))
paymentInitiator.expectMsg(SendPaymentToRoute(1000 msat, 1200 msat, pr, CltvExpiryDelta(123), route, Some("42"), Some(parentId), Some(secret), 100 msat, CltvExpiryDelta(144), trampolines))
}
test("call sendWithPreimage, which generate a random preimage, to perform a KeySend payment") { f =>
test("call sendWithPreimage, which generates a random preimage, to perform a KeySend payment") { f =>
import f._
val eclair = new EclairImpl(kit)
val nodeId = randomKey().publicKey
eclair.sendWithPreimage(None, nodeId, 12345 msat)
val send = paymentInitiator.expectMsgType[SendPaymentRequest]
val send = paymentInitiator.expectMsgType[SendSpontaneousPayment]
assert(send.externalId === None)
assert(send.recipientNodeId === nodeId)
assert(send.recipientAmount === 12345.msat)
assert(send.paymentRequest === None)
assert(send.userCustomTlvs.length === 1)
val keySendTlv = send.userCustomTlvs.head
assert(keySendTlv.tag === UInt64(5482373484L))
val preimage = ByteVector32(keySendTlv.value)
assert(Crypto.sha256(preimage) === send.paymentHash)
assert(send.paymentHash === Crypto.sha256(send.paymentPreimage))
assert(send.userCustomTlvs.isEmpty)
}
test("call sendWithPreimage, giving a specific preimage, to perform a KeySend payment") { f =>
@ -418,20 +413,16 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I
val expectedPaymentHash = Crypto.sha256(expectedPaymentPreimage)
eclair.sendWithPreimage(None, nodeId, 12345 msat, paymentPreimage = expectedPaymentPreimage)
val send = paymentInitiator.expectMsgType[SendPaymentRequest]
val send = paymentInitiator.expectMsgType[SendSpontaneousPayment]
assert(send.externalId === None)
assert(send.recipientNodeId === nodeId)
assert(send.recipientAmount === 12345.msat)
assert(send.paymentRequest === None)
assert(send.paymentPreimage === expectedPaymentPreimage)
assert(send.paymentHash === expectedPaymentHash)
assert(send.userCustomTlvs.length === 1)
val keySendTlv = send.userCustomTlvs.head
assert(keySendTlv.tag === UInt64(5482373484L))
assert(expectedPaymentPreimage === ByteVector32(keySendTlv.value))
assert(send.userCustomTlvs.isEmpty)
}
test("sign & verify an arbitrary message with the node's private key") { f =>
test("sign and verify an arbitrary message with the node's private key") { f =>
import f._
val eclair = new EclairImpl(kit)

View file

@ -33,7 +33,6 @@ import fr.acinq.eclair.payment.receive.MultiPartHandler.ReceivePayment
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.wire.protocol.Onion.FinalLegacyPayload
import fr.acinq.eclair.wire.protocol._
import grizzled.slf4j.Logging
import org.scalatest.funsuite.FixtureAnyFunSuiteLike
@ -118,11 +117,11 @@ class FuzzySpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with StateT
// we don't want to be below htlcMinimumMsat
val requiredAmount = 1000000 msat
def buildCmdAdd(paymentHash: ByteVector32, dest: PublicKey) = {
def buildCmdAdd(paymentHash: ByteVector32, dest: PublicKey, paymentSecret: ByteVector32): 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(blockHeight = 400000)
OutgoingPacket.buildCommand(self, Upstream.Local(UUID.randomUUID()), paymentHash, ChannelHop(null, dest, null) :: Nil, FinalLegacyPayload(amount, expiry))._1
OutgoingPacket.buildCommand(self, Upstream.Local(UUID.randomUUID()), paymentHash, ChannelHop(null, dest, null) :: Nil, Onion.createSinglePartPayload(amount, expiry, paymentSecret))._1
}
def initiatePaymentOrStop(remaining: Int): Unit =
@ -130,7 +129,7 @@ class FuzzySpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with StateT
paymentHandler ! ReceivePayment(Some(requiredAmount), "One coffee")
context become {
case req: PaymentRequest =>
sendChannel ! buildCmdAdd(req.paymentHash, req.nodeId)
sendChannel ! buildCmdAdd(req.paymentHash, req.nodeId, req.paymentSecret.get)
context become {
case RES_SUCCESS(_: CMD_ADD_HTLC, _) => ()
case RES_ADD_SETTLED(_, htlc, _: HtlcResult.Fulfill) =>

View file

@ -33,9 +33,8 @@ import fr.acinq.eclair.payment.OutgoingPacket.Upstream
import fr.acinq.eclair.router.Router.ChannelHop
import fr.acinq.eclair.transactions.Transactions
import fr.acinq.eclair.transactions.Transactions._
import fr.acinq.eclair.wire.protocol.Onion.FinalLegacyPayload
import fr.acinq.eclair.wire.protocol._
import fr.acinq.eclair.{FeatureSupport, Features, NodeParams, TestConstants, randomBytes32, _}
import fr.acinq.eclair._
import org.scalatest.{FixtureTestSuite, ParallelTestExecution}
import java.util.UUID
@ -190,7 +189,7 @@ trait StateTestsHelperMethods extends TestKitBase {
def makeCmdAdd(amount: MilliSatoshi, destination: PublicKey, currentBlockHeight: Long, paymentPreimage: ByteVector32 = randomBytes32(), upstream: Upstream = Upstream.Local(UUID.randomUUID), replyTo: ActorRef = TestProbe().ref): (ByteVector32, CMD_ADD_HTLC) = {
val paymentHash: ByteVector32 = Crypto.sha256(paymentPreimage)
val expiry = CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight)
val cmd = OutgoingPacket.buildCommand(replyTo, upstream, paymentHash, ChannelHop(null, destination, null) :: Nil, FinalLegacyPayload(amount, expiry))._1.copy(commit = false)
val cmd = OutgoingPacket.buildCommand(replyTo, upstream, paymentHash, ChannelHop(null, destination, null) :: Nil, Onion.createSinglePartPayload(amount, expiry, randomBytes32()))._1.copy(commit = false)
(paymentPreimage, cmd)
}

View file

@ -19,9 +19,9 @@ package fr.acinq.eclair.channel.states.f
import akka.testkit.TestProbe
import fr.acinq.bitcoin.Crypto.PrivateKey
import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto, SatoshiLong, ScriptFlags, Transaction}
import fr.acinq.eclair.blockchain.{CurrentBlockCount, CurrentFeerates}
import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._
import fr.acinq.eclair.blockchain.fee.{FeeratePerKw, FeeratesPerKw}
import fr.acinq.eclair.blockchain.{CurrentBlockCount, CurrentFeerates}
import fr.acinq.eclair.channel.TxPublisher.{PublishRawTx, PublishTx}
import fr.acinq.eclair.channel._
import fr.acinq.eclair.channel.states.{StateTestsBase, StateTestsTags}
@ -29,8 +29,7 @@ import fr.acinq.eclair.payment.OutgoingPacket.Upstream
import fr.acinq.eclair.payment._
import fr.acinq.eclair.payment.relay.Relayer._
import fr.acinq.eclair.router.Router.ChannelHop
import fr.acinq.eclair.wire.protocol.Onion.FinalLegacyPayload
import fr.acinq.eclair.wire.protocol.{ClosingSigned, CommitSig, Error, FailureMessageCodecs, PermanentChannelFailure, RevokeAndAck, Shutdown, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFee, UpdateFulfillHtlc}
import fr.acinq.eclair.wire.protocol.{ClosingSigned, CommitSig, Error, FailureMessageCodecs, Onion, PermanentChannelFailure, RevokeAndAck, Shutdown, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFee, UpdateFulfillHtlc}
import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, MilliSatoshiLong, TestConstants, TestKitBaseClass, randomBytes32}
import org.scalatest.funsuite.FixtureAnyFunSuiteLike
import org.scalatest.{Outcome, Tag}
@ -60,7 +59,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 = OutgoingPacket.buildCommand(sender.ref, Upstream.Local(UUID.randomUUID), h1, ChannelHop(null, TestConstants.Bob.nodeParams.nodeId, null) :: Nil, FinalLegacyPayload(amount1, expiry1))._1.copy(commit = false)
val cmd1 = OutgoingPacket.buildCommand(sender.ref, Upstream.Local(UUID.randomUUID), h1, ChannelHop(null, TestConstants.Bob.nodeParams.nodeId, null) :: Nil, Onion.createSinglePartPayload(amount1, expiry1, randomBytes32()))._1.copy(commit = false)
alice ! cmd1
sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]]
val htlc1 = alice2bob.expectMsgType[UpdateAddHtlc]
@ -70,7 +69,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 = OutgoingPacket.buildCommand(sender.ref, Upstream.Local(UUID.randomUUID), h2, ChannelHop(null, TestConstants.Bob.nodeParams.nodeId, null) :: Nil, FinalLegacyPayload(amount2, expiry2))._1.copy(commit = false)
val cmd2 = OutgoingPacket.buildCommand(sender.ref, Upstream.Local(UUID.randomUUID), h2, ChannelHop(null, TestConstants.Bob.nodeParams.nodeId, null) :: Nil, Onion.createSinglePartPayload(amount2, expiry2, randomBytes32()))._1.copy(commit = false)
alice ! cmd2
sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]]
val htlc2 = alice2bob.expectMsgType[UpdateAddHtlc]

View file

@ -22,7 +22,7 @@ import akka.testkit.TestProbe
import com.google.common.net.HostAndPort
import com.typesafe.config.ConfigFactory
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.bitcoin.{Base58, Base58Check, Bech32, BtcDouble, ByteVector32, Crypto, OP_0, OP_CHECKSIG, OP_DUP, OP_EQUAL, OP_EQUALVERIFY, OP_HASH160, OP_PUSHDATA, OutPoint, SatoshiLong, Script, ScriptFlags, Transaction}
import fr.acinq.bitcoin.{Base58, Base58Check, Bech32, Block, BtcDouble, ByteVector32, Crypto, OP_0, OP_CHECKSIG, OP_DUP, OP_EQUAL, OP_EQUALVERIFY, OP_HASH160, OP_PUSHDATA, OutPoint, SatoshiLong, Script, ScriptFlags, Transaction}
import fr.acinq.eclair.blockchain.bitcoind.BitcoindService.BitcoinReq
import fr.acinq.eclair.blockchain.bitcoind.rpc.ExtendedBitcoinClient
import fr.acinq.eclair.channel._
@ -31,7 +31,8 @@ import fr.acinq.eclair.io.{Peer, PeerConnection}
import fr.acinq.eclair.payment._
import fr.acinq.eclair.payment.receive.MultiPartHandler.ReceivePayment
import fr.acinq.eclair.payment.receive.{ForwardHandler, PaymentHandler}
import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentRequest
import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle.PreimageReceived
import fr.acinq.eclair.payment.send.PaymentInitiator.SendPayment
import fr.acinq.eclair.router.Router
import fr.acinq.eclair.transactions.{Scripts, Transactions}
import fr.acinq.eclair.wire.protocol.{ChannelAnnouncement, ChannelUpdate, PermanentChannelFailure, UpdateAddHtlc}
@ -144,7 +145,7 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec {
val preimage = randomBytes32()
val paymentHash = Crypto.sha256(preimage)
// A sends a payment to F
val paymentReq = SendPaymentRequest(100000000 msat, paymentHash, nodes("F").nodeParams.nodeId, maxAttempts = 1, fallbackFinalExpiryDelta = finalCltvExpiryDelta, routeParams = integrationTestRouteParams)
val paymentReq = SendPayment(100000000 msat, PaymentRequest(Block.RegtestGenesisBlock.hash, None, paymentHash, nodes("F").nodeParams.privateKey, "test", finalCltvExpiryDelta), maxAttempts = 1, routeParams = integrationTestRouteParams)
val paymentSender = TestProbe()
paymentSender.send(nodes("A").paymentInitiator, paymentReq)
val paymentId = paymentSender.expectMsgType[UUID]
@ -367,7 +368,7 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec {
def send(amountMsat: MilliSatoshi, paymentHandler: ActorRef, paymentInitiator: ActorRef): UUID = {
sender.send(paymentHandler, ReceivePayment(Some(amountMsat), "1 coffee"))
val pr = sender.expectMsgType[PaymentRequest]
val sendReq = SendPaymentRequest(amountMsat, pr.paymentHash, pr.nodeId, maxAttempts = 1, fallbackFinalExpiryDelta = finalCltvExpiryDelta, routeParams = integrationTestRouteParams)
val sendReq = SendPayment(amountMsat, pr, maxAttempts = 1, fallbackFinalExpiryDelta = finalCltvExpiryDelta, routeParams = integrationTestRouteParams)
sender.send(paymentInitiator, sendReq)
sender.expectMsgType[UUID]
}
@ -405,18 +406,22 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec {
buffer.expectMsgType[IncomingPacket.FinalPacket]
buffer.forward(paymentHandlerF)
sigListener.expectMsgType[ChannelSignatureReceived]
val preimage1 = sender.expectMsgType[PaymentSent].paymentPreimage
val preimage1 = sender.expectMsgType[PreimageReceived].paymentPreimage
assert(sender.expectMsgType[PaymentSent].paymentPreimage === preimage1)
buffer.expectMsgType[IncomingPacket.FinalPacket]
buffer.forward(paymentHandlerF)
sigListener.expectMsgType[ChannelSignatureReceived]
val preimage2 = sender.expectMsgType[PaymentSent].paymentPreimage
val preimage2 = sender.expectMsgType[PreimageReceived].paymentPreimage
assert(sender.expectMsgType[PaymentSent].paymentPreimage === preimage2)
buffer.expectMsgType[IncomingPacket.FinalPacket]
buffer.forward(paymentHandlerC)
sigListener.expectMsgType[ChannelSignatureReceived]
sender.expectMsgType[PreimageReceived]
sender.expectMsgType[PaymentSent]
buffer.expectMsgType[IncomingPacket.FinalPacket]
buffer.forward(paymentHandlerC)
sigListener.expectMsgType[ChannelSignatureReceived]
sender.expectMsgType[PreimageReceived]
sender.expectMsgType[PaymentSent]
// we then generate blocks to make htlcs timeout (nothing will happen in the channel because all of them have already been fulfilled)
generateBlocks(40)
@ -661,8 +666,10 @@ class AnchorOutputChannelIntegrationSpec extends ChannelIntegrationSpec {
val pr = sender.expectMsgType[PaymentRequest]
// then we make the actual payment
sender.send(nodes("C").paymentInitiator, SendPaymentRequest(amountMsat, pr.paymentHash, nodes("F").nodeParams.nodeId, maxAttempts = 1, fallbackFinalExpiryDelta = finalCltvExpiryDelta))
sender.send(nodes("C").paymentInitiator, SendPayment(amountMsat, pr, maxAttempts = 1, fallbackFinalExpiryDelta = finalCltvExpiryDelta))
val paymentId = sender.expectMsgType[UUID]
val preimage = sender.expectMsgType[PreimageReceived].paymentPreimage
assert(Crypto.sha256(preimage) === pr.paymentHash)
val ps = sender.expectMsgType[PaymentSent](60 seconds)
assert(ps.id == paymentId)

View file

@ -16,16 +16,16 @@
package fr.acinq.eclair.integration
import akka.actor.typed.scaladsl.adapter.actorRefAdapter
import akka.actor.ActorRef
import akka.actor.typed.scaladsl.adapter.actorRefAdapter
import akka.testkit.TestProbe
import com.typesafe.config.ConfigFactory
import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey}
import fr.acinq.bitcoin.{Block, ByteVector32, SatoshiLong}
import fr.acinq.bitcoin.{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.rpc.ExtendedBitcoinClient
import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{Watch, WatchFundingConfirmed}
import fr.acinq.eclair.blockchain.bitcoind.rpc.ExtendedBitcoinClient
import fr.acinq.eclair.channel.Channel.{BroadcastChannelUpdate, PeriodicRefresh}
import fr.acinq.eclair.channel._
import fr.acinq.eclair.crypto.Sphinx.DecryptedFailurePacket
@ -37,7 +37,7 @@ import fr.acinq.eclair.payment._
import fr.acinq.eclair.payment.receive.MultiPartHandler.ReceivePayment
import fr.acinq.eclair.payment.relay.Relayer
import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle.PreimageReceived
import fr.acinq.eclair.payment.send.PaymentInitiator.{SendPaymentRequest, SendTrampolinePaymentRequest}
import fr.acinq.eclair.payment.send.PaymentInitiator.{SendPayment, SendTrampolinePayment}
import fr.acinq.eclair.router.Graph.WeightRatios
import fr.acinq.eclair.router.Router.{GossipDecision, PublicChannel}
import fr.acinq.eclair.router.{Announcements, AnnouncementsBatchValidationSpec, Router}
@ -95,7 +95,7 @@ class PaymentIntegrationSpec extends IntegrationSpec {
awaitCond({
val watches = nodes.values.foldLeft(Set.empty[Watch[_]]) {
case (watches, setup) =>
setup.watcher ! ZmqWatcher.ListWatches(sender.ref)
setup.watcher ! ZmqWatcher.ListWatches(sender.ref)
watches ++ sender.expectMsgType[Set[Watch[_]]]
}
watches.count(_.isInstanceOf[WatchFundingConfirmed]) == channelEndpointsCount
@ -159,8 +159,10 @@ class PaymentIntegrationSpec extends IntegrationSpec {
sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), "1 coffee"))
val pr = sender.expectMsgType[PaymentRequest]
// then we make the actual payment
sender.send(nodes("A").paymentInitiator, SendPaymentRequest(amountMsat, pr.paymentHash, nodes("D").nodeParams.nodeId, fallbackFinalExpiryDelta = finalCltvExpiryDelta, routeParams = integrationTestRouteParams, maxAttempts = 1))
sender.send(nodes("A").paymentInitiator, SendPayment(amountMsat, pr, fallbackFinalExpiryDelta = finalCltvExpiryDelta, routeParams = integrationTestRouteParams, maxAttempts = 1))
val paymentId = sender.expectMsgType[UUID]
val preimage = sender.expectMsgType[PreimageReceived].paymentPreimage
assert(Crypto.sha256(preimage) === pr.paymentHash)
val ps = sender.expectMsgType[PaymentSent]
assert(ps.id == paymentId)
}
@ -183,10 +185,12 @@ class PaymentIntegrationSpec extends IntegrationSpec {
sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), "1 coffee"))
val pr = sender.expectMsgType[PaymentRequest]
// then we make the actual payment, do not randomize the route to make sure we route through node B
val sendReq = SendPaymentRequest(amountMsat, pr.paymentHash, nodes("D").nodeParams.nodeId, fallbackFinalExpiryDelta = finalCltvExpiryDelta, routeParams = integrationTestRouteParams, maxAttempts = 5)
val sendReq = SendPayment(amountMsat, pr, fallbackFinalExpiryDelta = finalCltvExpiryDelta, routeParams = integrationTestRouteParams, maxAttempts = 5)
sender.send(nodes("A").paymentInitiator, sendReq)
// A will receive an error from B that include the updated channel update, then will retry the payment
val paymentId = sender.expectMsgType[UUID]
val preimage = sender.expectMsgType[PreimageReceived].paymentPreimage
assert(Crypto.sha256(preimage) === pr.paymentHash)
val ps = sender.expectMsgType[PaymentSent]
assert(ps.id == paymentId)
@ -223,16 +227,19 @@ class PaymentIntegrationSpec extends IntegrationSpec {
sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), "1 coffee"))
val pr = sender.expectMsgType[PaymentRequest]
// then we make the payment (B-C has a smaller capacity than A-B and C-D)
val sendReq = SendPaymentRequest(amountMsat, pr.paymentHash, nodes("D").nodeParams.nodeId, fallbackFinalExpiryDelta = finalCltvExpiryDelta, routeParams = integrationTestRouteParams, maxAttempts = 5)
val sendReq = SendPayment(amountMsat, pr, fallbackFinalExpiryDelta = finalCltvExpiryDelta, routeParams = integrationTestRouteParams, maxAttempts = 5)
sender.send(nodes("A").paymentInitiator, sendReq)
// A will first receive an error from C, then retry and route around C: A->B->E->C->D
sender.expectMsgType[UUID]
sender.expectMsgType[PreimageReceived]
sender.expectMsgType[PaymentSent] // the payment FSM will also reply to the sender after the payment is completed
}
test("send an HTLC A->D with an unknown payment hash") {
val sender = TestProbe()
val pr = SendPaymentRequest(100000000 msat, randomBytes32(), nodes("D").nodeParams.nodeId, routeParams = integrationTestRouteParams, maxAttempts = 5)
val amount = 100000000 msat
val unknownInvoice = PaymentRequest(Block.RegtestGenesisBlock.hash, Some(amount), randomBytes32(), nodes("D").nodeParams.privateKey, "test", finalCltvExpiryDelta)
val pr = SendPayment(amount, unknownInvoice, routeParams = integrationTestRouteParams, maxAttempts = 5)
sender.send(nodes("A").paymentInitiator, pr)
// A will receive an error from D and won't retry
@ -241,7 +248,7 @@ class PaymentIntegrationSpec extends IntegrationSpec {
assert(failed.id == paymentId)
assert(failed.paymentHash === pr.paymentHash)
assert(failed.failures.size === 1)
assert(failed.failures.head.asInstanceOf[RemoteFailure].e === DecryptedFailurePacket(nodes("D").nodeParams.nodeId, IncorrectOrUnknownPaymentDetails(100000000 msat, getBlockCount)))
assert(failed.failures.head.asInstanceOf[RemoteFailure].e === DecryptedFailurePacket(nodes("D").nodeParams.nodeId, IncorrectOrUnknownPaymentDetails(amount, getBlockCount)))
}
test("send an HTLC A->D with a lower amount than requested") {
@ -252,7 +259,7 @@ class PaymentIntegrationSpec extends IntegrationSpec {
val pr = sender.expectMsgType[PaymentRequest]
// A send payment of only 1 mBTC
val sendReq = SendPaymentRequest(100000000 msat, pr.paymentHash, nodes("D").nodeParams.nodeId, fallbackFinalExpiryDelta = finalCltvExpiryDelta, routeParams = integrationTestRouteParams, maxAttempts = 5)
val sendReq = SendPayment(100000000 msat, pr, fallbackFinalExpiryDelta = finalCltvExpiryDelta, routeParams = integrationTestRouteParams, maxAttempts = 5)
sender.send(nodes("A").paymentInitiator, sendReq)
// A will first receive an IncorrectPaymentAmount error from D
@ -272,7 +279,7 @@ class PaymentIntegrationSpec extends IntegrationSpec {
val pr = sender.expectMsgType[PaymentRequest]
// A send payment of 6 mBTC
val sendReq = SendPaymentRequest(600000000 msat, pr.paymentHash, nodes("D").nodeParams.nodeId, fallbackFinalExpiryDelta = finalCltvExpiryDelta, routeParams = integrationTestRouteParams, maxAttempts = 5)
val sendReq = SendPayment(600000000 msat, pr, fallbackFinalExpiryDelta = finalCltvExpiryDelta, routeParams = integrationTestRouteParams, maxAttempts = 5)
sender.send(nodes("A").paymentInitiator, sendReq)
// A will first receive an IncorrectPaymentAmount error from D
@ -292,7 +299,7 @@ class PaymentIntegrationSpec extends IntegrationSpec {
val pr = sender.expectMsgType[PaymentRequest]
// A send payment of 3 mBTC, more than asked but it should still be accepted
val sendReq = SendPaymentRequest(300000000 msat, pr.paymentHash, nodes("D").nodeParams.nodeId, fallbackFinalExpiryDelta = finalCltvExpiryDelta, routeParams = integrationTestRouteParams, maxAttempts = 5)
val sendReq = SendPayment(300000000 msat, pr, fallbackFinalExpiryDelta = finalCltvExpiryDelta, routeParams = integrationTestRouteParams, maxAttempts = 5)
sender.send(nodes("A").paymentInitiator, sendReq)
sender.expectMsgType[UUID]
}
@ -305,9 +312,10 @@ class PaymentIntegrationSpec extends IntegrationSpec {
sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), "1 payment"))
val pr = sender.expectMsgType[PaymentRequest]
val sendReq = SendPaymentRequest(amountMsat, pr.paymentHash, nodes("D").nodeParams.nodeId, fallbackFinalExpiryDelta = finalCltvExpiryDelta, routeParams = integrationTestRouteParams, maxAttempts = 5)
val sendReq = SendPayment(amountMsat, pr, fallbackFinalExpiryDelta = finalCltvExpiryDelta, routeParams = integrationTestRouteParams, maxAttempts = 5)
sender.send(nodes("A").paymentInitiator, sendReq)
sender.expectMsgType[UUID]
sender.expectMsgType[PreimageReceived]
sender.expectMsgType[PaymentSent] // the payment FSM will also reply to the sender after the payment is completed
}
}
@ -320,14 +328,11 @@ class PaymentIntegrationSpec extends IntegrationSpec {
val pr = sender.expectMsgType[PaymentRequest]
// the payment is requesting to use a capacity-optimized route which will select node G even though it's a bit more expensive
sender.send(nodes("A").paymentInitiator, SendPaymentRequest(amountMsat, pr.paymentHash, nodes("C").nodeParams.nodeId, maxAttempts = 1, fallbackFinalExpiryDelta = finalCltvExpiryDelta, routeParams = integrationTestRouteParams.map(_.copy(ratios = Some(WeightRatios(0, 0, 1))))))
sender.send(nodes("A").paymentInitiator, SendPayment(amountMsat, pr, maxAttempts = 1, fallbackFinalExpiryDelta = finalCltvExpiryDelta, routeParams = integrationTestRouteParams.map(_.copy(ratios = Some(WeightRatios(0, 0, 1))))))
sender.expectMsgType[UUID]
sender.expectMsgType[PaymentEvent] match {
case PaymentFailed(_, _, failures, _) => failures == Seq.empty // if something went wrong fail with a hint
case PaymentSent(_, _, _, _, _, part :: Nil) => part.route.getOrElse(Nil).exists(_.nodeId == nodes("G").nodeParams.nodeId)
case e => fail(s"unexpected payment event: $e")
}
sender.expectMsgType[PreimageReceived]
val ps = sender.expectMsgType[PaymentSent]
ps.parts.foreach(part => assert(part.route.getOrElse(Nil).exists(_.nodeId == nodes("G").nodeParams.nodeId)))
}
test("send a multi-part payment B->D") {
@ -338,7 +343,7 @@ class PaymentIntegrationSpec extends IntegrationSpec {
val pr = sender.expectMsgType[PaymentRequest]
assert(pr.features.allowMultiPart)
sender.send(nodes("B").paymentInitiator, SendPaymentRequest(amount, pr.paymentHash, nodes("D").nodeParams.nodeId, 5, paymentRequest = Some(pr)))
sender.send(nodes("B").paymentInitiator, SendPayment(amount, pr, maxAttempts = 5))
val paymentId = sender.expectMsgType[UUID]
assert(sender.expectMsgType[PreimageReceived].paymentHash === pr.paymentHash)
val paymentSent = sender.expectMsgType[PaymentSent](max = 30 seconds)
@ -382,7 +387,7 @@ class PaymentIntegrationSpec extends IntegrationSpec {
val canSend = sender.expectMsgType[Relayer.OutgoingChannels].channels.map(_.commitments.availableBalanceForSend).sum
assert(canSend > amount)
sender.send(nodes("B").paymentInitiator, SendPaymentRequest(amount, pr.paymentHash, nodes("D").nodeParams.nodeId, 1, paymentRequest = Some(pr)))
sender.send(nodes("B").paymentInitiator, SendPayment(amount, pr, maxAttempts = 1))
val paymentId = sender.expectMsgType[UUID]
val paymentFailed = sender.expectMsgType[PaymentFailed](max = 30 seconds)
assert(paymentFailed.id === paymentId, paymentFailed)
@ -405,7 +410,7 @@ class PaymentIntegrationSpec extends IntegrationSpec {
val pr = sender.expectMsgType[PaymentRequest]
assert(pr.features.allowMultiPart)
sender.send(nodes("D").paymentInitiator, SendPaymentRequest(amount, pr.paymentHash, nodes("C").nodeParams.nodeId, 3, paymentRequest = Some(pr)))
sender.send(nodes("D").paymentInitiator, SendPayment(amount, pr, maxAttempts = 3))
val paymentId = sender.expectMsgType[UUID]
assert(sender.expectMsgType[PreimageReceived].paymentHash === pr.paymentHash)
val paymentSent = sender.expectMsgType[PaymentSent](max = 30 seconds)
@ -438,7 +443,7 @@ class PaymentIntegrationSpec extends IntegrationSpec {
val canSend = sender.expectMsgType[Relayer.OutgoingChannels].channels.map(_.commitments.availableBalanceForSend).sum
assert(canSend < amount)
sender.send(nodes("D").paymentInitiator, SendPaymentRequest(amount, pr.paymentHash, nodes("C").nodeParams.nodeId, 1, paymentRequest = Some(pr)))
sender.send(nodes("D").paymentInitiator, SendPayment(amount, pr, maxAttempts = 1))
val paymentId = sender.expectMsgType[UUID]
val paymentFailed = sender.expectMsgType[PaymentFailed](max = 30 seconds)
assert(paymentFailed.id === paymentId, paymentFailed)
@ -464,7 +469,7 @@ class PaymentIntegrationSpec extends IntegrationSpec {
// The first attempt should fail, but the second one should succeed.
val attempts = (1000 msat, CltvExpiryDelta(42)) :: (1000000 msat, CltvExpiryDelta(288)) :: Nil
val payment = SendTrampolinePaymentRequest(amount, pr, nodes("G").nodeParams.nodeId, attempts)
val payment = SendTrampolinePayment(amount, pr, nodes("G").nodeParams.nodeId, attempts)
sender.send(nodes("B").paymentInitiator, payment)
val paymentId = sender.expectMsgType[UUID]
val paymentSent = sender.expectMsgType[PaymentSent](max = 30 seconds)
@ -504,7 +509,7 @@ class PaymentIntegrationSpec extends IntegrationSpec {
assert(pr.features.allowMultiPart)
assert(pr.features.allowTrampoline)
val payment = SendTrampolinePaymentRequest(amount, pr, nodes("C").nodeParams.nodeId, Seq((350000 msat, CltvExpiryDelta(288))))
val payment = SendTrampolinePayment(amount, pr, nodes("C").nodeParams.nodeId, Seq((350000 msat, CltvExpiryDelta(288))))
sender.send(nodes("D").paymentInitiator, payment)
val paymentId = sender.expectMsgType[UUID]
val paymentSent = sender.expectMsgType[PaymentSent](max = 30 seconds)
@ -553,7 +558,7 @@ class PaymentIntegrationSpec extends IntegrationSpec {
assert(pr.features.allowMultiPart)
assert(!pr.features.allowTrampoline)
val payment = SendTrampolinePaymentRequest(amount, pr, nodes("C").nodeParams.nodeId, Seq((1000000 msat, CltvExpiryDelta(432))))
val payment = SendTrampolinePayment(amount, pr, nodes("C").nodeParams.nodeId, Seq((1000000 msat, CltvExpiryDelta(432))))
sender.send(nodes("F").paymentInitiator, payment)
val paymentId = sender.expectMsgType[UUID]
val paymentSent = sender.expectMsgType[PaymentSent](max = 30 seconds)
@ -588,7 +593,7 @@ class PaymentIntegrationSpec extends IntegrationSpec {
// We put most of the capacity C <-> D on D's side.
sender.send(nodes("D").paymentHandler, ReceivePayment(Some(8000000000L msat), "plz send everything"))
val pr1 = sender.expectMsgType[PaymentRequest]
sender.send(nodes("C").paymentInitiator, SendPaymentRequest(8000000000L msat, pr1.paymentHash, nodes("D").nodeParams.nodeId, 3, paymentRequest = Some(pr1)))
sender.send(nodes("C").paymentInitiator, SendPayment(8000000000L msat, pr1, maxAttempts = 3))
sender.expectMsgType[UUID]
sender.expectMsgType[PreimageReceived](max = 30 seconds)
sender.expectMsgType[PaymentSent](max = 30 seconds)
@ -600,7 +605,7 @@ class PaymentIntegrationSpec extends IntegrationSpec {
assert(pr.features.allowMultiPart)
assert(pr.features.allowTrampoline)
val payment = SendTrampolinePaymentRequest(amount, pr, nodes("C").nodeParams.nodeId, Seq((250000 msat, CltvExpiryDelta(288))))
val payment = SendTrampolinePayment(amount, pr, nodes("C").nodeParams.nodeId, Seq((250000 msat, CltvExpiryDelta(288))))
sender.send(nodes("B").paymentInitiator, payment)
val paymentId = sender.expectMsgType[UUID]
val paymentFailed = sender.expectMsgType[PaymentFailed](max = 30 seconds)
@ -621,7 +626,7 @@ class PaymentIntegrationSpec extends IntegrationSpec {
assert(pr.features.allowMultiPart)
assert(pr.features.allowTrampoline)
val payment = SendTrampolinePaymentRequest(amount, pr, nodes("B").nodeParams.nodeId, Seq((450000 msat, CltvExpiryDelta(288))))
val payment = SendTrampolinePayment(amount, pr, nodes("B").nodeParams.nodeId, Seq((450000 msat, CltvExpiryDelta(288))))
sender.send(nodes("A").paymentInitiator, payment)
val paymentId = sender.expectMsgType[UUID]
val paymentFailed = sender.expectMsgType[PaymentFailed](max = 30 seconds)

View file

@ -32,7 +32,7 @@ import fr.acinq.eclair.payment.receive.MultiPartPaymentFSM.HtlcPart
import fr.acinq.eclair.payment.receive.{MultiPartPaymentFSM, PaymentHandler}
import fr.acinq.eclair.wire.protocol.Onion.FinalTlvPayload
import fr.acinq.eclair.wire.protocol._
import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Feature, FeatureSupport, Features, MilliSatoshiLong, NodeParams, ShortChannelId, TestConstants, TestKitBaseClass, randomBytes32, randomKey}
import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Features, MilliSatoshiLong, NodeParams, ShortChannelId, TestConstants, TestKitBaseClass, randomBytes32, randomKey}
import org.scalatest.Outcome
import org.scalatest.funsuite.FixtureAnyFunSuiteLike
@ -92,7 +92,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
assert(Crypto.sha256(incoming.get.paymentPreimage) === pr.paymentHash)
val add = UpdateAddHtlc(ByteVector32.One, 0, amountMsat, pr.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket)
sender.send(handlerWithoutMpp, IncomingPacket.FinalPacket(add, Onion.FinalLegacyPayload(add.amountMsat, add.cltvExpiry)))
sender.send(handlerWithoutMpp, IncomingPacket.FinalPacket(add, Onion.createSinglePartPayload(add.amountMsat, add.cltvExpiry, pr.paymentSecret.get)))
register.expectMsgType[Register.Forward[CMD_FULFILL_HTLC]]
val paymentReceived = eventListener.expectMsgType[PaymentReceived]
@ -111,7 +111,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending)
val add = UpdateAddHtlc(ByteVector32.One, 0, amountMsat, pr.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket)
sender.send(handlerWithMpp, IncomingPacket.FinalPacket(add, Onion.FinalLegacyPayload(add.amountMsat, add.cltvExpiry)))
sender.send(handlerWithMpp, IncomingPacket.FinalPacket(add, Onion.createSinglePartPayload(add.amountMsat, add.cltvExpiry, pr.paymentSecret.get)))
register.expectMsgType[Register.Forward[CMD_FULFILL_HTLC]]
val paymentReceived = eventListener.expectMsgType[PaymentReceived]
@ -129,7 +129,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending)
val add = UpdateAddHtlc(ByteVector32.One, 0, amountMsat, pr.paymentHash, CltvExpiryDelta(3).toCltvExpiry(nodeParams.currentBlockHeight), TestConstants.emptyOnionPacket)
sender.send(handlerWithMpp, IncomingPacket.FinalPacket(add, Onion.FinalLegacyPayload(add.amountMsat, add.cltvExpiry)))
sender.send(handlerWithMpp, IncomingPacket.FinalPacket(add, Onion.createSinglePartPayload(add.amountMsat, add.cltvExpiry, pr.paymentSecret.get)))
val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message
assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(amountMsat, nodeParams.currentBlockHeight)))
assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending)
@ -243,7 +243,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
assert(pr.isExpired)
val add = UpdateAddHtlc(ByteVector32.One, 0, 1000 msat, pr.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket)
sender.send(handlerWithoutMpp, IncomingPacket.FinalPacket(add, Onion.FinalLegacyPayload(add.amountMsat, add.cltvExpiry)))
sender.send(handlerWithoutMpp, IncomingPacket.FinalPacket(add, Onion.createSinglePartPayload(add.amountMsat, add.cltvExpiry, pr.paymentSecret.get)))
register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]]
val Some(incoming) = nodeParams.db.payments.getIncomingPayment(pr.paymentHash)
assert(incoming.paymentRequest.isExpired && incoming.status === IncomingPaymentStatus.Expired)
@ -476,7 +476,8 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
val amountMsat = 42000 msat
val paymentPreimage = randomBytes32()
val paymentHash = Crypto.sha256(paymentPreimage)
val payload = FinalTlvPayload(TlvStream(Seq(OnionTlv.AmountToForward(amountMsat), OnionTlv.OutgoingCltv(defaultExpiry), OnionTlv.KeySend(paymentPreimage))))
val paymentSecret = randomBytes32()
val payload = FinalTlvPayload(TlvStream(Seq(OnionTlv.AmountToForward(amountMsat), OnionTlv.OutgoingCltv(defaultExpiry), OnionTlv.PaymentData(paymentSecret, 0 msat), OnionTlv.KeySend(paymentPreimage))))
assert(nodeParams.db.payments.getIncomingPayment(paymentHash) === None)
@ -497,7 +498,8 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
val amountMsat = 42000 msat
val paymentPreimage = randomBytes32()
val paymentHash = Crypto.sha256(paymentPreimage)
val payload = FinalTlvPayload(TlvStream(Seq(OnionTlv.AmountToForward(amountMsat), OnionTlv.OutgoingCltv(defaultExpiry), OnionTlv.KeySend(paymentPreimage))))
val paymentSecret = randomBytes32()
val payload = FinalTlvPayload(TlvStream(Seq(OnionTlv.AmountToForward(amountMsat), OnionTlv.OutgoingCltv(defaultExpiry), OnionTlv.PaymentData(paymentSecret, 0 msat), OnionTlv.KeySend(paymentPreimage))))
assert(nodeParams.db.payments.getIncomingPayment(paymentHash) === None)

View file

@ -36,8 +36,8 @@ import fr.acinq.eclair.wire.protocol._
import org.scalatest.Outcome
import org.scalatest.funsuite.FixtureAnyFunSuiteLike
import scodec.bits.{ByteVector, HexStringSyntax}
import java.util.UUID
import java.util.UUID
import scala.concurrent.duration._
/**
@ -85,7 +85,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
val childPayment = childPayFsm.expectMsgType[SendPaymentToRoute]
assert(childPayment.route === Right(singleRoute))
assert(childPayment.finalPayload.expiry === expiry)
assert(childPayment.finalPayload.paymentSecret === Some(payment.paymentSecret))
assert(childPayment.finalPayload.paymentSecret === payment.paymentSecret)
assert(childPayment.finalPayload.amount === finalAmount)
assert(childPayment.finalPayload.totalAmount === finalAmount)
assert(payFsm.stateName === PAYMENT_IN_PROGRESS)
@ -114,7 +114,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
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))
assert(childPayments.map(_.finalPayload.paymentSecret.get).toSet === Set(payment.paymentSecret))
assert(childPayments.map(_.finalPayload.paymentSecret).toSet === Set(payment.paymentSecret))
assert(childPayments.map(_.finalPayload.amount).toSet === Set(500000 msat, 700000 msat))
assert(childPayments.map(_.finalPayload.totalAmount).toSet === Set(1200000 msat))
assert(payFsm.stateName === PAYMENT_IN_PROGRESS)

View file

@ -29,12 +29,11 @@ import fr.acinq.eclair.payment.PaymentPacketSpec._
import fr.acinq.eclair.payment.PaymentRequest.{ExtraHop, PaymentRequestFeatures}
import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle.SendMultiPartPayment
import fr.acinq.eclair.payment.send.PaymentInitiator._
import fr.acinq.eclair.payment.send.PaymentLifecycle.{SendPayment, SendPaymentToRoute}
import fr.acinq.eclair.payment.send.{PaymentError, 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.Onion.{FinalLegacyPayload, FinalTlvPayload}
import fr.acinq.eclair.wire.protocol.OnionTlv.{AmountToForward, OutgoingCltv}
import fr.acinq.eclair.wire.protocol.Onion.FinalTlvPayload
import fr.acinq.eclair.wire.protocol.OnionTlv.{AmountToForward, KeySend, OutgoingCltv}
import fr.acinq.eclair.wire.protocol.{Onion, OnionCodecs, OnionTlv, TrampolineFeeInsufficient, _}
import fr.acinq.eclair.{CltvExpiryDelta, Features, MilliSatoshiLong, NodeParams, TestConstants, TestKitBaseClass, randomBytes32, randomKey}
import org.scalatest.funsuite.FixtureAnyFunSuiteLike
@ -88,22 +87,36 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
test("forward payment with user custom tlv records") { f =>
import f._
val keySendTlvRecords = Seq(GenericTlv(5482373484L, paymentPreimage))
val req = SendPaymentRequest(finalAmount, paymentHash, c, 1, CltvExpiryDelta(42), userCustomTlvs = keySendTlvRecords)
val customRecords = Seq(GenericTlv(500L, hex"01020304"), GenericTlv(501L, hex"d34db33f"))
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, None, paymentHash, priv_c.privateKey, "test", Channel.MIN_CLTV_EXPIRY_DELTA)
val req = SendPayment(finalAmount, pr, 1, Channel.MIN_CLTV_EXPIRY_DELTA, userCustomTlvs = customRecords)
sender.send(initiator, req)
sender.expectMsgType[UUID]
payFsm.expectMsgType[SendPaymentConfig]
val FinalTlvPayload(tlvs) = payFsm.expectMsgType[SendPayment].finalPayload
val FinalTlvPayload(tlvs) = payFsm.expectMsgType[PaymentLifecycle.SendPayment].finalPayload
assert(tlvs.get[AmountToForward].get.amount == finalAmount)
assert(tlvs.get[OutgoingCltv].get.cltv == req.fallbackFinalExpiryDelta.toCltvExpiry(nodeParams.currentBlockHeight + 1))
assert(tlvs.unknown == keySendTlvRecords)
assert(tlvs.unknown == customRecords)
}
test("forward keysend payment") { f =>
import f._
val req = SendSpontaneousPayment(finalAmount, c, paymentPreimage, 1)
sender.send(initiator, req)
sender.expectMsgType[UUID]
payFsm.expectMsgType[SendPaymentConfig]
val FinalTlvPayload(tlvs) = payFsm.expectMsgType[PaymentLifecycle.SendPayment].finalPayload
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)
}
test("reject payment with unknown mandatory feature") { f =>
import f._
val unknownFeature = 42
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, randomKey(), "Some invoice", CltvExpiryDelta(18), features = Some(PaymentRequestFeatures(unknownFeature)))
val req = SendPaymentRequest(finalAmount + 100.msat, paymentHash, c, 1, CltvExpiryDelta(42), Some(pr))
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, randomKey(), "Some invoice", CltvExpiryDelta(18), features = PaymentRequestFeatures(Features.VariableLengthOnion.mandatory, Features.PaymentSecret.mandatory, unknownFeature))
val req = SendPayment(finalAmount + 100.msat, pr, 1, CltvExpiryDelta(42))
sender.send(initiator, req)
val id = sender.expectMsgType[UUID]
val fail = sender.expectMsgType[PaymentFailed]
@ -116,46 +129,30 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
// we prioritize the invoice's finalExpiryDelta over the one from SendPaymentToRouteRequest
val ignoredFinalExpiryDelta = CltvExpiryDelta(18)
val finalExpiryDelta = CltvExpiryDelta(36)
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, "Some invoice", finalExpiryDelta, features = None)
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, "Some invoice", finalExpiryDelta)
val route = PredefinedNodeRoute(Seq(a, b, c))
sender.send(initiator, SendPaymentToRouteRequest(finalAmount, finalAmount, None, None, pr, ignoredFinalExpiryDelta, route, None, 0 msat, CltvExpiryDelta(0), Nil))
sender.send(initiator, SendPaymentToRoute(finalAmount, finalAmount, pr, ignoredFinalExpiryDelta, route, None, None, None, 0 msat, CltvExpiryDelta(0), 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(sender.ref, Left(route), FinalLegacyPayload(finalAmount, finalExpiryDelta.toCltvExpiry(nodeParams.currentBlockHeight + 1))))
}
test("forward legacy payment") { f =>
import f._
val finalExpiryDelta = CltvExpiryDelta(42)
val hints = Seq(Seq(ExtraHop(b, channelUpdate_bc.shortChannelId, feeBase = 10 msat, feeProportionalMillionths = 1, cltvExpiryDelta = CltvExpiryDelta(12))))
val routeParams = RouteParams(randomize = true, 15 msat, 1.5, 5, CltvExpiryDelta(561), None, MultiPartParams(10000 msat, 5))
sender.send(initiator, SendPaymentRequest(finalAmount, paymentHash, c, 1, finalExpiryDelta, assistedRoutes = hints, routeParams = Some(routeParams)))
val id1 = sender.expectMsgType[UUID]
payFsm.expectMsg(SendPaymentConfig(id1, id1, None, paymentHash, finalAmount, c, Upstream.Local(id1), None, storeInDb = true, publishEvent = true, Nil))
payFsm.expectMsg(SendPayment(sender.ref, c, FinalLegacyPayload(finalAmount, finalExpiryDelta.toCltvExpiry(nodeParams.currentBlockHeight + 1)), 1, hints, Some(routeParams)))
sender.send(initiator, SendPaymentRequest(finalAmount, paymentHash, e, 3))
val id2 = sender.expectMsgType[UUID]
payFsm.expectMsg(SendPaymentConfig(id2, id2, None, paymentHash, finalAmount, e, Upstream.Local(id2), None, storeInDb = true, publishEvent = true, Nil))
payFsm.expectMsg(SendPayment(sender.ref, e, FinalLegacyPayload(finalAmount, Channel.MIN_CLTV_EXPIRY_DELTA.toCltvExpiry(nodeParams.currentBlockHeight + 1)), 3))
payFsm.expectMsg(PaymentLifecycle.SendPaymentToRoute(sender.ref, Left(route), Onion.createSinglePartPayload(finalAmount, finalExpiryDelta.toCltvExpiry(nodeParams.currentBlockHeight + 1), pr.paymentSecret.get)))
}
test("forward single-part payment when multi-part deactivated", Tag("mpp_disabled")) { f =>
import f._
val finalExpiryDelta = CltvExpiryDelta(24)
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, randomKey(), "Some MPP invoice", finalExpiryDelta, features = Some(PaymentRequestFeatures(VariableLengthOnion.mandatory, PaymentSecret.mandatory, BasicMultiPartPayment.optional)))
val req = SendPaymentRequest(finalAmount, paymentHash, c, 1, /* ignored since the invoice provides it */ CltvExpiryDelta(12), Some(pr))
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, "Some MPP invoice", finalExpiryDelta, features = PaymentRequestFeatures(VariableLengthOnion.mandatory, PaymentSecret.mandatory, BasicMultiPartPayment.optional))
val req = SendPayment(finalAmount, pr, 1, /* ignored since the invoice provides it */ CltvExpiryDelta(12))
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(pr), storeInDb = true, publishEvent = true, Nil))
payFsm.expectMsg(SendPayment(sender.ref, c, FinalTlvPayload(TlvStream(OnionTlv.AmountToForward(finalAmount), OnionTlv.OutgoingCltv(req.finalExpiry(nodeParams.currentBlockHeight)), OnionTlv.PaymentData(pr.paymentSecret.get, finalAmount))), 1))
payFsm.expectMsg(PaymentLifecycle.SendPayment(sender.ref, c, FinalTlvPayload(TlvStream(OnionTlv.AmountToForward(finalAmount), OnionTlv.OutgoingCltv(req.finalExpiry(nodeParams.currentBlockHeight)), OnionTlv.PaymentData(pr.paymentSecret.get, finalAmount))), 1))
}
test("forward multi-part payment") { f =>
import f._
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, randomKey(), "Some invoice", CltvExpiryDelta(18), features = Some(PaymentRequestFeatures(VariableLengthOnion.mandatory, PaymentSecret.mandatory, BasicMultiPartPayment.optional)))
val req = SendPaymentRequest(finalAmount + 100.msat, paymentHash, c, 1, CltvExpiryDelta(42), Some(pr))
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, "Some invoice", CltvExpiryDelta(18), features = PaymentRequestFeatures(VariableLengthOnion.mandatory, PaymentSecret.mandatory, BasicMultiPartPayment.optional))
val req = SendPayment(finalAmount + 100.msat, pr, 1, CltvExpiryDelta(42))
sender.send(initiator, req)
val id = sender.expectMsgType[UUID]
multiPartPayFsm.expectMsg(SendPaymentConfig(id, id, None, paymentHash, finalAmount + 100.msat, c, Upstream.Local(id), Some(pr), storeInDb = true, publishEvent = true, Nil))
@ -164,27 +161,27 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
test("forward multi-part payment with pre-defined route") { f =>
import f._
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, "Some invoice", CltvExpiryDelta(18), features = Some(PaymentRequestFeatures(VariableLengthOnion.optional, PaymentSecret.optional, BasicMultiPartPayment.optional)))
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, "Some invoice", CltvExpiryDelta(18), features = PaymentRequestFeatures(VariableLengthOnion.mandatory, PaymentSecret.mandatory, BasicMultiPartPayment.optional))
val route = PredefinedChannelRoute(c, Seq(channelUpdate_ab.shortChannelId, channelUpdate_bc.shortChannelId))
val req = SendPaymentToRouteRequest(finalAmount / 2, finalAmount, None, None, pr, Channel.MIN_CLTV_EXPIRY_DELTA, route, None, 0 msat, CltvExpiryDelta(0), Nil)
val req = SendPaymentToRoute(finalAmount / 2, finalAmount, pr, Channel.MIN_CLTV_EXPIRY_DELTA, route, None, None, None, 0 msat, CltvExpiryDelta(0), Nil)
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(pr), storeInDb = true, publishEvent = true, Nil))
val msg = payFsm.expectMsgType[SendPaymentToRoute]
val msg = payFsm.expectMsgType[PaymentLifecycle.SendPaymentToRoute]
assert(msg.route === Left(route))
assert(msg.finalPayload.amount === finalAmount / 2)
assert(msg.finalPayload.expiry === req.finalExpiry(nodeParams.currentBlockHeight))
assert(msg.finalPayload.paymentSecret === pr.paymentSecret)
assert(msg.finalPayload.paymentSecret === pr.paymentSecret.get)
assert(msg.finalPayload.totalAmount === finalAmount)
}
test("forward trampoline payment") { f =>
import f._
val features = PaymentRequestFeatures(VariableLengthOnion.optional, PaymentSecret.optional, BasicMultiPartPayment.optional, TrampolinePayment.optional)
val features = PaymentRequestFeatures(VariableLengthOnion.mandatory, PaymentSecret.mandatory, BasicMultiPartPayment.optional, TrampolinePayment.optional)
val ignoredRoutingHints = List(List(ExtraHop(b, channelUpdate_bc.shortChannelId, feeBase = 10 msat, feeProportionalMillionths = 1, cltvExpiryDelta = CltvExpiryDelta(12))))
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, "Some phoenix invoice", CltvExpiryDelta(9), features = Some(features), extraHops = ignoredRoutingHints)
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, "Some phoenix invoice", CltvExpiryDelta(9), features = features, extraHops = ignoredRoutingHints)
val trampolineFees = 21000 msat
val req = SendTrampolinePaymentRequest(finalAmount, pr, b, Seq((trampolineFees, CltvExpiryDelta(12))), /* ignored since the invoice provides it */ CltvExpiryDelta(18))
val req = SendTrampolinePayment(finalAmount, pr, b, Seq((trampolineFees, CltvExpiryDelta(12))), /* ignored since the invoice provides it */ CltvExpiryDelta(18))
sender.send(initiator, req)
sender.expectMsgType[UUID]
multiPartPayFsm.expectMsgType[SendPaymentConfig]
@ -216,14 +213,14 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
assert(finalPayload.amount === finalAmount)
assert(finalPayload.totalAmount === finalAmount)
assert(finalPayload.expiry.toLong === currentBlockCount + 9 + 1)
assert(finalPayload.paymentSecret === pr.paymentSecret)
assert(finalPayload.paymentSecret === pr.paymentSecret.get)
}
test("forward trampoline to legacy payment") { f =>
import f._
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, "Some eclair-mobile invoice", CltvExpiryDelta(9))
val trampolineFees = 21000 msat
val req = SendTrampolinePaymentRequest(finalAmount, pr, b, Seq((trampolineFees, CltvExpiryDelta(12))))
val req = SendTrampolinePayment(finalAmount, pr, b, Seq((trampolineFees, CltvExpiryDelta(12))))
sender.send(initiator, req)
sender.expectMsgType[UUID]
multiPartPayFsm.expectMsgType[SendPaymentConfig]
@ -252,10 +249,10 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
import f._
// This is disabled because it would let the trampoline node steal the whole payment (if malicious).
val routingHints = List(List(PaymentRequest.ExtraHop(b, channelUpdate_bc.shortChannelId, 10 msat, 100, CltvExpiryDelta(144))))
val features = PaymentRequestFeatures(VariableLengthOnion.optional, PaymentSecret.optional, BasicMultiPartPayment.optional)
val pr = PaymentRequest(Block.RegtestGenesisBlock.hash, None, paymentHash, priv_a.privateKey, "#abittooreckless", CltvExpiryDelta(18), None, None, routingHints, features = Some(features))
val features = PaymentRequestFeatures(VariableLengthOnion.mandatory, PaymentSecret.mandatory, BasicMultiPartPayment.optional)
val pr = PaymentRequest(Block.RegtestGenesisBlock.hash, None, paymentHash, priv_a.privateKey, "#abittooreckless", CltvExpiryDelta(18), None, None, routingHints, features = features)
val trampolineFees = 21000 msat
val req = SendTrampolinePaymentRequest(finalAmount, pr, b, Seq((trampolineFees, CltvExpiryDelta(12))), CltvExpiryDelta(9))
val req = SendTrampolinePayment(finalAmount, pr, b, Seq((trampolineFees, CltvExpiryDelta(12))), CltvExpiryDelta(9))
sender.send(initiator, req)
val id = sender.expectMsgType[UUID]
val fail = sender.expectMsgType[PaymentFailed]
@ -268,10 +265,10 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
test("retry trampoline payment") { f =>
import f._
val features = PaymentRequestFeatures(VariableLengthOnion.optional, PaymentSecret.optional, BasicMultiPartPayment.optional, TrampolinePayment.optional)
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, "Some phoenix invoice", CltvExpiryDelta(18), features = Some(features))
val features = PaymentRequestFeatures(VariableLengthOnion.mandatory, PaymentSecret.mandatory, BasicMultiPartPayment.optional, TrampolinePayment.optional)
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, "Some phoenix invoice", CltvExpiryDelta(18), features = features)
val trampolineAttempts = (21000 msat, CltvExpiryDelta(12)) :: (25000 msat, CltvExpiryDelta(24)) :: Nil
val req = SendTrampolinePaymentRequest(finalAmount, pr, b, trampolineAttempts, CltvExpiryDelta(9))
val req = SendTrampolinePayment(finalAmount, pr, b, trampolineAttempts, CltvExpiryDelta(9))
sender.send(initiator, req)
sender.expectMsgType[UUID]
val cfg = multiPartPayFsm.expectMsgType[SendPaymentConfig]
@ -298,10 +295,10 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
test("retry trampoline payment and fail") { f =>
import f._
val features = PaymentRequestFeatures(VariableLengthOnion.optional, PaymentSecret.optional, BasicMultiPartPayment.optional, TrampolinePayment.optional)
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, "Some phoenix invoice", CltvExpiryDelta(18), features = Some(features))
val features = PaymentRequestFeatures(VariableLengthOnion.mandatory, PaymentSecret.mandatory, BasicMultiPartPayment.optional, TrampolinePayment.optional)
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, "Some phoenix invoice", CltvExpiryDelta(18), features = features)
val trampolineAttempts = (21000 msat, CltvExpiryDelta(12)) :: (25000 msat, CltvExpiryDelta(24)) :: Nil
val req = SendTrampolinePaymentRequest(finalAmount, pr, b, trampolineAttempts, CltvExpiryDelta(9))
val req = SendTrampolinePayment(finalAmount, pr, b, trampolineAttempts, CltvExpiryDelta(9))
sender.send(initiator, req)
sender.expectMsgType[UUID]
val cfg = multiPartPayFsm.expectMsgType[SendPaymentConfig]
@ -328,10 +325,10 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
test("retry trampoline payment and fail (route not found)") { f =>
import f._
val features = PaymentRequestFeatures(VariableLengthOnion.optional, PaymentSecret.optional, BasicMultiPartPayment.optional, TrampolinePayment.optional)
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, "Some phoenix invoice", CltvExpiryDelta(18), features = Some(features))
val features = PaymentRequestFeatures(VariableLengthOnion.mandatory, PaymentSecret.mandatory, BasicMultiPartPayment.optional, TrampolinePayment.optional)
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, "Some phoenix invoice", CltvExpiryDelta(18), features = features)
val trampolineAttempts = (21000 msat, CltvExpiryDelta(12)) :: (25000 msat, CltvExpiryDelta(24)) :: Nil
val req = SendTrampolinePaymentRequest(finalAmount, pr, b, trampolineAttempts, CltvExpiryDelta(9))
val req = SendTrampolinePayment(finalAmount, pr, b, trampolineAttempts, CltvExpiryDelta(9))
sender.send(initiator, req)
sender.expectMsgType[UUID]
@ -359,15 +356,15 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, "Some invoice", CltvExpiryDelta(18))
val trampolineFees = 100 msat
val route = PredefinedNodeRoute(Seq(a, b))
val req = SendPaymentToRouteRequest(finalAmount + trampolineFees, finalAmount, None, None, pr, Channel.MIN_CLTV_EXPIRY_DELTA, route, None, trampolineFees, CltvExpiryDelta(144), Seq(b, c))
val req = SendPaymentToRoute(finalAmount + trampolineFees, finalAmount, pr, Channel.MIN_CLTV_EXPIRY_DELTA, 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(pr), storeInDb = true, publishEvent = true, Seq(NodeHop(b, c, CltvExpiryDelta(0), 0 msat))))
val msg = payFsm.expectMsgType[SendPaymentToRoute]
val msg = payFsm.expectMsgType[PaymentLifecycle.SendPaymentToRoute]
assert(msg.route === Left(route))
assert(msg.finalPayload.amount === finalAmount + trampolineFees)
assert(msg.finalPayload.paymentSecret === payment.trampolineSecret)
assert(msg.finalPayload.paymentSecret === payment.trampolineSecret.get)
assert(msg.finalPayload.totalAmount === finalAmount + trampolineFees)
assert(msg.finalPayload.isInstanceOf[Onion.FinalTlvPayload])
val trampolineOnion = msg.finalPayload.asInstanceOf[Onion.FinalTlvPayload].records.get[OnionTlv.TrampolineOnion]

View file

@ -32,7 +32,7 @@ import fr.acinq.eclair.io.Peer.PeerRoutingMessage
import fr.acinq.eclair.payment.OutgoingPacket.Upstream
import fr.acinq.eclair.payment.PaymentRequest.ExtraHop
import fr.acinq.eclair.payment.PaymentSent.PartialPayment
import fr.acinq.eclair.payment.send.PaymentInitiator.{SendPaymentConfig, SendPaymentRequest}
import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentConfig
import fr.acinq.eclair.payment.send.PaymentLifecycle
import fr.acinq.eclair.payment.send.PaymentLifecycle._
import fr.acinq.eclair.router.Announcements.makeChannelUpdate
@ -40,7 +40,6 @@ import fr.acinq.eclair.router.BaseRouterSpec.channelAnnouncement
import fr.acinq.eclair.router.Router._
import fr.acinq.eclair.router._
import fr.acinq.eclair.transactions.Scripts
import fr.acinq.eclair.wire.protocol.Onion.FinalLegacyPayload
import fr.acinq.eclair.wire.protocol._
import java.util.UUID
@ -59,7 +58,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
val defaultPaymentHash = Crypto.sha256(defaultPaymentPreimage)
val defaultOrigin = Origin.LocalCold(UUID.randomUUID())
val defaultExternalId = UUID.randomUUID().toString
val defaultPaymentRequest = SendPaymentRequest(defaultAmountMsat, defaultPaymentHash, d, 1, externalId = Some(defaultExternalId))
val defaultInvoice = PaymentRequest(Block.RegtestGenesisBlock.hash, None, defaultPaymentHash, priv_d, "test", Channel.MIN_CLTV_EXPIRY_DELTA)
def defaultRouteRequest(source: PublicKey, target: PublicKey, cfg: SendPaymentConfig): RouteRequest = RouteRequest(source, target, defaultAmountMsat, defaultMaxFee, paymentContext = Some(cfg.paymentContext))
@ -75,7 +74,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
def createPaymentLifecycle(storeInDb: Boolean = true, publishEvent: 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), defaultPaymentRequest.paymentRequest, storeInDb, publishEvent, Nil)
val cfg = SendPaymentConfig(id, parentId, Some(defaultExternalId), defaultPaymentHash, defaultAmountMsat, d, Upstream.Local(id), Some(defaultInvoice), storeInDb, publishEvent, Nil)
val (routerForwarder, register, sender, monitor, eventListener) = (TestProbe(), TestProbe(), TestProbe(), TestProbe(), TestProbe())
val paymentFSM = TestFSMRef(new PaymentLifecycle(nodeParams, cfg, routerForwarder.ref, register.ref))
paymentFSM ! SubscribeTransitionCallBack(monitor.ref)
@ -98,7 +97,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
// pre-computed route going from A to D
val route = Route(defaultAmountMsat, ChannelHop(a, b, update_ab) :: ChannelHop(b, c, update_bc) :: ChannelHop(c, d, update_cd) :: Nil)
val request = SendPaymentToRoute(sender.ref, Right(route), FinalLegacyPayload(defaultAmountMsat, defaultExpiry))
val request = SendPaymentToRoute(sender.ref, Right(route), Onion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get))
sender.send(paymentFSM, request)
routerForwarder.expectNoMsg(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[_]])
@ -106,7 +105,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
val Transition(_, WAITING_FOR_ROUTE, WAITING_FOR_PAYMENT_COMPLETE) = monitor.expectMsgClass(classOf[Transition[_]])
awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending))
val Some(outgoing) = nodeParams.db.payments.getOutgoingPayment(id)
assert(outgoing.copy(createdAt = 0) === OutgoingPayment(id, parentId, Some(defaultExternalId), defaultPaymentHash, PaymentType.Standard, defaultAmountMsat, defaultAmountMsat, d, 0, None, OutgoingPaymentStatus.Pending))
assert(outgoing.copy(createdAt = 0) === OutgoingPayment(id, parentId, Some(defaultExternalId), defaultPaymentHash, PaymentType.Standard, defaultAmountMsat, defaultAmountMsat, d, 0, Some(defaultInvoice), OutgoingPaymentStatus.Pending))
sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFulfill(UpdateFulfillHtlc(ByteVector32.Zeroes, 0, defaultPaymentPreimage))))
val ps = sender.expectMsgType[PaymentSent]
@ -122,7 +121,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), FinalLegacyPayload(defaultAmountMsat, defaultExpiry))
val request = SendPaymentToRoute(sender.ref, Left(route), Onion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get))
sender.send(paymentFSM, request)
routerForwarder.expectMsg(FinalizeRoute(defaultAmountMsat, route, paymentContext = Some(cfg.paymentContext)))
@ -132,7 +131,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
val Transition(_, WAITING_FOR_ROUTE, WAITING_FOR_PAYMENT_COMPLETE) = monitor.expectMsgClass(classOf[Transition[_]])
awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending))
val Some(outgoing) = nodeParams.db.payments.getOutgoingPayment(id)
assert(outgoing.copy(createdAt = 0) === OutgoingPayment(id, parentId, Some(defaultExternalId), defaultPaymentHash, PaymentType.Standard, defaultAmountMsat, defaultAmountMsat, d, 0, None, OutgoingPaymentStatus.Pending))
assert(outgoing.copy(createdAt = 0) === OutgoingPayment(id, parentId, Some(defaultExternalId), defaultPaymentHash, PaymentType.Standard, defaultAmountMsat, defaultAmountMsat, d, 0, Some(defaultInvoice), OutgoingPaymentStatus.Pending))
sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFulfill(UpdateFulfillHtlc(ByteVector32.Zeroes, 0, defaultPaymentPreimage))))
val ps = sender.expectMsgType[PaymentSent]
@ -144,7 +143,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
val payFixture = createPaymentLifecycle()
import payFixture._
val brokenRoute = SendPaymentToRoute(sender.ref, Left(PredefinedNodeRoute(Seq(randomKey().publicKey, randomKey().publicKey, randomKey().publicKey))), FinalLegacyPayload(defaultAmountMsat, defaultExpiry))
val brokenRoute = SendPaymentToRoute(sender.ref, Left(PredefinedNodeRoute(Seq(randomKey().publicKey, randomKey().publicKey, randomKey().publicKey))), Onion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get))
sender.send(paymentFSM, brokenRoute)
routerForwarder.expectMsgType[FinalizeRoute]
routerForwarder.forward(routerFixture.router)
@ -157,7 +156,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
val payFixture = createPaymentLifecycle()
import payFixture._
val brokenRoute = SendPaymentToRoute(sender.ref, Left(PredefinedChannelRoute(randomKey().publicKey, Seq(ShortChannelId(1), ShortChannelId(2)))), FinalLegacyPayload(defaultAmountMsat, defaultExpiry))
val brokenRoute = SendPaymentToRoute(sender.ref, Left(PredefinedChannelRoute(randomKey().publicKey, Seq(ShortChannelId(1), ShortChannelId(2)))), Onion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get))
sender.send(paymentFSM, brokenRoute)
routerForwarder.expectMsgType[FinalizeRoute]
routerForwarder.forward(routerFixture.router)
@ -174,7 +173,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
val recipient = randomKey().publicKey
val route = PredefinedNodeRoute(Seq(a, b, c, recipient))
val routingHint = Seq(Seq(ExtraHop(c, ShortChannelId(561), 1 msat, 100, CltvExpiryDelta(144))))
val request = SendPaymentToRoute(sender.ref, Left(route), FinalLegacyPayload(defaultAmountMsat, defaultExpiry), routingHint)
val request = SendPaymentToRoute(sender.ref, Left(route), Onion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get), routingHint)
sender.send(paymentFSM, request)
routerForwarder.expectMsg(FinalizeRoute(defaultAmountMsat, route, routingHint, paymentContext = Some(cfg.paymentContext)))
@ -196,7 +195,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
import payFixture._
import cfg._
val request = SendPayment(sender.ref, f, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 5)
val request = SendPayment(sender.ref, f, Onion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get), 5)
sender.send(paymentFSM, request)
val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]])
val routeRequest = routerForwarder.expectMsgType[RouteRequest]
@ -212,7 +211,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
import payFixture._
import cfg._
val request = SendPayment(sender.ref, d, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 5, routeParams = Some(RouteParams(randomize = false, 100 msat, 0.0, 20, CltvExpiryDelta(2016), None, MultiPartParams(10000 msat, 5))))
val request = SendPayment(sender.ref, d, Onion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get), 5, routeParams = Some(RouteParams(randomize = false, 100 msat, 0.0, 20, CltvExpiryDelta(2016), None, MultiPartParams(10000 msat, 5))))
sender.send(paymentFSM, request)
val routeRequest = routerForwarder.expectMsgType[RouteRequest]
val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]])
@ -227,7 +226,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
import payFixture._
import cfg._
val request = SendPayment(sender.ref, d, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 2)
val request = SendPayment(sender.ref, d, Onion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get), 2)
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))
@ -263,7 +262,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
import payFixture._
import cfg._
val request = SendPayment(sender.ref, d, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 2)
val request = SendPayment(sender.ref, d, Onion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get), 2)
sender.send(paymentFSM, request)
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending))
routerForwarder.expectMsgType[RouteRequest]
@ -284,7 +283,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
import payFixture._
import cfg._
val request = SendPayment(sender.ref, d, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 2)
val request = SendPayment(sender.ref, d, Onion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get), 2)
sender.send(paymentFSM, request)
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending))
routerForwarder.expectMsgType[RouteRequest]
@ -304,7 +303,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
import payFixture._
import cfg._
val request = SendPayment(sender.ref, d, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 2)
val request = SendPayment(sender.ref, d, Onion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get), 2)
sender.send(paymentFSM, request)
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending))
@ -327,7 +326,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
import payFixture._
import cfg._
val request = SendPayment(sender.ref, d, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 2)
val request = SendPayment(sender.ref, d, Onion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get), 2)
sender.send(paymentFSM, request)
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending))
@ -350,7 +349,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
import payFixture._
import cfg._
val request = SendPayment(sender.ref, d, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 2)
val request = SendPayment(sender.ref, d, Onion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get), 2)
sender.send(paymentFSM, request)
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending))
@ -373,7 +372,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
val payFixture = createPaymentLifecycle()
import payFixture._
val request = SendPayment(sender.ref, d, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 2)
val request = SendPayment(sender.ref, d, Onion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get), 2)
sender.send(paymentFSM, request)
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE)
val WaitingForRoute(_, Nil, _) = paymentFSM.stateData
@ -403,7 +402,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
import payFixture._
import cfg._
val request = SendPayment(sender.ref, d, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 5)
val request = SendPayment(sender.ref, d, Onion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get), 5)
sender.send(paymentFSM, request)
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending))
@ -455,7 +454,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
val payFixture = createPaymentLifecycle()
import payFixture._
val request = SendPayment(sender.ref, d, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 1)
val request = SendPayment(sender.ref, d, Onion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get), 1)
sender.send(paymentFSM, request)
routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d, cfg))
routerForwarder.forward(routerFixture.router)
@ -485,7 +484,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
ExtraHop(c, channelId_cd, update_cd.feeBaseMsat, update_cd.feeProportionalMillionths, update_cd.cltvExpiryDelta)
))
val request = SendPayment(sender.ref, d, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 5, assistedRoutes = assistedRoutes)
val request = SendPayment(sender.ref, d, Onion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get), 5, assistedRoutes = assistedRoutes)
sender.send(paymentFSM, request)
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending))
@ -526,7 +525,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
// we build an assisted route for channel cd
val assistedRoutes = Seq(Seq(ExtraHop(c, channelId_cd, update_cd.feeBaseMsat, update_cd.feeProportionalMillionths, update_cd.cltvExpiryDelta)))
val request = SendPayment(sender.ref, d, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 1, assistedRoutes = assistedRoutes)
val request = SendPayment(sender.ref, d, Onion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get), 1, assistedRoutes = assistedRoutes)
sender.send(paymentFSM, request)
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending))
@ -551,7 +550,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
import payFixture._
import cfg._
val request = SendPayment(sender.ref, d, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 2)
val request = SendPayment(sender.ref, d, Onion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get), 2)
sender.send(paymentFSM, request)
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending))
@ -589,7 +588,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
import payFixture._
import cfg._
val request = SendPayment(sender.ref, d, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 5)
val request = SendPayment(sender.ref, d, Onion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get), 5)
sender.send(paymentFSM, request)
routerForwarder.expectMsgType[RouteRequest]
val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]])
@ -597,7 +596,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
val Transition(_, WAITING_FOR_ROUTE, WAITING_FOR_PAYMENT_COMPLETE) = monitor.expectMsgClass(classOf[Transition[_]])
awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending))
val Some(outgoing) = nodeParams.db.payments.getOutgoingPayment(id)
assert(outgoing.copy(createdAt = 0) === OutgoingPayment(id, parentId, Some(defaultExternalId), defaultPaymentHash, PaymentType.Standard, defaultAmountMsat, defaultAmountMsat, d, 0, None, OutgoingPaymentStatus.Pending))
assert(outgoing.copy(createdAt = 0) === OutgoingPayment(id, parentId, Some(defaultExternalId), defaultPaymentHash, PaymentType.Standard, defaultAmountMsat, defaultAmountMsat, d, 0, Some(defaultInvoice), OutgoingPaymentStatus.Pending))
sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFulfill(UpdateFulfillHtlc(ByteVector32.Zeroes, 0, defaultPaymentPreimage))))
val ps = eventListener.expectMsgType[PaymentSent]
@ -636,7 +635,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
import payFixture._
// we send a payment to H
val request = SendPayment(sender.ref, h, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 5)
val request = SendPayment(sender.ref, h, Onion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get), 5)
sender.send(paymentFSM, request)
routerForwarder.expectMsgType[RouteRequest]
@ -713,7 +712,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
import payFixture._
import cfg._
val request = SendPayment(sender.ref, d, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 3)
val request = SendPayment(sender.ref, d, Onion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get), 3)
sender.send(paymentFSM, request)
routerForwarder.expectMsgType[RouteRequest]
val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]])

View file

@ -28,10 +28,10 @@ import fr.acinq.eclair.payment.OutgoingPacket._
import fr.acinq.eclair.payment.PaymentRequest.PaymentRequestFeatures
import fr.acinq.eclair.router.Router.{ChannelHop, NodeHop}
import fr.acinq.eclair.transactions.Transactions.InputInfo
import fr.acinq.eclair.wire.protocol.Onion.{FinalLegacyPayload, FinalTlvPayload, RelayLegacyPayload}
import fr.acinq.eclair.wire.protocol.Onion.{ChannelRelayTlvPayload, FinalTlvPayload, RelayLegacyPayload}
import fr.acinq.eclair.wire.protocol.OnionTlv.{AmountToForward, OutgoingCltv, PaymentData}
import fr.acinq.eclair.wire.protocol._
import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TestConstants, UInt64, nodeFee, randomBytes32, randomKey}
import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TestConstants, nodeFee, randomBytes32, randomKey}
import org.scalatest.BeforeAndAfterAll
import org.scalatest.funsuite.AnyFunSuite
import scodec.Attempt
@ -59,12 +59,8 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
assert(ref === fee)
}
def testBuildOnion(legacy: Boolean): Unit = {
val finalPayload = if (legacy) {
FinalLegacyPayload(finalAmount, finalExpiry)
} else {
FinalTlvPayload(TlvStream(AmountToForward(finalAmount), OutgoingCltv(finalExpiry)))
}
def testBuildOnion(): Unit = {
val finalPayload = FinalTlvPayload(TlvStream(AmountToForward(finalAmount), OutgoingCltv(finalExpiry), PaymentData(paymentSecret, 0 msat)))
val (firstAmount, firstExpiry, onion) = buildPacket(Sphinx.PaymentPacket)(paymentHash, hops, finalPayload)
assert(firstAmount === amount_ab)
assert(firstExpiry === expiry_ab)
@ -111,19 +107,15 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
assert(payload_e.amount === finalAmount)
assert(payload_e.totalAmount === finalAmount)
assert(payload_e.expiry === finalExpiry)
assert(payload_e.paymentSecret === None)
assert(payload_e.paymentSecret === paymentSecret)
}
test("build onion with final legacy payload") {
testBuildOnion(legacy = true)
}
test("build onion with final tlv payload") {
testBuildOnion(legacy = false)
test("build onion with final payload") {
testBuildOnion()
}
test("build a command including the onion") {
val (add, _) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID), paymentHash, hops, FinalLegacyPayload(finalAmount, finalExpiry))
val (add, _) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID), paymentHash, hops, Onion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret))
assert(add.amount > finalAmount)
assert(add.cltvExpiry === finalExpiry + channelUpdate_de.cltvExpiryDelta + channelUpdate_cd.cltvExpiryDelta + channelUpdate_bc.cltvExpiryDelta)
assert(add.paymentHash === paymentHash)
@ -134,7 +126,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
}
test("build a command with no hops") {
val (add, _) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, hops.take(1), FinalLegacyPayload(finalAmount, finalExpiry))
val (add, _) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, hops.take(1), Onion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret))
assert(add.amount === finalAmount)
assert(add.cltvExpiry === finalExpiry)
assert(add.paymentHash === paymentHash)
@ -147,7 +139,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
assert(payload_b.amount === finalAmount)
assert(payload_b.totalAmount === finalAmount)
assert(payload_b.expiry === finalExpiry)
assert(payload_b.paymentSecret === None)
assert(payload_b.paymentSecret === paymentSecret)
}
test("build a trampoline payment") {
@ -167,7 +159,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
val add_b = UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry, onion.packet)
val Right(ChannelRelayPacket(add_b2, payload_b, packet_c)) = decrypt(add_b, priv_b.privateKey)
assert(add_b2 === add_b)
assert(payload_b === RelayLegacyPayload(channelUpdate_bc.shortChannelId, amount_bc, expiry_bc))
assert(payload_b === ChannelRelayTlvPayload(channelUpdate_bc.shortChannelId, amount_bc, expiry_bc))
val add_c = UpdateAddHtlc(randomBytes32(), 2, amount_bc, paymentHash, expiry_bc, packet_c)
val Right(NodeRelayPacket(add_c2, outer_c, inner_c, packet_d)) = decrypt(add_c, priv_c.privateKey)
@ -217,8 +209,8 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
val routingHints = List(List(PaymentRequest.ExtraHop(randomKey().publicKey, ShortChannelId(42), 10 msat, 100, CltvExpiryDelta(144))))
val invoiceFeatures = PaymentRequestFeatures(VariableLengthOnion.mandatory, PaymentSecret.mandatory, BasicMultiPartPayment.optional)
val invoice = PaymentRequest(Block.RegtestGenesisBlock.hash, Some(finalAmount), paymentHash, priv_a.privateKey, "#reckless", CltvExpiryDelta(18), None, None, routingHints, features = Some(invoiceFeatures))
val (amount_ac, expiry_ac, trampolineOnion) = buildTrampolineToLegacyPacket(invoice, trampolineHops, FinalLegacyPayload(finalAmount, finalExpiry))
val invoice = PaymentRequest(Block.RegtestGenesisBlock.hash, Some(finalAmount), paymentHash, priv_a.privateKey, "#reckless", CltvExpiryDelta(18), None, None, routingHints, features = invoiceFeatures)
val (amount_ac, expiry_ac, trampolineOnion) = buildTrampolineToLegacyPacket(invoice, trampolineHops, Onion.createSinglePartPayload(finalAmount, finalExpiry, invoice.paymentSecret.get))
assert(amount_ac === amount_bc)
assert(expiry_ac === expiry_bc)
@ -265,12 +257,12 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
val routingHintOverflow = List(List.fill(7)(PaymentRequest.ExtraHop(randomKey().publicKey, ShortChannelId(1), 10 msat, 100, CltvExpiryDelta(12))))
val invoice = PaymentRequest(Block.RegtestGenesisBlock.hash, Some(finalAmount), paymentHash, priv_a.privateKey, "#reckless", CltvExpiryDelta(18), None, None, routingHintOverflow)
assertThrows[IllegalArgumentException](
buildTrampolineToLegacyPacket(invoice, trampolineHops, FinalLegacyPayload(finalAmount, finalExpiry))
buildTrampolineToLegacyPacket(invoice, trampolineHops, Onion.createSinglePartPayload(finalAmount, finalExpiry, invoice.paymentSecret.get))
)
}
test("fail to decrypt when the onion is invalid") {
val (firstAmount, firstExpiry, onion) = buildPacket(Sphinx.PaymentPacket)(paymentHash, hops, FinalLegacyPayload(finalAmount, finalExpiry))
val (firstAmount, firstExpiry, onion) = buildPacket(Sphinx.PaymentPacket)(paymentHash, hops, Onion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret))
val add = UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry, onion.packet.copy(payload = onion.packet.payload.reverse))
val Left(failure) = decrypt(add, priv_b.privateKey)
assert(failure.isInstanceOf[InvalidOnionHmac])
@ -287,21 +279,21 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
}
test("fail to decrypt when payment hash doesn't match associated data") {
val (firstAmount, firstExpiry, onion) = buildPacket(Sphinx.PaymentPacket)(paymentHash.reverse, hops, FinalLegacyPayload(finalAmount, finalExpiry))
val (firstAmount, firstExpiry, onion) = buildPacket(Sphinx.PaymentPacket)(paymentHash.reverse, hops, Onion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret))
val add = UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry, onion.packet)
val Left(failure) = decrypt(add, priv_b.privateKey)
assert(failure.isInstanceOf[InvalidOnionHmac])
}
test("fail to decrypt at the final node when amount has been modified by next-to-last node") {
val (firstAmount, firstExpiry, onion) = buildPacket(Sphinx.PaymentPacket)(paymentHash, hops.take(1), FinalLegacyPayload(finalAmount, finalExpiry))
val (firstAmount, firstExpiry, onion) = buildPacket(Sphinx.PaymentPacket)(paymentHash, hops.take(1), Onion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret))
val add = UpdateAddHtlc(randomBytes32(), 1, firstAmount - 100.msat, paymentHash, firstExpiry, onion.packet)
val Left(failure) = decrypt(add, priv_b.privateKey)
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 (firstAmount, firstExpiry, onion) = buildPacket(Sphinx.PaymentPacket)(paymentHash, hops.take(1), FinalLegacyPayload(finalAmount, finalExpiry))
val (firstAmount, firstExpiry, onion) = buildPacket(Sphinx.PaymentPacket)(paymentHash, hops.take(1), Onion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret))
val add = UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry - CltvExpiryDelta(12), onion.packet)
val Left(failure) = decrypt(add, priv_b.privateKey)
assert(failure === FinalIncorrectCltvExpiry(firstExpiry - CltvExpiryDelta(12)))
@ -337,22 +329,8 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
assert(failure === FinalIncorrectCltvExpiry(invalidExpiry))
}
test("fail to decrypt at the final trampoline node when payment secret is missing") {
val (amount_ac, expiry_ac, trampolineOnion) = buildPacket(Sphinx.TrampolinePacket)(paymentHash, trampolineHops, Onion.createSinglePartPayload(finalAmount, finalExpiry)) // no payment secret
val (firstAmount, firstExpiry, onion) = buildPacket(Sphinx.PaymentPacket)(paymentHash, trampolineChannelHops, Onion.createTrampolinePayload(amount_ac, amount_ac, expiry_ac, randomBytes32(), trampolineOnion.packet))
val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry, onion.packet), priv_b.privateKey)
val Right(NodeRelayPacket(_, _, _, packet_d)) = decrypt(UpdateAddHtlc(randomBytes32(), 2, amount_bc, paymentHash, expiry_bc, packet_c), priv_c.privateKey)
// c forwards the trampoline payment to d.
val (amount_d, expiry_d, onion_d) = buildPacket(Sphinx.PaymentPacket)(paymentHash, ChannelHop(c, d, channelUpdate_cd) :: Nil, Onion.createTrampolinePayload(amount_cd, amount_cd, expiry_cd, randomBytes32(), packet_d))
val Right(NodeRelayPacket(_, _, _, packet_e)) = decrypt(UpdateAddHtlc(randomBytes32(), 3, amount_d, paymentHash, expiry_d, onion_d.packet), priv_d.privateKey)
// d forwards the trampoline payment to e.
val (amount_e, expiry_e, onion_e) = buildPacket(Sphinx.PaymentPacket)(paymentHash, ChannelHop(d, e, channelUpdate_de) :: Nil, Onion.createTrampolinePayload(amount_de, amount_de, expiry_de, randomBytes32(), packet_e))
val Left(failure) = decrypt(UpdateAddHtlc(randomBytes32(), 4, amount_e, paymentHash, expiry_e, onion_e.packet), priv_e.privateKey)
assert(failure === InvalidOnionPayload(UInt64(8), 0))
}
test("fail to decrypt at intermediate trampoline node when amount is invalid") {
val (amount_ac, expiry_ac, trampolineOnion) = buildPacket(Sphinx.TrampolinePacket)(paymentHash, trampolineHops, Onion.createSinglePartPayload(finalAmount, finalExpiry)) // no payment secret
val (amount_ac, expiry_ac, trampolineOnion) = buildPacket(Sphinx.TrampolinePacket)(paymentHash, trampolineHops, Onion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret))
val (firstAmount, firstExpiry, onion) = buildPacket(Sphinx.PaymentPacket)(paymentHash, trampolineChannelHops, Onion.createTrampolinePayload(amount_ac, amount_ac, expiry_ac, randomBytes32(), trampolineOnion.packet))
val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry, onion.packet), priv_b.privateKey)
// A trampoline relay is very similar to a final node: it can validate that the HTLC amount matches the onion outer amount.
@ -361,7 +339,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
}
test("fail to decrypt at intermediate trampoline node when expiry is invalid") {
val (amount_ac, expiry_ac, trampolineOnion) = buildPacket(Sphinx.TrampolinePacket)(paymentHash, trampolineHops, Onion.createSinglePartPayload(finalAmount, finalExpiry)) // no payment secret
val (amount_ac, expiry_ac, trampolineOnion) = buildPacket(Sphinx.TrampolinePacket)(paymentHash, trampolineHops, Onion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret))
val (firstAmount, firstExpiry, onion) = buildPacket(Sphinx.PaymentPacket)(paymentHash, trampolineChannelHops, Onion.createTrampolinePayload(amount_ac, amount_ac, expiry_ac, randomBytes32(), trampolineOnion.packet))
val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry, onion.packet), priv_b.privateKey)
// A trampoline relay is very similar to a final node: it can validate that the HTLC expiry matches the onion outer expiry.

View file

@ -383,29 +383,30 @@ class PaymentRequestSpec extends AnyFunSuite {
test("supported payment request features") {
val nodeParams = TestConstants.Alice.nodeParams.copy(features = Features(knownFeatures.map(f => f -> FeatureSupport.Optional).toMap))
case class Result(allowMultiPart: Boolean, requirePaymentSecret: Boolean, areSupported: Boolean) // "supported" is based on the "it's okay to be odd" rule"
case class Result(allowMultiPart: Boolean, requirePaymentSecret: Boolean, areSupported: Boolean) // "supported" is based on the "it's okay to be odd" rule
val featureBits = Map(
PaymentRequestFeatures(bin" 00000000000000000000") -> Result(allowMultiPart = false, requirePaymentSecret = false, areSupported = true),
PaymentRequestFeatures(bin" 00011000001000000000") -> Result(allowMultiPart = true, requirePaymentSecret = false, areSupported = true),
PaymentRequestFeatures(bin" 00101000001000000000") -> Result(allowMultiPart = true, requirePaymentSecret = false, areSupported = true),
PaymentRequestFeatures(bin" 00010100001000000000") -> Result(allowMultiPart = true, requirePaymentSecret = true, areSupported = true),
PaymentRequestFeatures(bin" 00011000001000000000") -> Result(allowMultiPart = true, requirePaymentSecret = false, areSupported = true),
PaymentRequestFeatures(bin" 00101000001000000000") -> Result(allowMultiPart = true, requirePaymentSecret = false, areSupported = true),
PaymentRequestFeatures(bin" 01000000000000000000") -> Result(allowMultiPart = false, requirePaymentSecret = false, areSupported = true),
PaymentRequestFeatures(bin" 0000010000000000000000000") -> Result(allowMultiPart = false, requirePaymentSecret = false, areSupported = true),
PaymentRequestFeatures(bin" 0000011000000000000000000") -> Result(allowMultiPart = false, requirePaymentSecret = false, areSupported = true),
PaymentRequestFeatures(bin" 0000110000001000000000000") -> Result(allowMultiPart = false, requirePaymentSecret = false, areSupported = true),
PaymentRequestFeatures(bin" 0000100000001000000000000") -> Result(allowMultiPart = false, requirePaymentSecret = false, areSupported = true),
PaymentRequestFeatures(bin" 00000100000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = true),
PaymentRequestFeatures(bin" 00010100000100000000") -> Result(allowMultiPart = true, requirePaymentSecret = true, areSupported = true),
PaymentRequestFeatures(bin" 00100100000100000000") -> Result(allowMultiPart = true, requirePaymentSecret = true, areSupported = true),
PaymentRequestFeatures(bin" 00010100000100000000") -> Result(allowMultiPart = true, requirePaymentSecret = true, areSupported = true),
PaymentRequestFeatures(bin" 00010100000100000000") -> Result(allowMultiPart = true, requirePaymentSecret = true, areSupported = true),
PaymentRequestFeatures(bin" 00100100000100000000") -> Result(allowMultiPart = true, requirePaymentSecret = true, areSupported = true),
PaymentRequestFeatures(bin" 01000100000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = true),
PaymentRequestFeatures(bin" 0000010000100000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = true),
PaymentRequestFeatures(bin" 0000011000100000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = true),
PaymentRequestFeatures(bin" 0000110000101000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = true),
PaymentRequestFeatures(bin" 0000100000101000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = true),
// those are useful for nonreg testing of the areSupported method (which needs to be updated with every new supported mandatory bit)
PaymentRequestFeatures(bin" 0010000000000000000000000") -> Result(allowMultiPart = false, requirePaymentSecret = false, areSupported = false),
PaymentRequestFeatures(bin" 000001000000000000000000000000") -> Result(allowMultiPart = false, requirePaymentSecret = false, areSupported = false),
PaymentRequestFeatures(bin" 000100000000000000000000000000") -> Result(allowMultiPart = false, requirePaymentSecret = false, areSupported = true),
PaymentRequestFeatures(bin"00000010000000000000000000000000000") -> Result(allowMultiPart = false, requirePaymentSecret = false, areSupported = false),
PaymentRequestFeatures(bin"00001000000000000000000000000000000") -> Result(allowMultiPart = false, requirePaymentSecret = false, areSupported = false)
PaymentRequestFeatures(bin" 0010000000100000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = false),
PaymentRequestFeatures(bin" 000001000000000100000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = false),
PaymentRequestFeatures(bin" 000100000000000100000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = true),
PaymentRequestFeatures(bin"00000010000000000000100000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = false),
PaymentRequestFeatures(bin"00001000000000000000100000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = false)
)
for ((features, res) <- featureBits) {
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, "Some invoice", CltvExpiryDelta(18), features = Some(features))
println(features.features)
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, "Some invoice", CltvExpiryDelta(18), features = features)
assert(Result(pr.features.allowMultiPart, pr.features.requirePaymentSecret, pr.features.areSupported(nodeParams)) === res)
assert(PaymentRequest.read(PaymentRequest.write(pr)) === pr)
}
@ -448,7 +449,7 @@ class PaymentRequestSpec extends AnyFunSuite {
// A multi-part invoice must use a payment secret.
assertThrows[IllegalArgumentException](
PaymentRequest(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, "MPP without secrets", CltvExpiryDelta(18), features = Some(PaymentRequestFeatures(BasicMultiPartPayment.optional, VariableLengthOnion.optional)))
PaymentRequest(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, "MPP without secrets", CltvExpiryDelta(18), features = PaymentRequestFeatures(BasicMultiPartPayment.optional, VariableLengthOnion.optional))
)
}
@ -456,11 +457,11 @@ class PaymentRequestSpec extends AnyFunSuite {
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, "Some invoice", CltvExpiryDelta(18))
assert(!pr.features.allowTrampoline)
val pr1 = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, "Some invoice", CltvExpiryDelta(18), features = Some(PaymentRequestFeatures(VariableLengthOnion.optional, PaymentSecret.optional, TrampolinePayment.optional)))
val pr1 = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, "Some invoice", CltvExpiryDelta(18), features = PaymentRequestFeatures(VariableLengthOnion.mandatory, PaymentSecret.mandatory, TrampolinePayment.optional))
assert(!pr1.features.allowMultiPart)
assert(pr1.features.allowTrampoline)
val pr2 = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, "Some invoice", CltvExpiryDelta(18), features = Some(PaymentRequestFeatures(VariableLengthOnion.optional, PaymentSecret.optional, BasicMultiPartPayment.optional, TrampolinePayment.optional)))
val pr2 = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, "Some invoice", CltvExpiryDelta(18), features = PaymentRequestFeatures(VariableLengthOnion.mandatory, PaymentSecret.mandatory, BasicMultiPartPayment.optional, TrampolinePayment.optional))
assert(pr2.features.allowMultiPart)
assert(pr2.features.allowTrampoline)

View file

@ -32,7 +32,6 @@ import fr.acinq.eclair.payment.relay.{PostRestartHtlcCleaner, Relayer}
import fr.acinq.eclair.router.Router.ChannelHop
import fr.acinq.eclair.transactions.{DirectedHtlc, IncomingHtlc, OutgoingHtlc}
import fr.acinq.eclair.wire.internal.channel.ChannelCodecsSpec
import fr.acinq.eclair.wire.protocol.Onion.FinalLegacyPayload
import fr.acinq.eclair.wire.protocol._
import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, CustomCommitmentsPlugin, MilliSatoshi, MilliSatoshiLong, NodeParams, TestConstants, TestKitBaseClass, randomBytes32}
import org.scalatest.funsuite.FixtureAnyFunSuiteLike
@ -674,7 +673,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 (cmd, _) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, hops, FinalLegacyPayload(finalAmount, finalExpiry))
val (cmd, _) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, hops, Onion.createSinglePartPayload(finalAmount, finalExpiry, randomBytes32()))
UpdateAddHtlc(channelId, htlcId, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion)
}
@ -683,7 +682,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 (cmd, _) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, ChannelHop(a, TestConstants.Bob.nodeParams.nodeId, channelUpdate_ab) :: Nil, FinalLegacyPayload(finalAmount, finalExpiry))
val (cmd, _) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, ChannelHop(a, TestConstants.Bob.nodeParams.nodeId, channelUpdate_ab) :: Nil, Onion.createSinglePartPayload(finalAmount, finalExpiry, randomBytes32()))
IncomingHtlc(UpdateAddHtlc(channelId, htlcId, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion))
}

View file

@ -37,7 +37,7 @@ import fr.acinq.eclair.payment.send.PaymentLifecycle.SendPayment
import fr.acinq.eclair.router.Router.RouteRequest
import fr.acinq.eclair.router.{BalanceTooLow, RouteNotFound}
import fr.acinq.eclair.wire.protocol._
import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, MilliSatoshi, MilliSatoshiLong, NodeParams, ShortChannelId, TestConstants, nodeFee, randomBytes, randomBytes32, randomKey}
import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, MilliSatoshi, MilliSatoshiLong, NodeParams, ShortChannelId, TestConstants, UInt64, nodeFee, randomBytes, randomBytes32, randomKey}
import org.scalatest.Outcome
import org.scalatest.funsuite.FixtureAnyFunSuiteLike
import scodec.bits.HexStringSyntax
@ -55,10 +55,10 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl
import NodeRelayerSpec._
case class FixtureParam(nodeParams: NodeParams, router: TestProbe[Any], register: TestProbe[Any], mockPayFSM: TestProbe[Any], eventListener: TestProbe[PaymentEvent]) {
def createNodeRelay(packetIn: IncomingPacket.NodeRelayPacket, paymentSecret: ByteVector32 = incomingSecret, useRealPaymentFactory: Boolean = false): (ActorRef[NodeRelay.Command], TestProbe[NodeRelayer.Command]) = {
def createNodeRelay(packetIn: IncomingPacket.NodeRelayPacket, useRealPaymentFactory: Boolean = false): (ActorRef[NodeRelay.Command], TestProbe[NodeRelayer.Command]) = {
val parent = TestProbe[NodeRelayer.Command]("parent-relayer")
val outgoingPaymentFactory = if (useRealPaymentFactory) RealOutgoingPaymentFactory(this) else FakeOutgoingPaymentFactory(this)
val nodeRelay = testKit.spawn(NodeRelay(nodeParams, parent.ref, register.ref.toClassic, relayId, packetIn, paymentSecret, outgoingPaymentFactory))
val nodeRelay = testKit.spawn(NodeRelay(nodeParams, parent.ref, register.ref.toClassic, relayId, packetIn, outgoingPaymentFactory))
(nodeRelay, parent)
}
}
@ -95,20 +95,14 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl
parentRelayer ! NodeRelayer.GetPendingPayments(probe.ref.toClassic)
probe.expectMessage(Map.empty)
val paymentNoSecret = createPartialIncomingPacket(randomBytes32, randomBytes32).copy(outerPayload = Onion.createSinglePartPayload(incomingAmount, CltvExpiry(500000)))
parentRelayer ! NodeRelayer.Relay(paymentNoSecret)
val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]]
assert(fwd.channelId === paymentNoSecret.add.channelId)
assert(fwd.message === CMD_FAIL_HTLC(paymentNoSecret.add.id, Right(IncorrectOrUnknownPaymentDetails(paymentNoSecret.add.amountMsat, nodeParams.currentBlockHeight)), commit = true))
val (paymentHash1, paymentSecret1) = (randomBytes32, randomBytes32)
val (paymentHash1, paymentSecret1) = (randomBytes32(), randomBytes32())
val payment1 = createPartialIncomingPacket(paymentHash1, paymentSecret1)
parentRelayer ! NodeRelayer.Relay(payment1)
parentRelayer ! NodeRelayer.GetPendingPayments(probe.ref.toClassic)
val pending1 = probe.expectMessageType[Map[PaymentKey, ActorRef[NodeRelay.Command]]]
assert(pending1.keySet === Set(PaymentKey(paymentHash1, paymentSecret1)))
val (paymentHash2, paymentSecret2) = (randomBytes32, randomBytes32)
val (paymentHash2, paymentSecret2) = (randomBytes32(), randomBytes32())
val payment2 = createPartialIncomingPacket(paymentHash2, paymentSecret2)
parentRelayer ! NodeRelayer.Relay(payment2)
parentRelayer ! NodeRelayer.GetPendingPayments(probe.ref.toClassic)
@ -156,9 +150,9 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl
probe.expectMessage(Map(PaymentKey(paymentHash2, paymentSecret2) -> child2.ref))
}
{
val paymentHash = randomBytes32
val (paymentSecret1, child1) = (randomBytes32, TestProbe[NodeRelay.Command])
val (paymentSecret2, child2) = (randomBytes32, TestProbe[NodeRelay.Command])
val paymentHash = randomBytes32()
val (paymentSecret1, child1) = (randomBytes32(), TestProbe[NodeRelay.Command])
val (paymentSecret2, child2) = (randomBytes32(), TestProbe[NodeRelay.Command])
val children = Map(PaymentKey(paymentHash, paymentSecret1) -> child1.ref, PaymentKey(paymentHash, paymentSecret2) -> child2.ref)
val parentRelayer = testKit.spawn(NodeRelayer(nodeParams, register.ref.toClassic, outgoingPaymentFactory, children))
parentRelayer ! NodeRelayer.GetPendingPayments(probe.ref.toClassic)
@ -259,7 +253,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl
// Receive new HTLC with different details, but for the same payment hash.
val i2 = IncomingPacket.NodeRelayPacket(
UpdateAddHtlc(randomBytes32(), Random.nextInt(100), 1500 msat, paymentHash, CltvExpiry(499990), TestConstants.emptyOnionPacket),
Onion.createSinglePartPayload(1500 msat, CltvExpiry(499990), Some(incomingSecret)),
Onion.createSinglePartPayload(1500 msat, CltvExpiry(499990), incomingSecret),
Onion.createNodeRelayPayload(1250 msat, outgoingExpiry, outgoingNodeId),
nextTrampolinePacket)
nodeRelayer ! NodeRelay.Relay(i2)
@ -272,42 +266,6 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl
register.expectNoMessage(100 millis)
}
test("fail to relay an incoming payment without payment secret") { f =>
import f._
val p = createValidIncomingPacket(2000000 msat, 2000000 msat, CltvExpiry(500000), outgoingAmount, outgoingExpiry).copy(
outerPayload = Onion.createSinglePartPayload(2000000 msat, CltvExpiry(500000)) // missing outer payment secret
)
val (nodeRelayer, _) = f.createNodeRelay(p)
nodeRelayer ! NodeRelay.Relay(p)
val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]]
assert(fwd.channelId === p.add.channelId)
val failure = IncorrectOrUnknownPaymentDetails(2000000 msat, nodeParams.currentBlockHeight)
assert(fwd.message === CMD_FAIL_HTLC(p.add.id, Right(failure), commit = true))
register.expectNoMessage(100 millis)
}
test("fail to relay when incoming payment secrets don't match") { f =>
import f._
val p1 = createValidIncomingPacket(2000000 msat, 3000000 msat, CltvExpiry(500000), 2500000 msat, outgoingExpiry)
val p2 = createValidIncomingPacket(1000000 msat, 3000000 msat, CltvExpiry(500000), 2500000 msat, outgoingExpiry).copy(
outerPayload = Onion.createMultiPartPayload(1000000 msat, 3000000 msat, CltvExpiry(500000), randomBytes32())
)
val (nodeRelayer, _) = f.createNodeRelay(p1)
nodeRelayer ! NodeRelay.Relay(p1)
nodeRelayer ! NodeRelay.Relay(p2)
val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]]
assert(fwd.channelId === p2.add.channelId)
val failure = IncorrectOrUnknownPaymentDetails(1000000 msat, nodeParams.currentBlockHeight)
assert(fwd.message === CMD_FAIL_HTLC(p2.add.id, Right(failure), commit = true))
register.expectNoMessage(100 millis)
}
test("fail to relay when expiry is too soon (single-part)") { f =>
import f._
@ -576,7 +534,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl
// Receive an upstream multi-part payment.
val hints = List(List(ExtraHop(outgoingNodeId, ShortChannelId(42), feeBase = 10 msat, feeProportionalMillionths = 1, cltvExpiryDelta = CltvExpiryDelta(12))))
val features = PaymentRequestFeatures(VariableLengthOnion.mandatory, PaymentSecret.mandatory, BasicMultiPartPayment.optional)
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(outgoingAmount * 3), paymentHash, randomKey(), "Some invoice", CltvExpiryDelta(18), extraHops = hints, features = Some(features))
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(outgoingAmount * 3), paymentHash, randomKey(), "Some invoice", CltvExpiryDelta(18), extraHops = hints, features = features)
val incomingPayments = incomingMultiPart.map(incoming => incoming.copy(innerPayload = Onion.createNodeRelayToNonTrampolinePayload(
incoming.innerPayload.amountToForward, outgoingAmount * 3, outgoingExpiry, outgoingNodeId, pr
)))
@ -617,7 +575,8 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl
// Receive an upstream multi-part payment.
val hints = List(List(ExtraHop(outgoingNodeId, ShortChannelId(42), feeBase = 10 msat, feeProportionalMillionths = 1, cltvExpiryDelta = CltvExpiryDelta(12))))
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(outgoingAmount), paymentHash, randomKey(), "Some invoice", CltvExpiryDelta(18), extraHops = hints, features = Some(PaymentRequestFeatures()))
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(outgoingAmount), paymentHash, randomKey(), "Some invoice", CltvExpiryDelta(18), extraHops = hints)
assert(!pr.features.allowMultiPart)
val incomingPayments = incomingMultiPart.map(incoming => incoming.copy(innerPayload = Onion.createNodeRelayToNonTrampolinePayload(
incoming.innerPayload.amountToForward, incoming.innerPayload.amountToForward, outgoingExpiry, outgoingNodeId, pr
)))
@ -651,6 +610,26 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl
register.expectNoMessage(100 millis)
}
test("fail to relay to non-trampoline recipient missing payment secret") { f =>
import f._
// Receive an upstream multi-part payment.
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(outgoingAmount), paymentHash, randomKey(), "Some invoice", CltvExpiryDelta(18))
val incomingPayments = incomingMultiPart.map(incoming => {
val innerPayload = Onion.createNodeRelayToNonTrampolinePayload(incoming.innerPayload.amountToForward, incoming.innerPayload.amountToForward, outgoingExpiry, outgoingNodeId, pr)
val invalidPayload = innerPayload.copy(records = TlvStream(innerPayload.records.records.collect { case r if !r.isInstanceOf[OnionTlv.PaymentData] => r })) // we remove the payment secret
incoming.copy(innerPayload = invalidPayload)
})
val (nodeRelayer, _) = f.createNodeRelay(incomingPayments.head)
incomingPayments.foreach(incoming => nodeRelayer ! NodeRelay.Relay(incoming))
incomingMultiPart.foreach { p =>
val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]]
assert(fwd.channelId === p.add.channelId)
assert(fwd.message === CMD_FAIL_HTLC(p.add.id, Right(InvalidOnionPayload(UInt64(8), 0)), commit = true))
}
}
def validateOutgoingCfg(outgoingCfg: SendPaymentConfig, upstream: Upstream): Unit = {
assert(!outgoingCfg.publishEvent)
assert(!outgoingCfg.storeInDb)
@ -709,7 +688,7 @@ object NodeRelayerSpec {
def createValidIncomingPacket(amountIn: MilliSatoshi, totalAmountIn: MilliSatoshi, expiryIn: CltvExpiry, amountOut: MilliSatoshi, expiryOut: CltvExpiry): IncomingPacket.NodeRelayPacket = {
val outerPayload = if (amountIn == totalAmountIn) {
Onion.createSinglePartPayload(amountIn, expiryIn, Some(incomingSecret))
Onion.createSinglePartPayload(amountIn, expiryIn, incomingSecret)
} else {
Onion.createMultiPartPayload(amountIn, totalAmountIn, expiryIn, incomingSecret)
}
@ -724,7 +703,7 @@ object NodeRelayerSpec {
val (expiryIn, expiryOut) = (CltvExpiry(500000), CltvExpiry(490000))
val amountIn = incomingAmount / 2
IncomingPacket.NodeRelayPacket(
UpdateAddHtlc(randomBytes32, Random.nextInt(100), amountIn, paymentHash, expiryIn, TestConstants.emptyOnionPacket),
UpdateAddHtlc(randomBytes32(), Random.nextInt(100), amountIn, paymentHash, expiryIn, TestConstants.emptyOnionPacket),
Onion.createMultiPartPayload(amountIn, incomingAmount, expiryIn, paymentSecret),
Onion.createNodeRelayPayload(outgoingAmount, expiryOut, outgoingNodeId),
nextTrampolinePacket)

View file

@ -31,7 +31,6 @@ import fr.acinq.eclair.payment.PaymentPacketSpec._
import fr.acinq.eclair.payment.relay.Relayer._
import fr.acinq.eclair.payment.{OutgoingPacket, PaymentPacketSpec}
import fr.acinq.eclair.router.Router.{ChannelHop, NodeHop}
import fr.acinq.eclair.wire.protocol.Onion.FinalLegacyPayload
import fr.acinq.eclair.wire.protocol._
import fr.acinq.eclair.{NodeParams, TestConstants, randomBytes32, _}
import org.scalatest.concurrent.PatienceConfiguration
@ -85,7 +84,7 @@ class RelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat
}
// we use this to build a valid onion
val (cmd, _) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, hops, FinalLegacyPayload(finalAmount, finalExpiry))
val (cmd, _) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, hops, Onion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret))
// and then manually build an htlc
val add_ab = UpdateAddHtlc(channelId = randomBytes32(), id = 123456, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion)
relayer ! RelayForward(add_ab)
@ -95,14 +94,14 @@ class RelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat
test("relay an htlc-add at the final node to the payment handler") { f =>
import f._
val (cmd, _) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, hops.take(1), FinalLegacyPayload(finalAmount, finalExpiry))
val (cmd, _) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, hops.take(1), Onion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret))
val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion)
relayer ! RelayForward(add_ab)
val fp = paymentHandler.expectMessageType[FinalPacket]
assert(fp.add === add_ab)
assert(fp.payload === FinalLegacyPayload(finalAmount, finalExpiry))
assert(fp.payload === Onion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret))
register.expectNoMessage(50 millis)
}
@ -130,7 +129,7 @@ class RelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat
assert(fp.payload.amount === finalAmount)
assert(fp.payload.totalAmount === totalAmount)
assert(fp.payload.expiry === finalExpiry)
assert(fp.payload.paymentSecret === Some(paymentSecret))
assert(fp.payload.paymentSecret === paymentSecret)
register.expectNoMessage(50 millis)
}
@ -139,7 +138,7 @@ class RelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat
import f._
// we use this to build a valid onion
val (cmd, _) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, hops, FinalLegacyPayload(finalAmount, finalExpiry))
val (cmd, _) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, hops, Onion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret))
// 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))
@ -160,7 +159,7 @@ 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 (trampolineAmount, trampolineExpiry, trampolineOnion) = OutgoingPacket.buildPacket(Sphinx.TrampolinePacket)(paymentHash, trampolineHops, Onion.createSinglePartPayload(finalAmount, finalExpiry))
val (trampolineAmount, trampolineExpiry, trampolineOnion) = OutgoingPacket.buildPacket(Sphinx.TrampolinePacket)(paymentHash, trampolineHops, Onion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret))
val (cmd, _) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, ChannelHop(a, b, channelUpdate_ab) :: Nil, Onion.createTrampolinePayload(trampolineAmount, trampolineAmount, trampolineExpiry, randomBytes32(), trampolineOnion.packet))
// and then manually build an htlc

View file

@ -61,24 +61,6 @@ class OnionCodecsSpec extends AnyFunSuite {
}
}
test("encode/decode fixed-size (legacy) final per-hop payload") {
val testCases = Map(
FinalLegacyPayload(0 msat, CltvExpiry(0)) -> hex"00 0000000000000000 0000000000000000 00000000 000000000000000000000000",
FinalLegacyPayload(142000 msat, CltvExpiry(500000)) -> hex"00 0000000000000000 0000000000022ab0 0007a120 000000000000000000000000",
FinalLegacyPayload(1105 msat, CltvExpiry(1729)) -> hex"00 0000000000000000 0000000000000451 000006c1 000000000000000000000000"
)
for ((expected, bin) <- testCases) {
val decoded = finalPerHopPayloadCodec.decode(bin.bits).require.value
assert(decoded === expected)
assert(decoded.paymentSecret === None)
assert(decoded.totalAmount === decoded.amount)
val encoded = finalPerHopPayloadCodec.encode(expected).require.bytes
assert(encoded === bin)
}
}
test("decode payload length") {
val testCases = Seq(
(1, hex"00"),
@ -160,15 +142,14 @@ class OnionCodecsSpec extends AnyFunSuite {
test("encode/decode variable-length (tlv) final per-hop payload") {
val testCases = Map(
TlvStream[OnionTlv](AmountToForward(561 msat), OutgoingCltv(CltvExpiry(42))) -> hex"07 02020231 04012a",
TlvStream[OnionTlv](AmountToForward(561 msat), OutgoingCltv(CltvExpiry(42)), PaymentData(ByteVector32(hex"eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619"), 0 msat)) -> hex"29 02020231 04012a 0820eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619",
TlvStream[OnionTlv](AmountToForward(561 msat), OutgoingCltv(CltvExpiry(42)), PaymentData(ByteVector32(hex"eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619"), 1105 msat)) -> hex"2b 02020231 04012a 0822eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f2836866190451",
TlvStream[OnionTlv](AmountToForward(561 msat), OutgoingCltv(CltvExpiry(42)), PaymentData(ByteVector32(hex"eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619"), 4294967295L msat)) -> hex"2d 02020231 04012a 0824eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619ffffffff",
TlvStream[OnionTlv](AmountToForward(561 msat), OutgoingCltv(CltvExpiry(42)), PaymentData(ByteVector32(hex"eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619"), 4294967296L msat)) -> hex"2e 02020231 04012a 0825eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f2836866190100000000",
TlvStream[OnionTlv](AmountToForward(561 msat), OutgoingCltv(CltvExpiry(42)), PaymentData(ByteVector32(hex"eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619"), 1099511627775L msat)) -> hex"2e 02020231 04012a 0825eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619ffffffffff",
TlvStream[OnionTlv](AmountToForward(561 msat), OutgoingCltv(CltvExpiry(42)), OutgoingChannelId(ShortChannelId(1105))) -> hex"11 02020231 04012a 06080000000000000451",
TlvStream[OnionTlv](Seq(AmountToForward(561 msat), OutgoingCltv(CltvExpiry(42))), Seq(GenericTlv(65535, hex"06c1"))) -> hex"0d 02020231 04012a fdffff0206c1",
TlvStream[OnionTlv](AmountToForward(561 msat), OutgoingCltv(CltvExpiry(42)), TrampolineOnion(OnionRoutingPacket(0, hex"02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619", hex"cff34152f3a36e52ca94e74927203a560392b9cc7ce3c45809c6be52166c24a595716880f95f178bf5b30ca63141f74db6e92795c6130877cfdac3d4bd3087ee73c65d627ddd709112a848cc99e303f3706509aa43ba7c8a88cba175fccf9a8f5016ef06d3b935dbb15196d7ce16dc1a7157845566901d7b2197e52cab4ce487014b14816e5805f9fcacb4f8f88b8ff176f1b94f6ce6b00bc43221130c17d20ef629db7c5f7eafaa166578c720619561dd14b3277db557ec7dcdb793771aef0f2f667cfdbeae3ac8d331c5994779dffb31e5fc0dbdedc0c592ca6d21c18e47fe3528d6975c19517d7e2ea8c5391cf17d0fe30c80913ed887234ccb48808f7ef9425bcd815c3b586210979e3bb286ef2851bf9ce04e28c40a203df98fd648d2f1936fd2f1def0e77eecb277229b4b682322371c0a1dbfcd723a991993df8cc1f2696b84b055b40a1792a29f710295a18fbd351b0f3ff34cd13941131b8278ba79303c89117120eea691738a9954908195143b039dbeed98f26a92585f3d15cf742c953799d3272e0545e9b744be9d3b4c", ByteVector32(hex"bb079bfc4b35190eee9f59a1d7b41ba2f773179f322dafb4b1af900c289ebd6c")))) -> hex"fd01e1 02020231 04012a fe00010234fd01d20002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619cff34152f3a36e52ca94e74927203a560392b9cc7ce3c45809c6be52166c24a595716880f95f178bf5b30ca63141f74db6e92795c6130877cfdac3d4bd3087ee73c65d627ddd709112a848cc99e303f3706509aa43ba7c8a88cba175fccf9a8f5016ef06d3b935dbb15196d7ce16dc1a7157845566901d7b2197e52cab4ce487014b14816e5805f9fcacb4f8f88b8ff176f1b94f6ce6b00bc43221130c17d20ef629db7c5f7eafaa166578c720619561dd14b3277db557ec7dcdb793771aef0f2f667cfdbeae3ac8d331c5994779dffb31e5fc0dbdedc0c592ca6d21c18e47fe3528d6975c19517d7e2ea8c5391cf17d0fe30c80913ed887234ccb48808f7ef9425bcd815c3b586210979e3bb286ef2851bf9ce04e28c40a203df98fd648d2f1936fd2f1def0e77eecb277229b4b682322371c0a1dbfcd723a991993df8cc1f2696b84b055b40a1792a29f710295a18fbd351b0f3ff34cd13941131b8278ba79303c89117120eea691738a9954908195143b039dbeed98f26a92585f3d15cf742c953799d3272e0545e9b744be9d3b4cbb079bfc4b35190eee9f59a1d7b41ba2f773179f322dafb4b1af900c289ebd6c"
TlvStream[OnionTlv](AmountToForward(561 msat), OutgoingCltv(CltvExpiry(42)), OutgoingChannelId(ShortChannelId(1105)), PaymentData(ByteVector32(hex"eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619"), 0 msat)) -> hex"33 02020231 04012a 06080000000000000451 0820eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619",
TlvStream[OnionTlv](Seq(AmountToForward(561 msat), OutgoingCltv(CltvExpiry(42)), PaymentData(ByteVector32(hex"eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619"), 0 msat)), Seq(GenericTlv(65535, hex"06c1"))) -> hex"2f 02020231 04012a 0820eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619 fdffff0206c1",
TlvStream[OnionTlv](AmountToForward(561 msat), OutgoingCltv(CltvExpiry(42)), PaymentData(ByteVector32(hex"eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619"), 0 msat), TrampolineOnion(OnionRoutingPacket(0, hex"02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619", hex"cff34152f3a36e52ca94e74927203a560392b9cc7ce3c45809c6be52166c24a595716880f95f178bf5b30ca63141f74db6e92795c6130877cfdac3d4bd3087ee73c65d627ddd709112a848cc99e303f3706509aa43ba7c8a88cba175fccf9a8f5016ef06d3b935dbb15196d7ce16dc1a7157845566901d7b2197e52cab4ce487014b14816e5805f9fcacb4f8f88b8ff176f1b94f6ce6b00bc43221130c17d20ef629db7c5f7eafaa166578c720619561dd14b3277db557ec7dcdb793771aef0f2f667cfdbeae3ac8d331c5994779dffb31e5fc0dbdedc0c592ca6d21c18e47fe3528d6975c19517d7e2ea8c5391cf17d0fe30c80913ed887234ccb48808f7ef9425bcd815c3b586210979e3bb286ef2851bf9ce04e28c40a203df98fd648d2f1936fd2f1def0e77eecb277229b4b682322371c0a1dbfcd723a991993df8cc1f2696b84b055b40a1792a29f710295a18fbd351b0f3ff34cd13941131b8278ba79303c89117120eea691738a9954908195143b039dbeed98f26a92585f3d15cf742c953799d3272e0545e9b744be9d3b4c", ByteVector32(hex"bb079bfc4b35190eee9f59a1d7b41ba2f773179f322dafb4b1af900c289ebd6c")))) -> hex"fd0203 02020231 04012a 0820eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619 fe00010234fd01d20002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619cff34152f3a36e52ca94e74927203a560392b9cc7ce3c45809c6be52166c24a595716880f95f178bf5b30ca63141f74db6e92795c6130877cfdac3d4bd3087ee73c65d627ddd709112a848cc99e303f3706509aa43ba7c8a88cba175fccf9a8f5016ef06d3b935dbb15196d7ce16dc1a7157845566901d7b2197e52cab4ce487014b14816e5805f9fcacb4f8f88b8ff176f1b94f6ce6b00bc43221130c17d20ef629db7c5f7eafaa166578c720619561dd14b3277db557ec7dcdb793771aef0f2f667cfdbeae3ac8d331c5994779dffb31e5fc0dbdedc0c592ca6d21c18e47fe3528d6975c19517d7e2ea8c5391cf17d0fe30c80913ed887234ccb48808f7ef9425bcd815c3b586210979e3bb286ef2851bf9ce04e28c40a203df98fd648d2f1936fd2f1def0e77eecb277229b4b682322371c0a1dbfcd723a991993df8cc1f2696b84b055b40a1792a29f710295a18fbd351b0f3ff34cd13941131b8278ba79303c89117120eea691738a9954908195143b039dbeed98f26a92585f3d15cf742c953799d3272e0545e9b744be9d3b4cbb079bfc4b35190eee9f59a1d7b41ba2f773179f322dafb4b1af900c289ebd6c"
)
for ((expected, bin) <- testCases) {
@ -183,8 +164,8 @@ class OnionCodecsSpec extends AnyFunSuite {
}
test("encode/decode variable-length (tlv) final per-hop payload with custom user records") {
val tlvs = TlvStream[OnionTlv](Seq(AmountToForward(561 msat), OutgoingCltv(CltvExpiry(42))), Seq(GenericTlv(5432123456L, hex"16c7ec71663784ff100b6eface1e60a97b92ea9d18b8ece5e558586bc7453828")))
val bin = hex"31 02020231 04012a ff0000000143c7a0402016c7ec71663784ff100b6eface1e60a97b92ea9d18b8ece5e558586bc7453828"
val tlvs = TlvStream[OnionTlv](Seq(AmountToForward(561 msat), OutgoingCltv(CltvExpiry(42)), PaymentData(ByteVector32(hex"eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619"), 0 msat)), Seq(GenericTlv(5432123456L, hex"16c7ec71663784ff100b6eface1e60a97b92ea9d18b8ece5e558586bc7453828")))
val bin = hex"53 02020231 04012a 0820eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619 ff0000000143c7a0402016c7ec71663784ff100b6eface1e60a97b92ea9d18b8ece5e558586bc7453828"
val encoded = finalPerHopPayloadCodec.encode(FinalTlvPayload(tlvs)).require.bytes
assert(encoded === bin)
@ -192,21 +173,17 @@ class OnionCodecsSpec extends AnyFunSuite {
}
test("decode multi-part final per-hop payload") {
val notMultiPart = finalPerHopPayloadCodec.decode(hex"07 02020231 04012a".bits).require.value
assert(notMultiPart.totalAmount === 561.msat)
assert(notMultiPart.paymentSecret === None)
val multiPart = finalPerHopPayloadCodec.decode(hex"2b 02020231 04012a 0822eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f2836866190451".bits).require.value
assert(multiPart.amount === 561.msat)
assert(multiPart.expiry === CltvExpiry(42))
assert(multiPart.totalAmount === 1105.msat)
assert(multiPart.paymentSecret === Some(ByteVector32(hex"eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619")))
assert(multiPart.paymentSecret === ByteVector32(hex"eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619"))
val multiPartNoTotalAmount = finalPerHopPayloadCodec.decode(hex"29 02020231 04012a 0820eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619".bits).require.value
assert(multiPartNoTotalAmount.amount === 561.msat)
assert(multiPartNoTotalAmount.expiry === CltvExpiry(42))
assert(multiPartNoTotalAmount.totalAmount === 561.msat)
assert(multiPartNoTotalAmount.paymentSecret === Some(ByteVector32(hex"eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619")))
assert(multiPartNoTotalAmount.paymentSecret === ByteVector32(hex"eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619"))
}
test("decode variable-length (tlv) relay per-hop payload missing information") {
@ -241,8 +218,9 @@ class OnionCodecsSpec extends AnyFunSuite {
test("decode variable-length (tlv) final per-hop payload missing information") {
val testCases = Seq(
(InvalidOnionPayload(UInt64(2), 0), hex"03 04012a"), // missing amount
(InvalidOnionPayload(UInt64(4), 0), hex"04 02020231") // missing cltv
(InvalidOnionPayload(UInt64(2), 0), hex"25 04012a 0820eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619"), // missing amount
(InvalidOnionPayload(UInt64(4), 0), hex"26 02020231 0820eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619"), // missing cltv
(InvalidOnionPayload(UInt64(8), 0), hex"07 02020231 04012a"), // missing payment secret
)
for ((expectedErr, bin) <- testCases) {

View file

@ -25,7 +25,7 @@ import fr.acinq.eclair.gui.controllers._
import fr.acinq.eclair.io.{NodeURI, Peer}
import fr.acinq.eclair.payment._
import fr.acinq.eclair.payment.receive.MultiPartHandler.ReceivePayment
import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentRequest
import fr.acinq.eclair.payment.send.PaymentInitiator.SendPayment
import fr.acinq.eclair.{MilliSatoshi, _}
import grizzled.slf4j.Logging
@ -86,8 +86,8 @@ class Handlers(fKit: Future[Kit])(implicit ec: ExecutionContext = ExecutionConte
(for {
kit <- fKit
sendPayment = req.minFinalCltvExpiryDelta match {
case None => SendPaymentRequest(MilliSatoshi(amountMsat), req.paymentHash, req.nodeId, kit.nodeParams.maxPaymentAttempts, assistedRoutes = req.routingInfo)
case Some(minFinalCltvExpiry) => SendPaymentRequest(MilliSatoshi(amountMsat), req.paymentHash, req.nodeId, kit.nodeParams.maxPaymentAttempts, assistedRoutes = req.routingInfo, fallbackFinalExpiryDelta = minFinalCltvExpiry)
case None => SendPayment(MilliSatoshi(amountMsat), req, kit.nodeParams.maxPaymentAttempts, assistedRoutes = req.routingInfo)
case Some(minFinalCltvExpiry) => SendPayment(MilliSatoshi(amountMsat), req, kit.nodeParams.maxPaymentAttempts, assistedRoutes = req.routingInfo, fallbackFinalExpiryDelta = minFinalCltvExpiry)
}
res <- (kit.paymentInitiator ? sendPayment).mapTo[UUID]
} yield res).recover {

View file

@ -24,7 +24,7 @@ import fr.acinq.eclair.api.directives.EclairDirectives
import fr.acinq.eclair.api.serde.FormParamExtractors.{pubkeyListUnmarshaller, _}
import fr.acinq.eclair.payment.PaymentRequest
import fr.acinq.eclair.router.Router.{PredefinedChannelRoute, PredefinedNodeRoute}
import fr.acinq.eclair.{CltvExpiryDelta, MilliSatoshi}
import fr.acinq.eclair.{CltvExpiryDelta, MilliSatoshi, randomBytes32}
import java.util.UUID
@ -41,47 +41,18 @@ trait Payment {
formFields(invoiceFormParam, amountMsatFormParam.?, "maxAttempts".as[Int].?, "feeThresholdSat".as[Satoshi].?, "maxFeePct".as[Double].?, "externalId".?, "blocking".as[Boolean].?) {
case (invoice@PaymentRequest(_, Some(amount), _, nodeId, _, _), None, maxAttempts, feeThresholdSat_opt, maxFeePct_opt, externalId_opt, blocking_opt) =>
blocking_opt match {
case Some(true) => complete(eclairApi.sendBlocking(externalId_opt, nodeId, amount, invoice.paymentHash, Some(invoice), maxAttempts, feeThresholdSat_opt, maxFeePct_opt))
case _ => complete(eclairApi.send(externalId_opt, nodeId, amount, invoice.paymentHash, Some(invoice), maxAttempts, feeThresholdSat_opt, maxFeePct_opt))
case Some(true) => complete(eclairApi.sendBlocking(externalId_opt, amount, invoice, maxAttempts, feeThresholdSat_opt, maxFeePct_opt))
case _ => complete(eclairApi.send(externalId_opt, amount, invoice, maxAttempts, feeThresholdSat_opt, maxFeePct_opt))
}
case (invoice, Some(overrideAmount), maxAttempts, feeThresholdSat_opt, maxFeePct_opt, externalId_opt, blocking_opt) =>
blocking_opt match {
case Some(true) => complete(eclairApi.sendBlocking(externalId_opt, invoice.nodeId, overrideAmount, invoice.paymentHash, Some(invoice), maxAttempts, feeThresholdSat_opt, maxFeePct_opt))
case _ => complete(eclairApi.send(externalId_opt, invoice.nodeId, overrideAmount, invoice.paymentHash, Some(invoice), maxAttempts, feeThresholdSat_opt, maxFeePct_opt))
case Some(true) => complete(eclairApi.sendBlocking(externalId_opt, overrideAmount, invoice, maxAttempts, feeThresholdSat_opt, maxFeePct_opt))
case _ => complete(eclairApi.send(externalId_opt, overrideAmount, invoice, maxAttempts, feeThresholdSat_opt, maxFeePct_opt))
}
case _ => reject(MalformedFormFieldRejection("invoice", "The invoice must have an amount or you need to specify one using the field 'amountMsat'"))
}
}
val sendToNode: Route = postRequest("sendtonode") { implicit t =>
formFields(amountMsatFormParam, nodeIdFormParam, paymentHashFormParam.?, "maxAttempts".as[Int].?, "feeThresholdSat".as[Satoshi].?, "maxFeePct".as[Double].?, "externalId".?, "keysend".as[Boolean].?) {
case (amountMsat, nodeId, Some(paymentHash), maxAttempts_opt, feeThresholdSat_opt, maxFeePct_opt, externalId_opt, keySend) =>
keySend match {
case Some(true) => reject(MalformedFormFieldRejection(
"paymentHash", "You cannot request a KeySend payment and specify a paymentHash"
))
case _ => complete(eclairApi.send(
externalId_opt, nodeId, amountMsat, paymentHash,
maxAttempts_opt = maxAttempts_opt,
feeThresholdSat_opt = feeThresholdSat_opt,
maxFeePct_opt = maxFeePct_opt
))
}
case (amountMsat, nodeId, None, maxAttempts_opt, feeThresholdSat_opt, maxFeePct_opt, externalId_opt, keySend) =>
keySend match {
case Some(true) => complete(eclairApi.sendWithPreimage(
externalId_opt, nodeId, amountMsat,
maxAttempts_opt = maxAttempts_opt,
feeThresholdSat_opt = feeThresholdSat_opt,
maxFeePct_opt = maxFeePct_opt)
)
case _ => reject(MalformedFormFieldRejection(
"paymentHash", "No payment type specified. Either provide a paymentHash or use --keysend=true"
))
}
}
}
val sendToRoute: Route = postRequest("sendtoroute") { implicit t =>
withRoute { hops =>
formFields(amountMsatFormParam, "recipientAmountMsat".as[MilliSatoshi].?, invoiceFormParam, "finalCltvExpiry".as[Int], "externalId".?, "parentId".as[UUID].?,
@ -100,6 +71,13 @@ trait Payment {
}
}
val sendToNode: Route = postRequest("sendtonode") { implicit t =>
formFields(amountMsatFormParam, nodeIdFormParam, "maxAttempts".as[Int].?, "feeThresholdSat".as[Satoshi].?, "maxFeePct".as[Double].?, "externalId".?) {
case (amountMsat, nodeId, maxAttempts_opt, feeThresholdSat_opt, maxFeePct_opt, externalId_opt) =>
complete(eclairApi.sendWithPreimage(externalId_opt, nodeId, amountMsat, randomBytes32(), maxAttempts_opt, feeThresholdSat_opt, maxFeePct_opt))
}
}
val getSentInfo: Route = postRequest("getsentinfo") { implicit t =>
formFields("id".as[UUID]) { id =>
complete(eclairApi.sentInfo(Left(id)))

View file

@ -1 +1 @@
{"paymentRequest":{"prefix":"lnbc","timestamp":1496314658,"nodeId":"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad","serialized":"lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp","description":"1 cup coffee","paymentHash":"0001020304050607080900010203040506070809000102030405060708090102","expiry":60,"amount":250000000,"features":{"activated":{},"unknown":[]}},"paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","paymentType":"Standard","createdAt":42,"status":{"type":"expired"}}
{"paymentRequest":{"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":42,"status":{"type":"expired"}}

View file

@ -1 +1 @@
{"paymentRequest":{"prefix":"lnbc","timestamp":1496314658,"nodeId":"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad","serialized":"lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp","description":"1 cup coffee","paymentHash":"0001020304050607080900010203040506070809000102030405060708090102","expiry":60,"amount":250000000,"features":{"activated":{},"unknown":[]}},"paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","paymentType":"Standard","createdAt":42,"status":{"type":"pending"}}
{"paymentRequest":{"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":42,"status":{"type":"pending"}}

View file

@ -1 +1 @@
{"paymentRequest":{"prefix":"lnbc","timestamp":1496314658,"nodeId":"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad","serialized":"lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp","description":"1 cup coffee","paymentHash":"0001020304050607080900010203040506070809000102030405060708090102","expiry":60,"amount":250000000,"features":{"activated":{},"unknown":[]}},"paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","paymentType":"Standard","createdAt":42,"status":{"type":"received","amount":42,"receivedAt":45}}
{"paymentRequest":{"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":42,"status":{"type":"received","amount":42,"receivedAt":45}}

View file

@ -16,8 +16,6 @@
package fr.acinq.eclair.api
import java.util.UUID
import akka.actor.{ActorRef, ActorSystem}
import akka.http.scaladsl.model.FormData
import akka.http.scaladsl.model.StatusCodes._
@ -53,6 +51,7 @@ import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers
import scodec.bits._
import java.util.UUID
import scala.concurrent.Future
import scala.concurrent.duration._
import scala.io.Source
@ -370,7 +369,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM
test("'send' method should handle payment failures") {
val eclair = mock[Eclair]
eclair.send(any, any, any, any, any, any, any, any)(any[Timeout]) returns Future.failed(new IllegalArgumentException("invoice has expired"))
eclair.send(any, any, any, any, any, any)(any[Timeout]) returns Future.failed(new IllegalArgumentException("invoice has expired"))
val mockService = new MockService(eclair)
val invoice = "lnbc12580n1pw2ywztpp554ganw404sh4yjkwnysgn3wjcxfcq7gtx53gxczkjr9nlpc3hzvqdq2wpskwctddyxqr4rqrzjqwryaup9lh50kkranzgcdnn2fgvx390wgj5jd07rwr3vxeje0glc7z9rtvqqwngqqqqqqqlgqqqqqeqqjqrrt8smgjvfj7sg38dwtr9kc9gg3era9k3t2hvq3cup0jvsrtrxuplevqgfhd3rzvhulgcxj97yjuj8gdx8mllwj4wzjd8gdjhpz3lpqqvk2plh"
@ -383,7 +382,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM
assert(status == BadRequest)
val resp = entityAs[ErrorResponse](Json4sSupport.unmarshaller, ClassTag(classOf[ErrorResponse]))
assert(resp.error == "invoice has expired")
eclair.send(None, any, 1258000 msat, any, any, any, any, any)(any[Timeout]).wasCalled(once)
eclair.send(None, 1258000 msat, any, any, any, any)(any[Timeout]).wasCalled(once)
}
}
@ -392,7 +391,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM
val eclair = mock[Eclair]
val mockService = new MockService(eclair)
eclair.sendBlocking(any, any, any, any, any, any, any, any)(any[Timeout]).returns(Future.successful(Left(PreimageReceived(ByteVector32.Zeroes, ByteVector32.One))))
eclair.sendBlocking(any, any, any, any, any, any)(any[Timeout]).returns(Future.successful(Left(PreimageReceived(ByteVector32.Zeroes, ByteVector32.One))))
Post("/payinvoice", FormData("invoice" -> invoice, "blocking" -> "true").toEntity) ~>
addCredentials(BasicHttpCredentials("", mockApi().password)) ~>
Route.seal(mockService.payInvoice) ~>
@ -406,7 +405,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM
val uuid = UUID.fromString("487da196-a4dc-4b1e-92b4-3e5e905e9f3f")
val paymentSent = PaymentSent(uuid, ByteVector32.Zeroes, ByteVector32.One, 25 msat, aliceNodeId, Seq(PaymentSent.PartialPayment(uuid, 21 msat, 1 msat, ByteVector32.Zeroes, None, 1553784337711L)))
eclair.sendBlocking(any, any, any, any, any, any, any, any)(any[Timeout]).returns(Future.successful(Right(paymentSent)))
eclair.sendBlocking(any, any, any, any, any, any)(any[Timeout]).returns(Future.successful(Right(paymentSent)))
Post("/payinvoice", FormData("invoice" -> invoice, "blocking" -> "true").toEntity) ~>
addCredentials(BasicHttpCredentials("", mockApi().password)) ~>
Route.seal(mockService.payInvoice) ~>
@ -419,7 +418,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM
}
val paymentFailed = PaymentFailed(uuid, ByteVector32.Zeroes, failures = Seq.empty, timestamp = 1553784963659L)
eclair.sendBlocking(any, any, any, any, any, any, any, any)(any[Timeout]).returns(Future.successful(Right(paymentFailed)))
eclair.sendBlocking(any, any, any, any, any, any)(any[Timeout]).returns(Future.successful(Right(paymentFailed)))
Post("/payinvoice", FormData("invoice" -> invoice, "blocking" -> "true").toEntity) ~>
addCredentials(BasicHttpCredentials("", mockApi().password)) ~>
Route.seal(mockService.payInvoice) ~>
@ -436,7 +435,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM
val invoice = "lnbc12580n1pw2ywztpp554ganw404sh4yjkwnysgn3wjcxfcq7gtx53gxczkjr9nlpc3hzvqdq2wpskwctddyxqr4rqrzjqwryaup9lh50kkranzgcdnn2fgvx390wgj5jd07rwr3vxeje0glc7z9rtvqqwngqqqqqqqlgqqqqqeqqjqrrt8smgjvfj7sg38dwtr9kc9gg3era9k3t2hvq3cup0jvsrtrxuplevqgfhd3rzvhulgcxj97yjuj8gdx8mllwj4wzjd8gdjhpz3lpqqvk2plh"
val eclair = mock[Eclair]
eclair.send(any, any, any, any, any, any, any, any)(any[Timeout]) returns Future.successful(UUID.randomUUID())
eclair.send(any, any, any, any, any, any)(any[Timeout]) returns Future.successful(UUID.randomUUID())
val mockService = new MockService(eclair)
Post("/payinvoice", FormData("invoice" -> invoice).toEntity) ~>
@ -445,7 +444,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM
check {
assert(handled)
assert(status == OK)
eclair.send(None, any, 1258000 msat, any, any, any, any, any)(any[Timeout]).wasCalled(once)
eclair.send(None, 1258000 msat, any, any, any, any)(any[Timeout]).wasCalled(once)
}
Post("/payinvoice", FormData("invoice" -> invoice, "amountMsat" -> "123", "feeThresholdSat" -> "112233", "maxFeePct" -> "2.34", "externalId" -> "42").toEntity) ~>
@ -454,7 +453,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM
check {
assert(handled)
assert(status == OK)
eclair.send(Some("42"), any, 123 msat, any, any, any, Some(112233 sat), Some(2.34))(any[Timeout]).wasCalled(once)
eclair.send(Some("42"), 123 msat, any, any, Some(112233 sat), Some(2.34))(any[Timeout]).wasCalled(once)
}
Post("/payinvoice", FormData("invoice" -> invoice, "amountMsat" -> "456", "feeThresholdSat" -> "10", "maxFeePct" -> "0.5").toEntity) ~>
@ -463,7 +462,48 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM
check {
assert(handled)
assert(status == OK)
eclair.send(None, any, 456 msat, any, any, any, Some(10 sat), Some(0.5))(any[Timeout]).wasCalled(once)
eclair.send(None, 456 msat, any, any, Some(10 sat), Some(0.5))(any[Timeout]).wasCalled(once)
}
}
test("'sendtonode'") {
val eclair = mock[Eclair]
eclair.sendWithPreimage(any, any, any, any, any, any, any)(any[Timeout]) returns Future.successful(UUID.randomUUID())
val mockService = new MockService(eclair)
val remoteNodeId = PublicKey(hex"030bb6a5e0c6b203c7e2180fb78c7ba4bdce46126761d8201b91ddac089cdecc87")
Post("/sendtonode", FormData("amountMsat" -> "123").toEntity) ~>
addCredentials(BasicHttpCredentials("", mockApi().password)) ~>
Route.seal(mockService.sendToNode) ~>
check {
assert(handled)
assert(status == BadRequest)
}
Post("/sendtonode", FormData("nodeId" -> "030bb6a5e0c6b203c7e2180fb78c7ba4bdce46126761d8201b91ddac089cdecc87").toEntity) ~>
addCredentials(BasicHttpCredentials("", mockApi().password)) ~>
Route.seal(mockService.sendToNode) ~>
check {
assert(handled)
assert(status == BadRequest)
}
Post("/sendtonode", FormData("amountMsat" -> "123", "nodeId" -> "030bb6a5e0c6b203c7e2180fb78c7ba4bdce46126761d8201b91ddac089cdecc87").toEntity) ~>
addCredentials(BasicHttpCredentials("", mockApi().password)) ~>
Route.seal(mockService.sendToNode) ~>
check {
assert(handled)
assert(status == OK)
eclair.sendWithPreimage(None, remoteNodeId, 123 msat, any, None, None, None)(any[Timeout]).wasCalled(once)
}
Post("/sendtonode", FormData("amountMsat" -> "123", "nodeId" -> "030bb6a5e0c6b203c7e2180fb78c7ba4bdce46126761d8201b91ddac089cdecc87", "feeThresholdSat" -> "10000", "maxFeePct" -> "2.5", "externalId" -> "42").toEntity) ~>
addCredentials(BasicHttpCredentials("", mockApi().password)) ~>
Route.seal(mockService.sendToNode) ~>
check {
assert(handled)
assert(status == OK)
eclair.sendWithPreimage(Some("42"), remoteNodeId, 123 msat, any, any, Some(10000 sat), Some(2.5))(any[Timeout]).wasCalled(once)
}
}