Skip to content

Commit afccec8

Browse files
committed
Comments
1 parent 962c63c commit afccec8

31 files changed

+230
-199
lines changed

eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ final case class CMD_ADD_HTLC(replyTo: ActorRef,
215215

216216
sealed trait HtlcSettlementCommand extends HasOptionalReplyToCommand with ForbiddenCommandDuringQuiescenceNegotiation with ForbiddenCommandWhenQuiescent { def id: Long }
217217
final case class CMD_FULFILL_HTLC(id: Long, r: ByteVector32, commit: Boolean = false, replyTo_opt: Option[ActorRef] = None) extends HtlcSettlementCommand
218-
final case class CMD_FAIL_HTLC(id: Long, reason: FailureReason, startHoldTime: TimestampMilli, delay_opt: Option[FiniteDuration] = None, commit: Boolean = false, replyTo_opt: Option[ActorRef] = None) extends HtlcSettlementCommand
218+
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
219219
final case class CMD_FAIL_MALFORMED_HTLC(id: Long, onionHash: ByteVector32, failureCode: Int, commit: Boolean = false, replyTo_opt: Option[ActorRef] = None) extends HtlcSettlementCommand
220220
final case class CMD_UPDATE_FEE(feeratePerKw: FeeratePerKw, commit: Boolean = false, replyTo_opt: Option[ActorRef] = None) extends HasOptionalReplyToCommand with ForbiddenCommandDuringQuiescenceNegotiation with ForbiddenCommandWhenQuiescent
221221
final case class CMD_SIGN(replyTo_opt: Option[ActorRef] = None) extends HasOptionalReplyToCommand with ForbiddenCommandWhenQuiescent

eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -668,7 +668,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
668668
case PostRevocationAction.RejectHtlc(add) =>
669669
log.debug("rejecting incoming htlc {}", add)
670670
// NB: we don't set commit = true, we will sign all updates at once afterwards.
671-
self ! CMD_FAIL_HTLC(add.id, FailureReason.LocalFailure(TemporaryChannelFailure(Some(d.channelUpdate))), TimestampMilli.now(), commit = true)
671+
self ! CMD_FAIL_HTLC(add.id, FailureReason.LocalFailure(TemporaryChannelFailure(Some(d.channelUpdate))), Some(TimestampMilli.now()), commit = true)
672672
case PostRevocationAction.RelayFailure(result) =>
673673
log.debug("forwarding {} to relayer", result)
674674
relayer ! result
@@ -1617,11 +1617,11 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
16171617
case PostRevocationAction.RelayHtlc(add) =>
16181618
// BOLT 2: A sending node SHOULD fail to route any HTLC added after it sent shutdown.
16191619
log.debug("closing in progress: failing {}", add)
1620-
self ! CMD_FAIL_HTLC(add.id, FailureReason.LocalFailure(PermanentChannelFailure()), TimestampMilli.now(), commit = true)
1620+
self ! CMD_FAIL_HTLC(add.id, FailureReason.LocalFailure(PermanentChannelFailure()), Some(TimestampMilli.now()), commit = true)
16211621
case PostRevocationAction.RejectHtlc(add) =>
16221622
// BOLT 2: A sending node SHOULD fail to route any HTLC added after it sent shutdown.
16231623
log.debug("closing in progress: rejecting {}", add)
1624-
self ! CMD_FAIL_HTLC(add.id, FailureReason.LocalFailure(PermanentChannelFailure()), TimestampMilli.now(), commit = true)
1624+
self ! CMD_FAIL_HTLC(add.id, FailureReason.LocalFailure(PermanentChannelFailure()), Some(TimestampMilli.now()), commit = true)
16251625
case PostRevocationAction.RelayFailure(result) =>
16261626
log.debug("forwarding {} to relayer", result)
16271627
relayer ! result

eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala

Lines changed: 48 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -291,21 +291,14 @@ object Sphinx extends Logging {
291291
object FailurePacket {
292292

293293
/**
294-
* Create a failure packet that will be returned to the sender.
294+
* Create a failure packet that needs to be wrapped before being returned to the sender.
295295
* Each intermediate hop will add a layer of encryption and forward to the previous hop.
296296
* Note that malicious intermediate hops may drop the packet or alter it (which breaks the mac).
297297
*
298298
* @param sharedSecret destination node's shared secret that was computed when the original onion for the HTLC
299299
* was created or forwarded: see OnionPacket.create() and OnionPacket.wrap().
300300
* @param failure failure message.
301-
* @return a failure packet that can be sent to the destination node.
302-
*/
303-
def createAndWrap(sharedSecret: ByteVector32, failure: FailureMessage): ByteVector = {
304-
wrap(create(sharedSecret, failure), sharedSecret)
305-
}
306-
307-
/**
308-
* Create a failure packet that needs to be wrapped before being returned to the sender.
301+
* @return a failure packet that still needs to be wrapped before being sent to the destination node.
309302
*/
310303
def create(sharedSecret: ByteVector32, failure: FailureMessage): ByteVector = {
311304
val um = generateKey("um", sharedSecret)
@@ -348,44 +341,74 @@ object Sphinx extends Logging {
348341
val packet1 = wrap(packet, ss.secret)
349342
val attribution1_opt = attribution_opt.flatMap(Attribution.unwrap(_, packet1, ss.secret, hopIndex))
350343
val um = generateKey("um", ss.secret)
351-
val HtlcFailure(holdTimes, failure) = FailureMessageCodecs.failureOnionCodec(Hmac256(um)).decode(packet1.toBitVector) match {
344+
val HtlcFailure(downstreamHoldTimes, failure) = FailureMessageCodecs.failureOnionCodec(Hmac256(um)).decode(packet1.toBitVector) match {
352345
case Attempt.Successful(value) => HtlcFailure(Nil, Right(DecryptedFailurePacket(ss.remoteNodeId, value.value)))
353346
case _ => decrypt(packet1, attribution1_opt.map(_._2), tail, hopIndex + 1)
354347
}
355-
HtlcFailure(attribution1_opt.map(n => HoldTime(n._1, ss.remoteNodeId) +: holdTimes).getOrElse(Nil), failure)
348+
HtlcFailure(attribution1_opt.map(n => HoldTime(n._1, ss.remoteNodeId) +: downstreamHoldTimes).getOrElse(Nil), failure)
356349
}
357350
}
358351

352+
/**
353+
* Attribution data is added to the failure packet and prevents a node from evading responsibility for its failures.
354+
* Nodes that relay attribution data can prove that they are not the erring node and in case the erring node tries
355+
* to hide, there will only be at most two nodes that can be the erring node (the last one to send attribution data
356+
* and the one after it).
357+
* It also adds timing data for each node on the path.
358+
* https://github.com/lightning/bolts/pull/1044
359+
*/
359360
object Attribution {
361+
val maxNumHops = 20
362+
val holdTimeLength = 4
363+
val hmacLength = 4 // HMACs are truncated to 4 bytes to save space
364+
val totalLength = maxNumHops * holdTimeLength + maxNumHops * (maxNumHops + 1) / 2 * hmacLength // = 920
365+
360366
private def cipher(bytes: ByteVector, sharedSecret: ByteVector32): ByteVector = {
361367
val key = generateKey("ammagext", sharedSecret)
362-
val stream = generateStream(key, 920)
368+
val stream = generateStream(key, totalLength)
363369
bytes xor stream
364370
}
365371

372+
/**
373+
* Get the HMACs from the attribution data.
374+
* The layout of the attribution data is as follows (using maxNumHops = 3 for conciseness):
375+
* holdTime(0) ++ holdTime(1) ++ holdTime(2) ++
376+
* hmacs(0)(0) ++ hmacs(0)(1) ++ hmacs(0)(2) ++
377+
* hmacs(1)(0) ++ hmacs(1)(1) ++
378+
* hmacs(2)(0)
379+
*
380+
* Where `hmac(i)(j)` is the hmac added by node `i` (counted from the node that built the attribution data),
381+
* assuming it is `maxNumHops - 1 - i - j` hops away from the erring node.
382+
*/
366383
private def getHmacs(bytes: ByteVector): Seq[Seq[ByteVector]] =
367-
(0 until 20).map(i => (0 until (20 - i)).map(j => {
368-
val start = (20 + 20 * i - (i * (i - 1)) / 2 + j) * 4
369-
bytes.slice(start, start + 4)
384+
(0 until maxNumHops).map(i => (0 until (maxNumHops - i)).map(j => {
385+
val start = maxNumHops * holdTimeLength + (maxNumHops * i - (i * (i - 1)) / 2 + j) * hmacLength
386+
bytes.slice(start, start + hmacLength)
370387
}))
371388

389+
/**
390+
* Computes the HMACs for the node that is `minNumHop` hops away from us. Hence we only compute `maxNumHops - minNumHop` HMACs.
391+
* 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.
392+
*/
372393
private def computeHmacs(mac: Mac32, reason: ByteVector, holdTimes: ByteVector, hmacs: Seq[Seq[ByteVector]], minNumHop: Int): Seq[ByteVector] = {
373-
(minNumHop until 20).map(i => {
374-
val y = 20 - i
394+
(minNumHop until maxNumHops).map(i => {
395+
val y = maxNumHops - i
375396
mac.mac(reason ++
376-
holdTimes.take(y * 4) ++
377-
ByteVector.concat((0 until y - 1).map(j => hmacs(j)(i)))).bytes.take(4)
397+
holdTimes.take(y * holdTimeLength) ++
398+
ByteVector.concat((0 until y - 1).map(j => hmacs(j)(i)))).bytes.take(hmacLength)
378399
})
379400
}
380401

381402
/**
382403
* Create attribution data to send with the failure packet
404+
*
405+
* @param reason the failure packet before being wrapped
383406
*/
384407
def create(previousAttribution_opt: Option[ByteVector], reason: ByteVector, holdTime: FiniteDuration, sharedSecret: ByteVector32): ByteVector = {
385-
val previousAttribution = previousAttribution_opt.getOrElse(ByteVector.low(920))
408+
val previousAttribution = previousAttribution_opt.getOrElse(ByteVector.low(totalLength))
386409
val previousHmacs = getHmacs(previousAttribution).dropRight(1).map(_.drop(1))
387410
val mac = Hmac256(generateKey("um", sharedSecret))
388-
val holdTimes = uint32.encode(holdTime.toMillis).map(_.bytes).getOrElse(ByteVector.high(4)) ++ previousAttribution.take(19 * 4)
411+
val holdTimes = uint32.encode(holdTime.toMillis).require.bytes ++ previousAttribution.take((maxNumHops - 1) * holdTimeLength)
389412
val hmacs = computeHmacs(mac, reason, holdTimes, previousHmacs, 0) +: previousHmacs
390413
cipher(holdTimes ++ ByteVector.concat(hmacs.map(ByteVector.concat(_))), sharedSecret)
391414
}
@@ -396,12 +419,12 @@ object Sphinx extends Logging {
396419
*/
397420
def unwrap(encrypted: ByteVector, reason: ByteVector, sharedSecret: ByteVector32, minNumHop: Int): Option[(FiniteDuration, ByteVector)] = {
398421
val bytes = cipher(encrypted, sharedSecret)
399-
val holdTime = uint32.decode(bytes.take(4).bits).require.value.milliseconds
422+
val holdTime = uint32.decode(bytes.take(holdTimeLength).bits).require.value.milliseconds
400423
val hmacs = getHmacs(bytes)
401424
val mac = Hmac256(generateKey("um", sharedSecret))
402-
if (computeHmacs(mac, reason, bytes.take(20 * 4), hmacs.drop(1), minNumHop) == hmacs.head.drop(minNumHop)) {
403-
val unwraped = bytes.slice(4, 20 * 4) ++ ByteVector.low(4) ++ ByteVector.concat((hmacs.drop(1) :+ Seq()).map(s => ByteVector.low(4) ++ ByteVector.concat(s)))
404-
Some(holdTime, unwraped)
425+
if (computeHmacs(mac, reason, bytes.take(maxNumHops * holdTimeLength), hmacs.drop(1), minNumHop) == hmacs.head.drop(minNumHop)) {
426+
val unwrapped = bytes.slice(holdTimeLength, maxNumHops * holdTimeLength) ++ ByteVector.low(holdTimeLength) ++ ByteVector.concat((hmacs.drop(1) :+ Seq()).map(s => ByteVector.low(hmacLength) ++ ByteVector.concat(s)))
427+
Some(holdTime, unwrapped)
405428
} else {
406429
None
407430
}

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Feature, Features, MilliSat
2929
import scodec.bits.ByteVector
3030
import scodec.{Attempt, DecodeResult}
3131

32-
import scala.concurrent.duration.FiniteDuration
32+
import scala.concurrent.duration.{DurationInt, FiniteDuration}
3333
import scala.util.{Failure, Success}
3434

3535
/**
@@ -381,7 +381,8 @@ object OutgoingPaymentPacket {
381381
val failure = InvalidOnionBlinding(Sphinx.hash(add.onionRoutingPacket))
382382
Right(UpdateFailMalformedHtlc(add.channelId, add.id, failure.onionHash, failure.code))
383383
case None =>
384-
val holdTime = now - cmd.startHoldTime
384+
// If the htlcReceivedAt was lost (because the node restarted), we use a hold time of 0 which should be ignored by the payer.
385+
val holdTime = cmd.htlcReceivedAt_opt.map(now - _).getOrElse(0 millisecond)
385386
buildHtlcFailure(nodeSecret, useAttributableFailures, cmd.reason, add, holdTime).map {
386387
case (encryptedReason, tlvs) => UpdateFailHtlc(add.channelId, cmd.id, encryptedReason, tlvs)
387388
}

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

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ class MultiPartHandler(nodeParams: NodeParams, register: ActorRef, db: IncomingP
119119
ctx.self ! ProcessPacket(add, payload, Some(IncomingStandardPayment(invoice, paymentPreimage, PaymentType.KeySend, TimestampMilli.now(), IncomingPaymentStatus.Pending)))
120120
case _ =>
121121
Metrics.PaymentFailed.withTag(Tags.Direction, Tags.Directions.Received).withTag(Tags.Failure, "InvoiceNotFound").increment()
122-
val cmdFail = CMD_FAIL_HTLC(add.id, FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(payload.totalAmount, nodeParams.currentBlockHeight)), TimestampMilli.now(), commit = true)
122+
val cmdFail = CMD_FAIL_HTLC(add.id, FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(payload.totalAmount, nodeParams.currentBlockHeight)), Some(TimestampMilli.now()), commit = true)
123123
PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, add.channelId, cmdFail)
124124
}
125125
}
@@ -145,7 +145,7 @@ class MultiPartHandler(nodeParams: NodeParams, register: ActorRef, db: IncomingP
145145

146146
case RejectPacket(add, failure) if doHandle(add.paymentHash) =>
147147
Metrics.PaymentFailed.withTag(Tags.Direction, Tags.Directions.Received).withTag(Tags.Failure, failure.getClass.getSimpleName).increment()
148-
val cmdFail = CMD_FAIL_HTLC(add.id, FailureReason.LocalFailure(failure), TimestampMilli.now(), commit = true)
148+
val cmdFail = CMD_FAIL_HTLC(add.id, FailureReason.LocalFailure(failure), Some(TimestampMilli.now()), commit = true)
149149
PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, add.channelId, cmdFail)
150150

151151
case MultiPartPaymentFSM.MultiPartPaymentFailed(paymentHash, failure, parts) if doHandle(paymentHash) =>
@@ -154,7 +154,7 @@ class MultiPartHandler(nodeParams: NodeParams, register: ActorRef, db: IncomingP
154154
log.warning("payment with paidAmount={} failed ({})", parts.map(_.amount).sum, failure)
155155
pendingPayments.get(paymentHash).foreach { case (_, handler: ActorRef) => handler ! PoisonPill }
156156
parts.collect {
157-
case p: MultiPartPaymentFSM.HtlcPart => PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, p.htlc.channelId, CMD_FAIL_HTLC(p.htlc.id, FailureReason.LocalFailure(failure), p.startTime, commit = true))
157+
case p: MultiPartPaymentFSM.HtlcPart => PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, p.htlc.channelId, CMD_FAIL_HTLC(p.htlc.id, FailureReason.LocalFailure(failure), Some(p.receivedAt), commit = true))
158158
}
159159
pendingPayments = pendingPayments - paymentHash
160160
}
@@ -174,7 +174,7 @@ class MultiPartHandler(nodeParams: NodeParams, register: ActorRef, db: IncomingP
174174
Logs.withMdc(log)(Logs.mdc(paymentHash_opt = Some(paymentHash))) {
175175
failure match {
176176
case Some(failure) => p match {
177-
case p: MultiPartPaymentFSM.HtlcPart => PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, p.htlc.channelId, CMD_FAIL_HTLC(p.htlc.id, FailureReason.LocalFailure(failure), p.startTime, commit = true))
177+
case p: MultiPartPaymentFSM.HtlcPart => PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, p.htlc.channelId, CMD_FAIL_HTLC(p.htlc.id, FailureReason.LocalFailure(failure), Some(p.receivedAt), commit = true))
178178
case _: MultiPartPaymentFSM.RecipientBlindedPathFeePart => ()
179179
}
180180
case None => p match {
@@ -186,7 +186,7 @@ class MultiPartHandler(nodeParams: NodeParams, register: ActorRef, db: IncomingP
186186
PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, p.htlc.channelId, CMD_FULFILL_HTLC(p.htlc.id, record.paymentPreimage, commit = true))
187187
ctx.system.eventStream.publish(received)
188188
} else {
189-
val cmdFail = CMD_FAIL_HTLC(p.htlc.id, FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(received.amount, nodeParams.currentBlockHeight)), p.startTime, commit = true)
189+
val cmdFail = CMD_FAIL_HTLC(p.htlc.id, FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(received.amount, nodeParams.currentBlockHeight)), Some(p.receivedAt), commit = true)
190190
PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, p.htlc.channelId, cmdFail)
191191
}
192192
})
@@ -221,7 +221,7 @@ class MultiPartHandler(nodeParams: NodeParams, register: ActorRef, db: IncomingP
221221
parts.collect {
222222
case p: MultiPartPaymentFSM.HtlcPart =>
223223
Metrics.PaymentFailed.withTag(Tags.Direction, Tags.Directions.Received).withTag(Tags.Failure, "InvoiceNotFound").increment()
224-
val cmdFail = CMD_FAIL_HTLC(p.htlc.id, FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(received.amount, nodeParams.currentBlockHeight)), p.startTime, commit = true)
224+
val cmdFail = CMD_FAIL_HTLC(p.htlc.id, FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(received.amount, nodeParams.currentBlockHeight)), Some(p.receivedAt), commit = true)
225225
PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, p.htlc.channelId, cmdFail)
226226
}
227227
}
@@ -466,15 +466,15 @@ object MultiPartHandler {
466466

467467
private def validateStandardPayment(nodeParams: NodeParams, add: UpdateAddHtlc, payload: FinalPayload.Standard, record: IncomingStandardPayment)(implicit log: LoggingAdapter): Option[CMD_FAIL_HTLC] = {
468468
// We send the same error regardless of the failure to avoid probing attacks.
469-
val cmdFail = CMD_FAIL_HTLC(add.id, FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(payload.totalAmount, nodeParams.currentBlockHeight)), TimestampMilli.now(), commit = true)
469+
val cmdFail = CMD_FAIL_HTLC(add.id, FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(payload.totalAmount, nodeParams.currentBlockHeight)), Some(TimestampMilli.now()), commit = true)
470470
val commonOk = validateCommon(nodeParams, add, payload, record)
471471
val secretOk = validatePaymentSecret(add, payload, record.invoice)
472472
if (commonOk && secretOk) None else Some(cmdFail)
473473
}
474474

475475
private def validateBlindedPayment(nodeParams: NodeParams, add: UpdateAddHtlc, payload: FinalPayload.Blinded, record: IncomingBlindedPayment, maxRecipientPathFees: MilliSatoshi)(implicit log: LoggingAdapter): Option[CMD_FAIL_HTLC] = {
476476
// We send the same error regardless of the failure to avoid probing attacks.
477-
val cmdFail = CMD_FAIL_HTLC(add.id, FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(payload.totalAmount, nodeParams.currentBlockHeight)), TimestampMilli.now(), commit = true)
477+
val cmdFail = CMD_FAIL_HTLC(add.id, FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(payload.totalAmount, nodeParams.currentBlockHeight)), Some(TimestampMilli.now()), commit = true)
478478
val commonOk = validateCommon(nodeParams, add, payload, record)
479479
// The payer isn't aware of the blinded path fees if we decided to hide them. The HTLC amount will thus be smaller
480480
// than the onion amount, but should match when re-adding the blinded path fees.

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ object MultiPartPaymentFSM {
134134
def totalAmount: MilliSatoshi
135135
}
136136
/** An incoming HTLC. */
137-
case class HtlcPart(totalAmount: MilliSatoshi, htlc: UpdateAddHtlc, startTime: TimestampMilli = TimestampMilli.now()) extends PaymentPart {
137+
case class HtlcPart(totalAmount: MilliSatoshi, htlc: UpdateAddHtlc, receivedAt: TimestampMilli = TimestampMilli.now()) extends PaymentPart {
138138
override def paymentHash: ByteVector32 = htlc.paymentHash
139139
override def amount: MilliSatoshi = htlc.amountMsat
140140
}

0 commit comments

Comments
 (0)