Skip to content
Closed
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
```

### Package relay

Expand Down
3 changes: 1 addition & 2 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 @@ -88,7 +87,7 @@ eclair {
option_zeroconf = disabled
keysend = disabled
option_simple_close=optional
trampoline_payment_prototype = disabled
trampoline_routing = optional
async_payment_prototype = disabled
on_the_fly_funding = disabled
}
Expand Down
21 changes: 7 additions & 14 deletions eclair-core/src/main/scala/fr/acinq/eclair/Features.scala
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,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 @@ -320,17 +325,6 @@ 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.
case object TrampolinePaymentPrototype extends Feature with InitFeature with NodeFeature with Bolt11Feature {
val rfcName = "trampoline_payment_prototype"
val mandatory = 148
}

// TODO: @remyers update feature bits once spec-ed (currently reserved here: https://github.com/lightning/bolts/pull/989)
case object AsyncPaymentPrototype extends Feature with InitFeature with Bolt11Feature {
val rfcName = "async_payment_prototype"
Expand Down Expand Up @@ -387,7 +381,7 @@ object Features {
KeySend,
SimpleClose,
WakeUpNotificationClient,
TrampolinePaymentPrototype,
TrampolinePayment,
AsyncPaymentPrototype,
SplicePrototype,
OnTheFlyFunding,
Expand All @@ -402,10 +396,9 @@ object Features {
AnchorOutputs -> (StaticRemoteKey :: Nil),
AnchorOutputsZeroFeeHtlcTx -> (StaticRemoteKey :: Nil),
RouteBlinding -> (VariableLengthOnion :: Nil),
TrampolinePaymentPrototype -> (PaymentSecret :: Nil),
KeySend -> (VariableLengthOnion :: Nil),
SimpleClose -> (ShutdownAnySegwit :: Nil),
AsyncPaymentPrototype -> (TrampolinePaymentPrototype :: Nil),
AsyncPaymentPrototype -> (TrampolinePayment :: Nil),
OnTheFlyFunding -> (SplicePrototype :: 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 @@ -83,7 +83,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 @@ -682,7 +681,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 @@ -214,8 +214,8 @@ final case class CMD_ADD_HTLC(replyTo: ActorRef,
commit: Boolean = false) extends HasReplyToCommand with ForbiddenCommandDuringQuiescenceNegotiation with ForbiddenCommandWhenQuiescent

sealed trait HtlcSettlementCommand extends HasOptionalReplyToCommand with ForbiddenCommandDuringQuiescenceNegotiation with ForbiddenCommandWhenQuiescent { def id: Long }
final case class CMD_FULFILL_HTLC(id: Long, r: ByteVector32, downstreamAttribution_opt: Option[ByteVector], htlcReceivedAt_opt: Option[TimestampMilli], commit: Boolean = false, replyTo_opt: Option[ActorRef] = None) extends HtlcSettlementCommand
final case class CMD_FAIL_HTLC(id: Long, reason: FailureReason, htlcReceivedAt_opt: Option[TimestampMilli], delay_opt: Option[FiniteDuration] = None, commit: Boolean = false, replyTo_opt: Option[ActorRef] = None) extends HtlcSettlementCommand
final case class CMD_FULFILL_HTLC(id: Long, r: ByteVector32, downstreamAttribution_opt: Option[ByteVector], htlcReceivedAt_opt: Option[TimestampMilli], trampolinePaymentReceivedAt_opt: Option[TimestampMilli] = None, commit: Boolean = false, replyTo_opt: Option[ActorRef] = None) extends HtlcSettlementCommand
final case class CMD_FAIL_HTLC(id: Long, reason: FailureReason, htlcReceivedAt_opt: Option[TimestampMilli], trampolinePaymentReceivedAt_opt: Option[TimestampMilli] = None, 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
final case class CMD_SIGN(replyTo_opt: Option[ActorRef] = None) extends HasOptionalReplyToCommand with ForbiddenCommandWhenQuiescent
Expand Down
24 changes: 12 additions & 12 deletions eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala
Original file line number Diff line number Diff line change
Expand Up @@ -335,16 +335,16 @@ object Sphinx extends Logging {
* @return failure message if the origin of the packet could be identified and the packet decrypted, the unwrapped
* failure packet otherwise.
*/
def decrypt(packet: ByteVector, attribution_opt: Option[ByteVector], sharedSecrets: Seq[SharedSecret], hopIndex: Int = 0): HtlcFailure = {
def decrypt(packet: ByteVector, attribution_opt: Option[ByteVector], sharedSecrets: Seq[SharedSecret]): HtlcFailure = {
sharedSecrets match {
case Nil => HtlcFailure(Nil, Left(CannotDecryptFailurePacket(packet, attribution_opt)))
case ss :: tail =>
val packet1 = wrap(packet, ss.secret)
val attribution1_opt = attribution_opt.flatMap(Attribution.unwrap(_, packet1, ss.secret, hopIndex))
val attribution1_opt = attribution_opt.flatMap(Attribution.unwrap(_, packet1, ss.secret, sharedSecrets.length))
val um = generateKey("um", ss.secret)
val HtlcFailure(downstreamHoldTimes, failure) = FailureMessageCodecs.failureOnionCodec(Hmac256(um)).decode(packet1.toBitVector) match {
case Attempt.Successful(value) => HtlcFailure(Nil, Right(DecryptedFailurePacket(ss.remoteNodeId, value.value)))
case _ => decrypt(packet1, attribution1_opt.map(_._2), tail, hopIndex + 1)
case _ => decrypt(packet1, attribution1_opt.map(_._2), tail)
}
HtlcFailure(attribution1_opt.map(n => HoldTime(n._1, ss.remoteNodeId) +: downstreamHoldTimes).getOrElse(Nil), failure)
}
Expand Down Expand Up @@ -390,11 +390,11 @@ object Sphinx extends Logging {
}))

/**
* Computes the HMACs for the node that is `minNumHop` hops away from us. Hence we only compute `maxNumHops - minNumHop` HMACs.
* Computes the HMACs for the node that is `maxNumHops - remainingHops` hops away from us. Hence we only compute `remainingHops` HMACs.
* HMACs are truncated to 4 bytes to save space. An attacker has only one try to guess the HMAC so 4 bytes should be enough.
*/
private def computeHmacs(mac: Mac32, failurePacket: ByteVector, holdTimes: ByteVector, hmacs: Seq[Seq[ByteVector]], minNumHop: Int): Seq[ByteVector] = {
(minNumHop until maxNumHops).map(i => {
private def computeHmacs(mac: Mac32, failurePacket: ByteVector, holdTimes: ByteVector, hmacs: Seq[Seq[ByteVector]], remainingHops: Int): Seq[ByteVector] = {
((maxNumHops - remainingHops) until maxNumHops).map(i => {
val y = maxNumHops - i
mac.mac(failurePacket ++
holdTimes.take(y * holdTimeLength) ++
Expand All @@ -412,20 +412,20 @@ object Sphinx extends Logging {
val previousHmacs = getHmacs(previousAttribution).dropRight(1).map(_.drop(1))
val mac = Hmac256(generateKey("um", sharedSecret))
val holdTimes = uint32.encode(holdTime.toMillis / 100).require.bytes ++ previousAttribution.take((maxNumHops - 1) * holdTimeLength)
val hmacs = computeHmacs(mac, failurePacket_opt.getOrElse(ByteVector.empty), holdTimes, previousHmacs, 0) +: previousHmacs
val hmacs = computeHmacs(mac, failurePacket_opt.getOrElse(ByteVector.empty), holdTimes, previousHmacs, maxNumHops) +: previousHmacs
cipher(holdTimes ++ ByteVector.concat(hmacs.map(ByteVector.concat(_))), sharedSecret)
}

/**
* Unwrap one hop of attribution data
* @return a pair with the hold time for this hop and the attribution data for the next hop, or None if the attribution data was invalid
*/
def unwrap(encrypted: ByteVector, failurePacket: ByteVector, sharedSecret: ByteVector32, minNumHop: Int): Option[(FiniteDuration, ByteVector)] = {
def unwrap(encrypted: ByteVector, failurePacket: ByteVector, sharedSecret: ByteVector32, remainingHops: Int): Option[(FiniteDuration, ByteVector)] = {
val bytes = cipher(encrypted, sharedSecret)
val holdTime = (uint32.decode(bytes.take(holdTimeLength).bits).require.value * 100).milliseconds
val hmacs = getHmacs(bytes)
val mac = Hmac256(generateKey("um", sharedSecret))
if (computeHmacs(mac, failurePacket, bytes.take(maxNumHops * holdTimeLength), hmacs.drop(1), minNumHop) == hmacs.head.drop(minNumHop)) {
if (computeHmacs(mac, failurePacket, bytes.take(maxNumHops * holdTimeLength), hmacs.drop(1), remainingHops) == hmacs.head.drop(maxNumHops - remainingHops)) {
val unwrapped = bytes.slice(holdTimeLength, maxNumHops * holdTimeLength) ++ ByteVector.low(holdTimeLength) ++ ByteVector.concat((hmacs.drop(1) :+ Seq()).map(s => ByteVector.low(hmacLength) ++ ByteVector.concat(s)))
Some(holdTime, unwrapped)
} else {
Expand All @@ -438,13 +438,13 @@ object Sphinx extends Logging {
/**
* Decrypt the hold times from the attribution data of a fulfilled HTLC
*/
def fulfillHoldTimes(attribution: ByteVector, sharedSecrets: Seq[SharedSecret], hopIndex: Int = 0): UnwrappedAttribution = {
def fulfillHoldTimes(attribution: ByteVector, sharedSecrets: Seq[SharedSecret]): UnwrappedAttribution = {
sharedSecrets match {
case Nil => UnwrappedAttribution(Nil, Some(attribution))
case ss :: tail =>
unwrap(attribution, ByteVector.empty, ss.secret, hopIndex) match {
unwrap(attribution, ByteVector.empty, ss.secret, sharedSecrets.length) match {
case Some((holdTime, nextAttribution)) =>
val UnwrappedAttribution(holdTimes, remaining_opt) = fulfillHoldTimes(nextAttribution, tail, hopIndex + 1)
val UnwrappedAttribution(holdTimes, remaining_opt) = fulfillHoldTimes(nextAttribution, tail)
UnwrappedAttribution(HoldTime(holdTime, ss.remoteNodeId) :: holdTimes, remaining_opt)
case None => UnwrappedAttribution(Nil, None)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ object Monitoring {
def apply(cmdFail: CMD_FAIL_HTLC): String = cmdFail.reason match {
case _: FailureReason.EncryptedDownstreamFailure => Remote
case FailureReason.LocalFailure(f) => f.getClass.getSimpleName
case FailureReason.LocalTrampolineFailure(f) => f.getClass.getSimpleName
}

def apply(pf: PaymentFailure): String = pf match {
Expand Down
Loading