Skip to content

Commit d8344fb

Browse files
committed
Update trampoline to match spec proposal
We update the trampoline feature to match the official specification from lightning/bolts#836. We remove support for the previous version of trampoline, which means that when paying nodes that use the experimental version, we will use the trampoline-to-non-trampoline flow instead. Similarly, when older nodes pay updated nodes, they won't understand the new trampoline feature bit and will use the trampoline-to-non-trampoline flow. We update the trampoline-to-non-trampoline flow to remove the unused trampoline payload in the onion, which saves some space. Note that we don't want to officially specify this scenario, as it leaks some data about the recipient to the trampoline node. We rather wait for nodes to either support trampoline or blinded paths, which fixes this issue.
1 parent ce6ecf4 commit d8344fb

File tree

19 files changed

+252
-169
lines changed

19 files changed

+252
-169
lines changed

src/commonMain/kotlin/fr/acinq/lightning/Features.kt

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,13 @@ sealed class Feature {
147147
override val scopes: Set<FeatureScope> get() = setOf(FeatureScope.Invoice)
148148
}
149149

150+
@Serializable
151+
object TrampolinePayment : Feature() {
152+
override val rfcName get() = "trampoline_routing"
153+
override val mandatory get() = 56
154+
override val scopes: Set<FeatureScope> get() = setOf(FeatureScope.Init, FeatureScope.Node, FeatureScope.Invoice)
155+
}
156+
150157
// The following features have not been standardised, hence the high feature bits to avoid conflicts.
151158

152159
/** This feature bit should be activated when a node accepts having their channel reserve set to 0. */
@@ -189,15 +196,6 @@ sealed class Feature {
189196
override val scopes: Set<FeatureScope> get() = setOf(FeatureScope.Init, FeatureScope.Node)
190197
}
191198

192-
// The version of trampoline enabled by this feature bit does not match the latest spec PR: once the spec is accepted,
193-
// we will introduce a new version of trampoline that will work in parallel to this one, until we can safely deprecate it.
194-
@Serializable
195-
object ExperimentalTrampolinePayment : Feature() {
196-
override val rfcName get() = "trampoline_payment_experimental"
197-
override val mandatory get() = 148
198-
override val scopes: Set<FeatureScope> get() = setOf(FeatureScope.Init, FeatureScope.Node, FeatureScope.Invoice)
199-
}
200-
201199
@Serializable
202200
object ExperimentalSplice : Feature() {
203201
override val rfcName get() = "splice_experimental"
@@ -288,7 +286,7 @@ data class Features(val activated: Map<Feature, FeatureSupport>, val unknown: Se
288286
Feature.Quiescence,
289287
Feature.ChannelType,
290288
Feature.PaymentMetadata,
291-
Feature.ExperimentalTrampolinePayment,
289+
Feature.TrampolinePayment,
292290
Feature.ZeroReserveChannels,
293291
Feature.WakeUpNotificationClient,
294292
Feature.WakeUpNotificationProvider,
@@ -327,7 +325,7 @@ data class Features(val activated: Map<Feature, FeatureSupport>, val unknown: Se
327325
Feature.PaymentSecret to listOf(Feature.VariableLengthOnion),
328326
Feature.BasicMultiPartPayment to listOf(Feature.PaymentSecret),
329327
Feature.AnchorOutputs to listOf(Feature.StaticRemoteKey),
330-
Feature.ExperimentalTrampolinePayment to listOf(Feature.PaymentSecret),
328+
Feature.TrampolinePayment to listOf(Feature.BasicMultiPartPayment),
331329
Feature.OnTheFlyFunding to listOf(Feature.ExperimentalSplice),
332330
Feature.FundingFeeCredit to listOf(Feature.OnTheFlyFunding)
333331
)

src/commonMain/kotlin/fr/acinq/lightning/NodeParams.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ data class NodeParams(
199199
Feature.Quiescence to FeatureSupport.Mandatory,
200200
Feature.ChannelType to FeatureSupport.Mandatory,
201201
Feature.PaymentMetadata to FeatureSupport.Optional,
202-
Feature.ExperimentalTrampolinePayment to FeatureSupport.Optional,
202+
Feature.TrampolinePayment to FeatureSupport.Optional,
203203
Feature.ZeroReserveChannels to FeatureSupport.Optional,
204204
Feature.WakeUpNotificationClient to FeatureSupport.Optional,
205205
Feature.ChannelBackupClient to FeatureSupport.Optional,

src/commonMain/kotlin/fr/acinq/lightning/payment/OutgoingPaymentHandler.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,7 @@ class OutgoingPaymentHandler(val nodeParams: NodeParams, val walletParams: Walle
315315
if (request.recipient == walletParams.trampolineNode.id) {
316316
// We are directly paying our trampoline node.
317317
OutgoingPaymentPacket.buildPacketToTrampolinePeer(paymentRequest, request.amount, expiry)
318-
} else if (invoiceFeatures.hasFeature(Feature.ExperimentalTrampolinePayment)) {
318+
} else if (invoiceFeatures.hasFeature(Feature.TrampolinePayment)) {
319319
OutgoingPaymentPacket.buildPacketToTrampolineRecipient(paymentRequest, request.amount, expiry, hop)
320320
} else {
321321
OutgoingPaymentPacket.buildPacketToLegacyRecipient(paymentRequest, request.amount, expiry, hop)

src/commonMain/kotlin/fr/acinq/lightning/payment/OutgoingPaymentPacket.kt

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ object OutgoingPaymentPacket {
2626
return buildOnion(sessionKey, nodes, payloads, associatedData, payloadLength)
2727
}
2828

29-
private fun buildOnion(sessionKey: PrivateKey, nodes: List<PublicKey>, payloads: List<PaymentOnion.PerHopPayload>, associatedData: ByteVector32, payloadLength: Int? = null): PacketAndSecrets {
29+
fun buildOnion(sessionKey: PrivateKey, nodes: List<PublicKey>, payloads: List<PaymentOnion.PerHopPayload>, associatedData: ByteVector32, payloadLength: Int? = null): PacketAndSecrets {
3030
require(nodes.size == payloads.size)
3131
val payloadsBin = payloads.map { it.write() }
3232
val totalPayloadLength = payloadLength ?: payloadsBin.sumOf { it.size + Sphinx.MacLength }
@@ -43,12 +43,11 @@ object OutgoingPaymentPacket {
4343
* @param hop the trampoline hop from the trampoline node to the recipient.
4444
*/
4545
fun buildPacketToTrampolineRecipient(invoice: Bolt11Invoice, amount: MilliSatoshi, expiry: CltvExpiry, hop: NodeHop): Triple<MilliSatoshi, CltvExpiry, PacketAndSecrets> {
46-
require(invoice.features.hasFeature(Feature.ExperimentalTrampolinePayment)) { "invoice must support trampoline" }
46+
require(invoice.features.hasFeature(Feature.TrampolinePayment)) { "invoice must support trampoline" }
4747
val trampolineOnion = run {
4848
val finalPayload = PaymentOnion.FinalPayload.Standard.createSinglePartPayload(amount, expiry, invoice.paymentSecret, invoice.paymentMetadata)
4949
val trampolinePayload = PaymentOnion.NodeRelayPayload.create(amount, expiry, hop.nextNodeId)
50-
// We may be paying an older version of lightning-kmp that only supports trampoline packets of size 400.
51-
buildOnion(listOf(hop.nodeId, hop.nextNodeId), listOf(trampolinePayload, finalPayload), invoice.paymentHash, payloadLength = 400)
50+
buildOnion(listOf(hop.nodeId, hop.nextNodeId), listOf(trampolinePayload, finalPayload), invoice.paymentHash)
5251
}
5352
val trampolineAmount = amount + hop.fee(amount)
5453
val trampolineExpiry = expiry + hop.cltvExpiryDelta
@@ -67,7 +66,7 @@ object OutgoingPaymentPacket {
6766
* @param expiry cltv expiry that should be received by the final recipient.
6867
*/
6968
fun buildPacketToTrampolinePeer(invoice: Bolt11Invoice, amount: MilliSatoshi, expiry: CltvExpiry): Triple<MilliSatoshi, CltvExpiry, PacketAndSecrets> {
70-
require(invoice.features.hasFeature(Feature.ExperimentalTrampolinePayment)) { "invoice must support trampoline" }
69+
require(invoice.features.hasFeature(Feature.TrampolinePayment)) { "invoice must support trampoline" }
7170
val trampolineOnion = run {
7271
val finalPayload = PaymentOnion.FinalPayload.Standard.createSinglePartPayload(amount, expiry, invoice.paymentSecret, invoice.paymentMetadata)
7372
buildOnion(listOf(invoice.nodeId), listOf(finalPayload), invoice.paymentHash)
@@ -90,17 +89,14 @@ object OutgoingPaymentPacket {
9089
*/
9190
fun buildPacketToLegacyRecipient(invoice: Bolt11Invoice, amount: MilliSatoshi, expiry: CltvExpiry, hop: NodeHop): Triple<MilliSatoshi, CltvExpiry, PacketAndSecrets> {
9291
val trampolineOnion = run {
93-
// NB: the final payload will never reach the recipient, since the trampoline node will convert that to a legacy payment.
94-
// We use the smallest final payload possible, otherwise we may overflow the trampoline onion size.
95-
val dummyFinalPayload = PaymentOnion.FinalPayload.Standard.createSinglePartPayload(amount, expiry, invoice.paymentSecret, null)
9692
var routingInfo = invoice.routingInfo
9793
var trampolinePayload = PaymentOnion.RelayToNonTrampolinePayload.create(amount, amount, expiry, hop.nextNodeId, invoice, routingInfo)
98-
var trampolineOnion = buildOnion(listOf(hop.nodeId, hop.nextNodeId), listOf(trampolinePayload, dummyFinalPayload), invoice.paymentHash)
94+
var trampolineOnion = buildOnion(listOf(hop.nodeId), listOf(trampolinePayload), invoice.paymentHash)
9995
// 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.
10096
while (trampolineOnion.packet.payload.size() > 1000) {
10197
routingInfo = routingInfo.dropLast(1)
10298
trampolinePayload = PaymentOnion.RelayToNonTrampolinePayload.create(amount, amount, expiry, hop.nextNodeId, invoice, routingInfo)
103-
trampolineOnion = buildOnion(listOf(hop.nodeId, hop.nextNodeId), listOf(trampolinePayload, dummyFinalPayload), invoice.paymentHash)
99+
trampolineOnion = buildOnion(listOf(hop.nodeId), listOf(trampolinePayload), invoice.paymentHash)
104100
}
105101
trampolineOnion
106102
}

src/commonMain/kotlin/fr/acinq/lightning/wire/PaymentOnion.kt

Lines changed: 32 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,17 @@ sealed class OnionPaymentPayloadTlv : Tlv {
9898
}
9999
}
100100

101+
/** Id of the next node. */
102+
data class OutgoingNodeId(val nodeId: PublicKey) : OnionPaymentPayloadTlv() {
103+
override val tag: Long get() = OutgoingNodeId.tag
104+
override fun write(out: Output) = LightningCodecs.writeBytes(nodeId.value, out)
105+
106+
companion object : TlvValueReader<OutgoingNodeId> {
107+
const val tag: Long = 14
108+
override fun read(input: Input): OutgoingNodeId = OutgoingNodeId(PublicKey(LightningCodecs.bytes(input, 33)))
109+
}
110+
}
111+
101112
/**
102113
* When payment metadata is included in a Bolt 9 invoice, we should send it as-is to the recipient.
103114
* This lets recipients generate invoices without having to store anything on their side until the invoice is paid.
@@ -125,6 +136,20 @@ sealed class OnionPaymentPayloadTlv : Tlv {
125136
}
126137
}
127138

139+
/** An encrypted trampoline onion packet. */
140+
data class TrampolineOnion(val packet: OnionRoutingPacket) : OnionPaymentPayloadTlv() {
141+
override val tag: Long get() = TrampolineOnion.tag
142+
override fun write(out: Output) = OnionRoutingPacketSerializer(packet.payload.size()).write(packet, out)
143+
144+
companion object : TlvValueReader<TrampolineOnion> {
145+
const val tag: Long = 20
146+
override fun read(input: Input): TrampolineOnion {
147+
val payloadLength = input.availableBytes - 66 // 1 byte version + 33 bytes public key + 32 bytes HMAC
148+
return TrampolineOnion(OnionRoutingPacketSerializer(payloadLength).read(input))
149+
}
150+
}
151+
}
152+
128153
/**
129154
* Invoice feature bits. Only included for intermediate trampoline nodes when they should convert to a legacy payment
130155
* because the final recipient doesn't support trampoline.
@@ -139,17 +164,6 @@ sealed class OnionPaymentPayloadTlv : Tlv {
139164
}
140165
}
141166

142-
/** Id of the next node. */
143-
data class OutgoingNodeId(val nodeId: PublicKey) : OnionPaymentPayloadTlv() {
144-
override val tag: Long get() = OutgoingNodeId.tag
145-
override fun write(out: Output) = LightningCodecs.writeBytes(nodeId.value, out)
146-
147-
companion object : TlvValueReader<OutgoingNodeId> {
148-
const val tag: Long = 66098
149-
override fun read(input: Input): OutgoingNodeId = OutgoingNodeId(PublicKey(LightningCodecs.bytes(input, 33)))
150-
}
151-
}
152-
153167
/**
154168
* Invoice routing hints. Only included for intermediate trampoline nodes when they should convert to a legacy payment
155169
* because the final recipient doesn't support trampoline.
@@ -191,20 +205,6 @@ sealed class OnionPaymentPayloadTlv : Tlv {
191205
}
192206
}
193207

194-
/** An encrypted trampoline onion packet. */
195-
data class TrampolineOnion(val packet: OnionRoutingPacket) : OnionPaymentPayloadTlv() {
196-
override val tag: Long get() = TrampolineOnion.tag
197-
override fun write(out: Output) = OnionRoutingPacketSerializer(packet.payload.size()).write(packet, out)
198-
199-
companion object : TlvValueReader<TrampolineOnion> {
200-
const val tag: Long = 66100
201-
override fun read(input: Input): TrampolineOnion {
202-
val payloadLength = input.availableBytes - 66 // 1 byte version + 33 bytes public key + 32 bytes HMAC
203-
return TrampolineOnion(OnionRoutingPacketSerializer(payloadLength).read(input))
204-
}
205-
}
206-
}
207-
208208
/** Blinded paths to relay the payment to */
209209
data class OutgoingBlindedPaths(val paths: List<Bolt12Invoice.Companion.PaymentBlindedContactInfo>) : OnionPaymentPayloadTlv() {
210210
override val tag: Long get() = OutgoingBlindedPaths.tag
@@ -249,15 +249,15 @@ object PaymentOnion {
249249
OnionPaymentPayloadTlv.AmountToForward.tag to OnionPaymentPayloadTlv.AmountToForward.Companion as TlvValueReader<OnionPaymentPayloadTlv>,
250250
OnionPaymentPayloadTlv.OutgoingCltv.tag to OnionPaymentPayloadTlv.OutgoingCltv.Companion as TlvValueReader<OnionPaymentPayloadTlv>,
251251
OnionPaymentPayloadTlv.OutgoingChannelId.tag to OnionPaymentPayloadTlv.OutgoingChannelId.Companion as TlvValueReader<OnionPaymentPayloadTlv>,
252+
OnionPaymentPayloadTlv.OutgoingNodeId.tag to OnionPaymentPayloadTlv.OutgoingNodeId.Companion as TlvValueReader<OnionPaymentPayloadTlv>,
252253
OnionPaymentPayloadTlv.PaymentData.tag to OnionPaymentPayloadTlv.PaymentData.Companion as TlvValueReader<OnionPaymentPayloadTlv>,
253254
OnionPaymentPayloadTlv.EncryptedRecipientData.tag to OnionPaymentPayloadTlv.EncryptedRecipientData.Companion as TlvValueReader<OnionPaymentPayloadTlv>,
254255
OnionPaymentPayloadTlv.BlindingPoint.tag to OnionPaymentPayloadTlv.BlindingPoint.Companion as TlvValueReader<OnionPaymentPayloadTlv>,
255256
OnionPaymentPayloadTlv.PaymentMetadata.tag to OnionPaymentPayloadTlv.PaymentMetadata.Companion as TlvValueReader<OnionPaymentPayloadTlv>,
256257
OnionPaymentPayloadTlv.TotalAmount.tag to OnionPaymentPayloadTlv.TotalAmount.Companion as TlvValueReader<OnionPaymentPayloadTlv>,
258+
OnionPaymentPayloadTlv.TrampolineOnion.tag to OnionPaymentPayloadTlv.TrampolineOnion.Companion as TlvValueReader<OnionPaymentPayloadTlv>,
257259
OnionPaymentPayloadTlv.InvoiceFeatures.tag to OnionPaymentPayloadTlv.InvoiceFeatures.Companion as TlvValueReader<OnionPaymentPayloadTlv>,
258-
OnionPaymentPayloadTlv.OutgoingNodeId.tag to OnionPaymentPayloadTlv.OutgoingNodeId.Companion as TlvValueReader<OnionPaymentPayloadTlv>,
259260
OnionPaymentPayloadTlv.InvoiceRoutingInfo.tag to OnionPaymentPayloadTlv.InvoiceRoutingInfo.Companion as TlvValueReader<OnionPaymentPayloadTlv>,
260-
OnionPaymentPayloadTlv.TrampolineOnion.tag to OnionPaymentPayloadTlv.TrampolineOnion.Companion as TlvValueReader<OnionPaymentPayloadTlv>,
261261
OnionPaymentPayloadTlv.OutgoingBlindedPaths.tag to OnionPaymentPayloadTlv.OutgoingBlindedPaths.Companion as TlvValueReader<OnionPaymentPayloadTlv>,
262262
)
263263
)
@@ -457,20 +457,15 @@ object PaymentOnion {
457457
}
458458
}
459459

460+
/**
461+
* Create a trampoline payload to tell our trampoline node to relay to a Bolt 11 recipient that doesn't support trampoline.
462+
* This reveals to our trampoline node who the recipient is and details from the invoice.
463+
* This must be deprecated once recipients support either trampoline or blinded paths.
464+
*/
460465
data class RelayToNonTrampolinePayload(val records: TlvStream<OnionPaymentPayloadTlv>) : PerHopPayload() {
461466
val amountToForward = records.get<OnionPaymentPayloadTlv.AmountToForward>()!!.amount
462467
val outgoingCltv = records.get<OnionPaymentPayloadTlv.OutgoingCltv>()!!.cltv
463468
val outgoingNodeId = records.get<OnionPaymentPayloadTlv.OutgoingNodeId>()!!.nodeId
464-
val totalAmount = run {
465-
val paymentData = records.get<OnionPaymentPayloadTlv.PaymentData>()
466-
when {
467-
paymentData == null -> amountToForward
468-
paymentData.totalAmount == MilliSatoshi(0) -> amountToForward
469-
else -> paymentData.totalAmount
470-
}
471-
}
472-
473-
// NB: the following fields are only included in the trampoline-to-legacy case.
474469
val paymentSecret = records.get<OnionPaymentPayloadTlv.PaymentData>()!!.secret
475470
val paymentMetadata = records.get<OnionPaymentPayloadTlv.PaymentMetadata>()?.data
476471
val invoiceFeatures = records.get<OnionPaymentPayloadTlv.InvoiceFeatures>()!!.features

src/commonTest/kotlin/fr/acinq/lightning/payment/OutgoingPaymentHandlerTestsCommon.kt

Lines changed: 2 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -240,35 +240,6 @@ class OutgoingPaymentHandlerTestsCommon : LightningTestSuite() {
240240
val recipientKey = randomKey()
241241
val invoice = makeInvoice(amount = 195_000.msat, supportsTrampoline = true, privKey = recipientKey)
242242
val payment = PayInvoice(UUID.randomUUID(), 200_000.msat, LightningOutgoingPayment.Details.Normal(invoice)) // we slightly overpay the invoice amount
243-
testSinglePartTrampolinePayment(payment, invoice, recipientKey)
244-
}
245-
246-
@Test
247-
fun `successful first attempt -- backwards-compatibility trampoline bit`() = runSuspendTest {
248-
val recipientKey = randomKey()
249-
val invoice = run {
250-
// Invoices generated by older versions of wallets based on lightning-kmp will generate invoices with the following feature bits.
251-
val invoiceFeatures = mapOf(
252-
Feature.VariableLengthOnion to FeatureSupport.Optional,
253-
Feature.PaymentSecret to FeatureSupport.Mandatory,
254-
Feature.BasicMultiPartPayment to FeatureSupport.Optional,
255-
Feature.ExperimentalTrampolinePayment to FeatureSupport.Optional
256-
)
257-
Bolt11Invoice.create(
258-
chain = Chain.Mainnet,
259-
amount = 195_000.msat,
260-
paymentHash = randomBytes32(),
261-
privateKey = recipientKey,
262-
description = Either.Left("trampoline backwards-compatibility"),
263-
minFinalCltvExpiryDelta = Bolt11Invoice.DEFAULT_MIN_FINAL_EXPIRY_DELTA,
264-
features = Features(invoiceFeatures.toMap()),
265-
)
266-
}
267-
val payment = PayInvoice(UUID.randomUUID(), 200_000.msat, LightningOutgoingPayment.Details.Normal(invoice)) // we slightly overpay the invoice amount
268-
testSinglePartTrampolinePayment(payment, invoice, recipientKey)
269-
}
270-
271-
private suspend fun testSinglePartTrampolinePayment(payment: PayInvoice, invoice: Bolt11Invoice, recipientKey: PrivateKey) {
272243
val (alice, _) = TestsHelper.reachNormal()
273244
val walletParams = defaultWalletParams.copy(trampolineFees = listOf(TrampolineFees(3.sat, 10_000, CltvExpiryDelta(144))))
274245
val outgoingPaymentHandler = OutgoingPaymentHandler(TestConstants.Alice.nodeParams, walletParams, InMemoryPaymentsDb())
@@ -336,9 +307,8 @@ class OutgoingPaymentHandlerTestsCommon : LightningTestSuite() {
336307
assertEquals(300_000.msat, innerB.amountToForward)
337308
assertEquals(CltvExpiry(TestConstants.defaultBlockHeight.toLong()) + Bolt11Invoice.DEFAULT_MIN_FINAL_EXPIRY_DELTA, innerB.outgoingCltv)
338309
assertEquals(payment.recipient, innerB.outgoingNodeId)
339-
assertEquals(invoice.paymentSecret, innerB.paymentSecret)
310+
assertEquals(invoice.paymentSecret, outerB.paymentSecret)
340311
assertEquals(invoice.features.toByteArray().toByteVector(), innerB.invoiceFeatures)
341-
assertFalse(innerB.invoiceRoutingInfo.isEmpty())
342312
assertEquals(invoice.routingInfo.map { it.hints }, innerB.invoiceRoutingInfo)
343313

344314
val preimage = randomBytes32()
@@ -616,7 +586,7 @@ class OutgoingPaymentHandlerTestsCommon : LightningTestSuite() {
616586
put(Feature.VariableLengthOnion, FeatureSupport.Optional)
617587
put(Feature.PaymentSecret, FeatureSupport.Mandatory)
618588
put(Feature.BasicMultiPartPayment, FeatureSupport.Optional)
619-
if (supportsTrampoline) put(Feature.ExperimentalTrampolinePayment, FeatureSupport.Optional)
589+
if (supportsTrampoline) put(Feature.TrampolinePayment, FeatureSupport.Optional)
620590
}
621591
return Bolt11Invoice.create(
622592
chain = Chain.Mainnet,

0 commit comments

Comments
 (0)