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)