diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala index 7ce1343ee2..8c10c7d4af 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala @@ -360,7 +360,7 @@ class Setup(val datadir: File, offerManager = system.spawn(Behaviors.supervise(OfferManager(nodeParams, router, 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") - relayer = system.actorOf(SimpleSupervisor.props(Relayer.props(nodeParams, router, register, paymentHandler, triggerer, Some(postRestartCleanUpInitialized)), "relayer", SupervisorStrategy.Resume)) + relayer = system.actorOf(SimpleSupervisor.props(Relayer.props(nodeParams, router, register, paymentHandler, Some(postRestartCleanUpInitialized)), "relayer", SupervisorStrategy.Resume)) _ = relayer ! PostRestartHtlcCleaner.Init(channels) // Before initializing the switchboard (which re-connects us to the network) and the user-facing parts of the system, // we want to make sure the handler for post-restart broken HTLCs has finished initializing. diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerReadyNotifier.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerReadyNotifier.scala index 81d6c71b5c..4fad93e55e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerReadyNotifier.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerReadyNotifier.scala @@ -88,7 +88,11 @@ object PeerReadyNotifier { context.log.error("no switchboard found") replyTo ! PeerUnavailable(remoteNodeId) Behaviors.stopped - } + } + case Timeout => + context.log.info("timed out finding switchboard actor") + replyTo ! PeerUnavailable(remoteNodeId) + Behaviors.stopped } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelay.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelay.scala index 1270917aa4..9051cb37ef 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelay.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelay.scala @@ -16,15 +16,17 @@ package fr.acinq.eclair.payment.relay -import akka.actor.typed.Behavior import akka.actor.typed.eventstream.EventStream import akka.actor.typed.scaladsl.adapter.{TypedActorContextOps, TypedActorRefOps} import akka.actor.typed.scaladsl.{ActorContext, Behaviors} +import akka.actor.typed.{Behavior, SupervisorStrategy} import akka.actor.{ActorRef, typed} import com.softwaremill.quicklens.ModifyPimp import fr.acinq.bitcoin.scalacompat.ByteVector32 +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.channel.{CMD_FAIL_HTLC, CMD_FULFILL_HTLC} import fr.acinq.eclair.db.PendingCommandsDb +import fr.acinq.eclair.io.PeerReadyNotifier import fr.acinq.eclair.payment.IncomingPaymentPacket.NodeRelayPacket import fr.acinq.eclair.payment.Monitoring.{Metrics, Tags} import fr.acinq.eclair.payment.OutgoingPaymentPacket.Upstream @@ -40,11 +42,12 @@ import fr.acinq.eclair.router.Router.RouteParams import fr.acinq.eclair.router.{BalanceTooLow, RouteNotFound} import fr.acinq.eclair.wire.protocol.PaymentOnion.IntermediatePayload import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{CltvExpiry, Features, Logs, MilliSatoshi, NodeParams, TimestampMilli, UInt64, nodeFee, randomBytes32, randomKey} +import fr.acinq.eclair.{CltvExpiry, Features, Logs, MilliSatoshi, NodeParams, TimestampMilli, UInt64, nodeFee, randomBytes32} import java.util.UUID import java.util.concurrent.TimeUnit import scala.collection.immutable.Queue +import scala.concurrent.duration.DurationInt /** * It [[NodeRelay]] aggregates incoming HTLCs (in case multi-part was used upstream) and then forwards the requested amount (using the @@ -62,7 +65,8 @@ object NodeRelay { private case class WrappedPreimageReceived(preimageReceived: PreimageReceived) extends Command private case class WrappedPaymentSent(paymentSent: PaymentSent) extends Command private case class WrappedPaymentFailed(paymentFailed: PaymentFailed) extends Command - private[relay] case class WrappedPeerReadyResult(result: AsyncPaymentTriggerer.Result) extends Command + private[relay] case class WrappedAsyncPaymentResult(result: AsyncPaymentTriggerer.Result) extends Command + private case class WrappedPeerReadyResult(result: PeerReadyNotifier.Result) extends Command private case class WrappedResolvedPaths(resolved: Seq[ResolvedPath]) extends Command // @formatter:on @@ -88,7 +92,6 @@ object NodeRelay { relayId: UUID, nodeRelayPacket: NodeRelayPacket, outgoingPaymentFactory: OutgoingPaymentFactory, - triggerer: typed.ActorRef[AsyncPaymentTriggerer.Command], router: ActorRef): Behavior[Command] = Behaviors.setup { context => val paymentHash = nodeRelayPacket.add.paymentHash @@ -108,7 +111,7 @@ object NodeRelay { case IncomingPaymentPacket.RelayToTrampolinePacket(_, _, _, nextPacket) => Some(nextPacket) case _: IncomingPaymentPacket.RelayToBlindedPathsPacket => None } - new NodeRelay(nodeParams, parent, register, relayId, paymentHash, nodeRelayPacket.outerPayload.paymentSecret, context, outgoingPaymentFactory, triggerer, router) + new NodeRelay(nodeParams, parent, register, relayId, paymentHash, nodeRelayPacket.outerPayload.paymentSecret, context, outgoingPaymentFactory, router) .receiving(Queue.empty, nodeRelayPacket.innerPayload, nextPacket_opt, incomingPaymentHandler) } } @@ -125,18 +128,29 @@ object NodeRelay { Some(InvalidOnionPayload(UInt64(2), 0)) } else { payloadOut match { - case payloadOut: IntermediatePayload.NodeRelay.Standard => - if (payloadOut.invoiceFeatures.isDefined && payloadOut.paymentSecret.isEmpty) { - Some(InvalidOnionPayload(UInt64(8), 0)) // payment secret field is missing - } else { - None - } - case _: IntermediatePayload.NodeRelay.ToBlindedPaths => - None + // If we're relaying a standard payment to a non-trampoline recipient, we need the payment secret. + case payloadOut: IntermediatePayload.NodeRelay.Standard if payloadOut.invoiceFeatures.isDefined && payloadOut.paymentSecret.isEmpty => Some(InvalidOnionPayload(UInt64(8), 0)) + case _: IntermediatePayload.NodeRelay.Standard => None + case _: IntermediatePayload.NodeRelay.ToBlindedPaths => None } } } + private def shouldWakeUpNextNode(nodeParams: NodeParams, recipient: Recipient): Boolean = { + false + } + + /** When we have identified that the next node is one of our peers, return their (real) nodeId. */ + private def nodeIdToWakeUp(recipient: Recipient): PublicKey = { + recipient match { + case r: ClearRecipient => r.nodeId + case r: SpontaneousRecipient => r.nodeId + case r: TrampolineRecipient => r.nodeId + // When using blinded paths, the recipient nodeId is blinded. The actual node is the introduction of the path. + case r: BlindedRecipient => r.blindedHops.head.nodeId + } + } + /** Compute route params that honor our fee and cltv requirements. */ private def computeRouteParams(nodeParams: NodeParams, amountIn: MilliSatoshi, expiryIn: CltvExpiry, amountOut: MilliSatoshi, expiryOut: CltvExpiry): RouteParams = { nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams @@ -188,7 +202,6 @@ class NodeRelay private(nodeParams: NodeParams, paymentSecret: ByteVector32, context: ActorContext[NodeRelay.Command], outgoingPaymentFactory: NodeRelay.OutgoingPaymentFactory, - triggerer: typed.ActorRef[AsyncPaymentTriggerer.Command], router: ActorRef) { import NodeRelay._ @@ -223,53 +236,13 @@ class NodeRelay private(nodeParams: NodeParams, rejectPayment(upstream, Some(failure)) stopping() case None => - nextPayload match { - // TODO: async payments are not currently supported for blinded recipients. We should update the AsyncPaymentTriggerer to decrypt the blinded path. - case nextPayload: IntermediatePayload.NodeRelay.Standard if nextPayload.isAsyncPayment && nodeParams.features.hasFeature(Features.AsyncPaymentPrototype) => - waitForTrigger(upstream, nextPayload, nextPacket_opt) - case _ => - doSend(upstream, nextPayload, nextPacket_opt) - } + resolveNextNode(upstream, nextPayload, nextPacket_opt) } } - private def waitForTrigger(upstream: Upstream.Trampoline, nextPayload: IntermediatePayload.NodeRelay.Standard, nextPacket_opt: Option[OnionRoutingPacket]): Behavior[Command] = { - context.log.info(s"waiting for async payment to trigger before relaying trampoline payment (amountIn=${upstream.amountIn} expiryIn=${upstream.expiryIn} amountOut=${nextPayload.amountToForward} expiryOut=${nextPayload.outgoingCltv}, asyncPaymentsParams=${nodeParams.relayParams.asyncPaymentsParams})") - val timeoutBlock = nodeParams.currentBlockHeight + nodeParams.relayParams.asyncPaymentsParams.holdTimeoutBlocks - val safetyBlock = (upstream.expiryIn - nodeParams.relayParams.asyncPaymentsParams.cancelSafetyBeforeTimeout).blockHeight - // wait for notification until which ever occurs first: the hold timeout block or the safety block - val notifierTimeout = Seq(timeoutBlock, safetyBlock).min - val peerReadyResultAdapter = context.messageAdapter[AsyncPaymentTriggerer.Result](WrappedPeerReadyResult) - - triggerer ! AsyncPaymentTriggerer.Watch(peerReadyResultAdapter, nextPayload.outgoingNodeId, paymentHash, notifierTimeout) - context.system.eventStream ! EventStream.Publish(WaitingToRelayPayment(nextPayload.outgoingNodeId, paymentHash)) - Behaviors.receiveMessagePartial { - case WrappedPeerReadyResult(AsyncPaymentTriggerer.AsyncPaymentTimeout) => - context.log.warn("rejecting async payment; was not triggered before block {}", notifierTimeout) - rejectPayment(upstream, Some(TemporaryNodeFailure())) // TODO: replace failure type when async payment spec is finalized - stopping() - case WrappedPeerReadyResult(AsyncPaymentTriggerer.AsyncPaymentCanceled) => - context.log.warn(s"payment sender canceled a waiting async payment") - rejectPayment(upstream, Some(TemporaryNodeFailure())) // TODO: replace failure type when async payment spec is finalized - stopping() - case WrappedPeerReadyResult(AsyncPaymentTriggerer.AsyncPaymentTriggered) => - doSend(upstream, nextPayload, nextPacket_opt) - } - } - - private def doSend(upstream: Upstream.Trampoline, nextPayload: IntermediatePayload.NodeRelay, nextPacket_opt: Option[OnionRoutingPacket]): Behavior[Command] = { - context.log.debug(s"relaying trampoline payment (amountIn=${upstream.amountIn} expiryIn=${upstream.expiryIn} amountOut=${nextPayload.amountToForward} expiryOut=${nextPayload.outgoingCltv})") - relay(upstream, nextPayload, nextPacket_opt) - } - - private def relay(upstream: Upstream.Trampoline, payloadOut: IntermediatePayload.NodeRelay, packetOut_opt: Option[OnionRoutingPacket]): Behavior[Command] = { - val displayNodeId = payloadOut match { - case payloadOut: IntermediatePayload.NodeRelay.Standard => payloadOut.outgoingNodeId - case _: IntermediatePayload.NodeRelay.ToBlindedPaths => randomKey().publicKey - } - val paymentCfg = SendPaymentConfig(relayId, relayId, None, paymentHash, displayNodeId, upstream, None, None, storeInDb = false, publishEvent = false, recordPathFindingMetrics = true) - val routeParams = computeRouteParams(nodeParams, upstream.amountIn, upstream.expiryIn, payloadOut.amountToForward, payloadOut.outgoingCltv) - payloadOut match { + /** Once we've fully received the incoming HTLC set, we must identify the next node before forwarding the payment. */ + private def resolveNextNode(upstream: Upstream.Trampoline, nextPayload: IntermediatePayload.NodeRelay, nextPacket_opt: Option[OnionRoutingPacket]): Behavior[Command] = { + nextPayload match { case payloadOut: IntermediatePayload.NodeRelay.Standard => // If invoice features are provided in the onion, the sender is asking us to relay to a non-trampoline recipient. payloadOut.invoiceFeatures match { @@ -277,48 +250,76 @@ class NodeRelay private(nodeParams: NodeParams, val extraEdges = payloadOut.invoiceRoutingInfo.getOrElse(Nil).flatMap(Bolt11Invoice.toExtraEdges(_, payloadOut.outgoingNodeId)) val paymentSecret = payloadOut.paymentSecret.get // NB: we've verified that there was a payment secret in validateRelay val recipient = ClearRecipient(payloadOut.outgoingNodeId, Features(features).invoiceFeatures(), payloadOut.amountToForward, payloadOut.outgoingCltv, paymentSecret, extraEdges, payloadOut.paymentMetadata) - context.log.debug("sending the payment to non-trampoline recipient (MPP={})", recipient.features.hasFeature(Features.BasicMultiPartPayment)) - relayToRecipient(upstream, payloadOut, recipient, paymentCfg, routeParams, useMultiPart = recipient.features.hasFeature(Features.BasicMultiPartPayment)) + context.log.debug("forwarding payment to non-trampoline recipient {}", recipient.nodeId) + ensureRecipientReady(upstream, recipient, nextPayload, None) case None => - context.log.debug("sending the payment to the next trampoline node") val paymentSecret = randomBytes32() // we generate a new secret to protect against probing attacks - val recipient = ClearRecipient(payloadOut.outgoingNodeId, Features.empty, payloadOut.amountToForward, payloadOut.outgoingCltv, paymentSecret, nextTrampolineOnion_opt = packetOut_opt) - relayToRecipient(upstream, payloadOut, recipient, paymentCfg, routeParams, useMultiPart = true) + val recipient = ClearRecipient(payloadOut.outgoingNodeId, Features.empty, payloadOut.amountToForward, payloadOut.outgoingCltv, paymentSecret, nextTrampolineOnion_opt = nextPacket_opt) + context.log.debug("forwarding payment to the next trampoline node {}", recipient.nodeId) + ensureRecipientReady(upstream, recipient, nextPayload, nextPacket_opt) } case payloadOut: IntermediatePayload.NodeRelay.ToBlindedPaths => + // Blinded paths in Bolt 12 invoices may encode the introduction node with an scid and a direction: we need to + // resolve that to a nodeId in order to reach that introduction node and use the blinded path. + // If we are the introduction node ourselves, we'll also need to decrypt the onion and identify the next node. context.spawnAnonymous(BlindedPathsResolver(nodeParams, paymentHash, router, register)) ! Resolve(context.messageAdapter[Seq[ResolvedPath]](WrappedResolvedPaths), payloadOut.outgoingBlindedPaths) - waitForResolvedPaths(upstream, payloadOut, paymentCfg, routeParams) + Behaviors.receiveMessagePartial { + rejectExtraHtlcPartialFunction orElse { + case WrappedResolvedPaths(resolved) if resolved.isEmpty => + context.log.warn("rejecting trampoline payment to blinded paths: no usable blinded path") + rejectPayment(upstream, Some(UnknownNextPeer())) + stopping() + case WrappedResolvedPaths(resolved) => + // We don't have access to the invoice: we use the only node_id that somewhat makes sense for the recipient. + val blindedNodeId = resolved.head.route.blindedNodeIds.last + val recipient = BlindedRecipient.fromPaths(blindedNodeId, Features(payloadOut.invoiceFeatures).invoiceFeatures(), payloadOut.amountToForward, payloadOut.outgoingCltv, resolved, Set.empty) + context.log.debug("forwarding payment to blinded recipient {}", recipient.nodeId) + ensureRecipientReady(upstream, recipient, nextPayload, nextPacket_opt) + } + } + } + } + + /** + * The next node may be a mobile wallet directly connected to us: in that case, we'll need to wake them up before + * relaying the payment. + */ + private def ensureRecipientReady(upstream: Upstream.Trampoline, recipient: Recipient, nextPayload: IntermediatePayload.NodeRelay, nextPacket_opt: Option[OnionRoutingPacket]): Behavior[Command] = { + if (shouldWakeUpNextNode(nodeParams, recipient)) { + waitForPeerReady(upstream, recipient, nextPayload, nextPacket_opt) + } else { + relay(upstream, recipient, nextPayload, nextPacket_opt) } } /** - * Blinded paths in Bolt 12 invoices may encode the introduction node with an scid and a direction: we need to resolve - * that to a nodeId in order to reach that introduction node and use the blinded path. + * The next node is the payment recipient. They are directly connected to us and may be offline. We try to wake them + * up and will relay the payment once they're connected and channels are reestablished. */ - private def waitForResolvedPaths(upstream: Upstream.Trampoline, - payloadOut: IntermediatePayload.NodeRelay.ToBlindedPaths, - paymentCfg: SendPaymentConfig, - routeParams: RouteParams): Behavior[Command] = + private def waitForPeerReady(upstream: Upstream.Trampoline, recipient: Recipient, nextPayload: IntermediatePayload.NodeRelay, nextPacket_opt: Option[OnionRoutingPacket]): Behavior[Command] = { + val nextNodeId = nodeIdToWakeUp(recipient) + context.log.info("trying to wake up next peer (nodeId={})", nextNodeId) + val notifier = context.spawnAnonymous(Behaviors.supervise(PeerReadyNotifier(nextNodeId, timeout_opt = Some(Left(30 seconds)))).onFailure(SupervisorStrategy.stop)) + notifier ! PeerReadyNotifier.NotifyWhenPeerReady(context.messageAdapter(WrappedPeerReadyResult)) Behaviors.receiveMessagePartial { - case WrappedResolvedPaths(resolved) if resolved.isEmpty => - context.log.warn(s"rejecting trampoline payment to blinded paths: no usable blinded path") - rejectPayment(upstream, Some(UnknownNextPeer())) - stopping() - case WrappedResolvedPaths(resolved) => - val features = Features(payloadOut.invoiceFeatures).invoiceFeatures() - // We don't have access to the invoice: we use the only node_id that somewhat makes sense for the recipient. - val blindedNodeId = resolved.head.route.blindedNodeIds.last - val recipient = BlindedRecipient.fromPaths(blindedNodeId, features, payloadOut.amountToForward, payloadOut.outgoingCltv, resolved, Set.empty) - context.log.debug("sending the payment to blinded recipient, useMultiPart={}", features.hasFeature(Features.BasicMultiPartPayment)) - relayToRecipient(upstream, payloadOut, recipient, paymentCfg, routeParams, features.hasFeature(Features.BasicMultiPartPayment)) + rejectExtraHtlcPartialFunction orElse { + case WrappedPeerReadyResult(_: PeerReadyNotifier.PeerUnavailable) => + context.log.warn("rejecting payment: failed to wake-up remote peer") + rejectPayment(upstream, Some(UnknownNextPeer())) + stopping() + case WrappedPeerReadyResult(_: PeerReadyNotifier.PeerReady) => + relay(upstream, recipient, nextPayload, nextPacket_opt) + } } + } - private def relayToRecipient(upstream: Upstream.Trampoline, - payloadOut: IntermediatePayload.NodeRelay, - recipient: Recipient, - paymentCfg: SendPaymentConfig, - routeParams: RouteParams, - useMultiPart: Boolean): Behavior[Command] = { + /** Relay the payment to the next identified node: this is similar to sending an outgoing payment. */ + private def relay(upstream: Upstream.Trampoline, recipient: Recipient, payloadOut: IntermediatePayload.NodeRelay, packetOut_opt: Option[OnionRoutingPacket]): Behavior[Command] = { + context.log.debug("relaying trampoline payment (amountIn={} expiryIn={} amountOut={} expiryOut={})", upstream.amountIn, upstream.expiryIn, payloadOut.amountToForward, payloadOut.outgoingCltv) + val paymentCfg = SendPaymentConfig(relayId, relayId, None, paymentHash, recipient.nodeId, upstream, None, None, storeInDb = false, publishEvent = false, recordPathFindingMetrics = true) + val routeParams = computeRouteParams(nodeParams, upstream.amountIn, upstream.expiryIn, payloadOut.amountToForward, payloadOut.outgoingCltv) + // If the next node is using trampoline, we assume that they support MPP. + val useMultiPart = recipient.features.hasFeature(Features.BasicMultiPartPayment) || packetOut_opt.nonEmpty val payFsmAdapters = { context.messageAdapter[PreimageReceived](WrappedPreimageReceived) context.messageAdapter[PaymentSent](WrappedPaymentSent) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelayer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelayer.scala index d74ef9e0c5..26a449f3e3 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelayer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelayer.scala @@ -16,7 +16,6 @@ package fr.acinq.eclair.payment.relay -import akka.actor.typed import akka.actor.typed.scaladsl.Behaviors import akka.actor.typed.{ActorRef, Behavior} import fr.acinq.bitcoin.scalacompat.ByteVector32 @@ -57,7 +56,7 @@ object NodeRelayer { * NB: the payment secret used here is different from the invoice's payment secret and ensures we can * group together HTLCs that the previous trampoline node sent in the same MPP. */ - def apply(nodeParams: NodeParams, register: akka.actor.ActorRef, outgoingPaymentFactory: NodeRelay.OutgoingPaymentFactory, triggerer: typed.ActorRef[AsyncPaymentTriggerer.Command], router: akka.actor.ActorRef, children: Map[PaymentKey, ActorRef[NodeRelay.Command]] = Map.empty): Behavior[Command] = + def apply(nodeParams: NodeParams, register: akka.actor.ActorRef, outgoingPaymentFactory: NodeRelay.OutgoingPaymentFactory, router: akka.actor.ActorRef, children: Map[PaymentKey, ActorRef[NodeRelay.Command]] = Map.empty): Behavior[Command] = Behaviors.setup { context => Behaviors.withMdc(Logs.mdc(category_opt = Some(Logs.LogCategory.PAYMENT)), mdc) { Behaviors.receiveMessage { @@ -72,15 +71,15 @@ object NodeRelayer { case None => val relayId = UUID.randomUUID() context.log.debug(s"spawning a new handler with relayId=$relayId") - val handler = context.spawn(NodeRelay.apply(nodeParams, context.self, register, relayId, nodeRelayPacket, outgoingPaymentFactory, triggerer, router), relayId.toString) + val handler = context.spawn(NodeRelay.apply(nodeParams, context.self, register, relayId, nodeRelayPacket, outgoingPaymentFactory, router), relayId.toString) context.log.debug("forwarding incoming htlc #{} from channel {} to new handler", htlcIn.id, htlcIn.channelId) handler ! NodeRelay.Relay(nodeRelayPacket) - apply(nodeParams, register, outgoingPaymentFactory, triggerer, router, children + (childKey -> handler)) + apply(nodeParams, register, outgoingPaymentFactory, router, children + (childKey -> handler)) } case RelayComplete(childHandler, paymentHash, paymentSecret) => // we do a back-and-forth between parent and child before stopping the child to prevent a race condition childHandler ! NodeRelay.Stop - apply(nodeParams, register, outgoingPaymentFactory, triggerer, router, children - PaymentKey(paymentHash, paymentSecret)) + apply(nodeParams, register, outgoingPaymentFactory, router, children - PaymentKey(paymentHash, paymentSecret)) case GetPendingPayments(replyTo) => replyTo ! children Behaviors.same diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala index dd20d82397..45d3b7a6fe 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala @@ -49,7 +49,7 @@ import scala.util.Random * It also receives channel HTLC events (fulfill / failed) and relays those to the appropriate handlers. * It also maintains an up-to-date view of local channel balances. */ -class Relayer(nodeParams: NodeParams, router: ActorRef, register: ActorRef, paymentHandler: ActorRef, triggerer: typed.ActorRef[AsyncPaymentTriggerer.Command], initialized: Option[Promise[Done]] = None) extends Actor with DiagnosticActorLogging { +class Relayer(nodeParams: NodeParams, router: ActorRef, register: ActorRef, paymentHandler: ActorRef, initialized: Option[Promise[Done]] = None) extends Actor with DiagnosticActorLogging { import Relayer._ @@ -58,7 +58,7 @@ class Relayer(nodeParams: NodeParams, router: ActorRef, register: ActorRef, paym private val postRestartCleaner = context.actorOf(PostRestartHtlcCleaner.props(nodeParams, register, initialized), "post-restart-htlc-cleaner") private val channelRelayer = context.spawn(Behaviors.supervise(ChannelRelayer(nodeParams, register)).onFailure(SupervisorStrategy.resume), "channel-relayer") - private val nodeRelayer = context.spawn(Behaviors.supervise(NodeRelayer(nodeParams, register, NodeRelay.SimpleOutgoingPaymentFactory(nodeParams, router, register), triggerer, router)).onFailure(SupervisorStrategy.resume), name = "node-relayer") + private val nodeRelayer = context.spawn(Behaviors.supervise(NodeRelayer(nodeParams, register, NodeRelay.SimpleOutgoingPaymentFactory(nodeParams, router, register), router)).onFailure(SupervisorStrategy.resume), name = "node-relayer") def receive: Receive = { case init: PostRestartHtlcCleaner.Init => postRestartCleaner forward init @@ -120,8 +120,8 @@ class Relayer(nodeParams: NodeParams, router: ActorRef, register: ActorRef, paym object Relayer extends Logging { - def props(nodeParams: NodeParams, router: ActorRef, register: ActorRef, paymentHandler: ActorRef, triggerer: typed.ActorRef[AsyncPaymentTriggerer.Command], initialized: Option[Promise[Done]] = None): Props = - Props(new Relayer(nodeParams, router, register, paymentHandler, triggerer, initialized)) + def props(nodeParams: NodeParams, router: ActorRef, register: ActorRef, paymentHandler: ActorRef, initialized: Option[Promise[Done]] = None): Props = + Props(new Relayer(nodeParams, router, register, paymentHandler, initialized)) // @formatter:off case class RelayFees(feeBase: MilliSatoshi, feeProportionalMillionths: Long) { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala index d174f1daac..788f4fb29c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala @@ -68,8 +68,8 @@ class FuzzySpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Channe val bobRegister = system.actorOf(Props(new TestRegister())) val alicePaymentHandler = system.actorOf(Props(new PaymentHandler(aliceParams, aliceRegister, TestProbe().ref))) val bobPaymentHandler = system.actorOf(Props(new PaymentHandler(bobParams, bobRegister, TestProbe().ref))) - val aliceRelayer = system.actorOf(Relayer.props(aliceParams, TestProbe().ref, aliceRegister, alicePaymentHandler, TestProbe().ref)) - val bobRelayer = system.actorOf(Relayer.props(bobParams, TestProbe().ref, bobRegister, bobPaymentHandler, TestProbe().ref)) + val aliceRelayer = system.actorOf(Relayer.props(aliceParams, TestProbe().ref, aliceRegister, alicePaymentHandler)) + val bobRelayer = system.actorOf(Relayer.props(bobParams, TestProbe().ref, bobRegister, bobPaymentHandler)) val wallet = new DummyOnChainWallet() val alice: TestFSMRef[ChannelState, ChannelData, Channel] = TestFSMRef(new Channel(aliceParams, wallet, bobParams.nodeId, alice2blockchain.ref, aliceRelayer, FakeTxPublisherFactory(alice2blockchain)), alicePeer.ref) val bob: TestFSMRef[ChannelState, ChannelData, Channel] = TestFSMRef(new Channel(bobParams, wallet, aliceParams.nodeId, bob2blockchain.ref, bobRelayer, FakeTxPublisherFactory(bob2blockchain)), bobPeer.ref) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/MinimalNodeFixture.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/MinimalNodeFixture.scala index 1fcbada4a6..bbb153a276 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/MinimalNodeFixture.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/MinimalNodeFixture.scala @@ -90,13 +90,12 @@ object MinimalNodeFixture extends Assertions with Eventually with IntegrationPat val bitcoinClient = new TestBitcoinCoreClient() val wallet = new SingleKeyOnChainWallet() val watcher = TestProbe("watcher") - val triggerer = TestProbe("payment-triggerer") val watcherTyped = watcher.ref.toTyped[ZmqWatcher.Command] val register = system.actorOf(Register.props(), "register") val router = system.actorOf(Router.props(nodeParams, watcherTyped), "router") val offerManager = system.spawn(OfferManager(nodeParams, router, 1 minute), "offer-manager") val paymentHandler = system.actorOf(PaymentHandler.props(nodeParams, register, offerManager), "payment-handler") - val relayer = system.actorOf(Relayer.props(nodeParams, router, register, paymentHandler, triggerer.ref.toTyped), "relayer") + val relayer = system.actorOf(Relayer.props(nodeParams, router, register, paymentHandler), "relayer") val txPublisherFactory = Channel.SimpleTxPublisherFactory(nodeParams, watcherTyped, bitcoinClient) val channelFactory = Peer.SimpleChannelFactory(nodeParams, watcherTyped, relayer, wallet, txPublisherFactory) val pendingChannelsRateLimiter = system.spawnAnonymous(Behaviors.supervise(PendingChannelsRateLimiter(nodeParams, router.toTyped, Seq())).onFailure(typed.SupervisorStrategy.resume)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PostRestartHtlcCleanerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PostRestartHtlcCleanerSpec.scala index d540260a80..07323f052b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PostRestartHtlcCleanerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PostRestartHtlcCleanerSpec.scala @@ -57,7 +57,7 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit case class FixtureParam(nodeParams: NodeParams, register: TestProbe, sender: TestProbe, eventListener: TestProbe) { def createRelayer(nodeParams1: NodeParams): (ActorRef, ActorRef) = { - val relayer = system.actorOf(Relayer.props(nodeParams1, TestProbe().ref, register.ref, TestProbe().ref, TestProbe().ref.toTyped)) + val relayer = system.actorOf(Relayer.props(nodeParams1, TestProbe().ref, register.ref, TestProbe().ref)) // we need ensure the post-htlc-restart child actor is initialized sender.send(relayer, Relayer.GetChildActors(sender.ref)) (relayer, sender.expectMsgType[Relayer.ChildActors].postRestartCleaner) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/NodeRelayerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/NodeRelayerSpec.scala index b179d9f17b..d294bd1d58 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/NodeRelayerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/NodeRelayerSpec.scala @@ -26,9 +26,9 @@ import com.softwaremill.quicklens.ModifyPimp import com.typesafe.config.ConfigFactory import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.scalacompat.{Block, BlockHash, ByteVector32, Crypto} +import fr.acinq.eclair.EncodedNodeId.ShortChannelIdDir import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} import fr.acinq.eclair.Features.{AsyncPaymentPrototype, BasicMultiPartPayment, PaymentSecret, VariableLengthOnion} -import fr.acinq.eclair.EncodedNodeId.ShortChannelIdDir import fr.acinq.eclair.channel.{CMD_FAIL_HTLC, CMD_FULFILL_HTLC, Register} import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop @@ -36,7 +36,6 @@ import fr.acinq.eclair.payment.IncomingPaymentPacket.{RelayToBlindedPathsPacket, import fr.acinq.eclair.payment.Invoice.ExtraEdge import fr.acinq.eclair.payment.OutgoingPaymentPacket.Upstream import fr.acinq.eclair.payment._ -import fr.acinq.eclair.payment.relay.AsyncPaymentTriggerer.{AsyncPaymentCanceled, AsyncPaymentTimeout, AsyncPaymentTriggered, Watch} import fr.acinq.eclair.payment.relay.NodeRelayer.PaymentKey import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle.{PreimageReceived, SendMultiPartPayment} import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentConfig @@ -50,8 +49,8 @@ import fr.acinq.eclair.wire.protocol.RouteBlindingEncryptedDataCodecs.blindedRou import fr.acinq.eclair.wire.protocol.RouteBlindingEncryptedDataTlv.{AllowedFeatures, PathId, PaymentConstraints} import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.{BlockHeight, Bolt11Feature, CltvExpiry, CltvExpiryDelta, FeatureSupport, Features, MilliSatoshi, MilliSatoshiLong, NodeParams, RealShortChannelId, ShortChannelId, TestConstants, TimestampMilli, UInt64, randomBytes, randomBytes32, randomKey} +import org.scalatest.Outcome import org.scalatest.funsuite.FixtureAnyFunSuiteLike -import org.scalatest.{Outcome, Tag} import scodec.bits.{ByteVector, HexStringSyntax} import java.util.UUID @@ -66,11 +65,11 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl import NodeRelayerSpec._ - case class FixtureParam(nodeParams: NodeParams, router: TestProbe[Any], register: TestProbe[Any], mockPayFSM: TestProbe[Any], eventListener: TestProbe[PaymentEvent], triggerer: TestProbe[AsyncPaymentTriggerer.Command]) { + case class FixtureParam(nodeParams: NodeParams, router: TestProbe[Any], register: TestProbe[Any], mockPayFSM: TestProbe[Any], eventListener: TestProbe[PaymentEvent]) { def createNodeRelay(packetIn: IncomingPaymentPacket.NodeRelayPacket, useRealPaymentFactory: Boolean = false): (ActorRef[NodeRelay.Command], TestProbe[NodeRelayer.Command]) = { val parent = TestProbe[NodeRelayer.Command]("parent-relayer") val outgoingPaymentFactory = if (useRealPaymentFactory) RealOutgoingPaymentFactory(this) else FakeOutgoingPaymentFactory(this) - val nodeRelay = testKit.spawn(NodeRelay(nodeParams, parent.ref, register.ref.toClassic, relayId, packetIn, outgoingPaymentFactory, triggerer.ref, router.ref.toClassic)) + val nodeRelay = testKit.spawn(NodeRelay(nodeParams, parent.ref, register.ref.toClassic, relayId, packetIn, outgoingPaymentFactory, router.ref.toClassic)) (nodeRelay, parent) } } @@ -93,21 +92,19 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl override def withFixture(test: OneArgTest): Outcome = { val nodeParams = TestConstants.Bob.nodeParams .modify(_.multiPartPaymentExpiry).setTo(5 seconds) - .modify(_.features).setToIf(test.tags.contains("async_payments"))(Features(AsyncPaymentPrototype -> Optional)) .modify(_.relayParams.asyncPaymentsParams.holdTimeoutBlocks).setToIf(test.tags.contains("long_hold_timeout"))(200000) // timeout after payment expires val router = TestProbe[Any]("router") val register = TestProbe[Any]("register") val eventListener = TestProbe[PaymentEvent]("event-listener") system.eventStream ! EventStream.Subscribe(eventListener.ref) val mockPayFSM = TestProbe[Any]("pay-fsm") - val triggerer = TestProbe[AsyncPaymentTriggerer.Command]("payment-triggerer") - withFixture(test.toNoArgTest(FixtureParam(nodeParams, router, register, mockPayFSM, eventListener, triggerer))) + withFixture(test.toNoArgTest(FixtureParam(nodeParams, router, register, mockPayFSM, eventListener))) } test("create child handlers for new payments") { f => import f._ val probe = TestProbe[Any]() - val parentRelayer = testKit.spawn(NodeRelayer(nodeParams, register.ref.toClassic, FakeOutgoingPaymentFactory(f), triggerer.ref, router.ref.toClassic)) + val parentRelayer = testKit.spawn(NodeRelayer(nodeParams, register.ref.toClassic, FakeOutgoingPaymentFactory(f), router.ref.toClassic)) parentRelayer ! NodeRelayer.GetPendingPayments(probe.ref.toClassic) probe.expectMessage(Map.empty) @@ -146,7 +143,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl val outgoingPaymentFactory = FakeOutgoingPaymentFactory(f) { - val parentRelayer = testKit.spawn(NodeRelayer(nodeParams, register.ref.toClassic, outgoingPaymentFactory, triggerer.ref, router.ref.toClassic)) + val parentRelayer = testKit.spawn(NodeRelayer(nodeParams, register.ref.toClassic, outgoingPaymentFactory, router.ref.toClassic)) parentRelayer ! NodeRelayer.GetPendingPayments(probe.ref.toClassic) probe.expectMessage(Map.empty) } @@ -154,7 +151,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl val (paymentHash1, paymentSecret1, child1) = (randomBytes32(), randomBytes32(), TestProbe[NodeRelay.Command]()) val (paymentHash2, paymentSecret2, child2) = (randomBytes32(), randomBytes32(), TestProbe[NodeRelay.Command]()) val children = Map(PaymentKey(paymentHash1, paymentSecret1) -> child1.ref, PaymentKey(paymentHash2, paymentSecret2) -> child2.ref) - val parentRelayer = testKit.spawn(NodeRelayer(nodeParams, register.ref.toClassic, outgoingPaymentFactory, triggerer.ref, router.ref.toClassic, children)) + val parentRelayer = testKit.spawn(NodeRelayer(nodeParams, register.ref.toClassic, outgoingPaymentFactory, router.ref.toClassic, children)) parentRelayer ! NodeRelayer.GetPendingPayments(probe.ref.toClassic) probe.expectMessage(children) @@ -170,7 +167,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl val (paymentSecret1, child1) = (randomBytes32(), TestProbe[NodeRelay.Command]()) val (paymentSecret2, child2) = (randomBytes32(), TestProbe[NodeRelay.Command]()) val children = Map(PaymentKey(paymentHash, paymentSecret1) -> child1.ref, PaymentKey(paymentHash, paymentSecret2) -> child2.ref) - val parentRelayer = testKit.spawn(NodeRelayer(nodeParams, register.ref.toClassic, outgoingPaymentFactory, triggerer.ref, router.ref.toClassic, children)) + val parentRelayer = testKit.spawn(NodeRelayer(nodeParams, register.ref.toClassic, outgoingPaymentFactory, router.ref.toClassic, children)) parentRelayer ! NodeRelayer.GetPendingPayments(probe.ref.toClassic) probe.expectMessage(children) @@ -180,7 +177,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl probe.expectMessage(Map(PaymentKey(paymentHash, paymentSecret2) -> child2.ref)) } { - val parentRelayer = testKit.spawn(NodeRelayer(nodeParams, register.ref.toClassic, outgoingPaymentFactory, triggerer.ref, router.ref.toClassic)) + val parentRelayer = testKit.spawn(NodeRelayer(nodeParams, register.ref.toClassic, outgoingPaymentFactory, router.ref.toClassic)) parentRelayer ! NodeRelayer.Relay(incomingMultiPart.head) parentRelayer ! NodeRelayer.GetPendingPayments(probe.ref.toClassic) val pending1 = probe.expectMessageType[Map[PaymentKey, ActorRef[NodeRelay.Command]]] @@ -336,115 +333,6 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl register.expectNoMessage(100 millis) } - test("fail to relay when not triggered before the hold timeout", Tag("async_payments")) { f => - import f._ - - val (nodeRelayer, _) = createNodeRelay(incomingAsyncPayment.head) - incomingAsyncPayment.foreach(p => nodeRelayer ! NodeRelay.Relay(p)) - - // wait until the NodeRelay is waiting for the trigger - eventListener.expectMessageType[WaitingToRelayPayment] - mockPayFSM.expectNoMessage(100 millis) // we should NOT trigger a downstream payment before we received a trigger - - // publish notification that peer is unavailable at the timeout height - val peerWatch = triggerer.expectMessageType[Watch] - assert(asyncTimeoutHeight(nodeParams) < asyncSafetyHeight(incomingAsyncPayment, nodeParams)) - assert(peerWatch.timeout == asyncTimeoutHeight(nodeParams)) - peerWatch.replyTo ! AsyncPaymentTimeout - - incomingAsyncPayment.foreach { p => - val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] - assert(fwd.channelId == p.add.channelId) - assert(fwd.message == CMD_FAIL_HTLC(p.add.id, Right(TemporaryNodeFailure()), commit = true)) - } - register.expectNoMessage(100 millis) - } - - test("relay the payment when triggered while waiting", Tag("async_payments"), Tag("long_hold_timeout")) { f => - import f._ - - val (nodeRelayer, parent) = createNodeRelay(incomingAsyncPayment.head) - incomingAsyncPayment.foreach(p => nodeRelayer ! NodeRelay.Relay(p)) - - // wait until the NodeRelay is waiting for the trigger - eventListener.expectMessageType[WaitingToRelayPayment] - mockPayFSM.expectNoMessage(100 millis) // we should NOT trigger a downstream payment before we received a trigger - - // publish notification that peer is ready before the safety interval before the current incoming payment expires (and before the timeout height) - val peerWatch = triggerer.expectMessageType[Watch] - assert(asyncTimeoutHeight(nodeParams) > asyncSafetyHeight(incomingAsyncPayment, nodeParams)) - assert(peerWatch.timeout == asyncSafetyHeight(incomingAsyncPayment, nodeParams)) - peerWatch.replyTo ! AsyncPaymentTriggered - - // upstream payment relayed - val outgoingCfg = mockPayFSM.expectMessageType[SendPaymentConfig] - validateOutgoingCfg(outgoingCfg, Upstream.Trampoline(incomingAsyncPayment.map(p => Upstream.ReceivedHtlc(p.add, TimestampMilli.now())))) - val outgoingPayment = mockPayFSM.expectMessageType[SendMultiPartPayment] - validateOutgoingPayment(outgoingPayment) - // those are adapters for pay-fsm messages - val nodeRelayerAdapters = outgoingPayment.replyTo - - // A first downstream HTLC is fulfilled: we should immediately forward the fulfill upstream. - nodeRelayerAdapters ! PreimageReceived(paymentHash, paymentPreimage) - incomingAsyncPayment.foreach { p => - val fwd = register.expectMessageType[Register.Forward[CMD_FULFILL_HTLC]] - assert(fwd.channelId == p.add.channelId) - assert(fwd.message == CMD_FULFILL_HTLC(p.add.id, paymentPreimage, commit = true)) - } - - // Once all the downstream payments have settled, we should emit the relayed event. - nodeRelayerAdapters ! createSuccessEvent() - val relayEvent = eventListener.expectMessageType[TrampolinePaymentRelayed] - validateRelayEvent(relayEvent) - assert(relayEvent.incoming.map(p => (p.amount, p.channelId)).toSet == incomingAsyncPayment.map(i => (i.add.amountMsat, i.add.channelId)).toSet) - assert(relayEvent.outgoing.nonEmpty) - parent.expectMessageType[NodeRelayer.RelayComplete] - register.expectNoMessage(100 millis) - } - - test("fail to relay when not triggered before the incoming expiry safety timeout", Tag("async_payments"), Tag("long_hold_timeout")) { f => - import f._ - - val (nodeRelayer, _) = createNodeRelay(incomingAsyncPayment.head) - incomingAsyncPayment.foreach(p => nodeRelayer ! NodeRelay.Relay(p)) - mockPayFSM.expectNoMessage(100 millis) // we should NOT trigger a downstream payment before we received a complete upstream payment - - // publish notification that peer is unavailable at the cancel-safety-before-timeout-block threshold before the current incoming payment expires (and before the timeout height) - val peerWatch = triggerer.expectMessageType[Watch] - assert(asyncTimeoutHeight(nodeParams) > asyncSafetyHeight(incomingAsyncPayment, nodeParams)) - assert(peerWatch.timeout == asyncSafetyHeight(incomingAsyncPayment, nodeParams)) - peerWatch.replyTo ! AsyncPaymentTimeout - - incomingAsyncPayment.foreach { p => - val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] - assert(fwd.channelId == p.add.channelId) - assert(fwd.message == CMD_FAIL_HTLC(p.add.id, Right(TemporaryNodeFailure()), commit = true)) - } - - register.expectNoMessage(100 millis) - } - - test("fail to relay payment when canceled by sender before timeout", Tag("async_payments")) { f => - import f._ - - val (nodeRelayer, _) = createNodeRelay(incomingAsyncPayment.head) - incomingAsyncPayment.foreach(p => nodeRelayer ! NodeRelay.Relay(p)) - - // wait until the NodeRelay is waiting for the trigger - eventListener.expectMessageType[WaitingToRelayPayment] - mockPayFSM.expectNoMessage(100 millis) // we should NOT trigger a downstream payment before we received a trigger - - // fail the payment if waiting when payment sender sends cancel message - nodeRelayer ! NodeRelay.WrappedPeerReadyResult(AsyncPaymentCanceled) - - incomingAsyncPayment.foreach { p => - val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] - assert(fwd.channelId == p.add.channelId) - assert(fwd.message == CMD_FAIL_HTLC(p.add.id, Right(TemporaryNodeFailure()), commit = true)) - } - register.expectNoMessage(100 millis) - } - test("relay the payment immediately when the async payment feature is disabled") { f => import f._ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/RelayerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/RelayerSpec.scala index 0d1f9a16e6..4c582f1808 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/RelayerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/RelayerSpec.scala @@ -57,11 +57,10 @@ class RelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat val router = TestProbe[Any]("router") val register = TestProbe[Any]("register") val paymentHandler = TestProbe[Any]("payment-handler") - val triggerer = TestProbe[AsyncPaymentTriggerer.Command]("payment-triggerer") val probe = TestProbe[Any]() // we can't spawn top-level actors with akka typed testKit.spawn(Behaviors.setup[Any] { context => - val relayer = context.toClassic.actorOf(Relayer.props(nodeParams, router.ref.toClassic, register.ref.toClassic, paymentHandler.ref.toClassic, triggerer.ref)) + val relayer = context.toClassic.actorOf(Relayer.props(nodeParams, router.ref.toClassic, register.ref.toClassic, paymentHandler.ref.toClassic)) probe.ref ! relayer Behaviors.empty[Any] })