Skip to content

Commit a47a435

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 2e6c6fe commit a47a435

20 files changed

+332
-144
lines changed

docs/release-notes/eclair-vnext.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,19 @@
44

55
## Major changes
66

7-
<insert changes>
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+
```
820

921
### Package relay
1022

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
@@ -309,6 +309,11 @@ object Features {
309309
val mandatory = 54
310310
}
311311

312+
case object TrampolinePayment extends Feature with InitFeature with NodeFeature with Bolt11Feature with Bolt12Feature {
313+
val rfcName = "trampoline_routing"
314+
val mandatory = 56
315+
}
316+
312317
case object SimpleClose extends Feature with InitFeature with NodeFeature {
313318
val rfcName = "option_simple_close"
314319
val mandatory = 60
@@ -320,17 +325,6 @@ object Features {
320325
val mandatory = 132
321326
}
322327

323-
// TODO: @t-bast: update feature bits once spec-ed (currently reserved here: https://github.com/lightningnetwork/lightning-rfc/issues/605)
324-
// We're not advertising these bits yet in our announcements, clients have to assume support.
325-
// This is why we haven't added them yet to `areSupported`.
326-
// The version of trampoline enabled by this feature bit does not match the latest spec PR: once the spec is accepted,
327-
// we will introduce a new version of trampoline that will work in parallel to this legacy one, until we can safely
328-
// deprecate it.
329-
case object TrampolinePaymentPrototype extends Feature with InitFeature with NodeFeature with Bolt11Feature {
330-
val rfcName = "trampoline_payment_prototype"
331-
val mandatory = 148
332-
}
333-
334328
// TODO: @remyers update feature bits once spec-ed (currently reserved here: https://github.com/lightning/bolts/pull/989)
335329
case object AsyncPaymentPrototype extends Feature with InitFeature with Bolt11Feature {
336330
val rfcName = "async_payment_prototype"
@@ -387,7 +381,7 @@ object Features {
387381
KeySend,
388382
SimpleClose,
389383
WakeUpNotificationClient,
390-
TrampolinePaymentPrototype,
384+
TrampolinePayment,
391385
AsyncPaymentPrototype,
392386
SplicePrototype,
393387
OnTheFlyFunding,
@@ -402,10 +396,9 @@ object Features {
402396
AnchorOutputs -> (StaticRemoteKey :: Nil),
403397
AnchorOutputsZeroFeeHtlcTx -> (StaticRemoteKey :: Nil),
404398
RouteBlinding -> (VariableLengthOnion :: Nil),
405-
TrampolinePaymentPrototype -> (PaymentSecret :: Nil),
406399
KeySend -> (VariableLengthOnion :: Nil),
407400
SimpleClose -> (ShutdownAnySegwit :: Nil),
408-
AsyncPaymentPrototype -> (TrampolinePaymentPrototype :: Nil),
401+
AsyncPaymentPrototype -> (TrampolinePayment :: Nil),
409402
OnTheFlyFunding -> (SplicePrototype :: Nil),
410403
FundingFeeCredit -> (OnTheFlyFunding :: Nil)
411404
)

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],
@@ -682,7 +681,6 @@ object NodeParams extends Logging {
682681
socksProxy_opt = socksProxy_opt,
683682
maxPaymentAttempts = config.getInt("max-payment-attempts"),
684683
paymentFinalExpiry = PaymentFinalExpiryConf(CltvExpiryDelta(config.getInt("send.recipient-final-expiry.min-delta")), CltvExpiryDelta(config.getInt("send.recipient-final-expiry.max-delta"))),
685-
enableTrampolinePayment = config.getBoolean("trampoline-payments-enable"),
686684
balanceCheckInterval = FiniteDuration(config.getDuration("balance-check-interval").getSeconds, TimeUnit.SECONDS),
687685
blockchainWatchdogThreshold = config.getInt("blockchain-watchdog.missing-blocks-threshold"),
688686
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
@@ -161,8 +161,9 @@ object IncomingPaymentPacket {
161161
case None if add.pathKey_opt.isDefined => Left(InvalidOnionBlinding(Sphinx.hash(add.onionRoutingPacket)))
162162
case None =>
163163
// We check if the payment is using trampoline: if it is, we may not be the final recipient.
164-
payload.get[OnionPaymentPayloadTlv.TrampolineOnion] match {
165-
case Some(OnionPaymentPayloadTlv.TrampolineOnion(trampolinePacket)) =>
164+
val trampolinePacket_opt = payload.get[OnionPaymentPayloadTlv.TrampolineOnion].map(_.packet).orElse(payload.get[OnionPaymentPayloadTlv.LegacyTrampolineOnion].map(_.packet))
165+
trampolinePacket_opt match {
166+
case Some(trampolinePacket) =>
166167
val outerPayload = payload.get[OnionPaymentPayloadTlv.PaymentData] match {
167168
case Some(_) => payload
168169
// The spec allows omitting the payment_secret field when not using MPP to reach the trampoline node.
@@ -251,7 +252,7 @@ object IncomingPaymentPacket {
251252
case innerPayload =>
252253
// We merge contents from the outer and inner payloads.
253254
// We must use the inner payload's total amount and payment secret because the payment may be split between multiple trampoline payments (#reckless).
254-
val trampolinePacket = outerPayload.records.get[OnionPaymentPayloadTlv.TrampolineOnion].map(_.packet)
255+
val trampolinePacket = outerPayload.records.get[OnionPaymentPayloadTlv.TrampolineOnion].map(_.packet).orElse(outerPayload.records.get[OnionPaymentPayloadTlv.LegacyTrampolineOnion].map(_.packet))
255256
Right(FinalPacket(add, FinalPayload.Standard.createPayload(outerPayload.amount, innerPayload.totalAmount, innerPayload.expiry, innerPayload.paymentSecret, innerPayload.paymentMetadata, trampolinePacket), TimestampMilli.now()))
256257
}
257258
}
@@ -322,7 +323,10 @@ object OutgoingPaymentPacket {
322323
* In that case, packetPayloadLength_opt must be greater than the actual onion's content.
323324
*/
324325
def buildOnion(payloads: Seq[NodePayload], associatedData: ByteVector32, packetPayloadLength_opt: Option[Int]): Either[OutgoingPaymentError, Sphinx.PacketAndSecrets] = {
325-
val sessionKey = randomKey()
326+
buildOnion(randomKey(), payloads, associatedData, packetPayloadLength_opt)
327+
}
328+
329+
def buildOnion(sessionKey: PrivateKey, payloads: Seq[NodePayload], associatedData: ByteVector32, packetPayloadLength_opt: Option[Int]): Either[OutgoingPaymentError, Sphinx.PacketAndSecrets] = {
326330
val nodeIds = payloads.map(_.nodeId)
327331
val payloadsBin = payloads
328332
.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
@@ -311,11 +311,6 @@ object MultiPartHandler {
311311
val paymentHash = Crypto.sha256(paymentPreimage)
312312
val expirySeconds = r.expirySeconds_opt.getOrElse(nodeParams.invoiceExpiry.toSeconds)
313313
val paymentMetadata = hex"2a"
314-
val featuresTrampolineOpt = if (nodeParams.enableTrampolinePayment) {
315-
nodeParams.features.bolt11Features().add(Features.TrampolinePaymentPrototype, FeatureSupport.Optional)
316-
} else {
317-
nodeParams.features.bolt11Features()
318-
}
319314
val invoice = Bolt11Invoice(
320315
nodeParams.chainHash,
321316
r.amount_opt,
@@ -327,7 +322,7 @@ object MultiPartHandler {
327322
expirySeconds = Some(expirySeconds),
328323
extraHops = r.extraHops,
329324
paymentMetadata = Some(paymentMetadata),
330-
features = featuresTrampolineOpt
325+
features = nodeParams.features.bolt11Features()
331326
)
332327
context.log.debug("generated invoice={} from amount={}", invoice.toString, r.amount_opt)
333328
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
@@ -41,7 +41,7 @@ import fr.acinq.eclair.router.Router.{ChannelHop, HopRelayParams, Route, RoutePa
4141
import fr.acinq.eclair.router.{BalanceTooLow, RouteNotFound}
4242
import fr.acinq.eclair.wire.protocol.PaymentOnion.IntermediatePayload
4343
import fr.acinq.eclair.wire.protocol._
44-
import fr.acinq.eclair.{Alias, CltvExpiry, CltvExpiryDelta, EncodedNodeId, Features, InitFeature, Logs, MilliSatoshi, MilliSatoshiLong, NodeParams, TimestampMilli, UInt64, nodeFee, randomBytes32}
44+
import fr.acinq.eclair.{Alias, CltvExpiry, CltvExpiryDelta, EncodedNodeId, FeatureSupport, Features, InitFeature, InvoiceFeature, Logs, MilliSatoshi, MilliSatoshiLong, NodeParams, TimestampMilli, UInt64, UnknownFeature, nodeFee, randomBytes32}
4545
import scodec.bits.ByteVector
4646

4747
import java.util.UUID
@@ -249,7 +249,9 @@ class NodeRelay private(nodeParams: NodeParams,
249249
nextPayload match {
250250
case payloadOut: IntermediatePayload.NodeRelay.Standard =>
251251
val paymentSecret = randomBytes32() // we generate a new secret to protect against probing attacks
252-
val recipient = ClearRecipient(payloadOut.outgoingNodeId, Features.empty, payloadOut.amountToForward, payloadOut.outgoingCltv, paymentSecret, nextTrampolineOnion_opt = nextPacket_opt)
252+
// If the recipient is using the legacy trampoline feature, we will use the legacy onion format.
253+
val features = if (payloadOut.isLegacy) Features(Map.empty[InvoiceFeature, FeatureSupport], Set(UnknownFeature(149))) else Features.empty[InvoiceFeature]
254+
val recipient = ClearRecipient(payloadOut.outgoingNodeId, features, payloadOut.amountToForward, payloadOut.outgoingCltv, paymentSecret, nextTrampolineOnion_opt = nextPacket_opt)
253255
context.log.debug("forwarding payment to the next trampoline node {}", recipient.nodeId)
254256
attemptWakeUpIfRecipientIsWallet(upstream, recipient, nextPayload, nextPacket_opt)
255257
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, RealShortChannelId, TimestampMilli}
32+
import fr.acinq.eclair.{CltvExpiryDelta, Features, Logs, MilliSatoshi, NodeParams, RealShortChannelId, TimestampMilli}
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, FailureReason.LocalFailure(RequiredNodeFeatureMissing()), Some(r.receivedAt), 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, 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
@@ -211,7 +211,7 @@ object TrampolinePayment {
211211
def buildOutgoingPayment(trampolineNodeId: PublicKey, invoice: Invoice, amount: MilliSatoshi, expiry: CltvExpiry, trampolinePaymentSecret_opt: Option[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)