diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala index 314cfe563..8d8444cb8 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala @@ -20,7 +20,7 @@ import akka.event.LoggingAdapter import fr.acinq.bitcoin.ByteVector32 import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} import fr.acinq.eclair.Features.VariableLengthOnion -import fr.acinq.eclair.channel.{CMD_ADD_HTLC, Upstream} +import fr.acinq.eclair.channel.{CMD_ADD_HTLC, CannotExtractSharedSecret, Upstream} import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.router.Router.{ChannelHop, Hop, NodeHop} import fr.acinq.eclair.wire._ @@ -29,6 +29,8 @@ import scodec.bits.ByteVector import scodec.{Attempt, DecodeResult} import scala.reflect.ClassTag +import scala.util.{Failure, Success, Try} + /** * Created by t-bast on 08/10/2019. @@ -242,4 +244,15 @@ object OutgoingPacket { CMD_ADD_HTLC(firstAmount, paymentHash, firstExpiry, onion.packet, upstream, commit = true) -> onion.sharedSecrets } + def buildHtlcFailure(nodeSecret: PrivateKey, reason: Either[ByteVector, FailureMessage], add: UpdateAddHtlc): Try[ByteVector] = { + Sphinx.PaymentPacket.peel(nodeSecret, add.paymentHash, add.onionRoutingPacket) match { + case Right(Sphinx.DecryptedPacket(_, _, sharedSecret)) => + val encryptedReason = reason match { + case Left(forwarded) => Sphinx.FailurePacket.wrap(forwarded, sharedSecret) + case Right(failure) => Sphinx.FailurePacket.create(sharedSecret, failure) + } + Success(encryptedReason) + case Left(_) => Failure(CannotExtractSharedSecret(add.channelId, add)) + } + } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala index 99a42d547..b1bce4f30 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala @@ -25,7 +25,6 @@ import fr.acinq.eclair.db.{IncomingPayment, IncomingPaymentStatus, IncomingPayme import fr.acinq.eclair.io.PayToOpenRequestEvent import fr.acinq.eclair.payment.Monitoring.{Metrics, Tags} import fr.acinq.eclair.payment.PaymentRequest.ExtraHop -import fr.acinq.eclair.payment.receive.MultiPartPaymentFSM.PayToOpenPart import fr.acinq.eclair.payment.{IncomingPacket, PaymentReceived, PaymentRequest} import fr.acinq.eclair.wire._ import fr.acinq.eclair.{CltvExpiry, Features, Logs, MilliSatoshi, NodeParams, randomBytes32, _} @@ -117,7 +116,7 @@ class MultiPartHandler(nodeParams: NodeParams, register: ActorRef, db: IncomingP case None => p.amountMsat // in all failure cases we assume it is a single part payment } db.getIncomingPayment(p.paymentHash) match { - case Some(record) => validatePayToOpen(p, totalAmount, record) match { + case Some(record) => validatePayToOpen(nodeParams, p, totalAmount, record) match { case Some(payToOpenResponseDenied) => ctx.sender() ! payToOpenResponseDenied case None => @@ -131,7 +130,7 @@ class MultiPartHandler(nodeParams: NodeParams, register: ActorRef, db: IncomingP pendingPayments = pendingPayments + (p.paymentHash -> (record.paymentPreimage, handler)) } } - case None => ctx.sender() ! p.denied + case None => ctx.sender() ! p.denied(nodeParams.privateKey, Some(IncorrectOrUnknownPaymentDetails(p.amountMsat, nodeParams.currentBlockHeight))) } } @@ -144,7 +143,7 @@ class MultiPartHandler(nodeParams: NodeParams, register: ActorRef, db: IncomingP case p: MultiPartPaymentFSM.HtlcPart => PendingRelayDb.safeSend(register, nodeParams.db.pendingRelay, p.htlc.channelId, CMD_FAIL_HTLC(p.htlc.id, Right(failure), commit = true)) } parts.collectFirst { - case p: PayToOpenPart => p.peer ! p.payToOpen.denied + case p: MultiPartPaymentFSM.PayToOpenPart => p.peer ! p.payToOpen.denied(nodeParams.privateKey, Some(failure)) } pendingPayments = pendingPayments - paymentHash } @@ -156,7 +155,7 @@ class MultiPartHandler(nodeParams: NodeParams, register: ActorRef, db: IncomingP case (preimage: ByteVector32, handler: ActorRef) => handler ! PoisonPill parts - .collect { case p: PayToOpenPart => p } + .collect { case p: MultiPartPaymentFSM.PayToOpenPart => p } .toList match { case Nil => // regular mpp payment, we just fulfill the upstream htlcs @@ -167,7 +166,7 @@ class MultiPartHandler(nodeParams: NodeParams, register: ActorRef, db: IncomingP // first we combine all pay-to-open requests into one val summarizedPayToOpenRequest = PayToOpenRequest.combine(payToOpenParts.map(_.payToOpen)) // and we do as if we had received only that pay-to-open request (this is what will be written to db) - val parts1 = parts.collect { case h: MultiPartPaymentFSM.HtlcPart => h } :+ PayToOpenPart(parts.head.totalAmount, summarizedPayToOpenRequest, payToOpenParts.head.peer) + val parts1 = parts.collect { case h: MultiPartPaymentFSM.HtlcPart => h } :+ MultiPartPaymentFSM.PayToOpenPart(parts.head.totalAmount, summarizedPayToOpenRequest, payToOpenParts.head.peer) log.info(s"received pay-to-open payment for amount=${summarizedPayToOpenRequest.amountMsat}") if (summarizedPayToOpenRequest.feeSatoshis == 0.sat) { // we always say ok when fee is zero, without asking the user @@ -203,7 +202,7 @@ class MultiPartHandler(nodeParams: NodeParams, register: ActorRef, db: IncomingP failure match { case Some(failure) => p match { case p: MultiPartPaymentFSM.HtlcPart => PendingRelayDb.safeSend(register, nodeParams.db.pendingRelay, p.htlc.channelId, CMD_FAIL_HTLC(p.htlc.id, Right(failure), commit = true)) - case p: PayToOpenPart => p.peer ! p.payToOpen.denied + case p: MultiPartPaymentFSM.PayToOpenPart => p.peer ! p.payToOpen.denied(nodeParams.privateKey, Some(failure)) } case None => p match { // NB: this case shouldn't happen unless the sender violated the spec, so it's ok that we take a slightly more @@ -214,7 +213,7 @@ class MultiPartHandler(nodeParams: NodeParams, register: ActorRef, db: IncomingP db.receiveIncomingPayment(paymentHash, p.amount, received.timestamp) ctx.system.eventStream.publish(received) }) - case _: PayToOpenPart => // we don't do anything here because we have already previously either accepted or rejected which has settled the pay-to-open + case _: MultiPartPaymentFSM.PayToOpenPart => // we don't do anything here because we have already previously either accepted or rejected which has settled the pay-to-open } } } @@ -224,16 +223,19 @@ class MultiPartHandler(nodeParams: NodeParams, register: ActorRef, db: IncomingP log.info("fulfilling payment for amount={}", parts.map(_.amount).sum) val received = PaymentReceived(paymentHash, parts.map { case p: MultiPartPaymentFSM.HtlcPart => PaymentReceived.PartialPayment(p.amount, p.htlc.channelId) - case p: PayToOpenPart => PaymentReceived.PartialPayment(p.amount - p.payToOpen.feeSatoshis, ByteVector32.Zeroes) + case p: MultiPartPaymentFSM.PayToOpenPart => PaymentReceived.PartialPayment(p.amount - p.payToOpen.feeSatoshis, ByteVector32.Zeroes) }) + // The first thing we do is store the payment. This allows us to reconcile pending HTLCs after a restart. db.receiveIncomingPayment(paymentHash, received.amount, received.timestamp) parts.collect { case p: MultiPartPaymentFSM.HtlcPart => PendingRelayDb.safeSend(register, nodeParams.db.pendingRelay, p.htlc.channelId, CMD_FULFILL_HTLC(p.htlc.id, preimage, commit = true)) } - parts.collectFirst { case p: PayToOpenPart => p.peer ! PayToOpenResponse( + parts.collectFirst { + case p: MultiPartPaymentFSM.PayToOpenPart => p.peer ! PayToOpenResponse( chainHash = p.payToOpen.chainHash, paymentHash = p.paymentHash, - paymentPreimage = preimage) + paymentPreimage = preimage, + failureReason_opt = None) } postFulfill(received) ctx.system.eventStream.publish(received) @@ -336,9 +338,9 @@ object MultiPartHandler { if (paymentAmountOk && paymentCltvOk && paymentStatusOk && paymentFeaturesOk) None else Some(cmdFail) } - private def validatePayToOpen(p: PayToOpenRequest, totalAmount: MilliSatoshi, record: IncomingPayment)(implicit log: LoggingAdapter): Option[PayToOpenResponse] = { + private def validatePayToOpen(nodeParams: NodeParams, p: PayToOpenRequest, totalAmount: MilliSatoshi, record: IncomingPayment)(implicit log: LoggingAdapter): Option[PayToOpenResponse] = { val paymentAmountOk = record.paymentRequest.amount.forall(a => validatePaymentAmount(p.amountMsat, totalAmount, a)) val paymentStatusOk = validatePaymentStatus(p.amountMsat, totalAmount, record) - if (paymentAmountOk && paymentStatusOk) None else Some(p.denied) + if (paymentAmountOk && paymentStatusOk) None else Some(p.denied(nodeParams.privateKey, Some(IncorrectOrUnknownPaymentDetails(totalAmount, nodeParams.currentBlockHeight)))) } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageCodecs.scala index 917d229b9..9f543fe73 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageCodecs.scala @@ -308,7 +308,8 @@ object LightningMessageCodecs { val payToOpenResponseCodec: Codec[PayToOpenResponse] = ( ("chainHash" | bytes32) :: ("paymentHash" | bytes32) :: - ("paymentPreimage" | bytes32)).as[PayToOpenResponse] + ("paymentPreimage" | bytes32) :: + ("failureReason_opt" | optional(bitsRemaining, varsizebinarydata))).as[PayToOpenResponse] // val swapInRequestCodec: Codec[SwapInRequest] = ("channelId" | bytes32).as[SwapInRequest] diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageTypes.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageTypes.scala index acd0fe941..4f8d1bf7a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageTypes.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageTypes.scala @@ -19,12 +19,12 @@ package fr.acinq.eclair.wire import java.net.{Inet4Address, Inet6Address, InetAddress, InetSocketAddress} import java.nio.charset.StandardCharsets -import fr.acinq.eclair._ import com.google.common.base.Charsets import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Satoshi} +import fr.acinq.eclair.payment.OutgoingPacket import fr.acinq.eclair.router.Announcements -import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Features, MilliSatoshi, ShortChannelId, UInt64} +import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Features, MilliSatoshi, ShortChannelId, UInt64, _} import scodec.bits.ByteVector import scala.util.Try @@ -307,7 +307,14 @@ case class PayToOpenRequest(chainHash: ByteVector32, expireAt: Long, htlc_opt: Option[UpdateAddHtlc] ) extends LightningMessage with HasChainHash { - def denied: PayToOpenResponse = PayToOpenResponse(chainHash, paymentHash, ByteVector32.Zeroes) // preimage all-zero means user says no to the pay-to-open request + def denied(nodeSecret: PrivateKey, failure_opt: Option[FailureMessage]): PayToOpenResponse = { + // if we have the necessary information, we include a properly onion-encrypted failure reason + val encryptedFailure_opt = (failure_opt, htlc_opt) match { + case (Some(failure), Some(htlc)) => OutgoingPacket.buildHtlcFailure(nodeSecret, Right(failure), htlc).toOption + case _ => None + } + PayToOpenResponse(chainHash, paymentHash, ByteVector32.Zeroes, encryptedFailure_opt) // preimage all-zero means user says no to the pay-to-open request + } } object PayToOpenRequest { @@ -349,7 +356,8 @@ object PayToOpenRequest { case class PayToOpenResponse(chainHash: ByteVector32, paymentHash: ByteVector32, - paymentPreimage: ByteVector32 + paymentPreimage: ByteVector32, + failureReason_opt: Option[ByteVector] // contains the onion-encrypted failure if applicable ) extends LightningMessage with HasChainHash //