commonMain.fr.acinq.lightning.wire.PaymentOnion.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of lightning-kmp Show documentation
Show all versions of lightning-kmp Show documentation
A Kotlin Multiplatform implementation of the Lightning Network
package fr.acinq.lightning.wire
import fr.acinq.bitcoin.ByteVector
import fr.acinq.bitcoin.ByteVector32
import fr.acinq.bitcoin.PublicKey
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.CltvExpiry
import fr.acinq.lightning.CltvExpiryDelta
import fr.acinq.lightning.MilliSatoshi
import fr.acinq.lightning.ShortChannelId
import fr.acinq.lightning.payment.PaymentRequest
import fr.acinq.lightning.utils.msat
sealed class OnionPaymentPayloadTlv : Tlv {
/** Amount to forward to the next node. */
data class AmountToForward(val amount: MilliSatoshi) : OnionPaymentPayloadTlv() {
override val tag: Long get() = AmountToForward.tag
override fun write(out: Output) = LightningCodecs.writeTU64(amount.toLong(), out)
companion object : TlvValueReader {
const val tag: Long = 2
override fun read(input: Input): AmountToForward = AmountToForward(MilliSatoshi(LightningCodecs.tu64(input)))
}
}
/** CLTV value to use for the HTLC offered to the next node. */
data class OutgoingCltv(val cltv: CltvExpiry) : OnionPaymentPayloadTlv() {
override val tag: Long get() = OutgoingCltv.tag
override fun write(out: Output) = LightningCodecs.writeTU32(cltv.toLong().toInt(), out)
companion object : TlvValueReader {
const val tag: Long = 4
override fun read(input: Input): OutgoingCltv = OutgoingCltv(CltvExpiry(LightningCodecs.tu32(input).toLong()))
}
}
/** Id of the channel to use to forward a payment to the next node. */
data class OutgoingChannelId(val shortChannelId: ShortChannelId) : OnionPaymentPayloadTlv() {
override val tag: Long get() = OutgoingChannelId.tag
override fun write(out: Output) = LightningCodecs.writeU64(shortChannelId.toLong(), out)
companion object : TlvValueReader {
const val tag: Long = 6
override fun read(input: Input): OutgoingChannelId = OutgoingChannelId(ShortChannelId(LightningCodecs.u64(input)))
}
}
/**
* Bolt 11 payment details (only included for the last node).
*
* @param secret payment secret specified in the Bolt 11 invoice.
* @param totalAmount total amount in multi-part payments. When missing, assumed to be equal to AmountToForward.
*/
data class PaymentData(val secret: ByteVector32, val totalAmount: MilliSatoshi) : OnionPaymentPayloadTlv() {
override val tag: Long get() = PaymentData.tag
override fun write(out: Output) {
LightningCodecs.writeBytes(secret, out)
LightningCodecs.writeTU64(totalAmount.toLong(), out)
}
companion object : TlvValueReader {
const val tag: Long = 8
override fun read(input: Input): PaymentData = PaymentData(ByteVector32(LightningCodecs.bytes(input, 32)), MilliSatoshi(LightningCodecs.tu64(input)))
}
}
/**
* When payment metadata is included in a Bolt 9 invoice, we should send it as-is to the recipient.
* This lets recipients generate invoices without having to store anything on their side until the invoice is paid.
*/
data class PaymentMetadata(val data: ByteVector) : OnionPaymentPayloadTlv() {
override val tag: Long get() = PaymentMetadata.tag
override fun write(out: Output) = LightningCodecs.writeBytes(data, out)
companion object : TlvValueReader {
const val tag: Long = 16
override fun read(input: Input): PaymentMetadata = PaymentMetadata(ByteVector(LightningCodecs.bytes(input, input.availableBytes)))
}
}
/**
* Invoice feature bits. Only included for intermediate trampoline nodes when they should convert to a legacy payment
* because the final recipient doesn't support trampoline.
*/
data class InvoiceFeatures(val features: ByteVector) : OnionPaymentPayloadTlv() {
override val tag: Long get() = InvoiceFeatures.tag
override fun write(out: Output) = LightningCodecs.writeBytes(features, out)
companion object : TlvValueReader {
const val tag: Long = 66097
override fun read(input: Input): InvoiceFeatures = InvoiceFeatures(ByteVector(LightningCodecs.bytes(input, input.availableBytes)))
}
}
/** Id of the next node. */
data class OutgoingNodeId(val nodeId: PublicKey) : OnionPaymentPayloadTlv() {
override val tag: Long get() = OutgoingNodeId.tag
override fun write(out: Output) = LightningCodecs.writeBytes(nodeId.value, out)
companion object : TlvValueReader {
const val tag: Long = 66098
override fun read(input: Input): OutgoingNodeId = OutgoingNodeId(PublicKey(LightningCodecs.bytes(input, 33)))
}
}
/**
* Invoice routing hints. Only included for intermediate trampoline nodes when they should convert to a legacy payment
* because the final recipient doesn't support trampoline.
*/
data class InvoiceRoutingInfo(val extraHops: List>) : OnionPaymentPayloadTlv() {
override val tag: Long get() = InvoiceRoutingInfo.tag
override fun write(out: Output) {
for (routeHint in extraHops) {
LightningCodecs.writeByte(routeHint.size, out)
routeHint.map {
LightningCodecs.writeBytes(it.nodeId.value, out)
LightningCodecs.writeU64(it.shortChannelId.toLong(), out)
LightningCodecs.writeU32(it.feeBase.toLong().toInt(), out)
LightningCodecs.writeU32(it.feeProportionalMillionths.toInt(), out)
LightningCodecs.writeU16(it.cltvExpiryDelta.toInt(), out)
}
}
}
companion object : TlvValueReader {
const val tag: Long = 66099
override fun read(input: Input): InvoiceRoutingInfo {
val extraHops = mutableListOf>()
while (input.availableBytes > 0) {
val hopCount = LightningCodecs.byte(input)
val extraHop = (0 until hopCount).map {
PaymentRequest.TaggedField.ExtraHop(
PublicKey(LightningCodecs.bytes(input, 33)),
ShortChannelId(LightningCodecs.u64(input)),
MilliSatoshi(LightningCodecs.u32(input).toLong()),
LightningCodecs.u32(input).toLong(),
CltvExpiryDelta(LightningCodecs.u16(input))
)
}
extraHops.add(extraHop)
}
return InvoiceRoutingInfo(extraHops)
}
}
}
/** An encrypted trampoline onion packet. */
data class TrampolineOnion(val packet: OnionRoutingPacket) : OnionPaymentPayloadTlv() {
override val tag: Long get() = TrampolineOnion.tag
override fun write(out: Output) = OnionRoutingPacketSerializer(OnionRoutingPacket.TrampolinePacketLength).write(packet, out)
companion object : TlvValueReader {
const val tag: Long = 66100
override fun read(input: Input): TrampolineOnion = TrampolineOnion(OnionRoutingPacketSerializer(OnionRoutingPacket.TrampolinePacketLength).read(input))
}
}
}
object PaymentOnion {
sealed class PerHopPayload {
abstract fun write(out: Output)
fun write(): ByteArray {
val out = ByteArrayOutput()
write(out)
return out.toByteArray()
}
companion object {
val tlvSerializer = TlvStreamSerializer(
true, @Suppress("UNCHECKED_CAST") mapOf(
OnionPaymentPayloadTlv.AmountToForward.tag to OnionPaymentPayloadTlv.AmountToForward.Companion as TlvValueReader,
OnionPaymentPayloadTlv.OutgoingCltv.tag to OnionPaymentPayloadTlv.OutgoingCltv.Companion as TlvValueReader,
OnionPaymentPayloadTlv.OutgoingChannelId.tag to OnionPaymentPayloadTlv.OutgoingChannelId.Companion as TlvValueReader,
OnionPaymentPayloadTlv.PaymentData.tag to OnionPaymentPayloadTlv.PaymentData.Companion as TlvValueReader,
OnionPaymentPayloadTlv.PaymentMetadata.tag to OnionPaymentPayloadTlv.PaymentMetadata.Companion as TlvValueReader,
OnionPaymentPayloadTlv.InvoiceFeatures.tag to OnionPaymentPayloadTlv.InvoiceFeatures.Companion as TlvValueReader,
OnionPaymentPayloadTlv.OutgoingNodeId.tag to OnionPaymentPayloadTlv.OutgoingNodeId.Companion as TlvValueReader,
OnionPaymentPayloadTlv.InvoiceRoutingInfo.tag to OnionPaymentPayloadTlv.InvoiceRoutingInfo.Companion as TlvValueReader,
OnionPaymentPayloadTlv.TrampolineOnion.tag to OnionPaymentPayloadTlv.TrampolineOnion.Companion as TlvValueReader,
)
)
}
}
interface PerHopPayloadReader {
fun read(input: Input): T
fun read(bytes: ByteArray): T = read(ByteArrayInput(bytes))
}
data class FinalPayload(val records: TlvStream) : PerHopPayload() {
val amount = records.get()!!.amount
val expiry = records.get()!!.cltv
val paymentSecret = records.get()!!.secret
val totalAmount = run {
val total = records.get()!!.totalAmount
if (total > 0.msat) total else amount
}
val paymentMetadata = records.get()?.data
override fun write(out: Output) = tlvSerializer.write(records, out)
companion object : PerHopPayloadReader {
override fun read(input: Input): FinalPayload = FinalPayload(tlvSerializer.read(input))
/** Create a single-part payment (total amount sent at once). */
fun createSinglePartPayload(amount: MilliSatoshi, expiry: CltvExpiry, paymentSecret: ByteVector32, paymentMetadata: ByteVector?, userCustomTlvs: Set = setOf()): FinalPayload {
val tlvs = buildSet {
add(OnionPaymentPayloadTlv.AmountToForward(amount))
add(OnionPaymentPayloadTlv.OutgoingCltv(expiry))
add(OnionPaymentPayloadTlv.PaymentData(paymentSecret, amount))
paymentMetadata?.let { add(OnionPaymentPayloadTlv.PaymentMetadata(it)) }
}
return FinalPayload(TlvStream(tlvs, userCustomTlvs))
}
/** Create a partial payment (total amount split between multiple payments). */
fun createMultiPartPayload(
amount: MilliSatoshi,
totalAmount: MilliSatoshi,
expiry: CltvExpiry,
paymentSecret: ByteVector32,
paymentMetadata: ByteVector?,
additionalTlvs: Set = setOf(),
userCustomTlvs: Set = setOf()
): FinalPayload {
val tlvs = buildSet {
add(OnionPaymentPayloadTlv.AmountToForward(amount))
add(OnionPaymentPayloadTlv.OutgoingCltv(expiry))
add(OnionPaymentPayloadTlv.PaymentData(paymentSecret, totalAmount))
paymentMetadata?.let { add(OnionPaymentPayloadTlv.PaymentMetadata(it)) }
addAll(additionalTlvs)
}
return FinalPayload(TlvStream(tlvs, userCustomTlvs))
}
/** Create a trampoline outer payload. */
fun createTrampolinePayload(amount: MilliSatoshi, totalAmount: MilliSatoshi, expiry: CltvExpiry, paymentSecret: ByteVector32, trampolinePacket: OnionRoutingPacket): FinalPayload {
val tlvs = TlvStream(
OnionPaymentPayloadTlv.AmountToForward(amount),
OnionPaymentPayloadTlv.OutgoingCltv(expiry),
OnionPaymentPayloadTlv.PaymentData(paymentSecret, totalAmount),
OnionPaymentPayloadTlv.TrampolineOnion(trampolinePacket)
)
return FinalPayload(tlvs)
}
}
}
data class ChannelRelayPayload(val records: TlvStream) : PerHopPayload() {
val amountToForward = records.get()!!.amount
val outgoingCltv = records.get()!!.cltv
val outgoingChannelId = records.get()!!.shortChannelId
override fun write(out: Output) = tlvSerializer.write(records, out)
companion object : PerHopPayloadReader {
override fun read(input: Input): ChannelRelayPayload = ChannelRelayPayload(tlvSerializer.read(input))
fun create(outgoingChannelId: ShortChannelId, amountToForward: MilliSatoshi, outgoingCltv: CltvExpiry): ChannelRelayPayload =
ChannelRelayPayload(TlvStream(OnionPaymentPayloadTlv.AmountToForward(amountToForward), OnionPaymentPayloadTlv.OutgoingCltv(outgoingCltv), OnionPaymentPayloadTlv.OutgoingChannelId(outgoingChannelId)))
}
}
data class NodeRelayPayload(val records: TlvStream) : PerHopPayload() {
val amountToForward = records.get()!!.amount
val outgoingCltv = records.get()!!.cltv
val outgoingNodeId = records.get()!!.nodeId
val totalAmount = run {
val paymentData = records.get()
when {
paymentData == null -> amountToForward
paymentData.totalAmount == MilliSatoshi(0) -> amountToForward
else -> paymentData.totalAmount
}
}
// NB: the following fields are only included in the trampoline-to-legacy case.
val paymentSecret = records.get()?.secret
val paymentMetadata = records.get()?.data
val invoiceFeatures = records.get()?.features
val invoiceRoutingInfo = records.get()?.extraHops
override fun write(out: Output) = tlvSerializer.write(records, out)
companion object : PerHopPayloadReader {
override fun read(input: Input): NodeRelayPayload = NodeRelayPayload(tlvSerializer.read(input))
fun create(amount: MilliSatoshi, expiry: CltvExpiry, nextNodeId: PublicKey) =
NodeRelayPayload(TlvStream(OnionPaymentPayloadTlv.AmountToForward(amount), OnionPaymentPayloadTlv.OutgoingCltv(expiry), OnionPaymentPayloadTlv.OutgoingNodeId(nextNodeId)))
/** Create a trampoline inner payload instructing the trampoline node to relay via a non-trampoline payment. */
fun createNodeRelayToNonTrampolinePayload(amount: MilliSatoshi, totalAmount: MilliSatoshi, expiry: CltvExpiry, targetNodeId: PublicKey, invoice: PaymentRequest): NodeRelayPayload {
// NB: we limit the number of routing hints to ensure we don't overflow the onion.
// A better solution is to provide the routing hints outside the onion (in the `update_add_htlc` tlv stream).
val prunedRoutingHints = invoice.routingInfo.shuffled().fold(listOf()) { previous, current ->
if (previous.flatMap { it.hints }.size + current.hints.size <= 4) {
previous + current
} else {
previous
}
}.map { it.hints }
return NodeRelayPayload(
TlvStream(
buildSet {
add(OnionPaymentPayloadTlv.AmountToForward(amount))
add(OnionPaymentPayloadTlv.OutgoingCltv(expiry))
add(OnionPaymentPayloadTlv.OutgoingNodeId(targetNodeId))
add(OnionPaymentPayloadTlv.PaymentData(invoice.paymentSecret, totalAmount))
invoice.paymentMetadata?.let { add(OnionPaymentPayloadTlv.PaymentMetadata(it)) }
add(OnionPaymentPayloadTlv.InvoiceFeatures(invoice.features))
add(OnionPaymentPayloadTlv.InvoiceRoutingInfo(prunedRoutingHints))
}
)
)
}
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy