Skip to content
Draft
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
14 changes: 13 additions & 1 deletion docs/release-notes/eclair-vnext.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,19 @@

## Major changes

<insert changes>
### Trampoline payments

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.

Eclair started supporting [trampoline payments](https://github.com/lightning/bolts/pull/829) in v0.3.3.
The specification has evolved since then and has recently been added to the [BOLTs](https://github.com/lightning/bolts/pull/836).

With this release, eclair nodes are able to relay and receive trampoline payments (activated by default).
This feature can be disabled if you don't want to relay or receive trampoline payments:

```conf
eclair.features.trampoline_routing = disabled
```

### Configuration changes

Expand Down
8 changes: 4 additions & 4 deletions eclair-core/src/main/resources/reference.conf
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ eclair {
node-alias = "eclair"
node-color = "49daaa"

trampoline-payments-enable = false // TODO: @t-bast: once spec-ed this should use a global feature flag
// see https://github.com/lightningnetwork/lightning-rfc/blob/master/09-features.md
features {
// option_upfront_shutdown_script is not activated by default.
Expand Down Expand Up @@ -91,6 +90,7 @@ eclair {
option_simple_close = optional
option_splice = optional
option_simple_taproot = optional
trampoline_routing = optional
trampoline_payment_prototype = disabled
async_payment_prototype = disabled
on_the_fly_funding = disabled
Expand Down Expand Up @@ -733,11 +733,11 @@ eclair {

offers {
// Minimum length of an offer blinded path when hiding our real node id
message-path-min-length = 2
message-path-min-length = 1
// Number of payment paths to put in Bolt12 invoices when hiding our real node id
payment-path-count = 2
payment-path-count = 1
// Length of payment paths to put in Bolt12 invoices when hiding our real node id
payment-path-length = 2
payment-path-length = 1
// Expiry delta of payment paths to put in Bolt12 invoices when hiding our real node id
payment-path-expiry-delta = 500
}
Expand Down
7 changes: 7 additions & 0 deletions eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,8 @@ trait Eclair {

def sendBlocking(externalId_opt: Option[String], amount: MilliSatoshi, invoice: Bolt11Invoice, maxAttempts_opt: Option[Int] = None, maxFeeFlat_opt: Option[Satoshi] = None, maxFeePct_opt: Option[Double] = None, pathFindingExperimentName_opt: Option[String] = None)(implicit timeout: Timeout): Future[PaymentEvent]

def sendTrampoline(trampolineNodeId: PublicKey, amount: MilliSatoshi, invoice: Bolt11Invoice)(implicit timeout: Timeout): Future[PaymentEvent]

def sendWithPreimage(externalId_opt: Option[String], recipientNodeId: PublicKey, amount: MilliSatoshi, paymentPreimage: ByteVector32 = randomBytes32(), maxAttempts_opt: Option[Int] = None, maxFeeFlat_opt: Option[Satoshi] = None, maxFeePct_opt: Option[Double] = None, pathFindingExperimentName_opt: Option[String] = None)(implicit timeout: Timeout): Future[UUID]

def sentInfo(id: PaymentIdentifier)(implicit timeout: Timeout): Future[Seq[OutgoingPayment]]
Expand Down Expand Up @@ -544,6 +546,11 @@ class EclairImpl(val appKit: Kit) extends Eclair with Logging with SpendFromChan
}
}

override def sendTrampoline(trampolineNodeId: PublicKey, amount: MilliSatoshi, invoice: Bolt11Invoice)(implicit timeout: Timeout): Future[PaymentEvent] = {
val routeParams = appKit.nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams
appKit.paymentInitiator.toTyped.ask[PaymentEvent](ref => SendTrampolinePayment(ref.toClassic, invoice, trampolineNodeId, routeParams, blockUntilComplete = true))
}

override def sendWithPreimage(externalId_opt: Option[String], recipientNodeId: PublicKey, amount: MilliSatoshi, paymentPreimage: ByteVector32, maxAttempts_opt: Option[Int], maxFeeFlat_opt: Option[Satoshi], maxFeePct_opt: Option[Double], pathFindingExperimentName_opt: Option[String])(implicit timeout: Timeout): Future[UUID] = {
val maxAttempts = maxAttempts_opt.getOrElse(appKit.nodeParams.maxPaymentAttempts)
getRouteParams(pathFindingExperimentName_opt) match {
Expand Down
16 changes: 8 additions & 8 deletions eclair-core/src/main/scala/fr/acinq/eclair/Features.scala
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,11 @@ object Features {
val mandatory = 54
}

case object TrampolinePayment extends Feature with InitFeature with NodeFeature with Bolt11Feature with Bolt12Feature {
val rfcName = "trampoline_routing"
val mandatory = 56
}

case object SimpleClose extends Feature with InitFeature with NodeFeature {
val rfcName = "option_simple_close"
val mandatory = 60
Expand All @@ -420,12 +425,7 @@ object Features {
val mandatory = 132
}

// TODO: @t-bast: update feature bits once spec-ed (currently reserved here: https://github.com/lightningnetwork/lightning-rfc/issues/605)
// We're not advertising these bits yet in our announcements, clients have to assume support.
// This is why we haven't added them yet to `areSupported`.
// The version of trampoline enabled by this feature bit does not match the latest spec PR: once the spec is accepted,
// we will introduce a new version of trampoline that will work in parallel to this legacy one, until we can safely
// deprecate it.
// TODO: we can remove this feature once we've migrated existing Phoenix users
case object TrampolinePaymentPrototype extends Feature with InitFeature with NodeFeature with Bolt11Feature {
val rfcName = "trampoline_payment_prototype"
val mandatory = 148
Expand Down Expand Up @@ -496,6 +496,7 @@ object Features {
SimpleTaprootChannels,
SimpleTaprootChannelsPhoenix,
WakeUpNotificationClient,
TrampolinePayment,
TrampolinePaymentPrototype,
AsyncPaymentPrototype,
SplicePrototype,
Expand All @@ -512,12 +513,11 @@ object Features {
AnchorOutputs -> (StaticRemoteKey :: Nil),
AnchorOutputsZeroFeeHtlcTx -> (StaticRemoteKey :: Nil),
RouteBlinding -> (VariableLengthOnion :: Nil),
TrampolinePaymentPrototype -> (PaymentSecret :: Nil),
KeySend -> (VariableLengthOnion :: Nil),
SimpleClose -> (ShutdownAnySegwit :: Nil),
SimpleTaprootChannels -> (ChannelType :: SimpleClose :: Nil),
SimpleTaprootChannelsPhoenix -> (ChannelType :: SimpleClose :: Nil),
AsyncPaymentPrototype -> (TrampolinePaymentPrototype :: Nil),
AsyncPaymentPrototype -> (TrampolinePayment :: Nil),
FundingFeeCredit -> (OnTheFlyFunding :: Nil)
)

Expand Down
2 changes: 0 additions & 2 deletions eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,6 @@ case class NodeParams(nodeKeyManager: NodeKeyManager,
socksProxy_opt: Option[Socks5ProxyParams],
maxPaymentAttempts: Int,
paymentFinalExpiry: PaymentFinalExpiryConf,
enableTrampolinePayment: Boolean,
balanceCheckInterval: FiniteDuration,
blockchainWatchdogThreshold: Int,
blockchainWatchdogSources: Seq[String],
Expand Down Expand Up @@ -691,7 +690,6 @@ object NodeParams extends Logging {
socksProxy_opt = socksProxy_opt,
maxPaymentAttempts = config.getInt("max-payment-attempts"),
paymentFinalExpiry = PaymentFinalExpiryConf(CltvExpiryDelta(config.getInt("send.recipient-final-expiry.min-delta")), CltvExpiryDelta(config.getInt("send.recipient-final-expiry.max-delta"))),
enableTrampolinePayment = config.getBoolean("trampoline-payments-enable"),
balanceCheckInterval = FiniteDuration(config.getDuration("balance-check-interval").getSeconds, TimeUnit.SECONDS),
blockchainWatchdogThreshold = config.getInt("blockchain-watchdog.missing-blocks-threshold"),
blockchainWatchdogSources = config.getStringList("blockchain-watchdog.sources").asScala.toSeq,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ case class FailureAttributionData(htlcReceivedAt: TimestampMilli, trampolineRece
case class FulfillAttributionData(htlcReceivedAt: TimestampMilli, trampolineReceivedAt_opt: Option[TimestampMilli], downstreamAttribution_opt: Option[ByteVector])

sealed trait HtlcSettlementCommand extends HasOptionalReplyToCommand with ForbiddenCommandDuringQuiescenceNegotiation with ForbiddenCommandWhenQuiescent { def id: Long }
final case class CMD_FULFILL_HTLC(id: Long, r: ByteVector32, attribution_opt: Option[FulfillAttributionData], commit: Boolean = false, replyTo_opt: Option[ActorRef] = None) extends HtlcSettlementCommand
final case class CMD_FULFILL_HTLC(id: Long, r: ByteVector32, fulfillmentPayload_opt: Option[ByteVector], attribution_opt: Option[FulfillAttributionData], commit: Boolean = false, replyTo_opt: Option[ActorRef] = None) extends HtlcSettlementCommand
final case class CMD_FAIL_HTLC(id: Long, reason: FailureReason, attribution_opt: Option[FailureAttributionData], delay_opt: Option[FiniteDuration] = None, commit: Boolean = false, replyTo_opt: Option[ActorRef] = None) extends HtlcSettlementCommand
final case class CMD_FAIL_MALFORMED_HTLC(id: Long, onionHash: ByteVector32, failureCode: Int, commit: Boolean = false, replyTo_opt: Option[ActorRef] = None) extends HtlcSettlementCommand
final case class CMD_UPDATE_FEE(feeratePerKw: FeeratePerKw, commit: Boolean = false, replyTo_opt: Option[ActorRef] = None) extends HasOptionalReplyToCommand with ForbiddenCommandDuringQuiescenceNegotiation with ForbiddenCommandWhenQuiescent
Expand Down Expand Up @@ -304,7 +304,13 @@ final case class RES_FAILURE[+C <: Command, +T <: Throwable](cmd: C, t: T) exten
final case class RES_ADD_FAILED[+T <: ChannelException](c: CMD_ADD_HTLC, t: T, channelUpdate: Option[ChannelUpdate]) extends CommandFailure[CMD_ADD_HTLC, T] { override def toString = s"cannot add htlc with origin=${c.origin} reason=${t.getMessage}" }
sealed trait HtlcResult
object HtlcResult {
sealed trait Fulfill extends HtlcResult { def paymentPreimage: ByteVector32 }
sealed trait Fulfill extends HtlcResult {
def paymentPreimage: ByteVector32
def fulfillmentPayload_opt: Option[ByteVector] = this match {
case RemoteFulfill(fulfill) => fulfill.fulfillmentPayload_opt
case _: OnChainFulfill => None
}
}
case class RemoteFulfill(fulfill: UpdateFulfillHtlc) extends Fulfill { override val paymentPreimage: ByteVector32 = fulfill.paymentPreimage }
case class OnChainFulfill(paymentPreimage: ByteVector32) extends Fulfill
sealed trait Fail extends HtlcResult
Expand Down
Loading
Loading