Skip to content

Commit 10ee4e6

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 7372a87 commit 10ee4e6

File tree

19 files changed

+332
-142
lines changed

19 files changed

+332
-142
lines changed

docs/release-notes/eclair-vnext.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,20 @@
44

55
## Major changes
66

7+
### Trampoline payments
8+
9+
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.
10+
11+
Eclair started supporting [trampoline payments](https://github.com/lightning/bolts/pull/829) in v0.3.3.
12+
The specification has evolved since then and has recently been added to the [BOLTs](https://github.com/lightning/bolts/pull/836).
13+
14+
With this release, eclair nodes are able to relay and receive trampoline payments (activated by default).
15+
This feature can be disabled if you don't want to relay or receive trampoline payments:
16+
17+
```conf
18+
eclair.features.trampoline_routing = disabled
19+
```
20+
721
### New MPP splitting strategy
822

923
Eclair can send large payments using multiple low-capacity routes by sending as much as it can through each route (if `randomize-route-selection = false`) or some random fraction (if `randomize-route-selection = true`).

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

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

52-
trampoline-payments-enable = false // TODO: @t-bast: once spec-ed this should use a global feature flag
5352
// see https://github.com/lightningnetwork/lightning-rfc/blob/master/09-features.md
5453
features {
5554
// option_upfront_shutdown_script is not activated by default.
@@ -88,7 +87,7 @@ eclair {
8887
option_zeroconf = disabled
8988
keysend = disabled
9089
option_simple_close=optional
91-
trampoline_payment_prototype = disabled
90+
trampoline_routing = optional
9291
async_payment_prototype = disabled
9392
on_the_fly_funding = disabled
9493
}

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

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,11 @@ object Features {
307307
val mandatory = 54
308308
}
309309

310+
case object TrampolinePayment extends Feature with InitFeature with NodeFeature with Bolt11Feature with Bolt12Feature {
311+
val rfcName = "trampoline_routing"
312+
val mandatory = 56
313+
}
314+
310315
case object SimpleClose extends Feature with InitFeature with NodeFeature {
311316
val rfcName = "option_simple_close"
312317
val mandatory = 60
@@ -323,17 +328,6 @@ object Features {
323328
val mandatory = 132
324329
}
325330

326-
// TODO: @t-bast: update feature bits once spec-ed (currently reserved here: https://github.com/lightningnetwork/lightning-rfc/issues/605)
327-
// We're not advertising these bits yet in our announcements, clients have to assume support.
328-
// This is why we haven't added them yet to `areSupported`.
329-
// The version of trampoline enabled by this feature bit does not match the latest spec PR: once the spec is accepted,
330-
// we will introduce a new version of trampoline that will work in parallel to this legacy one, until we can safely
331-
// deprecate it.
332-
case object TrampolinePaymentPrototype extends Feature with InitFeature with NodeFeature with Bolt11Feature {
333-
val rfcName = "trampoline_payment_prototype"
334-
val mandatory = 148
335-
}
336-
337331
// TODO: @remyers update feature bits once spec-ed (currently reserved here: https://github.com/lightning/bolts/pull/989)
338332
case object AsyncPaymentPrototype extends Feature with InitFeature with Bolt11Feature {
339333
val rfcName = "async_payment_prototype"
@@ -402,7 +396,7 @@ object Features {
402396
SimpleTaprootChannelsPhoenix,
403397
SimpleTaprootChannelsStaging,
404398
WakeUpNotificationClient,
405-
TrampolinePaymentPrototype,
399+
TrampolinePayment,
406400
AsyncPaymentPrototype,
407401
SplicePrototype,
408402
OnTheFlyFunding,
@@ -418,11 +412,10 @@ object Features {
418412
AnchorOutputs -> (StaticRemoteKey :: Nil),
419413
AnchorOutputsZeroFeeHtlcTx -> (StaticRemoteKey :: Nil),
420414
RouteBlinding -> (VariableLengthOnion :: Nil),
421-
TrampolinePaymentPrototype -> (PaymentSecret :: Nil),
422415
KeySend -> (VariableLengthOnion :: Nil),
423416
SimpleClose -> (ShutdownAnySegwit :: Nil),
424417
SimpleTaprootChannelsPhoenix -> (ChannelType :: SimpleClose :: Nil),
425-
AsyncPaymentPrototype -> (TrampolinePaymentPrototype :: Nil),
418+
AsyncPaymentPrototype -> (TrampolinePayment :: Nil),
426419
OnTheFlyFunding -> (SplicePrototype :: Nil),
427420
FundingFeeCredit -> (OnTheFlyFunding :: Nil)
428421
)

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

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,6 @@ case class NodeParams(nodeKeyManager: NodeKeyManager,
8484
socksProxy_opt: Option[Socks5ProxyParams],
8585
maxPaymentAttempts: Int,
8686
paymentFinalExpiry: PaymentFinalExpiryConf,
87-
enableTrampolinePayment: Boolean,
8887
balanceCheckInterval: FiniteDuration,
8988
blockchainWatchdogThreshold: Int,
9089
blockchainWatchdogSources: Seq[String],
@@ -683,7 +682,6 @@ object NodeParams extends Logging {
683682
socksProxy_opt = socksProxy_opt,
684683
maxPaymentAttempts = config.getInt("max-payment-attempts"),
685684
paymentFinalExpiry = PaymentFinalExpiryConf(CltvExpiryDelta(config.getInt("send.recipient-final-expiry.min-delta")), CltvExpiryDelta(config.getInt("send.recipient-final-expiry.max-delta"))),
686-
enableTrampolinePayment = config.getBoolean("trampoline-payments-enable"),
687685
balanceCheckInterval = FiniteDuration(config.getDuration("balance-check-interval").getSeconds, TimeUnit.SECONDS),
688686
blockchainWatchdogThreshold = config.getInt("blockchain-watchdog.missing-blocks-threshold"),
689687
blockchainWatchdogSources = config.getStringList("blockchain-watchdog.sources").asScala.toSeq,

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

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -162,8 +162,9 @@ object IncomingPaymentPacket {
162162
case None if add.pathKey_opt.isDefined => Left(InvalidOnionBlinding(Sphinx.hash(add.onionRoutingPacket)))
163163
case None =>
164164
// We check if the payment is using trampoline: if it is, we may not be the final recipient.
165-
payload.get[OnionPaymentPayloadTlv.TrampolineOnion] match {
166-
case Some(OnionPaymentPayloadTlv.TrampolineOnion(trampolinePacket)) =>
165+
val trampolinePacket_opt = payload.get[OnionPaymentPayloadTlv.TrampolineOnion].map(_.packet).orElse(payload.get[OnionPaymentPayloadTlv.LegacyTrampolineOnion].map(_.packet))
166+
trampolinePacket_opt match {
167+
case Some(trampolinePacket) =>
167168
val outerPayload = payload.get[OnionPaymentPayloadTlv.PaymentData] match {
168169
case Some(_) => payload
169170
// The spec allows omitting the payment_secret field when not using MPP to reach the trampoline node.
@@ -252,7 +253,7 @@ object IncomingPaymentPacket {
252253
case innerPayload =>
253254
// We merge contents from the outer and inner payloads.
254255
// We must use the inner payload's total amount and payment secret because the payment may be split between multiple trampoline payments (#reckless).
255-
val trampolinePacket = outerPayload.records.get[OnionPaymentPayloadTlv.TrampolineOnion].map(_.packet)
256+
val trampolinePacket = outerPayload.records.get[OnionPaymentPayloadTlv.TrampolineOnion].map(_.packet).orElse(outerPayload.records.get[OnionPaymentPayloadTlv.LegacyTrampolineOnion].map(_.packet))
256257
Right(FinalPacket(add, FinalPayload.Standard.createPayload(outerPayload.amount, innerPayload.totalAmount, innerPayload.expiry, innerPayload.paymentSecret, innerPayload.paymentMetadata, trampolinePacket), TimestampMilli.now()))
257258
}
258259
}
@@ -323,7 +324,10 @@ object OutgoingPaymentPacket {
323324
* In that case, packetPayloadLength_opt must be greater than the actual onion's content.
324325
*/
325326
def buildOnion(payloads: Seq[NodePayload], associatedData: ByteVector32, packetPayloadLength_opt: Option[Int]): Either[OutgoingPaymentError, Sphinx.PacketAndSecrets] = {
326-
val sessionKey = randomKey()
327+
buildOnion(randomKey(), payloads, associatedData, packetPayloadLength_opt)
328+
}
329+
330+
def buildOnion(sessionKey: PrivateKey, payloads: Seq[NodePayload], associatedData: ByteVector32, packetPayloadLength_opt: Option[Int]): Either[OutgoingPaymentError, Sphinx.PacketAndSecrets] = {
327331
val nodeIds = payloads.map(_.nodeId)
328332
val payloadsBin = payloads
329333
.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
@@ -322,11 +322,6 @@ object MultiPartHandler {
322322
val paymentHash = Crypto.sha256(paymentPreimage)
323323
val expirySeconds = r.expirySeconds_opt.getOrElse(nodeParams.invoiceExpiry.toSeconds)
324324
val paymentMetadata = hex"2a"
325-
val featuresTrampolineOpt = if (nodeParams.enableTrampolinePayment) {
326-
nodeParams.features.bolt11Features().add(Features.TrampolinePaymentPrototype, FeatureSupport.Optional)
327-
} else {
328-
nodeParams.features.bolt11Features()
329-
}
330325
val invoice = Bolt11Invoice(
331326
nodeParams.chainHash,
332327
r.amount_opt,
@@ -338,7 +333,7 @@ object MultiPartHandler {
338333
expirySeconds = Some(expirySeconds),
339334
extraHops = r.extraHops,
340335
paymentMetadata = Some(paymentMetadata),
341-
features = featuresTrampolineOpt
336+
features = nodeParams.features.bolt11Features()
342337
)
343338
context.log.debug("generated invoice={} from amount={}", invoice.toString, r.amount_opt)
344339
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
@@ -43,7 +43,7 @@ import fr.acinq.eclair.router.Router.{ChannelHop, HopRelayParams, Route, RoutePa
4343
import fr.acinq.eclair.router.{BalanceTooLow, RouteNotFound}
4444
import fr.acinq.eclair.wire.protocol.PaymentOnion.IntermediatePayload
4545
import fr.acinq.eclair.wire.protocol._
46-
import fr.acinq.eclair.{Alias, CltvExpiry, CltvExpiryDelta, EncodedNodeId, Features, InitFeature, Logs, MilliSatoshi, MilliSatoshiLong, NodeParams, TimestampMilli, UInt64, nodeFee, randomBytes32}
46+
import fr.acinq.eclair.{Alias, CltvExpiry, CltvExpiryDelta, EncodedNodeId, FeatureSupport, Features, InitFeature, InvoiceFeature, Logs, MilliSatoshi, MilliSatoshiLong, NodeParams, TimestampMilli, UInt64, UnknownFeature, nodeFee, randomBytes32}
4747
import scodec.bits.ByteVector
4848

4949
import java.util.UUID
@@ -251,7 +251,9 @@ class NodeRelay private(nodeParams: NodeParams,
251251
nextPayload match {
252252
case payloadOut: IntermediatePayload.NodeRelay.Standard =>
253253
val paymentSecret = randomBytes32() // we generate a new secret to protect against probing attacks
254-
val recipient = ClearRecipient(payloadOut.outgoingNodeId, Features.empty, payloadOut.amountToForward, payloadOut.outgoingCltv, paymentSecret, nextTrampolineOnion_opt = nextPacket_opt)
254+
// If the recipient is using the legacy trampoline feature, we will use the legacy onion format.
255+
val features = if (payloadOut.isLegacy) Features(Map.empty[InvoiceFeature, FeatureSupport], Set(UnknownFeature(149))) else Features.empty[InvoiceFeature]
256+
val recipient = ClearRecipient(payloadOut.outgoingNodeId, features, payloadOut.amountToForward, payloadOut.outgoingCltv, paymentSecret, nextTrampolineOnion_opt = nextPacket_opt)
255257
context.log.debug("forwarding payment to the next trampoline node {}", recipient.nodeId)
256258
attemptWakeUpIfRecipientIsWallet(upstream, recipient, nextPayload, nextPacket_opt)
257259
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
@@ -30,7 +30,7 @@ import fr.acinq.eclair.db.PendingCommandsDb
3030
import fr.acinq.eclair.payment._
3131
import fr.acinq.eclair.reputation.{Reputation, ReputationRecorder}
3232
import fr.acinq.eclair.wire.protocol._
33-
import fr.acinq.eclair.{CltvExpiryDelta, Logs, MilliSatoshi, NodeParams, RealShortChannelId, TimestampMilli}
33+
import fr.acinq.eclair.{CltvExpiryDelta, Features, Logs, MilliSatoshi, NodeParams, RealShortChannelId, TimestampMilli}
3434
import grizzled.slf4j.Logging
3535

3636
import scala.concurrent.Promise
@@ -72,7 +72,7 @@ class Relayer(nodeParams: NodeParams, router: ActorRef, register: ActorRef, paym
7272
case Right(r: IncomingPaymentPacket.ChannelRelayPacket) =>
7373
channelRelayer ! ChannelRelayer.Relay(r, originNode, incomingChannelOccupancy)
7474
case Right(r: IncomingPaymentPacket.NodeRelayPacket) =>
75-
if (!nodeParams.enableTrampolinePayment) {
75+
if (!nodeParams.features.hasFeature(Features.TrampolinePayment)) {
7676
log.warning(s"rejecting htlc #${add.id} from channelId=${add.channelId} reason=trampoline disabled")
7777
val attribution = FailureAttributionData(htlcReceivedAt = r.receivedAt, trampolineReceivedAt_opt = None)
7878
PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, add.channelId, CMD_FAIL_HTLC(add.id, FailureReason.LocalFailure(RequiredNodeFeatureMissing()), Some(attribution), commit = true))

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, trampolineOnion_opt = None, customTlvs = 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
@@ -279,7 +279,7 @@ object TrampolinePayment {
279279
def buildOutgoingPayment(trampolineNodeId: PublicKey, invoice: Invoice, amount: MilliSatoshi, expiry: CltvExpiry, trampolinePaymentSecret_opt: Option[ByteVector32], attemptNumber: Int): OutgoingPayment = {
280280
val totalAmount = invoice.amount_opt.get
281281
val trampolineOnion = invoice match {
282-
case invoice: Bolt11Invoice if invoice.features.hasFeature(Features.TrampolinePaymentPrototype) =>
282+
case invoice: Bolt11Invoice if invoice.features.hasFeature(Features.TrampolinePayment) =>
283283
val finalPayload = PaymentOnion.FinalPayload.Standard.createPayload(amount, totalAmount, expiry, invoice.paymentSecret, invoice.paymentMetadata)
284284
val trampolinePayload = PaymentOnion.IntermediatePayload.NodeRelay.Standard(totalAmount, expiry, invoice.nodeId)
285285
buildOnion(NodePayload(trampolineNodeId, trampolinePayload) :: NodePayload(invoice.nodeId, finalPayload) :: Nil, invoice.paymentHash, None).toOption.get

0 commit comments

Comments
 (0)