Skip to content

Commit 23c1dc7

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 40dd1dd commit 23c1dc7

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
@@ -82,9 +82,11 @@ data class CannotAffordFirstCommitFees (override val channelId: Byte
8282
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")
8383
data class CannotSignWithoutChanges (override val channelId: ByteVector32) : ChannelException(channelId, "cannot sign when there are no change")
8484
data class CannotSignBeforeRevocation (override val channelId: ByteVector32) : ChannelException(channelId, "cannot sign until next revocation hash is received")
85+
data class CannotSignDisconnected (override val channelId: ByteVector32) : ChannelException(channelId, "disconnected before signing outgoing payments")
8586
data class UnexpectedRevocation (override val channelId: ByteVector32) : ChannelException(channelId, "received unexpected RevokeAndAck message")
8687
data class InvalidRevocation (override val channelId: ByteVector32) : ChannelException(channelId, "invalid revocation")
8788
data class InvalidFailureCode (override val channelId: ByteVector32) : ChannelException(channelId, "UpdateFailMalformedHtlc message doesn't have BADONION bit set")
89+
data class CannotDecryptFailure (override val channelId: ByteVector32, val details: String) : ChannelException(channelId, "cannot decrypt failure message: $details")
8890
data class PleasePublishYourCommitment (override val channelId: ByteVector32) : ChannelException(channelId, "please publish your local commitment")
8991
data class CommandUnavailableInThisState (override val channelId: ByteVector32, val state: String) : ChannelException(channelId, "cannot execute command in state=$state")
9092
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
@@ -787,12 +787,7 @@ class Peer(
787787
is ChannelAction.ProcessIncomingHtlc -> processIncomingPayment(Either.Right(action.add))
788788
is ChannelAction.ProcessCmdRes.NotExecuted -> logger.warning(action.t) { "command not executed" }
789789
is ChannelAction.ProcessCmdRes.AddFailed -> {
790-
when (val result = outgoingPaymentHandler.processAddFailed(actualChannelId, action, _channels)) {
791-
is OutgoingPaymentHandler.Progress -> {
792-
_eventsFlow.emit(PaymentProgress(result.request, result.fees))
793-
result.actions.forEach { input.send(it) }
794-
}
795-
790+
when (val result = outgoingPaymentHandler.processAddFailed(actualChannelId, action)) {
796791
is OutgoingPaymentHandler.Failure -> _eventsFlow.emit(PaymentNotSent(result.request, result.failure))
797792
null -> logger.debug { "non-final error, more partial payments are still pending: ${action.error.message}" }
798793
}
@@ -813,7 +808,6 @@ class Peer(
813808
is ChannelAction.ProcessCmdRes.AddSettledFulfill -> {
814809
when (val result = outgoingPaymentHandler.processAddSettled(action)) {
815810
is OutgoingPaymentHandler.Success -> _eventsFlow.emit(PaymentSent(result.request, result.payment))
816-
is OutgoingPaymentHandler.PreimageReceived -> logger.debug(mapOf("paymentId" to result.request.paymentId)) { "payment preimage received: ${result.preimage}" }
817811
null -> logger.debug { "unknown payment" }
818812
}
819813
}

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
@@ -484,101 +484,52 @@ class PeerTest : LightningTestSuite() {
484484

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

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

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

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

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

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

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

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

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

0 commit comments

Comments
 (0)