Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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 @@ -47,6 +47,8 @@ data class Bolt11Invoice(

val routingInfo: List<TaggedField.RoutingInfo> = tags.filterIsInstance<TaggedField.RoutingInfo>()

override val accountable: Boolean = tags.contains(TaggedField.Accountable)

init {
val f = features.invoiceFeatures()
require(f.hasFeature(Feature.VariableLengthOnion)) { "${Feature.VariableLengthOnion.rfcName} must be supported" }
Expand Down Expand Up @@ -140,7 +142,8 @@ data class Bolt11Invoice(
TaggedField.MinFinalCltvExpiry(minFinalCltvExpiryDelta.toInt()),
TaggedField.PaymentSecret(paymentSecret),
// We remove unknown features which could make the invoice too big.
TaggedField.Features(features.invoiceFeatures().copy(unknown = setOf()).toByteArray().toByteVector())
TaggedField.Features(features.invoiceFeatures().copy(unknown = setOf()).toByteArray().toByteVector()),
TaggedField.Accountable
)
description.left?.let { tags.add(TaggedField.Description(it)) }
description.right?.let { tags.add(TaggedField.DescriptionHash(it)) }
Expand Down Expand Up @@ -198,6 +201,7 @@ data class Bolt11Invoice(
TaggedField.Features.tag -> tags.add(kotlin.runCatching { TaggedField.Features.decode(value) }.getOrDefault(TaggedField.InvalidTag(tag, value)))
TaggedField.RoutingInfo.tag -> tags.add(kotlin.runCatching { TaggedField.RoutingInfo.decode(value) }.getOrDefault(TaggedField.InvalidTag(tag, value)))
TaggedField.NodeId.tag -> tags.add(kotlin.runCatching { TaggedField.NodeId.decode(value) }.getOrDefault(TaggedField.InvalidTag(tag, value)))
TaggedField.Accountable.tag -> tags.add(if (value.isEmpty()) TaggedField.Accountable else TaggedField.InvalidTag(tag, value))
else -> tags.add(TaggedField.UnknownTag(tag, value))
}
loop(input.drop(3 + len))
Expand Down Expand Up @@ -518,6 +522,12 @@ data class Bolt11Invoice(
}
}

/** Present if the recipient is willing to be held accountable for the timely resolution of HTLCs. */
data object Accountable : TaggedField() {
override val tag: Int5 = 31
override fun encode(): List<Int5> = emptyList()
}

/** Unknown tag (may or may not be valid) */
data class UnknownTag(override val tag: Int5, val value: List<Int5>) : TaggedField() {
override fun encode(): List<Int5> = value.toList()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,12 @@ import fr.acinq.bitcoin.*
import fr.acinq.bitcoin.utils.Either
import fr.acinq.bitcoin.utils.Try
import fr.acinq.bitcoin.utils.runTrying
import fr.acinq.lightning.Feature
import fr.acinq.lightning.FeatureSupport
import fr.acinq.lightning.Features
import fr.acinq.lightning.Features.Companion.invoke
import fr.acinq.lightning.MilliSatoshi
import fr.acinq.lightning.utils.currentTimestampSeconds
import fr.acinq.lightning.utils.toByteVector
import fr.acinq.lightning.wire.*
import fr.acinq.lightning.wire.OfferTypes.InvoiceAccountable
import fr.acinq.lightning.wire.OfferTypes.ContactInfo.BlindedPath
import fr.acinq.lightning.wire.OfferTypes.FallbackAddress
import fr.acinq.lightning.wire.OfferTypes.InvoiceAmount
Expand Down Expand Up @@ -50,6 +48,8 @@ data class Bolt12Invoice(val records: TlvStream<InvoiceTlv>) : PaymentRequest()
val fallbacks: List<FallbackAddress>? = records.get<InvoiceFallbacks>()?.addresses
val signature: ByteVector64 = records.get<Signature>()!!.signature

override val accountable: Boolean = records.get<InvoiceAccountable>() != null

override fun isExpired(currentTimestampSeconds: Long): Boolean = createdAtSeconds + relativeExpirySeconds <= currentTimestampSeconds

// It is assumed that the request is valid for this offer.
Expand Down Expand Up @@ -112,6 +112,7 @@ data class Bolt12Invoice(val records: TlvStream<InvoiceTlv>) : PaymentRequest()
val amount = request.amount ?: (request.offer.amount!! * request.quantity)
val tlvs: Set<InvoiceTlv> = removeSignature(request.records).records + setOfNotNull(
InvoicePaths(paths.map { it.route }),
InvoiceAccountable,
InvoiceBlindedPay(paths.map { it.paymentInfo }),
InvoiceCreatedAt(currentTimestampSeconds()),
InvoiceRelativeExpiry(invoiceExpirySeconds),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ object IncomingPaymentPacket {
else -> {
// We merge contents from the outer and inner payloads.
// We must use the inner payload's total amount and payment secret because the payment may be split between multiple trampoline payments (#reckless).
Either.Right(PaymentOnion.FinalPayload.Standard.createMultiPartPayload(outerPayload.amount, innerPayload.totalAmount, outerPayload.expiry, innerPayload.paymentSecret, innerPayload.paymentMetadata))
Either.Right(PaymentOnion.FinalPayload.Standard.createMultiPartPayload(outerPayload.amount, innerPayload.totalAmount, outerPayload.expiry, innerPayload.paymentSecret, innerPayload.paymentMetadata, innerPayload.upgradeAccountability))
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ class OfferManager(val nodeParams: NodeParams, val walletParams: WalletParams, v
payerNote = truncatedPayerNote,
quantity = request.quantity_opt
).toPathId(nodeParams.nodePrivateKey)
val recipientPayload = RouteBlindingEncryptedData(TlvStream(RouteBlindingEncryptedDataTlv.PathId(metadata))).write().toByteVector()
val recipientPayload = RouteBlindingEncryptedData(TlvStream(RouteBlindingEncryptedDataTlv.UpgradeAccountability, RouteBlindingEncryptedDataTlv.PathId(metadata))).write().toByteVector()
val cltvExpiryDelta = remoteChannelUpdates.maxOfOrNull { it.cltvExpiryDelta } ?: walletParams.invoiceDefaultRoutingFees.cltvExpiryDelta
val paymentInfo = OfferTypes.PaymentInfo(
feeBase = remoteChannelUpdates.maxOfOrNull { it.feeBaseMsat } ?: walletParams.invoiceDefaultRoutingFees.feeBase,
Expand All @@ -189,6 +189,7 @@ class OfferManager(val nodeParams: NodeParams, val walletParams: WalletParams, v
val pathExpiry = (paymentInfo.cltvExpiryDelta + CltvExpiryDelta(720) + (nodeParams.bolt12InvoiceExpiry.inWholeMinutes.toInt() / 10)).toCltvExpiry(currentBlockHeight.toLong())
val remoteNodePayload = RouteBlindingEncryptedData(
TlvStream(
RouteBlindingEncryptedDataTlv.UpgradeAccountability,
RouteBlindingEncryptedDataTlv.OutgoingNodeId(EncodedNodeId.WithPublicKey.Wallet(nodeParams.nodeId)),
RouteBlindingEncryptedDataTlv.PaymentRelay(cltvExpiryDelta, paymentInfo.feeProportionalMillionths, paymentInfo.feeBase),
RouteBlindingEncryptedDataTlv.PaymentConstraints(pathExpiry, paymentInfo.minHtlc)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,16 +46,16 @@ object OutgoingPaymentPacket {
fun buildPacketToTrampolineRecipient(invoice: Bolt11Invoice, amount: MilliSatoshi, expiry: CltvExpiry, hop: NodeHop): Triple<MilliSatoshi, CltvExpiry, PacketAndSecrets> {
require(invoice.features.hasFeature(Feature.ExperimentalTrampolinePayment)) { "invoice must support trampoline" }
val trampolineOnion = run {
val finalPayload = PaymentOnion.FinalPayload.Standard.createSinglePartPayload(amount, expiry, invoice.paymentSecret, invoice.paymentMetadata)
val trampolinePayload = PaymentOnion.NodeRelayPayload.create(amount, expiry, hop.nextNodeId)
val finalPayload = PaymentOnion.FinalPayload.Standard.createSinglePartPayload(amount, expiry, invoice.paymentSecret, invoice.paymentMetadata, invoice.accountable)
val trampolinePayload = PaymentOnion.NodeRelayPayload.create(amount, expiry, hop.nextNodeId, invoice.accountable)
// We may be paying an older version of lightning-kmp that only supports trampoline packets of size 400.
buildOnion(listOf(hop.nodeId, hop.nextNodeId), listOf(trampolinePayload, finalPayload), invoice.paymentHash, payloadLength = 400)
}
val trampolineAmount = amount + hop.fee(amount)
val trampolineExpiry = expiry + hop.cltvExpiryDelta
// We generate a random secret to avoid leaking the invoice secret to the trampoline node.
val trampolinePaymentSecret = Lightning.randomBytes32()
val payload = PaymentOnion.FinalPayload.Standard.createTrampolinePayload(trampolineAmount, trampolineAmount, trampolineExpiry, trampolinePaymentSecret, trampolineOnion.packet)
val payload = PaymentOnion.FinalPayload.Standard.createTrampolinePayload(trampolineAmount, trampolineAmount, trampolineExpiry, trampolinePaymentSecret, trampolineOnion.packet, invoice.accountable)
val paymentOnion = buildOnion(listOf(hop.nodeId), listOf(payload), invoice.paymentHash, OnionRoutingPacket.PaymentPacketLength)
return Triple(trampolineAmount, trampolineExpiry, paymentOnion)
}
Expand All @@ -70,10 +70,10 @@ object OutgoingPaymentPacket {
fun buildPacketToTrampolinePeer(invoice: Bolt11Invoice, amount: MilliSatoshi, expiry: CltvExpiry): Triple<MilliSatoshi, CltvExpiry, PacketAndSecrets> {
require(invoice.features.hasFeature(Feature.ExperimentalTrampolinePayment)) { "invoice must support trampoline" }
val trampolineOnion = run {
val finalPayload = PaymentOnion.FinalPayload.Standard.createSinglePartPayload(amount, expiry, invoice.paymentSecret, invoice.paymentMetadata)
val finalPayload = PaymentOnion.FinalPayload.Standard.createSinglePartPayload(amount, expiry, invoice.paymentSecret, invoice.paymentMetadata, invoice.accountable)
buildOnion(listOf(invoice.nodeId), listOf(finalPayload), invoice.paymentHash)
}
val payload = PaymentOnion.FinalPayload.Standard.createTrampolinePayload(amount, amount, expiry, invoice.paymentSecret, trampolineOnion.packet)
val payload = PaymentOnion.FinalPayload.Standard.createTrampolinePayload(amount, amount, expiry, invoice.paymentSecret, trampolineOnion.packet, invoice.accountable)
val paymentOnion = buildOnion(listOf(invoice.nodeId), listOf(payload), invoice.paymentHash, OnionRoutingPacket.PaymentPacketLength)
return Triple(amount, expiry, paymentOnion)
}
Expand All @@ -93,21 +93,21 @@ object OutgoingPaymentPacket {
val trampolineOnion = run {
// NB: the final payload will never reach the recipient, since the trampoline node will convert that to a legacy payment.
// We use the smallest final payload possible, otherwise we may overflow the trampoline onion size.
val dummyFinalPayload = PaymentOnion.FinalPayload.Standard.createSinglePartPayload(amount, expiry, invoice.paymentSecret, null)
val dummyFinalPayload = PaymentOnion.FinalPayload.Standard.createSinglePartPayload(amount, expiry, invoice.paymentSecret, null, invoice.accountable)
var routingInfo = invoice.routingInfo
var trampolinePayload = PaymentOnion.RelayToNonTrampolinePayload.create(amount, amount, expiry, hop.nextNodeId, invoice, routingInfo)
var trampolinePayload = PaymentOnion.RelayToNonTrampolinePayload.create(amount, amount, expiry, hop.nextNodeId, invoice, routingInfo, invoice.accountable)
var trampolineOnion = buildOnion(listOf(hop.nodeId, hop.nextNodeId), listOf(trampolinePayload, dummyFinalPayload), invoice.paymentHash)
// Ensure that this onion can fit inside the outer 1300 bytes onion. The outer onion fields need ~150 bytes and we add some safety margin.
while (trampolineOnion.packet.payload.size() > 1000) {
routingInfo = routingInfo.dropLast(1)
trampolinePayload = PaymentOnion.RelayToNonTrampolinePayload.create(amount, amount, expiry, hop.nextNodeId, invoice, routingInfo)
trampolinePayload = PaymentOnion.RelayToNonTrampolinePayload.create(amount, amount, expiry, hop.nextNodeId, invoice, routingInfo, invoice.accountable)
trampolineOnion = buildOnion(listOf(hop.nodeId, hop.nextNodeId), listOf(trampolinePayload, dummyFinalPayload), invoice.paymentHash)
}
trampolineOnion
}
val trampolineAmount = amount + hop.fee(amount)
val trampolineExpiry = expiry + hop.cltvExpiryDelta
val payload = PaymentOnion.FinalPayload.Standard.createTrampolinePayload(trampolineAmount, trampolineAmount, trampolineExpiry, invoice.paymentSecret, trampolineOnion.packet)
val payload = PaymentOnion.FinalPayload.Standard.createTrampolinePayload(trampolineAmount, trampolineAmount, trampolineExpiry, invoice.paymentSecret, trampolineOnion.packet, invoice.accountable)
val paymentOnion = buildOnion(listOf(hop.nodeId), listOf(payload), invoice.paymentHash, OnionRoutingPacket.PaymentPacketLength)
return Triple(trampolineAmount, trampolineExpiry, paymentOnion)
}
Expand Down Expand Up @@ -140,7 +140,7 @@ object OutgoingPaymentPacket {
val trampolineExpiry = expiry + hop.cltvExpiryDelta
// We generate a random secret to avoid leaking the invoice secret to the trampoline node.
val trampolinePaymentSecret = Lightning.randomBytes32()
val payload = PaymentOnion.FinalPayload.Standard.createTrampolinePayload(trampolineAmount, trampolineAmount, trampolineExpiry, trampolinePaymentSecret, trampolineOnion.packet)
val payload = PaymentOnion.FinalPayload.Standard.createTrampolinePayload(trampolineAmount, trampolineAmount, trampolineExpiry, trampolinePaymentSecret, trampolineOnion.packet, invoice.accountable)
val paymentOnion = buildOnion(listOf(hop.nodeId), listOf(payload), invoice.paymentHash, OnionRoutingPacket.PaymentPacketLength)
return Triple(trampolineAmount, trampolineExpiry, paymentOnion)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ sealed class PaymentRequest {
abstract val paymentHash: ByteVector32
abstract val nodeId: PublicKey
abstract val features: Features
abstract val accountable: Boolean

abstract fun isExpired(currentTimestampSeconds: Long = currentTimestampSeconds()): Boolean

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,19 @@ object OfferTypes {
}
}

/**
* By setting this field, we let the payer know that we will resolve the payment quickly once we receive it.
* If we don't, our reputation will be negatively impacted (channel jamming protection).
*/
data object InvoiceAccountable : InvoiceTlv(), TlvValueReader<InvoiceAccountable> {
override val tag: Long = 161
override fun write(out: Output) {}
override fun read(input: Input): InvoiceAccountable {
require(input.availableBytes == 0)
return InvoiceAccountable
}
}

data class PaymentInfo(
val feeBase: MilliSatoshi,
val feeProportionalMillionths: Long,
Expand Down Expand Up @@ -1030,6 +1043,7 @@ object OfferTypes {
InvoiceRequestPayerNote.tag to InvoiceRequestPayerNote as TlvValueReader<InvoiceTlv>,
// Invoice part
InvoicePaths.tag to InvoicePaths as TlvValueReader<InvoiceTlv>,
InvoiceAccountable.tag to InvoiceAccountable as TlvValueReader<InvoiceTlv>,
InvoiceBlindedPay.tag to InvoiceBlindedPay as TlvValueReader<InvoiceTlv>,
InvoiceCreatedAt.tag to InvoiceCreatedAt as TlvValueReader<InvoiceTlv>,
InvoiceRelativeExpiry.tag to InvoiceRelativeExpiry as TlvValueReader<InvoiceTlv>,
Expand Down
Loading