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:
parent
13ff55b68b
commit
20eb41fb01
4 changed files with 43 additions and 19 deletions
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
|
||||
//
|
||||
|
|
Loading…
Add table
Reference in a new issue