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:
parent
e750474c72
commit
bbfbad5975
31 changed files with 556 additions and 618 deletions
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)(_ ++ _)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) =>
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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[_]])
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)))
|
||||
|
|
|
@ -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"}}
|
|
@ -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"}}
|
|
@ -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}}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue