Skip to content

Commit fc3c459

Browse files
committed
Add support for official trampoline payments
We add support for the official version of trampoline payments, as specified in lightning/bolts#836. We keep supporting trampoline payments that use the legacy protocol to allow a smooth transition. We hardcode the legacy feature bit 149 in a few places to make this work, which is a bit hacky but simple and should be removed 6 months after releasing the official version. We also keep supporting payments from trampoline wallets to nodes that don't support trampoline: this is bad from a privacy standpoint, but will be fixed when recipients start supporting Bolt 12.
1 parent 304290d commit fc3c459

File tree

19 files changed

+317
-138
lines changed

19 files changed

+317
-138
lines changed

docs/release-notes/eclair-vnext.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,20 @@ Every node advertizes the rates at which they sell their liquidity, and buyers c
1313
The liquidity ads specification is still under review and will likely change.
1414
This feature isn't meant to be used on mainnet yet and is thus disabled by default.
1515

16+
### Trampoline payments
17+
18+
Trampoline payments allow nodes running on constrained devices to sync only a small portion of the network and leverage trampoline nodes to calculate the missing parts of the payment route, while providing the same privacy as fully source-routed payments.
19+
20+
Eclair started supporting [trampoline payments](https://github.com/lightning/bolts/pull/829) in v0.3.3.
21+
The specification has evolved since then and has recently been added to the [BOLTs](https://github.com/lightning/bolts/pull/836).
22+
23+
With this release, eclair nodes are able to relay and receive trampoline payments (activated by default).
24+
This feature can be disabled if you don't want to relay or receive trampoline payments:
25+
26+
```conf
27+
eclair.features.trampoline_routing = disabled
28+
```
29+
1630
### Update minimal version of Bitcoin Core
1731

1832
With this release, eclair requires using Bitcoin Core 27.2.

eclair-core/src/main/resources/reference.conf

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@ eclair {
4747
node-alias = "eclair"
4848
node-color = "49daaa"
4949

50-
trampoline-payments-enable = false // TODO: @t-bast: once spec-ed this should use a global feature flag
5150
// see https://github.com/lightningnetwork/lightning-rfc/blob/master/09-features.md
5251
features {
5352
// option_upfront_shutdown_script is not activated by default.
@@ -81,7 +80,7 @@ eclair {
8180
// node that you trust using override-init-features (see below).
8281
option_zeroconf = disabled
8382
keysend = disabled
84-
trampoline_payment_prototype = disabled
83+
trampoline_routing = optional
8584
async_payment_prototype = disabled
8685
on_the_fly_funding = disabled
8786
}

eclair-core/src/main/scala/fr/acinq/eclair/Features.scala

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -300,15 +300,9 @@ object Features {
300300
val mandatory = 54
301301
}
302302

303-
// TODO: @t-bast: update feature bits once spec-ed (currently reserved here: https://github.com/lightningnetwork/lightning-rfc/issues/605)
304-
// We're not advertising these bits yet in our announcements, clients have to assume support.
305-
// This is why we haven't added them yet to `areSupported`.
306-
// The version of trampoline enabled by this feature bit does not match the latest spec PR: once the spec is accepted,
307-
// we will introduce a new version of trampoline that will work in parallel to this legacy one, until we can safely
308-
// deprecate it.
309-
case object TrampolinePaymentPrototype extends Feature with InitFeature with NodeFeature with Bolt11Feature {
310-
val rfcName = "trampoline_payment_prototype"
311-
val mandatory = 148
303+
case object TrampolinePayment extends Feature with InitFeature with NodeFeature with Bolt11Feature with Bolt12Feature {
304+
val rfcName = "trampoline_routing"
305+
val mandatory = 56
312306
}
313307

314308
// TODO: @remyers update feature bits once spec-ed (currently reserved here: https://github.com/lightning/bolts/pull/989)
@@ -363,7 +357,7 @@ object Features {
363357
PaymentMetadata,
364358
ZeroConf,
365359
KeySend,
366-
TrampolinePaymentPrototype,
360+
TrampolinePayment,
367361
AsyncPaymentPrototype,
368362
SplicePrototype,
369363
OnTheFlyFunding,
@@ -378,9 +372,9 @@ object Features {
378372
AnchorOutputs -> (StaticRemoteKey :: Nil),
379373
AnchorOutputsZeroFeeHtlcTx -> (StaticRemoteKey :: Nil),
380374
RouteBlinding -> (VariableLengthOnion :: Nil),
381-
TrampolinePaymentPrototype -> (PaymentSecret :: Nil),
375+
TrampolinePayment -> (BasicMultiPartPayment :: Nil),
382376
KeySend -> (VariableLengthOnion :: Nil),
383-
AsyncPaymentPrototype -> (TrampolinePaymentPrototype :: Nil),
377+
AsyncPaymentPrototype -> (TrampolinePayment :: Nil),
384378
OnTheFlyFunding -> (SplicePrototype :: Nil),
385379
FundingFeeCredit -> (OnTheFlyFunding :: Nil)
386380
)

eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,6 @@ case class NodeParams(nodeKeyManager: NodeKeyManager,
8383
socksProxy_opt: Option[Socks5ProxyParams],
8484
maxPaymentAttempts: Int,
8585
paymentFinalExpiry: PaymentFinalExpiryConf,
86-
enableTrampolinePayment: Boolean,
8786
balanceCheckInterval: FiniteDuration,
8887
blockchainWatchdogThreshold: Int,
8988
blockchainWatchdogSources: Seq[String],
@@ -657,7 +656,6 @@ object NodeParams extends Logging {
657656
socksProxy_opt = socksProxy_opt,
658657
maxPaymentAttempts = config.getInt("max-payment-attempts"),
659658
paymentFinalExpiry = PaymentFinalExpiryConf(CltvExpiryDelta(config.getInt("send.recipient-final-expiry.min-delta")), CltvExpiryDelta(config.getInt("send.recipient-final-expiry.max-delta"))),
660-
enableTrampolinePayment = config.getBoolean("trampoline-payments-enable"),
661659
balanceCheckInterval = FiniteDuration(config.getDuration("balance-check-interval").getSeconds, TimeUnit.SECONDS),
662660
blockchainWatchdogThreshold = config.getInt("blockchain-watchdog.missing-blocks-threshold"),
663661
blockchainWatchdogSources = config.getStringList("blockchain-watchdog.sources").asScala.toSeq,

eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -158,8 +158,9 @@ object IncomingPaymentPacket {
158158
case None if add.pathKey_opt.isDefined => Left(InvalidOnionBlinding(Sphinx.hash(add.onionRoutingPacket)))
159159
case None =>
160160
// We check if the payment is using trampoline: if it is, we may not be the final recipient.
161-
payload.get[OnionPaymentPayloadTlv.TrampolineOnion] match {
162-
case Some(OnionPaymentPayloadTlv.TrampolineOnion(trampolinePacket)) =>
161+
val trampolinePacket_opt = payload.get[OnionPaymentPayloadTlv.TrampolineOnion].map(_.packet).orElse(payload.get[OnionPaymentPayloadTlv.LegacyTrampolineOnion].map(_.packet))
162+
trampolinePacket_opt match {
163+
case Some(trampolinePacket) =>
163164
// NB: when we enable blinded trampoline routes, we will need to check if the outer onion contains a
164165
// path key and use it to derive the decryption key for the blinded trampoline onion.
165166
decryptOnion(add.paymentHash, privateKey, trampolinePacket).flatMap {
@@ -308,7 +309,10 @@ object OutgoingPaymentPacket {
308309
* In that case, packetPayloadLength_opt must be greater than the actual onion's content.
309310
*/
310311
def buildOnion(payloads: Seq[NodePayload], associatedData: ByteVector32, packetPayloadLength_opt: Option[Int]): Either[OutgoingPaymentError, Sphinx.PacketAndSecrets] = {
311-
val sessionKey = randomKey()
312+
buildOnion(randomKey(), payloads, associatedData, packetPayloadLength_opt)
313+
}
314+
315+
def buildOnion(sessionKey: PrivateKey, payloads: Seq[NodePayload], associatedData: ByteVector32, packetPayloadLength_opt: Option[Int]): Either[OutgoingPaymentError, Sphinx.PacketAndSecrets] = {
312316
val nodeIds = payloads.map(_.nodeId)
313317
val payloadsBin = payloads
314318
.map(p => PaymentOnionCodecs.perHopPayloadCodec.encode(p.payload.records))

eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -329,11 +329,6 @@ object MultiPartHandler {
329329
val paymentHash = Crypto.sha256(paymentPreimage)
330330
val expirySeconds = r.expirySeconds_opt.getOrElse(nodeParams.invoiceExpiry.toSeconds)
331331
val paymentMetadata = hex"2a"
332-
val featuresTrampolineOpt = if (nodeParams.enableTrampolinePayment) {
333-
nodeParams.features.bolt11Features().add(Features.TrampolinePaymentPrototype, FeatureSupport.Optional)
334-
} else {
335-
nodeParams.features.bolt11Features()
336-
}
337332
val invoice = Bolt11Invoice(
338333
nodeParams.chainHash,
339334
r.amount_opt,
@@ -345,7 +340,7 @@ object MultiPartHandler {
345340
expirySeconds = Some(expirySeconds),
346341
extraHops = r.extraHops,
347342
paymentMetadata = Some(paymentMetadata),
348-
features = featuresTrampolineOpt
343+
features = nodeParams.features.bolt11Features()
349344
)
350345
context.log.debug("generated invoice={} from amount={}", invoice.toString, r.amount_opt)
351346
nodeParams.db.payments.addIncomingPayment(invoice, paymentPreimage, r.paymentType)

eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelay.scala

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ import fr.acinq.eclair.router.Router.{ChannelHop, HopRelayParams, Route, RoutePa
4242
import fr.acinq.eclair.router.{BalanceTooLow, RouteNotFound}
4343
import fr.acinq.eclair.wire.protocol.PaymentOnion.IntermediatePayload
4444
import fr.acinq.eclair.wire.protocol._
45-
import fr.acinq.eclair.{Alias, CltvExpiry, CltvExpiryDelta, EncodedNodeId, Features, InitFeature, Logs, MilliSatoshi, MilliSatoshiLong, NodeParams, TimestampMilli, UInt64, nodeFee, randomBytes32}
45+
import fr.acinq.eclair.{Alias, CltvExpiry, CltvExpiryDelta, EncodedNodeId, FeatureSupport, Features, InitFeature, InvoiceFeature, Logs, MilliSatoshi, MilliSatoshiLong, NodeParams, TimestampMilli, UInt64, UnknownFeature, nodeFee, randomBytes32}
4646

4747
import java.util.UUID
4848
import java.util.concurrent.TimeUnit
@@ -262,7 +262,9 @@ class NodeRelay private(nodeParams: NodeParams,
262262
nextPayload match {
263263
case payloadOut: IntermediatePayload.NodeRelay.Standard =>
264264
val paymentSecret = randomBytes32() // we generate a new secret to protect against probing attacks
265-
val recipient = ClearRecipient(payloadOut.outgoingNodeId, Features.empty, payloadOut.amountToForward, payloadOut.outgoingCltv, paymentSecret, nextTrampolineOnion_opt = nextPacket_opt)
265+
// If the recipient is using the legacy trampoline feature, we will use the legacy onion format.
266+
val features = if (payloadOut.isLegacy) Features(Map.empty[InvoiceFeature, FeatureSupport], Set(UnknownFeature(149))) else Features.empty[InvoiceFeature]
267+
val recipient = ClearRecipient(payloadOut.outgoingNodeId, features, payloadOut.amountToForward, payloadOut.outgoingCltv, paymentSecret, nextTrampolineOnion_opt = nextPacket_opt)
266268
context.log.debug("forwarding payment to the next trampoline node {}", recipient.nodeId)
267269
ensureRecipientReady(upstream, recipient, nextPayload, nextPacket_opt)
268270
case payloadOut: IntermediatePayload.NodeRelay.ToNonTrampoline =>

eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import fr.acinq.eclair.channel._
2929
import fr.acinq.eclair.db.PendingCommandsDb
3030
import fr.acinq.eclair.payment._
3131
import fr.acinq.eclair.wire.protocol._
32-
import fr.acinq.eclair.{CltvExpiryDelta, Logs, MilliSatoshi, NodeParams}
32+
import fr.acinq.eclair.{CltvExpiryDelta, Features, Logs, MilliSatoshi, NodeParams}
3333
import grizzled.slf4j.Logging
3434

3535
import scala.concurrent.Promise
@@ -71,7 +71,7 @@ class Relayer(nodeParams: NodeParams, router: ActorRef, register: ActorRef, paym
7171
case Right(r: IncomingPaymentPacket.ChannelRelayPacket) =>
7272
channelRelayer ! ChannelRelayer.Relay(r, originNode)
7373
case Right(r: IncomingPaymentPacket.NodeRelayPacket) =>
74-
if (!nodeParams.enableTrampolinePayment) {
74+
if (!nodeParams.features.hasFeature(Features.TrampolinePayment)) {
7575
log.warning(s"rejecting htlc #${add.id} from channelId=${add.channelId} reason=trampoline disabled")
7676
PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, add.channelId, CMD_FAIL_HTLC(add.id, Right(RequiredNodeFeatureMissing()), commit = true))
7777
} else {

eclair-core/src/main/scala/fr/acinq/eclair/payment/send/Recipient.scala

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import fr.acinq.eclair.payment.{Bolt11Invoice, Bolt12Invoice}
2525
import fr.acinq.eclair.router.Router._
2626
import fr.acinq.eclair.wire.protocol.PaymentOnion.{FinalPayload, IntermediatePayload, OutgoingBlindedPerHopPayload}
2727
import fr.acinq.eclair.wire.protocol.{GenericTlv, OnionRoutingPacket}
28-
import fr.acinq.eclair.{CltvExpiry, Features, InvoiceFeature, MilliSatoshi, ShortChannelId}
28+
import fr.acinq.eclair.{CltvExpiry, Features, InvoiceFeature, MilliSatoshi, ShortChannelId, UnknownFeature}
2929
import scodec.bits.ByteVector
3030

3131
/**
@@ -74,9 +74,13 @@ case class ClearRecipient(nodeId: PublicKey,
7474
paymentMetadata_opt: Option[ByteVector] = None,
7575
nextTrampolineOnion_opt: Option[OnionRoutingPacket] = None,
7676
customTlvs: Set[GenericTlv] = Set.empty) extends Recipient {
77+
// Feature bit used by the legacy trampoline feature.
78+
private val isLegacyTrampoline = features.unknown.contains(UnknownFeature(149))
79+
7780
override def buildPayloads(paymentHash: ByteVector32, route: Route): Either[OutgoingPaymentError, PaymentPayloads] = {
7881
ClearRecipient.validateRoute(nodeId, route).map(_ => {
7982
val finalPayload = nextTrampolineOnion_opt match {
83+
case Some(trampolinePacket) if isLegacyTrampoline => NodePayload(nodeId, FinalPayload.Standard.createLegacyTrampolinePayload(route.amount, totalAmount, expiry, paymentSecret, trampolinePacket))
8084
case Some(trampolinePacket) => NodePayload(nodeId, FinalPayload.Standard.createTrampolinePayload(route.amount, totalAmount, expiry, paymentSecret, trampolinePacket))
8185
case None => NodePayload(nodeId, FinalPayload.Standard.createPayload(route.amount, totalAmount, expiry, paymentSecret, paymentMetadata_opt, customTlvs))
8286
}

eclair-core/src/main/scala/fr/acinq/eclair/payment/send/TrampolinePaymentLifecycle.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ object TrampolinePayment {
211211
def buildOutgoingPayment(trampolineNodeId: PublicKey, invoice: Invoice, amount: MilliSatoshi, expiry: CltvExpiry, trampolinePaymentSecret: ByteVector32, attemptNumber: Int): OutgoingPayment = {
212212
val totalAmount = invoice.amount_opt.get
213213
val trampolineOnion = invoice match {
214-
case invoice: Bolt11Invoice if invoice.features.hasFeature(Features.TrampolinePaymentPrototype) =>
214+
case invoice: Bolt11Invoice if invoice.features.hasFeature(Features.TrampolinePayment) =>
215215
val finalPayload = PaymentOnion.FinalPayload.Standard.createPayload(amount, totalAmount, expiry, invoice.paymentSecret, invoice.paymentMetadata)
216216
val trampolinePayload = PaymentOnion.IntermediatePayload.NodeRelay.Standard(totalAmount, expiry, invoice.nodeId)
217217
buildOnion(NodePayload(trampolineNodeId, trampolinePayload) :: NodePayload(invoice.nodeId, finalPayload) :: Nil, invoice.paymentHash, None).toOption.get

0 commit comments

Comments
 (0)