Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -221,9 +221,12 @@ final case class CMD_ADD_HTLC(replyTo: ActorRef,
origin: Origin.Hot,
commit: Boolean = false) extends HasReplyToCommand with ForbiddenCommandDuringQuiescenceNegotiation with ForbiddenCommandWhenQuiescent

case class FailureAttributionData(htlcReceivedAt: TimestampMilli, trampolineReceivedAt_opt: Option[TimestampMilli])
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, 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, 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
final case class CMD_SIGN(replyTo_opt: Option[ActorRef] = None) extends HasOptionalReplyToCommand with ForbiddenCommandWhenQuiescent
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -715,7 +715,8 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
case PostRevocationAction.RejectHtlc(add) =>
log.debug("rejecting incoming htlc {}", add)
// NB: we don't set commit = true, we will sign all updates at once afterwards.
self ! CMD_FAIL_HTLC(add.id, FailureReason.LocalFailure(TemporaryChannelFailure(Some(d.channelUpdate))), Some(TimestampMilli.now()), commit = true)
val attribution = FailureAttributionData(htlcReceivedAt = TimestampMilli.now(), trampolineReceivedAt_opt = None)
self ! CMD_FAIL_HTLC(add.id, FailureReason.LocalFailure(TemporaryChannelFailure(Some(d.channelUpdate))), Some(attribution), commit = true)
case PostRevocationAction.RelayFailure(result) =>
log.debug("forwarding {} to relayer", result)
relayer ! result
Expand Down Expand Up @@ -1660,11 +1661,13 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
case PostRevocationAction.RelayHtlc(add) =>
// BOLT 2: A sending node SHOULD fail to route any HTLC added after it sent shutdown.
log.debug("closing in progress: failing {}", add)
self ! CMD_FAIL_HTLC(add.id, FailureReason.LocalFailure(PermanentChannelFailure()), Some(TimestampMilli.now()), commit = true)
val attribution = FailureAttributionData(htlcReceivedAt = TimestampMilli.now(), trampolineReceivedAt_opt = None)
self ! CMD_FAIL_HTLC(add.id, FailureReason.LocalFailure(PermanentChannelFailure()), Some(attribution), commit = true)
case PostRevocationAction.RejectHtlc(add) =>
// BOLT 2: A sending node SHOULD fail to route any HTLC added after it sent shutdown.
log.debug("closing in progress: rejecting {}", add)
self ! CMD_FAIL_HTLC(add.id, FailureReason.LocalFailure(PermanentChannelFailure()), Some(TimestampMilli.now()), commit = true)
val attribution = FailureAttributionData(htlcReceivedAt = TimestampMilli.now(), trampolineReceivedAt_opt = None)
self ! CMD_FAIL_HTLC(add.id, FailureReason.LocalFailure(PermanentChannelFailure()), Some(attribution), commit = true)
case PostRevocationAction.RelayFailure(result) =>
log.debug("forwarding {} to relayer", result)
relayer ! result
Expand Down
35 changes: 18 additions & 17 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 @@ -403,29 +403,30 @@ object Sphinx extends Logging {
}

/**
* Create attribution data to send with the failure packet or with a fulfilled HTLC
* Create attribution data to send when settling an HTLC (in both failure and success cases).
*
* @param failurePacket_opt the failure packet before being wrapped or `None` for fulfilled HTLCs
* @param failurePacket_opt the failure packet before being wrapped or `None` for fulfilled HTLCs.
*/
def create(previousAttribution_opt: Option[ByteVector], failurePacket_opt: Option[ByteVector], holdTime: FiniteDuration, sharedSecret: ByteVector32): ByteVector = {
val previousAttribution = previousAttribution_opt.getOrElse(ByteVector.low(totalLength))
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
* 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 @@ -436,15 +437,15 @@ object Sphinx extends Logging {
case class UnwrappedAttribution(holdTimes: List[HoldTime], remaining_opt: Option[ByteVector])

/**
* Decrypt the hold times from the attribution data of a fulfilled HTLC
* Unwrap many hops of attribution data (e.g. used for fulfilled HTLCs).
*/
def fulfillHoldTimes(attribution: ByteVector, sharedSecrets: Seq[SharedSecret], hopIndex: Int = 0): UnwrappedAttribution = {
def unwrap(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) = unwrap(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 @@ -18,7 +18,7 @@ package fr.acinq.eclair.payment

import fr.acinq.bitcoin.scalacompat.ByteVector32
import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey}
import fr.acinq.eclair.channel.{CMD_ADD_HTLC, CMD_FAIL_HTLC, CMD_FULFILL_HTLC, CannotExtractSharedSecret, Origin}
import fr.acinq.eclair.channel._
import fr.acinq.eclair.crypto.Sphinx
import fr.acinq.eclair.payment.send.Recipient
import fr.acinq.eclair.router.Router.Route
Expand Down Expand Up @@ -384,7 +384,7 @@ object OutgoingPaymentPacket {
Right(UpdateFailMalformedHtlc(add.channelId, add.id, failure.onionHash, failure.code))
case None =>
// If the htlcReceivedAt was lost (because the node restarted), we use a hold time of 0 which should be ignored by the payer.
val holdTime = cmd.htlcReceivedAt_opt.map(now - _).getOrElse(0 millisecond)
val holdTime = cmd.attribution_opt.map(now - _.htlcReceivedAt).getOrElse(0 millisecond)
buildHtlcFailure(nodeSecret, useAttributableFailures, cmd.reason, add, holdTime).map {
case (encryptedReason, tlvs) => UpdateFailHtlc(add.channelId, cmd.id, encryptedReason, tlvs)
}
Expand All @@ -397,8 +397,8 @@ object OutgoingPaymentPacket {
extractSharedSecret(nodeSecret, add) match {
case Left(_) => TlvStream.empty
case Right(sharedSecret) =>
val holdTime = cmd.htlcReceivedAt_opt.map(now - _).getOrElse(0 millisecond)
TlvStream(UpdateFulfillHtlcTlv.AttributionData(Sphinx.Attribution.create(cmd.downstreamAttribution_opt, None, holdTime, sharedSecret)))
val holdTime = cmd.attribution_opt.map(now - _.htlcReceivedAt).getOrElse(0 millisecond)
TlvStream(UpdateFulfillHtlcTlv.AttributionData(Sphinx.Attribution.create(cmd.attribution_opt.flatMap(_.downstreamAttribution_opt), None, holdTime, sharedSecret)))
}
} else {
TlvStream.empty
Expand Down
Loading