1
0
Fork 0
mirror of https://github.com/ACINQ/eclair.git synced 2025-03-13 19:37:35 +01:00

pay-to-open: return errors for final node

Only implemented for channel relay.

This is backward compatible, any errors returned by existing Phoenix
will be interpreted as generic errors and will return an
`UnknownNextPeer`.
This commit is contained in:
pm47 2020-11-13 14:57:04 +01:00
parent 13ff55b68b
commit 20eb41fb01
No known key found for this signature in database
GPG key ID: E434ED292E85643A
4 changed files with 43 additions and 19 deletions

View file

@ -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))
}
}
}

View file

@ -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))))
}
}

View file

@ -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]

View file

@ -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
//