Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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