From 85317ebf3c11e9c92daadfe18e672b3fdc296752 Mon Sep 17 00:00:00 2001 From: Thomas HUET Date: Mon, 28 Nov 2022 11:18:08 +0100 Subject: [PATCH 1/6] Basic support for attributable errors 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. --- .../fr/acinq/eclair/channel/ChannelData.scala | 4 +- .../fr/acinq/eclair/channel/fsm/Channel.scala | 6 +- .../scala/fr/acinq/eclair/crypto/Sphinx.scala | 87 +++++++++++++++++++ .../acinq/eclair/payment/PaymentPacket.scala | 9 +- .../payment/receive/MultiPartHandler.scala | 28 +++--- .../payment/receive/MultiPartPaymentFSM.scala | 3 +- .../eclair/payment/relay/ChannelRelay.scala | 37 ++++---- .../eclair/payment/relay/NodeRelay.scala | 20 ++--- .../relay/PostRestartHtlcCleaner.scala | 7 +- .../acinq/eclair/payment/relay/Relayer.scala | 8 +- .../eclair/wire/internal/CommandCodecs.scala | 7 +- .../wire/protocol/AttributableError.scala | 51 +++++++++++ .../eclair/wire/protocol/CommonCodecs.scala | 2 + .../eclair/wire/protocol/FailureMessage.scala | 2 +- .../eclair/wire/protocol/PaymentOnion.scala | 10 +++ .../test/resources/attributable_error.json | 25 ++++++ .../eclair/balance/CheckBalanceSpec.scala | 2 +- .../eclair/channel/ChannelDataSpec.scala | 6 +- .../eclair/channel/CommitmentsSpec.scala | 4 +- .../ChannelStateTestsHelperMethods.scala | 2 +- .../states/e/NormalQuiescentStateSpec.scala | 2 +- .../channel/states/e/NormalStateSpec.scala | 16 ++-- .../channel/states/e/OfflineStateSpec.scala | 4 +- .../channel/states/f/ShutdownStateSpec.scala | 10 +-- .../fr/acinq/eclair/crypto/SphinxSpec.scala | 81 ++++++++++++++++- .../eclair/db/PendingCommandsDbSpec.scala | 8 +- .../eclair/payment/MultiPartHandlerSpec.scala | 26 +++--- .../payment/MultiPartPaymentFSMSpec.scala | 4 +- .../eclair/payment/PaymentPacketSpec.scala | 26 +++--- .../payment/PostRestartHtlcCleanerSpec.scala | 40 ++++----- .../payment/relay/ChannelRelayerSpec.scala | 44 +++++----- .../payment/relay/NodeRelayerSpec.scala | 52 +++++------ .../wire/internal/CommandCodecsSpec.scala | 12 +-- 33 files changed, 458 insertions(+), 187 deletions(-) create mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/AttributableError.scala create mode 100644 eclair-core/src/test/resources/attributable_error.json diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala index 9b946c5092..8e9401f62f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala @@ -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, TxSignatures, 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 @@ -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 diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala index 44cb81c11b..341b585c3d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala @@ -640,7 +640,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 @@ -1341,11 +1341,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 diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala index 1f9f27f37e..d58b5298c4 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala @@ -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} /** @@ -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. diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala index 2ff77b5998..3d8116ad4a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala @@ -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} /** @@ -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 @@ -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) } @@ -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)) } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala index 7764f11128..edd8516598 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala @@ -61,10 +61,10 @@ class MultiPartHandler(nodeParams: NodeParams, register: ActorRef, db: IncomingP private def addHtlcPart(ctx: ActorContext, add: UpdateAddHtlc, payload: FinalPayload, payment: IncomingPayment): Unit = { pendingPayments.get(add.paymentHash) match { case Some((_, handler)) => - handler ! MultiPartPaymentFSM.HtlcPart(payload.totalAmount, add) + handler ! MultiPartPaymentFSM.HtlcPart(payload.totalAmount, add, payload.useAttributableError) case None => val handler = ctx.actorOf(MultiPartPaymentFSM.props(nodeParams, add.paymentHash, payload.totalAmount, ctx.self)) - handler ! MultiPartPaymentFSM.HtlcPart(payload.totalAmount, add) + handler ! MultiPartPaymentFSM.HtlcPart(payload.totalAmount, add, payload.useAttributableError) pendingPayments = pendingPayments + (add.paymentHash -> (payment, handler)) } } @@ -124,7 +124,7 @@ class MultiPartHandler(nodeParams: NodeParams, register: ActorRef, db: IncomingP ctx.self ! ProcessPacket(add, payload, Some(IncomingStandardPayment(invoice, paymentPreimage, PaymentType.KeySend, TimestampMilli.now(), IncomingPaymentStatus.Pending))) case _ => Metrics.PaymentFailed.withTag(Tags.Direction, Tags.Directions.Received).withTag(Tags.Failure, "InvoiceNotFound").increment() - val cmdFail = CMD_FAIL_HTLC(add.id, Right(IncorrectOrUnknownPaymentDetails(payload.totalAmount, nodeParams.currentBlockHeight)), commit = true) + val cmdFail = CMD_FAIL_HTLC(add.id, Right(IncorrectOrUnknownPaymentDetails(payload.totalAmount, nodeParams.currentBlockHeight)), useAttributableErrors = payload.useAttributableError, TimestampMilli.now(), commit = true) PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, add.channelId, cmdFail) } } @@ -142,9 +142,9 @@ class MultiPartHandler(nodeParams: NodeParams, register: ActorRef, db: IncomingP } } - case RejectPacket(add, failure) if doHandle(add.paymentHash) => + case RejectPacket(add, failure, useAttributableError) if doHandle(add.paymentHash) => Metrics.PaymentFailed.withTag(Tags.Direction, Tags.Directions.Received).withTag(Tags.Failure, failure.getClass.getSimpleName).increment() - val cmdFail = CMD_FAIL_HTLC(add.id, Right(failure), commit = true) + val cmdFail = CMD_FAIL_HTLC(add.id, Right(failure), useAttributableError, TimestampMilli.now(), commit = true) PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, add.channelId, cmdFail) case MultiPartPaymentFSM.MultiPartPaymentFailed(paymentHash, failure, parts) if doHandle(paymentHash) => @@ -153,7 +153,7 @@ class MultiPartHandler(nodeParams: NodeParams, register: ActorRef, db: IncomingP log.warning("payment with paidAmount={} failed ({})", parts.map(_.amount).sum, failure) pendingPayments.get(paymentHash).foreach { case (_, handler: ActorRef) => handler ! PoisonPill } parts.collect { - case p: MultiPartPaymentFSM.HtlcPart => PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, p.htlc.channelId, CMD_FAIL_HTLC(p.htlc.id, Right(failure), commit = true)) + case p: MultiPartPaymentFSM.HtlcPart => PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, p.htlc.channelId, CMD_FAIL_HTLC(p.htlc.id, Right(failure), p.useAttributableErrors, p.startTime, commit = true)) } pendingPayments = pendingPayments - paymentHash } @@ -173,7 +173,7 @@ class MultiPartHandler(nodeParams: NodeParams, register: ActorRef, db: IncomingP Logs.withMdc(log)(Logs.mdc(paymentHash_opt = Some(paymentHash))) { failure match { case Some(failure) => p match { - case p: MultiPartPaymentFSM.HtlcPart => PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, p.htlc.channelId, CMD_FAIL_HTLC(p.htlc.id, Right(failure), commit = true)) + case p: MultiPartPaymentFSM.HtlcPart => PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, p.htlc.channelId, CMD_FAIL_HTLC(p.htlc.id, Right(failure), p.useAttributableErrors, p.startTime, commit = true)) } case None => p match { // NB: this case shouldn't happen unless the sender violated the spec, so it's ok that we take a slightly more @@ -184,7 +184,7 @@ class MultiPartHandler(nodeParams: NodeParams, register: ActorRef, db: IncomingP PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, p.htlc.channelId, CMD_FULFILL_HTLC(p.htlc.id, record.paymentPreimage, commit = true)) ctx.system.eventStream.publish(received) } else { - val cmdFail = CMD_FAIL_HTLC(p.htlc.id, Right(IncorrectOrUnknownPaymentDetails(received.amount, nodeParams.currentBlockHeight)), commit = true) + val cmdFail = CMD_FAIL_HTLC(p.htlc.id, Right(IncorrectOrUnknownPaymentDetails(received.amount, nodeParams.currentBlockHeight)), p.useAttributableErrors, p.startTime, commit = true) PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, p.htlc.channelId, cmdFail) } }) @@ -217,7 +217,7 @@ class MultiPartHandler(nodeParams: NodeParams, register: ActorRef, db: IncomingP parts.collect { case p: MultiPartPaymentFSM.HtlcPart => Metrics.PaymentFailed.withTag(Tags.Direction, Tags.Directions.Received).withTag(Tags.Failure, "InvoiceNotFound").increment() - val cmdFail = CMD_FAIL_HTLC(p.htlc.id, Right(IncorrectOrUnknownPaymentDetails(received.amount, nodeParams.currentBlockHeight)), commit = true) + val cmdFail = CMD_FAIL_HTLC(p.htlc.id, Right(IncorrectOrUnknownPaymentDetails(received.amount, nodeParams.currentBlockHeight)), p.useAttributableErrors, p.startTime, commit = true) PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, p.htlc.channelId, cmdFail) } } @@ -235,7 +235,7 @@ object MultiPartHandler { // @formatter:off case class ProcessPacket(add: UpdateAddHtlc, payload: FinalPayload.Standard, payment_opt: Option[IncomingStandardPayment]) case class ProcessBlindedPacket(add: UpdateAddHtlc, payload: FinalPayload.Blinded, payment: IncomingBlindedPayment) - case class RejectPacket(add: UpdateAddHtlc, failure: FailureMessage) + case class RejectPacket(add: UpdateAddHtlc, failure: FailureMessage, useAttributableError: Boolean) case class DoFulfill(payment: IncomingPayment, success: MultiPartPaymentFSM.MultiPartPaymentSucceeded) case object GetPendingPayments @@ -425,7 +425,7 @@ object MultiPartHandler { nodeParams.db.payments.getIncomingPayment(packet.add.paymentHash) match { case Some(_: IncomingBlindedPayment) => context.log.info("rejecting non-blinded htlc #{} from channel {}: expected a blinded payment", packet.add.id, packet.add.channelId) - replyTo ! RejectPacket(packet.add, IncorrectOrUnknownPaymentDetails(payload.totalAmount, nodeParams.currentBlockHeight)) + replyTo ! RejectPacket(packet.add, IncorrectOrUnknownPaymentDetails(payload.totalAmount, nodeParams.currentBlockHeight), payload.useAttributableError) case Some(payment: IncomingStandardPayment) => replyTo ! ProcessPacket(packet.add, payload, Some(payment)) case None => replyTo ! ProcessPacket(packet.add, payload, None) } @@ -446,7 +446,7 @@ object MultiPartHandler { Behaviors.stopped case RejectPayment(reason) => context.log.info("rejecting blinded htlc #{} from channel {}: {}", add.id, add.channelId, reason) - replyTo ! RejectPacket(add, IncorrectOrUnknownPaymentDetails(payload.totalAmount, nodeParams.currentBlockHeight)) + replyTo ! RejectPacket(add, IncorrectOrUnknownPaymentDetails(payload.totalAmount, nodeParams.currentBlockHeight), payload.useAttributableError) Behaviors.stopped } } @@ -520,7 +520,7 @@ object MultiPartHandler { private def validateStandardPayment(nodeParams: NodeParams, add: UpdateAddHtlc, payload: FinalPayload.Standard, record: IncomingStandardPayment)(implicit log: LoggingAdapter): Option[CMD_FAIL_HTLC] = { // We send the same error regardless of the failure to avoid probing attacks. - val cmdFail = CMD_FAIL_HTLC(add.id, Right(IncorrectOrUnknownPaymentDetails(payload.totalAmount, nodeParams.currentBlockHeight)), commit = true) + val cmdFail = CMD_FAIL_HTLC(add.id, Right(IncorrectOrUnknownPaymentDetails(payload.totalAmount, nodeParams.currentBlockHeight)), payload.useAttributableError, TimestampMilli.now(), commit = true) val commonOk = validateCommon(nodeParams, add, payload, record) val secretOk = validatePaymentSecret(add, payload, record.invoice) if (commonOk && secretOk) None else Some(cmdFail) @@ -528,7 +528,7 @@ object MultiPartHandler { private def validateBlindedPayment(nodeParams: NodeParams, add: UpdateAddHtlc, payload: FinalPayload.Blinded, record: IncomingBlindedPayment)(implicit log: LoggingAdapter): Option[CMD_FAIL_HTLC] = { // We send the same error regardless of the failure to avoid probing attacks. - val cmdFail = CMD_FAIL_HTLC(add.id, Right(IncorrectOrUnknownPaymentDetails(payload.totalAmount, nodeParams.currentBlockHeight)), commit = true) + val cmdFail = CMD_FAIL_HTLC(add.id, Right(IncorrectOrUnknownPaymentDetails(payload.totalAmount, nodeParams.currentBlockHeight)), payload.useAttributableError, TimestampMilli.now(), commit = true) val commonOk = validateCommon(nodeParams, add, payload, record) if (commonOk) None else Some(cmdFail) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartPaymentFSM.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartPaymentFSM.scala index db28e8f4ab..bd7d62c8bb 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartPaymentFSM.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartPaymentFSM.scala @@ -26,6 +26,7 @@ import fr.acinq.eclair.{FSMDiagnosticActorLogging, Logs, MilliSatoshi, NodeParam import java.util.concurrent.TimeUnit import scala.collection.immutable.Queue +import scala.concurrent.duration.FiniteDuration /** * Created by t-bast on 18/07/2019. @@ -134,7 +135,7 @@ object MultiPartPaymentFSM { def totalAmount: MilliSatoshi } /** An incoming HTLC. */ - case class HtlcPart(totalAmount: MilliSatoshi, htlc: UpdateAddHtlc) extends PaymentPart { + case class HtlcPart(totalAmount: MilliSatoshi, htlc: UpdateAddHtlc, useAttributableErrors: Boolean, startTime: TimestampMilli = TimestampMilli.now()) extends PaymentPart { override def paymentHash: ByteVector32 = htlc.paymentHash override def amount: MilliSatoshi = htlc.amountMsat } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelay.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelay.scala index adcb96729f..e768d31cdf 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelay.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelay.scala @@ -35,7 +35,7 @@ import fr.acinq.eclair.{Logs, NodeParams, TimestampMilli, TimestampSecond, chann import java.util.UUID import java.util.concurrent.TimeUnit -import scala.concurrent.duration.DurationLong +import scala.concurrent.duration.{DurationLong, FiniteDuration} import scala.util.Random object ChannelRelay { @@ -85,13 +85,13 @@ object ChannelRelay { } } - def translateRelayFailure(originHtlcId: Long, fail: HtlcResult.Fail): CMD_FAIL_HTLC = { + def translateRelayFailure(originHtlcId: Long, fail: HtlcResult.Fail, useAttributableErrors: Boolean, startHoldTime: TimestampMilli): CMD_FAIL_HTLC = { fail match { - case f: HtlcResult.RemoteFail => CMD_FAIL_HTLC(originHtlcId, Left(f.fail.reason), commit = true) - case f: HtlcResult.RemoteFailMalformed => CMD_FAIL_HTLC(originHtlcId, Right(createBadOnionFailure(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) + case f: HtlcResult.RemoteFail => CMD_FAIL_HTLC(originHtlcId, Left(f.fail.reason), useAttributableErrors, startHoldTime, commit = true) + case f: HtlcResult.RemoteFailMalformed => CMD_FAIL_HTLC(originHtlcId, Right(createBadOnionFailure(f.fail.onionHash, f.fail.failureCode)), useAttributableErrors, startHoldTime, commit = true) + case _: HtlcResult.OnChainFail => CMD_FAIL_HTLC(originHtlcId, Right(PermanentChannelFailure()), useAttributableErrors, startHoldTime, commit = true) + case HtlcResult.ChannelFailureBeforeSigned => CMD_FAIL_HTLC(originHtlcId, Right(PermanentChannelFailure()), useAttributableErrors, startHoldTime, commit = true) + case f: HtlcResult.DisconnectedBeforeSigned => CMD_FAIL_HTLC(originHtlcId, Right(TemporaryChannelFailure(f.channelUpdate)), useAttributableErrors, startHoldTime, commit = true) } } @@ -138,7 +138,7 @@ class ChannelRelay private(nodeParams: NodeParams, Behaviors.receiveMessagePartial { case WrappedForwardFailure(Register.ForwardFailure(Register.Forward(_, channelId, CMD_ADD_HTLC(_, _, _, _, _, _, o: Origin.ChannelRelayedHot, _)))) => context.log.warn(s"couldn't resolve downstream channel $channelId, failing htlc #${o.add.id}") - val cmdFail = CMD_FAIL_HTLC(o.add.id, Right(UnknownNextPeer()), commit = true) + val cmdFail = makeCmdFailHtlc(o.add.id, UnknownNextPeer()) Metrics.recordPaymentRelayFailed(Tags.FailureType(cmdFail), Tags.RelayType.Channel) safeSendAndStop(o.add.channelId, cmdFail) @@ -164,7 +164,7 @@ class ChannelRelay private(nodeParams: NodeParams, case WrappedAddResponse(RES_ADD_SETTLED(o: Origin.ChannelRelayedHot, _, fail: HtlcResult.Fail)) => context.log.debug("relaying fail to upstream") Metrics.recordPaymentRelayFailed(Tags.FailureType.Remote, Tags.RelayType.Channel) - val cmd = translateRelayFailure(o.originHtlcId, fail) + val cmd = translateRelayFailure(o.originHtlcId, fail, r.payload.useAttributableError, startedAt) recordRelayDuration(isSuccess = false) safeSendAndStop(o.originChannelId, cmd) } @@ -180,7 +180,7 @@ class ChannelRelay private(nodeParams: NodeParams, case Some(_) => // We are the introduction node: we add a delay to make it look like it could come from further downstream. val delay = Some(Random.nextLong(1000).millis) - CMD_FAIL_HTLC(cmd.id, Right(failure), delay, commit = true) + makeCmdFailHtlc(cmd.id, failure, delay) case None => // We are not the introduction node. CMD_FAIL_MALFORMED_HTLC(cmd.id, failure.onionHash, failure.code, commit = true) @@ -211,7 +211,7 @@ class ChannelRelay private(nodeParams: NodeParams, // otherwise we return the error for the first channel tried .getOrElse(previousFailures.head) .failure - RelayFailure(CMD_FAIL_HTLC(r.add.id, Right(translateLocalError(error.t, error.channelUpdate)), commit = true)) + RelayFailure(makeCmdFailHtlc(r.add.id, translateLocalError(error.t, error.channelUpdate))) case outgoingChannel_opt => relayOrFail(outgoingChannel_opt) } @@ -248,7 +248,7 @@ class ChannelRelay private(nodeParams: NodeParams, channel.channelUpdate, relayResult match { case _: RelaySuccess => "success" - case RelayFailure(CMD_FAIL_HTLC(_, Right(failureReason), _, _, _)) => failureReason + case RelayFailure(CMD_FAIL_HTLC(_, Right(failureReason), _, _, _, _, _)) => failureReason case other => other }) (channel, relayResult) @@ -292,18 +292,18 @@ class ChannelRelay private(nodeParams: NodeParams, def relayOrFail(outgoingChannel_opt: Option[OutgoingChannelParams]): RelayResult = { outgoingChannel_opt match { case None => - RelayFailure(CMD_FAIL_HTLC(r.add.id, Right(UnknownNextPeer()), commit = true)) + RelayFailure(makeCmdFailHtlc(r.add.id, UnknownNextPeer())) case Some(c) if !c.channelUpdate.channelFlags.isEnabled => - RelayFailure(CMD_FAIL_HTLC(r.add.id, Right(ChannelDisabled(c.channelUpdate.messageFlags, c.channelUpdate.channelFlags, c.channelUpdate)), commit = true)) + RelayFailure(makeCmdFailHtlc(r.add.id, ChannelDisabled(c.channelUpdate.messageFlags, c.channelUpdate.channelFlags, c.channelUpdate))) case Some(c) if r.amountToForward < c.channelUpdate.htlcMinimumMsat => - RelayFailure(CMD_FAIL_HTLC(r.add.id, Right(AmountBelowMinimum(r.amountToForward, c.channelUpdate)), commit = true)) + RelayFailure(makeCmdFailHtlc(r.add.id, AmountBelowMinimum(r.amountToForward, c.channelUpdate))) case Some(c) if r.expiryDelta < c.channelUpdate.cltvExpiryDelta => - RelayFailure(CMD_FAIL_HTLC(r.add.id, Right(IncorrectCltvExpiry(r.outgoingCltv, c.channelUpdate)), commit = true)) + RelayFailure(makeCmdFailHtlc(r.add.id, IncorrectCltvExpiry(r.outgoingCltv, c.channelUpdate))) case Some(c) if r.relayFeeMsat < nodeFee(c.channelUpdate.relayFees, r.amountToForward) && // fees also do not satisfy the previous channel update for `enforcementDelay` seconds after current update (TimestampSecond.now() - c.channelUpdate.timestamp > nodeParams.relayParams.enforcementDelay || outgoingChannel_opt.flatMap(_.prevChannelUpdate).forall(c => r.relayFeeMsat < nodeFee(c.relayFees, r.amountToForward))) => - RelayFailure(CMD_FAIL_HTLC(r.add.id, Right(FeeInsufficient(r.add.amountMsat, c.channelUpdate)), commit = true)) + RelayFailure(makeCmdFailHtlc(r.add.id, FeeInsufficient(r.add.amountMsat, c.channelUpdate))) case Some(c: OutgoingChannel) => val origin = Origin.ChannelRelayedHot(addResponseAdapter.toClassic, r.add, r.amountToForward) val nextBlindingKey_opt = r.payload match { @@ -314,6 +314,9 @@ class ChannelRelay private(nodeParams: NodeParams, } } + private def makeCmdFailHtlc(originHtlcId: Long, reason: FailureMessage, delay_opt: Option[FiniteDuration] = None): CMD_FAIL_HTLC = + CMD_FAIL_HTLC(originHtlcId, Right(reason), r.payload.useAttributableError, startedAt, delay_opt, commit = true) + private def recordRelayDuration(isSuccess: Boolean): Unit = Metrics.RelayedPaymentDuration .withTag(Tags.Relay, Tags.RelayType.Channel) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelay.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelay.scala index cce7d0095f..2c41ad7889 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelay.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelay.scala @@ -188,12 +188,12 @@ class NodeRelay private(nodeParams: NodeParams, case Relay(IncomingPaymentPacket.NodeRelayPacket(add, outer, _, _)) => require(outer.paymentSecret == paymentSecret, "payment secret mismatch") context.log.debug("forwarding incoming htlc #{} from channel {} to the payment FSM", add.id, add.channelId) - handler ! MultiPartPaymentFSM.HtlcPart(outer.totalAmount, add) - receiving(htlcs :+ Upstream.ReceivedHtlc(add, TimestampMilli.now()), nextPayload, nextPacket, handler) + handler ! MultiPartPaymentFSM.HtlcPart(outer.totalAmount, add, outer.useAttributableError) + receiving(htlcs :+ Upstream.ReceivedHtlc(add, TimestampMilli.now(), outer.useAttributableError), nextPayload, nextPacket, handler) case WrappedMultiPartPaymentFailed(MultiPartPaymentFSM.MultiPartPaymentFailed(_, failure, parts)) => context.log.warn("could not complete incoming multi-part payment (parts={} paidAmount={} failure={})", parts.size, parts.map(_.amount).sum, failure) Metrics.recordPaymentRelayFailed(failure.getClass.getSimpleName, Tags.RelayType.Trampoline) - parts.collect { case p: MultiPartPaymentFSM.HtlcPart => rejectHtlc(p.htlc.id, p.htlc.channelId, p.amount, Some(failure)) } + parts.collect { case p: MultiPartPaymentFSM.HtlcPart => rejectHtlc(p.htlc.id, p.htlc.channelId, p.amount, Some(failure), p.useAttributableErrors, p.startTime) } stopping() case WrappedMultiPartPaymentSucceeded(MultiPartPaymentFSM.MultiPartPaymentSucceeded(_, parts)) => context.log.info("completed incoming multi-part payment with parts={} paidAmount={}", parts.size, parts.map(_.amount).sum) @@ -332,31 +332,31 @@ class NodeRelay private(nodeParams: NodeParams, private def rejectExtraHtlcPartialFunction: PartialFunction[Command, Behavior[Command]] = { case Relay(nodeRelayPacket) => - rejectExtraHtlc(nodeRelayPacket.add) + rejectExtraHtlc(nodeRelayPacket.add, nodeRelayPacket.outerPayload.useAttributableError, TimestampMilli.now()) Behaviors.same // NB: this message would be sent from the payment FSM which we stopped before going to this state, but all this is asynchronous. // We always fail extraneous HTLCs. They are a spec violation from the sender, but harmless in the relay case. // By failing them fast (before the payment has reached the final recipient) there's a good chance the sender won't lose any money. // We don't expect to relay pay-to-open payments. case WrappedMultiPartExtraPaymentReceived(extraPaymentReceived) => - rejectExtraHtlc(extraPaymentReceived.payment.htlc) + rejectExtraHtlc(extraPaymentReceived.payment.htlc, extraPaymentReceived.payment.useAttributableErrors, extraPaymentReceived.payment.startTime) Behaviors.same } - private def rejectExtraHtlc(add: UpdateAddHtlc): Unit = { + private def rejectExtraHtlc(add: UpdateAddHtlc, useAttributableErrors: Boolean, startHoldTime: TimestampMilli): Unit = { context.log.warn("rejecting extra htlc #{} from channel {}", add.id, add.channelId) - rejectHtlc(add.id, add.channelId, add.amountMsat) + rejectHtlc(add.id, add.channelId, add.amountMsat, None, useAttributableErrors, startHoldTime) } - private def rejectHtlc(htlcId: Long, channelId: ByteVector32, amount: MilliSatoshi, failure: Option[FailureMessage] = None): Unit = { + private def rejectHtlc(htlcId: Long, channelId: ByteVector32, amount: MilliSatoshi, failure: Option[FailureMessage] = None, useAttributableErrors: Boolean, startHoldTime: TimestampMilli): Unit = { val failureMessage = failure.getOrElse(IncorrectOrUnknownPaymentDetails(amount, nodeParams.currentBlockHeight)) - val cmd = CMD_FAIL_HTLC(htlcId, Right(failureMessage), commit = true) + val cmd = CMD_FAIL_HTLC(htlcId, Right(failureMessage), useAttributableErrors, startHoldTime, commit = true) PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, channelId, cmd) } private def rejectPayment(upstream: Upstream.Trampoline, failure: Option[FailureMessage]): Unit = { Metrics.recordPaymentRelayFailed(failure.map(_.getClass.getSimpleName).getOrElse("Unknown"), Tags.RelayType.Trampoline) - upstream.adds.foreach(r => rejectHtlc(r.add.id, r.add.channelId, upstream.amountIn, failure)) + upstream.adds.foreach(r => rejectHtlc(r.add.id, r.add.channelId, upstream.amountIn, failure, r.useAttributableErrors, r.receivedAt)) } private def fulfillPayment(upstream: Upstream.Trampoline, paymentPreimage: ByteVector32): Unit = upstream.adds.foreach(r => { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/PostRestartHtlcCleaner.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/PostRestartHtlcCleaner.scala index 8e48763f27..ba87470c73 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/PostRestartHtlcCleaner.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/PostRestartHtlcCleaner.scala @@ -32,6 +32,7 @@ import fr.acinq.eclair.wire.protocol.{FailureMessage, InvalidOnionBlinding, Temp import fr.acinq.eclair.{CustomCommitmentsPlugin, Feature, Features, Logs, MilliSatoshiLong, NodeParams, TimestampMilli} import scala.concurrent.Promise +import scala.concurrent.duration.DurationInt import scala.util.Try /** @@ -131,7 +132,7 @@ class PostRestartHtlcCleaner(nodeParams: NodeParams, register: ActorRef, initial val failure = InvalidOnionBlinding(ByteVector32.Zeroes) CMD_FAIL_MALFORMED_HTLC(htlc.id, failure.onionHash, failure.code, commit = true) case None => - CMD_FAIL_HTLC(htlc.id, Right(TemporaryNodeFailure()), commit = true) + CMD_FAIL_HTLC(htlc.id, Right(TemporaryNodeFailure()), useAttributableErrors = false, TimestampMilli.min, commit = true) } channel ! cmd } else { @@ -261,7 +262,7 @@ class PostRestartHtlcCleaner(nodeParams: NodeParams, register: ActorRef, initial val failure = InvalidOnionBlinding(ByteVector32.Zeroes) CMD_FAIL_MALFORMED_HTLC(originHtlcId, failure.onionHash, failure.code, commit = true) case None => - ChannelRelay.translateRelayFailure(originHtlcId, fail) + ChannelRelay.translateRelayFailure(originHtlcId, fail, useAttributableErrors = false, TimestampMilli.min) } PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, originChannelId, cmd) case Origin.TrampolineRelayedCold(origins) => @@ -270,7 +271,7 @@ class PostRestartHtlcCleaner(nodeParams: NodeParams, register: ActorRef, initial Metrics.Resolved.withTag(Tags.Success, value = false).withTag(Metrics.Relayed, value = true).increment() // We don't bother decrypting the downstream failure to forward a more meaningful error upstream, it's // very likely that it won't be actionable anyway because of our node restart. - PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, channelId, CMD_FAIL_HTLC(htlcId, Right(TemporaryNodeFailure()), commit = true)) + PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, channelId, CMD_FAIL_HTLC(htlcId, Right(TemporaryNodeFailure()), useAttributableErrors = false, TimestampMilli.min, commit = true)) } } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala index 0b7e31ec06..f2c28a4201 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala @@ -29,7 +29,7 @@ import fr.acinq.eclair.channel._ import fr.acinq.eclair.db.PendingCommandsDb import fr.acinq.eclair.payment._ import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{CltvExpiryDelta, Logs, MilliSatoshi, NodeParams} +import fr.acinq.eclair.{CltvExpiryDelta, Logs, MilliSatoshi, NodeParams, TimestampMilli} import grizzled.slf4j.Logging import scala.concurrent.Promise @@ -73,7 +73,7 @@ class Relayer(nodeParams: NodeParams, router: ActorRef, register: ActorRef, paym case Right(r: IncomingPaymentPacket.NodeRelayPacket) => if (!nodeParams.enableTrampolinePayment) { log.warning(s"rejecting htlc #${add.id} from channelId=${add.channelId} to nodeId=${r.innerPayload.outgoingNodeId} reason=trampoline disabled") - PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, add.channelId, CMD_FAIL_HTLC(add.id, Right(RequiredNodeFeatureMissing()), commit = true)) + PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, add.channelId, CMD_FAIL_HTLC(add.id, Right(RequiredNodeFeatureMissing()), r.outerPayload.useAttributableError,TimestampMilli.now(), commit = true)) } else { nodeRelayer ! NodeRelayer.Relay(r) } @@ -84,7 +84,7 @@ class Relayer(nodeParams: NodeParams, router: ActorRef, register: ActorRef, paym // We are the introduction point of a blinded path: we add a non-negligible delay to make it look like it // could come from a downstream node. val delay = Some(500.millis + Random.nextLong(1500).millis) - CMD_FAIL_HTLC(add.id, Right(InvalidOnionBlinding(badOnion.onionHash)), delay, commit = true) + CMD_FAIL_HTLC(add.id, Right(InvalidOnionBlinding(badOnion.onionHash)), useAttributableErrors = false, TimestampMilli.now(), delay, commit = true) case _ => CMD_FAIL_MALFORMED_HTLC(add.id, badOnion.onionHash, badOnion.code, commit = true) } @@ -92,7 +92,7 @@ class Relayer(nodeParams: NodeParams, router: ActorRef, register: ActorRef, paym PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, add.channelId, cmdFail) case Left(failure) => log.warning(s"rejecting htlc #${add.id} from channelId=${add.channelId} reason=$failure") - val cmdFail = CMD_FAIL_HTLC(add.id, Right(failure), commit = true) + val cmdFail = CMD_FAIL_HTLC(add.id, Right(failure), useAttributableErrors = false, TimestampMilli.now(), commit = true) PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, add.channelId, cmdFail) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/CommandCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/CommandCodecs.scala index 8bf7c6d20a..9aae27c671 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/CommandCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/CommandCodecs.scala @@ -17,6 +17,7 @@ package fr.acinq.eclair.wire.internal import akka.actor.ActorRef +import fr.acinq.eclair.TimestampMilli import fr.acinq.eclair.channel._ import fr.acinq.eclair.wire.protocol.CommonCodecs._ import fr.acinq.eclair.wire.protocol.FailureMessageCodecs._ @@ -24,7 +25,7 @@ import fr.acinq.eclair.wire.protocol._ import scodec.Codec import scodec.codecs._ -import scala.concurrent.duration.FiniteDuration +import scala.concurrent.duration.{DurationInt, FiniteDuration} object CommandCodecs { @@ -33,6 +34,8 @@ object CommandCodecs { private val legacyCmdFailCodec: Codec[CMD_FAIL_HTLC] = (("id" | int64) :: ("reason" | either(bool, varsizebinarydata, provide(TemporaryNodeFailure()).upcast[FailureMessage])) :: + ("useAttributableErrors" | provide(false)) :: + ("startHoldTime" | provide(TimestampMilli.min)) :: ("delay_opt" | provide(Option.empty[FiniteDuration])) :: ("commit" | provide(false)) :: ("replyTo_opt" | provide(Option.empty[ActorRef]))).as[CMD_FAIL_HTLC] @@ -46,6 +49,8 @@ object CommandCodecs { private val cmdFailCodec: Codec[CMD_FAIL_HTLC] = (("id" | int64) :: ("reason" | either(bool8, varsizebinarydata, variableSizeBytes(uint16, failureMessageCodec))) :: + ("useAttributableErrors" | bool8) :: + ("startHoldTime" | uint64overflow.xmapc(TimestampMilli(_))(_.toLong)) :: // No need to delay commands after a restart, we've been offline which already created a random delay. ("delay_opt" | provide(Option.empty[FiniteDuration])) :: ("commit" | provide(false)) :: diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/AttributableError.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/AttributableError.scala new file mode 100644 index 0000000000..785d14d094 --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/AttributableError.scala @@ -0,0 +1,51 @@ +/* + * Copyright 2022 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fr.acinq.eclair.wire.protocol + +import fr.acinq.eclair.wire.protocol.CommonCodecs._ +import scodec.Codec +import scodec.bits.ByteVector +import scodec.codecs._ + +import scala.concurrent.duration.{DurationLong, FiniteDuration} + + +case class AttributableError(failurePayload: ByteVector, hopPayloads: Seq[ByteVector], hmacs: Seq[Seq[ByteVector]]) + +object AttributableError { + case class HopPayload(isPayloadSource: Boolean, holdTime: FiniteDuration) + + def hopPayloadCodec: Codec[HopPayload] = ( + ("payload_source" | bool8) :: + ("hold_time_ms" | uint32.xmap[FiniteDuration](_.millis, _.toMillis))).as[HopPayload] + + private def hmacsCodec(n: Int): Codec[Seq[Seq[ByteVector]]] = + if (n == 0) { + provide(Nil) + } + else { + (listOfN(provide(n), bytes4).xmap[Seq[ByteVector]](_.toSeq, _.toList) :: + hmacsCodec(n - 1)).as[(Seq[ByteVector], Seq[Seq[ByteVector]])] + .xmap(pair => pair._1 +: pair._2, seq => (seq.head, seq.tail)) + } + + def attributableErrorCodec(totalLength: Int, hopPayloadLength: Int, maxNumHop: Int): Codec[AttributableError] = { + val metadataLength = maxNumHop * hopPayloadLength + (maxNumHop * (maxNumHop + 1)) / 2 * 4 + (("failure_payload" | bytes(totalLength - metadataLength)) :: + ("hop_payloads" | listOfN(provide(maxNumHop), bytes(hopPayloadLength)).xmap[Seq[ByteVector]](_.toSeq, _.toList)) :: + ("hmacs" | hmacsCodec(maxNumHop))).as[AttributableError].complete} +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/CommonCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/CommonCodecs.scala index d96d8c4131..de0272144c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/CommonCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/CommonCodecs.scala @@ -101,6 +101,8 @@ object CommonCodecs { // It is useful in combination with variableSizeBytesLong to encode/decode TLV lengths because those will always be < 2^63. val varintoverflow: Codec[Long] = varint.narrow(l => if (l <= UInt64(Long.MaxValue)) Attempt.successful(l.toBigInt.toLong) else Attempt.failure(Err(s"overflow for value $l")), l => UInt64(l)) + val bytes4: Codec[ByteVector] = limitedSizeBytes(4, bytesStrict(4)) + val bytes32: Codec[ByteVector32] = limitedSizeBytes(32, bytesStrict(32).xmap(d => ByteVector32(d), d => d.bytes)) val bytes64: Codec[ByteVector64] = limitedSizeBytes(64, bytesStrict(64).xmap(d => ByteVector64(d), d => d.bytes)) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/FailureMessage.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/FailureMessage.scala index c2f907251c..b29cd049a8 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/FailureMessage.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/FailureMessage.scala @@ -154,7 +154,7 @@ object FailureMessageCodecs { fallback = unknownFailureMessageCodec.upcast[FailureMessage] ) - private def failureOnionPayload(payloadAndPadLength: Int): Codec[FailureMessage] = Codec( + def failureOnionPayload(payloadAndPadLength: Int): Codec[FailureMessage] = Codec( encoder = f => variableSizeBytes(uint16, failureMessageCodec).encode(f).flatMap(bits => { val payloadLength = bits.bytes.length - 2 val padLen = payloadAndPadLength - payloadLength diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/PaymentOnion.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/PaymentOnion.scala index 9c458ba466..da56ee0765 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/PaymentOnion.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/PaymentOnion.scala @@ -164,6 +164,11 @@ object OnionPaymentPayloadTlv { */ case class PaymentMetadata(data: ByteVector) extends OnionPaymentPayloadTlv + /** + * Flag to signal that attributable errors mut be used for this HTLC. + */ + case class UseAttributableError() extends OnionPaymentPayloadTlv + /** * Invoice feature bits. Only included for intermediate trampoline nodes when they should convert to a legacy payment * because the final recipient doesn't support trampoline. @@ -215,6 +220,8 @@ object PaymentOnion { /** Per-hop payload from an HTLC's payment onion (after decryption and decoding). */ sealed trait PerHopPayload { def records: TlvStream[OnionPaymentPayloadTlv] + + def useAttributableError: Boolean = records.get[UseAttributableError].isDefined } /** Per-hop payload for an intermediate node. */ @@ -505,6 +512,8 @@ object PaymentOnionCodecs { private val totalAmount: Codec[TotalAmount] = tlvField(tmillisatoshi) + private val useAttributableError: Codec[UseAttributableError] = fixedLengthTlvField(0, provide(UseAttributableError())) + private val invoiceFeatures: Codec[InvoiceFeatures] = tlvField(bytes) private val invoiceRoutingInfo: Codec[InvoiceRoutingInfo] = tlvField(list(listOfN(uint8, Bolt11Invoice.Codecs.extraHopCodec))) @@ -524,6 +533,7 @@ object PaymentOnionCodecs { .typecase(UInt64(12), blindingPoint) .typecase(UInt64(16), paymentMetadata) .typecase(UInt64(18), totalAmount) + .typecase(UInt64(20), useAttributableError) // Types below aren't specified - use cautiously when deploying (be careful with backwards-compatibility). .typecase(UInt64(66097), invoiceFeatures) .typecase(UInt64(66098), outgoingNodeId) diff --git a/eclair-core/src/test/resources/attributable_error.json b/eclair-core/src/test/resources/attributable_error.json new file mode 100644 index 0000000000..218f9e3c20 --- /dev/null +++ b/eclair-core/src/test/resources/attributable_error.json @@ -0,0 +1,25 @@ +{ + "encodedFailureMessage": "0140400f0000000000000064000c3500fd84d1fd012c80808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808002c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "hops": [ + { + "sharedSecret": "b5756b9b542727dbafc6765a49488b023a725d631af688fc031217e90770c328", + "encryptedMessage": "e88935bf7c9a374ba64fd9da3aa2a05e6cf9e52cfb1f72698fd33b74b9c79346284931ea3571af54de5df341304833b0825fb7e8817fd82a29c0803f0a0679a6a073c33a6fb8250090a3152eba3f11a85184fa87b67f1b0354d6f48e3b342e332a17b7710f342f342a87cf32eccdf0afc2160808d58abb5e5840d2c760c538e63a6f841970f97d2e6fe5b8739dc45e2f7f5f532f227bcc2988ab0f9cc6d3f12909cd5842c37bc8c7608475a5ebbe10626d5ecc1f3388ad5f645167b44a4d166f87863fe34918cea25c18059b4c4d9cb414b59f6bc50c1cea749c80c43e2344f5d23159122ed4ab9722503b212016470d9610b46c35dbeebaf2e342e09770b38392a803bc9d2e7c8d6d384ffcbeb74943fe3f64afb2a543a6683c7db3088441c531eeb4647518cb41992f8954f1269fb969630944928c2d2b45593731b5da0c4e70d0c84ad72f642fc26919927b347808bade4b1c321b08bc363f20745ba2f97f0ced2996a232f55ba28fe7dfa70a9ab0433a085388f25cce8d53de6a2fbd7546377d6ede9027ad173ba1f95767461a3689ef405ab608a21086165c64b02c1782b04a6dba2361a7784603069124e12f2f6dcb1ec7612a4fbf94c0e14631a2bef6190c3d5f35e0c4b32aa85201f449d830fd8f782ec758b0910428e3ec3ca1dba3b6c7d89f69e1ee1b9df3dfbbf6d361e1463886b38d52e8f43b73a3bd48c6f36f5897f514b93364a31d49d1d506340b1315883d425cb36f4ea553430d538fd6f3596d4afc518db2f317dd051abc0d4bfb0a7870c3db70f19fe78d6604bbf088fcb4613f54e67b038277fedcd9680eb97bdffc3be1ab2cbcbafd625b8a7ac34d8c190f98d3064ecd3b95b8895157c6a37f31ef4de094b2cb9dbf8ff1f419ba0ecacb1bb13df0253b826bec2ccca1e745dd3b3e7cc6277ce284d649e7b8285727735ff4ef6cca6c18e2714f4e2a1ac67b25213d3bb49763b3b94e7ebf72507b71fb2fe0329666477ee7cb7ebd6b88ad5add8b217188b1ca0fa13de1ec09cc674346875105be6e0e0d6c8928eb0df23c39a639e04e4aedf535c4e093f08b2c905a14f25c0c0fe47a5a1535ab9eae0d9d67bdd79de13a08d59ee05385c7ea4af1ad3248e61dd22f8990e9e99897d653dd7b1b1433a6d464ea9f74e377f2d8ce99ba7dbc753297644234d25ecb5bd528e2e2082824681299ac30c05354baaa9c3967d86d7c07736f87fc0f63e5036d47235d7ae12178ced3ae36ee5919c093a02579e4fc9edad2c446c656c790704bfc8e2c491a42500aa1d75c8d4921ce29b753f883e17c79b09ea324f1f32ddf1f3284cd70e847b09d90f6718c42e5c94484cc9cbb0df659d255630a3f5a27e7d5dd14fa6b974d1719aa98f01a20fb4b7b1c77b42d57fab3c724339d459ee4a1c6b5d3bd4e08624c786a257872acc9ad3ff62222f2265a658d9e2a007228a5293b67ec91c84c4b4407c228434bad8a815ca9b256c776bd2c9f92e2cf87a89ed786194104eba6b7973491f04d0b924c10e6fcff336d037b6e53fe763919da946a640960978994e8d0e5a2d555c9a897ce38a324c766fd01e9416acd91f1ea345c12aa1cfa5933c5c1230c5e45efb8c7e8d75bd9dd85ce8228cf80a52c915282375663690d1286ba0e70201af791a25715819dfd1035feb5239e3df7c230956cb3be858395094d3d99cbc2352cb8adc7a8fe8f4755a2f93bef3926f57bbcff17956c4031a2ae8c88d57dd9235b49a0253e86a7f173d96907aa2e162c82f1626dd5c07188e5e01d79724a546bed2b89a2084230b770ff97b2271158569ed7d00f967cdc51d216fa1578a9624f9142d8de1039b5d4f51de09324c91582f830c7730934feffbc7c51d5d87e8760a77e0712d947190ea6f896a4685045a3de3b8187490ee65f68a9c40cf708e03ab5f28a3b7e5e4a164c3cdb3a7a393b120a2306671a3e310419f873e1d978ff08535353a85eb1773e7476f1f102e3f2364036427a633d32cbf1c34ee0a223f696e69d9e296ac4981d64c99e9966d93eef673163ac774b2545e36e64816030b4ebb7775afeb77f88396c565d58bf2f2d07601dfc5338e5a5a71853dac2e42fc2d89a2da7bf913a8d5c1705ef3c869dc6a7d3a6a8ec7cde4e99380c0223b8d766506574f45cafbe96b25ebefd066945bffc1d2262d1e7d1057660ec2916055a493f2930afe8133de9e593b470e2d512ab5bedb363a6c9ac59fd82379f528d02bae1ade38fafe2d7ac7b097bb6fa4e00e43b087d3480f41402a5156379052526827da75486cf8703ed9ba34f38a9b445da35c3e5bd799156f25081e88051d54fe47fd0f7bca364d7cdeb01cb28aadc9b03fdd91712036f1da31a554e230a897e7142c0043c98e1d9a4bd996d26dfbce431bce8a6e29048783a7142c84483895c44df5d65c3bf8ca1bd6069ad305e525df7a4c584299525549bd3d013dcbfaebf18dcd82a9d29618a9b3e564b19cfdeac6eb7a6c4ad42268747fbd162c2f300aaad722d59dde7db179d93468b435724d97078df797d75d1728e75d0687e661fb603fa1b264466b8cedb75482ef042151f2ca045c795683a5e56dfff85a17f82cc5aba7e187784f159a996c3200b8b3f2a91de25d378557c5b33f2a17ec877b15cccc5615836899c30912cb83390e24902cbecb57cb95c0738d798debc43a07b09060347b145d5ccc150fcc46bc0a21f372622f25acae867946346e54498737be61c312e93086748b37633cbd8ab62433e6375914f117b9c1cb802513f" + }, + { + "sharedSecret": "21e13c2d7cfe7e18836df50872466117a295783ab8aab0e7ecc8c725503ad02d", + "encryptedMessage": "89f5945b1ed1f4bbe9a33a706089c10f7c0dc4c0995fff7f4b5d9db50e04aca120031a33e1148091d8d3a3138f25397a6048d7f022a71f751e44a72d9b3f79809c8c51c9f0843daa8fe83587844fedeacb7348362003b31922cbb4d6169b2087b6f8d192d9cfe5363254cd1fde24641bde9e422f170c3eb146f194c48a459ae2889d706dc654235fa9dd20307ea54091d09970bf956c067a3bcc05af03c41e01af949a131533778bf6ee3b546caf2eabe9d53d0fb2e8cc952b7e0f5326a69ed2e58e088729a1d85971c6b2e129a5643f3ac43da031e655b27081f10543262cf9d72d6f64d5d96387ac0d43da3e3a03da0c309af121dcf3e99192efa754eab6960c256ffd4c546208e292e0ab9894e3605db098dc16b40f17c320aa4a0e42fc8b105c22f08c9bc6537182c24e32062c6cd6d7ec7062a0c2c2ecdae1588c82185cdc615a346e11eaf8f32cd44d5f1213d4738768f081978420697b454700ade1c093c02a6ca0e78a7e2f3d9e5c7e49e20c3a56b624bfea51196ec9e88e4e56be38ff56031369f45f1e03be826d44a182f270c153ee0d9f8cf9f1f4132f33974e37c7887d5b857365c873cb218cbf20d4be3abdb2a2011b14add0a5672e01e5845421cf6dd6faca1f2f443757aae575c53ab797c2227ecdab03882bbbf4599318cefafa72fa0c9a0f5a51d13c9d0e5d25bfcfb0154ed25895260a9df8743ac188714a3f16960e6e2ff663c08bffda41743d50960ea2f28cda0bc3bd4a180e297b5b41c700b674cb31d99c7f2a1445e121e772984abff2bbe3f42d757ceeda3d03fb1ffe710aecabda21d738b1f4620e757e57b123dbc3c4aa5d9617dfa72f4a12d788ca596af14bea583f502f16fdc13a5e739afb0715424af2767049f6b9aa107f69c5da0e85f6d8c5e46507e14616d5d0b797c3dea8b74a1b12d4e47ba7f57f09d515f6c7314543f78b5e85329d50c5f96ee2f55bbe0df742b4003b24ccbd4598a64413ee4807dc7f2a9c0b92424e4ae1b418a3cdf02ea4da5c3b12139348aa7022cc8272a3a1714ee3e4ae111cffd1bdfd62c503c80bdf27b2feaea0d5ab8fe00f9cec66e570b00fd24b4a2ed9a5f6384f148a4d6325110a41ca5659ebc5b98721d298a52819b6fb150f273383f1c5754d320be428941922da790e17f482989c365c078f7f3ae100965e1b38c052041165295157e1a7c5b7a57671b842d4d85a7d971323ad1f45e17a16c4656d889fc75c12fc3d8033f598306196e29571e414281c5da19c12605f48347ad5b4648e371757cbe1c40adb93052af1d6110cfbf611af5c8fc682b7e2ade3bfca8b5c7717d19fc9f97964ba6025aebbc91a6671e259949dcf40984342118de1f6b514a7786bd4f6598ffbe1604cef476b2a4cb1343db608aca09d1d38fc23e98ee9c65e7f6023a8d1e61fd4f34f753454bd8e858c8ad6be649cc7c5ebe91be307bcd3ef972eac04ee1411897667db31217e01aad868554ab56be9a21a0827e29cd8829428ddbe7bb86f23d0a46aa6d54aa36e9b61498e690229236bd5e7b25afbc5a31661cd9713b904e57c17187a147f2eafedd595db46b26e5a9f6393b82a55e81a93d72f4b050fbedcd3a527992e8b4f2f5a1ca36aff1037e645937f2633c27a3cd0902342527df7617d332efb2199718fc5646aaba27826267e79705f766e8a0d6f2249dad7f5814957c48d1ef27f94317ad4e523289315b34b4f47579814e6513bc55ea6535fc6963388dbcbe6caa8d708f1f800cdf4c6060bfb69e001d8664a02fb09698b4f1b8fc4a2787ef9080d22b0b4f19dafce139769bcf532ad1e21d5cb7a7e65061444030ca30726a69f09756c8e2e6a37feea37af317ff77f6303922472ca6b261de9ae33626c9880df0b48667dbba7b04b7e7836519dab7358567c7324d106c831657528d86ec2edfd845faae8ea94223254fbefb738db202e0eca9001b3313127d1a33f470c4b29b05280b3b2f7b4bfc9f33497ffef59e78a120513e63d743d60c1ff176f40283de63ce4d37d9cc99af46ed15dd58e61dc1669d8bfa5b0eda8feda7dc511b1d07c26452dacefb5ad5377669785ade7d3a8c53dedfacbfa4461416c7cb4a5dc9140194088d2672f4c0f7caa214d2d1705e4d73bc3632639b5ef15b49bd7c50de3f2ab66608c25d212711a23c6a0ef9f22b224fa1a2059d8174aad941094cfd05cc6cd5f634ec5d0f4cec8454d1ee4817c48b36d2089d8c5c0e970e9839ae33b10bd0969e8af37e38f4361de930546d07f3e3745dac990fe470fda1d3b1041d3eaad6d19ef6e70c1585ef9f3d14280a4d3730104dfa49d3fe40e51e7c49eef6db35011c7e7f4daca2f0f36fc87742306208f10e6b85415f53f3a2316a2f124136aa6b6a9a2a5a43bc755f481344d3067bd28810adfe60cba3dec1e11cb66c4648749b42108cab1dc85fdb5bf28d4c7c95e273cd8203a35831b35ce40f4153778f0ddd200ea2652032840a3ddb1e46a2ab383d18ef9b9db426d016c6bfb9a05068f04f2a54be9405cac27c2e601568bdbaafeb5353363b2ab07168758a94ae6bc903e57e0cd41a2c753d2ccc5b6988a77637efc22ed648e2fb6eeac5504dfc05553f3f611f4f56248f170eabf4ddb19b4d006cbf6358f384914f1bd586c48b63aea04e35bea4fa5e56d8ed6780f30aaee19171f804532d654663c66ff4593ce7b495525c66a10bb7935f068a0154ae2a13704ab80176d1283d3e92bbf663ee100425ac01c73137e36ab8d8c845a725c3ecabc6973d35dfd" + }, + { + "sharedSecret": "3a6b412548762f0dbccce5c7ae7bb8147d1caf9b5471c34120b30bc9c04891cc", + "encryptedMessage": "e8bc60d044af7b8686febd7b1ae04f2f30fb809233ebb730ba837bc4f06751ea7f1862ddc1535f37c6eef4789bbed4d5e34d5875d2cd2e07bd669dfb5f4c54162fa504138dabd6ebcf0db8017840c35f12a2cfb84f89cc7c8959a6d51815b1d2c5136cedec2e4106bb5f2af9a21bd0a02c40b44ded6e6a90a145850614fb1b0eef2a03389f3f2693bc8a755630fc81fff1d87a147052863a71ad5aebe8770537f333e07d841761ec448257f948540d8f26b1d5b66f86e073746106dfdbb86ac9475acf59d95ece037fba360670d924dce53aaa74262711e62a8fc9eb70cd8618fbedae22853d3053c7f10b1a6f75369d7f73c419baa7dbf9f1fc5895362dcc8b6bd60cca4943ef7143956c91992119bccbe1666a20b7de8a2ff30a46112b53a6bb79b763903ecbd1f1f74952fb1d8eb0950c504df31fe702679c23b463f82a921a2c1155802b8866064f7bad07a50da5cf31f8c3151c4c52e525fb22ecf48f8fa39bb5adf932b50c12c10be90174b37d454a3f8b284c849e86578a6182c4a7b2e47dd57d44730a1be9fec4ad07287a397e28dce4fda57e9cdfdb2eb5afdf0d38ef19d982341d18d07a556bb16c1416f480a396f278373b8fd9897023a4ac506e65cf4c306377730f9c8ca63cf47565240b59c4861e52f1dab84d938e96fb31820064d534aca05fd3d2600834fe4caea98f2a748eb8f200af77bd9fbf46141952b9ddda66ef0ebea17ea1e7bb5bce65b6e71554c56dd0d4e14f4cf74c77a150776bf31e7419756c71e7421dc22efe9cf01de9e19fc8808d5b525431b944400db121a77994518d6025711cb25a18774068bba7faaa16d8f65c91bec8768848333156dcb4a08dfbbd9fef392da3e4de13d4d74e83a7d6e46cfe530ee7a6f711e2caf8ad5461ba8177b2ef0a518baf9058ff9156e6aa7b08d938bd8d1485a787809d7b4c8aed97be880708470cd2b2cdf8e2f13428cc4b04ef1f2acbc9562f3693b948d0aa94b0e6113cafa684f8e4a67dc431dfb835726874bef1de36f273f52ee694ec46b0700f77f8538067642a552968e866a72a3f2031ad116663ac17b172b446c5bc705b84777363a9a3fdc6443c07b2f4ef58858122168d4ebbaee920cefc312e1cea870ed6e15eec046ab2073bbf08b0a3366f55cfc6ad4681a12ab0946534e7b6f90ea8992d530ec3daa6b523b3cf03101c60cadd914f30dec932c1ef4341b5a8efac3c921e203574cfe0f1f83433fddb8ccfd273f7c3cab7bc27efe3bb61fdccd5146f1185364b9b621e7fb2b74b51f5ee6be72ab6ff46a6359dc2c855e61469724c1dbeb273df9d2e1c1fb74891239c0019dc12d5c7535f7238f963b761d7102b585372cf021b64c4fc85bfb3161e59d2e298bba44cfd34d6859d9dba9dc6271e5047d525468c814f2ae438474b0a977273036da1a2292f88fcfb89574a6bdca11bb685616be825941bd8b2cac7d37d1a28e5a4531601bfa9146c590e2bc0ce1fdabd288184b1dea153ad23f85402dc956e0b25f025dd2456f74b0d44b0d8e2d244fcaa04c65f1d027519158a8e5a83b00988d8c8bded5a8478326738e2b9ab467f27c42fecec7f6e48f5ee435266220b6c6ebfd9eb7a50b96d665f018adb86427a46208f39e63ab71876131d50bdb524367f5d538cfdba78a94cd1ca4e6381749f8823a100ea020169f16596f3d8cfff49dbda803c3afb89c7fcfec9dbe979b89882a347fa8e441b1d3bea00654e247912ccc4b85946c38f6a7c32d6b102c596dfa9bd78a24603d8ae93a713c342660036249d60fd0c3874c2e9545b3d099afaffe9dfced8e9123f4720de054e6d91329abcd91b07907af03673652823f9988039f87ebfa2ff156831c8881d16776771ec81b20ee37918b5fe81bef9e6710d4430c8e924ca15e8b5d061982e369923f819b1313ad94b9bc0e846df659367079f0113bb43f7c99dc5d5ae8958c605d7bafaed7c04a078c16d1cc8bd622a92676269452f06dcdc3a0891a091ed9403606d482d4cd7e99b067bb66ff3cf428a2fc4469bafc76ae172402338e3a59a3a607361293192385c141e31caa6a49615115ffaca858b400385be7e5472e35afb2a5b4b761b8c69767727516f974734f5d00b917deb2ec7a5ca05fb3683fc0faf155bec35854359707da65fa13df32ba237ef0ae21da9f60e68f157a196b1155e5fb33f43789ba9f43d2f6852c63c147f26a51dc5fc767326e2b56464efe376afb90c4a90d071d288051fd8af10ffb52421bbbdbe37f3d83dd6408f33241f54b135460ac285df9b27cd6ab2c109996f3fb4eb6244f9daa7920bb492c0958caa0f4239205e50c7297b575dbb9ce8f3154a5a5a9afa7eb80dd4b9de6143603b135fe4e0471a49fb8b364e0f828a32f57a21dec19a41d999e2f9b8c262437016d9d5cd62517a1bed4a8fb1d9d71e6058b2b22386e4cb5699ad04ab358a49359376cb9e360645758fcd28031705b48f393ca7d4cead494939e9a6956eaa5af0666897f0ce525dc6fff80b1c2b972ccc5d9a0b4bd710396c416991d6bbc545b2e0348c249ed2c3f3032e350252e05c974ee73d72341b466e40d0e89e132f17759ab9d0764e1c1e2f3464aa22cb928a9adc4216dd31a39e59ddc9d8b775d946bde0f651b2d5a32e6da41ffddbe7520a75329a5ac1c2e75f2a73d7b67ca41923b6517b7101b46304604607348ae458ca70de9f4fda848f9076145a0a6d8f26a1c6504a018c3f79f9d3a74c37a7ac5785acd87bf7ac39c148135f8d956b20eb396abe" + }, + { + "sharedSecret": "a6519e98832a0b179f62123b3567c106db99ee37bef036e783263602f3488fae", + "encryptedMessage": "e7aca8bf4139f65f977604e3f4a593f2cb031f7b9a711107e6c19691d0c5060a375d0d39d6920d2e08d72d59cee7827bccbacd62aeaee61e9e59a630c4c77cf383cb37b07413aa4de2f2fbf5b40ae40a91a8f4c6d74aeacef1bb1be4ecbc26ec2c824d2bc45db4b9098e732a769788f1cff3f5b41b0d25c132d40dc5ad045ef0043b15332ca3c5a09de2cdb17455a0f82a8f20da08346282823dab062cdbd2111e238528141d69de13de6d83994fbc711e3e269df63a12d3a4177c5c149150eb4dc2f589cd8acabcddba14dec3b0dada12d663b36176cd3c257c5460bab93981ad99f58660efa9b31d7e63b39915329695b3fa60e0a3bdb93e7e29a54ca6a8f360d3848866198f9c3da3ba958e7730847fe1e6478ce8597848d3412b4ae48b06e05ba9a104e648f6eaf183226b5f63ed2e68f77f7e38711b393766a6fab7921b03eb2a6bddfc370eb45c1699c856969e2d574fdd155945ed727fdf2aec4f056a4d49fdefc3abafe41c365a5bd14fd486d6b5e2f24199319e7813e02e798877ffe31a70ae2398d9e31b9e3727e6c1a3c0d995c67d37bb6e72e9660aaaa9232670f382add2edd468927e3303b6142672546997fe105583e7c5a3c4c2b599731308b5416e6c9a3f3ba55b181ad0439d3535356108b059f2cb8742eed7a58d4eba9fe79eaa77c34b12aff1abdaea93197aabd0e74cb271269ca464b3b06aef1d6573df5e1224179616036b368677f26479376681b772d3760e871d99efd34cca5cd6beca95190d967da820b21e5bec60082ea46d776b0517488c84f26d12873912d1f68fafd67bcf4c298e43cfa754959780682a2db0f75f95f0598c0d04fd014c50e4beb86a9e37d95f2bba7e5065ae052dc306555bca203d104c44a538b438c9762de299e1c4ad30d5b4a6460a76484661fc907682af202cd69b9a4473813b2fdc1142f1403a49b7e69a650b7cde9ff133997dcc6d43f049ecac5fce097a21e2bce49c810346426585e3a5a18569b4cddd5ff6bdec66d0b69fcbc5ab3b137b34cc8aefb8b850a764df0e685c81c326611d901c392a519866e132bbb73234f6a358ba284fbafb21aa3605cacbaf9d0c901390a98b7a7dac9d4f0b405f7291c88b2ff45874241c90ac6c5fc895a440453c344d3a365cb929f9c91b9e39cb98b142444aae03a6ae8284c77eb04b0a163813d4c21883df3c0f398f47bf127b5525f222107a2d8fe55289f0cfd3f4bbad6c5387b0594ef8a966afc9e804ccaf75fe39f35c6446f7ee076d433f2f8a44dba1515acc78e589fa8c71b0a006fe14feebd51d0e0aa4e51110d16759eee86192eee90b34432130f387e0ccd2ee71023f1f641cddb571c690107e08f592039fe36d81336a421e89378f351e633932a2f5f697d25b620ffb8e84bb6478e9bd229bf3b164b48d754ae97bd23f319e3c56b3bcdaaeb3bd7fc0369b609e1d6b7f9a98d6f139d335d34022afc76f3c22354b0b43dd865b1c211cf8b1850ac389a1ba507ecc961434b3b61e906895d17f34580cf3ceefd4d77c06c1560aea2b20798f3217c3fab544800b1b19538971f4650217c566f14bb6845e210a0339bed7b93a9d53ae8a6bdca8ede02544601bb6da225d0b138e4217c33c1be2b964a265a8065b893dbcc8fcf74a861311136c5f236b7c3cd2d28e5175789bcf6cbc9974b6ce6285fd958cb7f6f5e5d879a76b4fd6619c34d643120383791a2ce6bfc611885319a13f47c7ba2e8c1ac4c50da3714b5a3c63b0afaa73cbbebc3724383da6c044ff5480fc3ec4d8b43b45dbe206de176e443fcdd383fbacdf3e4256a13068b9750dd1ff605a7f340edd9dbad0f213d828d5ad10bc99189b8b3b3bfeea855c02050e51f66c402b588b9b7c354875b259e128702f6760cd6f77e9dfed0db82075aadb3ccbd5e7024d819e11f1fe57fee830d3b732a9d409d3ee39824857843b41c9d17b6e78bdd014209a2e9006a2823940bd7750ee94a1011e55428933ed90a588feefdb1bbb684ad5538eb4126488812dda96d8b68de37302458d2c06121877a33d2a0decf879407cc53f6a02b5b111b2a4504ac10a8c770e039a68cf50c5ba146dea36bbc8cef7cdcce348fbc607dca64214d9045adbc8987a8ab76666c6f4e219b2e53a53d46740153b25e3e872037ff0322f03159ce4309511d97b8b9d5ba43c8d49ef7962ceacbf04d5c704deb341917ca18fbf3ff6025ffb3b2b400a3ca6fa32a3672d29c3530eb68a5425f022c9958993f92ebcee24a64046ff1dde5ae330016eef4536e2e0a13aa2e13ff3c93fa241c5bcba94bd9493fe8b453b24497a41c832f2899b52b5eaba962c8456fac88b4d6857b2f6d45330d2447142c447038b46177ee57693b9387f5b2ac7a491df3e03094b36b6ccdf668abe836571850476540ac313269bdbed5417f0f6f56f1a35e0a37a28c85f6268b6a89e29ddbd45593f6d69e9ea550c41fc9d265a2a552e1423cc1005487ba118d1164871a16ebe3933b05fb3a3c8614adbf0a4513eaf5720b81794ecce8d9048c8856379760c7e382f8fb0ad7978070087fa1f8c064fa671269bb64c4b1fc9f860b84755a6fda1ef798c1033a1e7a4719c78201acadb2165bc97026f66a21b556d4d09b1e8b0e29e050f6f034a9e649a4a2d1d34f42263b24c4e257909ab62d259188aab42f77f1018490d1de38ef9af5b973335537b09e3816b117a8bb187cb7bd30a1779aec88cf70821a6a0ee4d76e220dbdbacb37e777dd863eccbfc86c04650916c0adbc40d3b7f80" + }, + { + "sharedSecret": "53eb63ea8a3fec3b3cd433b85cd62a4b145e1dda09391b348c4e1cd36a03ea66", + "encryptedMessage": "d135558a6bad7c5a617272dfff6fba9a1ee9d5509b697129b42407b020959566ba84011d09efc5e187c148ba548bf7922e7a5d6c0ceb2ff3a559ae244acd9d783c65977765c5d4e00b723d00f12475aafaafff7b31c1be5a589e6e25f8da2959107206dd42bbcb43438129ce6cce2b6b4ae63edc76b876136ca5ea6cd1c6a04ca86eca143d15e53ccdc9e23953e49dc2f87bb11e5238cd6536e57387225b8fff3bf5f3e686fd08458ffe0211b87d64770db9353500af9b122828a006da754cf979738b4374e146ea79dd93656170b89c98c5f2299d6e9c0410c826c721950c780486cd6d5b7130380d7eaff994a8503a8fef3270ce94889fe996da66ed121741987010f785494415ca991b2e8b39ef2df6bde98efd2aec7d251b2772485194c8368451ad49c2354f9d30d95367bde316fec6cbdddc7dc0d25e99d3075e13d3de0822e4d8e15a7c521d67ce2cc836c49118f205c99f18570504504221e337a29e2716fb28671b2bb91e38ef5e18aaf32c6c02f2fb690358872a1ed28166172631a82c2568d23238017188ebbd48944a147f6cdb3690d5f88e51371cb70adf1fa02afe4ed8b581afc8bcc5104922843a55d52acde09bc9d2b71a663e178788280f3c3eae127d21b0b95777976b3eb17be40a702c244d0e5f833ff49dae6403ff44b131e66df8b88e33ab0a58e379f2c34bf5113c66b9ea8241fc7aa2b1fa53cf4ed3cdd91d407730c66fb039ef3a36d4050dde37d34e80bcfe02a48a6b14ae28227b1627b5ad07608a7763a531f2ffc96dff850e8c583461831b19feffc783bc1beab6301f647e9617d14c92c4b1d63f5147ccda56a35df8ca4806b8884c4aa3c3cc6a174fdc2232404822569c01aba686c1df5eecc059ba97e9688c8b16b70f0d24eacfdba15db1c71f72af1b2af85bd168f0b0800483f115eeccd9b02adf03bdd4a88eab03e43ce342877af2b61f9d3d85497cd1c6b96674f3d4f07f635bb26add1e36835e321d70263b1c04234e222124dad30ffb9f2a138e3ef453442df1af7e566890aedee568093aa922dd62db188aa8361c55503f8e2c2e6ba93de744b55c15260f15ec8e69bb01048ca1fa7bbbd26975bde80930a5b95054688a0ea73af0353cc84b997626a987cc06a517e18f91e02908829d4f4efc011b9867bd9bfe04c5f94e4b9261d30cc39982eb7b250f12aee2a4cce0484ff34eebba89bc6e35bd48d3968e4ca2d77527212017e202141900152f2fd8af0ac3aa456aae13276a13b9b9492a9a636e18244654b3245f07b20eb76b8e1cea8c55e5427f08a63a16b0a633af67c8e48ef8e53519041c9138176eb14b8782c6c2ee76146b8490b97978ee73cd0104e12f483be5a4af414404618e9f6633c55dda6f22252cb793d3d16fae4f0e1431434e7acc8fa2c009d4f6e345ade172313d558a4e61b4377e31b8ed4e28f7e3d387988960f41b82bdda83a4e39828a8644af0377cb3f1cfa6bb26cfa878a5d4599ad2cbd72475931259903165cccb17679e3d0aaa2577f798de15b227f4b0dbf92b8ef891d17c814ab2e4e599b01f4b66c830b7dbbcfb3ab33a9e0af7398c4076f2256986710f7ca41cd8b477579a2e45c32830a3498baf7607b2ead37b75242a7c58dfe486b9b959ff639d04c2b0398fd00a8f8ee76f531b72fd5a3f09ed79e0de573c228f32488179c302be2677576354e22079eeb0a3ffaef65c875078c714d16ed36cf10892352b76d14764f51f0ffbb62f98faf263f41e84ec1ab0d4f723d279879f655be2c5caef768ed6be313a751f0f2af9e3517107f23012c54f36bdcc96f0f1c721bcb8a927cf2ed82e886d0a83975d00e9a0ff849cb9b44d8c030c293c9a02a47df169f4684a84b03c843ac2955bd7566909bb93ef5ae14b3be4fb481358b0e0894a8b4ff6599e4fac7bfb48841aa660dd1b70193e492221ec5d4a28f4dad0818ed1d72ef08da12eb7583f29276586a3e69f10bbe13ba4afca584e86ce2ce599cc7df8b98318a8d448fb3c75ad94671f9d80bc7a1660dc716422176fb19fa1e959d204116d538d2b0ca478b2c9f0ade86cf515d54e95676b0b17a508c13f97deb70bdbf4bc45f4ead427951d7455f892ea20d6a9bac83446a2217607747287b2a990bc8a7a823884d94fbcca38aceb666ca2afc47e8b9d8a3eeea093c92876b83d4e41e032d30365eea2cd717dc70f79cc4c9b7695e81769d2a0c30ca02b98f834bfb968948f150670d1cec91500c27301fc0d01faeacaa96fe0800b177bae2b57eee7909000934b597eb8517ddb11136f5235ce8d7204c22a6784fe8c187121457715b7383ee699a42df1d58a413725c86730d704538af546e7d1a70a15a968f5e7aa9afedcaf8a8e4909d7af6840d9018f8c3ba42f697eca710d31344b0ec20c8e9020e271e67c3822021afdbbbe2b77ef8e2e5d1b6a47865bf5cf925758df8e3a68e0be4cd0ece8c1bf26bf41686a5632bf9f9c720999b62d7d0a6d69bc3afe3d3f202a51ab5e077f76f4f77b9533de8151fbc431e453ad6c1bd6dd65a5c034d9cedcbaef57f4bbeff247ce4d0d5bfba92442bbd34e823edc351080732df64abdac453d982d8cb305dd583b7ffb7296d9d6076c411c3a36f76f5965a78d01795000fa1311cf08a662c74a9aa5369fadd6c8cf29d3de62303b51473b403ffe12a7164b6dc756b3d57377d0528a793b4c478c15a1cb68fc13318182095cc1a756db47ff3e141e59d783a2a87a0c3822f6644cb96813645f62ea76878be6d2ae36ef14f80e" + } + ] +} \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/balance/CheckBalanceSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/balance/CheckBalanceSpec.scala index 54a8c05e1e..df41d013f3 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/balance/CheckBalanceSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/balance/CheckBalanceSpec.scala @@ -170,7 +170,7 @@ class CheckBalanceSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val (ra2, htlca2) = addHtlc(100000000 msat, alice, bob, alice2bob, bob2alice) val (_, htlca3) = addHtlc(10000 msat, alice, bob, alice2bob, bob2alice) // for this one we set a non-local upstream to simulate a relayed payment - val (_, htlca4) = addHtlc(30000000 msat, CltvExpiryDelta(144), alice, bob, alice2bob, bob2alice, upstream = Upstream.Trampoline(Upstream.ReceivedHtlc(UpdateAddHtlc(randomBytes32(), 42, 30003000 msat, randomBytes32(), CltvExpiry(144), TestConstants.emptyOnionPacket, TlvStream.empty[UpdateAddHtlcTlv]), TimestampMilli(1687345927000L)) :: Nil), replyTo = TestProbe().ref) + val (_, htlca4) = addHtlc(30000000 msat, CltvExpiryDelta(144), alice, bob, alice2bob, bob2alice, upstream = Upstream.Trampoline(Upstream.ReceivedHtlc(UpdateAddHtlc(randomBytes32(), 42, 30003000 msat, randomBytes32(), CltvExpiry(144), TestConstants.emptyOnionPacket, TlvStream.empty[UpdateAddHtlcTlv]), TimestampMilli(1687345927000L), useAttributableErrors = false) :: Nil), replyTo = TestProbe().ref) val (rb1, htlcb1) = addHtlc(50000000 msat, bob, alice, bob2alice, alice2bob) val (_, _) = addHtlc(55000000 msat, bob, alice, bob2alice, alice2bob) crossSign(alice, bob, alice2bob, bob2alice) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelDataSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelDataSpec.scala index bc980eaded..e54b2195ce 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelDataSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelDataSpec.scala @@ -24,7 +24,7 @@ import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.states.ChannelStateTestsBase import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.wire.protocol.{CommitSig, RevokeAndAck, UnknownNextPeer, UpdateAddHtlc} -import fr.acinq.eclair.{MilliSatoshiLong, NodeParams, TestKitBaseClass} +import fr.acinq.eclair.{MilliSatoshiLong, NodeParams, TestKitBaseClass, TimestampMilli} import org.scalatest.funsuite.AnyFunSuiteLike import scodec.bits.ByteVector @@ -248,7 +248,7 @@ class ChannelDataSpec extends TestKitBaseClass with AnyFunSuiteLike with Channel // at this point the pending incoming htlc is waiting for a preimage assert(lcp4.htlcTxs(remainingHtlcOutpoint) == None) - alice ! CMD_FAIL_HTLC(1, Right(UnknownNextPeer()), replyTo_opt = Some(probe.ref)) + alice ! CMD_FAIL_HTLC(1, Right(UnknownNextPeer()), useAttributableErrors = false, TimestampMilli.now(), replyTo_opt = Some(probe.ref)) probe.expectMsgType[CommandSuccess[CMD_FAIL_HTLC]] val aliceClosing1 = alice.stateData.asInstanceOf[DATA_CLOSING] val lcp5 = aliceClosing1.localCommitPublished.get.copy(irrevocablySpent = lcp4.irrevocablySpent, claimHtlcDelayedTxs = lcp4.claimHtlcDelayedTxs) @@ -378,7 +378,7 @@ class ChannelDataSpec extends TestKitBaseClass with AnyFunSuiteLike with Channel } assert(!rcp3.isDone) - bob ! CMD_FAIL_HTLC(bobPendingHtlc.htlc.id, Right(UnknownNextPeer()), replyTo_opt = Some(probe.ref)) + bob ! CMD_FAIL_HTLC(bobPendingHtlc.htlc.id, Right(UnknownNextPeer()), useAttributableErrors = false, TimestampMilli.now(), replyTo_opt = Some(probe.ref)) probe.expectMsgType[CommandSuccess[CMD_FAIL_HTLC]] val bobClosing1 = bob.stateData.asInstanceOf[DATA_CLOSING] val rcp4 = bobClosing1.remoteCommitPublished.get.copy(irrevocablySpent = rcp3.irrevocablySpent) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/CommitmentsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/CommitmentsSpec.scala index fb0f5be56d..f8737a7c75 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/CommitmentsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/CommitmentsSpec.scala @@ -198,7 +198,7 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(bc4.availableBalanceForSend == b) assert(bc4.availableBalanceForReceive == a - p - htlcOutputFee) - val cmdFail = CMD_FAIL_HTLC(0, Right(IncorrectOrUnknownPaymentDetails(p, BlockHeight(42)))) + val cmdFail = CMD_FAIL_HTLC(0, Right(IncorrectOrUnknownPaymentDetails(p, BlockHeight(42))), useAttributableErrors = false, TimestampMilli.now()) val Right((bc5, fail: UpdateFailHtlc)) = bc4.sendFail(cmdFail, bob.underlyingActor.nodeParams.privateKey) assert(bc5.availableBalanceForSend == b) assert(bc5.availableBalanceForReceive == a - p - htlcOutputFee) // a's balance won't return to previous before she acknowledges the fail @@ -321,7 +321,7 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(bc8.availableBalanceForSend == b + p1 - p3) // as soon as we have the fulfill, the balance increases assert(bc8.availableBalanceForReceive == a - p1 - htlcOutputFee - p2 - htlcOutputFee - htlcOutputFee) - val cmdFail2 = CMD_FAIL_HTLC(1, Right(IncorrectOrUnknownPaymentDetails(p2, BlockHeight(42)))) + val cmdFail2 = CMD_FAIL_HTLC(1, Right(IncorrectOrUnknownPaymentDetails(p2, BlockHeight(42))), useAttributableErrors = false, TimestampMilli.now()) val Right((bc9, fail2: UpdateFailHtlc)) = bc8.sendFail(cmdFail2, bob.underlyingActor.nodeParams.privateKey) assert(bc9.availableBalanceForSend == b + p1 - p3) assert(bc9.availableBalanceForReceive == a - p1 - htlcOutputFee - p2 - htlcOutputFee - htlcOutputFee) // a's balance won't return to previous before she acknowledges the fail diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala index 142e4a2a45..e5d01a134d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala @@ -423,7 +423,7 @@ trait ChannelStateTestsBase extends Assertions with Eventually { } def failHtlc(id: Long, s: TestFSMRef[ChannelState, ChannelData, Channel], r: TestFSMRef[ChannelState, ChannelData, Channel], s2r: TestProbe, r2s: TestProbe): Unit = { - s ! CMD_FAIL_HTLC(id, Right(TemporaryNodeFailure())) + s ! CMD_FAIL_HTLC(id, Right(TemporaryNodeFailure()), useAttributableErrors = false, TimestampMilli.now()) val fail = s2r.expectMsgType[UpdateFailHtlc] s2r.forward(r) eventually(assert(r.stateData.asInstanceOf[ChannelDataWithCommitments].commitments.changes.remoteChanges.proposed.contains(fail))) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalQuiescentStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalQuiescentStateSpec.scala index 476c7c46c5..e089b9e742 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalQuiescentStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalQuiescentStateSpec.scala @@ -190,7 +190,7 @@ class NormalQuiescentStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteL val (preimage, add) = addHtlc(10_000 msat, bob, alice, bob2alice, alice2bob) val cmd = c match { case FulfillHtlc => CMD_FULFILL_HTLC(add.id, preimage) - case FailHtlc => CMD_FAIL_HTLC(add.id, Left(randomBytes32())) + case FailHtlc => CMD_FAIL_HTLC(add.id, Left(randomBytes32()), useAttributableErrors = false, TimestampMilli.now()) } crossSign(bob, alice, bob2alice, alice2bob) val sender = initiateQuiescence(f, sendInitialStfu) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala index 037782e285..3c528931e0 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala @@ -1498,7 +1498,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ val (_, htlc) = addHtlc(150000000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) - bob ! CMD_FAIL_HTLC(htlc.id, Right(PermanentChannelFailure())) + bob ! CMD_FAIL_HTLC(htlc.id, Right(PermanentChannelFailure()), useAttributableErrors = false, TimestampMilli.now()) val fail = bob2alice.expectMsgType[UpdateFailHtlc] bob2alice.forward(alice) bob ! CMD_SIGN() @@ -1782,7 +1782,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // actual test begins val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] - val cmd = CMD_FAIL_HTLC(htlc.id, Right(PermanentChannelFailure())) + val cmd = CMD_FAIL_HTLC(htlc.id, Right(PermanentChannelFailure()), useAttributableErrors = false, TimestampMilli.now()) val Right(fail) = OutgoingPaymentPacket.buildHtlcFailure(Bob.nodeParams.privateKey, cmd, htlc) assert(fail.id == htlc.id) bob ! cmd @@ -1812,7 +1812,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with crossSign(alice, bob, alice2bob, bob2alice) val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] - val cmd = CMD_FAIL_HTLC(htlc.id, Right(PermanentChannelFailure()), delay_opt = Some(50 millis)) + val cmd = CMD_FAIL_HTLC(htlc.id, Right(PermanentChannelFailure()), useAttributableErrors = false, TimestampMilli.now(), delay_opt = Some(50 millis)) val Right(fail) = OutgoingPaymentPacket.buildHtlcFailure(Bob.nodeParams.privateKey, cmd, htlc) assert(fail.id == htlc.id) bob ! cmd @@ -1825,7 +1825,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val sender = TestProbe() val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] - val c = CMD_FAIL_HTLC(42, Right(PermanentChannelFailure()), replyTo_opt = Some(sender.ref)) + val c = CMD_FAIL_HTLC(42, Right(PermanentChannelFailure()), useAttributableErrors = false, TimestampMilli.now(), replyTo_opt = Some(sender.ref)) bob ! c sender.expectMsg(RES_FAILURE(c, UnknownHtlcId(channelId(bob), 42))) assert(initialState == bob.stateData) @@ -1845,7 +1845,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob2alice.expectMsgType[CommitSig] // We cannot fail the HTLC, we must wait for the fulfill to be acked. - val c = CMD_FAIL_HTLC(htlc.id, Right(TemporaryNodeFailure()), replyTo_opt = Some(sender.ref)) + val c = CMD_FAIL_HTLC(htlc.id, Right(TemporaryNodeFailure()), useAttributableErrors = false, TimestampMilli.now(), replyTo_opt = Some(sender.ref)) bob ! c sender.expectMsg(RES_FAILURE(c, UnknownHtlcId(channelId(bob), htlc.id))) } @@ -1855,7 +1855,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val sender = TestProbe() val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] - val c = CMD_FAIL_HTLC(42, Right(PermanentChannelFailure()), replyTo_opt = Some(sender.ref)) + val c = CMD_FAIL_HTLC(42, Right(PermanentChannelFailure()), useAttributableErrors = false, TimestampMilli.now(), replyTo_opt = Some(sender.ref)) sender.send(bob, c) // this will fail sender.expectMsg(RES_FAILURE(c, UnknownHtlcId(channelId(bob), 42))) awaitCond(bob.underlyingActor.nodeParams.db.pendingCommands.listSettlementCommands(initialState.channelId).isEmpty) @@ -1909,7 +1909,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ val (_, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) - bob ! CMD_FAIL_HTLC(htlc.id, Right(PermanentChannelFailure())) + bob ! CMD_FAIL_HTLC(htlc.id, Right(PermanentChannelFailure()), useAttributableErrors = false, TimestampMilli.now()) val fail = bob2alice.expectMsgType[UpdateFailHtlc] // actual test begins @@ -2016,7 +2016,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val (_, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) // Bob receives a failure with a completely invalid onion error (missing mac) - bob ! CMD_FAIL_HTLC(htlc.id, Left(ByteVector.fill(561)(42))) + bob ! CMD_FAIL_HTLC(htlc.id, Left(ByteVector.fill(561)(42)), useAttributableErrors = false, TimestampMilli.now()) val fail = bob2alice.expectMsgType[UpdateFailHtlc] assert(fail.id == htlc.id) // We propagate failure upstream (hopefully the sender knows how to unwrap them). diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala index fbeebfdb32..1f4ebd1d8b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala @@ -33,7 +33,7 @@ import fr.acinq.eclair.channel.states.ChannelStateTestsBase import fr.acinq.eclair.channel.states.ChannelStateTestsBase.PimpTestFSM import fr.acinq.eclair.transactions.Transactions.{ClaimHtlcTimeoutTx, HtlcSuccessTx} import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, MilliSatoshiLong, TestConstants, TestKitBaseClass, TestUtils, randomBytes32} +import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, MilliSatoshiLong, TestConstants, TestKitBaseClass, TestUtils, TimestampMilli, randomBytes32} import org.scalatest.funsuite.FixtureAnyFunSuiteLike import org.scalatest.{Outcome, Tag} @@ -625,7 +625,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // We simulate a pending failure on that HTLC. // Even if we get close to expiring upstream we shouldn't close the channel, because we have nothing to lose. - bob ! CMD_FAIL_HTLC(htlc.id, Right(IncorrectOrUnknownPaymentDetails(0 msat, BlockHeight(0)))) + bob ! CMD_FAIL_HTLC(htlc.id, Right(IncorrectOrUnknownPaymentDetails(0 msat, BlockHeight(0))), useAttributableErrors = false, TimestampMilli.now()) bob ! CurrentBlockHeight(htlc.cltvExpiry.blockHeight - bob.underlyingActor.nodeParams.channelConf.fulfillSafetyBeforeTimeout.toInt) bob2blockchain.expectNoMessage(250 millis) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala index 73cf31d99d..b4462299ca 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala @@ -33,7 +33,7 @@ import fr.acinq.eclair.payment._ import fr.acinq.eclair.payment.relay.Relayer._ import fr.acinq.eclair.payment.send.SpontaneousRecipient import fr.acinq.eclair.wire.protocol.{AnnouncementSignatures, ClosingSigned, CommitSig, Error, FailureMessageCodecs, PermanentChannelFailure, RevokeAndAck, Shutdown, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFee, UpdateFulfillHtlc} -import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, MilliSatoshiLong, TestConstants, TestKitBaseClass, randomBytes32} +import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, MilliSatoshiLong, TestConstants, TestKitBaseClass, TimestampMilli, randomBytes32} import org.scalatest.funsuite.FixtureAnyFunSuiteLike import org.scalatest.{Outcome, Tag} import scodec.bits.ByteVector @@ -244,7 +244,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit test("recv CMD_FAIL_HTLC") { f => import f._ val initialState = bob.stateData.asInstanceOf[DATA_SHUTDOWN] - bob ! CMD_FAIL_HTLC(1, Right(PermanentChannelFailure())) + bob ! CMD_FAIL_HTLC(1, Right(PermanentChannelFailure()), useAttributableErrors = false, TimestampMilli.now()) val fail = bob2alice.expectMsgType[UpdateFailHtlc] awaitCond(bob.stateData == initialState .modify(_.commitments.changes.localChanges.proposed).using(_ :+ fail) @@ -255,7 +255,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit import f._ val sender = TestProbe() val initialState = bob.stateData.asInstanceOf[DATA_SHUTDOWN] - val c = CMD_FAIL_HTLC(42, Right(PermanentChannelFailure()), replyTo_opt = Some(sender.ref)) + val c = CMD_FAIL_HTLC(42, Right(PermanentChannelFailure()), useAttributableErrors = false, TimestampMilli.now(), replyTo_opt = Some(sender.ref)) bob ! c sender.expectMsg(RES_FAILURE(c, UnknownHtlcId(channelId(bob), 42))) assert(initialState == bob.stateData) @@ -265,7 +265,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit import f._ val sender = TestProbe() val initialState = bob.stateData.asInstanceOf[DATA_SHUTDOWN] - val c = CMD_FAIL_HTLC(42, Right(PermanentChannelFailure()), replyTo_opt = Some(sender.ref)) + val c = CMD_FAIL_HTLC(42, Right(PermanentChannelFailure()), useAttributableErrors = false, TimestampMilli.now(), replyTo_opt = Some(sender.ref)) sender.send(bob, c) // this will fail sender.expectMsg(RES_FAILURE(c, UnknownHtlcId(channelId(bob), 42))) awaitCond(bob.underlyingActor.nodeParams.db.pendingCommands.listSettlementCommands(initialState.channelId).isEmpty) @@ -504,7 +504,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit test("recv RevokeAndAck (forward UpdateFailHtlc)") { f => import f._ - bob ! CMD_FAIL_HTLC(1, Right(PermanentChannelFailure())) + bob ! CMD_FAIL_HTLC(1, Right(PermanentChannelFailure()), useAttributableErrors = false, TimestampMilli.now()) val fail = bob2alice.expectMsgType[UpdateFailHtlc] bob2alice.forward(alice) bob ! CMD_SIGN() diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala index 55251ea1ad..8e7a75e437 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala @@ -21,10 +21,16 @@ import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} import fr.acinq.eclair.crypto.Sphinx.RouteBlinding.{BlindedRoute, BlindedRouteDetails} import fr.acinq.eclair.wire.protocol import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, MilliSatoshiLong, ShortChannelId, UInt64, randomKey} +import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, MilliSatoshiLong, ShortChannelId, UInt64, randomBytes, randomKey} +import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, MilliSatoshiLong, ShortChannelId, UInt64, randomBytes, randomKey} +import org.json4s.JsonAST._ +import org.json4s.jackson.JsonMethods import org.scalatest.funsuite.AnyFunSuite import scodec.bits._ +import java.io.File +import scala.concurrent.duration.DurationInt +import scala.io.Source import scala.util.Success /** @@ -392,6 +398,79 @@ class SphinxSpec extends AnyFunSuite { assert(failure == InvalidRealm()) } + test("decrypt fat error") { + val sharedSecrets = Seq( + hex"0101010101010101010101010101010101010101010101010101010101010101", + hex"0202020202020202020202020202020202020202020202020202020202020202", + hex"0303030303030303030303030303030303030303030303030303030303030303", + hex"0404040404040404040404040404040404040404040404040404040404040404", + hex"0505050505050505050505050505050505050505050505050505050505050505", + ).map(ByteVector32(_)) + + val expected = DecryptedFailurePacket(publicKeys(2), InvalidOnionKey(ByteVector32.One)) + + val packet1 = AttributableErrorPacket.create(sharedSecrets(2), expected.failureMessage, 3 millis) + assert(packet1.length == 1200) + + val Right(decrypted1) = AttributableErrorPacket.decrypt(packet1, (2 to 4).map(i => (sharedSecrets(i), publicKeys(i)))) + assert(decrypted1 == expected) + + val Success(packet2) = AttributableErrorPacket.wrap(packet1, sharedSecrets(1), 5 millis, isSource = false) + assert(packet2.length == 1200) + + val Right(decrypted2) = AttributableErrorPacket.decrypt(packet2, (1 to 4).map(i => (sharedSecrets(i), publicKeys(i)))) + assert(decrypted2 == expected) + + val Success(packet3) = AttributableErrorPacket.wrap(packet2, sharedSecrets(0), 9 millis, isSource = false) + assert(packet3.length == 1200) + + val Right(decrypted3) = AttributableErrorPacket.decrypt(packet3, (0 to 4).map(i => (sharedSecrets(i), publicKeys(i)))) + assert(decrypted3 == expected) + } + + test("decrypt fat error with random data") { + val sharedSecrets = Seq( + hex"0101010101010101010101010101010101010101010101010101010101010101", + hex"0202020202020202020202020202020202020202020202020202020202020202", + hex"0303030303030303030303030303030303030303030303030303030303030303", + hex"0404040404040404040404040404040404040404040404040404040404040404", + hex"0505050505050505050505050505050505050505050505050505050505050505", + ).map(ByteVector32(_)) + + // publicKeys(2) creates an invalid random packet, or publicKeys(1) tries to shift blame by pretending to receive random data from publicKeys(2) + val packet1 = randomBytes(12599) + + val hopPayload2 = AttributableError.HopPayload(isPayloadSource = false, 50 millis) + val Success(packet2) = AttributableErrorPacket.wrap(packet1, sharedSecrets(1), 50 millis, isSource = false) + assert(packet2.length == 12599) + + val hopPayload3 = AttributableError.HopPayload(isPayloadSource = false, 100 millis) + val Success(packet3) = AttributableErrorPacket.wrap(packet2, sharedSecrets(0), 100 millis, isSource = false) + assert(packet3.length == 12599) + + val Left(decryptionError) = AttributableErrorPacket.decrypt(packet3, (0 to 4).map(i => (sharedSecrets(i), publicKeys(i)))) + val expected = InvalidAttributableErrorPacket(Seq((publicKeys(0), hopPayload3), (publicKeys(1), hopPayload2)), publicKeys(2)) + assert(decryptionError == expected) + } + + test("attributable error test vector") { + val src = Source.fromFile(new File(getClass.getResource(s"/attributable_error.json").getFile)) + try { + val testVector = JsonMethods.parse(src.mkString).asInstanceOf[JObject].values + val encodedFailureMessage = ByteVector.fromValidHex(testVector("encodedFailureMessage").asInstanceOf[String]) + val expected = FailureMessageCodecs.failureOnionPayload(0).decode(encodedFailureMessage.bits).require.value + val hops = testVector("hops").asInstanceOf[List[Map[String, String]]] + val sharedSecrets = hops.map(hop => ByteVector32(ByteVector.fromValidHex(hop("sharedSecret")))).reverse + val encryptedMessage = hops.map(hop => ByteVector.fromValidHex(hop("encryptedMessage"))).last + val nodeIds = (1 to 5).map(_ => randomKey().publicKey) + val Right(DecryptedFailurePacket(originNode, failureMessage)) = AttributableErrorPacket.decrypt(encryptedMessage, sharedSecrets.zip(nodeIds)) + assert(originNode == nodeIds.last) + assert(failureMessage == expected) + } finally { + src.close() + } + } + test("create blinded route (reference test vector)") { val alice = PrivateKey(hex"4141414141414141414141414141414141414141414141414141414141414141") val bob = PrivateKey(hex"4242424242424242424242424242424242424242424242424242424242424242") diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/PendingCommandsDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/PendingCommandsDbSpec.scala index 705252566f..214e987755 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/PendingCommandsDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/PendingCommandsDbSpec.scala @@ -22,7 +22,7 @@ import fr.acinq.eclair.channel.{CMD_FAIL_HTLC, CMD_FAIL_MALFORMED_HTLC, CMD_FULF import fr.acinq.eclair.db.pg.PgPendingCommandsDb import fr.acinq.eclair.db.sqlite.SqlitePendingCommandsDb import fr.acinq.eclair.db.sqlite.SqliteUtils.{setVersion, using} -import fr.acinq.eclair.randomBytes32 +import fr.acinq.eclair.{TimestampMilli, randomBytes32} import fr.acinq.eclair.wire.internal.CommandCodecs.cmdCodec import fr.acinq.eclair.wire.protocol.{FailureMessageCodecs, UnknownNextPeer} import org.scalatest.funsuite.AnyFunSuite @@ -53,8 +53,8 @@ class PendingCommandsDbSpec extends AnyFunSuite { val channelId2 = randomBytes32() val msg0 = CMD_FULFILL_HTLC(0, randomBytes32()) val msg1 = CMD_FULFILL_HTLC(1, randomBytes32()) - val msg2 = CMD_FAIL_HTLC(2, Left(randomBytes32())) - val msg3 = CMD_FAIL_HTLC(3, Left(randomBytes32())) + val msg2 = CMD_FAIL_HTLC(2, Left(randomBytes32()), useAttributableErrors = false, TimestampMilli.now()) + val msg3 = CMD_FAIL_HTLC(3, Left(randomBytes32()), useAttributableErrors = false, TimestampMilli.now()) val msg4 = CMD_FAIL_MALFORMED_HTLC(4, randomBytes32(), FailureMessageCodecs.BADONION) assert(db.listSettlementCommands(channelId1).toSet == Set.empty) @@ -135,7 +135,7 @@ object PendingCommandsDbSpec { val cmds = (0 until Random.nextInt(5)).map { _ => Random.nextInt(2) match { case 0 => CMD_FULFILL_HTLC(Random.nextLong(100_000), randomBytes32()) - case 1 => CMD_FAIL_HTLC(Random.nextLong(100_000), Right(UnknownNextPeer())) + case 1 => CMD_FAIL_HTLC(Random.nextLong(100_000), Right(UnknownNextPeer()), useAttributableErrors = false, TimestampMilli.now()) } } cmds.map(cmd => TestCase(channelId, cmd)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala index e767e2c5b5..7b4881cdad 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala @@ -23,7 +23,7 @@ import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, Crypto} import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} import fr.acinq.eclair.Features.{KeySend, _} import fr.acinq.eclair.TestConstants.Alice -import fr.acinq.eclair.channel.{CMD_FAIL_HTLC, CMD_FULFILL_HTLC, Register} +import fr.acinq.eclair.channel.{CMD_FAIL_HTLC, CMD_FULFILL_HTLC, HtlcSettlementCommand, Register} import fr.acinq.eclair.db.{IncomingBlindedPayment, IncomingPaymentStatus, IncomingStandardPayment, PaymentType} import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop import fr.acinq.eclair.payment.PaymentReceived.PartialPayment @@ -593,9 +593,9 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike } val commands = f.register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]] :: f.register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]] :: Nil - assert(commands.toSet == Set( - Register.Forward(null, ByteVector32.One, CMD_FAIL_HTLC(0, Right(PaymentTimeout()), commit = true)), - Register.Forward(null, ByteVector32.One, CMD_FAIL_HTLC(1, Right(PaymentTimeout()), commit = true)) + assert(commands.toSet[Register.Forward[CMD_FAIL_HTLC]].map(fwd => fwd.copy(message = fwd.message.copy(startHoldTime = TimestampMilli.min))) == Set( + Register.Forward(null, ByteVector32.One, CMD_FAIL_HTLC(0, Right(PaymentTimeout()), useAttributableErrors = false, TimestampMilli.min, commit = true)), + Register.Forward(null, ByteVector32.One, CMD_FAIL_HTLC(1, Right(PaymentTimeout()), useAttributableErrors = false, TimestampMilli.min, commit = true)) )) awaitCond({ f.sender.send(handler, GetPendingPayments) @@ -603,8 +603,9 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike }) // Extraneous HTLCs should be failed. - f.sender.send(handler, MultiPartPaymentFSM.ExtraPaymentReceived(pr1.paymentHash, HtlcPart(1000 msat, UpdateAddHtlc(ByteVector32.One, 42, 200 msat, pr1.paymentHash, add1.cltvExpiry, add1.onionRoutingPacket, None)), Some(PaymentTimeout()))) - f.register.expectMsg(Register.Forward(null, ByteVector32.One, CMD_FAIL_HTLC(42, Right(PaymentTimeout()), commit = true))) + f.sender.send(handler, MultiPartPaymentFSM.ExtraPaymentReceived(pr1.paymentHash, HtlcPart(1000 msat, UpdateAddHtlc(ByteVector32.One, 42, 200 msat, pr1.paymentHash, add1.cltvExpiry, add1.onionRoutingPacket, None), useAttributableErrors = false, TimestampMilli.now()), Some(PaymentTimeout()))) + val fwd = f.register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]] + assert(fwd.copy(message = fwd.message.copy(startHoldTime = TimestampMilli.min)) == Register.Forward(null, ByteVector32.One, CMD_FAIL_HTLC(42, Right(PaymentTimeout()), useAttributableErrors = false, TimestampMilli.min, commit = true))) // The payment should still be pending in DB. val Some(incomingPayment) = nodeParams.db.payments.getIncomingPayment(pr1.paymentHash) @@ -627,8 +628,9 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val add3 = add2.copy(id = 43) f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add3, FinalPayload.Standard.createPayload(add3.amountMsat, 1000 msat, add3.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) + val fwd = f.register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]] + assert(fwd.copy(message = fwd.message.copy(startHoldTime = TimestampMilli.min)) == Register.Forward(null, add2.channelId, CMD_FAIL_HTLC(add2.id, Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight)), useAttributableErrors = false, startHoldTime = TimestampMilli.min, commit = true))) f.register.expectMsgAllOf( - Register.Forward(null, add2.channelId, CMD_FAIL_HTLC(add2.id, Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight)), commit = true)), Register.Forward(null, add1.channelId, CMD_FULFILL_HTLC(add1.id, preimage, commit = true)), Register.Forward(null, add3.channelId, CMD_FULFILL_HTLC(add3.id, preimage, commit = true)) ) @@ -644,7 +646,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike }) // Extraneous HTLCs should be fulfilled. - f.sender.send(handler, MultiPartPaymentFSM.ExtraPaymentReceived(invoice.paymentHash, HtlcPart(1000 msat, UpdateAddHtlc(ByteVector32.One, 44, 200 msat, invoice.paymentHash, add1.cltvExpiry, add1.onionRoutingPacket, None)), None)) + f.sender.send(handler, MultiPartPaymentFSM.ExtraPaymentReceived(invoice.paymentHash, HtlcPart(1000 msat, UpdateAddHtlc(ByteVector32.One, 44, 200 msat, invoice.paymentHash, add1.cltvExpiry, add1.onionRoutingPacket, None), useAttributableErrors = false, TimestampMilli.now()), None)) f.register.expectMsg(Register.Forward(null, ByteVector32.One, CMD_FULFILL_HTLC(44, preimage, commit = true))) assert(f.eventListener.expectMsgType[PaymentReceived].amount == 200.msat) val received2 = nodeParams.db.payments.getIncomingPayment(invoice.paymentHash) @@ -691,7 +693,8 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val add1 = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, invoice.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket, None) f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add1, FinalPayload.Standard.createPayload(add1.amountMsat, 1000 msat, add1.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) - f.register.expectMsg(Register.Forward(null, ByteVector32.One, CMD_FAIL_HTLC(0, Right(PaymentTimeout()), commit = true))) + val fwd = f.register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]] + assert(fwd.copy(message = fwd.message.copy(startHoldTime = TimestampMilli.min)) == Register.Forward(null, ByteVector32.One, CMD_FAIL_HTLC(0, Right(PaymentTimeout()), useAttributableErrors = false, TimestampMilli.min, commit = true))) awaitCond({ f.sender.send(handler, GetPendingPayments) f.sender.expectMsgType[PendingPayments].paymentHashes.isEmpty @@ -777,7 +780,8 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val add = UpdateAddHtlc(ByteVector32.One, 0, amountMsat, paymentHash, defaultExpiry, TestConstants.emptyOnionPacket, None) sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, payload)) - f.register.expectMsg(Register.Forward(null, add.channelId, CMD_FAIL_HTLC(add.id, Right(IncorrectOrUnknownPaymentDetails(42000 msat, nodeParams.currentBlockHeight)), commit = true))) + val fwd = f.register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]] + assert(fwd.copy(message = fwd.message.copy(startHoldTime = TimestampMilli.min)) == Register.Forward(null, add.channelId, CMD_FAIL_HTLC(add.id, Right(IncorrectOrUnknownPaymentDetails(42000 msat, nodeParams.currentBlockHeight)), useAttributableErrors = false, TimestampMilli.min, commit = true))) assert(nodeParams.db.payments.getIncomingPayment(paymentHash).isEmpty) } @@ -819,7 +823,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val add = UpdateAddHtlc(ByteVector32.One, 0, 1000 msat, paymentHash, defaultExpiry, TestConstants.emptyOnionPacket, None) val invoice = Bolt11Invoice(Block.TestnetGenesisBlock.hash, None, paymentHash, randomKey(), Left("dummy"), CltvExpiryDelta(12)) val incomingPayment = IncomingStandardPayment(invoice, paymentPreimage, PaymentType.Standard, invoice.createdAt.toTimestampMilli, IncomingPaymentStatus.Pending) - val fulfill = DoFulfill(incomingPayment, MultiPartPaymentFSM.MultiPartPaymentSucceeded(paymentHash, Queue(HtlcPart(1000 msat, add)))) + val fulfill = DoFulfill(incomingPayment, MultiPartPaymentFSM.MultiPartPaymentSucceeded(paymentHash, Queue(HtlcPart(1000 msat, add, useAttributableErrors = false, TimestampMilli.now())))) sender.send(handlerWithoutMpp, fulfill) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.id == add.id) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentFSMSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentFSMSpec.scala index 2f8f3dfa22..ffb8bfe4cb 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentFSMSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentFSMSpec.scala @@ -23,7 +23,7 @@ import fr.acinq.eclair.payment.receive.MultiPartPaymentFSM import fr.acinq.eclair.payment.receive.MultiPartPaymentFSM._ import fr.acinq.eclair.wire.protocol import fr.acinq.eclair.wire.protocol.{IncorrectOrUnknownPaymentDetails, UpdateAddHtlc} -import fr.acinq.eclair.{BlockHeight, CltvExpiry, MilliSatoshi, MilliSatoshiLong, NodeParams, TestConstants, TestKitBaseClass, randomBytes32} +import fr.acinq.eclair.{BlockHeight, CltvExpiry, MilliSatoshi, MilliSatoshiLong, NodeParams, TestConstants, TestKitBaseClass, TimestampMilli, randomBytes32} import org.scalatest.funsuite.AnyFunSuiteLike import scodec.bits.ByteVector @@ -233,7 +233,7 @@ object MultiPartPaymentFSMSpec { def createMultiPartHtlc(totalAmount: MilliSatoshi, htlcAmount: MilliSatoshi, htlcId: Long): HtlcPart = { val htlc = UpdateAddHtlc(htlcIdToChannelId(htlcId), htlcId, htlcAmount, paymentHash, CltvExpiry(42), TestConstants.emptyOnionPacket, None) - HtlcPart(totalAmount, htlc) + HtlcPart(totalAmount, htlc, useAttributableErrors = false, TimestampMilli.now()) } } \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala index 128b360929..a580556261 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala @@ -315,7 +315,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { // c forwards the trampoline payment to e through d. val recipient_e = ClearRecipient(e, Features.empty, inner_c.amountToForward, inner_c.outgoingCltv, randomBytes32(), nextTrampolineOnion_opt = Some(trampolinePacket_e)) - val Right(payment_e) = buildOutgoingPayment(ActorRef.noSender, priv_c.privateKey, Upstream.Trampoline(Seq(Upstream.ReceivedHtlc(add_c, TimestampMilli(1687345927000L)))), paymentHash, Route(inner_c.amountToForward, afterTrampolineChannelHops, None), recipient_e) + val Right(payment_e) = buildOutgoingPayment(ActorRef.noSender, priv_c.privateKey, Upstream.Trampoline(Seq(Upstream.ReceivedHtlc(add_c, TimestampMilli(1687345927000L), useAttributableErrors = false))), paymentHash, Route(inner_c.amountToForward, afterTrampolineChannelHops, None), recipient_e) assert(payment_e.outgoingChannel == channelUpdate_cd.shortChannelId) assert(payment_e.cmd.amount == amount_cd) assert(payment_e.cmd.cltvExpiry == expiry_cd) @@ -366,7 +366,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { // c forwards the trampoline payment to e through d. val recipient_e = ClearRecipient(e, Features.empty, inner_c.amountToForward, inner_c.outgoingCltv, inner_c.paymentSecret.get, invoice.extraEdges, inner_c.paymentMetadata) - val Right(payment_e) = buildOutgoingPayment(ActorRef.noSender, priv_c.privateKey, Upstream.Trampoline(Seq(Upstream.ReceivedHtlc(add_c, TimestampMilli(1687345927000L)))), paymentHash, Route(inner_c.amountToForward, afterTrampolineChannelHops, None), recipient_e) + val Right(payment_e) = buildOutgoingPayment(ActorRef.noSender, priv_c.privateKey, Upstream.Trampoline(Seq(Upstream.ReceivedHtlc(add_c, TimestampMilli(1687345927000L), useAttributableErrors = false))), paymentHash, Route(inner_c.amountToForward, afterTrampolineChannelHops, None), recipient_e) assert(payment_e.outgoingChannel == channelUpdate_cd.shortChannelId) assert(payment_e.cmd.amount == amount_cd) assert(payment_e.cmd.cltvExpiry == expiry_cd) @@ -444,7 +444,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { // c forwards an invalid trampoline onion to e through d. val recipient_e = ClearRecipient(e, Features.empty, inner_c.amountToForward, inner_c.outgoingCltv, randomBytes32(), nextTrampolineOnion_opt = Some(trampolinePacket_e.copy(payload = trampolinePacket_e.payload.reverse))) - val Right(payment_e) = buildOutgoingPayment(ActorRef.noSender, priv_c.privateKey, Upstream.Trampoline(Seq(Upstream.ReceivedHtlc(add_c, TimestampMilli(1687345927000L)))), paymentHash, Route(inner_c.amountToForward, afterTrampolineChannelHops, None), recipient_e) + val Right(payment_e) = buildOutgoingPayment(ActorRef.noSender, priv_c.privateKey, Upstream.Trampoline(Seq(Upstream.ReceivedHtlc(add_c, TimestampMilli(1687345927000L), useAttributableErrors = false))), paymentHash, Route(inner_c.amountToForward, afterTrampolineChannelHops, None), recipient_e) assert(payment_e.outgoingChannel == channelUpdate_cd.shortChannelId) val add_d = UpdateAddHtlc(randomBytes32(), 3, payment_e.cmd.amount, paymentHash, payment_e.cmd.cltvExpiry, payment_e.cmd.onion, None) val Right(ChannelRelayPacket(_, _, packet_e)) = decrypt(add_d, priv_d.privateKey, Features.empty) @@ -591,7 +591,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { // c forwards an invalid amount to e through (the outer total amount doesn't match the inner amount). val invalidTotalAmount = inner_c.amountToForward - 1.msat val recipient_e = ClearRecipient(e, Features.empty, invalidTotalAmount, inner_c.outgoingCltv, randomBytes32(), nextTrampolineOnion_opt = Some(trampolinePacket_e)) - val Right(payment_e) = buildOutgoingPayment(ActorRef.noSender, priv_c.privateKey, Upstream.Trampoline(Seq(Upstream.ReceivedHtlc(add_c, TimestampMilli(1687345927000L)))), paymentHash, Route(invalidTotalAmount, afterTrampolineChannelHops, None), recipient_e) + val Right(payment_e) = buildOutgoingPayment(ActorRef.noSender, priv_c.privateKey, Upstream.Trampoline(Seq(Upstream.ReceivedHtlc(add_c, TimestampMilli(1687345927000L), useAttributableErrors = false))), paymentHash, Route(invalidTotalAmount, afterTrampolineChannelHops, None), recipient_e) val add_d = UpdateAddHtlc(randomBytes32(), 3, payment_e.cmd.amount, paymentHash, payment_e.cmd.cltvExpiry, payment_e.cmd.onion, None) val Right(ChannelRelayPacket(_, payload_d, packet_e)) = decrypt(add_d, priv_d.privateKey, Features.empty) @@ -607,7 +607,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { // c forwards an invalid amount to e through (the outer expiry doesn't match the inner expiry). val invalidExpiry = inner_c.outgoingCltv - CltvExpiryDelta(12) val recipient_e = ClearRecipient(e, Features.empty, inner_c.amountToForward, invalidExpiry, randomBytes32(), nextTrampolineOnion_opt = Some(trampolinePacket_e)) - val Right(payment_e) = buildOutgoingPayment(ActorRef.noSender, priv_c.privateKey, Upstream.Trampoline(Seq(Upstream.ReceivedHtlc(add_c, TimestampMilli(1687345927000L)))), paymentHash, Route(inner_c.amountToForward, afterTrampolineChannelHops, None), recipient_e) + val Right(payment_e) = buildOutgoingPayment(ActorRef.noSender, priv_c.privateKey, Upstream.Trampoline(Seq(Upstream.ReceivedHtlc(add_c, TimestampMilli(1687345927000L), useAttributableErrors = false))), paymentHash, Route(inner_c.amountToForward, afterTrampolineChannelHops, None), recipient_e) val add_d = UpdateAddHtlc(randomBytes32(), 3, payment_e.cmd.amount, paymentHash, payment_e.cmd.cltvExpiry, payment_e.cmd.onion, None) val Right(ChannelRelayPacket(_, payload_d, packet_e)) = decrypt(add_d, priv_d.privateKey, Features.empty) @@ -647,13 +647,13 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { // e returns a failure val failure = IncorrectOrUnknownPaymentDetails(finalAmount, BlockHeight(currentBlockCount)) - val Right(fail_e: UpdateFailHtlc) = buildHtlcFailure(priv_e.privateKey, CMD_FAIL_HTLC(add_e.id, Right(failure)), add_e) + val Right(fail_e: UpdateFailHtlc) = buildHtlcFailure(priv_e.privateKey, CMD_FAIL_HTLC(add_e.id, Right(failure), useAttributableErrors = false, TimestampMilli.now()), add_e) assert(fail_e.id == add_e.id) - val Right(fail_d: UpdateFailHtlc) = buildHtlcFailure(priv_d.privateKey, CMD_FAIL_HTLC(add_d.id, Left(fail_e.reason)), add_d) + val Right(fail_d: UpdateFailHtlc) = buildHtlcFailure(priv_d.privateKey, CMD_FAIL_HTLC(add_d.id, Left(fail_e.reason), useAttributableErrors = false, TimestampMilli.now()), add_d) assert(fail_d.id == add_d.id) - val Right(fail_c: UpdateFailHtlc) = buildHtlcFailure(priv_c.privateKey, CMD_FAIL_HTLC(add_c.id, Left(fail_d.reason)), add_c) + val Right(fail_c: UpdateFailHtlc) = buildHtlcFailure(priv_c.privateKey, CMD_FAIL_HTLC(add_c.id, Left(fail_d.reason), useAttributableErrors = false, TimestampMilli.now()), add_c) assert(fail_c.id == add_c.id) - val Right(fail_b: UpdateFailHtlc) = buildHtlcFailure(priv_b.privateKey, CMD_FAIL_HTLC(add_b.id, Left(fail_c.reason)), add_b) + val Right(fail_b: UpdateFailHtlc) = buildHtlcFailure(priv_b.privateKey, CMD_FAIL_HTLC(add_b.id, Left(fail_c.reason), useAttributableErrors = false, TimestampMilli.now()), add_b) assert(fail_b.id == add_b.id) val Success(Sphinx.DecryptedFailurePacket(failingNode, decryptedFailure)) = Sphinx.FailurePacket.decrypt(fail_b.reason, payment.sharedSecrets) assert(failingNode == e) @@ -677,19 +677,19 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { assert(payload_e.isInstanceOf[FinalPayload.Blinded]) // nodes after the introduction node cannot send `update_fail_htlc` messages - val Right(fail_e: UpdateFailMalformedHtlc) = buildHtlcFailure(priv_e.privateKey, CMD_FAIL_HTLC(add_e.id, Right(TemporaryNodeFailure())), add_e) + val Right(fail_e: UpdateFailMalformedHtlc) = buildHtlcFailure(priv_e.privateKey, CMD_FAIL_HTLC(add_e.id, Right(TemporaryNodeFailure()), useAttributableErrors = false, TimestampMilli.now()), add_e) assert(fail_e.id == add_e.id) assert(fail_e.onionHash == Sphinx.hash(add_e.onionRoutingPacket)) assert(fail_e.failureCode == InvalidOnionBlinding(fail_e.onionHash).code) - val Right(fail_d: UpdateFailMalformedHtlc) = buildHtlcFailure(priv_d.privateKey, CMD_FAIL_HTLC(add_d.id, Right(UnknownNextPeer())), add_d) + val Right(fail_d: UpdateFailMalformedHtlc) = buildHtlcFailure(priv_d.privateKey, CMD_FAIL_HTLC(add_d.id, Right(UnknownNextPeer()), useAttributableErrors = false, TimestampMilli.now()), add_d) assert(fail_d.id == add_d.id) assert(fail_d.onionHash == Sphinx.hash(add_d.onionRoutingPacket)) assert(fail_d.failureCode == InvalidOnionBlinding(fail_d.onionHash).code) // only the introduction node is allowed to send an `update_fail_htlc` message val failure = InvalidOnionBlinding(Sphinx.hash(add_c.onionRoutingPacket)) - val Right(fail_c: UpdateFailHtlc) = buildHtlcFailure(priv_c.privateKey, CMD_FAIL_HTLC(add_c.id, Right(failure)), add_c) + val Right(fail_c: UpdateFailHtlc) = buildHtlcFailure(priv_c.privateKey, CMD_FAIL_HTLC(add_c.id, Right(failure), useAttributableErrors = false, TimestampMilli.now()), add_c) assert(fail_c.id == add_c.id) - val Right(fail_b: UpdateFailHtlc) = buildHtlcFailure(priv_b.privateKey, CMD_FAIL_HTLC(add_b.id, Left(fail_c.reason)), add_b) + val Right(fail_b: UpdateFailHtlc) = buildHtlcFailure(priv_b.privateKey, CMD_FAIL_HTLC(add_b.id, Left(fail_c.reason), useAttributableErrors = false, TimestampMilli.now()), add_b) assert(fail_b.id == add_b.id) val Success(Sphinx.DecryptedFailurePacket(failingNode, decryptedFailure)) = Sphinx.FailurePacket.decrypt(fail_b.reason, payment.sharedSecrets) assert(failingNode == c) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PostRestartHtlcCleanerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PostRestartHtlcCleanerSpec.scala index bc244f7481..64b848f02b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PostRestartHtlcCleanerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PostRestartHtlcCleanerSpec.scala @@ -122,7 +122,7 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit // channel 1 goes to NORMAL state: system.eventStream.publish(ChannelStateChanged(channel.ref, channels.head.commitments.channelId, system.deadLetters, a, OFFLINE, NORMAL, Some(channels.head.commitments))) channel.expectMsgAllOf( - CMD_FAIL_HTLC(1, Right(TemporaryNodeFailure()), commit = true), + CMD_FAIL_HTLC(1, Right(TemporaryNodeFailure()), useAttributableErrors = false, TimestampMilli.min, commit = true), CMD_FAIL_MALFORMED_HTLC(4, ByteVector32.Zeroes, FailureMessageCodecs.BADONION | FailureMessageCodecs.PERM | 24, commit = true) ) channel.expectNoMessage(100 millis) @@ -130,15 +130,15 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit // channel 2 goes to NORMAL state: system.eventStream.publish(ChannelStateChanged(channel.ref, channels(1).commitments.channelId, system.deadLetters, a, OFFLINE, NORMAL, Some(channels(1).commitments))) channel.expectMsgAllOf( - CMD_FAIL_HTLC(0, Right(TemporaryNodeFailure()), commit = true), - CMD_FAIL_HTLC(4, Right(TemporaryNodeFailure()), commit = true) + CMD_FAIL_HTLC(0, Right(TemporaryNodeFailure()), useAttributableErrors = false, TimestampMilli.min, commit = true), + CMD_FAIL_HTLC(4, Right(TemporaryNodeFailure()), useAttributableErrors = false, TimestampMilli.min, commit = true) ) channel.expectNoMessage(100 millis) // let's assume that channel 1 was disconnected before having signed the fails, and gets connected again: system.eventStream.publish(ChannelStateChanged(channel.ref, channels.head.channelId, system.deadLetters, a, OFFLINE, NORMAL, Some(channels.head.commitments))) channel.expectMsgAllOf( - CMD_FAIL_HTLC(1, Right(TemporaryNodeFailure()), commit = true), + CMD_FAIL_HTLC(1, Right(TemporaryNodeFailure()), useAttributableErrors = false, TimestampMilli.min, commit = true), CMD_FAIL_MALFORMED_HTLC(4, ByteVector32.Zeroes, FailureMessageCodecs.BADONION | FailureMessageCodecs.PERM | 24, commit = true) ) channel.expectNoMessage(100 millis) @@ -188,10 +188,10 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit // channel 1 goes to NORMAL state: system.eventStream.publish(ChannelStateChanged(channel.ref, channels.head.channelId, system.deadLetters, a, OFFLINE, NORMAL, Some(channels.head.commitments))) val expected1 = Set( - CMD_FAIL_HTLC(0, Right(TemporaryNodeFailure()), commit = true), + CMD_FAIL_HTLC(0, Right(TemporaryNodeFailure()), useAttributableErrors = false, TimestampMilli.min, commit = true), CMD_FULFILL_HTLC(3, preimage, commit = true), CMD_FULFILL_HTLC(5, preimage, commit = true), - CMD_FAIL_HTLC(7, Right(TemporaryNodeFailure()), commit = true) + CMD_FAIL_HTLC(7, Right(TemporaryNodeFailure()), useAttributableErrors = false, TimestampMilli.min, commit = true) ) val received1 = expected1.map(_ => channel.expectMsgType[Command]) assert(received1 == expected1) @@ -200,10 +200,10 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit // channel 2 goes to NORMAL state: system.eventStream.publish(ChannelStateChanged(channel.ref, channels(1).channelId, system.deadLetters, a, OFFLINE, NORMAL, Some(channels(1).commitments))) val expected2 = Set( - CMD_FAIL_HTLC(1, Right(TemporaryNodeFailure()), commit = true), - CMD_FAIL_HTLC(3, Right(TemporaryNodeFailure()), commit = true), + CMD_FAIL_HTLC(1, Right(TemporaryNodeFailure()), useAttributableErrors = false, TimestampMilli.min, commit = true), + CMD_FAIL_HTLC(3, Right(TemporaryNodeFailure()), useAttributableErrors = false, TimestampMilli.min, commit = true), CMD_FULFILL_HTLC(4, preimage, commit = true), - CMD_FAIL_HTLC(9, Right(TemporaryNodeFailure()), commit = true) + CMD_FAIL_HTLC(9, Right(TemporaryNodeFailure()), useAttributableErrors = false, TimestampMilli.min, commit = true) ) val received2 = expected2.map(_ => channel.expectMsgType[Command]) assert(received2 == expected2) @@ -337,9 +337,9 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit val htlc_upstream_1 = Seq(buildHtlcIn(0, channelId_ab_1, paymentHash1), buildHtlcIn(5, channelId_ab_1, paymentHash2)) val htlc_upstream_2 = Seq(buildHtlcIn(7, channelId_ab_2, paymentHash1), buildHtlcIn(9, channelId_ab_2, paymentHash2)) val htlc_upstream_3 = Seq(buildHtlcIn(11, randomBytes32(), paymentHash3)) - val upstream_1 = Upstream.Trampoline(Upstream.ReceivedHtlc(htlc_upstream_1.head.add, TimestampMilli(1687345927000L)) :: Upstream.ReceivedHtlc(htlc_upstream_2.head.add, TimestampMilli(1687345967000L)) :: Nil) - val upstream_2 = Upstream.Trampoline(Upstream.ReceivedHtlc(htlc_upstream_1(1).add, TimestampMilli(1687345902000L)) :: Upstream.ReceivedHtlc(htlc_upstream_2(1).add, TimestampMilli(1687345999000L)) :: Nil) - val upstream_3 = Upstream.Trampoline(Upstream.ReceivedHtlc(htlc_upstream_3.head.add, TimestampMilli(1687345980000L)) :: Nil) + val upstream_1 = Upstream.Trampoline(Upstream.ReceivedHtlc(htlc_upstream_1.head.add, TimestampMilli(1687345927000L), useAttributableErrors = false) :: Upstream.ReceivedHtlc(htlc_upstream_2.head.add, TimestampMilli(1687345967000L), useAttributableErrors = false) :: Nil) + val upstream_2 = Upstream.Trampoline(Upstream.ReceivedHtlc(htlc_upstream_1(1).add, TimestampMilli(1687345902000L), useAttributableErrors = false) :: Upstream.ReceivedHtlc(htlc_upstream_2(1).add, TimestampMilli(1687345999000L), useAttributableErrors = false) :: Nil) + val upstream_3 = Upstream.Trampoline(Upstream.ReceivedHtlc(htlc_upstream_3.head.add, TimestampMilli(1687345980000L), useAttributableErrors = false) :: Nil) val data_upstream_1 = ChannelCodecsSpec.makeChannelDataNormal(htlc_upstream_1, Map.empty) val data_upstream_2 = ChannelCodecsSpec.makeChannelDataNormal(htlc_upstream_2, Map.empty) val data_upstream_3 = ChannelCodecsSpec.makeChannelDataNormal(htlc_upstream_3, Map.empty) @@ -410,8 +410,8 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit system.eventStream.publish(ChannelStateChanged(channel_upstream_3.ref, data_upstream_3.channelId, system.deadLetters, a, OFFLINE, NORMAL, Some(data_upstream_3.commitments))) // Payment 1 should fail instantly. - channel_upstream_1.expectMsg(CMD_FAIL_HTLC(0, Right(TemporaryNodeFailure()), commit = true)) - channel_upstream_2.expectMsg(CMD_FAIL_HTLC(7, Right(TemporaryNodeFailure()), commit = true)) + channel_upstream_1.expectMsg(CMD_FAIL_HTLC(0, Right(TemporaryNodeFailure()), useAttributableErrors = false, TimestampMilli.min, commit = true)) + channel_upstream_2.expectMsg(CMD_FAIL_HTLC(7, Right(TemporaryNodeFailure()), useAttributableErrors = false, TimestampMilli.min, commit = true)) channel_upstream_1.expectNoMessage(100 millis) channel_upstream_2.expectNoMessage(100 millis) @@ -438,7 +438,7 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit ) val channelData = ChannelCodecsSpec.makeChannelDataNormal(htlc_ab, Map.empty) nodeParams.db.pendingCommands.addSettlementCommand(channelId_ab_1, CMD_FULFILL_HTLC(1, randomBytes32())) - nodeParams.db.pendingCommands.addSettlementCommand(channelId_ab_1, CMD_FAIL_HTLC(4, Right(PermanentChannelFailure()))) + nodeParams.db.pendingCommands.addSettlementCommand(channelId_ab_1, CMD_FAIL_HTLC(4, Right(PermanentChannelFailure()), useAttributableErrors = false, TimestampMilli.min)) val (_, postRestart) = f.createRelayer(nodeParams) postRestart ! PostRestartHtlcCleaner.Init(List(channelData)) @@ -556,7 +556,7 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit sender.send(relayer, buildForwardFail(testCase.downstream_1_1, testCase.origin_1)) val fails = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]] :: register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]] :: Nil assert(fails.toSet == testCase.origin_1.htlcs.map { - case (channelId, htlcId) => Register.Forward(null, channelId, CMD_FAIL_HTLC(htlcId, Right(TemporaryNodeFailure()), commit = true)) + case (channelId, htlcId) => Register.Forward(null, channelId, CMD_FAIL_HTLC(htlcId, Right(TemporaryNodeFailure()), useAttributableErrors = false, TimestampMilli.min, commit = true)) }.toSet) sender.send(relayer, buildForwardFail(testCase.downstream_1_1, testCase.origin_1)) @@ -568,7 +568,7 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit sender.send(relayer, buildForwardFail(testCase.downstream_2_3, testCase.origin_2)) register.expectMsg(testCase.origin_2.htlcs.map { - case (channelId, htlcId) => Register.Forward(null, channelId, CMD_FAIL_HTLC(htlcId, Right(TemporaryNodeFailure()), commit = true)) + case (channelId, htlcId) => Register.Forward(null, channelId, CMD_FAIL_HTLC(htlcId, Right(TemporaryNodeFailure()), useAttributableErrors = false, TimestampMilli.min, commit = true)) }.head) register.expectNoMessage(100 millis) @@ -651,7 +651,7 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit // Standard channel goes to NORMAL state: system.eventStream.publish(ChannelStateChanged(channel.ref, c.commitments.channelId, system.deadLetters, a, OFFLINE, NORMAL, Some(c.commitments))) - channel.expectMsg(CMD_FAIL_HTLC(1L, Right(TemporaryNodeFailure()), commit = true)) + channel.expectMsg(CMD_FAIL_HTLC(1L, Right(TemporaryNodeFailure()), useAttributableErrors = false, TimestampMilli.min, commit = true)) channel.expectNoMessage(100 millis) } @@ -669,8 +669,8 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit } // @formatter:on - val cmd1 = CMD_FAIL_HTLC(id = 0L, reason = Left(ByteVector.empty), replyTo_opt = None) - val cmd2 = CMD_FAIL_HTLC(id = 1L, reason = Left(ByteVector.empty), replyTo_opt = None) + val cmd1 = CMD_FAIL_HTLC(id = 0L, reason = Left(ByteVector.empty), useAttributableErrors = false, TimestampMilli.min, replyTo_opt = None) + val cmd2 = CMD_FAIL_HTLC(id = 1L, reason = Left(ByteVector.empty), useAttributableErrors = false, TimestampMilli.min, replyTo_opt = None) val nodeParams1 = nodeParams.copy(pluginParams = List(pluginParams)) nodeParams1.db.pendingCommands.addSettlementCommand(channelId_ab_1, cmd1) nodeParams1.db.pendingCommands.addSettlementCommand(channelId_ab_1, cmd2) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/ChannelRelayerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/ChannelRelayerSpec.scala index 4cdd3ef8f9..af51215273 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/ChannelRelayerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/ChannelRelayerSpec.scala @@ -60,9 +60,9 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a } } - def expectFwdFail(register: TestProbe[Any], channelId: ByteVector32, cmd: channel.Command): Register.Forward[channel.Command] = { - val fwd = register.expectMessageType[Register.Forward[channel.Command]] - assert(fwd.message == cmd) + def expectFwdFail(register: TestProbe[Any], channelId: ByteVector32, cmd: CMD_FAIL_HTLC): Register.Forward[CMD_FAIL_HTLC] = { + val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] + assert(fwd.message.copy(startHoldTime = TimestampMilli.min) == cmd.copy(startHoldTime = TimestampMilli.min)) assert(fwd.channelId == channelId) fwd } @@ -92,7 +92,7 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a if (success) { expectFwdAdd(register, lcu.channelId, outgoingAmount, outgoingExpiry) } else { - expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, Right(UnknownNextPeer()), commit = true)) + expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, Right(UnknownNextPeer()), useAttributableErrors = false, TimestampMilli.now(), commit = true)) } } @@ -194,7 +194,7 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a fwd1.message.replyTo ! RES_ADD_FAILED(fwd2.message, HtlcValueTooHighInFlight(channelIds(realScid1), 1000000000 msat, 1516977616 msat), Some(u1.channelUpdate)) // the relayer should give up - expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, Right(TemporaryChannelFailure(u1.channelUpdate)), commit = true)) + expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, Right(TemporaryChannelFailure(u1.channelUpdate)), useAttributableErrors = false, TimestampMilli.now(), commit = true)) } test("fail to relay when we have no channel_update for the next channel") { f => @@ -205,7 +205,7 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a channelRelayer ! Relay(r) - expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, Right(UnknownNextPeer()), commit = true)) + expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, Right(UnknownNextPeer()), useAttributableErrors = false, TimestampMilli.now(), commit = true)) } test("fail to relay when register returns an error") { f => @@ -221,7 +221,7 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a val fwd = expectFwdAdd(register, channelIds(realScid1), outgoingAmount, outgoingExpiry) fwd.replyTo ! Register.ForwardFailure(fwd) - expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, Right(UnknownNextPeer()), commit = true)) + expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, Right(UnknownNextPeer()), useAttributableErrors = false, TimestampMilli.now(), commit = true)) } test("fail to relay when the channel is advertised as unusable (down)") { f => @@ -236,7 +236,7 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a channelRelayer ! WrappedLocalChannelDown(d) channelRelayer ! Relay(r) - expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, Right(UnknownNextPeer()), commit = true)) + expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, Right(UnknownNextPeer()), useAttributableErrors = false, TimestampMilli.now(), commit = true)) } test("fail to relay when channel is disabled") { f => @@ -249,7 +249,7 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a channelRelayer ! WrappedLocalChannelUpdate(u) channelRelayer ! Relay(r) - expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, Right(ChannelDisabled(u.channelUpdate.messageFlags, u.channelUpdate.channelFlags, u.channelUpdate)), commit = true)) + expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, Right(ChannelDisabled(u.channelUpdate.messageFlags, u.channelUpdate.channelFlags, u.channelUpdate)), useAttributableErrors = false, TimestampMilli.now(), commit = true)) } test("fail to relay when amount is below minimum") { f => @@ -262,7 +262,7 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a channelRelayer ! WrappedLocalChannelUpdate(u) channelRelayer ! Relay(r) - expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, Right(AmountBelowMinimum(outgoingAmount, u.channelUpdate)), commit = true)) + expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, Right(AmountBelowMinimum(outgoingAmount, u.channelUpdate)), useAttributableErrors = false, TimestampMilli.now(), commit = true)) } test("fail to relay blinded payment") { f => @@ -317,7 +317,7 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a channelRelayer ! WrappedLocalChannelUpdate(u) channelRelayer ! Relay(r) - expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, Right(IncorrectCltvExpiry(r.outgoingCltv, u.channelUpdate)), commit = true)) + expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, Right(IncorrectCltvExpiry(r.outgoingCltv, u.channelUpdate)), useAttributableErrors = false, TimestampMilli.now(), commit = true)) } test("fail to relay when fee is insufficient") { f => @@ -330,7 +330,7 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a channelRelayer ! WrappedLocalChannelUpdate(u) channelRelayer ! Relay(r) - expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, Right(FeeInsufficient(r.add.amountMsat, u.channelUpdate)), commit = true)) + expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, Right(FeeInsufficient(r.add.amountMsat, u.channelUpdate)), useAttributableErrors = false, TimestampMilli.now(), commit = true)) } test("relay that would fail (fee insufficient) with a recent channel update but succeed with the previous update") { f => @@ -361,7 +361,7 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a channelRelayer ! Relay(r) // relay fails because the current update (u3) with higher fees occurred more than 10 minutes ago - expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, Right(FeeInsufficient(r.add.amountMsat, u3.channelUpdate)), commit = true)) + expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, Right(FeeInsufficient(r.add.amountMsat, u3.channelUpdate)), useAttributableErrors = false, TimestampMilli.now(), commit = true)) } test("fail to relay when there is a local error") { f => @@ -390,7 +390,7 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a channelRelayer ! Relay(r) val fwd = expectFwdAdd(register, channelIds(realScid1), outgoingAmount, outgoingExpiry) fwd.message.replyTo ! RES_ADD_FAILED(fwd.message, testCase.exc, Some(testCase.update)) - expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, Right(testCase.failure), commit = true)) + expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, Right(testCase.failure), useAttributableErrors = false, TimestampMilli.now(), commit = true)) } } @@ -436,7 +436,7 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a val cmd4 = expectFwdAdd(register, channelUpdates(ShortChannelId(11111)).channelId, r.amountToForward, r.outgoingCltv).message cmd4.replyTo ! RES_ADD_FAILED(cmd4, HtlcValueTooHighInFlight(randomBytes32(), 100000000 msat, 100000000 msat), Some(channelUpdates(ShortChannelId(11111)).channelUpdate)) // all the suitable channels have been tried - expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, Right(TemporaryChannelFailure(channelUpdates(ShortChannelId(12345)).channelUpdate)), commit = true)) + expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, Right(TemporaryChannelFailure(channelUpdates(ShortChannelId(12345)).channelUpdate)), useAttributableErrors = false, TimestampMilli.now(), commit = true)) } { // higher amount payment (have to increased incoming htlc amount for fees to be sufficient) @@ -471,7 +471,7 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a val payload = ChannelRelay.Standard(ShortChannelId(12345), 998900 msat, CltvExpiry(61)) val r = createValidIncomingPacket(payload, 1000000 msat, CltvExpiry(70)) channelRelayer ! Relay(r) - expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, Right(IncorrectCltvExpiry(CltvExpiry(61), channelUpdates(ShortChannelId(12345)).channelUpdate)), commit = true)) + expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, Right(IncorrectCltvExpiry(CltvExpiry(61), channelUpdates(ShortChannelId(12345)).channelUpdate)), useAttributableErrors = false, TimestampMilli.now(), commit = true)) } } @@ -484,14 +484,14 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a val u_disabled = createLocalUpdate(channelId1, enabled = false) val downstream_htlc = UpdateAddHtlc(channelId1, 7, outgoingAmount, paymentHash, outgoingExpiry, emptyOnionPacket, None) - case class TestCase(result: HtlcResult, cmd: channel.HtlcSettlementCommand) + case class TestCase(result: HtlcResult, cmd: CMD_FAIL_HTLC) val testCases = Seq( - TestCase(HtlcResult.RemoteFail(UpdateFailHtlc(channelId1, downstream_htlc.id, hex"deadbeef")), CMD_FAIL_HTLC(r.add.id, Left(hex"deadbeef"), commit = true)), - TestCase(HtlcResult.RemoteFailMalformed(UpdateFailMalformedHtlc(channelId1, downstream_htlc.id, ByteVector32.One, FailureMessageCodecs.BADONION | FailureMessageCodecs.PERM | 5)), CMD_FAIL_HTLC(r.add.id, Right(InvalidOnionHmac(ByteVector32.One)), commit = true)), - TestCase(HtlcResult.OnChainFail(HtlcOverriddenByLocalCommit(channelId1, downstream_htlc)), CMD_FAIL_HTLC(r.add.id, Right(PermanentChannelFailure()), commit = true)), - TestCase(HtlcResult.DisconnectedBeforeSigned(u_disabled.channelUpdate), CMD_FAIL_HTLC(r.add.id, Right(TemporaryChannelFailure(u_disabled.channelUpdate)), commit = true)), - TestCase(HtlcResult.ChannelFailureBeforeSigned, CMD_FAIL_HTLC(r.add.id, Right(PermanentChannelFailure()), commit = true)) + TestCase(HtlcResult.RemoteFail(UpdateFailHtlc(channelId1, downstream_htlc.id, hex"deadbeef")), CMD_FAIL_HTLC(r.add.id, Left(hex"deadbeef"), useAttributableErrors = false, TimestampMilli.now(), commit = true)), + TestCase(HtlcResult.RemoteFailMalformed(UpdateFailMalformedHtlc(channelId1, downstream_htlc.id, ByteVector32.One, FailureMessageCodecs.BADONION | FailureMessageCodecs.PERM | 5)), CMD_FAIL_HTLC(r.add.id, Right(InvalidOnionHmac(ByteVector32.One)), useAttributableErrors = false, TimestampMilli.now(), commit = true)), + TestCase(HtlcResult.OnChainFail(HtlcOverriddenByLocalCommit(channelId1, downstream_htlc)), CMD_FAIL_HTLC(r.add.id, Right(PermanentChannelFailure()), useAttributableErrors = false, TimestampMilli.now(), commit = true)), + TestCase(HtlcResult.DisconnectedBeforeSigned(u_disabled.channelUpdate), CMD_FAIL_HTLC(r.add.id, Right(TemporaryChannelFailure(u_disabled.channelUpdate)), useAttributableErrors = false, TimestampMilli.now(), commit = true)), + TestCase(HtlcResult.ChannelFailureBeforeSigned, CMD_FAIL_HTLC(r.add.id, Right(PermanentChannelFailure()), useAttributableErrors = false, TimestampMilli.now(), commit = true)) ) testCases.foreach { testCase => diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/NodeRelayerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/NodeRelayerSpec.scala index 19b88cf48d..099911fd28 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/NodeRelayerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/NodeRelayerSpec.scala @@ -205,7 +205,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]](30 seconds) assert(fwd.channelId == p.add.channelId) val failure = Right(PaymentTimeout()) - assert(fwd.message == CMD_FAIL_HTLC(p.add.id, failure, commit = true)) + assert(fwd.message.copy(startHoldTime = TimestampMilli.min) == CMD_FAIL_HTLC(p.add.id, failure, useAttributableErrors = false, TimestampMilli.min, commit = true)) } parent.expectMessageType[NodeRelayer.RelayComplete] @@ -230,7 +230,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] assert(fwd.channelId == extra.add.channelId) val failure = IncorrectOrUnknownPaymentDetails(extra.add.amountMsat, nodeParams.currentBlockHeight) - assert(fwd.message == CMD_FAIL_HTLC(extra.add.id, Right(failure), commit = true)) + assert(fwd.message.copy(startHoldTime = TimestampMilli.min) == CMD_FAIL_HTLC(extra.add.id, Right(failure), useAttributableErrors = false, TimestampMilli.min, commit = true)) register.expectNoMessage(100 millis) } @@ -243,7 +243,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl incomingMultiPart.foreach(incoming => nodeRelayer ! NodeRelay.Relay(incoming)) val outgoingCfg = mockPayFSM.expectMessageType[SendPaymentConfig] - validateOutgoingCfg(outgoingCfg, Upstream.Trampoline(incomingMultiPart.map(p => Upstream.ReceivedHtlc(p.add, TimestampMilli.now())))) + validateOutgoingCfg(outgoingCfg, Upstream.Trampoline(incomingMultiPart.map(p => Upstream.ReceivedHtlc(p.add, TimestampMilli.now(), useAttributableErrors = false)))) val outgoingPayment = mockPayFSM.expectMessageType[SendMultiPartPayment] validateOutgoingPayment(outgoingPayment) @@ -258,7 +258,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl val fwd1 = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] assert(fwd1.channelId == i1.add.channelId) val failure1 = IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight) - assert(fwd1.message == CMD_FAIL_HTLC(i1.add.id, Right(failure1), commit = true)) + assert(fwd1.message.copy(startHoldTime = TimestampMilli.min) == CMD_FAIL_HTLC(i1.add.id, Right(failure1), useAttributableErrors = false, TimestampMilli.min, commit = true)) // Receive new HTLC with different details, but for the same payment hash. val i2 = IncomingPaymentPacket.NodeRelayPacket( @@ -271,7 +271,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl val fwd2 = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] assert(fwd1.channelId == i1.add.channelId) val failure2 = IncorrectOrUnknownPaymentDetails(1500 msat, nodeParams.currentBlockHeight) - assert(fwd2.message == CMD_FAIL_HTLC(i2.add.id, Right(failure2), commit = true)) + assert(fwd2.message.copy(startHoldTime = TimestampMilli.min) == CMD_FAIL_HTLC(i2.add.id, Right(failure2), useAttributableErrors = false, TimestampMilli.min, commit = true)) register.expectNoMessage(100 millis) } @@ -287,7 +287,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] assert(fwd.channelId == p.add.channelId) - assert(fwd.message == CMD_FAIL_HTLC(p.add.id, Right(TrampolineExpiryTooSoon()), commit = true)) + assert(fwd.message.copy(startHoldTime = TimestampMilli.min) == CMD_FAIL_HTLC(p.add.id, Right(TrampolineExpiryTooSoon()), useAttributableErrors = false, TimestampMilli.min, commit = true)) register.expectNoMessage(100 millis) } @@ -303,7 +303,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] assert(fwd.channelId == p.add.channelId) - assert(fwd.message == CMD_FAIL_HTLC(p.add.id, Right(TrampolineExpiryTooSoon()), commit = true)) + assert(fwd.message.copy(startHoldTime = TimestampMilli.min) == CMD_FAIL_HTLC(p.add.id, Right(TrampolineExpiryTooSoon()), useAttributableErrors = false, TimestampMilli.min, commit = true)) register.expectNoMessage(100 millis) } @@ -324,7 +324,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl p.foreach { p => val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] assert(fwd.channelId == p.add.channelId) - assert(fwd.message == CMD_FAIL_HTLC(p.add.id, Right(TrampolineExpiryTooSoon()), commit = true)) + assert(fwd.message.copy(startHoldTime = TimestampMilli.min) == CMD_FAIL_HTLC(p.add.id, Right(TrampolineExpiryTooSoon()), useAttributableErrors = false, TimestampMilli.min, commit = true)) } register.expectNoMessage(100 millis) @@ -349,7 +349,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl incomingAsyncPayment.foreach { p => val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] assert(fwd.channelId == p.add.channelId) - assert(fwd.message == CMD_FAIL_HTLC(p.add.id, Right(TemporaryNodeFailure()), commit = true)) + assert(fwd.message.copy(startHoldTime = TimestampMilli.min) == CMD_FAIL_HTLC(p.add.id, Right(TemporaryNodeFailure()), useAttributableErrors = false, TimestampMilli.min, commit = true)) } register.expectNoMessage(100 millis) } @@ -372,7 +372,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl // upstream payment relayed val outgoingCfg = mockPayFSM.expectMessageType[SendPaymentConfig] - validateOutgoingCfg(outgoingCfg, Upstream.Trampoline(incomingAsyncPayment.map(p => Upstream.ReceivedHtlc(p.add, TimestampMilli.now())))) + validateOutgoingCfg(outgoingCfg, Upstream.Trampoline(incomingAsyncPayment.map(p => Upstream.ReceivedHtlc(p.add, TimestampMilli.now(), useAttributableErrors = false)))) val outgoingPayment = mockPayFSM.expectMessageType[SendMultiPartPayment] validateOutgoingPayment(outgoingPayment) // those are adapters for pay-fsm messages @@ -412,7 +412,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl incomingAsyncPayment.foreach { p => val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] assert(fwd.channelId == p.add.channelId) - assert(fwd.message == CMD_FAIL_HTLC(p.add.id, Right(TemporaryNodeFailure()), commit = true)) + assert(fwd.message.copy(startHoldTime = TimestampMilli.min) == CMD_FAIL_HTLC(p.add.id, Right(TemporaryNodeFailure()), useAttributableErrors = false, TimestampMilli.min, commit = true)) } register.expectNoMessage(100 millis) @@ -434,7 +434,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl incomingAsyncPayment.foreach { p => val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] assert(fwd.channelId == p.add.channelId) - assert(fwd.message == CMD_FAIL_HTLC(p.add.id, Right(TemporaryNodeFailure()), commit = true)) + assert(fwd.message.copy(startHoldTime = TimestampMilli.min) == CMD_FAIL_HTLC(p.add.id, Right(TemporaryNodeFailure()), useAttributableErrors = false, TimestampMilli.min, commit = true)) } register.expectNoMessage(100 millis) } @@ -449,7 +449,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl // upstream payment relayed val outgoingCfg = mockPayFSM.expectMessageType[SendPaymentConfig] - validateOutgoingCfg(outgoingCfg, Upstream.Trampoline(incomingAsyncPayment.map(p => Upstream.ReceivedHtlc(p.add, TimestampMilli.now())))) + validateOutgoingCfg(outgoingCfg, Upstream.Trampoline(incomingAsyncPayment.map(p => Upstream.ReceivedHtlc(p.add, TimestampMilli.now(), useAttributableErrors = false)))) val outgoingPayment = mockPayFSM.expectMessageType[SendMultiPartPayment] validateOutgoingPayment(outgoingPayment) // those are adapters for pay-fsm messages @@ -482,7 +482,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] assert(fwd.channelId == p.add.channelId) - assert(fwd.message == CMD_FAIL_HTLC(p.add.id, Right(TrampolineFeeInsufficient()), commit = true)) + assert(fwd.message.copy(startHoldTime = TimestampMilli.min) == CMD_FAIL_HTLC(p.add.id, Right(TrampolineFeeInsufficient()), useAttributableErrors = false, TimestampMilli.min, commit = true)) register.expectNoMessage(100 millis) } @@ -500,7 +500,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl p.foreach { p => val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] assert(fwd.channelId == p.add.channelId) - assert(fwd.message == CMD_FAIL_HTLC(p.add.id, Right(TrampolineFeeInsufficient()), commit = true)) + assert(fwd.message.copy(startHoldTime = TimestampMilli.min) == CMD_FAIL_HTLC(p.add.id, Right(TrampolineFeeInsufficient()), useAttributableErrors = false, TimestampMilli.min, commit = true)) } register.expectNoMessage(100 millis) @@ -515,7 +515,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] assert(fwd.channelId == p.add.channelId) - assert(fwd.message == CMD_FAIL_HTLC(p.add.id, Right(InvalidOnionPayload(UInt64(2), 0)), commit = true)) + assert(fwd.message.copy(startHoldTime = TimestampMilli.min) == CMD_FAIL_HTLC(p.add.id, Right(InvalidOnionPayload(UInt64(2), 0)), useAttributableErrors = false, TimestampMilli.min, commit = true)) register.expectNoMessage(100 millis) } @@ -533,7 +533,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl p.foreach { p => val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] assert(fwd.channelId == p.add.channelId) - assert(fwd.message == CMD_FAIL_HTLC(p.add.id, Right(InvalidOnionPayload(UInt64(2), 0)), commit = true)) + assert(fwd.message.copy(startHoldTime = TimestampMilli.min) == CMD_FAIL_HTLC(p.add.id, Right(InvalidOnionPayload(UInt64(2), 0)), useAttributableErrors = false, TimestampMilli.min, commit = true)) } register.expectNoMessage(100 millis) @@ -555,7 +555,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl incomingMultiPart.foreach { p => val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] assert(fwd.channelId == p.add.channelId) - assert(fwd.message == CMD_FAIL_HTLC(p.add.id, Right(TrampolineFeeInsufficient()), commit = true)) + assert(fwd.message.copy(startHoldTime = TimestampMilli.min) == CMD_FAIL_HTLC(p.add.id, Right(TrampolineFeeInsufficient()), useAttributableErrors = false, TimestampMilli.min, commit = true)) } register.expectNoMessage(100 millis) @@ -580,7 +580,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl incoming.foreach { p => val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] assert(fwd.channelId == p.add.channelId) - assert(fwd.message == CMD_FAIL_HTLC(p.add.id, Right(TemporaryNodeFailure()), commit = true)) + assert(fwd.message.copy(startHoldTime = TimestampMilli.min) == CMD_FAIL_HTLC(p.add.id, Right(TemporaryNodeFailure()), useAttributableErrors = false, TimestampMilli.min, commit = true)) } register.expectNoMessage(100 millis) @@ -603,7 +603,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl incomingMultiPart.foreach { p => val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] assert(fwd.channelId == p.add.channelId) - assert(fwd.message == CMD_FAIL_HTLC(p.add.id, Right(TrampolineFeeInsufficient()), commit = true)) + assert(fwd.message.copy(startHoldTime = TimestampMilli.min) == CMD_FAIL_HTLC(p.add.id, Right(TrampolineFeeInsufficient()), useAttributableErrors = false, TimestampMilli.min, commit = true)) } register.expectNoMessage(100 millis) @@ -625,7 +625,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl incomingMultiPart.foreach { p => val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] assert(fwd.channelId == p.add.channelId) - assert(fwd.message == CMD_FAIL_HTLC(p.add.id, Right(FinalIncorrectHtlcAmount(42 msat)), commit = true)) + assert(fwd.message.copy(startHoldTime = TimestampMilli.min) == CMD_FAIL_HTLC(p.add.id, Right(FinalIncorrectHtlcAmount(42 msat)), useAttributableErrors = false, TimestampMilli.min, commit = true)) } register.expectNoMessage(100 millis) @@ -657,7 +657,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl nodeRelayer ! NodeRelay.Relay(incomingMultiPart.last) val outgoingCfg = mockPayFSM.expectMessageType[SendPaymentConfig] - validateOutgoingCfg(outgoingCfg, Upstream.Trampoline(incomingMultiPart.map(p => Upstream.ReceivedHtlc(p.add, TimestampMilli.now())))) + validateOutgoingCfg(outgoingCfg, Upstream.Trampoline(incomingMultiPart.map(p => Upstream.ReceivedHtlc(p.add, TimestampMilli.now(), useAttributableErrors = false)))) val outgoingPayment = mockPayFSM.expectMessageType[SendMultiPartPayment] validateOutgoingPayment(outgoingPayment) // those are adapters for pay-fsm messages @@ -693,7 +693,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl nodeRelayer ! NodeRelay.Relay(incomingSinglePart) val outgoingCfg = mockPayFSM.expectMessageType[SendPaymentConfig] - validateOutgoingCfg(outgoingCfg, Upstream.Trampoline(Upstream.ReceivedHtlc(incomingSinglePart.add, TimestampMilli.now()) :: Nil)) + validateOutgoingCfg(outgoingCfg, Upstream.Trampoline(Upstream.ReceivedHtlc(incomingSinglePart.add, TimestampMilli.now(), useAttributableErrors = false) :: Nil)) val outgoingPayment = mockPayFSM.expectMessageType[SendMultiPartPayment] validateOutgoingPayment(outgoingPayment) // those are adapters for pay-fsm messages @@ -728,7 +728,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl incomingPayments.foreach(incoming => nodeRelayer ! NodeRelay.Relay(incoming)) val outgoingCfg = mockPayFSM.expectMessageType[SendPaymentConfig] - validateOutgoingCfg(outgoingCfg, Upstream.Trampoline(incomingMultiPart.map(p => Upstream.ReceivedHtlc(p.add, TimestampMilli.now())))) + validateOutgoingCfg(outgoingCfg, Upstream.Trampoline(incomingMultiPart.map(p => Upstream.ReceivedHtlc(p.add, TimestampMilli.now(), useAttributableErrors = false)))) val outgoingPayment = mockPayFSM.expectMessageType[SendMultiPartPayment] assert(outgoingPayment.recipient.nodeId == outgoingNodeId) assert(outgoingPayment.recipient.totalAmount == outgoingAmount) @@ -772,7 +772,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl incomingPayments.foreach(incoming => nodeRelayer ! NodeRelay.Relay(incoming)) val outgoingCfg = mockPayFSM.expectMessageType[SendPaymentConfig] - validateOutgoingCfg(outgoingCfg, Upstream.Trampoline(incomingMultiPart.map(p => Upstream.ReceivedHtlc(p.add, TimestampMilli.now())))) + validateOutgoingCfg(outgoingCfg, Upstream.Trampoline(incomingMultiPart.map(p => Upstream.ReceivedHtlc(p.add, TimestampMilli.now(), useAttributableErrors = false)))) val outgoingPayment = mockPayFSM.expectMessageType[SendPaymentToNode] assert(outgoingPayment.recipient.nodeId == outgoingNodeId) assert(outgoingPayment.amount == outgoingAmount) @@ -818,7 +818,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl incomingMultiPart.foreach { p => val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] assert(fwd.channelId == p.add.channelId) - assert(fwd.message == CMD_FAIL_HTLC(p.add.id, Right(InvalidOnionPayload(UInt64(8), 0)), commit = true)) + assert(fwd.message.copy(startHoldTime = TimestampMilli.min) == CMD_FAIL_HTLC(p.add.id, Right(InvalidOnionPayload(UInt64(8), 0)), useAttributableErrors = false, TimestampMilli.min, commit = true)) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/CommandCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/CommandCodecsSpec.scala index 7bcb786430..4e8fc108e3 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/CommandCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/CommandCodecsSpec.scala @@ -17,7 +17,7 @@ package fr.acinq.eclair.wire.internal import fr.acinq.bitcoin.scalacompat.ByteVector32 -import fr.acinq.eclair.UInt64 +import fr.acinq.eclair.{TimestampMilli, UInt64} import fr.acinq.eclair.channel._ import fr.acinq.eclair.wire.protocol.{FailureMessageCodecs, FailureMessageTlv, GenericTlv, TemporaryNodeFailure, TlvStream} import org.scalatest.funsuite.AnyFunSuite @@ -32,9 +32,9 @@ class CommandCodecsSpec extends AnyFunSuite { test("encode/decode all settlement commands") { val testCases: Map[HtlcSettlementCommand, ByteVector] = Map( CMD_FULFILL_HTLC(1573, ByteVector32(hex"e64e7c07667366e517886af99a25a5dd547014c95ba392ea4623fbf47fe00927")) -> hex"0000 0000000000000625 e64e7c07667366e517886af99a25a5dd547014c95ba392ea4623fbf47fe00927", - CMD_FAIL_HTLC(42456, Left(hex"d21a88a158067efecbee41da24e1d7407747f135e585e7417843729d3bff5c160817d14ce569761d93749a23d227edc0ade99c1a8d59541e45e1f623af2602d568a9a3c3bca71f1b4860ae0b599ba016c58224eab7721ed930eb2bdfd83ff940cc9e8106b0bd6b2027821f8d102b8c680664d90ce9e69d8bb96453a7b495710b83c13e4b3085bb0156b7091ed927305c44")) -> hex"0003 000000000000a5d8 00 0091 d21a88a158067efecbee41da24e1d7407747f135e585e7417843729d3bff5c160817d14ce569761d93749a23d227edc0ade99c1a8d59541e45e1f623af2602d568a9a3c3bca71f1b4860ae0b599ba016c58224eab7721ed930eb2bdfd83ff940cc9e8106b0bd6b2027821f8d102b8c680664d90ce9e69d8bb96453a7b495710b83c13e4b3085bb0156b7091ed927305c44", - CMD_FAIL_HTLC(253, Right(TemporaryNodeFailure())) -> hex"0003 00000000000000fd ff 0002 2002", - CMD_FAIL_HTLC(253, Right(TemporaryNodeFailure(TlvStream(Set.empty[FailureMessageTlv], Set(GenericTlv(UInt64(17), hex"deadbeef")))))) -> hex"0003 00000000000000fd ff 0008 2002 1104deadbeef", + CMD_FAIL_HTLC(42456, Left(hex"d21a88a158067efecbee41da24e1d7407747f135e585e7417843729d3bff5c160817d14ce569761d93749a23d227edc0ade99c1a8d59541e45e1f623af2602d568a9a3c3bca71f1b4860ae0b599ba016c58224eab7721ed930eb2bdfd83ff940cc9e8106b0bd6b2027821f8d102b8c680664d90ce9e69d8bb96453a7b495710b83c13e4b3085bb0156b7091ed927305c44"), useAttributableErrors = false, TimestampMilli(111)) -> hex"0003 000000000000a5d8 00 0091 d21a88a158067efecbee41da24e1d7407747f135e585e7417843729d3bff5c160817d14ce569761d93749a23d227edc0ade99c1a8d59541e45e1f623af2602d568a9a3c3bca71f1b4860ae0b599ba016c58224eab7721ed930eb2bdfd83ff940cc9e8106b0bd6b2027821f8d102b8c680664d90ce9e69d8bb96453a7b495710b83c13e4b3085bb0156b7091ed927305c44 00 000000000000006f", + CMD_FAIL_HTLC(253, Right(TemporaryNodeFailure()), useAttributableErrors = false, TimestampMilli(1234)) -> hex"0003 00000000000000fd ff 0002 2002 00 00000000000004d2", + CMD_FAIL_HTLC(253, Right(TemporaryNodeFailure(TlvStream(Set.empty[FailureMessageTlv], Set(GenericTlv(UInt64(17), hex"deadbeef"))))), useAttributableErrors = true, TimestampMilli(56789)) -> hex"0003 00000000000000fd ff 0008 2002 1104deadbeef ff 000000000000ddd5", CMD_FAIL_MALFORMED_HTLC(7984, ByteVector32(hex"17cc093e177c7a7fcaa9e96ab407146c8886546a5690f945c98ac20c4ab3b4f3"), FailureMessageCodecs.BADONION) -> hex"0002 0000000000001f30 17cc093e177c7a7fcaa9e96ab407146c8886546a5690f945c98ac20c4ab3b4f38000", ) @@ -51,8 +51,8 @@ class CommandCodecsSpec extends AnyFunSuite { val data123 = hex"fea75bb8cf45349eb544d8da832af5af30eefa671ec27cf2e4867bacada2dbe00a6ce5141164aa153ac8b4b25c75c3af15c4b5cb6a293607751a079bc546da17f654b76a74bc57b6b21ed73d2d3909f3682f01b85418a0f0ecddb759e9481d4563a572ac1ddcb77c64ae167d8dfbd889703cb5c33b4b9636bad472" val testCases = Map( hex"0000 000000000000002ae4927c04913251b44d0a3a8e57ded746fee80ff3b424e70dad2a1428eeba86cb" -> CMD_FULFILL_HTLC(42, data32, commit = false, None), - hex"0001 000000000000002a003dff53addc67a29a4f5aa26c6d41957ad798777d338f613e7972433dd656d16df00536728a08b2550a9d645a592e3ae1d78ae25ae5b5149b03ba8d03cde2a36d0bfb2a5bb53a5e2bdb590f6b9e969c84f9b41780dc2a0c5078766edbacf4a40ea2b1d2b9560eee5bbe32570b3ec6fdec44b81e5ae19da5cb1b5d6a3900" -> CMD_FAIL_HTLC(42, Left(data123), None, commit = false, None), - hex"0001 000000000000002a900100" -> CMD_FAIL_HTLC(42, Right(TemporaryNodeFailure())), + hex"0001 000000000000002a003dff53addc67a29a4f5aa26c6d41957ad798777d338f613e7972433dd656d16df00536728a08b2550a9d645a592e3ae1d78ae25ae5b5149b03ba8d03cde2a36d0bfb2a5bb53a5e2bdb590f6b9e969c84f9b41780dc2a0c5078766edbacf4a40ea2b1d2b9560eee5bbe32570b3ec6fdec44b81e5ae19da5cb1b5d6a3900" -> CMD_FAIL_HTLC(42, Left(data123), useAttributableErrors = false, TimestampMilli.min, None, commit = false, None), + hex"0001 000000000000002a900100" -> CMD_FAIL_HTLC(42, Right(TemporaryNodeFailure()), useAttributableErrors = false, TimestampMilli.min), hex"0002 000000000000002ae4927c04913251b44d0a3a8e57ded746fee80ff3b424e70dad2a1428eeba86cb01c8" -> CMD_FAIL_MALFORMED_HTLC(42, data32, 456, commit = false, None), ) From e15ae71e10de1b473746ac6bb9183dbe64858e7b Mon Sep 17 00:00:00 2001 From: Thomas HUET Date: Mon, 14 Aug 2023 14:50:28 +0200 Subject: [PATCH 2/6] Add feature --- eclair-core/src/main/resources/reference.conf | 1 + .../src/main/scala/fr/acinq/eclair/Features.scala | 6 ++++++ .../scala/fr/acinq/eclair/channel/fsm/Channel.scala | 9 ++++++--- .../scala/fr/acinq/eclair/payment/PaymentPacket.scala | 7 ++++++- .../eclair/payment/relay/PostRestartHtlcCleaner.scala | 10 ++++++---- .../scala/fr/acinq/eclair/payment/relay/Relayer.scala | 8 +++++--- .../fr/acinq/eclair/wire/protocol/PaymentOnion.scala | 2 ++ 7 files changed, 32 insertions(+), 11 deletions(-) diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index 82fe9de1d5..faac26fc4c 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -71,6 +71,7 @@ eclair { option_route_blinding = disabled option_shutdown_anysegwit = optional option_dual_fund = disabled + option_attributable_error = optional option_quiesce = disabled option_onion_messages = optional option_channel_type = optional diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala index 204dd04238..50f3b5d11a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala @@ -264,6 +264,11 @@ object Features { val mandatory = 28 } + case object AttributableError extends Feature with InitFeature with NodeFeature with Bolt11Feature { + val rfcName = "option_attributable_error" + val mandatory = 30 + } + // TODO: this should also extend NodeFeature once the spec is finalized case object Quiescence extends Feature with InitFeature { val rfcName = "option_quiesce" @@ -339,6 +344,7 @@ object Features { RouteBlinding, ShutdownAnySegwit, DualFunding, + AttributableError, Quiescence, OnionMessages, ChannelType, diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala index 341b585c3d..f27ba1726e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala @@ -640,7 +640,8 @@ 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)), useAttributableErrors = false, TimestampMilli.now(), commit = true) + // The HTLC is rejected without reading the onion, we default to using attributable errors if the feature is activated. + self ! CMD_FAIL_HTLC(add.id, Right(TemporaryChannelFailure(d.channelUpdate)), useAttributableErrors = nodeParams.features.hasFeature(Features.AttributableError), TimestampMilli.now(), commit = true) case PostRevocationAction.RelayFailure(result) => log.debug("forwarding {} to relayer", result) relayer ! result @@ -1341,11 +1342,13 @@ 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()), useAttributableErrors = false, TimestampMilli.now(), commit = true) + // The HTLC is rejected without reading the onion, we default to using attributable errors if the feature is activated. + self ! CMD_FAIL_HTLC(add.id, Right(PermanentChannelFailure()), useAttributableErrors = nodeParams.features.hasFeature(Features.AttributableError), 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()), useAttributableErrors = false, TimestampMilli.now(), commit = true) + // The HTLC is rejected without reading the onion, we default to using attributable errors if the feature is activated. + self ! CMD_FAIL_HTLC(add.id, Right(PermanentChannelFailure()), useAttributableErrors = nodeParams.features.hasFeature(Features.AttributableError), TimestampMilli.now(), commit = true) case PostRevocationAction.RelayFailure(result) => log.debug("forwarding {} to relayer", result) relayer ! result diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala index 3d8116ad4a..da0395a3e2 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala @@ -23,9 +23,10 @@ import fr.acinq.eclair.channel.{CMD_ADD_HTLC, CMD_FAIL_HTLC, CannotExtractShared import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.payment.send.Recipient import fr.acinq.eclair.router.Router.{BlindedHop, ChannelHop, Route} +import fr.acinq.eclair.wire.protocol.OnionPaymentPayloadTlv.UseAttributableError import fr.acinq.eclair.wire.protocol.PaymentOnion.{FinalPayload, IntermediatePayload, PerHopPayload} import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Feature, Features, MilliSatoshi, ShortChannelId, TimestampMilli, UInt64, randomKey} +import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Feature, FeatureSupport, Features, MilliSatoshi, ShortChannelId, TimestampMilli, UInt64, randomKey} import scodec.bits.ByteVector import scodec.{Attempt, DecodeResult} @@ -115,6 +116,10 @@ object IncomingPaymentPacket { case None => privateKey } decryptOnion(add.paymentHash, outerOnionDecryptionKey, add.onionRoutingPacket).flatMap { + case DecodedOnionPacket(payload, _) if payload.get[UseAttributableError].isDefined && !features.hasFeature(Features.AttributableError) => + Left(InvalidOnionPayload(UInt64(20), 0)) + case DecodedOnionPacket(payload, _) if payload.get[UseAttributableError].isEmpty && features.hasFeature(Features.AttributableError, Some(FeatureSupport.Mandatory)) => + Left(InvalidOnionPayload(UInt64(20), 0)) case DecodedOnionPacket(payload, Some(nextPacket)) => payload.get[OnionPaymentPayloadTlv.EncryptedRecipientData] match { case Some(_) if !features.hasFeature(Features.RouteBlinding) => Left(InvalidOnionPayload(UInt64(10), 0)) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/PostRestartHtlcCleaner.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/PostRestartHtlcCleaner.scala index ba87470c73..d84a265600 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/PostRestartHtlcCleaner.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/PostRestartHtlcCleaner.scala @@ -32,7 +32,6 @@ import fr.acinq.eclair.wire.protocol.{FailureMessage, InvalidOnionBlinding, Temp import fr.acinq.eclair.{CustomCommitmentsPlugin, Feature, Features, Logs, MilliSatoshiLong, NodeParams, TimestampMilli} import scala.concurrent.Promise -import scala.concurrent.duration.DurationInt import scala.util.Try /** @@ -132,7 +131,8 @@ class PostRestartHtlcCleaner(nodeParams: NodeParams, register: ActorRef, initial val failure = InvalidOnionBlinding(ByteVector32.Zeroes) CMD_FAIL_MALFORMED_HTLC(htlc.id, failure.onionHash, failure.code, commit = true) case None => - CMD_FAIL_HTLC(htlc.id, Right(TemporaryNodeFailure()), useAttributableErrors = false, TimestampMilli.min, commit = true) + // By default we use attributable errors if the feature is activated. + CMD_FAIL_HTLC(htlc.id, Right(TemporaryNodeFailure()), useAttributableErrors = nodeParams.features.hasFeature(Features.AttributableError), TimestampMilli.min, commit = true) } channel ! cmd } else { @@ -262,7 +262,8 @@ class PostRestartHtlcCleaner(nodeParams: NodeParams, register: ActorRef, initial val failure = InvalidOnionBlinding(ByteVector32.Zeroes) CMD_FAIL_MALFORMED_HTLC(originHtlcId, failure.onionHash, failure.code, commit = true) case None => - ChannelRelay.translateRelayFailure(originHtlcId, fail, useAttributableErrors = false, TimestampMilli.min) + // By default we use attributable errors if the feature is activated. + ChannelRelay.translateRelayFailure(originHtlcId, fail, useAttributableErrors = nodeParams.features.hasFeature(Features.AttributableError), TimestampMilli.min) } PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, originChannelId, cmd) case Origin.TrampolineRelayedCold(origins) => @@ -271,7 +272,8 @@ class PostRestartHtlcCleaner(nodeParams: NodeParams, register: ActorRef, initial Metrics.Resolved.withTag(Tags.Success, value = false).withTag(Metrics.Relayed, value = true).increment() // We don't bother decrypting the downstream failure to forward a more meaningful error upstream, it's // very likely that it won't be actionable anyway because of our node restart. - PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, channelId, CMD_FAIL_HTLC(htlcId, Right(TemporaryNodeFailure()), useAttributableErrors = false, TimestampMilli.min, commit = true)) + // By default we use attributable errors if the feature is activated. + PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, channelId, CMD_FAIL_HTLC(htlcId, Right(TemporaryNodeFailure()), useAttributableErrors = nodeParams.features.hasFeature(Features.AttributableError), TimestampMilli.min, commit = true)) } } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala index f2c28a4201..fc30c729e0 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala @@ -29,7 +29,7 @@ import fr.acinq.eclair.channel._ import fr.acinq.eclair.db.PendingCommandsDb import fr.acinq.eclair.payment._ import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{CltvExpiryDelta, Logs, MilliSatoshi, NodeParams, TimestampMilli} +import fr.acinq.eclair.{CltvExpiryDelta, Features, Logs, MilliSatoshi, NodeParams, TimestampMilli} import grizzled.slf4j.Logging import scala.concurrent.Promise @@ -84,7 +84,8 @@ class Relayer(nodeParams: NodeParams, router: ActorRef, register: ActorRef, paym // We are the introduction point of a blinded path: we add a non-negligible delay to make it look like it // could come from a downstream node. val delay = Some(500.millis + Random.nextLong(1500).millis) - CMD_FAIL_HTLC(add.id, Right(InvalidOnionBlinding(badOnion.onionHash)), useAttributableErrors = false, TimestampMilli.now(), delay, commit = true) + // By default we use attributable errors if the feature is activated. + CMD_FAIL_HTLC(add.id, Right(InvalidOnionBlinding(badOnion.onionHash)), useAttributableErrors = nodeParams.features.hasFeature(Features.AttributableError), TimestampMilli.now(), delay, commit = true) case _ => CMD_FAIL_MALFORMED_HTLC(add.id, badOnion.onionHash, badOnion.code, commit = true) } @@ -92,7 +93,8 @@ class Relayer(nodeParams: NodeParams, router: ActorRef, register: ActorRef, paym PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, add.channelId, cmdFail) case Left(failure) => log.warning(s"rejecting htlc #${add.id} from channelId=${add.channelId} reason=$failure") - val cmdFail = CMD_FAIL_HTLC(add.id, Right(failure), useAttributableErrors = false, TimestampMilli.now(), commit = true) + // By default we use attributable errors if the feature is activated. + val cmdFail = CMD_FAIL_HTLC(add.id, Right(failure), useAttributableErrors = nodeParams.features.hasFeature(Features.AttributableError), TimestampMilli.now(), commit = true) PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, add.channelId, cmdFail) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/PaymentOnion.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/PaymentOnion.scala index da56ee0765..29453df202 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/PaymentOnion.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/PaymentOnion.scala @@ -283,6 +283,7 @@ object PaymentOnion { records.records.find { case _: EncryptedRecipientData => false case _: BlindingPoint => false + case _: UseAttributableError => false case _ => true } match { case Some(_) => return Left(ForbiddenTlv(UInt64(0))) @@ -435,6 +436,7 @@ object PaymentOnion { case _: EncryptedRecipientData => false case _: BlindingPoint => false case _: TotalAmount => false + case _: UseAttributableError => false case _ => true } match { case Some(_) => return Left(ForbiddenTlv(UInt64(0))) From b20a26aea5be08f55ce14221e043d922d4240a0c Mon Sep 17 00:00:00 2001 From: Thomas HUET Date: Fri, 18 Aug 2023 10:29:10 +0200 Subject: [PATCH 3/6] Better JSON handling in test --- .../fr/acinq/eclair/crypto/SphinxSpec.scala | 31 +++++++++---------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala index 8e7a75e437..da5225800c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala @@ -22,8 +22,7 @@ import fr.acinq.eclair.crypto.Sphinx.RouteBlinding.{BlindedRoute, BlindedRouteDe import fr.acinq.eclair.wire.protocol import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, MilliSatoshiLong, ShortChannelId, UInt64, randomBytes, randomKey} -import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, MilliSatoshiLong, ShortChannelId, UInt64, randomBytes, randomKey} -import org.json4s.JsonAST._ +import org.json4s.DefaultFormats import org.json4s.jackson.JsonMethods import org.scalatest.funsuite.AnyFunSuite import scodec.bits._ @@ -453,22 +452,22 @@ class SphinxSpec extends AnyFunSuite { assert(decryptionError == expected) } + case class TestVector(encodedFailureMessage: String, hops: Seq[TestHop]) + case class TestHop(sharedSecret: String, encryptedMessage: String) + implicit val formats: DefaultFormats.type = DefaultFormats + test("attributable error test vector") { val src = Source.fromFile(new File(getClass.getResource(s"/attributable_error.json").getFile)) - try { - val testVector = JsonMethods.parse(src.mkString).asInstanceOf[JObject].values - val encodedFailureMessage = ByteVector.fromValidHex(testVector("encodedFailureMessage").asInstanceOf[String]) - val expected = FailureMessageCodecs.failureOnionPayload(0).decode(encodedFailureMessage.bits).require.value - val hops = testVector("hops").asInstanceOf[List[Map[String, String]]] - val sharedSecrets = hops.map(hop => ByteVector32(ByteVector.fromValidHex(hop("sharedSecret")))).reverse - val encryptedMessage = hops.map(hop => ByteVector.fromValidHex(hop("encryptedMessage"))).last - val nodeIds = (1 to 5).map(_ => randomKey().publicKey) - val Right(DecryptedFailurePacket(originNode, failureMessage)) = AttributableErrorPacket.decrypt(encryptedMessage, sharedSecrets.zip(nodeIds)) - assert(originNode == nodeIds.last) - assert(failureMessage == expected) - } finally { - src.close() - } + val testVector = JsonMethods.parse(src.mkString).extract[TestVector] + src.close() + val encodedFailureMessage = ByteVector.fromValidHex(testVector.encodedFailureMessage) + val expected = FailureMessageCodecs.failureOnionPayload(0).decode(encodedFailureMessage.bits).require.value + val sharedSecrets = testVector.hops.map(hop => ByteVector32(ByteVector.fromValidHex(hop.sharedSecret))).reverse + val encryptedMessage = testVector.hops.map(hop => ByteVector.fromValidHex(hop.encryptedMessage)).last + val nodeIds = (1 to testVector.hops.length).map(_ => randomKey().publicKey) + val Right(DecryptedFailurePacket(originNode, failureMessage)) = AttributableErrorPacket.decrypt(encryptedMessage, sharedSecrets.zip(nodeIds)) + assert(originNode == nodeIds.last) + assert(failureMessage == expected) } test("create blinded route (reference test vector)") { From 363c1677affd3dd916154e4e7867443713b9795c Mon Sep 17 00:00:00 2001 From: Thomas HUET Date: Mon, 6 Nov 2023 16:21:57 +0100 Subject: [PATCH 4/6] 36/37 --- .../src/main/scala/fr/acinq/eclair/Features.scala | 10 +++++----- .../main/scala/fr/acinq/eclair/crypto/Sphinx.scala | 4 +++- .../scala/fr/acinq/eclair/crypto/SphinxSpec.scala | 6 +++--- .../integration/basic/payment/OfferPaymentSpec.scala | 12 ++++++------ 4 files changed, 17 insertions(+), 15 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala index 50f3b5d11a..02fc2fe129 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala @@ -264,17 +264,17 @@ object Features { val mandatory = 28 } - case object AttributableError extends Feature with InitFeature with NodeFeature with Bolt11Feature { - val rfcName = "option_attributable_error" - val mandatory = 30 - } - // TODO: this should also extend NodeFeature once the spec is finalized case object Quiescence extends Feature with InitFeature { val rfcName = "option_quiesce" val mandatory = 34 } + case object AttributableError extends Feature with InitFeature with NodeFeature with Bolt11Feature { + val rfcName = "option_attributable_error" + val mandatory = 36 + } + case object OnionMessages extends Feature with InitFeature with NodeFeature { val rfcName = "option_onion_messages" val mandatory = 38 diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala index d58b5298c4..7fe8bd05ab 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala @@ -376,7 +376,9 @@ object Sphinx extends Logging { def wrapOrCreate(errorPacket: ByteVector, sharedSecret: ByteVector32, holdTime: FiniteDuration): ByteVector = wrap(errorPacket, sharedSecret, holdTime, isSource = false) match { - case Failure(_) => create(sharedSecret, TemporaryNodeFailure(), holdTime) + case Failure(_) => + // There is no failure message for this use-case, using TemporaryNodeFailure instead. + create(sharedSecret, TemporaryNodeFailure(), holdTime) case Success(value) => value } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala index da5225800c..2560821fdf 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala @@ -437,15 +437,15 @@ class SphinxSpec extends AnyFunSuite { ).map(ByteVector32(_)) // publicKeys(2) creates an invalid random packet, or publicKeys(1) tries to shift blame by pretending to receive random data from publicKeys(2) - val packet1 = randomBytes(12599) + val packet1 = randomBytes(1200) val hopPayload2 = AttributableError.HopPayload(isPayloadSource = false, 50 millis) val Success(packet2) = AttributableErrorPacket.wrap(packet1, sharedSecrets(1), 50 millis, isSource = false) - assert(packet2.length == 12599) + assert(packet2.length == 1200) val hopPayload3 = AttributableError.HopPayload(isPayloadSource = false, 100 millis) val Success(packet3) = AttributableErrorPacket.wrap(packet2, sharedSecrets(0), 100 millis, isSource = false) - assert(packet3.length == 12599) + assert(packet3.length == 1200) val Left(decryptionError) = AttributableErrorPacket.decrypt(packet3, (0 to 4).map(i => (sharedSecrets(i), publicKeys(i)))) val expected = InvalidAttributableErrorPacket(Seq((publicKeys(0), hopPayload3), (publicKeys(1), hopPayload2)), publicKeys(2)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/payment/OfferPaymentSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/payment/OfferPaymentSpec.scala index e45c3c0c75..f416b22b3e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/payment/OfferPaymentSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/payment/OfferPaymentSpec.scala @@ -24,7 +24,7 @@ import com.softwaremill.quicklens.ModifyPimp import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.bitcoin.scalacompat.{ByteVector32, SatoshiLong} import fr.acinq.eclair.FeatureSupport.Optional -import fr.acinq.eclair.Features.{KeySend, RouteBlinding} +import fr.acinq.eclair.Features.{AttributableError, KeySend, RouteBlinding} import fr.acinq.eclair.channel.{DATA_NORMAL, RealScidStatus} import fr.acinq.eclair.integration.basic.fixtures.MinimalNodeFixture import fr.acinq.eclair.integration.basic.fixtures.MinimalNodeFixture.{connect, getChannelData, getPeerChannels, getRouterData, knownFundingTxs, nodeParamsFor, openChannel, watcherAutopilot} @@ -34,14 +34,11 @@ import fr.acinq.eclair.payment._ import fr.acinq.eclair.payment.offer.OfferManager import fr.acinq.eclair.payment.receive.MultiPartHandler.{DummyBlindedHop, ReceivingRoute} import fr.acinq.eclair.payment.send.PaymentInitiator.{SendPaymentToNode, SendSpontaneousPayment} -import fr.acinq.eclair.payment.send.{ClearRecipient, OfferPayment, PaymentLifecycle} -import fr.acinq.eclair.router.Router +import fr.acinq.eclair.payment.send.{OfferPayment, PaymentLifecycle} import fr.acinq.eclair.testutils.FixtureSpec import fr.acinq.eclair.wire.protocol.OfferTypes.{Offer, OfferPaths} -import fr.acinq.eclair.wire.protocol.{IncorrectOrUnknownPaymentDetails, InvalidOnionBlinding} -import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Features, MilliSatoshi, MilliSatoshiLong, ShortChannelId, randomBytes32, randomKey} import fr.acinq.eclair.wire.protocol.{IncorrectOrUnknownPaymentDetails, InvalidOnionBlinding, OfferTypes} -import fr.acinq.eclair.{CltvExpiryDelta, Features, MilliSatoshi, MilliSatoshiLong, randomBytes32, randomKey} +import fr.acinq.eclair.{CltvExpiryDelta, Features, MilliSatoshi, MilliSatoshiLong, ShortChannelId, randomBytes32, randomKey} import org.scalatest.concurrent.IntegrationPatience import org.scalatest.{Tag, TestData} import scodec.bits.HexStringSyntax @@ -64,16 +61,19 @@ class OfferPaymentSpec extends FixtureSpec with IntegrationPatience { val aliceParams = nodeParamsFor("alice", ByteVector32(hex"b4acd47335b25ab7b84b8c020997b12018592bb4631b868762154d77fa8b93a3")) .modify(_.onionMessageConfig.timeout).setTo(5 minutes) .modify(_.features.activated).using(_ + (RouteBlinding -> Optional)) + .modify(_.features.activated).using(_ - AttributableError) .modify(_.channelConf.channelFlags.announceChannel).setTo(!testData.tags.contains(PrivateChannels)) val bobParams = nodeParamsFor("bob", ByteVector32(hex"7620226fec887b0b2ebe76492e5a3fd3eb0e47cd3773263f6a81b59a704dc492")) .modify(_.onionMessageConfig.timeout).setTo(5 minutes) .modify(_.features.activated).using(_ + (RouteBlinding -> Optional)) + .modify(_.features.activated).using(_ - AttributableError) .modify(_.features.activated).usingIf(testData.tags.contains(RouteBlindingDisabledBob))(_ - RouteBlinding) .modify(_.channelConf.channelFlags.announceChannel).setTo(!testData.tags.contains(PrivateChannels)) val carolParams = nodeParamsFor("carol", ByteVector32(hex"ebd5a5d3abfb3ef73731eb3418d918f247445183180522674666db98a66411cc")) .modify(_.onionMessageConfig.timeout).setTo(5 minutes) .modify(_.features.activated).using(_ + (RouteBlinding -> Optional)) .modify(_.features.activated).using(_ + (KeySend -> Optional)) + .modify(_.features.activated).using(_ - AttributableError) .modify(_.features.activated).usingIf(testData.tags.contains(RouteBlindingDisabledCarol))(_ - RouteBlinding) .modify(_.channelConf.channelFlags.announceChannel).setTo(!testData.tags.contains(PrivateChannels)) From bfdb39e40e70270e407f0c21fb16941ef086a6c5 Mon Sep 17 00:00:00 2001 From: Thomas HUET Date: Tue, 7 Nov 2023 13:30:56 +0100 Subject: [PATCH 5/6] Default to legacy --- .../scala/fr/acinq/eclair/channel/fsm/Channel.scala | 12 ++++++------ .../payment/relay/PostRestartHtlcCleaner.scala | 12 ++++++------ .../fr/acinq/eclair/payment/relay/Relayer.scala | 8 ++++---- .../integration/basic/payment/OfferPaymentSpec.scala | 5 +---- 4 files changed, 17 insertions(+), 20 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala index f27ba1726e..92aaacbfe6 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala @@ -640,8 +640,8 @@ 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. - // The HTLC is rejected without reading the onion, we default to using attributable errors if the feature is activated. - self ! CMD_FAIL_HTLC(add.id, Right(TemporaryChannelFailure(d.channelUpdate)), useAttributableErrors = nodeParams.features.hasFeature(Features.AttributableError), TimestampMilli.now(), commit = true) + // The HTLC is rejected without reading the onion, we default to legacy errors. + 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 @@ -1342,13 +1342,13 @@ 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) - // The HTLC is rejected without reading the onion, we default to using attributable errors if the feature is activated. - self ! CMD_FAIL_HTLC(add.id, Right(PermanentChannelFailure()), useAttributableErrors = nodeParams.features.hasFeature(Features.AttributableError), TimestampMilli.now(), commit = true) + // The HTLC is rejected without reading the onion, we default to legacy errors. + 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) - // The HTLC is rejected without reading the onion, we default to using attributable errors if the feature is activated. - self ! CMD_FAIL_HTLC(add.id, Right(PermanentChannelFailure()), useAttributableErrors = nodeParams.features.hasFeature(Features.AttributableError), TimestampMilli.now(), commit = true) + // The HTLC is rejected without reading the onion, we default to legacy errors. + 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 diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/PostRestartHtlcCleaner.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/PostRestartHtlcCleaner.scala index d84a265600..1ec5655be4 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/PostRestartHtlcCleaner.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/PostRestartHtlcCleaner.scala @@ -131,8 +131,8 @@ class PostRestartHtlcCleaner(nodeParams: NodeParams, register: ActorRef, initial val failure = InvalidOnionBlinding(ByteVector32.Zeroes) CMD_FAIL_MALFORMED_HTLC(htlc.id, failure.onionHash, failure.code, commit = true) case None => - // By default we use attributable errors if the feature is activated. - CMD_FAIL_HTLC(htlc.id, Right(TemporaryNodeFailure()), useAttributableErrors = nodeParams.features.hasFeature(Features.AttributableError), TimestampMilli.min, commit = true) + // By default we use legacy errors. + CMD_FAIL_HTLC(htlc.id, Right(TemporaryNodeFailure()), useAttributableErrors = false, TimestampMilli.min, commit = true) } channel ! cmd } else { @@ -262,8 +262,8 @@ class PostRestartHtlcCleaner(nodeParams: NodeParams, register: ActorRef, initial val failure = InvalidOnionBlinding(ByteVector32.Zeroes) CMD_FAIL_MALFORMED_HTLC(originHtlcId, failure.onionHash, failure.code, commit = true) case None => - // By default we use attributable errors if the feature is activated. - ChannelRelay.translateRelayFailure(originHtlcId, fail, useAttributableErrors = nodeParams.features.hasFeature(Features.AttributableError), TimestampMilli.min) + // By default we use legacy errors. + ChannelRelay.translateRelayFailure(originHtlcId, fail, useAttributableErrors = false, TimestampMilli.min) } PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, originChannelId, cmd) case Origin.TrampolineRelayedCold(origins) => @@ -272,8 +272,8 @@ class PostRestartHtlcCleaner(nodeParams: NodeParams, register: ActorRef, initial Metrics.Resolved.withTag(Tags.Success, value = false).withTag(Metrics.Relayed, value = true).increment() // We don't bother decrypting the downstream failure to forward a more meaningful error upstream, it's // very likely that it won't be actionable anyway because of our node restart. - // By default we use attributable errors if the feature is activated. - PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, channelId, CMD_FAIL_HTLC(htlcId, Right(TemporaryNodeFailure()), useAttributableErrors = nodeParams.features.hasFeature(Features.AttributableError), TimestampMilli.min, commit = true)) + // By default we use legacy errors. + PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, channelId, CMD_FAIL_HTLC(htlcId, Right(TemporaryNodeFailure()), useAttributableErrors = false, TimestampMilli.min, commit = true)) } } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala index fc30c729e0..35c421e400 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala @@ -84,8 +84,8 @@ class Relayer(nodeParams: NodeParams, router: ActorRef, register: ActorRef, paym // We are the introduction point of a blinded path: we add a non-negligible delay to make it look like it // could come from a downstream node. val delay = Some(500.millis + Random.nextLong(1500).millis) - // By default we use attributable errors if the feature is activated. - CMD_FAIL_HTLC(add.id, Right(InvalidOnionBlinding(badOnion.onionHash)), useAttributableErrors = nodeParams.features.hasFeature(Features.AttributableError), TimestampMilli.now(), delay, commit = true) + // By default we use legacy errors. + CMD_FAIL_HTLC(add.id, Right(InvalidOnionBlinding(badOnion.onionHash)), useAttributableErrors = false, TimestampMilli.now(), delay, commit = true) case _ => CMD_FAIL_MALFORMED_HTLC(add.id, badOnion.onionHash, badOnion.code, commit = true) } @@ -93,8 +93,8 @@ class Relayer(nodeParams: NodeParams, router: ActorRef, register: ActorRef, paym PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, add.channelId, cmdFail) case Left(failure) => log.warning(s"rejecting htlc #${add.id} from channelId=${add.channelId} reason=$failure") - // By default we use attributable errors if the feature is activated. - val cmdFail = CMD_FAIL_HTLC(add.id, Right(failure), useAttributableErrors = nodeParams.features.hasFeature(Features.AttributableError), TimestampMilli.now(), commit = true) + // By default we use legacy errors. + val cmdFail = CMD_FAIL_HTLC(add.id, Right(failure), useAttributableErrors = false, TimestampMilli.now(), commit = true) PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, add.channelId, cmdFail) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/payment/OfferPaymentSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/payment/OfferPaymentSpec.scala index f416b22b3e..4a8de21eb5 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/payment/OfferPaymentSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/payment/OfferPaymentSpec.scala @@ -24,7 +24,7 @@ import com.softwaremill.quicklens.ModifyPimp import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.bitcoin.scalacompat.{ByteVector32, SatoshiLong} import fr.acinq.eclair.FeatureSupport.Optional -import fr.acinq.eclair.Features.{AttributableError, KeySend, RouteBlinding} +import fr.acinq.eclair.Features.{KeySend, RouteBlinding} import fr.acinq.eclair.channel.{DATA_NORMAL, RealScidStatus} import fr.acinq.eclair.integration.basic.fixtures.MinimalNodeFixture import fr.acinq.eclair.integration.basic.fixtures.MinimalNodeFixture.{connect, getChannelData, getPeerChannels, getRouterData, knownFundingTxs, nodeParamsFor, openChannel, watcherAutopilot} @@ -61,19 +61,16 @@ class OfferPaymentSpec extends FixtureSpec with IntegrationPatience { val aliceParams = nodeParamsFor("alice", ByteVector32(hex"b4acd47335b25ab7b84b8c020997b12018592bb4631b868762154d77fa8b93a3")) .modify(_.onionMessageConfig.timeout).setTo(5 minutes) .modify(_.features.activated).using(_ + (RouteBlinding -> Optional)) - .modify(_.features.activated).using(_ - AttributableError) .modify(_.channelConf.channelFlags.announceChannel).setTo(!testData.tags.contains(PrivateChannels)) val bobParams = nodeParamsFor("bob", ByteVector32(hex"7620226fec887b0b2ebe76492e5a3fd3eb0e47cd3773263f6a81b59a704dc492")) .modify(_.onionMessageConfig.timeout).setTo(5 minutes) .modify(_.features.activated).using(_ + (RouteBlinding -> Optional)) - .modify(_.features.activated).using(_ - AttributableError) .modify(_.features.activated).usingIf(testData.tags.contains(RouteBlindingDisabledBob))(_ - RouteBlinding) .modify(_.channelConf.channelFlags.announceChannel).setTo(!testData.tags.contains(PrivateChannels)) val carolParams = nodeParamsFor("carol", ByteVector32(hex"ebd5a5d3abfb3ef73731eb3418d918f247445183180522674666db98a66411cc")) .modify(_.onionMessageConfig.timeout).setTo(5 minutes) .modify(_.features.activated).using(_ + (RouteBlinding -> Optional)) .modify(_.features.activated).using(_ + (KeySend -> Optional)) - .modify(_.features.activated).using(_ - AttributableError) .modify(_.features.activated).usingIf(testData.tags.contains(RouteBlindingDisabledCarol))(_ - RouteBlinding) .modify(_.channelConf.channelFlags.announceChannel).setTo(!testData.tags.contains(PrivateChannels)) From df0dab1ae6637e5cd99dc969c5199739d61ce688 Mon Sep 17 00:00:00 2001 From: Thomas HUET Date: Tue, 7 Nov 2023 13:46:12 +0100 Subject: [PATCH 6/6] Fill with zeros if the error to relay is too short --- .../scala/fr/acinq/eclair/crypto/Sphinx.scala | 23 ++++++++----------- .../acinq/eclair/payment/PaymentPacket.scala | 2 +- .../wire/protocol/AttributableError.scala | 6 +++++ .../fr/acinq/eclair/crypto/SphinxSpec.scala | 8 +++---- 4 files changed, 20 insertions(+), 19 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala index 7fe8bd05ab..191f062204 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala @@ -20,7 +20,7 @@ import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.scalacompat.{ByteVector32, Crypto} import fr.acinq.eclair.wire.protocol._ import grizzled.slf4j.Logging -import scodec.Attempt +import scodec.{Attempt, DecodeResult} import scodec.bits.ByteVector import scala.annotation.tailrec @@ -347,10 +347,10 @@ object Sphinx extends Logging { 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 zeroPayloads = Seq.fill(maxNumHop)(ByteVector.low(hopPayloadLength)) + 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 + wrap(plainError, sharedSecret, holdTime, isSource = true) } private def computeHmacs(mac: Mac32, failurePayload: ByteVector, hopPayloads: Seq[ByteVector], hmacs: Seq[Seq[ByteVector]], minNumHop: Int): Seq[ByteVector] = { @@ -363,9 +363,12 @@ object Sphinx extends Logging { newHmacs } - def wrap(errorPacket: ByteVector, sharedSecret: ByteVector32, holdTime: FiniteDuration, isSource: Boolean): Try[ByteVector] = Try { + def wrap(errorPacket: ByteVector, sharedSecret: ByteVector32, holdTime: FiniteDuration, isSource: Boolean): ByteVector = { val um = generateKey("um", sharedSecret) - val error = attributableErrorCodec(errorPacket.length.toInt, hopPayloadLength, maxNumHop).decode(errorPacket.bits).require.value + val error = attributableErrorCodec(errorPacket.length.toInt, hopPayloadLength, maxNumHop).decode(errorPacket.bits) match { + case Attempt.Successful(DecodeResult(value, _)) => value + case Attempt.Failure(_) => AttributableError.zero(payloadAndPadLength, hopPayloadLength, maxNumHop) + } 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 @@ -374,14 +377,6 @@ object Sphinx extends Logging { newError xor stream } - def wrapOrCreate(errorPacket: ByteVector, sharedSecret: ByteVector32, holdTime: FiniteDuration): ByteVector = - wrap(errorPacket, sharedSecret, holdTime, isSource = false) match { - case Failure(_) => - // There is no failure message for this use-case, using TemporaryNodeFailure instead. - 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) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala index da0395a3e2..28cb41364b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala @@ -314,7 +314,7 @@ object OutgoingPaymentPacket { 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 Left(forwarded) if useAttributableErrors => Sphinx.AttributableErrorPacket.wrap(forwarded, sharedSecret, holdTime, isSource = false) 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) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/AttributableError.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/AttributableError.scala index 785d14d094..79c8001336 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/AttributableError.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/AttributableError.scala @@ -48,4 +48,10 @@ object AttributableError { (("failure_payload" | bytes(totalLength - metadataLength)) :: ("hop_payloads" | listOfN(provide(maxNumHop), bytes(hopPayloadLength)).xmap[Seq[ByteVector]](_.toSeq, _.toList)) :: ("hmacs" | hmacsCodec(maxNumHop))).as[AttributableError].complete} + + def zero(payloadAndPadLength: Int, hopPayloadLength: Int, maxNumHop: Int): AttributableError = + AttributableError( + ByteVector.low(payloadAndPadLength), + Seq.fill(maxNumHop)(ByteVector.low(hopPayloadLength)), + maxNumHop.to(1, -1).map(Seq.fill(_)(ByteVector.low(4)))) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala index 2560821fdf..03d96ac657 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala @@ -414,13 +414,13 @@ class SphinxSpec extends AnyFunSuite { val Right(decrypted1) = AttributableErrorPacket.decrypt(packet1, (2 to 4).map(i => (sharedSecrets(i), publicKeys(i)))) assert(decrypted1 == expected) - val Success(packet2) = AttributableErrorPacket.wrap(packet1, sharedSecrets(1), 5 millis, isSource = false) + val packet2 = AttributableErrorPacket.wrap(packet1, sharedSecrets(1), 5 millis, isSource = false) assert(packet2.length == 1200) val Right(decrypted2) = AttributableErrorPacket.decrypt(packet2, (1 to 4).map(i => (sharedSecrets(i), publicKeys(i)))) assert(decrypted2 == expected) - val Success(packet3) = AttributableErrorPacket.wrap(packet2, sharedSecrets(0), 9 millis, isSource = false) + val packet3 = AttributableErrorPacket.wrap(packet2, sharedSecrets(0), 9 millis, isSource = false) assert(packet3.length == 1200) val Right(decrypted3) = AttributableErrorPacket.decrypt(packet3, (0 to 4).map(i => (sharedSecrets(i), publicKeys(i)))) @@ -440,11 +440,11 @@ class SphinxSpec extends AnyFunSuite { val packet1 = randomBytes(1200) val hopPayload2 = AttributableError.HopPayload(isPayloadSource = false, 50 millis) - val Success(packet2) = AttributableErrorPacket.wrap(packet1, sharedSecrets(1), 50 millis, isSource = false) + val packet2 = AttributableErrorPacket.wrap(packet1, sharedSecrets(1), 50 millis, isSource = false) assert(packet2.length == 1200) val hopPayload3 = AttributableError.HopPayload(isPayloadSource = false, 100 millis) - val Success(packet3) = AttributableErrorPacket.wrap(packet2, sharedSecrets(0), 100 millis, isSource = false) + val packet3 = AttributableErrorPacket.wrap(packet2, sharedSecrets(0), 100 millis, isSource = false) assert(packet3.length == 1200) val Left(decryptionError) = AttributableErrorPacket.decrypt(packet3, (0 to 4).map(i => (sharedSecrets(i), publicKeys(i))))