Skip to content

Commit

Permalink
Basic support for attributable errors
Browse files Browse the repository at this point in the history
This commit adds support for creating and relaying attributable errors but does not ask for others to use them.
Also doe snot advertize the feature.
  • Loading branch information
thomash-acinq committed Aug 11, 2023
1 parent 4496ea7 commit 2f84f91
Show file tree
Hide file tree
Showing 33 changed files with 458 additions and 187 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import fr.acinq.eclair.payment.OutgoingPaymentPacket.Upstream
import fr.acinq.eclair.transactions.CommitmentSpec
import fr.acinq.eclair.transactions.Transactions._
import fr.acinq.eclair.wire.protocol.{ChannelAnnouncement, ChannelReady, ChannelReestablish, ChannelUpdate, ClosingSigned, CommitSig, FailureMessage, FundingCreated, FundingSigned, Init, OnionRoutingPacket, OpenChannel, OpenDualFundedChannel, Shutdown, SpliceInit, Stfu, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFulfillHtlc}
import fr.acinq.eclair.{Alias, BlockHeight, CltvExpiry, CltvExpiryDelta, Features, InitFeature, MilliSatoshi, MilliSatoshiLong, RealShortChannelId, UInt64}
import fr.acinq.eclair.{Alias, BlockHeight, CltvExpiry, CltvExpiryDelta, Features, InitFeature, MilliSatoshi, MilliSatoshiLong, RealShortChannelId, TimestampMilli, UInt64}
import scodec.bits.ByteVector

import java.util.UUID
Expand Down Expand Up @@ -192,7 +192,7 @@ sealed trait ForbiddenCommandDuringQuiescence extends Command
final case class CMD_ADD_HTLC(replyTo: ActorRef, amount: MilliSatoshi, paymentHash: ByteVector32, cltvExpiry: CltvExpiry, onion: OnionRoutingPacket, nextBlindingKey_opt: Option[PublicKey], origin: Origin.Hot, commit: Boolean = false) extends HasReplyToCommand with ForbiddenCommandDuringSplice with ForbiddenCommandDuringQuiescence
sealed trait HtlcSettlementCommand extends HasOptionalReplyToCommand with ForbiddenCommandDuringSplice with ForbiddenCommandDuringQuiescence { def id: Long }
final case class CMD_FULFILL_HTLC(id: Long, r: ByteVector32, commit: Boolean = false, replyTo_opt: Option[ActorRef] = None) extends HtlcSettlementCommand
final case class CMD_FAIL_HTLC(id: Long, reason: Either[ByteVector, FailureMessage], delay_opt: Option[FiniteDuration] = None, commit: Boolean = false, replyTo_opt: Option[ActorRef] = None) extends HtlcSettlementCommand
final case class CMD_FAIL_HTLC(id: Long, reason: Either[ByteVector, FailureMessage], useAttributableErrors: Boolean, startHoldTime: TimestampMilli, 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 ForbiddenCommandDuringSplice with ForbiddenCommandDuringQuiescence
final case class CMD_SIGN(replyTo_opt: Option[ActorRef] = None) extends HasOptionalReplyToCommand with ForbiddenCommandDuringSplice
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -624,7 +624,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
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, Right(TemporaryChannelFailure(d.channelUpdate)), commit = true)
self ! CMD_FAIL_HTLC(add.id, Right(TemporaryChannelFailure(d.channelUpdate)), useAttributableErrors = false, TimestampMilli.now(), commit = true)
case PostRevocationAction.RelayFailure(result) =>
log.debug("forwarding {} to relayer", result)
relayer ! result
Expand Down Expand Up @@ -1301,11 +1301,11 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
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, Right(PermanentChannelFailure()), commit = true)
self ! CMD_FAIL_HTLC(add.id, Right(PermanentChannelFailure()), useAttributableErrors = false, TimestampMilli.now(), 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, Right(PermanentChannelFailure()), commit = true)
self ! CMD_FAIL_HTLC(add.id, Right(PermanentChannelFailure()), useAttributableErrors = false, TimestampMilli.now(), commit = true)
case PostRevocationAction.RelayFailure(result) =>
log.debug("forwarding {} to relayer", result)
relayer ! result
Expand Down
87 changes: 87 additions & 0 deletions eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import scodec.Attempt
import scodec.bits.ByteVector

import scala.annotation.tailrec
import scala.collection.mutable.ArrayBuffer
import scala.concurrent.duration.FiniteDuration
import scala.util.{Failure, Success, Try}

/**
Expand Down Expand Up @@ -332,6 +334,91 @@ object Sphinx extends Logging {

}

case class InvalidAttributableErrorPacket(hopPayloads: Seq[(PublicKey, AttributableError.HopPayload)], failingNode: PublicKey)

object AttributableErrorPacket {

import AttributableError._

private val payloadAndPadLength = 256
private val hopPayloadLength = 5
private val maxNumHop = 20
private val totalLength = 4 + payloadAndPadLength + maxNumHop * hopPayloadLength + (maxNumHop * (maxNumHop + 1)) / 2 * 4

def create(sharedSecret: ByteVector32, failure: FailureMessage, holdTime: FiniteDuration): ByteVector = {
val failurePayload = FailureMessageCodecs.failureOnionPayload(payloadAndPadLength).encode(failure).require.toByteVector
val zeroPayloads = Seq.fill(maxNumHop)(ByteVector.fill(hopPayloadLength)(0))
val zeroHmacs = (maxNumHop.to(1, -1)).map(Seq.fill(_)(ByteVector.low(4)))
val plainError = attributableErrorCodec(totalLength, hopPayloadLength, maxNumHop).encode(AttributableError(failurePayload, zeroPayloads, zeroHmacs)).require.bytes
wrap(plainError, sharedSecret, holdTime, isSource = true).get
}

private def computeHmacs(mac: Mac32, failurePayload: ByteVector, hopPayloads: Seq[ByteVector], hmacs: Seq[Seq[ByteVector]], minNumHop: Int): Seq[ByteVector] = {
val newHmacs = (minNumHop until maxNumHop).map(i => {
val y = maxNumHop - i
mac.mac(failurePayload ++
ByteVector.concat(hopPayloads.take(y)) ++
ByteVector.concat((0 until y - 1).map(j => hmacs(j)(i)))).bytes.take(4)
})
newHmacs
}

def wrap(errorPacket: ByteVector, sharedSecret: ByteVector32, holdTime: FiniteDuration, isSource: Boolean): Try[ByteVector] = Try {
val um = generateKey("um", sharedSecret)
val error = attributableErrorCodec(errorPacket.length.toInt, hopPayloadLength, maxNumHop).decode(errorPacket.bits).require.value
val hopPayloads = hopPayloadCodec.encode(HopPayload(isSource, holdTime)).require.bytes +: error.hopPayloads.dropRight(1)
val hmacs = computeHmacs(Hmac256(um), error.failurePayload, hopPayloads, error.hmacs.map(_.drop(1)), 0) +: error.hmacs.dropRight(1).map(_.drop(1))
val newError = attributableErrorCodec(errorPacket.length.toInt, hopPayloadLength, maxNumHop).encode(AttributableError(error.failurePayload, hopPayloads, hmacs)).require.bytes
val key = generateKey("ammag", sharedSecret)
val stream = generateStream(key, newError.length.toInt)
newError xor stream
}

def wrapOrCreate(errorPacket: ByteVector, sharedSecret: ByteVector32, holdTime: FiniteDuration): ByteVector =
wrap(errorPacket, sharedSecret, holdTime, isSource = false) match {
case Failure(_) => create(sharedSecret, TemporaryNodeFailure(), holdTime)
case Success(value) => value
}

private def unwrap(errorPacket: ByteVector, sharedSecret: ByteVector32, minNumHop: Int): Try[(ByteVector, HopPayload)] = Try {
val key = generateKey("ammag", sharedSecret)
val stream = generateStream(key, errorPacket.length.toInt)
val error = attributableErrorCodec(errorPacket.length.toInt, hopPayloadLength, maxNumHop).decode((errorPacket xor stream).bits).require.value
val um = generateKey("um", sharedSecret)
val shiftedHmacs = error.hmacs.tail.map(ByteVector.low(4) +: _) :+ Seq(ByteVector.low(4))
val hmacs = computeHmacs(Hmac256(um), error.failurePayload, error.hopPayloads, error.hmacs.tail, minNumHop)
require(hmacs == error.hmacs.head.drop(minNumHop), "Invalid HMAC")
val shiftedHopPayloads = error.hopPayloads.tail :+ ByteVector.fill(hopPayloadLength)(0)
val unwrapedError = AttributableError(error.failurePayload, shiftedHopPayloads, shiftedHmacs)
(attributableErrorCodec(errorPacket.length.toInt, hopPayloadLength, maxNumHop).encode(unwrapedError).require.bytes,
hopPayloadCodec.decode(error.hopPayloads.head.bits).require.value)
}

def decrypt(errorPacket: ByteVector, sharedSecrets: Seq[(ByteVector32, PublicKey)]): Either[InvalidAttributableErrorPacket, DecryptedFailurePacket] = {
var packet = errorPacket
var minNumHop = 0
val hopPayloads = ArrayBuffer.empty[(PublicKey, HopPayload)]
for ((sharedSecret, nodeId) <- sharedSecrets) {
unwrap(packet, sharedSecret, minNumHop) match {
case Failure(_) => return Left(InvalidAttributableErrorPacket(hopPayloads.toSeq, nodeId))
case Success((unwrapedPacket, hopPayload)) if hopPayload.isPayloadSource =>
val failurePayload = attributableErrorCodec(errorPacket.length.toInt, hopPayloadLength, maxNumHop).decode(unwrapedPacket.bits).require.value.failurePayload
FailureMessageCodecs.failureOnionPayload(payloadAndPadLength).decode(failurePayload.bits) match {
case Attempt.Successful(failureMessage) =>
return Right(DecryptedFailurePacket(nodeId, failureMessage.value))
case Attempt.Failure(_) =>
return Left(InvalidAttributableErrorPacket(hopPayloads.toSeq, nodeId))
}
case Success((unwrapedPacket, hopPayload)) =>
packet = unwrapedPacket
minNumHop += 1
hopPayloads += ((nodeId, hopPayload))
}
}
Left(InvalidAttributableErrorPacket(hopPayloads.toSeq, sharedSecrets.last._2))
}
}

/**
* Route blinding is a lightweight technique to provide recipient anonymity by blinding an arbitrary amount of hops at
* the end of an onion path. It can be used for payments or onion messages.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import scodec.bits.ByteVector
import scodec.{Attempt, DecodeResult}

import java.util.UUID
import scala.concurrent.duration.FiniteDuration
import scala.util.{Failure, Success}

/**
Expand Down Expand Up @@ -246,7 +247,7 @@ object OutgoingPaymentPacket {
val expiryIn: CltvExpiry = adds.map(_.add.cltvExpiry).min
}

case class ReceivedHtlc(add: UpdateAddHtlc, receivedAt: TimestampMilli)
case class ReceivedHtlc(add: UpdateAddHtlc, receivedAt: TimestampMilli, useAttributableErrors: Boolean)
}
// @formatter:on

Expand Down Expand Up @@ -304,10 +305,12 @@ object OutgoingPaymentPacket {
}
}

private def buildHtlcFailure(nodeSecret: PrivateKey, reason: Either[ByteVector, FailureMessage], add: UpdateAddHtlc): Either[CannotExtractSharedSecret, ByteVector] = {
private def buildHtlcFailure(nodeSecret: PrivateKey, reason: Either[ByteVector, FailureMessage], add: UpdateAddHtlc, useAttributableErrors: Boolean, holdTime: FiniteDuration): Either[CannotExtractSharedSecret, ByteVector] = {
Sphinx.peel(nodeSecret, Some(add.paymentHash), add.onionRoutingPacket) match {
case Right(Sphinx.DecryptedPacket(_, _, sharedSecret)) =>
val encryptedReason = reason match {
case Left(forwarded) if useAttributableErrors => Sphinx.AttributableErrorPacket.wrapOrCreate(forwarded, sharedSecret, holdTime)
case Right(failure) if useAttributableErrors => Sphinx.AttributableErrorPacket.create(sharedSecret, failure, holdTime)
case Left(forwarded) => Sphinx.FailurePacket.wrap(forwarded, sharedSecret)
case Right(failure) => Sphinx.FailurePacket.create(sharedSecret, failure)
}
Expand All @@ -323,7 +326,7 @@ object OutgoingPaymentPacket {
val failure = InvalidOnionBlinding(Sphinx.hash(add.onionRoutingPacket))
Right(UpdateFailMalformedHtlc(add.channelId, add.id, failure.onionHash, failure.code))
case None =>
buildHtlcFailure(nodeSecret, cmd.reason, add).map(encryptedReason => UpdateFailHtlc(add.channelId, cmd.id, encryptedReason))
buildHtlcFailure(nodeSecret, cmd.reason, add, cmd.useAttributableErrors, TimestampMilli.now() - cmd.startHoldTime).map(encryptedReason => UpdateFailHtlc(add.channelId, cmd.id, encryptedReason))
}
}

Expand Down
Loading

0 comments on commit 2f84f91

Please sign in to comment.