Skip to content

Commit 59a4a1d

Browse files
committed
Simplify outgoing payment state machine
We previously supported having multiple channels with our peer, because we didn't yet support splicing. Now that we support splicing, we always have at most one active channel with our peer. This lets us simplify greatly the outgoing payment state machine: payments are always made with a single outgoing HTLC instead of potentially multiple HTLCs (MPP). We don't need any kind of path-finding: we simply need to check the balance of our active channel, if any. We may introduce support for connecting to multiple peers in the future. When that happens, we will still have a single active channel per peer, but we may allow splitting outgoing payments across our peers. We will need to re-work the outgoing payment state machine when this happens, but it is too early to support this now anyway. This refactoring makes it easier to create payment onion, by creating the trampoline onion *and* the outer onion in the same function call. This will make it simpler to migrate to the version of trampoline that is currently specified in lightning/bolts#836 where some fields will be included in the payment onion instead of the trampoline onion.
1 parent 969560e commit 59a4a1d

File tree

12 files changed

+942
-1799
lines changed

12 files changed

+942
-1799
lines changed

src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelException.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,11 @@ data class CannotAffordFirstCommitFees (override val channelId: Byte
8383
data class CannotAffordFees (override val channelId: ByteVector32, val missing: Satoshi, val reserve: Satoshi, val fees: Satoshi) : ChannelException(channelId, "can't pay the fee: missing=$missing reserve=$reserve fees=$fees")
8484
data class CannotSignWithoutChanges (override val channelId: ByteVector32) : ChannelException(channelId, "cannot sign when there are no change")
8585
data class CannotSignBeforeRevocation (override val channelId: ByteVector32) : ChannelException(channelId, "cannot sign until next revocation hash is received")
86+
data class CannotSignDisconnected (override val channelId: ByteVector32) : ChannelException(channelId, "disconnected before signing outgoing payments")
8687
data class UnexpectedRevocation (override val channelId: ByteVector32) : ChannelException(channelId, "received unexpected RevokeAndAck message")
8788
data class InvalidRevocation (override val channelId: ByteVector32) : ChannelException(channelId, "invalid revocation")
8889
data class InvalidFailureCode (override val channelId: ByteVector32) : ChannelException(channelId, "UpdateFailMalformedHtlc message doesn't have BADONION bit set")
90+
data class CannotDecryptFailure (override val channelId: ByteVector32, val details: String) : ChannelException(channelId, "cannot decrypt failure message: $details")
8991
data class PleasePublishYourCommitment (override val channelId: ByteVector32) : ChannelException(channelId, "please publish your local commitment")
9092
data class CommandUnavailableInThisState (override val channelId: ByteVector32, val state: String) : ChannelException(channelId, "cannot execute command in state=$state")
9193
data class ForbiddenDuringSplice (override val channelId: ByteVector32, val command: String?) : ChannelException(channelId, "cannot process $command while splicing")

src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -812,12 +812,7 @@ class Peer(
812812
is ChannelAction.ProcessIncomingHtlc -> processIncomingPayment(Either.Right(action.add))
813813
is ChannelAction.ProcessCmdRes.NotExecuted -> logger.warning(action.t) { "command not executed" }
814814
is ChannelAction.ProcessCmdRes.AddFailed -> {
815-
when (val result = outgoingPaymentHandler.processAddFailed(actualChannelId, action, _channels)) {
816-
is OutgoingPaymentHandler.Progress -> {
817-
_eventsFlow.emit(PaymentProgress(result.request, result.fees))
818-
result.actions.forEach { input.send(it) }
819-
}
820-
815+
when (val result = outgoingPaymentHandler.processAddFailed(actualChannelId, action)) {
821816
is OutgoingPaymentHandler.Failure -> _eventsFlow.emit(PaymentNotSent(result.request, result.failure))
822817
null -> logger.debug { "non-final error, more partial payments are still pending: ${action.error.message}" }
823818
}
@@ -838,7 +833,6 @@ class Peer(
838833
is ChannelAction.ProcessCmdRes.AddSettledFulfill -> {
839834
when (val result = outgoingPaymentHandler.processAddSettled(action)) {
840835
is OutgoingPaymentHandler.Success -> _eventsFlow.emit(PaymentSent(result.request, result.payment))
841-
is OutgoingPaymentHandler.PreimageReceived -> logger.debug(mapOf("paymentId" to result.request.paymentId)) { "payment preimage received: ${result.preimage}" }
842836
null -> logger.debug { "unknown payment" }
843837
}
844838
}

src/commonMain/kotlin/fr/acinq/lightning/payment/OutgoingPaymentHandler.kt

Lines changed: 212 additions & 327 deletions
Large diffs are not rendered by default.

src/commonMain/kotlin/fr/acinq/lightning/payment/OutgoingPaymentPacket.kt

Lines changed: 94 additions & 98 deletions
Large diffs are not rendered by default.

src/commonMain/kotlin/fr/acinq/lightning/payment/RouteCalculation.kt

Lines changed: 0 additions & 61 deletions
This file was deleted.

src/commonMain/kotlin/fr/acinq/lightning/wire/PaymentOnion.kt

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,7 @@ import fr.acinq.bitcoin.io.Input
99
import fr.acinq.bitcoin.io.Output
1010
import fr.acinq.bitcoin.utils.Either
1111
import fr.acinq.bitcoin.utils.flatMap
12-
import fr.acinq.lightning.CltvExpiry
13-
import fr.acinq.lightning.CltvExpiryDelta
14-
import fr.acinq.lightning.MilliSatoshi
15-
import fr.acinq.lightning.ShortChannelId
12+
import fr.acinq.lightning.*
1613
import fr.acinq.lightning.payment.Bolt11Invoice
1714
import fr.acinq.lightning.payment.Bolt12Invoice
1815
import fr.acinq.lightning.utils.msat
@@ -498,7 +495,7 @@ object PaymentOnion {
498495
}
499496
}
500497

501-
fun create(amount: MilliSatoshi, totalAmount: MilliSatoshi, expiry: CltvExpiry, targetNodeId: PublicKey, invoice: Bolt11Invoice): RelayToNonTrampolinePayload =
498+
fun create(amount: MilliSatoshi, totalAmount: MilliSatoshi, expiry: CltvExpiry, targetNodeId: PublicKey, invoice: Bolt11Invoice, routingInfo: List<Bolt11Invoice.TaggedField.RoutingInfo>): RelayToNonTrampolinePayload =
502499
RelayToNonTrampolinePayload(
503500
TlvStream(
504501
buildSet {
@@ -508,7 +505,7 @@ object PaymentOnion {
508505
add(OnionPaymentPayloadTlv.PaymentData(invoice.paymentSecret, totalAmount))
509506
invoice.paymentMetadata?.let { add(OnionPaymentPayloadTlv.PaymentMetadata(it)) }
510507
add(OnionPaymentPayloadTlv.InvoiceFeatures(invoice.features.toByteArray().toByteVector()))
511-
add(OnionPaymentPayloadTlv.InvoiceRoutingInfo(invoice.routingInfo.map { it.hints }))
508+
add(OnionPaymentPayloadTlv.InvoiceRoutingInfo(routingInfo.map { it.hints }))
512509
}
513510
)
514511
)
@@ -538,14 +535,14 @@ object PaymentOnion {
538535
}
539536
}
540537

541-
fun create(amount: MilliSatoshi, expiry: CltvExpiry, invoice: Bolt12Invoice): RelayToBlindedPayload =
538+
fun create(amount: MilliSatoshi, expiry: CltvExpiry, features: Features, blindedPaths: List<Bolt12Invoice.Companion.PaymentBlindedContactInfo>): RelayToBlindedPayload =
542539
RelayToBlindedPayload(
543540
TlvStream(
544541
setOf(
545542
OnionPaymentPayloadTlv.AmountToForward(amount),
546543
OnionPaymentPayloadTlv.OutgoingCltv(expiry),
547-
OnionPaymentPayloadTlv.OutgoingBlindedPaths(invoice.blindedPaths),
548-
OnionPaymentPayloadTlv.InvoiceFeatures(invoice.features.toByteArray().toByteVector())
544+
OnionPaymentPayloadTlv.OutgoingBlindedPaths(blindedPaths),
545+
OnionPaymentPayloadTlv.InvoiceFeatures(features.toByteArray().toByteVector())
549546
)
550547
)
551548
)

src/commonTest/kotlin/fr/acinq/lightning/channel/TestsHelper.kt

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import fr.acinq.lightning.json.JsonSerializers
1414
import fr.acinq.lightning.logging.MDCLogger
1515
import fr.acinq.lightning.logging.mdc
1616
import fr.acinq.lightning.payment.OutgoingPaymentPacket
17-
import fr.acinq.lightning.router.ChannelHop
1817
import fr.acinq.lightning.serialization.Serialization
1918
import fr.acinq.lightning.tests.TestConstants
2019
import fr.acinq.lightning.tests.utils.testLoggerFactory
@@ -404,16 +403,11 @@ object TestsHelper {
404403
}
405404

406405
fun makeCmdAdd(amount: MilliSatoshi, destination: PublicKey, currentBlockHeight: Long, paymentPreimage: ByteVector32 = randomBytes32(), paymentId: UUID = UUID.randomUUID()): Pair<ByteVector32, ChannelCommand.Htlc.Add> {
407-
val paymentHash: ByteVector32 = Crypto.sha256(paymentPreimage).toByteVector32()
406+
val paymentHash = Crypto.sha256(paymentPreimage).toByteVector32()
408407
val expiry = CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight)
409-
val dummyKey = PrivateKey(ByteVector32("0101010101010101010101010101010101010101010101010101010101010101")).publicKey()
410-
val dummyUpdate = ChannelUpdate(ByteVector64.Zeroes, BlockHash(ByteVector32.Zeroes), ShortChannelId(144, 0, 0), 0, 0, 0, CltvExpiryDelta(1), 0.msat, 0.msat, 0, null)
411-
val cmd = OutgoingPaymentPacket.buildCommand(
412-
paymentId,
413-
paymentHash,
414-
listOf(ChannelHop(dummyKey, destination, dummyUpdate)),
415-
PaymentOnion.FinalPayload.Standard.createSinglePartPayload(amount, expiry, randomBytes32(), null)
416-
).first.copy(commit = false)
408+
val payload = PaymentOnion.FinalPayload.Standard.createSinglePartPayload(amount, expiry, randomBytes32(), null)
409+
val onion = OutgoingPaymentPacket.buildOnion(listOf(destination), listOf(payload), paymentHash, OnionRoutingPacket.PaymentPacketLength).packet
410+
val cmd = ChannelCommand.Htlc.Add(amount, paymentHash, expiry, onion, paymentId, commit = false)
417411
return Pair(paymentPreimage, cmd)
418412
}
419413

src/commonTest/kotlin/fr/acinq/lightning/io/peer/PeerTest.kt

Lines changed: 26 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -485,101 +485,52 @@ class PeerTest : LightningTestSuite() {
485485

486486
@Test
487487
fun `payment between two nodes -- with disconnection`() = runSuspendTest {
488-
// We create two channels between Alice and Bob to ensure that the payment is split in two parts.
489-
val (aliceChan1, bobChan1) = TestsHelper.reachNormal(aliceFundingAmount = 100_000.sat, bobFundingAmount = 100_000.sat, alicePushAmount = 0.msat, bobPushAmount = 0.msat)
490-
val (aliceChan2, bobChan2) = TestsHelper.reachNormal(aliceFundingAmount = 100_000.sat, bobFundingAmount = 100_000.sat, alicePushAmount = 0.msat, bobPushAmount = 0.msat)
491-
val nodeParams = Pair(aliceChan1.staticParams.nodeParams, bobChan1.staticParams.nodeParams)
488+
val (alice0, bob0) = TestsHelper.reachNormal()
489+
val nodeParams = Pair(alice0.staticParams.nodeParams, bob0.staticParams.nodeParams)
492490
val walletParams = Pair(
493491
// Alice must declare Bob as her trampoline node to enable direct payments.
494-
TestConstants.Alice.walletParams.copy(trampolineNode = NodeUri(nodeParams.second.nodeId, "bob.com", 9735)),
492+
TestConstants.Alice.walletParams.copy(trampolineNode = NodeUri(bob0.staticParams.nodeParams.nodeId, "bob.com", 9735)),
495493
TestConstants.Bob.walletParams
496494
)
497-
// Bob sends a multipart payment to Alice.
498-
val (alice, bob, alice2bob1, bob2alice1) = newPeers(this, nodeParams, walletParams, listOf(aliceChan1 to bobChan1, aliceChan2 to bobChan2), automateMessaging = false)
495+
val (alice, bob, alice2bob1, bob2alice1) = newPeers(this, nodeParams, walletParams, listOf(alice0 to bob0), automateMessaging = false)
499496
val invoice = alice.createInvoice(randomBytes32(), 150_000_000.msat, Either.Left("test invoice"), null)
500497
bob.send(PayInvoice(UUID.randomUUID(), invoice.amount!!, LightningOutgoingPayment.Details.Normal(invoice)))
501498

502-
// Bob sends one HTLC on each channel.
503-
val htlcs = listOf(
504-
bob2alice1.expect<UpdateAddHtlc>(),
505-
bob2alice1.expect<UpdateAddHtlc>(),
506-
)
507-
assertEquals(2, htlcs.map { it.channelId }.toSet().size)
508-
val commitSigsBob = listOf(
509-
bob2alice1.expect<CommitSig>(),
510-
bob2alice1.expect<CommitSig>(),
511-
)
499+
// Bob sends an HTLC to Alice.
500+
alice.forward(bob2alice1.expect<UpdateAddHtlc>())
501+
alice.forward(bob2alice1.expect<CommitSig>())
512502

513-
// We cross-sign the HTLC on the first channel.
514-
run {
515-
val htlc = htlcs.find { it.channelId == aliceChan1.channelId }
516-
assertNotNull(htlc)
517-
alice.forward(htlc)
518-
val commitSigBob = commitSigsBob.find { it.channelId == aliceChan1.channelId }
519-
assertNotNull(commitSigBob)
520-
alice.forward(commitSigBob)
521-
bob.forward(alice2bob1.expect<RevokeAndAck>())
522-
bob.forward(alice2bob1.expect<CommitSig>())
523-
alice.forward(bob2alice1.expect<RevokeAndAck>())
524-
}
525-
// We start cross-signing the HTLC on the second channel.
526-
run {
527-
val htlc = htlcs.find { it.channelId == aliceChan2.channelId }
528-
assertNotNull(htlc)
529-
alice.forward(htlc)
530-
val commitSigBob = commitSigsBob.find { it.channelId == aliceChan2.channelId }
531-
assertNotNull(commitSigBob)
532-
alice.forward(commitSigBob)
533-
bob.forward(alice2bob1.expect<RevokeAndAck>())
534-
bob.forward(alice2bob1.expect<CommitSig>())
535-
bob2alice1.expect<RevokeAndAck>() // Alice doesn't receive Bob's revocation.
536-
}
503+
// We start cross-signing the HTLC.
504+
bob.forward(alice2bob1.expect<RevokeAndAck>())
505+
bob.forward(alice2bob1.expect<CommitSig>())
506+
bob2alice1.expect<RevokeAndAck>() // Alice doesn't receive Bob's revocation.
537507

538-
// We disconnect before Alice receives Bob's revocation on the second channel.
508+
// We disconnect before Alice receives Bob's revocation.
539509
alice.disconnect()
540510
alice.send(Disconnected)
541511
bob.disconnect()
542512
bob.send(Disconnected)
543513

544514
// On reconnection, Bob retransmits its revocation.
545-
val (_, _, alice2bob2, bob2alice2) = connect(this, connectionId = 1, alice, bob, channelsCount = 2, expectChannelReady = false, automateMessaging = false)
515+
val (_, _, alice2bob2, bob2alice2) = connect(this, connectionId = 1, alice, bob, channelsCount = 1, expectChannelReady = false, automateMessaging = false)
546516
alice.forward(bob2alice2.expect<RevokeAndAck>(), connectionId = 1)
547517

548518
// Alice has now received the complete payment and fulfills it.
549-
val fulfills = listOf(
550-
alice2bob2.expect<UpdateFulfillHtlc>(),
551-
alice2bob2.expect<UpdateFulfillHtlc>(),
552-
)
553-
val commitSigsAlice = listOf(
554-
alice2bob2.expect<CommitSig>(),
555-
alice2bob2.expect<CommitSig>(),
556-
)
519+
bob.forward(alice2bob2.expect<UpdateFulfillHtlc>(), connectionId = 1)
520+
bob.forward(alice2bob2.expect<CommitSig>(), connectionId = 1)
521+
alice.forward(bob2alice2.expect<RevokeAndAck>(), connectionId = 1)
522+
bob2alice2.expect<CommitSig>() // Alice doesn't receive Bob's signature.
557523

558-
// We fulfill the first HTLC.
559-
run {
560-
val fulfill = fulfills.find { it.channelId == aliceChan1.channelId }
561-
assertNotNull(fulfill)
562-
bob.forward(fulfill, connectionId = 1)
563-
val commitSigAlice = commitSigsAlice.find { it.channelId == aliceChan1.channelId }
564-
assertNotNull(commitSigAlice)
565-
bob.forward(commitSigAlice, connectionId = 1)
566-
alice.forward(bob2alice2.expect<RevokeAndAck>(), connectionId = 1)
567-
alice.forward(bob2alice2.expect<CommitSig>(), connectionId = 1)
568-
bob.forward(alice2bob2.expect<RevokeAndAck>(), connectionId = 1)
569-
}
524+
// We disconnect before Alice receives Bob's signature.
525+
alice.disconnect()
526+
alice.send(Disconnected)
527+
bob.disconnect()
528+
bob.send(Disconnected)
570529

571-
// We fulfill the second HTLC.
572-
run {
573-
val fulfill = fulfills.find { it.channelId == aliceChan2.channelId }
574-
assertNotNull(fulfill)
575-
bob.forward(fulfill, connectionId = 1)
576-
val commitSigAlice = commitSigsAlice.find { it.channelId == aliceChan2.channelId }
577-
assertNotNull(commitSigAlice)
578-
bob.forward(commitSigAlice, connectionId = 1)
579-
alice.forward(bob2alice2.expect<RevokeAndAck>(), connectionId = 1)
580-
alice.forward(bob2alice2.expect<CommitSig>(), connectionId = 1)
581-
bob.forward(alice2bob2.expect<RevokeAndAck>(), connectionId = 1)
582-
}
530+
// On reconnection, Bob retransmits its signature.
531+
val (_, _, alice2bob3, bob2alice3) = connect(this, connectionId = 2, alice, bob, channelsCount = 1, expectChannelReady = false, automateMessaging = false)
532+
alice.forward(bob2alice3.expect<CommitSig>(), connectionId = 2)
533+
bob.forward(alice2bob3.expect<RevokeAndAck>(), connectionId = 2)
583534

584535
assertEquals(invoice.amount, alice.db.payments.getIncomingPayment(invoice.paymentHash)?.received?.amount)
585536
}

0 commit comments

Comments
 (0)