Skip to content

Commit

Permalink
Parallel payment request generation (#1878)
Browse files Browse the repository at this point in the history
Delegate the payment request generation, signature and db write to a short-lived child actor.

There is small (~5%) gain in performance in `PerformanceIntegrationSpec` because what matters is the db write, and it is not parallelized in WAL mode.
  • Loading branch information
pm47 authored Jul 16, 2021
1 parent d02760d commit 3bb7ee8
Show file tree
Hide file tree
Showing 3 changed files with 55 additions and 30 deletions.
2 changes: 1 addition & 1 deletion eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging {

override def receive(description: String, amount_opt: Option[MilliSatoshi], expire_opt: Option[Long], fallbackAddress_opt: Option[String], paymentPreimage_opt: Option[ByteVector32])(implicit timeout: Timeout): Future[PaymentRequest] = {
fallbackAddress_opt.map { fa => fr.acinq.eclair.addressToPublicKeyScript(fa, appKit.nodeParams.chainHash) } // if it's not a bitcoin address throws an exception
(appKit.paymentHandler ? ReceivePayment(amount_opt, description, expire_opt, fallbackAddress = fallbackAddress_opt, paymentPreimage = paymentPreimage_opt)).mapTo[PaymentRequest]
(appKit.paymentHandler ? ReceivePayment(amount_opt, description, expire_opt, fallbackAddress_opt = fallbackAddress_opt, paymentPreimage_opt = paymentPreimage_opt)).mapTo[PaymentRequest]
}

override def newAddress(): Future[String] = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
package fr.acinq.eclair.payment.receive

import akka.actor.Actor.Receive
import akka.actor.typed.Behavior
import akka.actor.typed.scaladsl.Behaviors
import akka.actor.typed.scaladsl.adapter.ClassicActorContextOps
import akka.actor.{ActorContext, ActorRef, PoisonPill, Status}
import akka.event.{DiagnosticLoggingAdapter, LoggingAdapter}
import fr.acinq.bitcoin.{ByteVector32, Crypto}
Expand All @@ -28,6 +31,7 @@ import fr.acinq.eclair.payment.{IncomingPacket, PaymentReceived, PaymentRequest}
import fr.acinq.eclair.wire.protocol._
import fr.acinq.eclair.{Features, Logs, MilliSatoshi, NodeParams, randomBytes32}

import java.util.UUID
import scala.util.{Failure, Success, Try}

/**
Expand All @@ -52,26 +56,9 @@ class MultiPartHandler(nodeParams: NodeParams, register: ActorRef, db: IncomingP
def postFulfill(paymentReceived: PaymentReceived)(implicit log: LoggingAdapter): Unit = ()

override def handle(implicit ctx: ActorContext, log: DiagnosticLoggingAdapter): Receive = {
case ReceivePayment(amount_opt, desc, expirySeconds_opt, extraHops, fallbackAddress_opt, paymentPreimage_opt, paymentType) =>
Try {
val paymentPreimage = paymentPreimage_opt.getOrElse(randomBytes32())
val paymentHash = Crypto.sha256(paymentPreimage)
val expirySeconds = expirySeconds_opt.getOrElse(nodeParams.paymentRequestExpiry.toSeconds)
val features = {
val f1 = Seq(Features.PaymentSecret.mandatory, Features.VariableLengthOnion.mandatory)
val allowMultiPart = nodeParams.features.hasFeature(Features.BasicMultiPartPayment)
val f2 = if (allowMultiPart) Seq(Features.BasicMultiPartPayment.optional) else Nil
val f3 = if (nodeParams.enableTrampolinePayment) Seq(Features.TrampolinePayment.optional) else Nil
PaymentRequest.PaymentRequestFeatures(f1 ++ f2 ++ f3: _*)
}
val paymentRequest = PaymentRequest(nodeParams.chainHash, amount_opt, paymentHash, nodeParams.privateKey, desc, nodeParams.minFinalExpiryDelta, fallbackAddress_opt, expirySeconds = Some(expirySeconds), extraHops = extraHops, features = features)
log.debug("generated payment request={} from amount={}", PaymentRequest.write(paymentRequest), amount_opt)
db.addIncomingPayment(paymentRequest, paymentPreimage, paymentType)
paymentRequest
} match {
case Success(paymentRequest) => ctx.sender ! paymentRequest
case Failure(exception) => ctx.sender ! Status.Failure(exception)
}
case receivePayment: ReceivePayment =>
val child = ctx.spawn(CreateInvoiceActor(nodeParams), name = UUID.randomUUID().toString)
child ! CreateInvoiceActor.CreatePaymentRequest(ctx.sender(), receivePayment)

case p: IncomingPacket.FinalPacket if doHandle(p.add.paymentHash) =>
Logs.withMdc(log)(Logs.mdc(paymentHash_opt = Some(p.add.paymentHash))) {
Expand Down Expand Up @@ -188,21 +175,59 @@ object MultiPartHandler {
/**
* Use this message to create a Bolt 11 invoice to receive a payment.
*
* @param amount_opt amount to receive in milli-satoshis.
* @param description payment description.
* @param expirySeconds_opt number of seconds before the invoice expires (relative to the invoice creation time).
* @param extraHops routing hints to help the payer.
* @param fallbackAddress fallback Bitcoin address.
* @param paymentPreimage payment preimage.
* @param amount_opt amount to receive in milli-satoshis.
* @param description payment description.
* @param expirySeconds_opt number of seconds before the invoice expires (relative to the invoice creation time).
* @param extraHops routing hints to help the payer.
* @param fallbackAddress_opt fallback Bitcoin address.
* @param paymentPreimage_opt payment preimage.
*/
case class ReceivePayment(amount_opt: Option[MilliSatoshi],
description: String,
expirySeconds_opt: Option[Long] = None,
extraHops: List[List[ExtraHop]] = Nil,
fallbackAddress: Option[String] = None,
paymentPreimage: Option[ByteVector32] = None,
fallbackAddress_opt: Option[String] = None,
paymentPreimage_opt: Option[ByteVector32] = None,
paymentType: String = PaymentType.Standard)


object CreateInvoiceActor {

// @formatter:off
sealed trait Command
case class CreatePaymentRequest(replyTo: ActorRef, receivePayment: ReceivePayment) extends Command
// @formatter:on

def apply(nodeParams: NodeParams): Behavior[Command] = {
Behaviors.setup { context =>
Behaviors.receiveMessage {
case CreatePaymentRequest(replyTo, receivePayment) =>
Try {
import receivePayment._
val paymentPreimage = paymentPreimage_opt.getOrElse(randomBytes32())
val paymentHash = Crypto.sha256(paymentPreimage)
val expirySeconds = expirySeconds_opt.getOrElse(nodeParams.paymentRequestExpiry.toSeconds)
val features = {
val f1 = Seq(Features.PaymentSecret.mandatory, Features.VariableLengthOnion.mandatory)
val allowMultiPart = nodeParams.features.hasFeature(Features.BasicMultiPartPayment)
val f2 = if (allowMultiPart) Seq(Features.BasicMultiPartPayment.optional) else Nil
val f3 = if (nodeParams.enableTrampolinePayment) Seq(Features.TrampolinePayment.optional) else Nil
PaymentRequest.PaymentRequestFeatures(f1 ++ f2 ++ f3: _*)
}
val paymentRequest = PaymentRequest(nodeParams.chainHash, amount_opt, paymentHash, nodeParams.privateKey, description, nodeParams.minFinalExpiryDelta, fallbackAddress_opt, expirySeconds = Some(expirySeconds), extraHops = extraHops, features = features)
context.log.debug("generated payment request={} from amount={}", PaymentRequest.write(paymentRequest), amount_opt)
nodeParams.db.payments.addIncomingPayment(paymentRequest, paymentPreimage, paymentType)
paymentRequest
} match {
case Success(paymentRequest) => replyTo ! paymentRequest
case Failure(exception) => replyTo ! Status.Failure(exception)
}
Behaviors.stopped
}
}
}
}

private def validatePaymentStatus(payment: IncomingPacket.FinalPacket, record: IncomingPayment)(implicit log: LoggingAdapter): Boolean = {
if (record.status.isInstanceOf[IncomingPaymentStatus.Received]) {
log.warning("ignoring incoming payment for which has already been paid")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,7 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I

assert(receive.amount_opt === Some(123 msat))
assert(receive.expirySeconds_opt === Some(456))
assert(receive.fallbackAddress === Some(fallBackAddressRaw))
assert(receive.fallbackAddress_opt === Some(fallBackAddressRaw))

// try with wrong address format
assertThrows[IllegalArgumentException](eclair.receive("some desc", Some(123 msat), Some(456), Some("wassa wassa"), None))
Expand Down

0 comments on commit 3bb7ee8

Please sign in to comment.