Skip to content

Commit

Permalink
remove the LightningIncomingPayment.Received class
Browse files Browse the repository at this point in the history
Instead, the `parts` are a direct member of `LightningIncomingPayment`.
It makes the object simpler to manipulate and update. Each of them has
a `receivedAt` timestamp, offering more precision.
  • Loading branch information
pm47 committed Dec 12, 2024
1 parent 600c04d commit a110612
Show file tree
Hide file tree
Showing 7 changed files with 169 additions and 183 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ interface IncomingPaymentsDb {
*
* @param parts Is a list containing the payment parts holding the incoming amount.
*/
suspend fun receiveLightningPayment(paymentHash: ByteVector32, parts: List<LightningIncomingPayment.Received.Part>, receivedAt: Long = currentTimestampMillis())
suspend fun receiveLightningPayment(paymentHash: ByteVector32, parts: List<LightningIncomingPayment.Part>)

/** List expired unpaid normal payments created within specified time range (with the most recent payments first). */
suspend fun listLightningExpiredPayments(fromCreatedAt: Long = 0, toCreatedAt: Long = currentTimestampMillis()): List<LightningIncomingPayment>
Expand Down Expand Up @@ -129,46 +129,40 @@ sealed class LightningIncomingPayment(val paymentPreimage: ByteVector32) : Incom

override val id: UUID = UUID.fromBytes(paymentHash.toByteArray().copyOf(16))

/** Funds received for this payment, null if no funds have been received yet. */
abstract val received: Received?
/** Funds received for this payment, empty if no funds have been received yet. */
abstract val parts: List<Part>

/** This timestamp will be defined when the received amount is usable for spending. */
override val completedAt: Long? get() = received?.receivedAt
override val completedAt: Long? get() = parts.maxByOrNull { it.receivedAt }?.receivedAt

/** Total fees paid to receive this payment. */
override val fees: MilliSatoshi get() = received?.fees ?: 0.msat
override val fees: MilliSatoshi get() = parts.map { it.fees }.sum()

/** Total amount actually received for this payment after applying the fees. If someone sent you 500 and the fee was 10, this amount will be 490. */
override val amountReceived: MilliSatoshi get() = received?.amount ?: 0.msat
override val amountReceived: MilliSatoshi get() = parts.map { it.amountReceived }.sum()

data class Received(val parts: List<Part>, val receivedAt: Long = currentTimestampMillis()) {
/** Total amount received after applying the fees. */
val amount: MilliSatoshi = parts.map { it.amountReceived }.sum()

/** Fees applied to receive this payment. */
val fees: MilliSatoshi = parts.map { it.fees }.sum()
sealed class Part {
/** Amount received for this part after applying the fees. This is the final amount we can use. */
abstract val amountReceived: MilliSatoshi

sealed class Part {
/** Amount received for this part after applying the fees. This is the final amount we can use. */
abstract val amountReceived: MilliSatoshi
/** Fees applied to receive this part.*/
abstract val fees: MilliSatoshi

/** Fees applied to receive this part.*/
abstract val fees: MilliSatoshi
abstract val receivedAt: Long

/** Payment was received via existing lightning channels. */
data class Htlc(override val amountReceived: MilliSatoshi, val channelId: ByteVector32, val htlcId: Long, val fundingFee: LiquidityAds.FundingFee?) : Part() {
// If there is no funding fee, the fees are paid by the sender for lightning payments.
override val fees: MilliSatoshi = fundingFee?.amount ?: 0.msat
}
/** Payment was received via existing lightning channels. */
data class Htlc(override val amountReceived: MilliSatoshi, val channelId: ByteVector32, val htlcId: Long, val fundingFee: LiquidityAds.FundingFee?, override val receivedAt: Long = currentTimestampMillis()) : Part() {
// If there is no funding fee, the fees are paid by the sender for lightning payments.
override val fees: MilliSatoshi = fundingFee?.amount ?: 0.msat
}

/**
* Payment was added to our fee credit for future on-chain operations (see [fr.acinq.lightning.Feature.FundingFeeCredit]).
* We didn't really receive this amount yet, but we trust our peer to use it for future on-chain operations.
*/
data class FeeCredit(override val amountReceived: MilliSatoshi) : Part() {
// Adding to the fee credit doesn't cost any fees.
override val fees: MilliSatoshi = 0.msat
}
/**
* Payment was added to our fee credit for future on-chain operations (see [fr.acinq.lightning.Feature.FundingFeeCredit]).
* We didn't really receive this amount yet, but we trust our peer to use it for future on-chain operations.
*/
data class FeeCredit(override val amountReceived: MilliSatoshi, override val receivedAt: Long = currentTimestampMillis()) : Part() {
// Adding to the fee credit doesn't cost any fees.
override val fees: MilliSatoshi = 0.msat
}
}

Expand All @@ -177,14 +171,10 @@ sealed class LightningIncomingPayment(val paymentPreimage: ByteVector32) : Incom

companion object {
/** Helper method to facilitate updating child classes */
fun LightningIncomingPayment.addReceivedParts(parts: List<Received.Part>, receivedAt: Long): LightningIncomingPayment {
val newReceived = when (val received = this.received) {
null -> Received(parts, receivedAt)
else -> received.copy(parts = received.parts + parts)
}
fun LightningIncomingPayment.addReceivedParts(parts: List<Part>): LightningIncomingPayment {
return when (this) {
is Bolt11IncomingPayment -> copy(received = newReceived)
is Bolt12IncomingPayment -> copy(received = newReceived)
is Bolt11IncomingPayment -> copy(parts = this.parts + parts)
is Bolt12IncomingPayment -> copy(parts = this.parts + parts)
}
}
}
Expand All @@ -194,15 +184,15 @@ sealed class LightningIncomingPayment(val paymentPreimage: ByteVector32) : Incom
data class Bolt11IncomingPayment(
private val preimage: ByteVector32,
val paymentRequest: Bolt11Invoice,
override val received: Received?,
override val parts: List<Part> = emptyList(),
override val createdAt: Long = currentTimestampMillis()
) : LightningIncomingPayment(preimage)

/** A payment for a Bolt 12 offer: note that we only keep a few fields from the corresponding Bolt 12 invoice. */
data class Bolt12IncomingPayment(
private val preimage: ByteVector32,
val metadata: OfferPaymentMetadata,
override val received: Received?,
override val parts: List<Part> = emptyList(),
override val createdAt: Long = currentTimestampMillis()
) : LightningIncomingPayment(preimage)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -964,7 +964,7 @@ class Peer(
}
when (result) {
is IncomingPaymentHandler.ProcessAddResult.Accepted -> {
if ((result.incomingPayment.received?.parts?.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)))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import fr.acinq.lightning.blockchain.fee.FeeratePerKw
import fr.acinq.lightning.channel.*
import fr.acinq.lightning.crypto.sphinx.Sphinx
import fr.acinq.lightning.db.*
import fr.acinq.lightning.db.LightningIncomingPayment.Companion.addReceivedParts
import fr.acinq.lightning.io.AddLiquidityForIncomingPayment
import fr.acinq.lightning.io.PeerCommand
import fr.acinq.lightning.io.SendOnTheFlyFundingMessage
Expand Down Expand Up @@ -47,7 +48,7 @@ class IncomingPaymentHandler(val nodeParams: NodeParams, val db: PaymentsDb) {
sealed class ProcessAddResult {
abstract val actions: List<PeerCommand>

data class Accepted(override val actions: List<PeerCommand>, val incomingPayment: LightningIncomingPayment, val received: LightningIncomingPayment.Received) : ProcessAddResult()
data class Accepted(override val actions: List<PeerCommand>, val incomingPayment: LightningIncomingPayment, val parts: List<LightningIncomingPayment.Part>) : ProcessAddResult()
data class Rejected(override val actions: List<PeerCommand>, val incomingPayment: LightningIncomingPayment?) : ProcessAddResult()
data class Pending(val incomingPayment: LightningIncomingPayment, val pendingPayment: PendingPayment, override val actions: List<PeerCommand> = listOf()) : ProcessAddResult()
}
Expand Down Expand Up @@ -100,7 +101,7 @@ class IncomingPaymentHandler(val nodeParams: NodeParams, val db: PaymentsDb) {
timestampSeconds
)
logger.info(mapOf("paymentHash" to paymentHash)) { "generated payment request ${pr.write()}" }
val incomingPayment = Bolt11IncomingPayment(paymentPreimage, pr, received = null)
val incomingPayment = Bolt11IncomingPayment(paymentPreimage, pr)
db.addIncomingPayment(incomingPayment)
return pr
}
Expand Down Expand Up @@ -149,8 +150,8 @@ class IncomingPaymentHandler(val nodeParams: NodeParams, val db: PaymentsDb) {
is Either.Left -> validationResult.value
is Either.Right -> {
val incomingPayment = validationResult.value
val received = incomingPayment.received
if (received != null) {
val receivedParts = incomingPayment.parts
if (receivedParts.isNotEmpty()) {
return when (paymentPart) {
is HtlcPart -> {
// The invoice for this payment hash has already been paid. Two possible scenarios:
Expand All @@ -162,11 +163,11 @@ class IncomingPaymentHandler(val nodeParams: NodeParams, val db: PaymentsDb) {
//
// 2) This is a new htlc. This can happen when a sender pays an already paid invoice. In that case the
// htlc can be safely rejected.
val htlcsMapInDb = received.parts.filterIsInstance<LightningIncomingPayment.Received.Part.Htlc>().map { it.channelId to it.htlcId }
val htlcsMapInDb = receivedParts.filterIsInstance<LightningIncomingPayment.Part.Htlc>().map { it.channelId to it.htlcId }
if (htlcsMapInDb.contains(paymentPart.htlc.channelId to paymentPart.htlc.id)) {
logger.info { "accepting local replay of htlc=${paymentPart.htlc.id} on channel=${paymentPart.htlc.channelId}" }
val action = WrappedChannelCommand(paymentPart.htlc.channelId, ChannelCommand.Htlc.Settlement.Fulfill(paymentPart.htlc.id, incomingPayment.paymentPreimage, true))
ProcessAddResult.Accepted(listOf(action), incomingPayment, received)
ProcessAddResult.Accepted(listOf(action), incomingPayment, receivedParts)
} else {
logger.info { "rejecting htlc part for an invoice that has already been paid" }
val action = actionForFailureMessage(IncorrectOrUnknownPaymentDetails(paymentPart.totalAmount, currentBlockHeight.toLong()), paymentPart.htlc)
Expand Down Expand Up @@ -233,8 +234,8 @@ class IncomingPaymentHandler(val nodeParams: NodeParams, val db: PaymentsDb) {
addToFeeCredit -> {
logger.info { "adding on-the-fly funding to fee credit (amount=${willAddHtlcParts.map { it.amount }.sum()})" }
val parts = buildList {
htlcParts.forEach { add(LightningIncomingPayment.Received.Part.Htlc(it.amount, it.htlc.channelId, it.htlc.id, it.htlc.fundingFee)) }
willAddHtlcParts.forEach { add(LightningIncomingPayment.Received.Part.FeeCredit(it.amount)) }
htlcParts.forEach { add(LightningIncomingPayment.Part.Htlc(it.amount, it.htlc.channelId, it.htlc.id, it.htlc.fundingFee)) }
willAddHtlcParts.forEach { add(LightningIncomingPayment.Part.FeeCredit(it.amount)) }
}
val actions = buildList {
// We send a single add_fee_credit for the will_add_htlc set.
Expand Down Expand Up @@ -280,7 +281,7 @@ class IncomingPaymentHandler(val nodeParams: NodeParams, val db: PaymentsDb) {
rejectPayment(payment, incomingPayment, failure)
}
is Either.Right -> {
val parts = htlcParts.map { part -> LightningIncomingPayment.Received.Part.Htlc(part.amount, part.htlc.channelId, part.htlc.id, part.htlc.fundingFee) }
val parts = htlcParts.map { part -> LightningIncomingPayment.Part.Htlc(part.amount, part.htlc.channelId, part.htlc.id, part.htlc.fundingFee) }
val actions = htlcParts.map { part ->
val cmd = ChannelCommand.Htlc.Settlement.Fulfill(part.htlc.id, incomingPayment.paymentPreimage, true)
WrappedChannelCommand(part.htlc.channelId, cmd)
Expand All @@ -296,21 +297,17 @@ class IncomingPaymentHandler(val nodeParams: NodeParams, val db: PaymentsDb) {
}
}

private suspend fun acceptPayment(incomingPayment: LightningIncomingPayment, parts: List<LightningIncomingPayment.Received.Part>, actions: List<PeerCommand>): ProcessAddResult.Accepted {
private suspend fun acceptPayment(incomingPayment: LightningIncomingPayment, parts: List<LightningIncomingPayment.Part>, actions: List<PeerCommand>): ProcessAddResult.Accepted {
pending.remove(incomingPayment.paymentHash)
if (incomingPayment is Bolt12IncomingPayment) {
// We didn't store the Bolt 12 invoice in our DB when receiving the invoice_request (to protect against DoS).
// We need to create the DB entry now otherwise the payment won't be recorded.
db.addIncomingPayment(incomingPayment)
}
db.receiveLightningPayment(incomingPayment.paymentHash, parts)
val received = LightningIncomingPayment.Received(parts)
val incomingPayment1 = when (incomingPayment) {
is Bolt11IncomingPayment -> incomingPayment.copy(received = received)
is Bolt12IncomingPayment -> incomingPayment.copy(received = received)
}
val incomingPayment1 = incomingPayment.addReceivedParts(parts)
nodeParams._nodeEvents.emit(PaymentEvents.PaymentReceived(incomingPayment1))
return ProcessAddResult.Accepted(actions, incomingPayment1, received)
return ProcessAddResult.Accepted(actions, incomingPayment1, parts)
}

private fun rejectPayment(payment: PendingPayment, incomingPayment: LightningIncomingPayment, failure: FailureMessage): ProcessAddResult.Rejected {
Expand Down Expand Up @@ -410,7 +407,7 @@ class IncomingPaymentHandler(val nodeParams: NodeParams, val db: PaymentsDb) {
}
// Payments are rejected for expired invoices UNLESS invoice has already been paid
// We must accept payments for already paid invoices, because it could be the channel replaying HTLCs that we already fulfilled
incomingPayment.isExpired() && incomingPayment.received == null -> {
incomingPayment.isExpired() && incomingPayment.parts.isEmpty() -> {
logger.warning { "the invoice is expired" }
Either.Left(rejectPaymentPart(privateKey, paymentPart, incomingPayment, currentBlockHeight))
}
Expand Down Expand Up @@ -465,7 +462,7 @@ class IncomingPaymentHandler(val nodeParams: NodeParams, val db: PaymentsDb) {
Either.Left(rejectPaymentPart(privateKey, paymentPart, null, currentBlockHeight))
}
else -> {
val incomingPayment = db.getLightningIncomingPayment(paymentPart.paymentHash) ?: Bolt12IncomingPayment(metadata.preimage, metadata, received = null)
val incomingPayment = db.getLightningIncomingPayment(paymentPart.paymentHash) ?: Bolt12IncomingPayment(metadata.preimage, metadata)
when {
incomingPayment !is Bolt12IncomingPayment -> {
logger.warning { "unsupported payment type: ${incomingPayment::class}" }
Expand All @@ -483,7 +480,7 @@ class IncomingPaymentHandler(val nodeParams: NodeParams, val db: PaymentsDb) {
logger.warning { "payment with expiry too small: ${paymentPart.htlc.cltvExpiry}, min is ${minFinalCltvExpiry(nodeParams, paymentPart, incomingPayment, currentBlockHeight)}" }
Either.Left(rejectPaymentPart(privateKey, paymentPart, incomingPayment, currentBlockHeight))
}
metadata.createdAtMillis + nodeParams.bolt12InvoiceExpiry.inWholeMilliseconds < currentTimestampMillis() && incomingPayment.received == null -> {
metadata.createdAtMillis + nodeParams.bolt12InvoiceExpiry.inWholeMilliseconds < currentTimestampMillis() && incomingPayment.parts.isEmpty() -> {
logger.warning { "the invoice is expired" }
Either.Left(rejectPaymentPart(privateKey, paymentPart, incomingPayment, currentBlockHeight))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package fr.acinq.lightning.db

import fr.acinq.bitcoin.ByteVector32
import fr.acinq.bitcoin.TxId
import fr.acinq.lightning.db.LightningIncomingPayment.Companion.addReceivedParts
import fr.acinq.lightning.payment.FinalFailure
import fr.acinq.lightning.utils.UUID

Expand Down Expand Up @@ -29,22 +30,10 @@ class InMemoryPaymentsDb : PaymentsDb {

override suspend fun getLightningIncomingPayment(paymentHash: ByteVector32): LightningIncomingPayment? = incoming[paymentHash]

override suspend fun receiveLightningPayment(paymentHash: ByteVector32, parts: List<LightningIncomingPayment.Received.Part>, receivedAt: Long) {
override suspend fun receiveLightningPayment(paymentHash: ByteVector32, parts: List<LightningIncomingPayment.Part>) {
when (val payment = incoming[paymentHash]) {
null -> Unit // no-op
is Bolt11IncomingPayment ->
incoming[paymentHash] = payment.copy(
received = LightningIncomingPayment.Received(
parts = payment.received?.parts.orEmpty() + parts,
receivedAt = receivedAt
)
)
is Bolt12IncomingPayment -> incoming[paymentHash] = payment.copy(
received = LightningIncomingPayment.Received(
parts = payment.received?.parts.orEmpty() + parts,
receivedAt = receivedAt
)
)
else -> incoming[paymentHash] = payment.addReceivedParts(parts)
}
}

Expand All @@ -61,14 +50,14 @@ class InMemoryPaymentsDb : PaymentsDb {
.asSequence()
.filter { it.createdAt in fromCreatedAt until toCreatedAt }
.filter { it.isExpired() }
.filter { it.received == null }
.filter { it.parts.isEmpty() }
.sortedByDescending { it.createdAt }
.toList()

override suspend fun removeLightningIncomingPayment(paymentHash: ByteVector32): Boolean {
val payment = getLightningIncomingPayment(paymentHash)
return when (payment?.received) {
null -> incoming.remove(paymentHash) != null
return when (payment?.parts?.isEmpty()) {
true -> incoming.remove(paymentHash) != null
else -> false // do nothing if payment already partially paid
}
}
Expand Down
Loading

0 comments on commit a110612

Please sign in to comment.