From 66c9499da43acd130e86b75cbfe9a668020aec21 Mon Sep 17 00:00:00 2001 From: t-bast Date: Fri, 19 Jun 2026 10:31:57 +0200 Subject: [PATCH] Add support for fulfillment payload This adds support for including a payload in `update_fulfill_htlc` that the recipient encrypts for the payer. This can be useful to transmit data atomically with the fulfillment of a payment. The main challenge is that intermediate nodes may drop or tamper with this fulfillment payload, which is why we include it in the HMACs of the attribution data, which lets senders detect which pair of nodes may be malicious. See https://github.com/lightning/bolts/pull/1344 --- .../fr/acinq/eclair/channel/ChannelData.scala | 10 +- .../scala/fr/acinq/eclair/crypto/Sphinx.scala | 73 +++++++-- .../fr/acinq/eclair/db/pg/PgAuditDb.scala | 1 + .../eclair/db/sqlite/SqliteAuditDb.scala | 1 + .../acinq/eclair/payment/PaymentEvents.scala | 2 +- .../acinq/eclair/payment/PaymentPacket.scala | 73 ++++++--- .../payment/receive/MultiPartHandler.scala | 4 +- .../eclair/payment/relay/ChannelRelay.scala | 2 +- .../eclair/payment/relay/NodeRelay.scala | 10 +- .../payment/relay/OnTheFlyFunding.scala | 4 +- .../relay/PostRestartHtlcCleaner.scala | 17 ++- .../send/MultiPartPaymentLifecycle.scala | 18 +-- .../payment/send/PaymentInitiator.scala | 4 +- .../payment/send/PaymentLifecycle.scala | 20 ++- .../send/TrampolinePaymentLifecycle.scala | 4 +- .../eclair/wire/internal/CommandCodecs.scala | 18 ++- .../acinq/eclair/wire/protocol/HtlcTlv.scala | 6 + .../wire/protocol/LightningMessageTypes.scala | 1 + .../eclair/channel/CommitmentsSpec.scala | 7 +- .../fr/acinq/eclair/channel/HelpersSpec.scala | 4 +- .../publish/ReplaceableTxPublisherSpec.scala | 8 +- .../ChannelStateTestsHelperMethods.scala | 2 +- .../states/e/NormalQuiescentStateSpec.scala | 4 +- .../channel/states/e/NormalStateSpec.scala | 20 +-- .../channel/states/e/OfflineStateSpec.scala | 6 +- .../channel/states/f/ShutdownStateSpec.scala | 22 +-- .../channel/states/h/ClosingStateSpec.scala | 30 ++-- .../fr/acinq/eclair/crypto/SphinxSpec.scala | 44 +++++- .../fr/acinq/eclair/db/AuditDbSpec.scala | 8 +- .../fr/acinq/eclair/db/PaymentsDbSpec.scala | 6 +- .../eclair/db/PendingCommandsDbSpec.scala | 9 +- .../integration/ChannelIntegrationSpec.scala | 4 +- .../eclair/payment/MultiPartHandlerSpec.scala | 14 +- .../MultiPartPaymentLifecycleSpec.scala | 12 +- .../eclair/payment/PaymentInitiatorSpec.scala | 4 +- .../eclair/payment/PaymentLifecycleSpec.scala | 2 +- .../eclair/payment/PaymentPacketSpec.scala | 140 +++++++++++++++++- .../payment/PostRestartHtlcCleanerSpec.scala | 20 +-- .../payment/relay/NodeRelayerSpec.scala | 44 +++--- .../eclair/profit/PeerStatsTrackerSpec.scala | 4 +- .../wire/internal/CommandCodecsSpec.scala | 15 +- .../fr/acinq/eclair/api/ApiServiceSpec.scala | 4 +- 42 files changed, 490 insertions(+), 211 deletions(-) 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 49b33b287b..e11f75f62b 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 @@ -242,7 +242,7 @@ case class FailureAttributionData(htlcReceivedAt: TimestampMilli, trampolineRece case class FulfillAttributionData(htlcReceivedAt: TimestampMilli, trampolineReceivedAt_opt: Option[TimestampMilli], downstreamAttribution_opt: Option[ByteVector]) sealed trait HtlcSettlementCommand extends HasOptionalReplyToCommand with ForbiddenCommandDuringQuiescenceNegotiation with ForbiddenCommandWhenQuiescent { def id: Long } -final case class CMD_FULFILL_HTLC(id: Long, r: ByteVector32, attribution_opt: Option[FulfillAttributionData], commit: Boolean = false, replyTo_opt: Option[ActorRef] = None) extends HtlcSettlementCommand +final case class CMD_FULFILL_HTLC(id: Long, r: ByteVector32, fulfillmentPayload_opt: Option[ByteVector], attribution_opt: Option[FulfillAttributionData], commit: Boolean = false, replyTo_opt: Option[ActorRef] = None) extends HtlcSettlementCommand final case class CMD_FAIL_HTLC(id: Long, reason: FailureReason, attribution_opt: Option[FailureAttributionData], delay_opt: Option[FiniteDuration] = None, commit: Boolean = false, replyTo_opt: Option[ActorRef] = None) extends HtlcSettlementCommand final case class CMD_FAIL_MALFORMED_HTLC(id: Long, onionHash: ByteVector32, failureCode: Int, commit: Boolean = false, replyTo_opt: Option[ActorRef] = None) extends HtlcSettlementCommand final case class CMD_UPDATE_FEE(feeratePerKw: FeeratePerKw, commit: Boolean = false, replyTo_opt: Option[ActorRef] = None) extends HasOptionalReplyToCommand with ForbiddenCommandDuringQuiescenceNegotiation with ForbiddenCommandWhenQuiescent @@ -304,7 +304,13 @@ final case class RES_FAILURE[+C <: Command, +T <: Throwable](cmd: C, t: T) exten final case class RES_ADD_FAILED[+T <: ChannelException](c: CMD_ADD_HTLC, t: T, channelUpdate: Option[ChannelUpdate]) extends CommandFailure[CMD_ADD_HTLC, T] { override def toString = s"cannot add htlc with origin=${c.origin} reason=${t.getMessage}" } sealed trait HtlcResult object HtlcResult { - sealed trait Fulfill extends HtlcResult { def paymentPreimage: ByteVector32 } + sealed trait Fulfill extends HtlcResult { + def paymentPreimage: ByteVector32 + def fulfillmentPayload_opt: Option[ByteVector] = this match { + case RemoteFulfill(fulfill) => fulfill.fulfillmentPayload_opt + case _: OnChainFulfill => None + } + } case class RemoteFulfill(fulfill: UpdateFulfillHtlc) extends Fulfill { override val paymentPreimage: ByteVector32 = fulfill.paymentPreimage } case class OnChainFulfill(paymentPreimage: ByteVector32) extends Fulfill sealed trait Fail extends HtlcResult 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 71efa7867e..2d4c6f8be1 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 @@ -290,7 +290,7 @@ object Sphinx extends Logging { case class HtlcFailure(holdTimes: Seq[HoldTime], failure: Either[CannotDecryptFailurePacket, DecryptedFailurePacket]) - case class HtlcSuccess(holdTimes: Seq[HoldTime], remainingAttribution_opt: Option[ByteVector]) + case class HtlcSuccess(holdTimes: Seq[HoldTime], fulfillmentPayload_opt: Option[ByteVector], remainingAttribution_opt: Option[ByteVector]) object FailurePacket { /** @@ -347,7 +347,7 @@ object Sphinx extends Logging { val downstream = FailureMessageCodecs.failureOnionCodec(Hmac256(um)).decode(packet1.toBitVector) match { // We've identified the failing node: no need to continue decrypting. case Attempt.Successful(value) => HtlcFailure(Nil, Right(DecryptedFailurePacket(ss.remoteNodeId, index, value.value))) - // The failing node may be downstream: we keep decrypting. + // The failing node may be downstream: we keep decrypting. case _ => decrypt(packet1, attribution1_opt.map(_.downstreamAttribution), tail, index + 1) } HtlcFailure(attribution1_opt.map(_.holdTime).toSeq ++ downstream.holdTimes, downstream.failure) @@ -357,26 +357,71 @@ object Sphinx extends Logging { object SuccessPacket { /** - * Decrypt the attribution data provided in the HTLC-success case. + * Create an encrypted fulfillment payload, that will be wrapped by each intermediate node before being returned to + * the sender. Note that the final node (which creates this payload) does *not* apply additional wrapping since this + * is encrypted (unlike what happens for failure messages). + * + * Note that malicious intermediate hops may drop the packet or alter it, but since each intermediate node includes + * the payload they received in their attribution data, the sender will be able to infer who dropped or altered the + * payload. + */ + def create(sharedSecret: ByteVector32, payload: ByteVector): ByteVector = { + val key = generateKey("fulfillment", sharedSecret) + val (encryptedPayload, mac) = ChaCha20Poly1305.encrypt(key, zeroes(12), payload, ByteVector.empty) + encryptedPayload ++ mac + } + + /** + * Wrap the fulfillment payload received from the downstream node in an additional layer of onion encryption. + * Each intermediate node wraps the fulfillment payload until it reaches the original sender. + * Each intermediate node also includes the received fulfillment payload in its attribution data. + */ + def wrap(payload: ByteVector, sharedSecret: ByteVector32): ByteVector = { + val key = generateKey("ammag", sharedSecret) + val stream = generateStream(key, payload.length.toInt) + payload xor stream + } + + /** + * Decrypt the fulfillment payload and attribution data provided in the HTLC-success case. + * Node shared secrets are applied until we reach the recipient's shared secret, where the decryption step differs. * Note that malicious nodes in the route may have altered the packet, triggering a decryption failure. * + * @param payload_opt fulfillment payload. * @param attribution_opt attribution data for this success packet. * @param sharedSecrets nodes shared secrets. + * @param fullRoute must be set to false when decrypting a partial route (e.g. as an intermediate trampoline). */ - def decrypt(attribution_opt: Option[ByteVector], sharedSecrets: Seq[SharedSecret], index: Int = 1): HtlcSuccess = { + def decrypt(payload_opt: Option[ByteVector], attribution_opt: Option[ByteVector], sharedSecrets: Seq[SharedSecret], fullRoute: Boolean = true): HtlcSuccess = { sharedSecrets match { - case Nil => HtlcSuccess(Nil, attribution_opt) + case Nil => HtlcSuccess(Nil, payload_opt, attribution_opt) case ss :: tail => - attribution_opt match { - case None => HtlcSuccess(Nil, None) - case Some(attribution) => - Attribution.decrypt(attribution, None, ss, sharedSecrets.length) match { - case Some(perHopAttribution) => - val downstream = decrypt(Some(perHopAttribution.downstreamAttribution), tail, index + 1) - HtlcSuccess(perHopAttribution.holdTime +: downstream.holdTimes, downstream.remainingAttribution_opt) - case None => HtlcSuccess(Nil, Some(attribution)) - } + // We start by unwrapping the fulfillment payload, if provided. + val isFinalNode = tail.isEmpty && fullRoute + val unwrappedPayload_opt = payload_opt match { + case Some(payload) if isFinalNode => + // We decrypt the payload provided by the final node. + val key = generateKey("fulfillment", ss.secret) + Try(ChaCha20Poly1305.decrypt(key, zeroes(12), payload.dropRight(16), ByteVector.empty, payload.takeRight(16))).toOption + case Some(payload) => + // We peel the wrapping added by the intermediate node. + Some(wrap(payload, ss.secret)) + case None => None } + // We decrypt the attribution data provided by this node: its HMACs must cover the unwrapped fulfillment payload. + // The code below is quite subtle, because nodes inside a blinded path don't include attribution data, so when + // we reach that point in the recursion, attribution decryption will fail, which is expected. + // After that, attribution_opt will be set to None for recursive calls, because there is no point trying to + // decrypt attribution data that wasn't actually provided or that was tampered with. + // Note that if an intermediate node tampered with the attribution data, it will have the same effect: we will + // stop processing attribution after that node. + // The caller can look at the reported hold times to know which nodes provided valid attribution data: this + // allows identifying which nodes are acting maliciously, if any. + // We keep processing the fulfillment payload recursively though, because we need to use all shared secrets + // to decrypt it. + val attribution1_opt = attribution_opt.flatMap(attribution => Attribution.decrypt(attribution, if (!isFinalNode) unwrappedPayload_opt else None, ss, sharedSecrets.length)) + val downstream = decrypt(unwrappedPayload_opt, attribution1_opt.map(_.downstreamAttribution), tail, fullRoute) + HtlcSuccess(attribution1_opt.map(_.holdTime).toSeq ++ downstream.holdTimes, downstream.fulfillmentPayload_opt, downstream.remainingAttribution_opt) } } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgAuditDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgAuditDb.scala index aefead91c3..c918167ca2 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgAuditDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgAuditDb.scala @@ -592,6 +592,7 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging { PublicKey(rs.getByteVectorFromHex("recipient_node_id")), Seq(part), None, + None, part.startedAt) } sentByParentId + (parentId -> sent) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala index cd36b109eb..e570132e26 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala @@ -563,6 +563,7 @@ class SqliteAuditDb(val sqlite: Connection) extends AuditDb with Logging { PublicKey(rs.getByteVectorFromHex("recipient_node_id")), Seq(part), None, + None, part.startedAt) } sentByParentId + (parentId -> sent) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala index f8553a811a..0aafcc9f9c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala @@ -79,7 +79,7 @@ object PaymentEvent { * @param parts child payments (actual outgoing HTLCs). * @param remainingAttribution_opt for relayed trampoline payments, the attribution data that needs to be sent upstream */ -case class PaymentSent(id: UUID, paymentPreimage: ByteVector32, recipientAmount: MilliSatoshi, recipientNodeId: PublicKey, parts: Seq[PaymentSent.PaymentPart], remainingAttribution_opt: Option[ByteVector], startedAt: TimestampMilli) extends PaymentEvent { +case class PaymentSent(id: UUID, paymentPreimage: ByteVector32, recipientAmount: MilliSatoshi, recipientNodeId: PublicKey, parts: Seq[PaymentSent.PaymentPart], fulfillmentPayload_opt: Option[ByteVector], remainingAttribution_opt: Option[ByteVector], startedAt: TimestampMilli) extends PaymentEvent { require(parts.nonEmpty, "must have at least one payment part") val paymentHash: ByteVector32 = Crypto.sha256(paymentPreimage) val amountWithFees: MilliSatoshi = parts.map(_.amountWithFees).sum 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 8046fb006c..0bd96bbecf 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 @@ -356,7 +356,7 @@ object OutgoingPaymentPacket { } } - private case class HtlcSharedSecrets(outerOnionSecret: ByteVector32, trampolineOnionSecret_opt: Option[ByteVector32], blinded: Boolean) + private case class HtlcSharedSecrets(outerOnionSecret: ByteVector32, trampolineOnionSecret_opt: Option[ByteVector32], blinded: Boolean, isFinalNode: Boolean) /** * We decrypt the onion again to extract the shared secret(s) used to encrypt onion failures. @@ -365,24 +365,32 @@ object OutgoingPaymentPacket { * It's simpler to extract it again from the encrypted onion. */ private def extractSharedSecret(nodeSecret: PrivateKey, add: UpdateAddHtlc): Either[CannotExtractSharedSecret, HtlcSharedSecrets] = { - Sphinx.peel(nodeSecret, Some(add.paymentHash), add.onionRoutingPacket) match { - case Right(Sphinx.DecryptedPacket(payload, _, outerOnionSecret)) => + val outerOnionDecryptionKey = add.pathKey_opt match { + case Some(blinding) => Sphinx.RouteBlinding.derivePrivateKey(nodeSecret, blinding) + case None => nodeSecret + } + Sphinx.peel(outerOnionDecryptionKey, Some(add.paymentHash), add.onionRoutingPacket) match { + case Right(packet@Sphinx.DecryptedPacket(payload, _, outerOnionSecret)) => // Let's look at the onion payload to see if it contains a trampoline onion. PaymentOnionCodecs.perHopPayloadCodec.decode(payload.bits) match { case Attempt.Successful(DecodeResult(perHopPayload, _)) => // We try to extract the trampoline shared secret, if we can find one. - val trampolineOnionSecret_opt = perHopPayload.get[OnionPaymentPayloadTlv.TrampolineOnion].map(_.packet).flatMap(trampolinePacket => { + val trampolinePacket_opt = perHopPayload.get[OnionPaymentPayloadTlv.TrampolineOnion].map(_.packet).flatMap(trampolinePacket => { val trampolinePathKey_opt = perHopPayload.get[OnionPaymentPayloadTlv.PathKey].map(_.publicKey) val trampolineOnionDecryptionKey = trampolinePathKey_opt.map(pathKey => Sphinx.RouteBlinding.derivePrivateKey(nodeSecret, pathKey)).getOrElse(nodeSecret) - Sphinx.peel(trampolineOnionDecryptionKey, Some(add.paymentHash), trampolinePacket).toOption.map(_.sharedSecret) + Sphinx.peel(trampolineOnionDecryptionKey, Some(add.paymentHash), trampolinePacket).toOption }) // We check if we are an intermediate node in a blinded (potentially trampoline) path. - val blinded = trampolineOnionSecret_opt match { + val blinded = trampolinePacket_opt match { case Some(_) => perHopPayload.get[OnionPaymentPayloadTlv.PathKey].nonEmpty case None => add.pathKey_opt.nonEmpty } - Right(HtlcSharedSecrets(outerOnionSecret, trampolineOnionSecret_opt, blinded)) - case Attempt.Failure(_) => Right(HtlcSharedSecrets(outerOnionSecret, None, blinded = add.pathKey_opt.nonEmpty)) + val isFinalNode = trampolinePacket_opt match { + case Some(trampolinePacket) => trampolinePacket.isLastPacket + case None => packet.isLastPacket + } + Right(HtlcSharedSecrets(outerOnionSecret, trampolinePacket_opt.map(_.sharedSecret), blinded, isFinalNode)) + case Attempt.Failure(_) => Right(HtlcSharedSecrets(outerOnionSecret, None, blinded = add.pathKey_opt.nonEmpty, isFinalNode = packet.isLastPacket)) } case Left(_) => Left(CannotExtractSharedSecret(add.channelId, add)) } @@ -423,24 +431,43 @@ object OutgoingPaymentPacket { } } + private def wrapFulfillmentPayload(cmd: CMD_FULFILL_HTLC, sharedSecret: ByteVector32, isFinalNode: Boolean): Option[ByteVector] = { + if (isFinalNode) { + // The final node encrypts the fulfillment payload without applying the wrapping step. + cmd.fulfillmentPayload_opt.map(p => Sphinx.SuccessPacket.create(sharedSecret, p)) + } else { + // Intermediate nodes wrap the downstream fulfillment payload with their shared secret. + cmd.fulfillmentPayload_opt.map(p => Sphinx.SuccessPacket.wrap(p, sharedSecret)) + } + } + def buildHtlcFulfill(nodeSecret: PrivateKey, useAttributionData: Boolean, cmd: CMD_FULFILL_HTLC, add: UpdateAddHtlc, now: TimestampMilli = TimestampMilli.now()): UpdateFulfillHtlc = { - // If we are part of a blinded route, we must not include any attribution data. - val attributionData_opt = add.pathKey_opt match { - case None if useAttributionData => - val trampolineHoldTime = cmd.attribution_opt.flatMap(_.trampolineReceivedAt_opt).map(receivedAt => now - receivedAt).getOrElse(0 millisecond) - val holdTime = cmd.attribution_opt.map(a => now - a.htlcReceivedAt).getOrElse(0 millisecond) - extractSharedSecret(nodeSecret, add) match { - case Right(HtlcSharedSecrets(outerOnionSecret, None, _)) => - Some(Sphinx.Attribution.create(cmd.attribution_opt.flatMap(_.downstreamAttribution_opt), None, holdTime, outerOnionSecret)) - case Right(HtlcSharedSecrets(outerOnionSecret, Some(trampolineOnionSecret), blinded)) if !blinded => - val trampolineAttribution = Sphinx.Attribution.create(cmd.attribution_opt.flatMap(_.downstreamAttribution_opt), None, trampolineHoldTime, trampolineOnionSecret) - Some(Sphinx.Attribution.create(Some(trampolineAttribution), None, holdTime, outerOnionSecret)) - case _ => None - } - case _ => None + // Note that if we are part of a blinded route, we must not include any attribution data. + // But we must wrap the fulfillment payload in all cases to ensure that the sender receives it. + val downstreamAttribution_opt = cmd.attribution_opt.flatMap(_.downstreamAttribution_opt) + val trampolineHoldTime = cmd.attribution_opt.flatMap(_.trampolineReceivedAt_opt).map(receivedAt => now - receivedAt).getOrElse(0 millisecond) + val holdTime = cmd.attribution_opt.map(a => now - a.htlcReceivedAt).getOrElse(0 millisecond) + val (attributionData_opt, fulfillmentPayload_opt) = extractSharedSecret(nodeSecret, add) match { + case Right(HtlcSharedSecrets(outerOnionSecret, None, blinded, isFinalNode)) => + // The final node doesn't include its fulfillment payload in the attribution HMACs: it already has its own MAC. + val attribution = Sphinx.Attribution.create(downstreamAttribution_opt, if (!isFinalNode) cmd.fulfillmentPayload_opt else None, holdTime, outerOnionSecret) + val attribution_opt = if (useAttributionData && !blinded) Some(attribution) else None + val fulfillmentPayload_opt = wrapFulfillmentPayload(cmd, outerOnionSecret, isFinalNode) + (attribution_opt, fulfillmentPayload_opt) + case Right(HtlcSharedSecrets(outerOnionSecret, Some(trampolineOnionSecret), blinded, isFinalNode)) => + // We do a first pass with the trampoline shared secret. + val trampolineAttribution = Sphinx.Attribution.create(downstreamAttribution_opt, if (!isFinalNode) cmd.fulfillmentPayload_opt else None, trampolineHoldTime, trampolineOnionSecret) + val trampolineFulfillmentPayload_opt = wrapFulfillmentPayload(cmd, trampolineOnionSecret, isFinalNode) + // Then a second pass with the outer onion shared secret. + val attribution = Sphinx.Attribution.create(Some(trampolineAttribution), trampolineFulfillmentPayload_opt, holdTime, outerOnionSecret) + val attribution_opt = if (useAttributionData && !blinded) Some(attribution) else None + val fulfillmentPayload_opt = trampolineFulfillmentPayload_opt.map(p => Sphinx.SuccessPacket.wrap(p, outerOnionSecret)) + (attribution_opt, fulfillmentPayload_opt) + case Left(_) => (None, None) } val tlvs: Set[UpdateFulfillHtlcTlv] = Set( - attributionData_opt.map(UpdateFulfillHtlcTlv.AttributionData(_)) + attributionData_opt.map(UpdateFulfillHtlcTlv.AttributionData(_)), + fulfillmentPayload_opt.map(UpdateFulfillHtlcTlv.FulfillmentPayload(_)), ).flatten UpdateFulfillHtlc(add.channelId, cmd.id, cmd.r, TlvStream(tlvs)) } 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 6c9f271c6d..e45756bb8e 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 @@ -190,7 +190,7 @@ class MultiPartHandler(nodeParams: NodeParams, register: ActorRef, db: IncomingP val received = PaymentReceived(paymentHash, PaymentEvent.IncomingPayment(p.htlc.channelId, p.remoteNodeId, p.amount, p.receivedAt) :: Nil) if (db.receiveIncomingPayment(paymentHash, p.amount, received.settledAt)) { val attribution = FulfillAttributionData(htlcReceivedAt = p.receivedAt, trampolineReceivedAt_opt = None, downstreamAttribution_opt = None) - PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, p.htlc.channelId, CMD_FULFILL_HTLC(p.htlc.id, record.paymentPreimage, Some(attribution), commit = true)) + PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, p.htlc.channelId, CMD_FULFILL_HTLC(p.htlc.id, record.paymentPreimage, None, Some(attribution), commit = true)) ctx.system.eventStream.publish(received) } else { val attribution = FailureAttributionData(htlcReceivedAt = p.receivedAt, trampolineReceivedAt_opt = None) @@ -223,7 +223,7 @@ class MultiPartHandler(nodeParams: NodeParams, register: ActorRef, db: IncomingP parts.collect { case p: MultiPartPaymentFSM.HtlcPart => val attribution = FulfillAttributionData(htlcReceivedAt = p.receivedAt, trampolineReceivedAt_opt = None, downstreamAttribution_opt = None) - PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, p.htlc.channelId, CMD_FULFILL_HTLC(p.htlc.id, payment.paymentPreimage, Some(attribution), commit = true)) + PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, p.htlc.channelId, CMD_FULFILL_HTLC(p.htlc.id, payment.paymentPreimage, None, Some(attribution), commit = true)) } postFulfill(received) ctx.system.eventStream.publish(received) 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 4b46c130b8..c89af18ef4 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 @@ -255,7 +255,7 @@ class ChannelRelay private(nodeParams: NodeParams, case HtlcResult.OnChainFulfill(_) => None } val attribution = FulfillAttributionData(htlcReceivedAt = upstream.receivedAt, trampolineReceivedAt_opt = None, downstreamAttribution_opt = downstreamAttribution_opt) - val cmd = CMD_FULFILL_HTLC(upstream.add.id, fulfill.paymentPreimage, Some(attribution), commit = true) + val cmd = CMD_FULFILL_HTLC(upstream.add.id, fulfill.paymentPreimage, fulfill.fulfillmentPayload_opt, Some(attribution), commit = true) val incoming = PaymentEvent.IncomingPayment(upstream.add.channelId, upstream.receivedFrom, upstream.amountIn, upstream.receivedAt) val outgoing = PaymentEvent.OutgoingPayment(htlc.channelId, remoteNodeId, htlc.amountMsat, now) context.system.eventStream ! EventStream.Publish(ChannelPaymentRelayed(htlc.paymentHash, Seq(incoming), Seq(outgoing))) 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 e2b873c40e..f3bcdfc785 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 @@ -389,11 +389,11 @@ class NodeRelay private(nodeParams: NodeParams, Behaviors.receiveMessagePartial { rejectExtraHtlcPartialFunction orElse { // this is the fulfill that arrives from downstream channels - case WrappedPreimageReceived(PreimageReceived(_, paymentPreimage, attribution_opt)) => + case WrappedPreimageReceived(PreimageReceived(_, paymentPreimage, fulfillmentPayload_opt, attribution_opt)) => if (!fulfilledUpstream) { // We want to fulfill upstream as soon as we receive the preimage (even if not all HTLCs have fulfilled downstream). context.log.debug("got preimage from downstream") - fulfillPayment(upstream, paymentPreimage, attribution_opt) + fulfillPayment(upstream, paymentPreimage, attribution_opt, fulfillmentPayload_opt) sending(upstream, recipient, walletNodeId_opt, recipientFeatures_opt, nextPayload, startedAt, fulfilledUpstream = true, accountable) } else { // we don't want to fulfill multiple times @@ -508,16 +508,16 @@ class NodeRelay private(nodeParams: NodeParams, upstream.received.foreach(r => rejectHtlc(r.add.id, r.add.channelId, upstream.amountIn, r.receivedAt, Some(upstream.receivedAt), failure)) } - private def fulfillPayment(upstream: Upstream.Hot.Trampoline, paymentPreimage: ByteVector32, downstreamAttribution_opt: Option[ByteVector]): Unit = upstream.received.foreach(r => { + private def fulfillPayment(upstream: Upstream.Hot.Trampoline, paymentPreimage: ByteVector32, downstreamAttribution_opt: Option[ByteVector], downstreamFulfillmentPayload_opt: Option[ByteVector]): Unit = upstream.received.foreach(r => { val attribution = FulfillAttributionData(r.receivedAt, Some(upstream.receivedAt), downstreamAttribution_opt) - val cmd = CMD_FULFILL_HTLC(r.add.id, paymentPreimage, Some(attribution), commit = true) + val cmd = CMD_FULFILL_HTLC(r.add.id, paymentPreimage, downstreamFulfillmentPayload_opt, Some(attribution), commit = true) PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, r.add.channelId, cmd) }) private def success(upstream: Upstream.Hot.Trampoline, fulfilledUpstream: Boolean, paymentSent: PaymentSent): Unit = { // We may have already fulfilled upstream, but we can now emit an accurate relayed event and clean-up resources. if (!fulfilledUpstream) { - fulfillPayment(upstream, paymentSent.paymentPreimage, paymentSent.remainingAttribution_opt) + fulfillPayment(upstream, paymentSent.paymentPreimage, paymentSent.fulfillmentPayload_opt, paymentSent.remainingAttribution_opt) } val incoming = upstream.received.map(r => PaymentEvent.IncomingPayment(r.add.channelId, r.receivedFrom, r.add.amountMsat, r.receivedAt)) val outgoing = paymentSent.parts.map(p => PaymentEvent.OutgoingPayment(p.channelId, p.remoteNodeId, p.amountWithFees, p.settledAt)) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/OnTheFlyFunding.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/OnTheFlyFunding.scala index 4267cc2184..f6fedeaa3e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/OnTheFlyFunding.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/OnTheFlyFunding.scala @@ -134,10 +134,10 @@ object OnTheFlyFunding { /** Create commands to fulfill all upstream HTLCs. */ def createFulfillCommands(preimage: ByteVector32): Seq[(ByteVector32, CMD_FULFILL_HTLC)] = upstream match { case _: Upstream.Local => Nil - case u: Upstream.Hot.Channel => Seq(u.add.channelId -> CMD_FULFILL_HTLC(u.add.id, preimage, Some(FulfillAttributionData(htlcReceivedAt = u.receivedAt, trampolineReceivedAt_opt = None, downstreamAttribution_opt = None)), commit = true)) + case u: Upstream.Hot.Channel => Seq(u.add.channelId -> CMD_FULFILL_HTLC(u.add.id, preimage, None, Some(FulfillAttributionData(htlcReceivedAt = u.receivedAt, trampolineReceivedAt_opt = None, downstreamAttribution_opt = None)), commit = true)) case u: Upstream.Hot.Trampoline => u.received.map(c => { val attribution = FulfillAttributionData(htlcReceivedAt = c.receivedAt, trampolineReceivedAt_opt = Some(u.receivedAt), downstreamAttribution_opt = None) - c.add.channelId -> CMD_FULFILL_HTLC(c.add.id, preimage, Some(attribution), commit = true) + c.add.channelId -> CMD_FULFILL_HTLC(c.add.id, preimage, None, Some(attribution), commit = true) }) } } 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 93a4cdea47..8eb4962b02 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 @@ -31,6 +31,7 @@ import fr.acinq.eclair.transactions.DirectedHtlc.outgoing import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.{CustomCommitmentsPlugin, Feature, Features, Logs, MilliSatoshiLong, NodeParams, TimestampMilli} import kamon.metric.{Gauge, Metric} +import scodec.bits.ByteVector import scala.concurrent.Promise import scala.util.Try @@ -118,7 +119,7 @@ class PostRestartHtlcCleaner(nodeParams: NodeParams, register: ActorRef, initial Metrics.Resolved.withTag(Tags.Success, value = true).withTag(Metrics.Relayed, value = false).increment() if (e.currentState != CLOSED) { log.info(s"fulfilling broken htlc=$htlc") - channel ! CMD_FULFILL_HTLC(htlc.id, preimage, None, commit = true) + channel ! CMD_FULFILL_HTLC(htlc.id, preimage, None, None, commit = true) } else { log.info(s"got preimage but upstream channel is closed for htlc=$htlc") } @@ -157,7 +158,7 @@ class PostRestartHtlcCleaner(nodeParams: NodeParams, register: ActorRef, initial case RES_ADD_SETTLED(o: Origin.Cold, remoteNodeId, htlc, fulfill: HtlcResult.Fulfill) => log.info("htlc #{} from channelId={} fulfilled downstream by {}", htlc.id, htlc.channelId, remoteNodeId) - handleDownstreamFulfill(brokenHtlcs, o, remoteNodeId, htlc, fulfill.paymentPreimage) + handleDownstreamFulfill(brokenHtlcs, o, remoteNodeId, htlc, fulfill.paymentPreimage, fulfill.fulfillmentPayload_opt) case RES_ADD_SETTLED(o: Origin.Cold, remoteNodeId, htlc, fail: HtlcResult.Fail) => if (htlc.fundingFee_opt.nonEmpty) { @@ -171,14 +172,14 @@ class PostRestartHtlcCleaner(nodeParams: NodeParams, register: ActorRef, initial case GetBrokenHtlcs => sender() ! brokenHtlcs } - private def handleDownstreamFulfill(brokenHtlcs: BrokenHtlcs, origin: Origin.Cold, downstreamNodeId: PublicKey, fulfilledHtlc: UpdateAddHtlc, paymentPreimage: ByteVector32): Unit = + private def handleDownstreamFulfill(brokenHtlcs: BrokenHtlcs, origin: Origin.Cold, downstreamNodeId: PublicKey, fulfilledHtlc: UpdateAddHtlc, paymentPreimage: ByteVector32, fulfillmentPayload_opt: Option[ByteVector]): Unit = brokenHtlcs.relayedOut.get(origin) match { case Some(relayedOut) => origin.upstream match { case Upstream.Local(id) => val feesPaid = 0.msat // fees are unknown since we lost the reference to the payment nodeParams.db.payments.getOutgoingPayment(id) match { case Some(p) => - nodeParams.db.payments.updateOutgoingPayment(PaymentSent(p.parentId, paymentPreimage, p.recipientAmount, p.recipientNodeId, PaymentSent.PaymentPart(id, PaymentEvent.OutgoingPayment(fulfilledHtlc.channelId, downstreamNodeId, fulfilledHtlc.amountMsat, settledAt = TimestampMilli.now()), feesPaid, None, startedAt = p.createdAt) :: Nil, None, p.createdAt)) + nodeParams.db.payments.updateOutgoingPayment(PaymentSent(p.parentId, paymentPreimage, p.recipientAmount, p.recipientNodeId, PaymentSent.PaymentPart(id, PaymentEvent.OutgoingPayment(fulfilledHtlc.channelId, downstreamNodeId, fulfilledHtlc.amountMsat, settledAt = TimestampMilli.now()), feesPaid, None, startedAt = p.createdAt) :: Nil, fulfillmentPayload_opt, None, p.createdAt)) // If all downstream HTLCs are now resolved, we can emit the payment event. val payments = nodeParams.db.payments.listOutgoingPayments(p.parentId) if (!payments.exists(p => p.status == OutgoingPaymentStatus.Pending)) { @@ -186,7 +187,7 @@ class PostRestartHtlcCleaner(nodeParams: NodeParams, register: ActorRef, initial case OutgoingPayment(id, _, _, _, _, amount, _, _, createdAt, _, _, OutgoingPaymentStatus.Succeeded(_, feesPaid, _, completedAt)) => PaymentSent.PaymentPart(id, PaymentEvent.OutgoingPayment(ByteVector32.Zeroes, downstreamNodeId, amount, completedAt), feesPaid, None, createdAt) } - val sent = PaymentSent(p.parentId, paymentPreimage, p.recipientAmount, p.recipientNodeId, succeeded, None, p.createdAt) + val sent = PaymentSent(p.parentId, paymentPreimage, p.recipientAmount, p.recipientNodeId, succeeded, fulfillmentPayload_opt, None, p.createdAt) log.info(s"payment id=${sent.id} paymentHash=${sent.paymentHash} successfully sent (amount=${sent.recipientAmount})") context.system.eventStream.publish(sent) } @@ -198,7 +199,7 @@ class PostRestartHtlcCleaner(nodeParams: NodeParams, register: ActorRef, initial val dummyNodeId = nodeParams.nodeId val now = TimestampMilli.now() nodeParams.db.payments.addOutgoingPayment(OutgoingPayment(id, id, None, fulfilledHtlc.paymentHash, PaymentType.Standard, fulfilledHtlc.amountMsat, dummyFinalAmount, dummyNodeId, now, None, None, OutgoingPaymentStatus.Pending)) - nodeParams.db.payments.updateOutgoingPayment(PaymentSent(id, paymentPreimage, dummyFinalAmount, dummyNodeId, PaymentSent.PaymentPart(id, PaymentEvent.OutgoingPayment(fulfilledHtlc.channelId, downstreamNodeId, fulfilledHtlc.amountMsat, settledAt = now), feesPaid, None, startedAt = now) :: Nil, None, startedAt = now)) + nodeParams.db.payments.updateOutgoingPayment(PaymentSent(id, paymentPreimage, dummyFinalAmount, dummyNodeId, PaymentSent.PaymentPart(id, PaymentEvent.OutgoingPayment(fulfilledHtlc.channelId, downstreamNodeId, fulfilledHtlc.amountMsat, settledAt = now), feesPaid, None, startedAt = now) :: Nil, fulfillmentPayload_opt, None, startedAt = now)) } // There can never be more than one pending downstream HTLC for a given local origin (a multi-part payment is // instead spread across multiple local origins) so we can now forget this origin. @@ -209,7 +210,7 @@ class PostRestartHtlcCleaner(nodeParams: NodeParams, register: ActorRef, initial if (relayedOut != Set((fulfilledHtlc.channelId, fulfilledHtlc.id))) { log.error(s"unexpected channel relay downstream HTLCs: expected (${fulfilledHtlc.channelId},${fulfilledHtlc.id}), found $relayedOut") } - PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, u.originChannelId, CMD_FULFILL_HTLC(u.originHtlcId, paymentPreimage, None, commit = true)) + PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, u.originChannelId, CMD_FULFILL_HTLC(u.originHtlcId, paymentPreimage, fulfillmentPayload_opt, None, commit = true)) // We don't know when we received this HTLC so we just pretend that we received it just now. context.system.eventStream.publish(ChannelPaymentRelayed(fulfilledHtlc.paymentHash, Seq(PaymentEvent.IncomingPayment(u.originChannelId, u.originNodeId, u.amountIn, TimestampMilli.now())), Seq(PaymentEvent.OutgoingPayment(fulfilledHtlc.channelId, downstreamNodeId, fulfilledHtlc.amountMsat, TimestampMilli.now())))) Metrics.PendingRelayedOut.decrement() @@ -220,7 +221,7 @@ class PostRestartHtlcCleaner(nodeParams: NodeParams, register: ActorRef, initial log.info("received preimage for paymentHash={}: fulfilling {} HTLCs upstream", fulfilledHtlc.paymentHash, u.originHtlcs.length) u.originHtlcs.foreach { case Upstream.Cold.Channel(channelId, _, htlcId, _) => Metrics.Resolved.withTag(Tags.Success, value = true).withTag(Metrics.Relayed, value = true).increment() - PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, channelId, CMD_FULFILL_HTLC(htlcId, paymentPreimage, None, commit = true)) + PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, channelId, CMD_FULFILL_HTLC(htlcId, paymentPreimage, fulfillmentPayload_opt, None, commit = true)) } } val relayedOut1 = relayedOut diff Set((fulfilledHtlc.channelId, fulfilledHtlc.id)) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala index 1937346aa9..ecc9b7fb40 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala @@ -120,7 +120,7 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, case Event(ps: PaymentSent, d: PaymentProgress) => require(ps.parts.length == 1, "child payment must contain only one part") // As soon as we get the preimage we can consider that the whole payment succeeded (we have a proof of payment). - gotoSucceededOrStop(PaymentSucceeded(d.request, ps.paymentPreimage, ps.parts, d.pending.keySet - ps.parts.head.id, ps.remainingAttribution_opt)) + gotoSucceededOrStop(PaymentSucceeded(d.request, ps.paymentPreimage, ps.parts, d.pending.keySet - ps.parts.head.id, ps.fulfillmentPayload_opt, ps.remainingAttribution_opt)) } when(PAYMENT_IN_PROGRESS) { @@ -146,7 +146,7 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, require(ps.parts.length == 1, "child payment must contain only one part") // As soon as we get the preimage we can consider that the whole payment succeeded (we have a proof of payment). Metrics.PaymentAttempt.withTag(Tags.MultiPart, value = true).record(d.request.maxAttempts - d.remainingAttempts) - gotoSucceededOrStop(PaymentSucceeded(d.request, ps.paymentPreimage, ps.parts, d.pending.keySet - ps.parts.head.id, ps.remainingAttribution_opt)) + gotoSucceededOrStop(PaymentSucceeded(d.request, ps.paymentPreimage, ps.parts, d.pending.keySet - ps.parts.head.id, ps.fulfillmentPayload_opt, ps.remainingAttribution_opt)) } when(PAYMENT_ABORTED) { @@ -164,7 +164,7 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, case Event(ps: PaymentSent, d: PaymentAborted) => require(ps.parts.length == 1, "child payment must contain only one part") log.warning(s"payment recipient fulfilled incomplete multi-part payment (id=${ps.parts.head.id})") - gotoSucceededOrStop(PaymentSucceeded(d.request, ps.paymentPreimage, ps.parts, d.pending - ps.parts.head.id, ps.remainingAttribution_opt)) + gotoSucceededOrStop(PaymentSucceeded(d.request, ps.paymentPreimage, ps.parts, d.pending - ps.parts.head.id, ps.fulfillmentPayload_opt, ps.remainingAttribution_opt)) case Event(_: RouteResponse, _) => stay() case Event(_: PaymentRouteNotFound, _) => stay() @@ -176,7 +176,7 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, val parts = d.parts ++ ps.parts val pending = d.pending - ps.parts.head.id if (pending.isEmpty) { - myStop(d.request, Right(cfg.createPaymentSent(d.request.recipient, d.preimage, parts, d.remainingAttribution_opt, start))) + myStop(d.request, Right(cfg.createPaymentSent(d.request.recipient, d.preimage, parts, d.fulfillmentPayload_opt, d.remainingAttribution_opt, start))) } else { stay() using d.copy(parts = parts, pending = pending) } @@ -187,7 +187,7 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, log.warning(s"payment succeeded but partial payment failed (id=${pf.id})") val pending = d.pending - pf.id if (pending.isEmpty) { - myStop(d.request, Right(cfg.createPaymentSent(d.request.recipient, d.preimage, d.parts, d.remainingAttribution_opt, start))) + myStop(d.request, Right(cfg.createPaymentSent(d.request.recipient, d.preimage, d.parts, d.fulfillmentPayload_opt, d.remainingAttribution_opt, start))) } else { stay() using d.copy(pending = pending) } @@ -214,10 +214,10 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, private def gotoSucceededOrStop(d: PaymentSucceeded): State = { if (publishPreimage) { - d.request.replyTo ! PreimageReceived(paymentHash, d.preimage, d.remainingAttribution_opt) + d.request.replyTo ! PreimageReceived(paymentHash, d.preimage, d.fulfillmentPayload_opt, d.remainingAttribution_opt) } if (d.pending.isEmpty) { - myStop(d.request, Right(cfg.createPaymentSent(d.request.recipient, d.preimage, d.parts, d.remainingAttribution_opt, start))) + myStop(d.request, Right(cfg.createPaymentSent(d.request.recipient, d.preimage, d.parts, d.fulfillmentPayload_opt, d.remainingAttribution_opt, start))) } else goto(PAYMENT_SUCCEEDED) using d } @@ -312,7 +312,7 @@ object MultiPartPaymentLifecycle { * The payment FSM will wait for all child payments to settle before emitting payment events, but the preimage will be * shared as soon as it's received to unblock other actors that may need it. */ - case class PreimageReceived(paymentHash: ByteVector32, paymentPreimage: ByteVector32, remainingAttribution_opt: Option[ByteVector]) + case class PreimageReceived(paymentHash: ByteVector32, paymentPreimage: ByteVector32, fulfillmentPayload_opt: Option[ByteVector], remainingAttribution_opt: Option[ByteVector]) // @formatter:off sealed trait State @@ -369,7 +369,7 @@ object MultiPartPaymentLifecycle { * @param parts fulfilled child payments. * @param pending pending child payments (we are waiting for them to be fulfilled downstream). */ - case class PaymentSucceeded(request: SendMultiPartPayment, preimage: ByteVector32, parts: Seq[PaymentPart], pending: Set[UUID], remainingAttribution_opt: Option[ByteVector]) extends Data + case class PaymentSucceeded(request: SendMultiPartPayment, preimage: ByteVector32, parts: Seq[PaymentPart], pending: Set[UUID], fulfillmentPayload_opt: Option[ByteVector], remainingAttribution_opt: Option[ByteVector]) extends Data private def createRouteRequest(replyTo: ActorRef, nodeParams: NodeParams, routeParams: RouteParams, d: PaymentProgress, cfg: SendPaymentConfig): RouteRequest = { RouteRequest(replyTo.toTyped, nodeParams.nodeId, d.request.recipient, routeParams, d.ignore, allowMultiPart = true, d.pending.values.toSeq, Some(cfg.paymentContext)) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala index 041c50c1fa..266f070bf2 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala @@ -336,8 +336,8 @@ object PaymentInitiator { case _ => PaymentType.Standard } - def createPaymentSent(recipient: Recipient, preimage: ByteVector32, parts: Seq[PaymentSent.PaymentPart], remainingAttribution_opt: Option[ByteVector], startedAt: TimestampMilli): PaymentSent = { - PaymentSent(parentId, preimage, recipient.totalAmount, recipient.nodeId, parts, remainingAttribution_opt, startedAt) + def createPaymentSent(recipient: Recipient, preimage: ByteVector32, parts: Seq[PaymentSent.PaymentPart], fulfillmentPayload_opt: Option[ByteVector], remainingAttribution_opt: Option[ByteVector], startedAt: TimestampMilli): PaymentSent = { + PaymentSent(parentId, preimage, recipient.totalAmount, recipient.nodeId, parts, fulfillmentPayload_opt, remainingAttribution_opt, startedAt) } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala index e83c398ca7..e1f5806a2a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala @@ -115,20 +115,18 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A router ! Router.RouteDidRelay(d.route) Metrics.PaymentAttempt.withTag(Tags.MultiPart, value = false).record(d.failures.size + 1) val p = PaymentPart(id, PaymentEvent.OutgoingPayment(htlc.channelId, remoteNodeId, d.cmd.amount, settledAt = TimestampMilli.now()), d.cmd.amount - d.request.amount, Some(d.route.fullRoute), startedAt = d.sentAt) - val remainingAttribution_opt = fulfill match { - case HtlcResult.RemoteFulfill(updateFulfill) => - updateFulfill.attribution_opt match { - case Some(attribution) => - val attributionDetails = Sphinx.SuccessPacket.decrypt(Some(attribution), d.sharedSecrets) - if (attributionDetails.holdTimes.nonEmpty) { - context.system.eventStream.publish(Router.ReportedHoldTimes(attributionDetails.holdTimes)) - } - attributionDetails.remainingAttribution_opt - case None => None + val attribution_opt = fulfill match { + case HtlcResult.RemoteFulfill(f) => + val fullRoute = cfg.upstream match { + case _: Upstream.Local => true + case _: Upstream.Hot.Channel => false + case _: Upstream.Hot.Trampoline => false } + Some(Sphinx.SuccessPacket.decrypt(f.fulfillmentPayload_opt, f.attribution_opt, d.sharedSecrets, fullRoute)) case _: HtlcResult.OnChainFulfill => None } - myStop(d.request, Right(cfg.createPaymentSent(d.recipient, fulfill.paymentPreimage, p :: Nil, remainingAttribution_opt, start))) + attribution_opt.foreach(a => if (a.holdTimes.nonEmpty) context.system.eventStream.publish(Router.ReportedHoldTimes(a.holdTimes))) + myStop(d.request, Right(cfg.createPaymentSent(d.recipient, fulfill.paymentPreimage, p :: Nil, attribution_opt.flatMap(_.fulfillmentPayload_opt), attribution_opt.flatMap(_.remainingAttribution_opt), start))) case Event(RES_ADD_SETTLED(_, _, _, fail: HtlcResult.Fail), d: WaitingForComplete) => fail match { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/TrampolinePaymentLifecycle.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/TrampolinePaymentLifecycle.scala index 4f9c93b339..0215ce6533 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/TrampolinePaymentLifecycle.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/TrampolinePaymentLifecycle.scala @@ -133,7 +133,7 @@ object TrampolinePaymentLifecycle { val holdTimes = fulfill match { case HtlcResult.RemoteFulfill(updateFulfill) => updateFulfill.attribution_opt match { - case Some(attribution) => Sphinx.SuccessPacket.decrypt(Some(attribution), outerOnionSecrets).holdTimes + case Some(attribution) => Sphinx.SuccessPacket.decrypt(updateFulfill.fulfillmentPayload_opt, Some(attribution), outerOnionSecrets).holdTimes case None => Nil } case _: HtlcResult.OnChainFulfill => Nil @@ -237,7 +237,7 @@ class TrampolinePaymentLifecycle private(nodeParams: NodeParams, waitForSettlement(remaining - 1, attemptNumber, part +: fulfilledParts) } else { context.log.info("trampoline payment succeeded") - cmd.replyTo ! PaymentSent(cmd.paymentId, fulfill.paymentPreimage, totalAmount, cmd.invoice.nodeId, part +: fulfilledParts, None, startedAt) + cmd.replyTo ! PaymentSent(cmd.paymentId, fulfill.paymentPreimage, totalAmount, cmd.invoice.nodeId, part +: fulfilledParts, fulfill.fulfillmentPayload_opt, None, startedAt) Behaviors.stopped } case fail: HtlcResult.Fail => 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 0d056ac68d..5ef802270a 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 @@ -24,6 +24,7 @@ import fr.acinq.eclair.wire.protocol.CommonCodecs._ import fr.acinq.eclair.wire.protocol.FailureMessageCodecs._ import fr.acinq.eclair.wire.protocol._ import scodec.Codec +import scodec.bits.ByteVector import scodec.codecs._ import shapeless.{::, HNil} @@ -59,12 +60,13 @@ object CommandCodecs { ("replyTo_opt" | provide(Option.empty[ActorRef]))).map { case id :: r :: downstreamAttribution_opt :: htlcReceivedAt_opt :: commit :: replyTo_opt :: HNil => val attribution_opt = htlcReceivedAt_opt.map(receivedAt => FulfillAttributionData(receivedAt, None, downstreamAttribution_opt)) - CMD_FULFILL_HTLC(id, r, attribution_opt, commit, replyTo_opt) + CMD_FULFILL_HTLC(id, r, None, attribution_opt, commit, replyTo_opt) }.decodeOnly private val cmdFulfillWithoutAttributionCodec: Codec[CMD_FULFILL_HTLC] = (("id" | int64) :: ("r" | bytes32) :: + ("fulfillmentPayload_opt" | provide(Option.empty[ByteVector])) :: ("attribution_opt" | provide(Option.empty[FulfillAttributionData])) :: ("commit" | provide(false)) :: ("replyTo_opt" | provide(Option.empty[ActorRef]))).as[CMD_FULFILL_HTLC] @@ -74,9 +76,18 @@ object CommandCodecs { ("trampolineReceivedAt_opt" | optional(bool8, uint64overflow.as[TimestampMilli])) :: ("downstreamAttribution_opt" | optional(bool8, bytes(Sphinx.Attribution.totalLength)))).as[FulfillAttributionData] - private val cmdFullfillCodec: Codec[CMD_FULFILL_HTLC] = + private val cmdFullfillWithoutFulfillmentPayloadCodec: Codec[CMD_FULFILL_HTLC] = (("id" | int64) :: ("r" | bytes32) :: + ("fulfillmentPayload_opt" | provide(Option.empty[ByteVector])) :: + ("attribution_opt" | optional(bool8, fulfillAttributionCodec)) :: + ("commit" | provide(false)) :: + ("replyTo_opt" | provide(Option.empty[ActorRef]))).as[CMD_FULFILL_HTLC] + + private val cmdFulfillCodec: Codec[CMD_FULFILL_HTLC] = + (("id" | int64) :: + ("r" | bytes32) :: + ("fulfillmentPayload_opt" | optional(bool8, variableSizeBytes(uint16, bytes))) :: ("attribution_opt" | optional(bool8, fulfillAttributionCodec)) :: ("commit" | provide(false)) :: ("replyTo_opt" | provide(Option.empty[ActorRef]))).as[CMD_FULFILL_HTLC] @@ -144,7 +155,8 @@ object CommandCodecs { val cmdCodec: Codec[HtlcSettlementCommand] = discriminated[HtlcSettlementCommand].by(uint16) // NB: order matters! - .typecase(8, cmdFullfillCodec) + .typecase(9, cmdFulfillCodec) + .typecase(8, cmdFullfillWithoutFulfillmentPayloadCodec) .typecase(7, cmdFailCodec) .typecase(6, cmdFulfillWithPartialAttributionCodec) .typecase(5, cmdFailWithPartialAttributionCodec) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/HtlcTlv.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/HtlcTlv.scala index 42a733354e..6393337d01 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/HtlcTlv.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/HtlcTlv.scala @@ -67,10 +67,16 @@ sealed trait UpdateFulfillHtlcTlv extends Tlv object UpdateFulfillHtlcTlv { case class AttributionData(data: ByteVector) extends UpdateFulfillHtlcTlv + /** The recipient may include an encrypted arbitrary payload that will be returned to the sender. */ + case class FulfillmentPayload(data: ByteVector) extends UpdateFulfillHtlcTlv + private val attributionData: Codec[AttributionData] = (("length" | constant(hex"fd0398")) :: ("data" | bytes(Sphinx.Attribution.totalLength))).as[AttributionData] + private val fulfillmentPayload: Codec[FulfillmentPayload] = tlvField(bytes.as[FulfillmentPayload]) + val updateFulfillHtlcTlvCodec: Codec[TlvStream[UpdateFulfillHtlcTlv]] = tlvStream(discriminated[UpdateFulfillHtlcTlv].by(varint) .typecase(UInt64(1), attributionData) + .typecase(UInt64(3), fulfillmentPayload) ) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala index 8f405b55dd..c39b1fa217 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala @@ -580,6 +580,7 @@ case class UpdateFulfillHtlc(channelId: ByteVector32, paymentPreimage: ByteVector32, tlvStream: TlvStream[UpdateFulfillHtlcTlv] = TlvStream.empty) extends HtlcMessage with UpdateMessage with HasChannelId with HtlcSettlementMessage { val attribution_opt: Option[ByteVector] = tlvStream.get[UpdateFulfillHtlcTlv.AttributionData].map(_.data) + val fulfillmentPayload_opt: Option[ByteVector] = tlvStream.get[UpdateFulfillHtlcTlv.FulfillmentPayload].map(_.data) } case class UpdateFailHtlc(channelId: ByteVector32, 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 c314d2871c..3d8aff1cc1 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 @@ -24,7 +24,6 @@ import fr.acinq.eclair.blockchain.fee._ import fr.acinq.eclair.channel.ChannelSpendSignature.IndividualSignature import fr.acinq.eclair.channel.states.ChannelStateTestsBase import fr.acinq.eclair.crypto.ShaChain -import fr.acinq.eclair.reputation.Reputation import fr.acinq.eclair.transactions.Transactions.ZeroFeeHtlcTxAnchorOutputsCommitmentFormat import fr.acinq.eclair.transactions.{CommitmentSpec, Transactions} import fr.acinq.eclair.wire.protocol._ @@ -114,7 +113,7 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(bc4.availableBalanceForSend == b) assert(bc4.availableBalanceForReceive == a - p - htlcOutputFee) - val cmdFulfill = CMD_FULFILL_HTLC(0, payment_preimage, None) + val cmdFulfill = CMD_FULFILL_HTLC(0, payment_preimage, None, None) val Right((bc5, fulfill)) = bc4.sendFulfill(cmdFulfill, bob.underlyingActor.nodeParams.privateKey, useAttributionData = false) assert(bc5.availableBalanceForSend == b + p) // as soon as we have the fulfill, the balance increases assert(bc5.availableBalanceForReceive == a - p - htlcOutputFee) @@ -317,7 +316,7 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(ac8.availableBalanceForSend == a - p1 - htlcOutputFee - p2 - htlcOutputFee - htlcOutputFee) assert(ac8.availableBalanceForReceive == b - p3) - val cmdFulfill1 = CMD_FULFILL_HTLC(0, payment_preimage1, None) + val cmdFulfill1 = CMD_FULFILL_HTLC(0, payment_preimage1, None, None) val Right((bc8, fulfill1)) = bc7.sendFulfill(cmdFulfill1, bob.underlyingActor.nodeParams.privateKey, useAttributionData = false) 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) @@ -327,7 +326,7 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with 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 - val cmdFulfill3 = CMD_FULFILL_HTLC(0, payment_preimage3, None) + val cmdFulfill3 = CMD_FULFILL_HTLC(0, payment_preimage3, None, None) val Right((ac9, fulfill3)) = ac8.sendFulfill(cmdFulfill3, alice.underlyingActor.nodeParams.privateKey, useAttributionData = false) assert(ac9.availableBalanceForSend == a - p1 - htlcOutputFee - p2 - htlcOutputFee + p3) // as soon as we have the fulfill, the balance increases assert(ac9.availableBalanceForReceive == b - p3) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/HelpersSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/HelpersSpec.scala index 6393356220..3bbae5c24b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/HelpersSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/HelpersSpec.scala @@ -83,9 +83,9 @@ class HelpersSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStat crossSign(bob, alice, bob2alice, alice2bob) // Alice and Bob both know the preimage for only one of the two HTLCs they received. - alice ! CMD_FULFILL_HTLC(htlcb2.id, rb2, None, replyTo_opt = Some(probe.ref)) + alice ! CMD_FULFILL_HTLC(htlcb2.id, rb2, None, None, replyTo_opt = Some(probe.ref)) probe.expectMsgType[CommandSuccess[CMD_FULFILL_HTLC]] - bob ! CMD_FULFILL_HTLC(htlca2.id, ra2, None, replyTo_opt = Some(probe.ref)) + bob ! CMD_FULFILL_HTLC(htlca2.id, ra2, None, None, replyTo_opt = Some(probe.ref)) probe.expectMsgType[CommandSuccess[CMD_FULFILL_HTLC]] // Alice publishes her commitment. diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala index 6ac9f58a45..69b7cebdfb 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala @@ -1212,7 +1212,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w crossSign(alice, bob, alice2bob, bob2alice) val (r, htlc) = addHtlc(4_000_000 msat, bob, alice, bob2alice, alice2bob) crossSign(bob, alice, bob2alice, alice2bob) - probe.send(alice, CMD_FULFILL_HTLC(htlc.id, r, None, replyTo_opt = Some(probe.ref))) + probe.send(alice, CMD_FULFILL_HTLC(htlc.id, r, None, None, replyTo_opt = Some(probe.ref))) probe.expectMsgType[CommandSuccess[CMD_FULFILL_HTLC]] // Force-close channel. @@ -1323,7 +1323,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w crossSign(alice, bob, alice2bob, bob2alice) val (r, htlc) = addHtlc(incomingHtlcAmount, bob, alice, bob2alice, alice2bob) crossSign(bob, alice, bob2alice, alice2bob) - probe.send(alice, CMD_FULFILL_HTLC(htlc.id, r, None, replyTo_opt = Some(probe.ref))) + probe.send(alice, CMD_FULFILL_HTLC(htlc.id, r, None, None, replyTo_opt = Some(probe.ref))) probe.expectMsgType[CommandSuccess[CMD_FULFILL_HTLC]] // Force-close channel and verify txs sent to watcher. @@ -1780,7 +1780,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w crossSign(alice, bob, alice2bob, bob2alice) val (r, htlc) = addHtlc(20_000_000 msat, bob, alice, bob2alice, alice2bob) crossSign(bob, alice, bob2alice, alice2bob) - probe.send(alice, CMD_FULFILL_HTLC(htlc.id, r, None, replyTo_opt = Some(probe.ref))) + probe.send(alice, CMD_FULFILL_HTLC(htlc.id, r, None, None, replyTo_opt = Some(probe.ref))) probe.expectMsgType[CommandSuccess[CMD_FULFILL_HTLC]] // Force-close channel. @@ -1855,7 +1855,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } else { crossSign(alice, bob, alice2bob, bob2alice) } - probe.send(alice, CMD_FULFILL_HTLC(htlc.id, r, None, replyTo_opt = Some(probe.ref))) + probe.send(alice, CMD_FULFILL_HTLC(htlc.id, r, None, None, replyTo_opt = Some(probe.ref))) probe.expectMsgType[CommandSuccess[CMD_FULFILL_HTLC]] // Force-close channel and verify txs sent to watcher. 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 d52eed6e28..67946388c0 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 @@ -534,7 +534,7 @@ trait ChannelStateTestsBase extends Assertions with Eventually { } def fulfillHtlc(id: Long, preimage: ByteVector32, s: TestFSMRef[ChannelState, ChannelData, Channel], r: TestFSMRef[ChannelState, ChannelData, Channel], s2r: TestProbe, r2s: TestProbe): Unit = { - s ! CMD_FULFILL_HTLC(id, preimage, None) + s ! CMD_FULFILL_HTLC(id, preimage, None, None) val fulfill = s2r.expectMsgType[UpdateFulfillHtlc] s2r.forward(r) eventually(assert(r.stateData.asInstanceOf[ChannelDataWithCommitments].commitments.changes.remoteChanges.proposed.contains(fulfill))) 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 a7d7e004e6..550f863f05 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(50_000_000 msat, bob, alice, bob2alice, alice2bob) val cmd = c match { - case FulfillHtlc => CMD_FULFILL_HTLC(add.id, preimage, None) + case FulfillHtlc => CMD_FULFILL_HTLC(add.id, preimage, None, None) case FailHtlc => CMD_FAIL_HTLC(add.id, FailureReason.EncryptedDownstreamFailure(randomBytes(252), None), None) } crossSign(bob, alice, bob2alice, alice2bob) @@ -482,7 +482,7 @@ class NormalQuiescentStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteL bob2blockchain.expectNoMessage(100 millis) // bob receives the fulfill for htlc, which is ignored because the channel is quiescent - val fulfillHtlc = CMD_FULFILL_HTLC(add.id, preimage, None) + val fulfillHtlc = CMD_FULFILL_HTLC(add.id, preimage, None, None) safeSend(bob, Seq(fulfillHtlc)) // the HTLC timeout from alice is near, bob needs to close the channel to avoid an on-chain race condition 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 a372127371..49543d5d87 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 @@ -1676,7 +1676,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // actual test begins val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] - bob ! CMD_FULFILL_HTLC(htlc.id, r, None) + bob ! CMD_FULFILL_HTLC(htlc.id, r, None, None) val fulfill = bob2alice.expectMsgType[UpdateFulfillHtlc] awaitCond(bob.stateData == initialState.modify(_.commitments.changes.localChanges.proposed).using(_ :+ fulfill)) } @@ -1703,7 +1703,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val r = randomBytes32() val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] - val c = CMD_FULFILL_HTLC(42, r, None, replyTo_opt = Some(sender.ref)) + val c = CMD_FULFILL_HTLC(42, r, None, None, replyTo_opt = Some(sender.ref)) bob ! c sender.expectMsg(RES_FAILURE(c, UnknownHtlcId(channelId(bob), 42))) assert(initialState == bob.stateData) @@ -1717,7 +1717,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // actual test begins val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] - val c = CMD_FULFILL_HTLC(htlc.id, ByteVector32.Zeroes, None, replyTo_opt = Some(sender.ref)) + val c = CMD_FULFILL_HTLC(htlc.id, ByteVector32.Zeroes, None, None, replyTo_opt = Some(sender.ref)) bob ! c sender.expectMsg(RES_FAILURE(c, InvalidHtlcPreimage(channelId(bob), 0))) assert(initialState == bob.stateData) @@ -1731,7 +1731,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // actual test begins val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] - val c = CMD_FULFILL_HTLC(htlc.id, r, None, replyTo_opt = Some(sender.ref)) + val c = CMD_FULFILL_HTLC(htlc.id, r, None, None, replyTo_opt = Some(sender.ref)) // this would be done automatically when the relayer calls safeSend bob.underlyingActor.nodeParams.db.pendingCommands.addSettlementCommand(initialState.channelId, c) bob ! c @@ -1746,7 +1746,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val sender = TestProbe() val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] - val c = CMD_FULFILL_HTLC(42, randomBytes32(), None, replyTo_opt = Some(sender.ref)) + val c = CMD_FULFILL_HTLC(42, randomBytes32(), None, None, 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) @@ -1760,7 +1760,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val (r, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) - bob ! CMD_FULFILL_HTLC(htlc.id, r, None) + bob ! CMD_FULFILL_HTLC(htlc.id, r, None, None) val fulfill = bob2alice.expectMsgType[UpdateFulfillHtlc] // actual test begins @@ -1906,7 +1906,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with crossSign(alice, bob, alice2bob, bob2alice) // HTLC is fulfilled but alice doesn't send its revocation. - bob ! CMD_FULFILL_HTLC(htlc.id, r, None) + bob ! CMD_FULFILL_HTLC(htlc.id, r, None, None) bob ! CMD_SIGN() bob2alice.expectMsgType[UpdateFulfillHtlc] bob2alice.expectMsgType[CommitSig] @@ -2647,7 +2647,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val htlcSuccessTx = bob.htlcTxs().head assert(htlcSuccessTx.isInstanceOf[UnsignedHtlcSuccessTx]) - bob ! CMD_FULFILL_HTLC(htlc.id, r, None, commit = true) + bob ! CMD_FULFILL_HTLC(htlc.id, r, None, None, commit = true) bob2alice.expectMsgType[UpdateFulfillHtlc] bob2alice.expectMsgType[CommitSig] bob ! CurrentBlockHeight(htlc.cltvExpiry.blockHeight - Bob.nodeParams.channelConf.fulfillSafetyBeforeTimeout.toInt) @@ -2682,7 +2682,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val htlcSuccessTx = bob.htlcTxs().head assert(htlcSuccessTx.isInstanceOf[UnsignedHtlcSuccessTx]) - bob ! CMD_FULFILL_HTLC(htlc.id, r, None, commit = false) + bob ! CMD_FULFILL_HTLC(htlc.id, r, None, None, commit = false) bob2alice.expectMsgType[UpdateFulfillHtlc] bob2alice.expectNoMessage(100 millis) bob ! CurrentBlockHeight(htlc.cltvExpiry.blockHeight - Bob.nodeParams.channelConf.fulfillSafetyBeforeTimeout.toInt) @@ -2717,7 +2717,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val htlcSuccessTx = bob.htlcTxs().head assert(htlcSuccessTx.isInstanceOf[UnsignedHtlcSuccessTx]) - bob ! CMD_FULFILL_HTLC(htlc.id, r, None, commit = true) + bob ! CMD_FULFILL_HTLC(htlc.id, r, None, None, commit = true) bob2alice.expectMsgType[UpdateFulfillHtlc] bob2alice.forward(alice) bob2alice.expectMsgType[CommitSig] 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 2e69a3dbb6..821114b19a 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 @@ -558,7 +558,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with disconnect(alice, bob) // We simulate a pending fulfill - bob.underlyingActor.nodeParams.db.pendingCommands.addSettlementCommand(initialState.channelId, CMD_FULFILL_HTLC(htlc.id, r, None, commit = true)) + bob.underlyingActor.nodeParams.db.pendingCommands.addSettlementCommand(initialState.channelId, CMD_FULFILL_HTLC(htlc.id, r, None, None, commit = true)) // then we reconnect them reconnect(alice, bob, alice2bob, bob2alice) @@ -589,7 +589,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with disconnect(alice, bob) // We simulate a pending fulfill - bob.underlyingActor.nodeParams.db.pendingCommands.addSettlementCommand(initialState.channelId, CMD_FULFILL_HTLC(htlc.id, r, None, commit = true)) + bob.underlyingActor.nodeParams.db.pendingCommands.addSettlementCommand(initialState.channelId, CMD_FULFILL_HTLC(htlc.id, r, None, None, commit = true)) // then we reconnect them reconnect(alice, bob, alice2bob, bob2alice) @@ -627,7 +627,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // We simulate a pending fulfill on that HTLC but not relayed. // When it is close to expiring upstream, we should close the channel. - bob.underlyingActor.nodeParams.db.pendingCommands.addSettlementCommand(initialState.channelId, CMD_FULFILL_HTLC(htlc.id, r, None, commit = true)) + bob.underlyingActor.nodeParams.db.pendingCommands.addSettlementCommand(initialState.channelId, CMD_FULFILL_HTLC(htlc.id, r, None, None, commit = true)) bob ! CurrentBlockHeight(htlc.cltvExpiry.blockHeight - bob.underlyingActor.nodeParams.channelConf.fulfillSafetyBeforeTimeout.toInt) val ChannelErrorOccurred(_, _, _, LocalError(err), isFatal) = listener.expectMsgType[ChannelErrorOccurred] 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 748d15ae52..db5ffac841 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 @@ -154,7 +154,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit test("recv CMD_FULFILL_HTLC") { f => import f._ val initialState = bob.stateData.asInstanceOf[DATA_SHUTDOWN] - bob ! CMD_FULFILL_HTLC(0, r1, None) + bob ! CMD_FULFILL_HTLC(0, r1, None, None) val fulfill = bob2alice.expectMsgType[UpdateFulfillHtlc] awaitCond(bob.stateData == initialState.modify(_.commitments.changes.localChanges.proposed).using(_ :+ fulfill) ) @@ -163,7 +163,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit test("recv CMD_FULFILL_HTLC (taproot)", Tag(ChannelStateTestsTags.SimpleClose), Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => import f._ val initialState = bob.stateData.asInstanceOf[DATA_SHUTDOWN] - bob ! CMD_FULFILL_HTLC(0, r1, None) + bob ! CMD_FULFILL_HTLC(0, r1, None, None) val fulfill = bob2alice.expectMsgType[UpdateFulfillHtlc] awaitCond(bob.stateData == initialState.modify(_.commitments.changes.localChanges.proposed).using(_ :+ fulfill)) } @@ -172,7 +172,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit import f._ val sender = TestProbe() val initialState = bob.stateData.asInstanceOf[DATA_SHUTDOWN] - bob ! CMD_FULFILL_HTLC(42, randomBytes32(), None, replyTo_opt = Some(sender.ref)) + bob ! CMD_FULFILL_HTLC(42, randomBytes32(), None, None, replyTo_opt = Some(sender.ref)) sender.expectMsgType[RES_FAILURE[CMD_FULFILL_HTLC, UnknownHtlcId]] assert(initialState == bob.stateData) } @@ -181,7 +181,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit import f._ val sender = TestProbe() val initialState = bob.stateData.asInstanceOf[DATA_SHUTDOWN] - val c = CMD_FULFILL_HTLC(1, ByteVector32.Zeroes, None, replyTo_opt = Some(sender.ref)) + val c = CMD_FULFILL_HTLC(1, ByteVector32.Zeroes, None, None, replyTo_opt = Some(sender.ref)) bob ! c sender.expectMsg(RES_FAILURE(c, InvalidHtlcPreimage(channelId(bob), 1))) @@ -194,7 +194,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit // actual test begins val initialState = bob.stateData.asInstanceOf[DATA_SHUTDOWN] - val c = CMD_FULFILL_HTLC(0, r1, None, replyTo_opt = Some(sender.ref)) + val c = CMD_FULFILL_HTLC(0, r1, None, None, replyTo_opt = Some(sender.ref)) // this would be done automatically when the relayer calls safeSend bob.underlyingActor.nodeParams.db.pendingCommands.addSettlementCommand(initialState.channelId, c) bob ! c @@ -209,7 +209,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit val sender = TestProbe() val initialState = bob.stateData.asInstanceOf[DATA_SHUTDOWN] - val c = CMD_FULFILL_HTLC(42, randomBytes32(), None, replyTo_opt = Some(sender.ref)) + val c = CMD_FULFILL_HTLC(42, randomBytes32(), None, None, 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) @@ -357,7 +357,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit import f._ val sender = TestProbe() // we need to have something to sign so we first send a fulfill and acknowledge (=sign) it - bob ! CMD_FULFILL_HTLC(0, r1, None) + bob ! CMD_FULFILL_HTLC(0, r1, None, None) bob2alice.expectMsgType[UpdateFulfillHtlc] bob2alice.forward(alice) bob ! CMD_SIGN(replyTo_opt = Some(sender.ref)) @@ -373,7 +373,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit test("recv CMD_SIGN (taproot)", Tag(ChannelStateTestsTags.SimpleClose), Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => import f._ val sender = TestProbe() - bob ! CMD_FULFILL_HTLC(0, r1, None) + bob ! CMD_FULFILL_HTLC(0, r1, None, None) bob2alice.expectMsgType[UpdateFulfillHtlc] bob2alice.forward(alice) bob ! CMD_SIGN(replyTo_opt = Some(sender.ref)) @@ -397,7 +397,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit test("recv CMD_SIGN (while waiting for RevokeAndAck)") { f => import f._ val sender = TestProbe() - bob ! CMD_FULFILL_HTLC(0, r1, None) + bob ! CMD_FULFILL_HTLC(0, r1, None, None) bob2alice.expectMsgType[UpdateFulfillHtlc] bob ! CMD_SIGN(replyTo_opt = Some(sender.ref)) sender.expectMsgType[RES_SUCCESS[CMD_SIGN]] @@ -413,7 +413,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit test("recv CommitSig") { f => import f._ - bob ! CMD_FULFILL_HTLC(0, r1, None) + bob ! CMD_FULFILL_HTLC(0, r1, None, None) bob2alice.expectMsgType[UpdateFulfillHtlc] bob2alice.forward(alice) bob ! CMD_SIGN() @@ -484,7 +484,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit test("recv RevokeAndAck (invalid preimage)") { f => import f._ val tx = bob.signCommitTx() - bob ! CMD_FULFILL_HTLC(0, r1, None) + bob ! CMD_FULFILL_HTLC(0, r1, None, None) bob2alice.expectMsgType[UpdateFulfillHtlc] bob2alice.forward(alice) bob ! CMD_SIGN() diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala index 327881ce22..ec1663ce1d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala @@ -290,7 +290,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // actual test starts here val sender = TestProbe() - val c = CMD_FULFILL_HTLC(42, randomBytes32(), None, replyTo_opt = Some(sender.ref)) + val c = CMD_FULFILL_HTLC(42, randomBytes32(), None, None, replyTo_opt = Some(sender.ref)) alice ! c sender.expectMsg(RES_FAILURE(c, UnknownHtlcId(channelId(alice), 42))) } @@ -385,7 +385,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with crossSign(alice, bob, alice2bob, bob2alice) // Bob has the preimage for those HTLCs, but Alice force-closes before receiving it. - bob ! CMD_FULFILL_HTLC(htlc1.id, preimage, None) + bob ! CMD_FULFILL_HTLC(htlc1.id, preimage, None, None) bob2alice.expectMsgType[UpdateFulfillHtlc] // ignored val (lcp, closingTxs) = localClose(alice, alice2blockchain, htlcTimeoutCount = 2) assert(lcp.htlcOutputs.size == 2) @@ -445,7 +445,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with crossSign(alice, bob, alice2bob, bob2alice) // Bob has the preimage for those HTLCs, but he force-closes before Alice receives it. - bob ! CMD_FULFILL_HTLC(htlc1.id, preimage, None) + bob ! CMD_FULFILL_HTLC(htlc1.id, preimage, None, None) bob2alice.expectMsgType[UpdateFulfillHtlc] // ignored val (rcp, closingTxs) = localClose(bob, bob2blockchain, htlcSuccessCount = 2) @@ -504,7 +504,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // At that point, the HTLCs are not in Alice's commitment anymore. // But Bob has not revoked his commitment yet that contains them. bob.setState(NORMAL, bobStateWithHtlc) - bob ! CMD_FULFILL_HTLC(htlc1.id, preimage, None) + bob ! CMD_FULFILL_HTLC(htlc1.id, preimage, None, None) bob2alice.expectMsgType[UpdateFulfillHtlc] // ignored // Bob claims the htlc outputs from his previous commit tx using its preimage. @@ -600,7 +600,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // Bob doesn't have the preimage yet for any of those HTLCs. assert(closingTxs.htlcTxs.isEmpty) // Bob receives the preimage for the first two HTLCs. - bob ! CMD_FULFILL_HTLC(htlc1.id, preimage, None) + bob ! CMD_FULFILL_HTLC(htlc1.id, preimage, None, None) val htlcSuccessTxs = { val htlcSuccess = (0 until 2).map(_ => bob2blockchain.expectReplaceableTxPublished[HtlcSuccessTx]) assert(htlcSuccess.map(_.htlcId).toSet == Set(htlc1.id, htlc2.id)) @@ -895,7 +895,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(aliceCommitTx.txOut.size == 5) // 2 main outputs + 2 anchor outputs + 1 htlc // alice fulfills the HTLC but bob doesn't receive the signature - alice ! CMD_FULFILL_HTLC(htlc.id, r, None, commit = true) + alice ! CMD_FULFILL_HTLC(htlc.id, r, None, None, commit = true) alice2bob.expectMsgType[UpdateFulfillHtlc] alice2bob.forward(bob) inside(bob2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.Fulfill]]) { settled => @@ -966,7 +966,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(alice.stateName == CLOSING) // Alice receives the preimage for the first HTLC from downstream; she can now claim the corresponding HTLC output. - alice ! CMD_FULFILL_HTLC(htlc1.id, r1, None, commit = true) + alice ! CMD_FULFILL_HTLC(htlc1.id, r1, None, None, commit = true) val htlcSuccess = alice2blockchain.expectReplaceableTxPublished[HtlcSuccessTx](ConfirmationTarget.Absolute(htlc1.cltvExpiry.blockHeight)) assert(htlcSuccess.preimage == r1) Transaction.correctlySpends(htlcSuccess.sign(), closingState.commitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) @@ -1094,7 +1094,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2blockchain.expectNoMessage(100 millis) // Alice receives the preimage for the incoming HTLC. - alice ! CMD_FULFILL_HTLC(incomingHtlc.id, preimage, None, commit = true) + alice ! CMD_FULFILL_HTLC(incomingHtlc.id, preimage, None, None, commit = true) val htlcSuccess = alice2blockchain.expectReplaceableTxPublished[HtlcSuccessTx] assert(htlcSuccess.preimage == preimage) alice2blockchain.expectNoMessage(100 millis) @@ -1132,7 +1132,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2blockchain.expectWatchOutputsSpent(htlcDelayedTxs.map(_.input)) // We replay the HTLC fulfillment: nothing happens since we already published a 3rd-stage transaction. - alice ! CMD_FULFILL_HTLC(incomingHtlc.id, preimage, None, commit = true) + alice ! CMD_FULFILL_HTLC(incomingHtlc.id, preimage, None, None, commit = true) alice2blockchain.expectNoMessage(100 millis) // The remaining transactions confirm. @@ -1157,14 +1157,14 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with addHtlc(40_000_000 msat, bob, alice, bob2alice, alice2bob) crossSign(alice, bob, alice2bob, bob2alice) // Bob has the preimage for 2 of the 3 HTLCs he received. - bob ! CMD_FULFILL_HTLC(htlc1a.id, r1a, None) + bob ! CMD_FULFILL_HTLC(htlc1a.id, r1a, None, None) bob2alice.expectMsgType[UpdateFulfillHtlc] - bob ! CMD_FULFILL_HTLC(htlc2a.id, r2a, None) + bob ! CMD_FULFILL_HTLC(htlc2a.id, r2a, None, None) bob2alice.expectMsgType[UpdateFulfillHtlc] // Alice has the preimage for 2 of the 3 HTLCs she received. - alice ! CMD_FULFILL_HTLC(htlc1b.id, r1b, None) + alice ! CMD_FULFILL_HTLC(htlc1b.id, r1b, None, None) alice2bob.expectMsgType[UpdateFulfillHtlc] - alice ! CMD_FULFILL_HTLC(htlc2b.id, r2b, None) + alice ! CMD_FULFILL_HTLC(htlc2b.id, r2b, None, None) alice2bob.expectMsgType[UpdateFulfillHtlc] // Alice force-closes. @@ -1548,7 +1548,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(closingTxs.htlcTxs.isEmpty) // we don't have the preimage to claim the htlc-success yet // Alice receives the preimage for the first HTLC from downstream; she can now claim the corresponding HTLC output. - alice ! CMD_FULFILL_HTLC(htlc1.id, r1, None, commit = true) + alice ! CMD_FULFILL_HTLC(htlc1.id, r1, None, None, commit = true) val htlcSuccess = alice2blockchain.expectReplaceableTxPublished[ClaimHtlcSuccessTx](ConfirmationTarget.Absolute(htlc1.cltvExpiry.blockHeight)) assert(htlcSuccess.preimage == r1) Transaction.correctlySpends(htlcSuccess.sign(), bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) @@ -1734,7 +1734,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val htlcTimeoutTx = closingTxs.htlcTimeoutTxs.head // Alice receives the preimage for the first HTLC from downstream; she can now claim the corresponding HTLC output. - alice ! CMD_FULFILL_HTLC(htlc1.id, r1, None, commit = true) + alice ! CMD_FULFILL_HTLC(htlc1.id, r1, None, None, commit = true) val htlcSuccess = alice2blockchain.expectReplaceableTxPublished[ClaimHtlcSuccessTx](ConfirmationTarget.Absolute(htlc1.cltvExpiry.blockHeight)) assert(htlcSuccess.preimage == r1) Transaction.correctlySpends(htlcSuccess.sign(), bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) 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 69c0f2d569..15730aca95 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 @@ -361,12 +361,54 @@ class SphinxSpec extends AnyFunSuite { val attribution4 = Attribution.create(Some(attribution3), None, 500 milliseconds, sharedSecret0) assert(attribution4 == hex"84986c936d26bfd3bb2d34d3ec62cfdb63e0032fdb3d9d75f3e5d456f73dffa7e35aab1db4f1bd3b98ff585caf004f656c51037a3f4e810d275f3f6aea0c8e3a125ebee5f374b6440bcb9bb2955ebf70c06d64090f9f6cf098200305f7f4305ba9e1350a0c3f7dab4ccf35b8399b9650d8e363bf83d3a0a09706433f0adae6562eb338b21ea6f21329b3775905e59187c325c9cbf589f5da5e915d9e5ad1d21aa1431f9bdc587185ed8b5d4928e697e67cc96bee6d5354e3764cede3f385588fa665310356b2b1e68f8bd30c75d395405614a40a587031ebd6ace60dfb7c6dd188b572bd8e3e9a47b06c2187b528c5ed35c32da5130a21cd881138a5fcac806858ce6c596d810a7492eb261bcc91cead1dae75075b950c2e81cecf7e5fdb2b51df005d285803201ce914dfbf3218383829a0caa8f15486dd801133f1ed7edec436730b0ec98f48732547927229ac80269fcdc5e4f4db264274e940178732b429f9f0e582c559f994a7cdfb76c93ffc39de91ff936316726cc561a6520d47b2cd487299a96322dadc463ef06127fc63902ff9cc4f265e2fbd9de3fa5e48b7b51aa0850580ef9f3b5ebb60c6c3216c5a75a93e82936113d9cad57ae4a94dd6481954a9bd1b5cff4ab29ca221fa2bf9b28a362c9661206f896fc7cec563fb80aa5eaccb26c09fa4ef7a981e63028a9c4dac12f82ccb5bea090d56bbb1a4c431e315d9a169299224a8dbd099fb67ea61dfc604edf8a18ee742550b636836bb552dabb28820221bf8546331f32b0c143c1c89310c4fa2e1e0e895ce1a1eb0f43278fdb528131a3e32bfffe0c6de9006418f5309cba773ca38b6ad8507cc59445ccc0257506ebc16a4c01d4cd97e03fcf7a2049fea0db28447858f73b8e9fe98b391b136c9dc510288630a1f0af93b26a8891b857bfe4b818af99a1e011e6dbaa53982d29cf74ae7dffef45545279f19931708ed3eede5e82280eab908e8eb80abff3f1f023ab66869297b40da8496861dc455ac3abe1efa8a6f9e2c4eda48025d43a486a3f26f269743eaa30d6f0e1f48db6287751358a41f5b07aee0f098862e3493731fe2697acce734f004907c6f11eef189424fee52cd30ad708707eaf2e441f52bcf3d0c5440c1742458653c0c8a27b5ade784d9e09c8b47f1671901a29360e7e5e94946b9c75752a1a8d599d2a3e14ac81b84d42115cd688c8383a64fc6e7e1dc5568bb4837358ebe63207a4067af66b2027ad2ce8fb7ae3a452d40723a51fdf9f9c9913e8029a222cf81d12ad41e58860d75deb6de30ad") - val result = SuccessPacket.decrypt(Some(attribution4), sharedSecrets) + val result = SuccessPacket.decrypt(None, Some(attribution4), sharedSecrets) assert(result.remainingAttribution_opt.nonEmpty) assert(result.holdTimes == Seq(HoldTime(500 millisecond, publicKeys(0)), HoldTime(400 milliseconds, publicKeys(1)), HoldTime(300 milliseconds, publicKeys(2)), HoldTime(200 milliseconds, publicKeys(3)), HoldTime(100 milliseconds, publicKeys(4)))) } } + test("fulfilled HTLC with attribution data and fulfillment payload (reference test vector)") { + // We build the onion packet to obtain the shared secrets. + val Success(PacketAndSecrets(packet, sharedSecrets)) = create(sessionKey, 1300, publicKeys, referencePaymentPayloads, associatedData) + val Right(DecryptedPacket(_, packet1, sharedSecret0)) = peel(privKeys(0), associatedData, packet) + val Right(DecryptedPacket(_, packet2, sharedSecret1)) = peel(privKeys(1), associatedData, packet1) + val Right(DecryptedPacket(_, packet3, sharedSecret2)) = peel(privKeys(2), associatedData, packet2) + val Right(DecryptedPacket(_, packet4, sharedSecret3)) = peel(privKeys(3), associatedData, packet3) + val Right(lastPacket@DecryptedPacket(_, _, sharedSecret4)) = peel(privKeys(4), associatedData, packet4) + assert(lastPacket.isLastPacket) + + // The last node includes a fulfillment payload. + val fulfillmentPayload = hex"01f50000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 fe0001000103070809" + val fulfillmentPayload4 = SuccessPacket.create(sharedSecret4, fulfillmentPayload) + assert(fulfillmentPayload4 == hex"b5b1fed711469adc0da510494818ce54624b537e7a8aba4b190403d90439798a694bdca0d0d53c18487d9940664a8416eb855290621e186081b2222e279bfa6dcb1645a39c2cbc569ed66f8f9ed0c10d4dcbc61abe6685d558d5fb0ec104bf1d14ab1e42a4cf41f1950c3fd06574a8278f553382a78b2725f36559d6db7aeccd41569f88b3722c3535358915648bd2e82c53cbaaa14c34487a77799cc4650d5c2e2edece013cf4095ee99bc96e1a850fa3373538448bd6d5a1771f50ca39b2a4e85c7201be5bedd5fae801e5e99491e6fb1e76457c48094f347205d1170df06e2fa7128d0dfe7d24716fb621632b948f9ad9edba24fba9293906e193a7ca3c234cce42ca9470c08952be1dbd3dcc8452") + val attribution4 = Attribution.create(None, None, 100 millisecond, sharedSecret4) + assert(attribution4 == hex"d77d0711b5f71d1d1be56bd88b3bb7ebc1792bb739ea7ebc1bc3b031b8bc2df3a50e25aeb99f47d7f7ab39e24187d3f4df9c4333463b053832ee9ac07274a5261b8b2a01fc09ce9ea7cd04d7b585dfb83299fb6570d71f793c1fcac0ef498766952c8c6840efa02a567d558a3cf6822b12476324b9b9efa03e5f8f26f81fa93daac46cbf00c98e69b6747cf69caaa2a71b025bd18830c4c54cd08f598cfde6197b3f2a951aba907c964c0f5d19a44e6d1d7279637321fa598adde927b3087d238f8b426ecde500d318617cdb7a56e6ce3520fc95be41a549973764e4dc483853ecc313947709f1b5199cb077d46e701fa633e11d3e13b03e9212c115ca6fa004b2f3dd912814693b705a561a06da54cdf603677a3abecdc22c7358c2de3cef771b366a568150aeecc86ad1990bb0f4e2865933b03ea0df87901bff467908273dc6cea31cbab0e2b8d398d10b001058c259ed221b7b55762f4c7e49c8c11a45a107b7a2c605c26dc5b0b10d719b1c844670102b2b6a36c43fe4753a78a483fc39166ae28420f112d50c10ee64ca69569a2f690712905236b7c2cb7ac8954f02922d2d918c56d42649261593c47b14b324a65038c3c5be8d3c403ce0c8f19299b1664bf077d7cf1636c4fb9685a8e58b7029fd0939fa07925a60bed339b23f973293598f595e75c8f9d455d7cebe4b5e23357c8bd47d66d6628b39427e37e0aecbabf46c11be6771f7136e108a143ae9bafba0fc47a51b6c7deef4cba54bae906398ee3162a41f2191ca386b628bde7e1dd63d1611aa01a95c456df337c763cb8c3a81a6013aa633739d8cd554c688102211725e6adad165adc1bcd429d020c51b4b25d2117e8bb27eb0cc7020f9070d4ad19ac31a76ebdf5f9246646aeadbfb9a3f1d75bd8237961e786302516a1a781780e8b73f58dc06f307e58bd0eb1d8f5c9111f01312974c1dc777a6a2d3834d8a2a40014e9818d0685cb3919f6b3b788ddc640b0ff9b1854d7098c7dd6f35196e902b26709640bc87935a3914869a807e8339281e9cedaaca99474c3e7bdd35050bb998ab4546f9900904e0e39135e861ff7862049269701081ebce32e4cca992c6967ff0fd239e38233eaf614af31e186635e9439ec5884d798f9174da6ff569d68ed5c092b78bd3f880f5e88a7a8ab36789e1b57b035fb6c32a6358f51f83e4e5f46220bcad072943df8bd9541a61b7dae8f30fa3dd5fb39b1fd9a0b8e802552b78d4ec306ecee15bfe6da14b29ba6d19ce5be4dd478bca74a52429cd5309d404655c3dec85c252") + + val attribution3 = Attribution.create(Some(attribution4), Some(fulfillmentPayload4), 200 millisecond, sharedSecret3) + assert(attribution3 == hex"1571e10db7f8aa9f8e7e99caaf9c892e106c817df1d8e3b7b0e39d1c48f631e473e17e205489dd7b3c634cac3be0825cbf01418cd46e83c24b8d9c207742db9a0f0e5bcd888086498159f08080ba7bf3bc5cfe92707ca5d3bc81720c889a034b89cf2d376a67c5d63651e3af267efb768a38f488eac2940cf5525e959599984c3cf56c1daaa92476de574582eba16f90b9a6a74f4aa5f1cdad2115c994a09565f9755940ec0665f9e5bc58cad6e523091f94d0bcd3c6c65ca1a5d401128dcc5e14f9108b32e660017c13de598bcf9d403710857cccb0fb9c2a81bfd66bc4552e1132afa3119203a4aaa1e8839c1dab8cbdcde7b527aca3f54bde651aa9f3f2178829cee3f1c0b9292758a40cc63bd998fcd0d3ed4bdcaf1023267b8f8e44130a63ad15f76145936552381eabb6d684c0a3af6ba8efcf207cebaea5b7acdbb63f8e7221102409d10c23f0514dc9f4d0efb2264161a193a999a23e992632710580a0d320f676d367b9190721194514457761af05207cdab2b6328b1b3767eacb36a7ef4f7bd2e16762d13df188e0898b7410f62459458712a44bf594ae662fd89eb300abb6952ff8ad40164f2bcd7f86db5c7650b654b79046de55d51aa8061ce35f867a3e8f5bf98ad920be827101c64fb871d86e53a4b3c0455bfac5784168218aa72cbee86d9c750a9fa63c363a8b43d7bf4b2762516706a306f0aa3be1ec788b5e13f8b24837e53ac414f211e11c7a093cd9653dfa5fba4e377c79adfa5e841e2ddb6afc054fc715c05ddc6c8fc3e1ee3406e1ffceb2df77dc2f02652614d1bfcfaddebaa53ba919c7051034e2c7b7cfaabdf89f26e7f8e3f956d205dfab747ad0cb505b85b54a68439621b25832cbc2898919d0cd7c0a64cfd235388982dd4dd68240cb668f57e1d2619a656ed326f8c92357ee0d9acead3c20008bc5f04ca8059b55d77861c6d04dfc57cfba57315075acbe1451c96cf28e1e328e142890248d18f53b5d3513ce574dea7156cf596fdb3d909095ec287651f9cf1bcdc791c5938a5dd9b47e84c004d24ab3ae74492c7e8dcc1da15f65324be2672947ec82074cac8ce2b925bc555facbbf1b55d63ea6fbea6a785c97d4caf2e1dad9551b7f66c31caae5ebc7c0047e892f201308fcf452c588be0e63d89152113d87bf0dbd01603b4cdc7f0b724b0714a9851887a01f709408882e18230fe810b9fafa58a666654576d8eba3005f07221f55a6193815a672e5db56204053bc4286fa3db38250396309fd28011b5708a26a2d76c4a333b69b6bfd272fb") + val fulfillmentPayload3 = SuccessPacket.wrap(fulfillmentPayload4, sharedSecret3) + assert(fulfillmentPayload3 == hex"d4cd5f33730d592c4249f3e31233af0572bf729218ca375ddd8aa518b3fa466d6101f77904b013dd4ef3c912d9278edc09923288c1c6df3fb636053cb6a2fa4bf7e9d7500310a4fc819d4f26a0a03d4fd73c74ab281a2dcf2ec8bb56ecabb1a9884478a172348bf38ddf3dfd579d3c9393dd79a5650da2caedd41fd531fa4ec9f3a46bfc05df7244f30d115687eacc568395e83a165bfe1bc91073af0172e27488771c9fd7744b45c883d538e90bbbc627bcc428c5ebb71fee5877b7a6d23a198a544565dee2fb2ed736b69f8c7c696dd56fd48e88a24017306f74106a0898622abb24fbf6f3b534ff32ceda7d07d05800f9c32730fcb47a5a774cd464503936d2432e8b450ade0cdd14b2ea1bef2e71") + + val attribution2 = Attribution.create(Some(attribution3), Some(fulfillmentPayload3), 300 millisecond, sharedSecret2) + assert(attribution2 == hex"34e34397b8621ec2f2b54dbe6c14073e267324cd60b152bce76aec8729a6ddefb61bc263be4b57bd592aae604a32bea69afe6ef4a6b573c26b17d69381ec1fc9b5aa769d148f2f1f8b5377a73840bb6d40734019bf9dcbb731aeac3b1052e6703e8ffa550f2f3b6b40df0faa5bd67271a99a81e81265b567522733358a1c8a3b770d207ba8a22ba238abf056af11042474f4fd44bbb21e3ea8d978058a8cc67a11ad429f816de27a43dd69a3f1fe8ccf670fa081fd9d89af6079f7baf05c7f4bf318b2dcfe18f5bd8628795265f61cbf40b6efa757fc9242927c329f5e06ddbce3be8c1f5dd48a0370eb559f5dc181a09f2209da72f79ae6745992c803310d39f960e8ecf327aed706e4b3e2704eeb9b304dc0e0685f5dcd0389ec377bdba37610ad556a0e957a413a56339dd3c40817214bced5802beee2ee545bdd713208751added5fc0eb2bc89a5aa2decb18ee37dac39f22a33b60cc1a369d24de9f3d2d8b63c039e248806de4e36a47c7a0aed30edd30c3d62debdf1ad82bf7aedd7edec413850d91c261e12beec7ad1586a9ad25b2db62c58ca17119d61dcc4f3e5c4520c42a8e384a45d8659b338b3a08f9e123a1d3781f5fc97564ccff2c1d97f06fa0150cfa1e20eacabefb0c339ec109336d207cc63d9170752fc58314c43e6d4a528fd0975afa85f3aa186ff1b6b8cb12c97ed4ace295b0ef5f075f0217665b8bb180246b87982d10f43c9866b22878106f5214e99188781180478b07764a5e12876ddcb709e0a0a8dd42cf004c695c6fc1669a6fd0e4a1ca54b024d0d80eac492a9e5036501f36fb25b72a054189294955830e43c18e55668337c8c6733abb09fc2d4ade18d5a853a2b82f7b4d77151a64985004f1d9218f2945b63c56fdebd1e96a2a7e49fa70acb4c39873947b83c191c10e9a8f40f60f3ad5a2be47145c22ea59ed3f5f4e61cb069e875fb67142d281d784bf925cc286eacc2c43e94d08da4924b83e58dbf2e43fa625bdd620eba6d9ce960ff17d14ed1f2dbee7d08eceb540fdc75ff06dabc767267658fad8ce99e2a3236e46d2deedcb51c3c6f81589357edebac9772a70b3d910d83cd1b9ce6534a011e9fa557b891a23b5d88afcc0d9856c6dabeab25eea55e9a248182229e4927f268fe5431672fcce52f434ca3d27d1a2136bae5770bb36920df12fbc01d0e8165610efa04794f414c1417f1d4059435c5385bfe2de83ce0e238d6fd2dbd3c0487c69843298577bfa480fe2a16ab2a0e4bc712cd8b5a14871cda61c993b6835303d9043d7689a") + val fulfillmentPayload2 = SuccessPacket.wrap(fulfillmentPayload3, sharedSecret2) + assert(fulfillmentPayload2 == hex"b584abb82973d6112d1474e8685a21253e4936c0b27e7f122c5443694d99bb263e1a8f9724f7cc7b50ce9e79cdbc63738a97bd0d31acee4d15143fea72d1d7dd44c0828a7e3f4fbdc178c2a05caf13fa0eedf325479052aa855aa955e22520fcfbafc5de47d52fc304d4da1b2ba2882861038fc79f6ff6eb0a600e17af44cf25941318a95cb47788e65a4430c9b30d38a2d4e291f3657e5b83712cebeac1f942d4d066f146505d227aefb995cdf098e2e8d82c9118859bf9b1477e3b5bccce02288082bb2e1ded74d94a3278d500298e0a91435a9f6304436a614cfe59e33283067be5bda617e6e094ce861a2c48e51f73ba9dcfab879c6a3a19fbe60697432bb5b04dbc401d53757c133ed01a5ad4ad") + + val attribution1 = Attribution.create(Some(attribution2), Some(fulfillmentPayload2), 400 millisecond, sharedSecret1) + assert(attribution1 == hex"74a4ea61339463642a2182758871b2ea724f31f531aa98d80f1c3043febca41d5ee52e8b1e127e61719a0d078db8909748d57839e58424b91f063c4fbc8a221bef261140e66a9b596ca6d420a973ad54651be4bf8728ba81adcb9a1d3ce3820cea24f2c6bfdde427edb7706dfd1e8e50c0ab544022275fd457fbd57555cfc85bbfbed6eb0f7a8ea04ebc1b40fc8d3f67402a01d169ed1448ed2e708e16a0fe2c1ab5055be446594036b5ecae456486addbebbd09ca3423d4c15e2afce6b40b545bfbf5859b6bf913e80bcec00e88cba671fee9770fdace16301fbb23b0eca85549ae0be1a96bda5167b6e5fb837edecd981b61e06858badb34ead1e93746ab5afd2bc10e95bffc4dc965de242db63eb8ec221cf57e2c77241fcfca70818a85a447b6fbcaaf4b2fc2135565cbfc03804aa11ad0ab0582e5d86590df2ecfd561dc6e1cdb08d3e10901312326a45fb0498a177319389809c6ba07a76cfad621e07b9af097730e94df92fbd311b2cb5da32c80ab5f14971b6d40f8e2ab202ac98bd8439790764a40bf309ea2205c1632610956495720030a25dc7118e0c868fdfa78c3e9ecce58215579a0581b3bafdb7dbbe53be9e904567fdc0ce1236aab5d22f1ebc18997e3ea83d362d891e04c5785fd5238326f767bce499209f8db211a50e1402160486e98e7235cf397dbb9ae19fd9b79ef589c821c6f99f28be33452405a003b33f4540fe0a41dfcc286f4d7cc10b70552ba7850869abadcd4bb7f256823face853633d6e2a999ac9fcd259c71d08e266db5d744e1909a62c0db673745ad9585949d108ab96640d2bc27fb4acac7fa8b170a30055a5ede90e004df9a44bdc29aeb4a6bec1e85dde1de6aaf01c6a5d12405d0bec22f49026cb23264f8c04b8401d3c2ab6f2e109948b6193b3bec27adfe19fb8afb8a92364d6fc5b219e8737d583e7ff3a4bcb75d53edda3bf3f52896ac36d8a877ad9f296ea6c045603fc62ac4ae41272bde85ef7c3b3fd3538aacfd5b025fefbe277c2906821ecb20e6f75ea479fa3280f9100fb0089203455c56b6bc775e5c2f0f58c63edd63fa3eec0b40da4b276d0d41da2ec0ead865a98d12bc694e23d8eaadd2b4d0ee88e9570c88fb878930f492e036d27998d593e47763927ff7eb80b188864a3846dd2238f7f95f4090ed399ae95deaeb37abca1cf37c397cc12189affb42dca46b4ff6988eb8c060691d155302d448f50ff70a794d97c0408f8cee9385d6a71fa412e36edcb22dbf433db9db4779f27b682ee17fc05e70c8e794b9f7f6d1") + val fulfillmentPayload1 = SuccessPacket.wrap(fulfillmentPayload2, sharedSecret1) + assert(fulfillmentPayload1 == hex"ba9463d72ce55bc83c9ccd70861ffdf8c5b1a9291be4d9257016ae3c6d3becc6765fe07333369e629ef7475898e535dda560281a4dcf2654362b0421e95aff38e8aeb1298787331bec87815490e534af8de7c85bdf537418fdb81464168cb7c2123ee4186fa6da7cb60583c8ff2ed07982b0ce3e690cb9ba99f186d416bb8adb7f020ea2ef2894bbc732fcd78d1a2c3f7983b85f8b039ae370e1dd062e6d2e6439c003a4d65a55102db383ef1ceb291cd057dfba81396959613104b894e5f4202218b86b3ac9e9cb7b4a10a06669d788fd7d8a9dd832d8996592d17593978d1a500fbe1943c57f004e41eeb3da28e114997aa3b6f183fa2af59b8ad67c1c2753beb5c5fe6f4733980225e8d40d0cfd95") + + val attribution0 = Attribution.create(Some(attribution1), Some(fulfillmentPayload1), 500 millisecond, sharedSecret0) + assert(attribution0 == hex"84986c936d26bfd3bb2d34d3ec62cfdb63e0032fdb3d9d75f3e5d456f73dffa7e35aab1db4f1bd3b98ff585caf004f656c51037a3f4e810d275f3f6aea0c8e3a125ebee5f374b6440bcb9bb2955ebf706a8fd90a3bef40add4106d8c215290aafbc30881b17d68b6a7aac1b6ab52261df2fceef75d3a490862d4bab3aa510cfb8ca695b38c640b58bb8685db8bb57f93db49657ddda39d21d6074d9ecaa1ea0da6cc904e136dbb4c1ba9839a7fb8367c88662f7f573c82c2746d122cca96b2e7872a48e32c8adba45babdc57a9bca55720b1d2c922622fcf8f8a5ed7daae286484b701e69bd40554a4cc3418444ff7eb9982ecee020c8a92717f089ce1ab0ca00bb3a582253cd832a2f869703a2fadafd8261ebb93fde2a9f4f13dc8470436c3cd110bdcc7f5a1e9f9c743c8e55b50c45dac07e07a8e5c7416de895940fde032786594bbaa8a1147422bd5e926fea510408c249fb61164bf7bdbac53d9fb9704023314c332a865146893ad99743708c4a34a30a82fd8b756c561a6520d47b2cd487299a96322dadc463ef06127fc63902ff9cc4f265e2fbd9de3fa5e48b7b51aa0850580ef9f3b5ebb60c6c3216c5a75a93e82936113d9cad57ae4a94dd6481954a9bd1b5cff4ab29ca221fa2bf9b28a362c9661206f896fc7cec563fb80aa5eaccb26c09fa4ef7a981e63028a9c4dac12f82ccb5bea090d56bbb1a4c431e315d9a169299224a8dbd099fb67ea61dfc604edf8a18ee742550b636836bb552dabb28820221bf8546331f32b0c143c1c89310c4fa2e1e0e895ce1a1eb0f43278fdb528131a3e32bfffe0c6de9006418f5309cba773ca38b6ad8507cc59445ccc0257506ebc16a4c01d4cd97e03fcf7a2049fea0db28447858f73b8e9fe98b391b136c9dc510288630a1f0af93b26a8891b857bfe4b818af99a1e011e6dbaa53982d29cf74ae7dffef45545279f19931708ed3eede5e82280eab908e8eb80abff3f1f023ab66869297b40da8496861dc455ac3abe1efa8a6f9e2c4eda48025d43a486a3f26f269743eaa30d6f0e1f48db6287751358a41f5b07aee0f098862e3493731fe2697acce734f004907c6f11eef189424fee52cd30ad708707eaf2e441f52bcf3d0c5440c1742458653c0c8a27b5ade784d9e09c8b47f1671901a29360e7e5e94946b9c75752a1a8d599d2a3e14ac81b84d42115cd688c8383a64fc6e7e1dc5568bb4837358ebe63207a4067af66b2027ad2ce8fb7ae3a452d40723a51fdf9f9c9913e8029a222cf81d12ad41e58860d75deb6de30ad") + val fulfillmentPayload0 = SuccessPacket.wrap(fulfillmentPayload1, sharedSecret0) + assert(fulfillmentPayload0 == hex"8c0d9ee20671d1cdca98bb4c8dd5d490105b63021afcb90b22f33f1d9d6b7faafb86ec57ec4b56ad11e122bb0289403447a0b814ef8aefb90d2b0c3567501eb3570011ee96514db6050747a1d5cba50fe6e0c3e639d8208c549d61a502eab8772eceafeee940a586fc0ad92ce57773e307a5055604b9ea68c780617d6a797467d357d185fe9eb4279719d35faaab1105ab77299bd10f3504c439058720ed738a1c16756a44ba348bb193ec7d3dd9f11ac3d0cc1277ace098ed0ed8e25a01e83216a9c6a183a2659ddf2d971bc4a9b5ce776e1b07242a89a15026a3d208bbb8e3f91086f2785be68b5e4122f9d79583b883266ba6dfb4cf0c22737915dda898e1461651818c17f811f51f496f0842223c") + + val result = SuccessPacket.decrypt(Some(fulfillmentPayload0), Some(attribution0), sharedSecrets) + assert(result.fulfillmentPayload_opt.contains(fulfillmentPayload)) + assert(result.holdTimes == Seq(HoldTime(500 millisecond, publicKeys(0)), HoldTime(400 milliseconds, publicKeys(1)), HoldTime(300 milliseconds, publicKeys(2)), HoldTime(200 milliseconds, publicKeys(3)), HoldTime(100 milliseconds, publicKeys(4)))) + } + test("only some nodes in the route support attributable failures") { for ((payloads, packetPayloadLength) <- Seq((referencePaymentPayloads, 1300), (paymentPayloadsFull, 1300))) { // origin build the onion packet diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/AuditDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/AuditDbSpec.scala index 5eaf7c4f79..3bb0a48c7d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/AuditDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/AuditDbSpec.scala @@ -130,16 +130,16 @@ class AuditDbSpec extends AnyFunSuite { val preimage2 = randomBytes32() val paymentHash2 = Crypto.sha256(preimage2) - val e1 = PaymentSent(ZERO_UUID, preimage1, 40000 msat, remoteNodeId2, PaymentSent.PaymentPart(ZERO_UUID, PaymentEvent.OutgoingPayment(channelId1, remoteNodeId1, 42000 msat, now - 75.seconds), 1000 msat, None, now - 100.seconds) :: Nil, None, now - 100.seconds) + val e1 = PaymentSent(ZERO_UUID, preimage1, 40000 msat, remoteNodeId2, PaymentSent.PaymentPart(ZERO_UUID, PaymentEvent.OutgoingPayment(channelId1, remoteNodeId1, 42000 msat, now - 75.seconds), 1000 msat, None, now - 100.seconds) :: Nil, None, None, now - 100.seconds) val pp2a = PaymentEvent.IncomingPayment(channelId1, remoteNodeId1, 42000 msat, now - 1.seconds) val pp2b = PaymentEvent.IncomingPayment(channelId2, remoteNodeId2, 42100 msat, now) val e2 = PaymentReceived(paymentHash1, pp2a :: pp2b :: Nil) val e3 = ChannelPaymentRelayed(paymentHash1, Seq(PaymentEvent.IncomingPayment(channelId1, remoteNodeId1, 42000 msat, now - 3.seconds)), Seq(PaymentEvent.OutgoingPayment(channelId2, remoteNodeId2, 1000 msat, now))) val pp4a = PaymentSent.PaymentPart(uuid1, PaymentEvent.OutgoingPayment(channelId1, remoteNodeId1, 42000 msat, now - 15.seconds), 1000 msat, None, startedAt = now - 30.seconds) val pp4b = PaymentSent.PaymentPart(uuid2, PaymentEvent.OutgoingPayment(channelId2, remoteNodeId2, 42100 msat, now - 10.seconds), 900 msat, None, startedAt = now - 25.seconds) - val e4 = PaymentSent(uuid3, preimage1, 84100 msat, remoteNodeId2, pp4a :: pp4b :: Nil, None, startedAt = now - 30.seconds) + val e4 = PaymentSent(uuid3, preimage1, 84100 msat, remoteNodeId2, pp4a :: pp4b :: Nil, None, None, startedAt = now - 30.seconds) val pp5 = PaymentSent.PaymentPart(uuid2, PaymentEvent.OutgoingPayment(channelId1, remoteNodeId1, 42000 msat, settledAt = now + 10.minutes), 1000 msat, None, startedAt = now + 9.minutes) - val e5 = PaymentSent(uuid2, preimage1, 42000 msat, remoteNodeId1, pp5 :: Nil, None, startedAt = now + 9.minutes) + val e5 = PaymentSent(uuid2, preimage1, 42000 msat, remoteNodeId1, pp5 :: Nil, None, None, startedAt = now + 9.minutes) val e6 = TrampolinePaymentRelayed(paymentHash1, Seq( PaymentEvent.IncomingPayment(channelId1, remoteNodeId1, 20000 msat, now - 7.seconds), @@ -351,7 +351,7 @@ class AuditDbSpec extends AnyFunSuite { val channelCreated = ChannelEvent(channelId1, remoteNodeId1, fundingTx.txid, "anchor_outputs", 100_000 sat, isChannelOpener = true, isPrivate = false, "created", now) val txPublished = TransactionPublished(channelId1, remoteNodeId1, fundingTx, 200 sat, 100 sat, "funding", None) val txConfirmed = TransactionConfirmed(channelId1, remoteNodeId1, fundingTx) - val paymentSent = PaymentSent(UUID.randomUUID(), randomBytes32(), 25_000_000 msat, remoteNodeId2, Seq(PaymentSent.PaymentPart(UUID.randomUUID(), PaymentEvent.OutgoingPayment(channelId1, remoteNodeId1, 24_999_999 msat, now), 561 msat, None, now - 10.seconds)), None, now - 10.seconds) + val paymentSent = PaymentSent(UUID.randomUUID(), randomBytes32(), 25_000_000 msat, remoteNodeId2, Seq(PaymentSent.PaymentPart(UUID.randomUUID(), PaymentEvent.OutgoingPayment(channelId1, remoteNodeId1, 24_999_999 msat, now), 561 msat, None, now - 10.seconds)), None, None, now - 10.seconds) val paymentReceived = PaymentReceived(randomBytes32(), Seq(PaymentEvent.IncomingPayment(channelId1, remoteNodeId1, 15_350 msat, now - 1.seconds))) val paymentRelayed = ChannelPaymentRelayed(randomBytes32(), Seq(PaymentEvent.IncomingPayment(channelId1, remoteNodeId1, 1100 msat, now - 1.seconds)), Seq(PaymentEvent.OutgoingPayment(channelId2, remoteNodeId2, 1000 msat, now))) forAllDbs { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/PaymentsDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/PaymentsDbSpec.scala index 9700767d92..5d1230d418 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/PaymentsDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/PaymentsDbSpec.scala @@ -200,7 +200,7 @@ class PaymentsDbSpec extends AnyFunSuite { val ps6 = OutgoingPayment(UUID.randomUUID(), UUID.randomUUID(), Some("3"), randomBytes32(), PaymentType.Standard, 789 msat, 789 msat, bob, 1250 unixms, None, None, OutgoingPaymentStatus.Failed(Nil, 1300 unixms)) db.addOutgoingPayment(ps4) db.addOutgoingPayment(ps5.copy(status = OutgoingPaymentStatus.Pending)) - db.updateOutgoingPayment(PaymentSent(ps5.parentId, preimage1, ps5.amount, ps5.recipientNodeId, Seq(PaymentSent.PaymentPart(ps5.id, PaymentEvent.OutgoingPayment(randomBytes32(), randomKey().publicKey, ps5.amount, 1180 unixms), 42 msat, None, 1000 unixms)), None, 900 unixms)) + db.updateOutgoingPayment(PaymentSent(ps5.parentId, preimage1, ps5.amount, ps5.recipientNodeId, Seq(PaymentSent.PaymentPart(ps5.id, PaymentEvent.OutgoingPayment(randomBytes32(), randomKey().publicKey, ps5.amount, 1180 unixms), 42 msat, None, 1000 unixms)), None, None, 900 unixms)) db.addOutgoingPayment(ps6.copy(status = OutgoingPaymentStatus.Pending)) db.updateOutgoingPayment(PaymentFailed(ps6.id, ps6.paymentHash, Nil, 1100 unixms, 1300 unixms)) @@ -772,12 +772,12 @@ class PaymentsDbSpec extends AnyFunSuite { assert(db.getOutgoingPayment(s4.id).contains(ss4)) // can't update again once it's in a final state - assertThrows[IllegalArgumentException](db.updateOutgoingPayment(PaymentSent(parentId, preimage1, s3.recipientAmount, s3.recipientNodeId, Seq(PaymentSent.PaymentPart(s3.id, PaymentEvent.OutgoingPayment(randomBytes32(), randomKey().publicKey, s3.amount, settledAt = 500 unixms), 42 msat, None, startedAt = 100 unixms)), None, startedAt = 100 unixms))) + assertThrows[IllegalArgumentException](db.updateOutgoingPayment(PaymentSent(parentId, preimage1, s3.recipientAmount, s3.recipientNodeId, Seq(PaymentSent.PaymentPart(s3.id, PaymentEvent.OutgoingPayment(randomBytes32(), randomKey().publicKey, s3.amount, settledAt = 500 unixms), 42 msat, None, startedAt = 100 unixms)), None, None, startedAt = 100 unixms))) val paymentSent = PaymentSent(parentId, preimage1, 600 msat, carol, Seq( PaymentSent.PaymentPart(s1.id, PaymentEvent.OutgoingPayment(randomBytes32(), randomKey().publicKey, s1.amount, settledAt = 400 unixms), 15 msat, None, startedAt = 200 unixms), PaymentSent.PaymentPart(s2.id, PaymentEvent.OutgoingPayment(randomBytes32(), randomKey().publicKey, s2.amount, settledAt = 410 unixms), 20 msat, Some(Seq(hop_ab, hop_bc)), startedAt = 210 unixms) - ), None, startedAt = 100 unixms) + ), None, None, startedAt = 100 unixms) val ss1 = s1.copy(status = OutgoingPaymentStatus.Succeeded(preimage1, 15 msat, Nil, 400 unixms)) val ss2 = s2.copy(status = OutgoingPaymentStatus.Succeeded(preimage1, 20 msat, Seq(HopSummary(alice, bob, Some(ShortChannelId(42))), HopSummary(bob, carol, None)), 410 unixms)) db.updateOutgoingPayment(paymentSent) 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 496608644c..1b2b0aa4b9 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 @@ -28,6 +28,7 @@ import fr.acinq.eclair.wire.internal.CommandCodecs.cmdCodec import fr.acinq.eclair.wire.protocol.{FailureMessageCodecs, FailureReason, UnknownNextPeer} import fr.acinq.eclair.{TimestampMilli, randomBytes, randomBytes32} import org.scalatest.funsuite.AnyFunSuite +import scodec.bits.HexStringSyntax import scala.util.Random @@ -53,8 +54,8 @@ class PendingCommandsDbSpec extends AnyFunSuite { val channelId1 = randomBytes32() val channelId2 = randomBytes32() - val msg0 = CMD_FULFILL_HTLC(0, randomBytes32(), None) - val msg1 = CMD_FULFILL_HTLC(1, randomBytes32(), Some(FulfillAttributionData(TimestampMilli(600), None, Some(randomBytes(Attribution.totalLength))))) + val msg0 = CMD_FULFILL_HTLC(0, randomBytes32(), None, None) + val msg1 = CMD_FULFILL_HTLC(1, randomBytes32(), Some(hex"deadbeef"), Some(FulfillAttributionData(TimestampMilli(600), None, Some(randomBytes(Attribution.totalLength))))) val msg2 = CMD_FAIL_HTLC(2, FailureReason.EncryptedDownstreamFailure(randomBytes32(), None), None) val msg3 = CMD_FAIL_HTLC(3, FailureReason.EncryptedDownstreamFailure(randomBytes32(), Some(randomBytes(Sphinx.Attribution.totalLength))), Some(FailureAttributionData(TimestampMilli(500), Some(TimestampMilli(550))))) val msg4 = CMD_FAIL_MALFORMED_HTLC(4, randomBytes32(), FailureMessageCodecs.BADONION) @@ -66,7 +67,7 @@ class PendingCommandsDbSpec extends AnyFunSuite { db.addSettlementCommand(channelId1, msg1) db.addSettlementCommand(channelId1, msg2) db.addSettlementCommand(channelId1, msg3) - db.addSettlementCommand(channelId1, CMD_FULFILL_HTLC(msg3.id, randomBytes32(), None)) // conflicting command + db.addSettlementCommand(channelId1, CMD_FULFILL_HTLC(msg3.id, randomBytes32(), None, None)) // conflicting command db.addSettlementCommand(channelId1, msg4) db.addSettlementCommand(channelId2, msg0) // same messages but for different channel db.addSettlementCommand(channelId2, msg1) @@ -140,7 +141,7 @@ object PendingCommandsDbSpec { val channelId = randomBytes32() val cmds = (0 until Random.nextInt(5)).map { _ => Random.nextInt(2) match { - case 0 => CMD_FULFILL_HTLC(Random.nextLong(100_000), randomBytes32(), None) + case 0 => CMD_FULFILL_HTLC(Random.nextLong(100_000), randomBytes32(), None, None) case 1 => CMD_FAIL_HTLC(Random.nextLong(100_000), FailureReason.LocalFailure(UnknownNextPeer()), None) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala index 2b51f9bb60..b2fe551862 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala @@ -174,7 +174,7 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec { // we generate a few blocks to get the commit tx confirmed generateBlocks(3, Some(minerAddress)) // we then fulfill the htlc, which will make F redeem it on-chain - sender.send(nodes("F").register, Register.Forward(sender.ref.toTyped[Any], htlc.channelId, CMD_FULFILL_HTLC(htlc.id, preimage, None))) + sender.send(nodes("F").register, Register.Forward(sender.ref.toTyped[Any], htlc.channelId, CMD_FULFILL_HTLC(htlc.id, preimage, None, None))) // we don't need to generate blocks to confirm the htlc-success; C should extract the preimage as soon as it enters // the mempool and fulfill the payment upstream. paymentSender.expectMsgType[PaymentSent](max = 60 seconds) @@ -212,7 +212,7 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec { // we generate a few blocks to get the commit tx confirmed generateBlocks(3, Some(minerAddress)) // we then fulfill the htlc (it won't be sent to C, and will be used to pull funds on-chain) - sender.send(nodes("F").register, Register.Forward(sender.ref.toTyped[Any], htlc.channelId, CMD_FULFILL_HTLC(htlc.id, preimage, None))) + sender.send(nodes("F").register, Register.Forward(sender.ref.toTyped[Any], htlc.channelId, CMD_FULFILL_HTLC(htlc.id, preimage, None, None))) // we don't need to generate blocks to confirm the htlc-success; C should extract the preimage as soon as it enters // the mempool and fulfill the payment upstream. paymentSender.expectMsgType[PaymentSent](max = 60 seconds) 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 715a241ea7..e9959a5b6e 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 @@ -608,8 +608,8 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike f.register.expectMsgAllOf( Register.Forward(null, add2.channelId, CMD_FAIL_HTLC(add2.id, FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight)), Some(FailureAttributionData(receivedAt2, None)), commit = true)), - Register.Forward(null, add1.channelId, CMD_FULFILL_HTLC(add1.id, preimage, Some(FulfillAttributionData(receivedAt1, None, None)), commit = true)), - Register.Forward(null, add3.channelId, CMD_FULFILL_HTLC(add3.id, preimage, Some(FulfillAttributionData(receivedAt3, None, None)), commit = true)) + Register.Forward(null, add1.channelId, CMD_FULFILL_HTLC(add1.id, preimage, None, Some(FulfillAttributionData(receivedAt1, None, None)), commit = true)), + Register.Forward(null, add3.channelId, CMD_FULFILL_HTLC(add3.id, preimage, None, Some(FulfillAttributionData(receivedAt3, None, None)), commit = true)) ) val paymentReceived = f.eventListener.expectMsgType[PaymentReceived] @@ -626,7 +626,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val receivedAt4 = receivedAt1 + 3.millis val receivedFrom4 = randomKey().publicKey f.sender.send(handler, MultiPartPaymentFSM.ExtraPaymentReceived(invoice.paymentHash, HtlcPart(1000 msat, UpdateAddHtlc(ByteVector32.One, 44, 200 msat, invoice.paymentHash, add1.cltvExpiry, add1.onionRoutingPacket, None, accountable = false, None), receivedFrom4, receivedAt4), None)) - f.register.expectMsg(Register.Forward(null, ByteVector32.One, CMD_FULFILL_HTLC(44, preimage, Some(FulfillAttributionData(receivedAt4, None, None)), commit = true))) + f.register.expectMsg(Register.Forward(null, ByteVector32.One, CMD_FULFILL_HTLC(44, preimage, None, Some(FulfillAttributionData(receivedAt4, None, None)), commit = true))) assert(f.eventListener.expectMsgType[PaymentReceived].amount == 200.msat) val received2 = nodeParams.db.payments.getIncomingPayment(invoice.paymentHash) assert(received2.get.status.asInstanceOf[IncomingPaymentStatus.Received].amount == 1200.msat) @@ -653,8 +653,8 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike f.sender.send(handler, ReceivePacket(IncomingPaymentPacket.FinalPacket(add2, FinalPayload.Standard.createPayload(add2.amountMsat, 1500 msat, add2.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata, upgradeAccountability = false), receivedAt2), receivedFrom2)) f.register.expectMsgAllOf( - Register.Forward(null, add1.channelId, CMD_FULFILL_HTLC(add1.id, preimage, Some(FulfillAttributionData(receivedAt1, None, None)), commit = true)), - Register.Forward(null, add2.channelId, CMD_FULFILL_HTLC(add2.id, preimage, Some(FulfillAttributionData(receivedAt2, None, None)), commit = true)) + Register.Forward(null, add1.channelId, CMD_FULFILL_HTLC(add1.id, preimage, None, Some(FulfillAttributionData(receivedAt1, None, None)), commit = true)), + Register.Forward(null, add2.channelId, CMD_FULFILL_HTLC(add2.id, preimage, None, Some(FulfillAttributionData(receivedAt2, None, None)), commit = true)) ) val paymentReceived = f.eventListener.expectMsgType[PaymentReceived] @@ -695,8 +695,8 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike // the fulfill are not necessarily in the same order as the commands f.register.expectMsgAllOf( - Register.Forward(null, add2.channelId, CMD_FULFILL_HTLC(2, preimage, Some(FulfillAttributionData(receivedAt2, None, None)), commit = true)), - Register.Forward(null, add3.channelId, CMD_FULFILL_HTLC(5, preimage, Some(FulfillAttributionData(receivedAt3, None, None)), commit = true)) + Register.Forward(null, add2.channelId, CMD_FULFILL_HTLC(2, preimage, None, Some(FulfillAttributionData(receivedAt2, None, None)), commit = true)), + Register.Forward(null, add3.channelId, CMD_FULFILL_HTLC(5, preimage, None, Some(FulfillAttributionData(receivedAt3, None, None)), commit = true)) ) val paymentReceived = f.eventListener.expectMsgType[PaymentReceived] diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala index f9023a2c77..44c8e9f96a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala @@ -580,8 +580,8 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS awaitCond(payFsm.stateName == PAYMENT_ABORTED) sender.watch(payFsm) - childPayFsm.send(payFsm, PaymentSent(cfg.id, paymentPreimage, finalAmount, e, Seq(PaymentSent.PaymentPart(successId, PaymentEvent.OutgoingPayment(randomBytes32(), randomKey().publicKey, successRoute.amount, 250 unixms), successRoute.channelFee(false), Some(successRoute.fullRoute), 100 unixms)), None, 75 unixms)) - sender.expectMsg(PreimageReceived(paymentHash, paymentPreimage, None)) + childPayFsm.send(payFsm, PaymentSent(cfg.id, paymentPreimage, finalAmount, e, Seq(PaymentSent.PaymentPart(successId, PaymentEvent.OutgoingPayment(randomBytes32(), randomKey().publicKey, successRoute.amount, 250 unixms), successRoute.channelFee(false), Some(successRoute.fullRoute), 100 unixms)), None, None, 75 unixms)) + sender.expectMsg(PreimageReceived(paymentHash, paymentPreimage, None, None)) val result = sender.expectMsgType[PaymentSent] assert(result.id == cfg.id) assert(result.paymentHash == paymentHash) @@ -608,8 +608,8 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS childPayFsm.expectMsgType[SendPaymentToRoute] val (childId, route) :: (failedId, failedRoute) :: Nil = payFsm.stateData.asInstanceOf[PaymentProgress].pending.toSeq - childPayFsm.send(payFsm, PaymentSent(cfg.id, paymentPreimage, finalAmount, e, Seq(PaymentSent.PaymentPart(childId, PaymentEvent.OutgoingPayment(randomBytes32(), randomKey().publicKey, route.amount, 250 unixms), route.channelFee(false), Some(route.fullRoute), 100 unixms)), None, 75 unixms)) - sender.expectMsg(PreimageReceived(paymentHash, paymentPreimage, None)) + childPayFsm.send(payFsm, PaymentSent(cfg.id, paymentPreimage, finalAmount, e, Seq(PaymentSent.PaymentPart(childId, PaymentEvent.OutgoingPayment(randomBytes32(), randomKey().publicKey, route.amount, 250 unixms), route.channelFee(false), Some(route.fullRoute), 100 unixms)), None, None, 75 unixms)) + sender.expectMsg(PreimageReceived(paymentHash, paymentPreimage, None, None)) awaitCond(payFsm.stateName == PAYMENT_SUCCEEDED) sender.watch(payFsm) @@ -634,8 +634,8 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS val partialPayments = pending.map { case (childId, route) => PaymentSent.PaymentPart(childId, PaymentEvent.OutgoingPayment(randomBytes32(), randomKey().publicKey, route.amount + route.channelFee(false) + route.blindedFee, 250 unixms), route.channelFee(false) + route.blindedFee, Some(route.fullRoute), 100 unixms) } - partialPayments.foreach(pp => childPayFsm.send(payFsm, PaymentSent(cfg.id, paymentPreimage, finalAmount, e, Seq(pp), None, 100 unixms))) - sender.expectMsg(PreimageReceived(paymentHash, paymentPreimage, None)) + partialPayments.foreach(pp => childPayFsm.send(payFsm, PaymentSent(cfg.id, paymentPreimage, finalAmount, e, Seq(pp), None, None, 100 unixms))) + sender.expectMsg(PreimageReceived(paymentHash, paymentPreimage, None, None)) val result = sender.expectMsgType[PaymentSent] assert(result.id == cfg.id) assert(result.paymentHash == paymentHash) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala index 731c0755af..adf24bd154 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala @@ -231,7 +231,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike sender.send(initiator, GetPayment(PaymentIdentifier.PaymentHash(invoice.paymentHash))) sender.expectMsg(PaymentIsPending(id, invoice.paymentHash, PendingPaymentToNode(sender.ref, req))) - val ps = PaymentSent(id, paymentPreimage, finalAmount, priv_c.publicKey, Seq(PaymentPart(UUID.randomUUID(), PaymentEvent.OutgoingPayment(randomBytes32(), randomKey().publicKey, finalAmount, 200 unixms), 0 msat, None, 100 unixms)), None, 80 unixms) + val ps = PaymentSent(id, paymentPreimage, finalAmount, priv_c.publicKey, Seq(PaymentPart(UUID.randomUUID(), PaymentEvent.OutgoingPayment(randomBytes32(), randomKey().publicKey, finalAmount, 200 unixms), 0 msat, None, 100 unixms)), None, None, 80 unixms) payFsm.send(initiator, ps) sender.expectMsg(ps) eventListener.expectNoMessage(100 millis) @@ -350,7 +350,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike sender.send(initiator, GetPayment(PaymentIdentifier.PaymentHash(invoice.paymentHash))) sender.expectMsg(PaymentIsPending(id, invoice.paymentHash, PendingPaymentToNode(sender.ref, req))) - val ps = PaymentSent(id, paymentPreimage, finalAmount, invoice.nodeId, Seq(PaymentPart(UUID.randomUUID(), PaymentEvent.OutgoingPayment(randomBytes32(), randomKey().publicKey, finalAmount, 200 unixms), 0 msat, None, 100 unixms)), None, 100 unixms) + val ps = PaymentSent(id, paymentPreimage, finalAmount, invoice.nodeId, Seq(PaymentPart(UUID.randomUUID(), PaymentEvent.OutgoingPayment(randomBytes32(), randomKey().publicKey, finalAmount, 200 unixms), 0 msat, None, 100 unixms)), None, None, 100 unixms) payFsm.send(initiator, ps) sender.expectMsg(ps) eventListener.expectNoMessage(100 millis) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala index fab1526e66..08986ca898 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala @@ -847,7 +847,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { sender.send(paymentFSM, addCompleted(HtlcResult.OnChainFulfill(defaultPaymentPreimage))) val paymentOK = sender.expectMsgType[PaymentSent] - val PaymentSent(_, paymentOK.paymentPreimage, finalAmount, _, PaymentPart(_, part, fee, _, _) :: Nil, _, _) = eventListener.expectMsgType[PaymentSent] + val PaymentSent(_, paymentOK.paymentPreimage, finalAmount, _, PaymentPart(_, part, fee, _, _) :: Nil, _, _, _) = eventListener.expectMsgType[PaymentSent] assert(part.amount == request.amount) assert(finalAmount == defaultAmountMsat) 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 38f3306b61..93bbf80fe8 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 @@ -39,7 +39,7 @@ import fr.acinq.eclair.transactions.Transactions.ZeroFeeHtlcTxAnchorOutputsCommi import fr.acinq.eclair.wire.protocol.OfferTypes.{InvoiceRequest, Offer, PaymentInfo} import fr.acinq.eclair.wire.protocol.PaymentOnion.{FinalPayload, IntermediatePayload, OutgoingBlindedPerHopPayload} import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{BlockHeight, Bolt11Feature, Bolt12Feature, CltvExpiry, CltvExpiryDelta, EncodedNodeId, Features, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TestConstants, TimestampMilli, TimestampMilliLong, TimestampSecondLong, UInt64, nodeFee, randomBytes32, randomKey} +import fr.acinq.eclair.{BlockHeight, Bolt11Feature, Bolt12Feature, CltvExpiry, CltvExpiryDelta, EncodedNodeId, Features, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TestConstants, TimestampMilli, TimestampMilliLong, TimestampSecondLong, UInt64, nodeFee, randomBytes, randomBytes32, randomKey} import org.scalatest.BeforeAndAfterAll import org.scalatest.funsuite.AnyFunSuite import scodec.bits.{ByteVector, HexStringSyntax} @@ -643,6 +643,144 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { assert(failure.isInstanceOf[FinalIncorrectCltvExpiry]) } + test("build htlc success fulfillment payload") { + // a -> b -> c -> d -> e + val recipient = ClearRecipient(e, Features.empty, finalAmount, finalExpiry, paymentSecret, upgradeAccountability = false) + val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, Route(finalAmount, hops, None), recipient, Reputation.Score.max(accountable = false)) + val add_b = UpdateAddHtlc(randomBytes32(), 0, amount_ab, paymentHash, expiry_ab, payment.cmd.onion, None, accountable = false, None) + val Right(ChannelRelayPacket(_, _, packet_c, _)) = decrypt(add_b, priv_b.privateKey, Features.empty) + val add_c = UpdateAddHtlc(randomBytes32(), 1, amount_bc, paymentHash, expiry_bc, packet_c, None, accountable = false, None) + val Right(ChannelRelayPacket(_, _, packet_d, _)) = decrypt(add_c, priv_c.privateKey, Features.empty) + val add_d = UpdateAddHtlc(randomBytes32(), 2, amount_cd, paymentHash, expiry_cd, packet_d, None, accountable = false, None) + val Right(ChannelRelayPacket(_, _, packet_e, _)) = decrypt(add_d, priv_d.privateKey, Features.empty) + val add_e = UpdateAddHtlc(randomBytes32(), 3, amount_de, paymentHash, expiry_de, packet_e, None, accountable = false, None) + val Right(FinalPacket(_, payload_e, _)) = decrypt(add_e, priv_e.privateKey, Features.empty) + assert(payload_e.isInstanceOf[FinalPayload.Standard]) + + // e fulfills the payment and includes a fulfillment payload + val cmd_e = CMD_FULFILL_HTLC(add_e.id, paymentPreimage, Some(randomBytes(256)), Some(FulfillAttributionData(670 unixms, None, None))) + val fulfill_e = buildHtlcFulfill(priv_e.privateKey, useAttributionData = true, cmd_e, add_e, now = 800 unixms) + val cmd_d = CMD_FULFILL_HTLC(add_d.id, paymentPreimage, fulfill_e.fulfillmentPayload_opt, Some(FulfillAttributionData(650 unixms, None, fulfill_e.attribution_opt))) + val fulfill_d = buildHtlcFulfill(priv_d.privateKey, useAttributionData = true, cmd_d, add_d, now = 810 unixms) + val cmd_c = CMD_FULFILL_HTLC(add_c.id, paymentPreimage, fulfill_d.fulfillmentPayload_opt, Some(FulfillAttributionData(560 unixms, None, fulfill_d.attribution_opt))) + val fulfill_c = buildHtlcFulfill(priv_c.privateKey, useAttributionData = true, cmd_c, add_c, now = 815 unixms) + val cmd_b = CMD_FULFILL_HTLC(add_b.id, paymentPreimage, fulfill_c.fulfillmentPayload_opt, Some(FulfillAttributionData(490 unixms, None, fulfill_c.attribution_opt))) + val fulfill_b = buildHtlcFulfill(priv_b.privateKey, useAttributionData = true, cmd_b, add_b, now = 830 unixms) + val successDetails = Sphinx.SuccessPacket.decrypt(fulfill_b.fulfillmentPayload_opt, fulfill_b.attribution_opt, payment.sharedSecrets) + assert(successDetails.fulfillmentPayload_opt == cmd_e.fulfillmentPayload_opt) + assert(successDetails.holdTimes == Seq(HoldTime(300 millis, b), HoldTime(200 millis, c), HoldTime(100 millis, d), HoldTime(100 millis, e))) + } + + test("build htlc success fulfillment payload (malicious intermediate node)") { + // a -> b -> c -> d -> e + val recipient = ClearRecipient(e, Features.empty, finalAmount, finalExpiry, paymentSecret, upgradeAccountability = false) + val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, Route(finalAmount, hops, None), recipient, Reputation.Score.max(accountable = false)) + val add_b = UpdateAddHtlc(randomBytes32(), 0, amount_ab, paymentHash, expiry_ab, payment.cmd.onion, None, accountable = false, None) + val Right(ChannelRelayPacket(_, _, packet_c, _)) = decrypt(add_b, priv_b.privateKey, Features.empty) + val add_c = UpdateAddHtlc(randomBytes32(), 1, amount_bc, paymentHash, expiry_bc, packet_c, None, accountable = false, None) + val Right(ChannelRelayPacket(_, _, packet_d, _)) = decrypt(add_c, priv_c.privateKey, Features.empty) + val add_d = UpdateAddHtlc(randomBytes32(), 2, amount_cd, paymentHash, expiry_cd, packet_d, None, accountable = false, None) + val Right(ChannelRelayPacket(_, _, packet_e, _)) = decrypt(add_d, priv_d.privateKey, Features.empty) + val add_e = UpdateAddHtlc(randomBytes32(), 3, amount_de, paymentHash, expiry_de, packet_e, None, accountable = false, None) + val Right(FinalPacket(_, payload_e, _)) = decrypt(add_e, priv_e.privateKey, Features.empty) + assert(payload_e.isInstanceOf[FinalPayload.Standard]) + + // e fulfills the payment and includes a fulfillment payload + val cmd_e = CMD_FULFILL_HTLC(add_e.id, paymentPreimage, Some(randomBytes(256)), Some(FulfillAttributionData(670 unixms, None, None))) + val fulfill_e = buildHtlcFulfill(priv_e.privateKey, useAttributionData = true, cmd_e, add_e, now = 800 unixms) + val cmd_d = CMD_FULFILL_HTLC(add_d.id, paymentPreimage, fulfill_e.fulfillmentPayload_opt, Some(FulfillAttributionData(650 unixms, None, fulfill_e.attribution_opt))) + // d removes attribution data and modifies the fulfillment payload + val fulfill_d = buildHtlcFulfill(priv_d.privateKey, useAttributionData = true, cmd_d, add_d, now = 810 unixms).copy( + tlvStream = TlvStream(UpdateFulfillHtlcTlv.FulfillmentPayload(randomBytes(256))) + ) + val cmd_c = CMD_FULFILL_HTLC(add_c.id, paymentPreimage, fulfill_d.fulfillmentPayload_opt, Some(FulfillAttributionData(560 unixms, None, fulfill_d.attribution_opt))) + val fulfill_c = buildHtlcFulfill(priv_c.privateKey, useAttributionData = true, cmd_c, add_c, now = 815 unixms) + val cmd_b = CMD_FULFILL_HTLC(add_b.id, paymentPreimage, fulfill_c.fulfillmentPayload_opt, Some(FulfillAttributionData(490 unixms, None, fulfill_c.attribution_opt))) + val fulfill_b = buildHtlcFulfill(priv_b.privateKey, useAttributionData = true, cmd_b, add_b, now = 830 unixms) + val successDetails = Sphinx.SuccessPacket.decrypt(fulfill_b.fulfillmentPayload_opt, fulfill_b.attribution_opt, payment.sharedSecrets) + // a can attribute the issue to either c or d because it is missing attribution data after c + assert(successDetails.fulfillmentPayload_opt.isEmpty) + assert(successDetails.holdTimes == Seq(HoldTime(300 millis, b), HoldTime(200 millis, c))) + } + + test("build htlc success fulfillment payload (blinded payment)") { + // a -> b -> c -> d -> e, blinded after c + val (_, route, recipient) = longBlindedHops(hex"0451") + val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, route, recipient, Reputation.Score.max(accountable = false)) + val add_b = UpdateAddHtlc(randomBytes32(), 0, payment.cmd.amount, payment.cmd.paymentHash, payment.cmd.cltvExpiry, payment.cmd.onion, payment.cmd.nextPathKey_opt, accountable = false, payment.cmd.fundingFee_opt) + val Right(ChannelRelayPacket(_, _, packet_c, _)) = decrypt(add_b, priv_b.privateKey, Features.empty) + val add_c = UpdateAddHtlc(randomBytes32(), 1, amount_bc, paymentHash, expiry_bc, packet_c, None, accountable = false, None) + val Right(ChannelRelayPacket(_, payload_c, packet_d, _)) = decrypt(add_c, priv_c.privateKey, Features(RouteBlinding -> Optional)) + val pathKey_d = payload_c.asInstanceOf[IntermediatePayload.ChannelRelay.Blinded].nextPathKey + val add_d = UpdateAddHtlc(randomBytes32(), 2, amount_cd, paymentHash, expiry_cd, packet_d, Some(pathKey_d), accountable = false, None) + val Right(ChannelRelayPacket(_, payload_d, packet_e, _)) = decrypt(add_d, priv_d.privateKey, Features(RouteBlinding -> Optional)) + val pathKey_e = payload_d.asInstanceOf[IntermediatePayload.ChannelRelay.Blinded].nextPathKey + val add_e = UpdateAddHtlc(randomBytes32(), 3, amount_de, paymentHash, expiry_de, packet_e, Some(pathKey_e), accountable = false, None) + val Right(FinalPacket(_, payload_e, _)) = decrypt(add_e, priv_e.privateKey, Features(RouteBlinding -> Optional)) + assert(payload_e.isInstanceOf[FinalPayload.Blinded]) + + // all nodes include the fulfillment payload, even though attribution data is not included inside the blinded path + val cmd_e = CMD_FULFILL_HTLC(add_e.id, paymentPreimage, Some(randomBytes(256)), Some(FulfillAttributionData(820 unixms, None, None))) + val fulfill_e = buildHtlcFulfill(priv_e.privateKey, useAttributionData = true, cmd_e, add_e, now = 850 unixms) + assert(fulfill_e.attribution_opt.isEmpty) + assert(fulfill_e.fulfillmentPayload_opt.nonEmpty) + val cmd_d = CMD_FULFILL_HTLC(add_d.id, paymentPreimage, fulfill_e.fulfillmentPayload_opt, Some(FulfillAttributionData(810 unixms, None, fulfill_e.attribution_opt))) + val fulfill_d = buildHtlcFulfill(priv_d.privateKey, useAttributionData = true, cmd_d, add_d, now = 850 unixms) + assert(fulfill_d.attribution_opt.isEmpty) + assert(fulfill_d.fulfillmentPayload_opt.nonEmpty) + val cmd_c = CMD_FULFILL_HTLC(add_c.id, paymentPreimage, fulfill_d.fulfillmentPayload_opt, Some(FulfillAttributionData(750 unixms, None, fulfill_d.attribution_opt))) + val fulfill_c = buildHtlcFulfill(priv_c.privateKey, useAttributionData = true, cmd_c, add_c, now = 860 unixms) + assert(fulfill_c.attribution_opt.nonEmpty) + assert(fulfill_c.fulfillmentPayload_opt.nonEmpty) + val cmd_b = CMD_FULFILL_HTLC(add_b.id, paymentPreimage, fulfill_c.fulfillmentPayload_opt, Some(FulfillAttributionData(670 unixms, None, fulfill_c.attribution_opt))) + val fulfill_b = buildHtlcFulfill(priv_b.privateKey, useAttributionData = true, cmd_b, add_b, now = 870 unixms) + val successDetails = Sphinx.SuccessPacket.decrypt(fulfill_b.fulfillmentPayload_opt, fulfill_b.attribution_opt, payment.sharedSecrets) + assert(successDetails.fulfillmentPayload_opt == cmd_e.fulfillmentPayload_opt) + assert(successDetails.holdTimes == Seq(HoldTime(200 millis, b), HoldTime(100 millis, c))) + } + + test("build htlc success fulfillment payload (trampoline payment)") { + // Create a trampoline payment to e: + // .--> d --. + // / \ + // b -> c e + val invoiceFeatures = Features[Bolt11Feature](VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional, Features.TrampolinePaymentPrototype -> Optional) + val invoice = Bolt11Invoice(Block.RegtestGenesisBlock.hash, Some(finalAmount), paymentHash, priv_e.privateKey, Left("invoice"), CltvExpiryDelta(12), paymentSecret = paymentSecret, features = invoiceFeatures) + val payment = TrampolinePayment.buildOutgoingPayment(c, invoice, finalExpiry) + val add_c = UpdateAddHtlc(randomBytes32(), 0, payment.trampolineAmount, paymentHash, payment.trampolineExpiry, payment.onion.packet, None, accountable = false, None) + val Right(RelayToTrampolinePacket(_, _, payload_c, trampolinePacket_e, _)) = decrypt(add_c, priv_c.privateKey, Features.empty) + val (add_d, sharedSecrets_c) = { + // c finds a path c->d->e + val payloads = Seq( + NodePayload(d, PaymentOnion.IntermediatePayload.ChannelRelay.Standard(channelUpdate_de.shortChannelId, payload_c.amountToForward, payload_c.outgoingCltv, upgradeAccountability = false)), + NodePayload(e, PaymentOnion.FinalPayload.Standard.createTrampolinePayload(payload_c.amountToForward, payload_c.amountToForward, payload_c.outgoingCltv, paymentSecret, trampolinePacket_e, upgradeAccountability = false)) + ) + val onion_d = OutgoingPaymentPacket.buildOnion(payloads, paymentHash, Some(PaymentOnionCodecs.paymentOnionPayloadLength)).toOption.get + val add_d = UpdateAddHtlc(randomBytes32(), 0, payload_c.amountToForward + 500.msat, paymentHash, payload_c.outgoingCltv + CltvExpiryDelta(36), onion_d.packet, None, accountable = false, None) + (add_d, onion_d.sharedSecrets) + } + val Right(ChannelRelayPacket(_, _, packet_e, _)) = decrypt(add_d, priv_d.privateKey, Features.empty) + val add_e = UpdateAddHtlc(randomBytes32(), 3, payload_c.amountToForward, paymentHash, payload_c.outgoingCltv, packet_e, None, accountable = false, None) + val Right(FinalPacket(_, payload_e, _)) = decrypt(add_e, priv_e.privateKey, Features.empty) + assert(payload_e.isInstanceOf[FinalPayload.Standard]) + + // e fulfills the payment and includes a fulfillment payload + val cmd_e = CMD_FULFILL_HTLC(add_e.id, paymentPreimage, Some(randomBytes(256)), Some(FulfillAttributionData(800 unixms, Some(800 unixms), None))) + val fulfill_e = buildHtlcFulfill(priv_e.privateKey, useAttributionData = true, cmd_e, add_e, now = 850 unixms) + val cmd_d = CMD_FULFILL_HTLC(add_d.id, paymentPreimage, fulfill_e.fulfillmentPayload_opt, Some(FulfillAttributionData(750 unixms, None, fulfill_e.attribution_opt))) + val fulfill_d = buildHtlcFulfill(priv_d.privateKey, useAttributionData = true, cmd_d, add_d, now = 860 unixms) + // c needs to unwrap the fulfillment payload and attribution data from downstream + val success_c = Sphinx.SuccessPacket.decrypt(fulfill_d.fulfillmentPayload_opt, fulfill_d.attribution_opt, sharedSecrets_c, fullRoute = false) + assert(success_c.fulfillmentPayload_opt.nonEmpty) + assert(success_c.remainingAttribution_opt.nonEmpty) + assert(success_c.holdTimes == Seq(HoldTime(100 millis, d), HoldTime(0 millis, e))) + val cmd_c = CMD_FULFILL_HTLC(add_c.id, paymentPreimage, success_c.fulfillmentPayload_opt, Some(FulfillAttributionData(700 unixms, Some(700 unixms), success_c.remainingAttribution_opt))) + val fulfill_c = buildHtlcFulfill(priv_c.privateKey, useAttributionData = true, cmd_c, add_c, now = 875 unixms) + val successDetails = Sphinx.SuccessPacket.decrypt(fulfill_c.fulfillmentPayload_opt, fulfill_c.attribution_opt, payment.onion.sharedSecrets ++ payment.trampolineOnion.sharedSecrets) + assert(successDetails.fulfillmentPayload_opt == cmd_e.fulfillmentPayload_opt) + assert(successDetails.holdTimes == Seq(HoldTime(100 millis, c), HoldTime(100 millis, c), HoldTime(0 millis, e))) + } + test("build htlc failure onion") { // a -> b -> c -> d -> e val recipient = ClearRecipient(e, Features.empty, finalAmount, finalExpiry, paymentSecret, upgradeAccountability = false) 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 b346bccad1..d2fcbfa83b 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 @@ -226,8 +226,8 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit 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, FailureReason.LocalFailure(TemporaryNodeFailure()), None, commit = true), - CMD_FULFILL_HTLC(3, preimage, None, commit = true), - CMD_FULFILL_HTLC(5, preimage, None, commit = true), + CMD_FULFILL_HTLC(3, preimage, None, None, commit = true), + CMD_FULFILL_HTLC(5, preimage, None, None, commit = true), CMD_FAIL_HTLC(7, FailureReason.LocalFailure(TemporaryNodeFailure()), None, commit = true) ) val received1 = expected1.map(_ => channel.expectMsgType[Command]) @@ -239,7 +239,7 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit val expected2 = Set( CMD_FAIL_HTLC(1, FailureReason.LocalFailure(TemporaryNodeFailure()), None, commit = true), CMD_FAIL_HTLC(3, FailureReason.LocalFailure(TemporaryNodeFailure()), None, commit = true), - CMD_FULFILL_HTLC(4, preimage, None, commit = true), + CMD_FULFILL_HTLC(4, preimage, None, None, commit = true), CMD_FAIL_HTLC(9, FailureReason.LocalFailure(TemporaryNodeFailure()), None, commit = true) ) val received2 = expected2.map(_ => channel.expectMsgType[Command]) @@ -456,8 +456,8 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit val origin_2 = Origin.Cold(Upstream.Cold(upstream_2)) sender.send(relayer, RES_ADD_SETTLED(origin_2, a, htlc_2_2, HtlcResult.OnChainFulfill(preimage2))) register.expectMsgAllOf( - Register.Forward(replyTo = null, channelId_ab_1, CMD_FULFILL_HTLC(5, preimage2, None, commit = true)), - Register.Forward(replyTo = null, channelId_ab_2, CMD_FULFILL_HTLC(9, preimage2, None, commit = true)) + Register.Forward(replyTo = null, channelId_ab_1, CMD_FULFILL_HTLC(5, preimage2, None, None, commit = true)), + Register.Forward(replyTo = null, channelId_ab_2, CMD_FULFILL_HTLC(9, preimage2, None, None, commit = true)) ) // Payment 3 should not be failed: we are still waiting for on-chain confirmation. @@ -474,7 +474,7 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit buildHtlcIn(4, channelId_ab_1, randomBytes32()), ) val channelData = ChannelCodecsSpec.makeChannelDataNormal(htlc_ab, Map.empty) - nodeParams.db.pendingCommands.addSettlementCommand(channelId_ab_1, CMD_FULFILL_HTLC(1, randomBytes32(), None)) + nodeParams.db.pendingCommands.addSettlementCommand(channelId_ab_1, CMD_FULFILL_HTLC(1, randomBytes32(), None, None)) nodeParams.db.pendingCommands.addSettlementCommand(channelId_ab_1, CMD_FAIL_HTLC(4, FailureReason.LocalFailure(PermanentChannelFailure()), None)) val (_, postRestart) = f.createRelayer(nodeParams) @@ -571,7 +571,7 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit register.expectNoMessage(100 millis) sender.send(relayer, buildForwardFulfill(testCase.downstream, testCase.upstream, preimage1)) - register.expectMsg(Register.Forward(null, testCase.upstream.originChannelId, CMD_FULFILL_HTLC(testCase.upstream.originHtlcId, preimage1, None, commit = true))) + register.expectMsg(Register.Forward(null, testCase.upstream.originChannelId, CMD_FULFILL_HTLC(testCase.upstream.originHtlcId, preimage1, None, None, commit = true))) eventListener.expectMsgType[ChannelPaymentRelayed] sender.send(relayer, buildForwardFulfill(testCase.downstream, testCase.upstream, preimage1)) @@ -622,7 +622,7 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit sender.send(relayer, buildForwardFulfill(testCase.downstream_1_1, testCase.upstream_1, preimage1)) val fulfills = register.expectMsgType[Register.Forward[CMD_FULFILL_HTLC]] :: register.expectMsgType[Register.Forward[CMD_FULFILL_HTLC]] :: Nil assert(fulfills.toSet == testCase.upstream_1.originHtlcs.map { - case Upstream.Cold.Channel(channelId, _, htlcId, _) => Register.Forward(null, channelId, CMD_FULFILL_HTLC(htlcId, preimage1, None, commit = true)) + case Upstream.Cold.Channel(channelId, _, htlcId, _) => Register.Forward(null, channelId, CMD_FULFILL_HTLC(htlcId, preimage1, None, None, commit = true)) }.toSet) sender.send(relayer, buildForwardFulfill(testCase.downstream_1_1, testCase.upstream_1, preimage1)) @@ -631,7 +631,7 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit // This payment has 3 downstream HTLCs, but we should fulfill upstream as soon as we receive the preimage. sender.send(relayer, buildForwardFulfill(testCase.downstream_2_1, testCase.upstream_2, preimage2)) register.expectMsg(testCase.upstream_2.originHtlcs.map { - case Upstream.Cold.Channel(channelId, _, htlcId, _) => Register.Forward(null, channelId, CMD_FULFILL_HTLC(htlcId, preimage2, None, commit = true)) + case Upstream.Cold.Channel(channelId, _, htlcId, _) => Register.Forward(null, channelId, CMD_FULFILL_HTLC(htlcId, preimage2, None, None, commit = true)) }.head) sender.send(relayer, buildForwardFulfill(testCase.downstream_2_2, testCase.upstream_2, preimage2)) @@ -651,7 +651,7 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit sender.send(relayer, buildForwardFail(testCase.downstream_2_1, testCase.upstream_2)) sender.send(relayer, buildForwardFulfill(testCase.downstream_2_2, testCase.upstream_2, preimage2)) register.expectMsg(testCase.upstream_2.originHtlcs.map { - case Upstream.Cold.Channel(channelId, _, htlcId, _) => Register.Forward(null, channelId, CMD_FULFILL_HTLC(htlcId, preimage2, None, commit = true)) + case Upstream.Cold.Channel(channelId, _, htlcId, _) => Register.Forward(null, channelId, CMD_FULFILL_HTLC(htlcId, preimage2, None, None, commit = true)) }.head) sender.send(relayer, buildForwardFail(testCase.downstream_2_3, testCase.upstream_2)) 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 2eef4c1f5d..8a3048eb52 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 @@ -387,11 +387,11 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl val nodeRelayerAdapters = outgoingPayment.replyTo // A first downstream HTLC is fulfilled: we should immediately forward the fulfill upstream. - nodeRelayerAdapters ! PreimageReceived(paymentHash, paymentPreimage, None) + nodeRelayerAdapters ! PreimageReceived(paymentHash, paymentPreimage, None, None) incomingAsyncPayment.foreach { p => val fwd = register.expectMessageType[Register.Forward[CMD_FULFILL_HTLC]] assert(fwd.channelId == p.add.channelId) - assert(fwd.message == CMD_FULFILL_HTLC(p.add.id, paymentPreimage, Some(FulfillAttributionData(p.receivedAt, Some(incomingAsyncPayment.last.receivedAt), None)), commit = true)) + assert(fwd.message == CMD_FULFILL_HTLC(p.add.id, paymentPreimage, None, Some(FulfillAttributionData(p.receivedAt, Some(incomingAsyncPayment.last.receivedAt), None)), commit = true)) } // Once all the downstream payments have settled, we should emit the relayed event. @@ -616,15 +616,15 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl val nodeRelayerAdapters = outgoingPayment.replyTo // A first downstream HTLC is fulfilled: we should immediately forward the fulfill upstream. - nodeRelayerAdapters ! PreimageReceived(paymentHash, paymentPreimage, None) + nodeRelayerAdapters ! PreimageReceived(paymentHash, paymentPreimage, None, None) incomingMultiPart.foreach { p => val fwd = register.expectMessageType[Register.Forward[CMD_FULFILL_HTLC]] assert(fwd.channelId == p.add.channelId) - assert(fwd.message == CMD_FULFILL_HTLC(p.add.id, paymentPreimage, Some(FulfillAttributionData(p.receivedAt, Some(incomingMultiPart.last.receivedAt), None)), commit = true)) + assert(fwd.message == CMD_FULFILL_HTLC(p.add.id, paymentPreimage, None, Some(FulfillAttributionData(p.receivedAt, Some(incomingMultiPart.last.receivedAt), None)), commit = true)) } // If the payment FSM sends us duplicate preimage events, we should not fulfill a second time upstream. - nodeRelayerAdapters ! PreimageReceived(paymentHash, paymentPreimage, None) + nodeRelayerAdapters ! PreimageReceived(paymentHash, paymentPreimage, None, None) register.expectNoMessage(100 millis) // Once all the downstream payments have settled, we should emit the relayed event. @@ -654,11 +654,11 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl val nodeRelayerAdapters = outgoingPayment.replyTo // A first downstream HTLC is fulfilled: we immediately forward the fulfill upstream. - nodeRelayerAdapters ! PreimageReceived(paymentHash, paymentPreimage, None) + nodeRelayerAdapters ! PreimageReceived(paymentHash, paymentPreimage, None, None) val fulfills = incomingMultiPart.map { p => val fwd = register.expectMessageType[Register.Forward[CMD_FULFILL_HTLC]] assert(fwd.channelId == p.add.channelId) - assert(fwd.message == CMD_FULFILL_HTLC(p.add.id, paymentPreimage, Some(FulfillAttributionData(p.receivedAt, Some(incomingMultiPart.last.receivedAt), None)), commit = true)) + assert(fwd.message == CMD_FULFILL_HTLC(p.add.id, paymentPreimage, None, Some(FulfillAttributionData(p.receivedAt, Some(incomingMultiPart.last.receivedAt), None)), commit = true)) fwd } // We store the commands in our DB in case we restart before relaying them upstream. @@ -697,11 +697,11 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl // those are adapters for pay-fsm messages val nodeRelayerAdapters = outgoingPayment.replyTo - nodeRelayerAdapters ! PreimageReceived(paymentHash, paymentPreimage, None) + nodeRelayerAdapters ! PreimageReceived(paymentHash, paymentPreimage, None, None) val incomingAdd = incomingSinglePart.add val fwd = register.expectMessageType[Register.Forward[CMD_FULFILL_HTLC]] assert(fwd.channelId == incomingAdd.channelId) - assert(fwd.message == CMD_FULFILL_HTLC(incomingAdd.id, paymentPreimage, Some(FulfillAttributionData(incomingSinglePart.receivedAt, Some(incomingSinglePart.receivedAt), None)), commit = true)) + assert(fwd.message == CMD_FULFILL_HTLC(incomingAdd.id, paymentPreimage, None, Some(FulfillAttributionData(incomingSinglePart.receivedAt, Some(incomingSinglePart.receivedAt), None)), commit = true)) nodeRelayerAdapters ! createSuccessEvent() val relayEvent = eventListener.expectMessageType[TrampolinePaymentRelayed] @@ -828,11 +828,11 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl // those are adapters for pay-fsm messages val nodeRelayerAdapters = outgoingPayment.replyTo - nodeRelayerAdapters ! PreimageReceived(paymentHash, paymentPreimage, None) + nodeRelayerAdapters ! PreimageReceived(paymentHash, paymentPreimage, None, None) incomingMultiPart.foreach { p => val fwd = register.expectMessageType[Register.Forward[CMD_FULFILL_HTLC]] assert(fwd.channelId == p.add.channelId) - assert(fwd.message == CMD_FULFILL_HTLC(p.add.id, paymentPreimage, Some(FulfillAttributionData(p.receivedAt, Some(incomingMultiPart.last.receivedAt), None)), commit = true)) + assert(fwd.message == CMD_FULFILL_HTLC(p.add.id, paymentPreimage, None, Some(FulfillAttributionData(p.receivedAt, Some(incomingMultiPart.last.receivedAt), None)), commit = true)) } nodeRelayerAdapters ! createSuccessEvent() @@ -876,11 +876,11 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl // those are adapters for pay-fsm messages val nodeRelayerAdapters = outgoingPayment.replyTo - nodeRelayerAdapters ! PreimageReceived(paymentHash, paymentPreimage, None) + nodeRelayerAdapters ! PreimageReceived(paymentHash, paymentPreimage, None, None) incomingMultiPart.foreach { p => val fwd = register.expectMessageType[Register.Forward[CMD_FULFILL_HTLC]] assert(fwd.channelId == p.add.channelId) - assert(fwd.message == CMD_FULFILL_HTLC(p.add.id, paymentPreimage, Some(FulfillAttributionData(p.receivedAt, Some(incomingMultiPart.last.receivedAt), None)), commit = true)) + assert(fwd.message == CMD_FULFILL_HTLC(p.add.id, paymentPreimage, None, Some(FulfillAttributionData(p.receivedAt, Some(incomingMultiPart.last.receivedAt), None)), commit = true)) } nodeRelayerAdapters ! createSuccessEvent() @@ -909,11 +909,11 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl // those are adapters for pay-fsm messages val nodeRelayerAdapters = outgoingPayment.replyTo - nodeRelayerAdapters ! PreimageReceived(paymentHash, paymentPreimage, None) + nodeRelayerAdapters ! PreimageReceived(paymentHash, paymentPreimage, None, None) incomingPayments.foreach { p => val fwd = register.expectMessageType[Register.Forward[CMD_FULFILL_HTLC]] assert(fwd.channelId == p.add.channelId) - assert(fwd.message == CMD_FULFILL_HTLC(p.add.id, paymentPreimage, Some(FulfillAttributionData(p.receivedAt, Some(incomingMultiPart.last.receivedAt), None)), commit = true)) + assert(fwd.message == CMD_FULFILL_HTLC(p.add.id, paymentPreimage, None, Some(FulfillAttributionData(p.receivedAt, Some(incomingMultiPart.last.receivedAt), None)), commit = true)) } nodeRelayerAdapters ! createSuccessEvent() @@ -942,11 +942,11 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl // those are adapters for pay-fsm messages val nodeRelayerAdapters = outgoingPayment.replyTo - nodeRelayerAdapters ! PreimageReceived(paymentHash, paymentPreimage, None) + nodeRelayerAdapters ! PreimageReceived(paymentHash, paymentPreimage, None, None) incomingPayments.foreach { p => val fwd = register.expectMessageType[Register.Forward[CMD_FULFILL_HTLC]] assert(fwd.channelId == p.add.channelId) - assert(fwd.message == CMD_FULFILL_HTLC(p.add.id, paymentPreimage, Some(FulfillAttributionData(p.receivedAt, Some(incomingMultiPart.last.receivedAt), None)), commit = true)) + assert(fwd.message == CMD_FULFILL_HTLC(p.add.id, paymentPreimage, None, Some(FulfillAttributionData(p.receivedAt, Some(incomingMultiPart.last.receivedAt), None)), commit = true)) } nodeRelayerAdapters ! createSuccessEvent() @@ -984,11 +984,11 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl // those are adapters for pay-fsm messages val nodeRelayerAdapters = outgoingPayment.replyTo - nodeRelayerAdapters ! PreimageReceived(paymentHash, paymentPreimage, None) + nodeRelayerAdapters ! PreimageReceived(paymentHash, paymentPreimage, None, None) incomingPayments.foreach { p => val fwd = register.expectMessageType[Register.Forward[CMD_FULFILL_HTLC]] assert(fwd.channelId == p.add.channelId) - assert(fwd.message == CMD_FULFILL_HTLC(p.add.id, paymentPreimage, Some(FulfillAttributionData(p.receivedAt, Some(incomingMultiPart.last.receivedAt), None)), commit = true)) + assert(fwd.message == CMD_FULFILL_HTLC(p.add.id, paymentPreimage, None, Some(FulfillAttributionData(p.receivedAt, Some(incomingMultiPart.last.receivedAt), None)), commit = true)) } nodeRelayerAdapters ! createSuccessEvent() @@ -1115,11 +1115,11 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl // those are adapters for pay-fsm messages val nodeRelayerAdapters = outgoingPayment.replyTo - nodeRelayerAdapters ! PreimageReceived(paymentHash, paymentPreimage, None) + nodeRelayerAdapters ! PreimageReceived(paymentHash, paymentPreimage, None, None) incomingPayments.foreach { p => val fwd = register.expectMessageType[Register.Forward[CMD_FULFILL_HTLC]] assert(fwd.channelId == p.add.channelId) - assert(fwd.message == CMD_FULFILL_HTLC(p.add.id, paymentPreimage, Some(FulfillAttributionData(p.receivedAt, Some(incomingMultiPart.last.receivedAt), None)), commit = true)) + assert(fwd.message == CMD_FULFILL_HTLC(p.add.id, paymentPreimage, None, Some(FulfillAttributionData(p.receivedAt, Some(incomingMultiPart.last.receivedAt), None)), commit = true)) } nodeRelayerAdapters ! createSuccessEvent() @@ -1216,7 +1216,7 @@ object NodeRelayerSpec { (paymentPackets.map(_.outerPayload.expiry).min - nodeParams.relayParams.asyncPaymentsParams.cancelSafetyBeforeTimeout).blockHeight def createSuccessEvent(): PaymentSent = - PaymentSent(relayId, paymentPreimage, outgoingAmount, outgoingNodeId, Seq(PaymentSent.PaymentPart(UUID.randomUUID(), PaymentEvent.OutgoingPayment(randomBytes32(), randomKey().publicKey, outgoingAmount, 50 unixms), 10 msat, None, 0 unixms)), None, 0 unixms) + PaymentSent(relayId, paymentPreimage, outgoingAmount, outgoingNodeId, Seq(PaymentSent.PaymentPart(UUID.randomUUID(), PaymentEvent.OutgoingPayment(randomBytes32(), randomKey().publicKey, outgoingAmount, 50 unixms), 10 msat, None, 0 unixms)), None, None, 0 unixms) def createTrampolinePacket(amount: MilliSatoshi, expiry: CltvExpiry): OnionRoutingPacket = { val payload = NodePayload(outgoingNodeId, FinalPayload.Standard.createPayload(amount, amount, expiry, paymentSecret, upgradeAccountability = false)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/profit/PeerStatsTrackerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/profit/PeerStatsTrackerSpec.scala index ecc5eb1496..233a3c6a8e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/profit/PeerStatsTrackerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/profit/PeerStatsTrackerSpec.scala @@ -365,7 +365,7 @@ class PeerStatsTrackerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load( db.add(PaymentSent( UUID.randomUUID(), randomBytes32(), sentAmount - sentFees, randomKey().publicKey, PaymentSent.PaymentPart(UUID.randomUUID(), OutgoingPayment(channelId1, remoteNodeId1, sentAmount, sentAt), sentFees, None, sentAt - 10.seconds) :: Nil, - None, sentAt - 10.seconds + None, None, sentAt - 10.seconds )) // PaymentReceived through remoteNodeId2, 2 days ago. @@ -407,7 +407,7 @@ class PeerStatsTrackerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load( db.add(PaymentSent( UUID.randomUUID(), randomBytes32(), 500_000_000 msat, randomKey().publicKey, PaymentSent.PaymentPart(UUID.randomUUID(), OutgoingPayment(channelId1, remoteNodeId1, 500_000_000 msat, oldAt), 0 msat, None, oldAt) :: Nil, - None, oldAt + None, None, oldAt )) // Spawn tracker with channels for both peers. 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 586e188da7..654fd5c58b 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 @@ -31,9 +31,9 @@ class CommandCodecsSpec extends AnyFunSuite { test("encode/decode all settlement commands") { val testCases: Map[HtlcSettlementCommand, ByteVector] = Map( - CMD_FULFILL_HTLC(1573, ByteVector32(hex"e64e7c07667366e517886af99a25a5dd547014c95ba392ea4623fbf47fe00927"), Some(FulfillAttributionData(TimestampMilli(123), None, None))) -> hex"0008 0000000000000625 e64e7c07667366e517886af99a25a5dd547014c95ba392ea4623fbf47fe00927 ff 000000000000007b 00 00", - CMD_FULFILL_HTLC(1573, ByteVector32(hex"e64e7c07667366e517886af99a25a5dd547014c95ba392ea4623fbf47fe00927"), Some(FulfillAttributionData(TimestampMilli(123), Some(TimestampMilli(125)), None))) -> hex"0008 0000000000000625 e64e7c07667366e517886af99a25a5dd547014c95ba392ea4623fbf47fe00927 ff 000000000000007b ff 000000000000007d 00", - CMD_FULFILL_HTLC(1573, ByteVector32(hex"e64e7c07667366e517886af99a25a5dd547014c95ba392ea4623fbf47fe00927"), Some(FulfillAttributionData(TimestampMilli(123), None, Some(hex"636ac35e3d982332a8347578a2753f443fa53b7ca3ab9ce5486eeb857ebbf4c49eed261430e666aa5a724484e9ea4a194c3c21d7d1fbce42c13376e9b1c420b6d8c90cb3883af13a73c4d541ebd83e0fc6366263110922cd71133babfa375dc1e0aa8d898dfd152805a48cfc16e7fac88a3a298caf7511d23ab5ecd4eafeb0275a6f736f25d07f6d8182a75f7e692705a3ac481400bf7944f27f611bfb4cdffdfb726f7d877d3c031ff16992f67b7f6e8951aa8b974d245520487cb4a8d9149d0bce892e7af8da9997d679556d515f849a5acf028fbaa81290c681124b14957b1beba8b2716c5ace289fe495ddf3222dcf00764f73d3a4f979586327d460ae61ddbb3b249efa73b3ca8dfc1105a88bdd0b9470fe17b052f954ba41b75c6d28e5da27bd05f63a20cdde22211a37dd460137bb90bf684ac47f1a5e3a82b1ed7550414282d02bca3920f2d0db1064ede422de2151872586462623b280a99284a2faa571aab1b92da43ad331e82480cb18bfc62a9a20b2614e1daf28ed5a3ef294296bbca459edf90becf4a883e1486c0bf9db8a0a842b1868482447aed4fdb28f8de201d9aa6009a2e0a85b560f8333665400fad8d3f75fc924e72f1ef259db26dbf0a59813146fff405876bdde9b877d5828a933d6cccab461a81d3791712f65353863b64b7045a0d1bc27157dd71ad9c18913421a077e4d073d0c50480b0fa0f6b160ed72fd106e3c9aea00683f4532549dbc9d40a81f66b20ec4c9ee1a873bbbb6ca4b167ee163a001880e455da43e7d9c00bf747fdf3587b3407afe2de86c5f5a76a3052866260514bb06b433f64bf6ca2f69c335ac5afada81c801c708be9872c8a242ed93e5e9ebb70ec9b615a083700db350463d3e9368f6d778635663cfaaa3b725a03298e736e43a9201d222dff7d54e045da37ea84b0d607c7244ad46a90aa77d7905dae4a2160caadd1282918f80f8659a559f29f2eefc5709625d7169fd8abb9c532772fbdce93fd9b6495bad4f184a3cc39a302555cf1361d68c0b1528d38bb10f20638e407f3252f053f15d0b21ae26046c29b3a83ac2e8d69e354ce4178df615bc46cd66cc65e9206dfbb3b0d6613071c1d92d6f1116513f38777ee686771dcacc8ad5d8b615ca48ca6da267f48ea401bd2abd0e251dd5e1953c4f5d5619eb31e4d91176db345f9f269225d7598384ecc0a7eb0e24bccd1dc9a6822e41fb7c2fff7baddee630ff4d7ed08f15c241b15568ced090e2d8f94b51c8cb14d5b4f450ceda4008907f2e31ae636579a383499aaa85")))) -> hex"0008 0000000000000625 e64e7c07667366e517886af99a25a5dd547014c95ba392ea4623fbf47fe00927 ff 000000000000007b 00 ff 636ac35e3d982332a8347578a2753f443fa53b7ca3ab9ce5486eeb857ebbf4c49eed261430e666aa5a724484e9ea4a194c3c21d7d1fbce42c13376e9b1c420b6d8c90cb3883af13a73c4d541ebd83e0fc6366263110922cd71133babfa375dc1e0aa8d898dfd152805a48cfc16e7fac88a3a298caf7511d23ab5ecd4eafeb0275a6f736f25d07f6d8182a75f7e692705a3ac481400bf7944f27f611bfb4cdffdfb726f7d877d3c031ff16992f67b7f6e8951aa8b974d245520487cb4a8d9149d0bce892e7af8da9997d679556d515f849a5acf028fbaa81290c681124b14957b1beba8b2716c5ace289fe495ddf3222dcf00764f73d3a4f979586327d460ae61ddbb3b249efa73b3ca8dfc1105a88bdd0b9470fe17b052f954ba41b75c6d28e5da27bd05f63a20cdde22211a37dd460137bb90bf684ac47f1a5e3a82b1ed7550414282d02bca3920f2d0db1064ede422de2151872586462623b280a99284a2faa571aab1b92da43ad331e82480cb18bfc62a9a20b2614e1daf28ed5a3ef294296bbca459edf90becf4a883e1486c0bf9db8a0a842b1868482447aed4fdb28f8de201d9aa6009a2e0a85b560f8333665400fad8d3f75fc924e72f1ef259db26dbf0a59813146fff405876bdde9b877d5828a933d6cccab461a81d3791712f65353863b64b7045a0d1bc27157dd71ad9c18913421a077e4d073d0c50480b0fa0f6b160ed72fd106e3c9aea00683f4532549dbc9d40a81f66b20ec4c9ee1a873bbbb6ca4b167ee163a001880e455da43e7d9c00bf747fdf3587b3407afe2de86c5f5a76a3052866260514bb06b433f64bf6ca2f69c335ac5afada81c801c708be9872c8a242ed93e5e9ebb70ec9b615a083700db350463d3e9368f6d778635663cfaaa3b725a03298e736e43a9201d222dff7d54e045da37ea84b0d607c7244ad46a90aa77d7905dae4a2160caadd1282918f80f8659a559f29f2eefc5709625d7169fd8abb9c532772fbdce93fd9b6495bad4f184a3cc39a302555cf1361d68c0b1528d38bb10f20638e407f3252f053f15d0b21ae26046c29b3a83ac2e8d69e354ce4178df615bc46cd66cc65e9206dfbb3b0d6613071c1d92d6f1116513f38777ee686771dcacc8ad5d8b615ca48ca6da267f48ea401bd2abd0e251dd5e1953c4f5d5619eb31e4d91176db345f9f269225d7598384ecc0a7eb0e24bccd1dc9a6822e41fb7c2fff7baddee630ff4d7ed08f15c241b15568ced090e2d8f94b51c8cb14d5b4f450ceda4008907f2e31ae636579a383499aaa85", + CMD_FULFILL_HTLC(1573, ByteVector32(hex"e64e7c07667366e517886af99a25a5dd547014c95ba392ea4623fbf47fe00927"), None, Some(FulfillAttributionData(TimestampMilli(123), None, None))) -> hex"0009 0000000000000625 e64e7c07667366e517886af99a25a5dd547014c95ba392ea4623fbf47fe00927 00 ff 000000000000007b 00 00", + CMD_FULFILL_HTLC(1573, ByteVector32(hex"e64e7c07667366e517886af99a25a5dd547014c95ba392ea4623fbf47fe00927"), Some(hex"deadbeef"), Some(FulfillAttributionData(TimestampMilli(123), Some(TimestampMilli(125)), None))) -> hex"0009 0000000000000625 e64e7c07667366e517886af99a25a5dd547014c95ba392ea4623fbf47fe00927 ff0004deadbeef ff 000000000000007b ff 000000000000007d 00", + CMD_FULFILL_HTLC(1573, ByteVector32(hex"e64e7c07667366e517886af99a25a5dd547014c95ba392ea4623fbf47fe00927"), Some(hex"deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"), Some(FulfillAttributionData(TimestampMilli(123), None, Some(hex"636ac35e3d982332a8347578a2753f443fa53b7ca3ab9ce5486eeb857ebbf4c49eed261430e666aa5a724484e9ea4a194c3c21d7d1fbce42c13376e9b1c420b6d8c90cb3883af13a73c4d541ebd83e0fc6366263110922cd71133babfa375dc1e0aa8d898dfd152805a48cfc16e7fac88a3a298caf7511d23ab5ecd4eafeb0275a6f736f25d07f6d8182a75f7e692705a3ac481400bf7944f27f611bfb4cdffdfb726f7d877d3c031ff16992f67b7f6e8951aa8b974d245520487cb4a8d9149d0bce892e7af8da9997d679556d515f849a5acf028fbaa81290c681124b14957b1beba8b2716c5ace289fe495ddf3222dcf00764f73d3a4f979586327d460ae61ddbb3b249efa73b3ca8dfc1105a88bdd0b9470fe17b052f954ba41b75c6d28e5da27bd05f63a20cdde22211a37dd460137bb90bf684ac47f1a5e3a82b1ed7550414282d02bca3920f2d0db1064ede422de2151872586462623b280a99284a2faa571aab1b92da43ad331e82480cb18bfc62a9a20b2614e1daf28ed5a3ef294296bbca459edf90becf4a883e1486c0bf9db8a0a842b1868482447aed4fdb28f8de201d9aa6009a2e0a85b560f8333665400fad8d3f75fc924e72f1ef259db26dbf0a59813146fff405876bdde9b877d5828a933d6cccab461a81d3791712f65353863b64b7045a0d1bc27157dd71ad9c18913421a077e4d073d0c50480b0fa0f6b160ed72fd106e3c9aea00683f4532549dbc9d40a81f66b20ec4c9ee1a873bbbb6ca4b167ee163a001880e455da43e7d9c00bf747fdf3587b3407afe2de86c5f5a76a3052866260514bb06b433f64bf6ca2f69c335ac5afada81c801c708be9872c8a242ed93e5e9ebb70ec9b615a083700db350463d3e9368f6d778635663cfaaa3b725a03298e736e43a9201d222dff7d54e045da37ea84b0d607c7244ad46a90aa77d7905dae4a2160caadd1282918f80f8659a559f29f2eefc5709625d7169fd8abb9c532772fbdce93fd9b6495bad4f184a3cc39a302555cf1361d68c0b1528d38bb10f20638e407f3252f053f15d0b21ae26046c29b3a83ac2e8d69e354ce4178df615bc46cd66cc65e9206dfbb3b0d6613071c1d92d6f1116513f38777ee686771dcacc8ad5d8b615ca48ca6da267f48ea401bd2abd0e251dd5e1953c4f5d5619eb31e4d91176db345f9f269225d7598384ecc0a7eb0e24bccd1dc9a6822e41fb7c2fff7baddee630ff4d7ed08f15c241b15568ced090e2d8f94b51c8cb14d5b4f450ceda4008907f2e31ae636579a383499aaa85")))) -> hex"0009 0000000000000625 e64e7c07667366e517886af99a25a5dd547014c95ba392ea4623fbf47fe00927 ff0100deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef ff 000000000000007b 00 ff 636ac35e3d982332a8347578a2753f443fa53b7ca3ab9ce5486eeb857ebbf4c49eed261430e666aa5a724484e9ea4a194c3c21d7d1fbce42c13376e9b1c420b6d8c90cb3883af13a73c4d541ebd83e0fc6366263110922cd71133babfa375dc1e0aa8d898dfd152805a48cfc16e7fac88a3a298caf7511d23ab5ecd4eafeb0275a6f736f25d07f6d8182a75f7e692705a3ac481400bf7944f27f611bfb4cdffdfb726f7d877d3c031ff16992f67b7f6e8951aa8b974d245520487cb4a8d9149d0bce892e7af8da9997d679556d515f849a5acf028fbaa81290c681124b14957b1beba8b2716c5ace289fe495ddf3222dcf00764f73d3a4f979586327d460ae61ddbb3b249efa73b3ca8dfc1105a88bdd0b9470fe17b052f954ba41b75c6d28e5da27bd05f63a20cdde22211a37dd460137bb90bf684ac47f1a5e3a82b1ed7550414282d02bca3920f2d0db1064ede422de2151872586462623b280a99284a2faa571aab1b92da43ad331e82480cb18bfc62a9a20b2614e1daf28ed5a3ef294296bbca459edf90becf4a883e1486c0bf9db8a0a842b1868482447aed4fdb28f8de201d9aa6009a2e0a85b560f8333665400fad8d3f75fc924e72f1ef259db26dbf0a59813146fff405876bdde9b877d5828a933d6cccab461a81d3791712f65353863b64b7045a0d1bc27157dd71ad9c18913421a077e4d073d0c50480b0fa0f6b160ed72fd106e3c9aea00683f4532549dbc9d40a81f66b20ec4c9ee1a873bbbb6ca4b167ee163a001880e455da43e7d9c00bf747fdf3587b3407afe2de86c5f5a76a3052866260514bb06b433f64bf6ca2f69c335ac5afada81c801c708be9872c8a242ed93e5e9ebb70ec9b615a083700db350463d3e9368f6d778635663cfaaa3b725a03298e736e43a9201d222dff7d54e045da37ea84b0d607c7244ad46a90aa77d7905dae4a2160caadd1282918f80f8659a559f29f2eefc5709625d7169fd8abb9c532772fbdce93fd9b6495bad4f184a3cc39a302555cf1361d68c0b1528d38bb10f20638e407f3252f053f15d0b21ae26046c29b3a83ac2e8d69e354ce4178df615bc46cd66cc65e9206dfbb3b0d6613071c1d92d6f1116513f38777ee686771dcacc8ad5d8b615ca48ca6da267f48ea401bd2abd0e251dd5e1953c4f5d5619eb31e4d91176db345f9f269225d7598384ecc0a7eb0e24bccd1dc9a6822e41fb7c2fff7baddee630ff4d7ed08f15c241b15568ced090e2d8f94b51c8cb14d5b4f450ceda4008907f2e31ae636579a383499aaa85", CMD_FAIL_HTLC(42456, FailureReason.EncryptedDownstreamFailure(hex"d21a88a158067efecbee41da24e1d7407747f135e585e7417843729d3bff5c160817d14ce569761d93749a23d227edc0ade99c1a8d59541e45e1f623af2602d568a9a3c3bca71f1b4860ae0b599ba016c58224eab7721ed930eb2bdfd83ff940cc9e8106b0bd6b2027821f8d102b8c680664d90ce9e69d8bb96453a7b495710b83c13e4b3085bb0156b7091ed927305c44", None), Some(FailureAttributionData(TimestampMilli(123456), None))) -> hex"0007 000000000000a5d8 02 0091 d21a88a158067efecbee41da24e1d7407747f135e585e7417843729d3bff5c160817d14ce569761d93749a23d227edc0ade99c1a8d59541e45e1f623af2602d568a9a3c3bca71f1b4860ae0b599ba016c58224eab7721ed930eb2bdfd83ff940cc9e8106b0bd6b2027821f8d102b8c680664d90ce9e69d8bb96453a7b495710b83c13e4b3085bb0156b7091ed927305c44 00 ff 000000000001e240 00", CMD_FAIL_HTLC(42456, FailureReason.EncryptedDownstreamFailure(hex"d21a88a158067efecbee41da24e1d7407747f135e585e7417843729d3bff5c160817d14ce569761d93749a23d227edc0ade99c1a8d59541e45e1f623af2602d568a9a3c3bca71f1b4860ae0b599ba016c58224eab7721ed930eb2bdfd83ff940cc9e8106b0bd6b2027821f8d102b8c680664d90ce9e69d8bb96453a7b495710b83c13e4b3085bb0156b7091ed927305c44", Some(hex"c8e7a432d1db82765a5e4cecf360b439ff9cabe795f470249065a1b8d893c9ac5142092e9260b073caadc7097d3d0aa6b5f8181c02b8eb9ecb9c17a9867e8e533766e79c00844321a7a15afcd562cefbde4cc7a66865b57192f770f68260cc09e133876181921018ade17ac1bdca027af5073b5e354cbe888651a0592a228de9019b1136b702e56424c84e01ec60cee5df2ad7179196809bcbf58e17f91fd851f91fd2a5be1ea5efb43ea18d1a5b8ad2d1fe69a9f29fbea84565c34ca3088e848e5c4297f7bacea917f332a311beb365f3f131d21871120fd3fdeefb0566cb56e8dac56cdb1eee4ebb2bfcdc954326566cfb42481e7ef5fb3031ac9190e8e02f35a9430ce7cf5167f75e5c016056e2dba3022acabfe20a6891f57cfcf0abf09102b7af1b91223badca2eb865e8a523b141dcf91955631e4efd7e9664205e89aaa2282826ac65e9651620dc3392231f8f28821271da0ce9c5eb3f145837aefde0e5b33b5cb8f847de6caa51b3488baedbb706012c9b7034919f23b7c043d3e484f4be9ab72b9b37985c34c21b5ffcbb40e9b11fe83661cef912c97f5a6b4cce76518a0245d45d0fb2844b2853457a982a9418fa83934fead109f8a38ac23c3a03bbb32d573d2349bb2228c8a53efc65e9165526b53034e53d4f8540960129657b88e28e75c9f8d26f48c4d2cc86006456f03e0bff14262e97f94d64e58369798e43bb89f6f1ae731301eded7bce4ec21613670be6939ad17b8b6d4a9ca059cc5ed33dcc7c7608dda6e2810c7d76b20fa23f5e8adb91fa895ef2ff030086168e16bcc4a4376d36f2c169e5821bb40316df88e456a8b99057f130b8ad1f097e7c9f81d64c816531a325e6b517de4022bea321a41a92386ce50ca271ff1d2ebaf0694e57545e624ef3f76eae1454314136276bf1bab91e3fcdf541eb60052fc65932505be888e3ec1de782e27a3689727128cb1018fbd4ca7b51916ba05280f1004cf5bfdde6159453e2936b76842c1d978e34d0a5f65ba2a27dd235538c2875a1ca9433b7a799aa30e28facb6603e8644da1ae9a8cf169df35f905a366aada5e0c4fc1a7f9cb36f75a12983fd9e43d3339d506d37aebd9886a52f98cc330c812605508525d8c8ad5b93a9c08c5d41123dbd7e15644f61a9ab5758aa2615cc1781d997f4d2177d4b56c4be0276e67debb4cd01e398e8a6d9d3ceac030a01235b965b1a733f2710251bb1638cdb77894667aecedb1bd56b8e9979f938a4f9a66a9da44a6d6727fdfa01641b021d0d89061370a0cb2fa68d1f242a")), Some(FailureAttributionData(TimestampMilli(123456), None))) -> hex"0007 000000000000a5d8 02 0091 d21a88a158067efecbee41da24e1d7407747f135e585e7417843729d3bff5c160817d14ce569761d93749a23d227edc0ade99c1a8d59541e45e1f623af2602d568a9a3c3bca71f1b4860ae0b599ba016c58224eab7721ed930eb2bdfd83ff940cc9e8106b0bd6b2027821f8d102b8c680664d90ce9e69d8bb96453a7b495710b83c13e4b3085bb0156b7091ed927305c44 ff c8e7a432d1db82765a5e4cecf360b439ff9cabe795f470249065a1b8d893c9ac5142092e9260b073caadc7097d3d0aa6b5f8181c02b8eb9ecb9c17a9867e8e533766e79c00844321a7a15afcd562cefbde4cc7a66865b57192f770f68260cc09e133876181921018ade17ac1bdca027af5073b5e354cbe888651a0592a228de9019b1136b702e56424c84e01ec60cee5df2ad7179196809bcbf58e17f91fd851f91fd2a5be1ea5efb43ea18d1a5b8ad2d1fe69a9f29fbea84565c34ca3088e848e5c4297f7bacea917f332a311beb365f3f131d21871120fd3fdeefb0566cb56e8dac56cdb1eee4ebb2bfcdc954326566cfb42481e7ef5fb3031ac9190e8e02f35a9430ce7cf5167f75e5c016056e2dba3022acabfe20a6891f57cfcf0abf09102b7af1b91223badca2eb865e8a523b141dcf91955631e4efd7e9664205e89aaa2282826ac65e9651620dc3392231f8f28821271da0ce9c5eb3f145837aefde0e5b33b5cb8f847de6caa51b3488baedbb706012c9b7034919f23b7c043d3e484f4be9ab72b9b37985c34c21b5ffcbb40e9b11fe83661cef912c97f5a6b4cce76518a0245d45d0fb2844b2853457a982a9418fa83934fead109f8a38ac23c3a03bbb32d573d2349bb2228c8a53efc65e9165526b53034e53d4f8540960129657b88e28e75c9f8d26f48c4d2cc86006456f03e0bff14262e97f94d64e58369798e43bb89f6f1ae731301eded7bce4ec21613670be6939ad17b8b6d4a9ca059cc5ed33dcc7c7608dda6e2810c7d76b20fa23f5e8adb91fa895ef2ff030086168e16bcc4a4376d36f2c169e5821bb40316df88e456a8b99057f130b8ad1f097e7c9f81d64c816531a325e6b517de4022bea321a41a92386ce50ca271ff1d2ebaf0694e57545e624ef3f76eae1454314136276bf1bab91e3fcdf541eb60052fc65932505be888e3ec1de782e27a3689727128cb1018fbd4ca7b51916ba05280f1004cf5bfdde6159453e2936b76842c1d978e34d0a5f65ba2a27dd235538c2875a1ca9433b7a799aa30e28facb6603e8644da1ae9a8cf169df35f905a366aada5e0c4fc1a7f9cb36f75a12983fd9e43d3339d506d37aebd9886a52f98cc330c812605508525d8c8ad5b93a9c08c5d41123dbd7e15644f61a9ab5758aa2615cc1781d997f4d2177d4b56c4be0276e67debb4cd01e398e8a6d9d3ceac030a01235b965b1a733f2710251bb1638cdb77894667aecedb1bd56b8e9979f938a4f9a66a9da44a6d6727fdfa01641b021d0d89061370a0cb2fa68d1f242a ff 000000000001e240 00", CMD_FAIL_HTLC(253, FailureReason.LocalFailure(TemporaryNodeFailure()), Some(FailureAttributionData(TimestampMilli(123), None))) -> hex"0007 00000000000000fd 01 0002 2002 ff 000000000000007b 00", @@ -41,7 +41,6 @@ class CommandCodecsSpec extends AnyFunSuite { CMD_FAIL_HTLC(253, FailureReason.LocalFailure(TemporaryNodeFailure(TlvStream(Set.empty[FailureMessageTlv], Set(GenericTlv(UInt64(17), hex"deadbeef"))))), Some(FailureAttributionData(TimestampMilli(456), None))) -> hex"0007 00000000000000fd 01 0008 2002 1104deadbeef ff 00000000000001c8 00", CMD_FAIL_MALFORMED_HTLC(7984, ByteVector32(hex"17cc093e177c7a7fcaa9e96ab407146c8886546a5690f945c98ac20c4ab3b4f3"), FailureMessageCodecs.BADONION) -> hex"0002 0000000000001f30 17cc093e177c7a7fcaa9e96ab407146c8886546a5690f945c98ac20c4ab3b4f38000", ) - testCases.foreach { case (command, bin) => val encoded = CommandCodecs.cmdCodec.encode(command).require.bytes assert(encoded == bin) @@ -54,8 +53,8 @@ class CommandCodecsSpec extends AnyFunSuite { val data32 = ByteVector32(hex"e4927c04913251b44d0a3a8e57ded746fee80ff3b424e70dad2a1428eeba86cb") val data123 = hex"fea75bb8cf45349eb544d8da832af5af30eefa671ec27cf2e4867bacada2dbe00a6ce5141164aa153ac8b4b25c75c3af15c4b5cb6a293607751a079bc546da17f654b76a74bc57b6b21ed73d2d3909f3682f01b85418a0f0ecddb759e9481d4563a572ac1ddcb77c64ae167d8dfbd889703cb5c33b4b9636bad472" val testCases = Map( - hex"0000 000000000000002ae4927c04913251b44d0a3a8e57ded746fee80ff3b424e70dad2a1428eeba86cb" -> CMD_FULFILL_HTLC(42, data32, None, commit = false, None), - hex"0000 0000000000000625 e64e7c07667366e517886af99a25a5dd547014c95ba392ea4623fbf47fe00927" -> CMD_FULFILL_HTLC(1573, ByteVector32(hex"e64e7c07667366e517886af99a25a5dd547014c95ba392ea4623fbf47fe00927"), None), + hex"0000 000000000000002ae4927c04913251b44d0a3a8e57ded746fee80ff3b424e70dad2a1428eeba86cb" -> CMD_FULFILL_HTLC(42, data32, None, None, commit = false, None), + hex"0000 0000000000000625 e64e7c07667366e517886af99a25a5dd547014c95ba392ea4623fbf47fe00927" -> CMD_FULFILL_HTLC(1573, ByteVector32(hex"e64e7c07667366e517886af99a25a5dd547014c95ba392ea4623fbf47fe00927"), None, None), hex"0001 000000000000002a003dff53addc67a29a4f5aa26c6d41957ad798777d338f613e7972433dd656d16df00536728a08b2550a9d645a592e3ae1d78ae25ae5b5149b03ba8d03cde2a36d0bfb2a5bb53a5e2bdb590f6b9e969c84f9b41780dc2a0c5078766edbacf4a40ea2b1d2b9560eee5bbe32570b3ec6fdec44b81e5ae19da5cb1b5d6a3900" -> CMD_FAIL_HTLC(42, FailureReason.EncryptedDownstreamFailure(data123, None), None, None, commit = false, None), hex"0001 000000000000002a900100" -> CMD_FAIL_HTLC(42, FailureReason.LocalFailure(TemporaryNodeFailure()), None), hex"0003 000000000000a5d8 00 0091 d21a88a158067efecbee41da24e1d7407747f135e585e7417843729d3bff5c160817d14ce569761d93749a23d227edc0ade99c1a8d59541e45e1f623af2602d568a9a3c3bca71f1b4860ae0b599ba016c58224eab7721ed930eb2bdfd83ff940cc9e8106b0bd6b2027821f8d102b8c680664d90ce9e69d8bb96453a7b495710b83c13e4b3085bb0156b7091ed927305c44" -> CMD_FAIL_HTLC(42456, FailureReason.EncryptedDownstreamFailure(hex"d21a88a158067efecbee41da24e1d7407747f135e585e7417843729d3bff5c160817d14ce569761d93749a23d227edc0ade99c1a8d59541e45e1f623af2602d568a9a3c3bca71f1b4860ae0b599ba016c58224eab7721ed930eb2bdfd83ff940cc9e8106b0bd6b2027821f8d102b8c680664d90ce9e69d8bb96453a7b495710b83c13e4b3085bb0156b7091ed927305c44", None), None), @@ -65,8 +64,10 @@ class CommandCodecsSpec extends AnyFunSuite { hex"0004 000000000000a5d8 00 0091 d21a88a158067efecbee41da24e1d7407747f135e585e7417843729d3bff5c160817d14ce569761d93749a23d227edc0ade99c1a8d59541e45e1f623af2602d568a9a3c3bca71f1b4860ae0b599ba016c58224eab7721ed930eb2bdfd83ff940cc9e8106b0bd6b2027821f8d102b8c680664d90ce9e69d8bb96453a7b495710b83c13e4b3085bb0156b7091ed927305c44" -> CMD_FAIL_HTLC(42456, FailureReason.EncryptedDownstreamFailure(hex"d21a88a158067efecbee41da24e1d7407747f135e585e7417843729d3bff5c160817d14ce569761d93749a23d227edc0ade99c1a8d59541e45e1f623af2602d568a9a3c3bca71f1b4860ae0b599ba016c58224eab7721ed930eb2bdfd83ff940cc9e8106b0bd6b2027821f8d102b8c680664d90ce9e69d8bb96453a7b495710b83c13e4b3085bb0156b7091ed927305c44", None), None), hex"0004 00000000000000fd 01 0002 2002" -> CMD_FAIL_HTLC(253, FailureReason.LocalFailure(TemporaryNodeFailure()), None), hex"0004 00000000000000fd 01 0008 2002 1104deadbeef" -> CMD_FAIL_HTLC(253, FailureReason.LocalFailure(TemporaryNodeFailure(TlvStream(Set.empty[FailureMessageTlv], Set(GenericTlv(UInt64(17), hex"deadbeef"))))), None), + hex"0008 0000000000000625 e64e7c07667366e517886af99a25a5dd547014c95ba392ea4623fbf47fe00927 ff 000000000000007b 00 00" -> CMD_FULFILL_HTLC(1573, ByteVector32(hex"e64e7c07667366e517886af99a25a5dd547014c95ba392ea4623fbf47fe00927"), None, Some(FulfillAttributionData(TimestampMilli(123), None, None))), + hex"0008 0000000000000625 e64e7c07667366e517886af99a25a5dd547014c95ba392ea4623fbf47fe00927 ff 000000000000007b ff 000000000000007d 00" -> CMD_FULFILL_HTLC(1573, ByteVector32(hex"e64e7c07667366e517886af99a25a5dd547014c95ba392ea4623fbf47fe00927"), None, Some(FulfillAttributionData(TimestampMilli(123), Some(TimestampMilli(125)), None))), + hex"0008 0000000000000625 e64e7c07667366e517886af99a25a5dd547014c95ba392ea4623fbf47fe00927 ff 000000000000007b 00 ff 636ac35e3d982332a8347578a2753f443fa53b7ca3ab9ce5486eeb857ebbf4c49eed261430e666aa5a724484e9ea4a194c3c21d7d1fbce42c13376e9b1c420b6d8c90cb3883af13a73c4d541ebd83e0fc6366263110922cd71133babfa375dc1e0aa8d898dfd152805a48cfc16e7fac88a3a298caf7511d23ab5ecd4eafeb0275a6f736f25d07f6d8182a75f7e692705a3ac481400bf7944f27f611bfb4cdffdfb726f7d877d3c031ff16992f67b7f6e8951aa8b974d245520487cb4a8d9149d0bce892e7af8da9997d679556d515f849a5acf028fbaa81290c681124b14957b1beba8b2716c5ace289fe495ddf3222dcf00764f73d3a4f979586327d460ae61ddbb3b249efa73b3ca8dfc1105a88bdd0b9470fe17b052f954ba41b75c6d28e5da27bd05f63a20cdde22211a37dd460137bb90bf684ac47f1a5e3a82b1ed7550414282d02bca3920f2d0db1064ede422de2151872586462623b280a99284a2faa571aab1b92da43ad331e82480cb18bfc62a9a20b2614e1daf28ed5a3ef294296bbca459edf90becf4a883e1486c0bf9db8a0a842b1868482447aed4fdb28f8de201d9aa6009a2e0a85b560f8333665400fad8d3f75fc924e72f1ef259db26dbf0a59813146fff405876bdde9b877d5828a933d6cccab461a81d3791712f65353863b64b7045a0d1bc27157dd71ad9c18913421a077e4d073d0c50480b0fa0f6b160ed72fd106e3c9aea00683f4532549dbc9d40a81f66b20ec4c9ee1a873bbbb6ca4b167ee163a001880e455da43e7d9c00bf747fdf3587b3407afe2de86c5f5a76a3052866260514bb06b433f64bf6ca2f69c335ac5afada81c801c708be9872c8a242ed93e5e9ebb70ec9b615a083700db350463d3e9368f6d778635663cfaaa3b725a03298e736e43a9201d222dff7d54e045da37ea84b0d607c7244ad46a90aa77d7905dae4a2160caadd1282918f80f8659a559f29f2eefc5709625d7169fd8abb9c532772fbdce93fd9b6495bad4f184a3cc39a302555cf1361d68c0b1528d38bb10f20638e407f3252f053f15d0b21ae26046c29b3a83ac2e8d69e354ce4178df615bc46cd66cc65e9206dfbb3b0d6613071c1d92d6f1116513f38777ee686771dcacc8ad5d8b615ca48ca6da267f48ea401bd2abd0e251dd5e1953c4f5d5619eb31e4d91176db345f9f269225d7598384ecc0a7eb0e24bccd1dc9a6822e41fb7c2fff7baddee630ff4d7ed08f15c241b15568ced090e2d8f94b51c8cb14d5b4f450ceda4008907f2e31ae636579a383499aaa85" -> CMD_FULFILL_HTLC(1573, ByteVector32(hex"e64e7c07667366e517886af99a25a5dd547014c95ba392ea4623fbf47fe00927"), None, Some(FulfillAttributionData(TimestampMilli(123), None, Some(hex"636ac35e3d982332a8347578a2753f443fa53b7ca3ab9ce5486eeb857ebbf4c49eed261430e666aa5a724484e9ea4a194c3c21d7d1fbce42c13376e9b1c420b6d8c90cb3883af13a73c4d541ebd83e0fc6366263110922cd71133babfa375dc1e0aa8d898dfd152805a48cfc16e7fac88a3a298caf7511d23ab5ecd4eafeb0275a6f736f25d07f6d8182a75f7e692705a3ac481400bf7944f27f611bfb4cdffdfb726f7d877d3c031ff16992f67b7f6e8951aa8b974d245520487cb4a8d9149d0bce892e7af8da9997d679556d515f849a5acf028fbaa81290c681124b14957b1beba8b2716c5ace289fe495ddf3222dcf00764f73d3a4f979586327d460ae61ddbb3b249efa73b3ca8dfc1105a88bdd0b9470fe17b052f954ba41b75c6d28e5da27bd05f63a20cdde22211a37dd460137bb90bf684ac47f1a5e3a82b1ed7550414282d02bca3920f2d0db1064ede422de2151872586462623b280a99284a2faa571aab1b92da43ad331e82480cb18bfc62a9a20b2614e1daf28ed5a3ef294296bbca459edf90becf4a883e1486c0bf9db8a0a842b1868482447aed4fdb28f8de201d9aa6009a2e0a85b560f8333665400fad8d3f75fc924e72f1ef259db26dbf0a59813146fff405876bdde9b877d5828a933d6cccab461a81d3791712f65353863b64b7045a0d1bc27157dd71ad9c18913421a077e4d073d0c50480b0fa0f6b160ed72fd106e3c9aea00683f4532549dbc9d40a81f66b20ec4c9ee1a873bbbb6ca4b167ee163a001880e455da43e7d9c00bf747fdf3587b3407afe2de86c5f5a76a3052866260514bb06b433f64bf6ca2f69c335ac5afada81c801c708be9872c8a242ed93e5e9ebb70ec9b615a083700db350463d3e9368f6d778635663cfaaa3b725a03298e736e43a9201d222dff7d54e045da37ea84b0d607c7244ad46a90aa77d7905dae4a2160caadd1282918f80f8659a559f29f2eefc5709625d7169fd8abb9c532772fbdce93fd9b6495bad4f184a3cc39a302555cf1361d68c0b1528d38bb10f20638e407f3252f053f15d0b21ae26046c29b3a83ac2e8d69e354ce4178df615bc46cd66cc65e9206dfbb3b0d6613071c1d92d6f1116513f38777ee686771dcacc8ad5d8b615ca48ca6da267f48ea401bd2abd0e251dd5e1953c4f5d5619eb31e4d91176db345f9f269225d7598384ecc0a7eb0e24bccd1dc9a6822e41fb7c2fff7baddee630ff4d7ed08f15c241b15568ced090e2d8f94b51c8cb14d5b4f450ceda4008907f2e31ae636579a383499aaa85")))), ) - testCases.foreach { case (bin, command) => val decoded = CommandCodecs.cmdCodec.decode(bin.bits).require assert(decoded.value == command) diff --git a/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala b/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala index f3205591f3..1aedf9e160 100644 --- a/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala +++ b/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala @@ -624,7 +624,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM val mockService = new MockService(eclair) val uuid = UUID.fromString("487da196-a4dc-4b1e-92b4-3e5e905e9f3f") - val paymentSent = PaymentSent(uuid, ByteVector32.One, 25 msat, aliceNodeId, Seq(PaymentSent.PaymentPart(uuid, PaymentEvent.OutgoingPayment(ByteVector32.Zeroes, nextNodeId, 28 msat, TimestampMilli(1553784337711L)), 3 msat, None, TimestampMilli(1553784337650L))), None, TimestampMilli(1553784337120L)) + val paymentSent = PaymentSent(uuid, ByteVector32.One, 25 msat, aliceNodeId, Seq(PaymentSent.PaymentPart(uuid, PaymentEvent.OutgoingPayment(ByteVector32.Zeroes, nextNodeId, 28 msat, TimestampMilli(1553784337711L)), 3 msat, None, TimestampMilli(1553784337650L))), None, None, TimestampMilli(1553784337120L)) eclair.sendBlocking(any, any, any, any, any, any, any)(any[Timeout]).returns(Future.successful(paymentSent)) Post("/payinvoice", FormData("invoice" -> invoice, "blocking" -> "true").toEntity) ~> addCredentials(BasicHttpCredentials("", mockApi().password)) ~> @@ -1130,7 +1130,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM system.eventStream.publish(pf) wsClient.expectMessage(expectedSerializedPf) - val ps = PaymentSent(fixedUUID, ByteVector32.One, 25 msat, aliceNodeId, Seq(PaymentSent.PaymentPart(fixedUUID, PaymentEvent.OutgoingPayment(ByteVector32.Zeroes, nextNodeId, 28 msat, settledAt = TimestampMilli(1553784337711L)), 3 msat, None, startedAt = TimestampMilli(1553784337539L))), None, startedAt = TimestampMilli(1553784337073L)) + val ps = PaymentSent(fixedUUID, ByteVector32.One, 25 msat, aliceNodeId, Seq(PaymentSent.PaymentPart(fixedUUID, PaymentEvent.OutgoingPayment(ByteVector32.Zeroes, nextNodeId, 28 msat, settledAt = TimestampMilli(1553784337711L)), 3 msat, None, startedAt = TimestampMilli(1553784337539L))), None, None, startedAt = TimestampMilli(1553784337073L)) val expectedSerializedPs = """{"type":"payment-sent","id":"487da196-a4dc-4b1e-92b4-3e5e905e9f3f","paymentHash":"01d0fabd251fcbbe2b93b4b927b26ad2a1a99077152e45ded1e678afa45dbec5","paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","recipientAmount":25,"recipientNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","parts":[{"id":"487da196-a4dc-4b1e-92b4-3e5e905e9f3f","channelId":"0000000000000000000000000000000000000000000000000000000000000000","nextNodeId":"030bb6a5e0c6b203c7e2180fb78c7ba4bdce46126761d8201b91ddac089cdecc87","amountWithFees":28,"fees":3,"startedAt":{"iso":"2019-03-28T14:45:37.539Z","unix":1553784337},"settledAt":{"iso":"2019-03-28T14:45:37.711Z","unix":1553784337}}],"fees":3,"startedAt":{"iso":"2019-03-28T14:45:37.073Z","unix":1553784337},"settledAt":{"iso":"2019-03-28T14:45:37.711Z","unix":1553784337}}""" assert(serialization.write(ps) == expectedSerializedPs) system.eventStream.publish(ps)