Skip to content

Commit

Permalink
Rework IncomingPayment model (#722)
Browse files Browse the repository at this point in the history
We move from a flat model with a single `IncomingPayment` class and a combination of `Origin` + `ReceivedWith` parts to a hierarchical model.

This new model:
- makes incoming/outgoing databases symmetrical
- removes impossible combinations allowed in the previous model (e.g. an on-chain origin with a fee-credit part)
- removes hacks required for the handling of on-chain deposits, which were shoe-horned in what was originally a lightning-only model

Before:
```
Origin
   |
   `--- Invoice
   |
   `--- Offer
   |
   `--- SwapIn
   |
   `--- OnChain

ReceivedWith
      |
      `--- LightningPayment
      |
      `--- AddedToFeeCredit
      |
      `--- OnChainIncomingPayment
                    |
                    `--- NewChannel
                    |
                    `--- SpliceIn
```

After:
```
IncomingPayment
      |
      `--- LightningIncomingPayment
      |             |
      |             `--- Bolt11IncomingPayment
      |             |
      |             `--- Bolt12IncomingPayment
      |
      `--- OnChainIncomingPayment
                    |
                    `--- NewChannelIncomingPayment
                    |
                    `--- SpliceInIncomingPayment
                    |
                    `--- LegacySwapInIncomingPayment
                    |
                    `--- LegacyPayToOpenIncomingPayment

LightningIncomingPayment.Part
      |
      `--- Htlc
      |
      `--- FeeCredit
```

The handling of backward compatible data is tricky, especially for legacy pay-to-open/pay-to-splice, which can be a mix of lightning parts and on-chain parts, and either Bolt11 or Bolt12. Note that `Legacy*` classes are not used at all within `lightning-kmp`, they are just meant to handle pre-existing data in the database.

payment                        | old model                                     | new model
-------------------------------|-----------------------------------------------|------------------------
Plain Lightning Bolt11         | Origin=Invoice, ReceivedWith=LightningPayment | Bolt11IncomingPayment
Plain Lightning Bolt12         | Origin=Offer, ReceivedWith=LightningPayment   | Bolt12IncomingPayment
Pre-otf pay-to-open Bolt11     | Origin=Invoice, ReceivedWith=NewChannel       | LegacyPayToOpenIncomingPayment
Pre-otf pay-to-splice Bolt11   | Origin=Invoice, ReceivedWith=SpliceIn         | LegacyPayToOpenIncomingPayment
Pre-otf pay-to-open Bolt12     | Origin=Offer, ReceivedWith=NewChannel         | LegacyPayToOpenIncomingPayment
Pre-otf pay-to-splice Bolt12   | Origin=Offer, ReceivedWith=SpliceIn           | LegacyPayToOpenIncomingPayment
Legacy trusted swap-in         | Origin=SwapIn, ReceivedWith=NewChannel        | LegacySwapInIncomingPayment
Swap-in potentiam open         | Origin=OnChain, ReceivedWith=NewChannel       | NewChannelIncomingPayment
Swap-in potentiam splice       | Origin=OnChain, ReceivedWith=SpliceIn         | SpliceInIncomingPayment
  • Loading branch information
pm47 authored Dec 19, 2024
1 parent 7cfdb96 commit 6aab996
Show file tree
Hide file tree
Showing 10 changed files with 458 additions and 429 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import fr.acinq.lightning.channel.states.ChannelStateWithCommitments
import fr.acinq.lightning.channel.states.Normal
import fr.acinq.lightning.channel.states.WaitForFundingCreated
import fr.acinq.lightning.db.IncomingPayment
import fr.acinq.lightning.db.LightningIncomingPayment
import fr.acinq.lightning.db.OutgoingPayment
import fr.acinq.lightning.utils.sum
import fr.acinq.lightning.wire.Init
Expand Down Expand Up @@ -70,9 +71,6 @@ sealed interface SensitiveTaskEvents : NodeEvents {
data object UpgradeRequired : NodeEvents

sealed interface PaymentEvents : NodeEvents {
data class PaymentReceived(val paymentHash: ByteVector32, val receivedWith: List<IncomingPayment.ReceivedWith>) : PaymentEvents {
val amount: MilliSatoshi = receivedWith.map { it.amountReceived }.sum()
val fees: MilliSatoshi = receivedWith.map { it.fees }.sum()
}
data class PaymentReceived(val payment: IncomingPayment) : PaymentEvents
data class PaymentSent(val payment: OutgoingPayment) : PaymentEvents
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import fr.acinq.lightning.channel.states.PersistedChannelState
import fr.acinq.lightning.db.ChannelClosingType
import fr.acinq.lightning.transactions.Transactions
import fr.acinq.lightning.utils.UUID
import fr.acinq.lightning.utils.toMilliSatoshi
import fr.acinq.lightning.wire.*

/** Channel Actions (outputs produced by the state machine). */
Expand Down Expand Up @@ -80,7 +79,7 @@ sealed class ChannelAction {
abstract val txId: TxId
abstract val localInputs: Set<OutPoint>
data class ViaNewChannel(val amountReceived: MilliSatoshi, val serviceFee: MilliSatoshi, val miningFee: Satoshi, override val localInputs: Set<OutPoint>, override val txId: TxId, override val origin: Origin?) : StoreIncomingPayment()
data class ViaSpliceIn(val amountReceived: MilliSatoshi, val serviceFee: MilliSatoshi, val miningFee: Satoshi, override val localInputs: Set<OutPoint>, override val txId: TxId, override val origin: Origin?) : StoreIncomingPayment()
data class ViaSpliceIn(val amountReceived: MilliSatoshi, val miningFee: Satoshi, override val localInputs: Set<OutPoint>, override val txId: TxId, override val origin: Origin?) : StoreIncomingPayment()
}
/** Payment sent through on-chain operations (channel close or splice-out) */
sealed class StoreOutgoingPayment : Storage() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -883,7 +883,6 @@ data class Normal(
if (action.fundingTx.sharedTx.tx.localInputs.isNotEmpty()) add(
ChannelAction.Storage.StoreIncomingPayment.ViaSpliceIn(
amountReceived = action.fundingTx.sharedTx.tx.localInputs.map { i -> i.txOut.amount }.sum().toMilliSatoshi() - action.fundingTx.sharedTx.tx.localFees,
serviceFee = 0.msat,
miningFee = action.fundingTx.sharedTx.tx.localFees.truncateToSatoshi(),
localInputs = action.fundingTx.sharedTx.tx.localInputs.map { it.outPoint }.toSet(),
txId = action.fundingTx.txId,
Expand Down
253 changes: 159 additions & 94 deletions modules/core/src/commonMain/kotlin/fr/acinq/lightning/db/PaymentsDb.kt

Large diffs are not rendered by default.

38 changes: 31 additions & 7 deletions modules/core/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,6 @@ data class SendOnTheFlyFundingMessage(val message: OnTheFlyFundingMessage) : Pee

sealed class PeerEvent

@Deprecated("Replaced by NodeEvents", replaceWith = ReplaceWith("PaymentEvents.PaymentReceived", "fr.acinq.lightning.PaymentEvents"))
data class PaymentReceived(val incomingPayment: IncomingPayment, val received: IncomingPayment.Received) : PeerEvent()
data class PaymentProgress(val request: SendPayment, val fees: MilliSatoshi) : PeerEvent()
sealed class SendPaymentResult : PeerEvent() {
abstract val request: SendPayment
Expand Down Expand Up @@ -847,8 +845,36 @@ class Peer(
action.htlcs.forEach { db.channels.addHtlcInfo(actualChannelId, it.commitmentNumber, it.paymentHash, it.cltvExpiry) }
}
is ChannelAction.Storage.StoreIncomingPayment -> {
logger.info { "storing incoming payment $action" }
incomingPaymentHandler.process(actualChannelId, action)
logger.info { "storing $action" }
val payment = when (action) {
is ChannelAction.Storage.StoreIncomingPayment.ViaNewChannel ->
NewChannelIncomingPayment(
id = UUID.randomUUID(),
amountReceived = action.amountReceived,
serviceFee = action.serviceFee,
miningFee = action.miningFee,
channelId = channelId,
txId = action.txId,
localInputs = action.localInputs,
createdAt = currentTimestampMillis(),
confirmedAt = null,
lockedAt = null,
)
is ChannelAction.Storage.StoreIncomingPayment.ViaSpliceIn ->
SpliceInIncomingPayment(
id = UUID.randomUUID(),
amountReceived = action.amountReceived,
miningFee = action.miningFee,
channelId = channelId,
txId = action.txId,
localInputs = action.localInputs,
createdAt = currentTimestampMillis(),
confirmedAt = null,
lockedAt = null,
)
}
nodeParams._nodeEvents.emit(PaymentEvents.PaymentReceived(payment))
db.payments.addIncomingPayment(payment)
}
is ChannelAction.Storage.StoreOutgoingPayment -> {
logger.info { "storing $action" }
Expand Down Expand Up @@ -938,12 +964,10 @@ class Peer(
}
when (result) {
is IncomingPaymentHandler.ProcessAddResult.Accepted -> {
if ((result.incomingPayment.received?.receivedWith?.size ?: 0) > 1) {
if (result.incomingPayment.parts.size > 1) {
// this was a multi-part payment, we signal that the task is finished
nodeParams._nodeEvents.tryEmit(SensitiveTaskEvents.TaskEnded(SensitiveTaskEvents.TaskIdentifier.IncomingMultiPartPayment(result.incomingPayment.paymentHash)))
}
@Suppress("DEPRECATION")
_eventsFlow.emit(PaymentReceived(result.incomingPayment, result.received))
}
is IncomingPaymentHandler.ProcessAddResult.Pending -> if (result.pendingPayment.parts.size == 1) {
// this is the first part of a multi-part payment, we request to keep the app alive to receive subsequent parts
Expand Down
Loading

0 comments on commit 6aab996

Please sign in to comment.