Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,7 @@ class Setup(val datadir: File,
}
dbEventHandler = system.actorOf(SimpleSupervisor.props(DbEventHandler.props(nodeParams), "db-event-handler", SupervisorStrategy.Resume))
register = system.actorOf(SimpleSupervisor.props(Register.props(), "register", SupervisorStrategy.Resume))
offerManager = system.spawn(Behaviors.supervise(OfferManager(nodeParams, router, paymentTimeout = 1 minute)).onFailure(typed.SupervisorStrategy.resume), name = "offer-manager")
offerManager = system.spawn(Behaviors.supervise(OfferManager(nodeParams, paymentTimeout = 1 minute)).onFailure(typed.SupervisorStrategy.resume), name = "offer-manager")
paymentHandler = system.actorOf(SimpleSupervisor.props(PaymentHandler.props(nodeParams, register, offerManager), "payment-handler", SupervisorStrategy.Resume))
triggerer = system.spawn(Behaviors.supervise(AsyncPaymentTriggerer()).onFailure(typed.SupervisorStrategy.resume), name = "async-payment-triggerer")
peerReadyManager = system.spawn(Behaviors.supervise(PeerReadyManager()).onFailure(typed.SupervisorStrategy.restart), name = "peer-ready-manager")
Expand Down
11 changes: 11 additions & 0 deletions eclair-core/src/main/scala/fr/acinq/eclair/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,17 @@ package object eclair {

def nodeFee(relayFees: RelayFees, paymentAmount: MilliSatoshi): MilliSatoshi = nodeFee(relayFees.feeBase, relayFees.feeProportionalMillionths, paymentAmount)

/**
* @param baseFee fixed fee
* @param proportionalFee proportional fee (millionths)
* @param incomingAmount incoming payment amount
* @return the amount that a node should forward after paying itself the base and proportional fees
*/
def amountAfterFee(baseFee: MilliSatoshi, proportionalFee: Long, incomingAmount: MilliSatoshi): MilliSatoshi =
((incomingAmount - baseFee).toLong * 1_000_000 + 1_000_000 + proportionalFee - 1).msat / (1_000_000 + proportionalFee)

def amountAfterFee(relayFees: RelayFees, incomingAmount: MilliSatoshi): MilliSatoshi = amountAfterFee(relayFees.feeBase, relayFees.feeProportionalMillionths, incomingAmount)

implicit class MilliSatoshiLong(private val n: Long) extends AnyVal {
def msat = MilliSatoshi(n)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,6 @@ object IncomingPaymentPacket {
case payload if payload.paymentConstraints_opt.exists(c => add.amountMsat < c.minAmount) => Left(InvalidOnionBlinding(Sphinx.hash(add.onionRoutingPacket)))
case payload if payload.paymentConstraints_opt.exists(c => c.maxCltvExpiry < add.cltvExpiry) => Left(InvalidOnionBlinding(Sphinx.hash(add.onionRoutingPacket)))
case payload if !Features.areCompatible(Features.empty, payload.allowedFeatures) => Left(InvalidOnionBlinding(Sphinx.hash(add.onionRoutingPacket)))
case payload if add.amountMsat < payload.amount => Left(InvalidOnionBlinding(Sphinx.hash(add.onionRoutingPacket)))
case payload if add.cltvExpiry < payload.expiry => Left(InvalidOnionBlinding(Sphinx.hash(add.onionRoutingPacket)))
case payload => Right(FinalPacket(add, payload))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,21 @@ import akka.actor.typed.scaladsl.{ActorContext, Behaviors}
import akka.actor.typed.{ActorRef, Behavior}
import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey
import fr.acinq.bitcoin.scalacompat.{ByteVector32, Crypto}
import fr.acinq.eclair.EncodedNodeId.ShortChannelIdDir
import fr.acinq.eclair.crypto.Sphinx.RouteBlinding
import fr.acinq.eclair.db.{IncomingBlindedPayment, IncomingPaymentStatus, PaymentType}
import fr.acinq.eclair.message.{OnionMessages, Postman}
import fr.acinq.eclair.payment.MinimalBolt12Invoice
import fr.acinq.eclair.payment.offer.OfferPaymentMetadata.MinimalInvoiceData
import fr.acinq.eclair.payment.receive.MultiPartHandler
import fr.acinq.eclair.payment.receive.MultiPartHandler.{CreateInvoiceActor, ReceivingRoute}
import fr.acinq.eclair.payment.relay.Relayer.RelayFees
import fr.acinq.eclair.payment.{Bolt12Invoice, MinimalBolt12Invoice}
import fr.acinq.eclair.router.BlindedRouteCreation.aggregatePaymentInfo
import fr.acinq.eclair.router.Router
import fr.acinq.eclair.wire.protocol.OfferTypes.{InvoiceRequest, InvoiceTlv, Offer}
import fr.acinq.eclair.wire.protocol.PaymentOnion.FinalPayload
import fr.acinq.eclair.wire.protocol._
import fr.acinq.eclair.{Logs, MilliSatoshi, NodeParams, TimestampMilli, TimestampSecond, randomBytes32}
import fr.acinq.eclair.{CltvExpiryDelta, Logs, MilliSatoshi, MilliSatoshiLong, NodeParams, TimestampMilli, TimestampSecond, nodeFee, randomBytes32}
import scodec.bits.ByteVector

import scala.concurrent.duration.FiniteDuration
Expand Down Expand Up @@ -61,7 +65,7 @@ object OfferManager {

case class RequestInvoice(messagePayload: MessageOnion.InvoiceRequestPayload, blindedKey: PrivateKey, postman: ActorRef[Postman.SendMessage]) extends Command

case class ReceivePayment(replyTo: ActorRef[MultiPartHandler.GetIncomingPaymentActor.Command], paymentHash: ByteVector32, payload: FinalPayload.Blinded) extends Command
case class ReceivePayment(replyTo: ActorRef[MultiPartHandler.GetIncomingPaymentActor.Command], paymentHash: ByteVector32, payload: FinalPayload.Blinded, amountReceived: MilliSatoshi) extends Command

/**
* Offer handlers must be implemented in separate plugins and respond to these two `HandlerCommand`.
Expand Down Expand Up @@ -89,15 +93,15 @@ object OfferManager {

private case class RegisteredOffer(offer: Offer, nodeKey: Option[PrivateKey], pathId_opt: Option[ByteVector32], handler: ActorRef[HandlerCommand])

def apply(nodeParams: NodeParams, router: akka.actor.ActorRef, paymentTimeout: FiniteDuration): Behavior[Command] = {
def apply(nodeParams: NodeParams, paymentTimeout: FiniteDuration): Behavior[Command] = {
Behaviors.setup { context =>
Behaviors.withMdc(Logs.mdc(category_opt = Some(Logs.LogCategory.PAYMENT))) {
new OfferManager(nodeParams, router, paymentTimeout, context).normal(Map.empty)
new OfferManager(nodeParams, paymentTimeout, context).normal(Map.empty)
}
}
}

private class OfferManager(nodeParams: NodeParams, router: akka.actor.ActorRef, paymentTimeout: FiniteDuration, context: ActorContext[Command]) {
private class OfferManager(nodeParams: NodeParams, paymentTimeout: FiniteDuration, context: ActorContext[Command]) {
def normal(registeredOffers: Map[ByteVector32, RegisteredOffer]): Behavior[Command] = {
Behaviors.receiveMessage {
case RegisterOffer(offer, nodeKey, pathId_opt, handler) =>
Expand All @@ -108,19 +112,19 @@ object OfferManager {
registeredOffers.get(messagePayload.invoiceRequest.offer.offerId) match {
case Some(registered) if registered.pathId_opt.map(_.bytes) == messagePayload.pathId_opt && messagePayload.invoiceRequest.isValid =>
context.log.debug("received valid invoice request for offerId={}", messagePayload.invoiceRequest.offer.offerId)
val child = context.spawnAnonymous(InvoiceRequestActor(nodeParams, messagePayload.invoiceRequest, registered.handler, registered.nodeKey.getOrElse(blindedKey), router, messagePayload.replyPath, postman))
val child = context.spawnAnonymous(InvoiceRequestActor(nodeParams, messagePayload.invoiceRequest, registered.handler, registered.nodeKey.getOrElse(blindedKey), messagePayload.replyPath, postman))
child ! InvoiceRequestActor.RequestInvoice
case _ => context.log.debug("offer {} is not registered or invoice request is invalid", messagePayload.invoiceRequest.offer.offerId)
}
Behaviors.same
case ReceivePayment(replyTo, paymentHash, payload) =>
case ReceivePayment(replyTo, paymentHash, payload, amountReceived) =>
MinimalInvoiceData.decode(payload.pathId) match {
case Some(signed) =>
registeredOffers.get(signed.offerId) match {
case Some(RegisteredOffer(offer, _, _, handler)) =>
MinimalInvoiceData.verify(nodeParams.nodeId, signed) match {
case Some(metadata) if Crypto.sha256(metadata.preimage) == paymentHash =>
val child = context.spawnAnonymous(PaymentActor(nodeParams, replyTo, offer, metadata, paymentTimeout))
val child = context.spawnAnonymous(PaymentActor(nodeParams, replyTo, offer, metadata, amountReceived, paymentTimeout))
handler ! HandlePayment(child, signed.offerId, metadata.pluginData_opt)
case Some(_) => replyTo ! MultiPartHandler.GetIncomingPaymentActor.RejectPayment(s"preimage does not match payment hash for offer ${signed.offerId.toHex}")
case None => replyTo ! MultiPartHandler.GetIncomingPaymentActor.RejectPayment(s"invalid signature for metadata for offer ${signed.offerId.toHex}")
Expand Down Expand Up @@ -150,33 +154,53 @@ object OfferManager {
* @param customTlvs custom TLVs to add to the invoice.
*/
case class ApproveRequest(amount: MilliSatoshi,
routes: Seq[ReceivingRoute],
routes: Seq[Route],
pluginData_opt: Option[ByteVector] = None,
additionalTlvs: Set[InvoiceTlv] = Set.empty,
customTlvs: Set[GenericTlv] = Set.empty) extends Command

/**
* @param recipientPaysFees If true, fees for the blinded route will be hidden to the payer and paid by the recipient.
*/
case class Route(hops: Seq[Router.ChannelHop], recipientPaysFees: Boolean, maxFinalExpiryDelta: CltvExpiryDelta, shortChannelIdDir_opt: Option[ShortChannelIdDir] = None) {
def finalize(nodePriv: PrivateKey, preimage: ByteVector32, amount: MilliSatoshi, invoiceRequest: InvoiceRequest, minFinalExpiryDelta: CltvExpiryDelta, pluginData_opt: Option[ByteVector]): ReceivingRoute = {
val (paymentInfo, metadata) = if (recipientPaysFees) {
val realPaymentInfo = aggregatePaymentInfo(amount, hops, minFinalExpiryDelta)
val recipientFees = RelayFees(realPaymentInfo.feeBase, realPaymentInfo.feeProportionalMillionths)
val metadata = MinimalInvoiceData(preimage, invoiceRequest.payerId, TimestampSecond.now(), invoiceRequest.quantity, amount, recipientFees, pluginData_opt)
val paymentInfo = realPaymentInfo.copy(feeBase = 0 msat, feeProportionalMillionths = 0)
(paymentInfo, metadata)
} else {
val paymentInfo = aggregatePaymentInfo(amount, hops, minFinalExpiryDelta)
val metadata = MinimalInvoiceData(preimage, invoiceRequest.payerId, TimestampSecond.now(), invoiceRequest.quantity, amount, RelayFees.zero, pluginData_opt)
(paymentInfo, metadata)
}
val pathId = MinimalInvoiceData.encode(nodePriv, invoiceRequest.offer.offerId, metadata)
ReceivingRoute(hops, pathId, maxFinalExpiryDelta, paymentInfo, shortChannelIdDir_opt)
}
}

/**
* Sent by the offer handler to reject the request. For instance because stock has been exhausted.
*/
case class RejectRequest(message: String) extends Command

private case class WrappedInvoiceResponse(response: CreateInvoiceActor.Bolt12InvoiceResponse) extends Command
private case class WrappedInvoiceResponse(invoice: Bolt12Invoice) extends Command

private case class WrappedOnionMessageResponse(response: Postman.OnionMessageResponse) extends Command

def apply(nodeParams: NodeParams,
invoiceRequest: InvoiceRequest,
offerHandler: ActorRef[HandleInvoiceRequest],
nodeKey: PrivateKey,
router: akka.actor.ActorRef,
pathToSender: RouteBlinding.BlindedRoute,
postman: ActorRef[Postman.SendMessage]): Behavior[Command] = {
Behaviors.setup { context =>
Behaviors.withMdc(Logs.mdc(category_opt = Some(Logs.LogCategory.PAYMENT))) {
Behaviors.receiveMessagePartial {
case RequestInvoice =>
offerHandler ! HandleInvoiceRequest(context.self, invoiceRequest)
new InvoiceRequestActor(nodeParams, invoiceRequest, nodeKey, router, pathToSender, postman, context).waitForHandler()
new InvoiceRequestActor(nodeParams, invoiceRequest, nodeKey, pathToSender, postman, context).waitForHandler()
}
}
}
Expand All @@ -185,7 +209,6 @@ object OfferManager {
private class InvoiceRequestActor(nodeParams: NodeParams,
invoiceRequest: InvoiceRequest,
nodeKey: PrivateKey,
router: akka.actor.ActorRef,
pathToSender: RouteBlinding.BlindedRoute,
postman: ActorRef[Postman.SendMessage],
context: ActorContext[Command]) {
Expand All @@ -197,9 +220,8 @@ object OfferManager {
waitForSent()
case ApproveRequest(amount, routes, pluginData_opt, additionalTlvs, customTlvs) =>
val preimage = randomBytes32()
val metadata = MinimalInvoiceData(preimage, invoiceRequest.payerId, TimestampSecond.now(), invoiceRequest.quantity, amount, pluginData_opt)
val pathId = MinimalInvoiceData.encode(nodeParams.privateKey, invoiceRequest.offer.offerId, metadata)
val receivePayment = MultiPartHandler.ReceiveOfferPayment(context.messageAdapter[CreateInvoiceActor.Bolt12InvoiceResponse](WrappedInvoiceResponse), nodeKey, invoiceRequest, routes, router, preimage, pathId, additionalTlvs, customTlvs)
val receivingRoutes = routes.map(_.finalize(nodeParams.privateKey, preimage, amount, invoiceRequest, nodeParams.channelConf.minFinalExpiryDelta, pluginData_opt))
val receivePayment = MultiPartHandler.ReceiveOfferPayment(context.messageAdapter[Bolt12Invoice](WrappedInvoiceResponse), nodeKey, invoiceRequest, receivingRoutes, preimage, additionalTlvs, customTlvs)
val child = context.spawnAnonymous(CreateInvoiceActor(nodeParams))
child ! CreateInvoiceActor.CreateBolt12Invoice(receivePayment)
waitForInvoice()
Expand All @@ -208,16 +230,10 @@ object OfferManager {

private def waitForInvoice(): Behavior[Command] = {
Behaviors.receiveMessagePartial {
case WrappedInvoiceResponse(invoiceResponse) =>
invoiceResponse match {
case CreateInvoiceActor.InvoiceCreated(invoice) =>
context.log.debug("invoice created for offerId={} invoice={}", invoice.invoiceRequest.offer.offerId, invoice.toString)
postman ! Postman.SendMessage(OfferTypes.BlindedPath(pathToSender), OnionMessages.RoutingStrategy.FindRoute, TlvStream(OnionMessagePayloadTlv.Invoice(invoice.records)), expectsReply = false, context.messageAdapter[Postman.OnionMessageResponse](WrappedOnionMessageResponse))
waitForSent()
case f: CreateInvoiceActor.InvoiceCreationFailed =>
context.log.debug("invoice creation failed: {}", f.message)
Behaviors.stopped
}
case WrappedInvoiceResponse(invoice) =>
context.log.debug("invoice created for offerId={} invoice={}", invoice.invoiceRequest.offer.offerId, invoice.toString)
postman ! Postman.SendMessage(OfferTypes.BlindedPath(pathToSender), OnionMessages.RoutingStrategy.FindRoute, TlvStream(OnionMessagePayloadTlv.Invoice(invoice.records)), expectsReply = false, context.messageAdapter[Postman.OnionMessageResponse](WrappedOnionMessageResponse))
waitForSent()
}
}

Expand All @@ -234,7 +250,8 @@ object OfferManager {
sealed trait Command

/**
* Sent by the offer handler. Causes the creation of a dummy invoice that matches as best as possible the actual invoice for this payment (since the actual invoice is not stored) and will be used in the payment handler.
* Sent by the offer handler. Causes the creation of a dummy invoice that matches as best as possible the actual
* invoice for this payment (since the actual invoice is not stored) and will be used in the payment handler.
*
* @param additionalTlvs additional TLVs to add to the dummy invoice. Should be the same as what was used for the actual invoice.
* @param customTlvs custom TLVs to add to the dummy invoice. Should be the same as what was used for the actual invoice.
Expand All @@ -246,14 +263,21 @@ object OfferManager {
*/
case class RejectPayment(reason: String) extends Command

def apply(nodeParams: NodeParams, replyTo: ActorRef[MultiPartHandler.GetIncomingPaymentActor.Command], offer: Offer, metadata: MinimalInvoiceData, timeout: FiniteDuration): Behavior[Command] = {
def apply(nodeParams: NodeParams,
replyTo: ActorRef[MultiPartHandler.GetIncomingPaymentActor.Command],
offer: Offer,
metadata: MinimalInvoiceData,
amount: MilliSatoshi,
timeout: FiniteDuration): Behavior[Command] = {
Behaviors.setup { context =>
context.scheduleOnce(timeout, context.self, RejectPayment("plugin timeout"))
Behaviors.receiveMessage {
case AcceptPayment(additionalTlvs, customTlvs) =>
val minimalInvoice = MinimalBolt12Invoice(offer, nodeParams.chainHash, metadata.amount, metadata.quantity, Crypto.sha256(metadata.preimage), metadata.payerKey, metadata.createdAt, additionalTlvs, customTlvs)
val incomingPayment = IncomingBlindedPayment(minimalInvoice, metadata.preimage, PaymentType.Blinded, TimestampMilli.now(), IncomingPaymentStatus.Pending)
replyTo ! MultiPartHandler.GetIncomingPaymentActor.ProcessPayment(incomingPayment)
// We may be deducing some of the blinded path fees from the received amount.
val maxRecipientPathFees = nodeFee(metadata.recipientPathFees, amount)
replyTo ! MultiPartHandler.GetIncomingPaymentActor.ProcessPayment(incomingPayment, maxRecipientPathFees)
Behaviors.stopped
case RejectPayment(reason) =>
replyTo ! MultiPartHandler.GetIncomingPaymentActor.RejectPayment(reason)
Expand Down
Loading