Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement latest route blinding spec updates #2408

Merged
merged 4 commits into from Sep 12, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions eclair-core/src/main/resources/reference.conf
Expand Up @@ -57,6 +57,7 @@ eclair {
// Do not enable option_anchor_outputs unless you really know what you're doing.
option_anchor_outputs = disabled
option_anchors_zero_fee_htlc_tx = optional
option_route_blinding = disabled
option_shutdown_anysegwit = optional
option_dual_fund = disabled
option_onion_messages = optional
Expand Down
7 changes: 7 additions & 0 deletions eclair-core/src/main/scala/fr/acinq/eclair/Features.scala
Expand Up @@ -221,6 +221,11 @@ object Features {
val mandatory = 22
}

case object RouteBlinding extends Feature with InitFeature with NodeFeature with InvoiceFeature {
val rfcName = "option_route_blinding"
val mandatory = 24
}

case object ShutdownAnySegwit extends Feature with InitFeature with NodeFeature {
val rfcName = "option_shutdown_anysegwit"
val mandatory = 26
Expand Down Expand Up @@ -285,6 +290,7 @@ object Features {
StaticRemoteKey,
AnchorOutputs,
AnchorOutputsZeroFeeHtlcTx,
RouteBlinding,
ShutdownAnySegwit,
DualFunding,
OnionMessages,
Expand All @@ -303,6 +309,7 @@ object Features {
BasicMultiPartPayment -> (PaymentSecret :: Nil),
AnchorOutputs -> (StaticRemoteKey :: Nil),
AnchorOutputsZeroFeeHtlcTx -> (StaticRemoteKey :: Nil),
RouteBlinding -> (VariableLengthOnion :: Nil),
TrampolinePaymentPrototype -> (PaymentSecret :: Nil),
KeySend -> (VariableLengthOnion :: Nil)
)
Expand Down
Expand Up @@ -29,6 +29,7 @@ import fr.acinq.eclair.{Alias, BlockHeight, CltvExpiry, CltvExpiryDelta, Feature
import scodec.bits.ByteVector

import java.util.UUID
import scala.concurrent.duration.FiniteDuration

/**
* Created by PM on 20/05/2016.
Expand Down Expand Up @@ -183,7 +184,7 @@ final case class CMD_ADD_HTLC(replyTo: ActorRef, amount: MilliSatoshi, paymentHa
sealed trait HtlcSettlementCommand extends HasOptionalReplyToCommand { 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], 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_FAIL_MALFORMED_HTLC(id: Long, onionHash: ByteVector32, failureCode: Int, delay_opt: Option[FiniteDuration] = None, 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
final case class CMD_SIGN(replyTo_opt: Option[ActorRef] = None) extends HasOptionalReplyToCommand

Expand Down
Expand Up @@ -399,14 +399,20 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val
}

case Event(c: CMD_FAIL_MALFORMED_HTLC, d: DATA_NORMAL) =>
Commitments.sendFailMalformed(d.commitments, c) match {
case Right((commitments1, fail)) =>
if (c.commit) self ! CMD_SIGN()
context.system.eventStream.publish(AvailableBalanceChanged(self, d.channelId, d.shortIds, commitments1))
handleCommandSuccess(c, d.copy(commitments = commitments1)) sending fail
case Left(cause) =>
// we acknowledge the command right away in case of failure
handleCommandError(cause, c).acking(d.channelId, c)
c.delay_opt match {
case Some(delay) =>
log.debug("delaying CMD_FAIL_MALFORMED_HTLC with id={} for {}", c.id, delay)
context.system.scheduler.scheduleOnce(delay, self, c.copy(delay_opt = None))
stay()
case None => Commitments.sendFailMalformed(d.commitments, c) match {
case Right((commitments1, fail)) =>
if (c.commit) self ! CMD_SIGN()
context.system.eventStream.publish(AvailableBalanceChanged(self, d.channelId, d.shortIds, commitments1))
handleCommandSuccess(c, d.copy(commitments = commitments1)) sending fail
case Left(cause) =>
// we acknowledge the command right away in case of failure
handleCommandError(cause, c).acking(d.channelId, c)
}
}

case Event(fail: UpdateFailHtlc, d: DATA_NORMAL) =>
Expand Down
Expand Up @@ -25,7 +25,7 @@ import fr.acinq.eclair.crypto.Sphinx
import fr.acinq.eclair.router.Router.{ChannelHop, Hop, NodeHop}
import fr.acinq.eclair.wire.protocol.PaymentOnion.{FinalPayload, IntermediatePayload, PerHopPayload}
import fr.acinq.eclair.wire.protocol._
import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Features, MilliSatoshi, UInt64, randomBytes32, randomKey}
import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Feature, Features, MilliSatoshi, UInt64, randomBytes32, randomKey}
import scodec.bits.ByteVector
import scodec.{Attempt, DecodeResult}

Expand Down Expand Up @@ -77,24 +77,21 @@ object IncomingPaymentPacket {

private[payment] def decryptEncryptedRecipientData(add: UpdateAddHtlc, privateKey: PrivateKey, payload: TlvStream[OnionPaymentPayloadTlv], encryptedRecipientData: ByteVector): Either[FailureMessage, DecodedEncryptedRecipientData] = {
if (add.blinding_opt.isDefined && payload.get[OnionPaymentPayloadTlv.BlindingPoint].isDefined) {
// TODO: return an unparseable error
Left(InvalidOnionPayload(UInt64(12), 0))
Left(InvalidOnionBlinding(Sphinx.hash(add.onionRoutingPacket)))
} else {
add.blinding_opt.orElse(payload.get[OnionPaymentPayloadTlv.BlindingPoint].map(_.publicKey)) match {
case Some(blinding) => RouteBlindingEncryptedDataCodecs.decode(privateKey, blinding, encryptedRecipientData) match {
case Left(_) =>
// There are two possibilities in this case:
// - the blinding point is invalid: the sender or the previous node is buggy or malicious
// - the encrypted data is invalid: the sender, the previous node or the recipient must be buggy or malicious
// TODO: return an unparseable error
Left(InvalidOnionPayload(UInt64(12), 0))
Left(InvalidOnionBlinding(Sphinx.hash(add.onionRoutingPacket)))
case Right(decoded) => Right(DecodedEncryptedRecipientData(decoded.tlvs, decoded.nextBlinding))
}
case None =>
// The sender is trying to use route blinding, but we didn't receive the blinding point used to derive
// the decryption key. The sender or the previous peer is buggy or malicious.
// TODO: return an unparseable error
Left(InvalidOnionPayload(UInt64(12), 0))
Left(InvalidOnionBlinding(Sphinx.hash(add.onionRoutingPacket)))
}
}
}
Expand All @@ -110,7 +107,7 @@ object IncomingPaymentPacket {
* @param privateKey this node's private key
* @return whether the payment is to be relayed or if our node is the final recipient (or an error).
*/
def decrypt(add: UpdateAddHtlc, privateKey: PrivateKey)(implicit log: LoggingAdapter): Either[FailureMessage, IncomingPaymentPacket] = {
def decrypt(add: UpdateAddHtlc, privateKey: PrivateKey, features: Features[Feature])(implicit log: LoggingAdapter): Either[FailureMessage, IncomingPaymentPacket] = {
// We first derive the decryption key used to peel the onion.
val outerOnionDecryptionKey = add.blinding_opt match {
case Some(blinding) => Sphinx.RouteBlinding.derivePrivateKey(privateKey, blinding)
Expand All @@ -119,25 +116,25 @@ object IncomingPaymentPacket {
decryptOnion(add.paymentHash, outerOnionDecryptionKey, add.onionRoutingPacket).flatMap {
case DecodedOnionPacket(payload, Some(nextPacket)) =>
payload.get[OnionPaymentPayloadTlv.EncryptedRecipientData] match {
case Some(OnionPaymentPayloadTlv.EncryptedRecipientData(encryptedRecipientData)) =>
decryptEncryptedRecipientData(add, privateKey, payload, encryptedRecipientData).flatMap {
case Some(_) if !features.hasFeature(Features.RouteBlinding) => Left(InvalidOnionPayload(UInt64(10), 0))
case Some(encrypted) =>
decryptEncryptedRecipientData(add, privateKey, payload, encrypted.data).flatMap {
case DecodedEncryptedRecipientData(blindedPayload, nextBlinding) =>
validateBlindedChannelRelayPayload(add, payload, blindedPayload, nextBlinding, nextPacket)
}
case None if add.blinding_opt.isDefined => Left(InvalidOnionPayload(UInt64(12), 0))
t-bast marked this conversation as resolved.
Show resolved Hide resolved
case None if add.blinding_opt.isDefined => Left(InvalidOnionBlinding(Sphinx.hash(add.onionRoutingPacket)))
case None => IntermediatePayload.ChannelRelay.Standard.validate(payload).left.map(_.failureMessage).map {
payload => ChannelRelayPacket(add, payload, nextPacket)
}
}
case DecodedOnionPacket(payload, None) =>
payload.get[OnionPaymentPayloadTlv.EncryptedRecipientData] match {
case Some(OnionPaymentPayloadTlv.EncryptedRecipientData(encryptedRecipientData)) =>
decryptEncryptedRecipientData(add, privateKey, payload, encryptedRecipientData).flatMap {
case DecodedEncryptedRecipientData(blindedPayload, _) =>
// TODO: receiving through blinded routes is not supported yet.
FinalPayload.Blinded.validate(payload, blindedPayload).left.map(_.failureMessage).flatMap(_ => Left(InvalidOnionPayload(UInt64(12), 0)))
case Some(_) if !features.hasFeature(Features.RouteBlinding) => Left(InvalidOnionPayload(UInt64(10), 0))
case Some(encrypted) =>
decryptEncryptedRecipientData(add, privateKey, payload, encrypted.data).flatMap {
case DecodedEncryptedRecipientData(blindedPayload, _) => validateBlindedFinalPayload(add, payload, blindedPayload)
}
case None if add.blinding_opt.isDefined => Left(InvalidOnionPayload(UInt64(12), 0))
t-bast marked this conversation as resolved.
Show resolved Hide resolved
case None if add.blinding_opt.isDefined => Left(InvalidOnionBlinding(Sphinx.hash(add.onionRoutingPacket)))
case None =>
// We check if the payment is using trampoline: if it is, we may not be the final recipient.
payload.get[OnionPaymentPayloadTlv.TrampolineOnion] match {
Expand All @@ -146,7 +143,7 @@ object IncomingPaymentPacket {
// blinding point and use it to derive the decryption key for the blinded trampoline onion.
decryptOnion(add.paymentHash, privateKey, trampolinePacket).flatMap {
case DecodedOnionPacket(innerPayload, Some(next)) => validateNodeRelay(add, payload, innerPayload, next)
case DecodedOnionPacket(innerPayload, None) => validateFinalPayload(add, payload, innerPayload)
case DecodedOnionPacket(innerPayload, None) => validateTrampolineFinalPayload(add, payload, innerPayload)
}
case None => validateFinalPayload(add, payload)
}
Expand All @@ -156,10 +153,9 @@ object IncomingPaymentPacket {

private def validateBlindedChannelRelayPayload(add: UpdateAddHtlc, payload: TlvStream[OnionPaymentPayloadTlv], blindedPayload: TlvStream[RouteBlindingEncryptedDataTlv], nextBlinding: PublicKey, nextPacket: OnionRoutingPacket): Either[FailureMessage, ChannelRelayPacket] = {
IntermediatePayload.ChannelRelay.Blinded.validate(payload, blindedPayload, nextBlinding).left.map(_.failureMessage).flatMap {
// TODO: return an unparseable error
case payload if add.amountMsat < payload.paymentConstraints.minAmount => Left(InvalidOnionPayload(UInt64(12), 0))
case payload if add.cltvExpiry > payload.paymentConstraints.maxCltvExpiry => Left(InvalidOnionPayload(UInt64(12), 0))
case payload if !Features.areCompatible(Features.empty, payload.allowedFeatures) => Left(InvalidOnionPayload(UInt64(12), 0))
case payload if add.amountMsat < payload.paymentConstraints.minAmount => Left(InvalidOnionBlinding(Sphinx.hash(add.onionRoutingPacket)))
case payload if add.cltvExpiry > payload.paymentConstraints.maxCltvExpiry => Left(InvalidOnionBlinding(Sphinx.hash(add.onionRoutingPacket)))
case payload if !Features.areCompatible(Features.empty, payload.allowedFeatures) => Left(InvalidOnionBlinding(Sphinx.hash(add.onionRoutingPacket)))
case payload => Right(ChannelRelayPacket(add, payload, nextPacket))
}
}
Expand All @@ -172,7 +168,17 @@ object IncomingPaymentPacket {
}
}

private def validateFinalPayload(add: UpdateAddHtlc, outerPayload: TlvStream[OnionPaymentPayloadTlv], innerPayload: TlvStream[OnionPaymentPayloadTlv]): Either[FailureMessage, FinalPacket] = {
private def validateBlindedFinalPayload(add: UpdateAddHtlc, payload: TlvStream[OnionPaymentPayloadTlv], blindedPayload: TlvStream[RouteBlindingEncryptedDataTlv]): Either[FailureMessage, FinalPacket] = {
FinalPayload.Blinded.validate(payload, blindedPayload).left.map(_.failureMessage).flatMap {
case payload if add.amountMsat < payload.paymentConstraints.minAmount => Left(InvalidOnionBlinding(Sphinx.hash(add.onionRoutingPacket)))
case payload if add.cltvExpiry > payload.paymentConstraints.maxCltvExpiry => Left(InvalidOnionBlinding(Sphinx.hash(add.onionRoutingPacket)))
case payload if !Features.areCompatible(Features.empty, payload.allowedFeatures) => Left(InvalidOnionBlinding(Sphinx.hash(add.onionRoutingPacket)))
// TODO: receiving through blinded routes is not supported yet.
case _ => Left(InvalidOnionBlinding(Sphinx.hash(add.onionRoutingPacket)))
}
}

private def validateTrampolineFinalPayload(add: UpdateAddHtlc, outerPayload: TlvStream[OnionPaymentPayloadTlv], innerPayload: TlvStream[OnionPaymentPayloadTlv]): Either[FailureMessage, FinalPacket] = {
// The outer payload cannot use route blinding, but the inner payload may (but it's not supported yet).
FinalPayload.Standard.validate(outerPayload).left.map(_.failureMessage).flatMap { outerPayload =>
FinalPayload.Standard.validate(innerPayload).left.map(_.failureMessage).flatMap {
Expand Down
Expand Up @@ -23,6 +23,7 @@ import akka.actor.typed.scaladsl.adapter.TypedActorRefOps
import akka.actor.typed.scaladsl.{ActorContext, Behaviors}
import fr.acinq.bitcoin.scalacompat.ByteVector32
import fr.acinq.eclair.channel._
import fr.acinq.eclair.crypto.Sphinx
import fr.acinq.eclair.db.PendingCommandsDb
import fr.acinq.eclair.payment.Monitoring.{Metrics, Tags}
import fr.acinq.eclair.payment.relay.Relayer.{OutgoingChannel, OutgoingChannelParams}
Expand All @@ -32,6 +33,8 @@ import fr.acinq.eclair.wire.protocol._
import fr.acinq.eclair.{Logs, NodeParams, TimestampSecond, channel, nodeFee}

import java.util.UUID
import scala.concurrent.duration.DurationLong
import scala.util.Random

object ChannelRelay {

Expand Down Expand Up @@ -77,10 +80,21 @@ object ChannelRelay {
}
}

def translateRelayFailure(originHtlcId: Long, fail: HtlcResult.Fail): channel.Command with channel.HtlcSettlementCommand = {
def translateRelayFailure(originHtlcId: Long, fail: HtlcResult.Fail, relayPacket_opt: Option[IncomingPaymentPacket.ChannelRelayPacket]): channel.Command with channel.HtlcSettlementCommand = {
fail match {
case f: HtlcResult.RemoteFail => CMD_FAIL_HTLC(originHtlcId, Left(f.fail.reason), commit = true)
case f: HtlcResult.RemoteFailMalformed => CMD_FAIL_MALFORMED_HTLC(originHtlcId, f.fail.onionHash, f.fail.failureCode, commit = true)
case f: HtlcResult.RemoteFailMalformed => relayPacket_opt match {
case Some(IncomingPaymentPacket.ChannelRelayPacket(add, payload: IntermediatePayload.ChannelRelay.Blinded, _)) =>
// Bolt 2:
// - if it is part of a blinded route:
// - MUST return an `update_fail_malformed_htlc` error using the `invalid_onion_blinding` failure code, with the `sha256_of_onion` of the onion it received.
// - If its onion payload contains `current_blinding_point`:
// - SHOULD add a random delay before sending `update_fail_malformed_htlc`.
val delay_opt = payload.records.get[OnionPaymentPayloadTlv.BlindingPoint].map(_ => Random.nextLong(1000).millis)
CMD_FAIL_MALFORMED_HTLC(originHtlcId, Sphinx.hash(add.onionRoutingPacket), InvalidOnionBlinding(ByteVector32.Zeroes).code, delay_opt, commit = true)
case _ =>
CMD_FAIL_MALFORMED_HTLC(originHtlcId, f.fail.onionHash, f.fail.failureCode, commit = true)
}
case _: HtlcResult.OnChainFail => CMD_FAIL_HTLC(originHtlcId, Right(PermanentChannelFailure), commit = true)
case HtlcResult.ChannelFailureBeforeSigned => CMD_FAIL_HTLC(originHtlcId, Right(PermanentChannelFailure), commit = true)
case f: HtlcResult.DisconnectedBeforeSigned => CMD_FAIL_HTLC(originHtlcId, Right(TemporaryChannelFailure(f.channelUpdate)), commit = true)
Expand Down Expand Up @@ -154,7 +168,7 @@ class ChannelRelay private(nodeParams: NodeParams,
case WrappedAddResponse(RES_ADD_SETTLED(o: Origin.ChannelRelayedHot, _, fail: HtlcResult.Fail)) =>
context.log.info("relaying fail to upstream")
Metrics.recordPaymentRelayFailed(Tags.FailureType.Remote, Tags.RelayType.Channel)
val cmd = translateRelayFailure(o.originHtlcId, fail)
val cmd = translateRelayFailure(o.originHtlcId, fail, Some(r))
safeSendAndStop(o.originChannelId, cmd)
}

Expand Down