Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -503,7 +503,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.parts.isEmpty() && !paysPreviousOnTheFlyFunding -> {
metadata.createdAtSeconds + (metadata.relativeExpirySeconds ?: nodeParams.bolt12InvoiceExpiry.inWholeSeconds) < currentTimestampSeconds() && incomingPayment.parts.isEmpty() && !paysPreviousOnTheFlyFunding -> {
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 @@ -18,6 +18,7 @@ import fr.acinq.lightning.message.OnionMessages.Destination
import fr.acinq.lightning.message.OnionMessages.IntermediateNode
import fr.acinq.lightning.message.OnionMessages.buildMessage
import fr.acinq.lightning.utils.currentTimestampMillis
import fr.acinq.lightning.utils.currentTimestampSeconds
import fr.acinq.lightning.utils.toByteVector
import fr.acinq.lightning.wire.*
import kotlinx.coroutines.flow.MutableSharedFlow
Expand Down Expand Up @@ -153,14 +154,32 @@ class OfferManager(val nodeParams: NodeParams, val walletParams: WalletParams, v
else -> {
val amount = request.requestedAmount
val preimage = randomBytes32()
val truncatedPayerNote = (request.payerNote ?: request.offer.description)?.let {
val truncatedPayerNote = request.payerNote?.let {
if (it.length <= 64) {
it
} else {
it.take(63) + "…"
}
}
val metadata = OfferPaymentMetadata.V1(request.offer.offerId, amount, preimage, request.payerId, truncatedPayerNote, request.quantity, currentTimestampMillis()).toPathId(nodeParams.nodePrivateKey)
val truncatedDescription = request.offer.description?.let {
if (it.length <= 64) {
it
} else {
it.take(63) + "…"
}
}
val expirySeconds = request.offer.expirySeconds ?: nodeParams.bolt12InvoiceExpiry.inWholeSeconds
val metadata = OfferPaymentMetadata.V2(
offerId = request.offer.offerId,
amount = amount,
preimage = preimage,
createdAtSeconds = currentTimestampSeconds(),
relativeExpirySeconds = expirySeconds,
description = truncatedDescription,
payerKey = request.payerId,
payerNote = truncatedPayerNote,
quantity = request.quantity_opt
).toPathId(nodeParams.nodePrivateKey)
val recipientPayload = RouteBlindingEncryptedData(TlvStream(RouteBlindingEncryptedDataTlv.PathId(metadata))).write().toByteVector()
val cltvExpiryDelta = remoteChannelUpdates.maxOfOrNull { it.cltvExpiryDelta } ?: walletParams.invoiceDefaultRoutingFees.cltvExpiryDelta
val paymentInfo = OfferTypes.PaymentInfo(
Expand Down Expand Up @@ -191,7 +210,7 @@ class OfferManager(val nodeParams: NodeParams, val walletParams: WalletParams, v
).write().toByteVector()
val blindedRoute = RouteBlinding.create(randomKey(), listOf(remoteNodeId, nodeParams.nodeId), listOf(remoteNodePayload, recipientPayload)).route
val path = Bolt12Invoice.Companion.PaymentBlindedContactInfo(OfferTypes.ContactInfo.BlindedPath(blindedRoute), paymentInfo)
val invoice = Bolt12Invoice(request, preimage, blindedPrivateKey, nodeParams.bolt12InvoiceExpiry.inWholeSeconds, nodeParams.features.bolt12Features(), listOf(path))
val invoice = Bolt12Invoice(request, preimage, blindedPrivateKey, expirySeconds, nodeParams.features.bolt12Features(), listOf(path))
val destination = Destination.BlindedPath(replyPath)
when (val invoiceMessage = buildMessage(randomKey(), randomKey(), intermediateNodes(destination), destination, TlvStream(OnionMessagePayloadTlv.Invoice(invoice.records)))) {
is Left -> {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
package fr.acinq.lightning.payment

import fr.acinq.bitcoin.*
import fr.acinq.bitcoin.ByteVector32
import fr.acinq.bitcoin.io.ByteArrayInput
import fr.acinq.bitcoin.io.ByteArrayOutput
import fr.acinq.bitcoin.io.Input
import fr.acinq.bitcoin.io.Output
import fr.acinq.lightning.MilliSatoshi
import fr.acinq.lightning.utils.msat
import fr.acinq.lightning.wire.LightningCodecs
import kotlin.experimental.and
import kotlin.experimental.or
import kotlin.math.min

/**
* The flow for Bolt 12 offer payments is the following:
Expand All @@ -28,7 +32,8 @@ sealed class OfferPaymentMetadata {
abstract val offerId: ByteVector32
abstract val amount: MilliSatoshi
abstract val preimage: ByteVector32
abstract val createdAtMillis: Long
abstract val createdAtSeconds: Long
abstract val relativeExpirySeconds: Long?
val paymentHash: ByteVector32 get() = preimage.sha256()

/** Encode into a format that can be stored in the payments DB. */
Expand All @@ -37,17 +42,16 @@ sealed class OfferPaymentMetadata {
LightningCodecs.writeByte(this.version.toInt(), out)
when (this) {
is V1 -> this.write(out)
is V2 -> this.write(out)
}
return out.toByteArray().byteVector()
}

/** Encode into a path_id that must be included in the [Bolt12Invoice]'s blinded path. */
fun toPathId(nodeKey: PrivateKey): ByteVector = when (this) {
is V1 -> {
val encoded = this.encode()
val signature = Crypto.sign(Crypto.sha256(encoded), nodeKey)
encoded + signature
}
fun toPathId(nodeKey: PrivateKey): ByteVector {
val encoded = this.encode()
val signature = Crypto.sign(Crypto.sha256(encoded), nodeKey)
return encoded + signature
}

/** In this first version, we simply sign the payment metadata to verify its authenticity when receiving the payment. */
Expand All @@ -58,10 +62,13 @@ sealed class OfferPaymentMetadata {
val payerKey: PublicKey,
val payerNote: String?,
val quantity: Long,
override val createdAtMillis: Long
val createdAtMillis: Long
) : OfferPaymentMetadata() {
override val version: Byte get() = 1

override val createdAtSeconds: Long get() = createdAtMillis / 1000
override val relativeExpirySeconds: Long? get() = null

fun write(out: Output) {
LightningCodecs.writeBytes(offerId, out)
LightningCodecs.writeU64(amount.toLong(), out)
Expand All @@ -73,6 +80,8 @@ sealed class OfferPaymentMetadata {
}

companion object {
val minLength: Int get() = 121

fun read(input: Input): V1 {
val offerId = LightningCodecs.bytes(input, 32).byteVector32()
val amount = LightningCodecs.u64(input).msat
Expand All @@ -86,6 +95,78 @@ sealed class OfferPaymentMetadata {
}
}

data class V2(
override val offerId: ByteVector32,
override val amount: MilliSatoshi,
override val preimage: ByteVector32,
override val createdAtSeconds: Long,
override val relativeExpirySeconds: Long?,
val description: String?,
val payerKey: PublicKey?,
val payerNote: String?,
val quantity: Long?,

) : OfferPaymentMetadata() {
override val version: Byte get() = 2

fun write(out: Output) {
LightningCodecs.writeBytes(offerId, out)
LightningCodecs.writeBigSize(amount.toLong(), out)
LightningCodecs.writeBytes(preimage, out)
LightningCodecs.writeBigSize(createdAtSeconds, out)

var flags: Byte = 0
if (relativeExpirySeconds != null) { flags = flags or 0b00001 }
if (payerKey != null) { flags = flags or 0b00010 }
if (quantity != null) { flags = flags or 0b00100 }
if (description != null) { flags = flags or 0b01000 }
if (payerNote != null) { flags = flags or 0b10000 }
LightningCodecs.writeByte(flags.toInt(), out)

relativeExpirySeconds?.let { LightningCodecs.writeBigSize(it, out) }
payerKey?.let { LightningCodecs.writeBytes(it.value, out) }
quantity?.let { LightningCodecs.writeU64(it, out) }
description?.let {
if (payerNote != null) { LightningCodecs.writeBigSize(it.length.toLong(), out) }
LightningCodecs.writeBytes(it.encodeToByteArray(), out)
}
payerNote?.let {
LightningCodecs.writeBytes(it.encodeToByteArray(), out)
}
}

companion object {
val minLength: Int get() = 67

fun read(input: Input): V2 {
val offerId = LightningCodecs.bytes(input, 32).byteVector32()
val amount = LightningCodecs.bigSize(input).msat
val preimage = LightningCodecs.bytes(input, 32).byteVector32()
val createdAtSeconds = LightningCodecs.bigSize(input)
val flags = LightningCodecs.byte(input).toByte()

val hasExp = (flags and 0b00001) != 0.toByte()
val hasPKey = (flags and 0b00010) != 0.toByte()
val hasQnty = (flags and 0b00100) != 0.toByte()
val hasDesc = (flags and 0b01000) != 0.toByte()
val hasPNote = (flags and 0b10000) != 0.toByte()

val relativeExpirySeconds = if (hasExp) { LightningCodecs.bigSize(input) } else { null }
val payerKey = if (hasPKey) { PublicKey(LightningCodecs.bytes(input, 33)) } else { null }
val quantity = if (hasQnty) { LightningCodecs.u64(input) } else { null }
val description = if (hasDesc) {
val strLen = if (hasPNote) { LightningCodecs.bigSize(input).toInt() } else { input.availableBytes }
LightningCodecs.bytes(input, strLen).decodeToString()
} else { null }
val payerNote = if (hasPNote) {
if (input.availableBytes > 0) { LightningCodecs.bytes(input, input.availableBytes).decodeToString() } else { "" }
} else { null }

return V2(offerId, amount, preimage, createdAtSeconds, relativeExpirySeconds, description, payerKey, payerNote, quantity)
}
}
}

companion object {
/**
* Decode an [OfferPaymentMetadata] encoded using [encode] (e.g. from our payments DB).
Expand All @@ -95,6 +176,7 @@ sealed class OfferPaymentMetadata {
val input = ByteArrayInput(encoded.toByteArray())
return when (val version = LightningCodecs.byte(input)) {
1 -> V1.read(input)
2 -> V2.read(input)
else -> throw IllegalArgumentException("unknown offer payment metadata version: $version")
}
}
Expand All @@ -108,7 +190,8 @@ sealed class OfferPaymentMetadata {
val input = ByteArrayInput(pathId.toByteArray())
when (LightningCodecs.byte(input)) {
1 -> {
if (input.availableBytes < 185) return null
val minimum = V1.minLength + 64
if (input.availableBytes < minimum) return null
val metadataSize = input.availableBytes - 64
val metadata = LightningCodecs.bytes(input, metadataSize)
val signature = LightningCodecs.bytes(input, 64).byteVector64()
Expand All @@ -117,6 +200,17 @@ sealed class OfferPaymentMetadata {
// This call is safe since we verified that we have the right number of bytes and the signature was valid.
return V1.read(ByteArrayInput(metadata))
}
2 -> {
val minimum = V2.minLength + 64
if (input.availableBytes < minimum) return null
val metadataSize = input.availableBytes - 64
val metadata = LightningCodecs.bytes(input, metadataSize)
val signature = LightningCodecs.bytes(input, 64).byteVector64()
// Note that the signature includes the version byte.
if (!Crypto.verifySignature(Crypto.sha256(pathId.take(1 + metadataSize)), signature, nodeId)) return null
// This call is safe since we verified that we have the right number of bytes and the signature was valid.
return V2.read(ByteArrayInput(metadata))
}
else -> return null
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,13 @@ class OfferManagerTestsCommon : LightningTestSuite() {
return Pair(OnionMessage(relayInfo.nextPathKeyOverride ?: nextBlinding, decrypted.value.nextPacket), nextNode)
}

private fun decryptPathId(invoice: Bolt12Invoice, trampolineKey: PrivateKey): OfferPaymentMetadata.V1 {
private fun decryptPathId(invoice: Bolt12Invoice, trampolineKey: PrivateKey): OfferPaymentMetadata.V2 {
val blindedRoute = invoice.blindedPaths.first().route.route
assertEquals(2, blindedRoute.encryptedPayloads.size)
val (_, nextBlinding) = RouteBlinding.decryptPayload(trampolineKey, blindedRoute.firstPathKey, blindedRoute.encryptedPayloads.first()).right!!
val (lastPayload, _) = RouteBlinding.decryptPayload(TestConstants.Alice.nodeParams.nodePrivateKey, nextBlinding, blindedRoute.encryptedPayloads.last()).right!!
val pathId = RouteBlindingEncryptedData.read(lastPayload.toByteArray()).right!!.pathId!!
return OfferPaymentMetadata.fromPathId(TestConstants.Alice.nodeParams.nodeId, pathId) as OfferPaymentMetadata.V1
return OfferPaymentMetadata.fromPathId(TestConstants.Alice.nodeParams.nodeId, pathId) as OfferPaymentMetadata.V2
}

@Test
Expand Down
Loading