Skip to content

Commit

Permalink
Simplify outgoing payment state machine
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
t-bast committed Sep 16, 2024
1 parent 969560e commit 59a4a1d
Show file tree
Hide file tree
Showing 12 changed files with 942 additions and 1,799 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,11 @@ data class CannotAffordFirstCommitFees (override val channelId: Byte
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")
data class CannotSignWithoutChanges (override val channelId: ByteVector32) : ChannelException(channelId, "cannot sign when there are no change")
data class CannotSignBeforeRevocation (override val channelId: ByteVector32) : ChannelException(channelId, "cannot sign until next revocation hash is received")
data class CannotSignDisconnected (override val channelId: ByteVector32) : ChannelException(channelId, "disconnected before signing outgoing payments")
data class UnexpectedRevocation (override val channelId: ByteVector32) : ChannelException(channelId, "received unexpected RevokeAndAck message")
data class InvalidRevocation (override val channelId: ByteVector32) : ChannelException(channelId, "invalid revocation")
data class InvalidFailureCode (override val channelId: ByteVector32) : ChannelException(channelId, "UpdateFailMalformedHtlc message doesn't have BADONION bit set")
data class CannotDecryptFailure (override val channelId: ByteVector32, val details: String) : ChannelException(channelId, "cannot decrypt failure message: $details")
data class PleasePublishYourCommitment (override val channelId: ByteVector32) : ChannelException(channelId, "please publish your local commitment")
data class CommandUnavailableInThisState (override val channelId: ByteVector32, val state: String) : ChannelException(channelId, "cannot execute command in state=$state")
data class ForbiddenDuringSplice (override val channelId: ByteVector32, val command: String?) : ChannelException(channelId, "cannot process $command while splicing")
Expand Down
8 changes: 1 addition & 7 deletions src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -812,12 +812,7 @@ class Peer(
is ChannelAction.ProcessIncomingHtlc -> processIncomingPayment(Either.Right(action.add))
is ChannelAction.ProcessCmdRes.NotExecuted -> logger.warning(action.t) { "command not executed" }
is ChannelAction.ProcessCmdRes.AddFailed -> {
when (val result = outgoingPaymentHandler.processAddFailed(actualChannelId, action, _channels)) {
is OutgoingPaymentHandler.Progress -> {
_eventsFlow.emit(PaymentProgress(result.request, result.fees))
result.actions.forEach { input.send(it) }
}

when (val result = outgoingPaymentHandler.processAddFailed(actualChannelId, action)) {
is OutgoingPaymentHandler.Failure -> _eventsFlow.emit(PaymentNotSent(result.request, result.failure))
null -> logger.debug { "non-final error, more partial payments are still pending: ${action.error.message}" }
}
Expand All @@ -838,7 +833,6 @@ class Peer(
is ChannelAction.ProcessCmdRes.AddSettledFulfill -> {
when (val result = outgoingPaymentHandler.processAddSettled(action)) {
is OutgoingPaymentHandler.Success -> _eventsFlow.emit(PaymentSent(result.request, result.payment))
is OutgoingPaymentHandler.PreimageReceived -> logger.debug(mapOf("paymentId" to result.request.paymentId)) { "payment preimage received: ${result.preimage}" }
null -> logger.debug { "unknown payment" }
}
}
Expand Down

Large diffs are not rendered by default.

Large diffs are not rendered by default.

This file was deleted.

15 changes: 6 additions & 9 deletions src/commonMain/kotlin/fr/acinq/lightning/wire/PaymentOnion.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,7 @@ import fr.acinq.bitcoin.io.Input
import fr.acinq.bitcoin.io.Output
import fr.acinq.bitcoin.utils.Either
import fr.acinq.bitcoin.utils.flatMap
import fr.acinq.lightning.CltvExpiry
import fr.acinq.lightning.CltvExpiryDelta
import fr.acinq.lightning.MilliSatoshi
import fr.acinq.lightning.ShortChannelId
import fr.acinq.lightning.*
import fr.acinq.lightning.payment.Bolt11Invoice
import fr.acinq.lightning.payment.Bolt12Invoice
import fr.acinq.lightning.utils.msat
Expand Down Expand Up @@ -498,7 +495,7 @@ object PaymentOnion {
}
}

fun create(amount: MilliSatoshi, totalAmount: MilliSatoshi, expiry: CltvExpiry, targetNodeId: PublicKey, invoice: Bolt11Invoice): RelayToNonTrampolinePayload =
fun create(amount: MilliSatoshi, totalAmount: MilliSatoshi, expiry: CltvExpiry, targetNodeId: PublicKey, invoice: Bolt11Invoice, routingInfo: List<Bolt11Invoice.TaggedField.RoutingInfo>): RelayToNonTrampolinePayload =
RelayToNonTrampolinePayload(
TlvStream(
buildSet {
Expand All @@ -508,7 +505,7 @@ object PaymentOnion {
add(OnionPaymentPayloadTlv.PaymentData(invoice.paymentSecret, totalAmount))
invoice.paymentMetadata?.let { add(OnionPaymentPayloadTlv.PaymentMetadata(it)) }
add(OnionPaymentPayloadTlv.InvoiceFeatures(invoice.features.toByteArray().toByteVector()))
add(OnionPaymentPayloadTlv.InvoiceRoutingInfo(invoice.routingInfo.map { it.hints }))
add(OnionPaymentPayloadTlv.InvoiceRoutingInfo(routingInfo.map { it.hints }))
}
)
)
Expand Down Expand Up @@ -538,14 +535,14 @@ object PaymentOnion {
}
}

fun create(amount: MilliSatoshi, expiry: CltvExpiry, invoice: Bolt12Invoice): RelayToBlindedPayload =
fun create(amount: MilliSatoshi, expiry: CltvExpiry, features: Features, blindedPaths: List<Bolt12Invoice.Companion.PaymentBlindedContactInfo>): RelayToBlindedPayload =
RelayToBlindedPayload(
TlvStream(
setOf(
OnionPaymentPayloadTlv.AmountToForward(amount),
OnionPaymentPayloadTlv.OutgoingCltv(expiry),
OnionPaymentPayloadTlv.OutgoingBlindedPaths(invoice.blindedPaths),
OnionPaymentPayloadTlv.InvoiceFeatures(invoice.features.toByteArray().toByteVector())
OnionPaymentPayloadTlv.OutgoingBlindedPaths(blindedPaths),
OnionPaymentPayloadTlv.InvoiceFeatures(features.toByteArray().toByteVector())
)
)
)
Expand Down
14 changes: 4 additions & 10 deletions src/commonTest/kotlin/fr/acinq/lightning/channel/TestsHelper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import fr.acinq.lightning.json.JsonSerializers
import fr.acinq.lightning.logging.MDCLogger
import fr.acinq.lightning.logging.mdc
import fr.acinq.lightning.payment.OutgoingPaymentPacket
import fr.acinq.lightning.router.ChannelHop
import fr.acinq.lightning.serialization.Serialization
import fr.acinq.lightning.tests.TestConstants
import fr.acinq.lightning.tests.utils.testLoggerFactory
Expand Down Expand Up @@ -404,16 +403,11 @@ object TestsHelper {
}

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

Expand Down
101 changes: 26 additions & 75 deletions src/commonTest/kotlin/fr/acinq/lightning/io/peer/PeerTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -485,101 +485,52 @@ class PeerTest : LightningTestSuite() {

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

// Bob sends one HTLC on each channel.
val htlcs = listOf(
bob2alice1.expect<UpdateAddHtlc>(),
bob2alice1.expect<UpdateAddHtlc>(),
)
assertEquals(2, htlcs.map { it.channelId }.toSet().size)
val commitSigsBob = listOf(
bob2alice1.expect<CommitSig>(),
bob2alice1.expect<CommitSig>(),
)
// Bob sends an HTLC to Alice.
alice.forward(bob2alice1.expect<UpdateAddHtlc>())
alice.forward(bob2alice1.expect<CommitSig>())

// We cross-sign the HTLC on the first channel.
run {
val htlc = htlcs.find { it.channelId == aliceChan1.channelId }
assertNotNull(htlc)
alice.forward(htlc)
val commitSigBob = commitSigsBob.find { it.channelId == aliceChan1.channelId }
assertNotNull(commitSigBob)
alice.forward(commitSigBob)
bob.forward(alice2bob1.expect<RevokeAndAck>())
bob.forward(alice2bob1.expect<CommitSig>())
alice.forward(bob2alice1.expect<RevokeAndAck>())
}
// We start cross-signing the HTLC on the second channel.
run {
val htlc = htlcs.find { it.channelId == aliceChan2.channelId }
assertNotNull(htlc)
alice.forward(htlc)
val commitSigBob = commitSigsBob.find { it.channelId == aliceChan2.channelId }
assertNotNull(commitSigBob)
alice.forward(commitSigBob)
bob.forward(alice2bob1.expect<RevokeAndAck>())
bob.forward(alice2bob1.expect<CommitSig>())
bob2alice1.expect<RevokeAndAck>() // Alice doesn't receive Bob's revocation.
}
// We start cross-signing the HTLC.
bob.forward(alice2bob1.expect<RevokeAndAck>())
bob.forward(alice2bob1.expect<CommitSig>())
bob2alice1.expect<RevokeAndAck>() // Alice doesn't receive Bob's revocation.

// We disconnect before Alice receives Bob's revocation on the second channel.
// We disconnect before Alice receives Bob's revocation.
alice.disconnect()
alice.send(Disconnected)
bob.disconnect()
bob.send(Disconnected)

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

// Alice has now received the complete payment and fulfills it.
val fulfills = listOf(
alice2bob2.expect<UpdateFulfillHtlc>(),
alice2bob2.expect<UpdateFulfillHtlc>(),
)
val commitSigsAlice = listOf(
alice2bob2.expect<CommitSig>(),
alice2bob2.expect<CommitSig>(),
)
bob.forward(alice2bob2.expect<UpdateFulfillHtlc>(), connectionId = 1)
bob.forward(alice2bob2.expect<CommitSig>(), connectionId = 1)
alice.forward(bob2alice2.expect<RevokeAndAck>(), connectionId = 1)
bob2alice2.expect<CommitSig>() // Alice doesn't receive Bob's signature.

// We fulfill the first HTLC.
run {
val fulfill = fulfills.find { it.channelId == aliceChan1.channelId }
assertNotNull(fulfill)
bob.forward(fulfill, connectionId = 1)
val commitSigAlice = commitSigsAlice.find { it.channelId == aliceChan1.channelId }
assertNotNull(commitSigAlice)
bob.forward(commitSigAlice, connectionId = 1)
alice.forward(bob2alice2.expect<RevokeAndAck>(), connectionId = 1)
alice.forward(bob2alice2.expect<CommitSig>(), connectionId = 1)
bob.forward(alice2bob2.expect<RevokeAndAck>(), connectionId = 1)
}
// We disconnect before Alice receives Bob's signature.
alice.disconnect()
alice.send(Disconnected)
bob.disconnect()
bob.send(Disconnected)

// We fulfill the second HTLC.
run {
val fulfill = fulfills.find { it.channelId == aliceChan2.channelId }
assertNotNull(fulfill)
bob.forward(fulfill, connectionId = 1)
val commitSigAlice = commitSigsAlice.find { it.channelId == aliceChan2.channelId }
assertNotNull(commitSigAlice)
bob.forward(commitSigAlice, connectionId = 1)
alice.forward(bob2alice2.expect<RevokeAndAck>(), connectionId = 1)
alice.forward(bob2alice2.expect<CommitSig>(), connectionId = 1)
bob.forward(alice2bob2.expect<RevokeAndAck>(), connectionId = 1)
}
// On reconnection, Bob retransmits its signature.
val (_, _, alice2bob3, bob2alice3) = connect(this, connectionId = 2, alice, bob, channelsCount = 1, expectChannelReady = false, automateMessaging = false)
alice.forward(bob2alice3.expect<CommitSig>(), connectionId = 2)
bob.forward(alice2bob3.expect<RevokeAndAck>(), connectionId = 2)

assertEquals(invoice.amount, alice.db.payments.getIncomingPayment(invoice.paymentHash)?.received?.amount)
}
Expand Down
Loading

0 comments on commit 59a4a1d

Please sign in to comment.