From 3af89279796b04b7ced94a9908cd440d594791a0 Mon Sep 17 00:00:00 2001 From: Pierre-Marie Padiou Date: Tue, 4 Jul 2023 15:40:50 +0200 Subject: [PATCH] Create invoice outside of the peer's message loop (#492) Invoices creation is independent from the peer, and it would remove the artificial dependency between creating an invoice and connecting to electrum. --- .../kotlin/fr/acinq/lightning/io/Peer.kt | 59 +++++++------------ .../fr/acinq/lightning/io/peer/PeerTest.kt | 25 ++++---- 2 files changed, 32 insertions(+), 52 deletions(-) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt b/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt index 8c3109364..94b814341 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt @@ -55,7 +55,6 @@ data class WrappedChannelCommand(val channelId: ByteVector32, val channelCommand object Disconnected : PeerCommand() sealed class PaymentCommand : PeerCommand() -data class ReceivePayment(val paymentPreimage: ByteVector32, val amount: MilliSatoshi?, val description: Either, val expirySeconds: Long? = null, val result: CompletableDeferred) : PaymentCommand() private object CheckPaymentsTimeout : PaymentCommand() data class PayToOpenResponseCommand(val payToOpenResponse: PayToOpenResponse) : PeerCommand() data class SendPayment(val paymentId: UUID, val amount: MilliSatoshi, val recipient: PublicKey, val paymentRequest: PaymentRequest, val trampolineFeesOverride: List? = null) : PaymentCommand() { @@ -65,7 +64,6 @@ data class SendPayment(val paymentId: UUID, val amount: MilliSatoshi, val recipi data class PurgeExpiredPayments(val fromCreatedAt: Long, val toCreatedAt: Long) : PaymentCommand() sealed class PeerEvent -data class PaymentRequestGenerated(val receivePayment: ReceivePayment, val request: String) : PeerEvent() data class PaymentReceived(val incomingPayment: IncomingPayment, val received: IncomingPayment.Received) : PeerEvent() data class PaymentProgress(val request: SendPayment, val fees: MilliSatoshi) : PeerEvent() data class PaymentNotSent(val request: SendPayment, val reason: OutgoingPaymentFailure) : PeerEvent() @@ -478,16 +476,29 @@ class Peer( } } - suspend fun createInvoice(paymentPreimage: ByteVector32, amount: MilliSatoshi?, description: Either, expirySeconds: Long? = null) { - val command = ReceivePayment( - paymentPreimage = paymentPreimage, - amount = amount, - description = description, - expirySeconds = expirySeconds, - result = CompletableDeferred() + suspend fun createInvoice(paymentPreimage: ByteVector32, amount: MilliSatoshi?, description: Either, expirySeconds: Long? = null): PaymentRequest { + // we add one extra hop which uses a virtual channel with a "peer id", using the highest remote fees and expiry across all + // channels to maximize the likelihood of success on the first payment attempt + val remoteChannelUpdates = _channels.values.mapNotNull { channelState -> + when (channelState) { + is Normal -> channelState.remoteChannelUpdate + is Offline -> (channelState.state as? Normal)?.remoteChannelUpdate + is Syncing -> (channelState.state as? Normal)?.remoteChannelUpdate + else -> null + } + } + val extraHops = listOf( + listOf( + PaymentRequest.TaggedField.ExtraHop( + nodeId = walletParams.trampolineNode.id, + shortChannelId = ShortChannelId.peerId(nodeParams.nodeId), + feeBase = remoteChannelUpdates.maxOfOrNull { it.feeBaseMsat } ?: walletParams.invoiceDefaultRoutingFees.feeBase, + feeProportionalMillionths = remoteChannelUpdates.maxOfOrNull { it.feeProportionalMillionths } ?: walletParams.invoiceDefaultRoutingFees.feeProportional, + cltvExpiryDelta = remoteChannelUpdates.maxOfOrNull { it.cltvExpiryDelta } ?: walletParams.invoiceDefaultRoutingFees.cltvExpiryDelta + ) + ) ) - send(command) - command.result.await() + return incomingPaymentHandler.createInvoice(paymentPreimage, amount, description, extraHops, expirySeconds) } @OptIn(DelicateCoroutinesApi::class) @@ -1003,32 +1014,6 @@ class Peer( _channels = _channels + (msg.temporaryChannelId to state1) processActions(msg.temporaryChannelId, actions1) } - is ReceivePayment -> { - // we add one extra hop which uses a virtual channel with a "peer id", using the highest remote fees and expiry across all - // channels to maximize the likelihood of success on the first payment attempt - val remoteChannelUpdates = _channels.values.mapNotNull { channelState -> - when (channelState) { - is Normal -> channelState.remoteChannelUpdate - is Offline -> (channelState.state as? Normal)?.remoteChannelUpdate - is Syncing -> (channelState.state as? Normal)?.remoteChannelUpdate - else -> null - } - } - val extraHops = listOf( - listOf( - PaymentRequest.TaggedField.ExtraHop( - nodeId = walletParams.trampolineNode.id, - shortChannelId = ShortChannelId.peerId(nodeParams.nodeId), - feeBase = remoteChannelUpdates.maxOfOrNull { it.feeBaseMsat } ?: walletParams.invoiceDefaultRoutingFees.feeBase, - feeProportionalMillionths = remoteChannelUpdates.maxOfOrNull { it.feeProportionalMillionths } ?: walletParams.invoiceDefaultRoutingFees.feeProportional, - cltvExpiryDelta = remoteChannelUpdates.maxOfOrNull { it.cltvExpiryDelta } ?: walletParams.invoiceDefaultRoutingFees.cltvExpiryDelta - ) - ) - ) - val pr = incomingPaymentHandler.createInvoice(cmd.paymentPreimage, cmd.amount, cmd.description, extraHops, cmd.expirySeconds) - _eventsFlow.emit(PaymentRequestGenerated(cmd, pr.write())) - cmd.result.complete(pr) - } is PayToOpenResponseCommand -> { logger.info { "sending ${cmd.payToOpenResponse::class.simpleName}" } sendToPeer(cmd.payToOpenResponse) diff --git a/src/commonTest/kotlin/fr/acinq/lightning/io/peer/PeerTest.kt b/src/commonTest/kotlin/fr/acinq/lightning/io/peer/PeerTest.kt index a2475b1c1..7013c5eb2 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/io/peer/PeerTest.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/io/peer/PeerTest.kt @@ -27,6 +27,7 @@ import fr.acinq.lightning.utils.* import fr.acinq.lightning.wire.* import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map import kotlin.test.* class PeerTest : LightningTestSuite() { @@ -459,9 +460,7 @@ class PeerTest : LightningTestSuite() { val bob = newPeer(nodeParams, walletParams) run { - val deferredInvoice = CompletableDeferred() - bob.send(ReceivePayment(randomBytes32(), 1.msat, Either.Left("A description: \uD83D\uDE2C"), 3600L * 3, deferredInvoice)) - val invoice = deferredInvoice.await() + val invoice = bob.createInvoice(randomBytes32(), 1.msat, Either.Left("A description: \uD83D\uDE2C"), 3600L * 3) assertEquals(1.msat, invoice.amount) assertEquals(3600L * 3, invoice.expirySeconds) assertEquals("A description: \uD83D\uDE2C", invoice.description) @@ -477,9 +476,7 @@ class PeerTest : LightningTestSuite() { val (_, bob, _, _) = newPeers(this, nodeParams, walletParams, listOf(alice0 to bob0), automateMessaging = false) run { - val deferredInvoice = CompletableDeferred() - bob.send(ReceivePayment(randomBytes32(), 15_000_000.msat, Either.Left("default routing hints"), null, deferredInvoice)) - val invoice = deferredInvoice.await() + val invoice = bob.createInvoice(randomBytes32(), 15_000_000.msat, Either.Left("default routing hints"), null) // The routing hint uses default values since no channel update has been sent by Alice yet. assertEquals(1, invoice.routingInfo.size) assertEquals(1, invoice.routingInfo[0].hints.size) @@ -490,10 +487,12 @@ class PeerTest : LightningTestSuite() { val aliceUpdate = Announcements.makeChannelUpdate(alice0.staticParams.nodeParams.chainHash, alice0.ctx.privateKey, alice0.staticParams.remoteNodeId, alice0.state.shortChannelId, CltvExpiryDelta(48), 100.msat, 50.msat, 250, 150_000.msat) bob.forward(aliceUpdate) + // wait until the update is processed + bob.channelsFlow + .map { it.values.first() } + .first { it is Normal && it.remoteChannelUpdate?.feeBaseMsat == 50.msat } - val deferredInvoice = CompletableDeferred() - bob.send(ReceivePayment(randomBytes32(), 5_000_000.msat, Either.Left("updated routing hints"), null, deferredInvoice)) - val invoice = deferredInvoice.await() + val invoice = bob.createInvoice(randomBytes32(), 5_000_000.msat, Either.Left("updated routing hints"), null) // The routing hint uses values from Alice's channel update. assertEquals(1, invoice.routingInfo.size) assertEquals(1, invoice.routingInfo[0].hints.size) @@ -515,9 +514,7 @@ class PeerTest : LightningTestSuite() { ) val (alice, bob, alice2bob, bob2alice) = newPeers(this, nodeParams, walletParams, listOf(alice0 to bob0), automateMessaging = false) - val deferredInvoice = CompletableDeferred() - bob.send(ReceivePayment(randomBytes32(), 15_000_000.msat, Either.Left("test invoice"), null, deferredInvoice)) - val invoice = deferredInvoice.await() + val invoice = bob.createInvoice(randomBytes32(), 15_000_000.msat, Either.Left("test invoice"), null) alice.send(SendPayment(UUID.randomUUID(), invoice.amount!!, alice.remoteNodeId, invoice)) @@ -561,9 +558,7 @@ class PeerTest : LightningTestSuite() { ) val (alice, bob) = newPeers(this, nodeParams, walletParams, listOf(alice0 to bob0)) - val deferredInvoice = CompletableDeferred() - bob.send(ReceivePayment(randomBytes32(), 15_000_000.msat, Either.Left("test invoice"), null, deferredInvoice)) - val invoice = deferredInvoice.await() + val invoice = bob.createInvoice(randomBytes32(), 15_000_000.msat, Either.Left("test invoice"), null) alice.send(SendPayment(UUID.randomUUID(), invoice.amount!!, alice.remoteNodeId, invoice))