@@ -18,7 +18,7 @@ package fr.acinq.eclair.payment
1818
1919import fr .acinq .bitcoin .scalacompat .ByteVector32
2020import fr .acinq .bitcoin .scalacompat .Crypto .{PrivateKey , PublicKey }
21- import fr .acinq .eclair .channel .{ CMD_ADD_HTLC , CMD_FAIL_HTLC , CMD_FULFILL_HTLC , CannotExtractSharedSecret , Origin }
21+ import fr .acinq .eclair .channel ._
2222import fr .acinq .eclair .crypto .Sphinx
2323import fr .acinq .eclair .payment .send .Recipient
2424import fr .acinq .eclair .router .Router .Route
@@ -378,30 +378,81 @@ object OutgoingPaymentPacket {
378378 }
379379 }
380380
381- private def buildHtlcFailure (nodeSecret : PrivateKey , useAttributableFailures : Boolean , reason : FailureReason , add : UpdateAddHtlc , holdTime : FiniteDuration ): Either [CannotExtractSharedSecret , (ByteVector , TlvStream [UpdateFailHtlcTlv ])] = {
382- extractSharedSecret(nodeSecret, add).map(sharedSecret => {
383- val (packet, attribution) = reason match {
384- case FailureReason .EncryptedDownstreamFailure (packet, attribution) => (packet, attribution)
385- case FailureReason .LocalFailure (failure) => (Sphinx .FailurePacket .create(sharedSecret, failure), None )
381+ private def buildHtlcFailure (nodeSecret : PrivateKey , reason : FailureReason , add : UpdateAddHtlc , holdTime : FiniteDuration ): Either [CannotExtractSharedSecret , (ByteVector , Option [ByteVector ])] = {
382+ extractSharedSecret(nodeSecret, add).map(ss => {
383+ reason match {
384+ case FailureReason .EncryptedDownstreamFailure (packet, previousAttribution_opt) =>
385+ ss.trampolineOnionSecret_opt match {
386+ case Some (trampolineOnionSecret) if ! ss.blinded =>
387+ // If we are unable to decrypt the downstream failure and the payment is using trampoline, the failure is
388+ // intended for the payer. We encrypt it with the trampoline secret first and then the outer secret.
389+ val trampolinePacket = Sphinx .FailurePacket .wrap(packet, trampolineOnionSecret)
390+ val attributionInner = Sphinx .Attribution .create(previousAttribution_opt, Some (packet), holdTime, trampolineOnionSecret)
391+ val attributionOuter = Sphinx .Attribution .create(Some (attributionInner), Some (trampolinePacket), holdTime, ss.outerOnionSecret)
392+ (Sphinx .FailurePacket .wrap(trampolinePacket, ss.outerOnionSecret), Some (attributionOuter))
393+ case Some (trampolineOnionSecret) =>
394+ // When we're inside a blinded path, we don't report our attribution data.
395+ val trampolinePacket = Sphinx .FailurePacket .wrap(packet, trampolineOnionSecret)
396+ (Sphinx .FailurePacket .wrap(trampolinePacket, ss.outerOnionSecret), None )
397+ case None =>
398+ val attribution = Sphinx .Attribution .create(previousAttribution_opt, Some (packet), holdTime, ss.outerOnionSecret)
399+ (Sphinx .FailurePacket .wrap(packet, ss.outerOnionSecret), Some (attribution))
400+ }
401+ case FailureReason .LocalFailure (failure) =>
402+ // This isn't a trampoline failure, so we only encrypt it for the node who created the outer onion.
403+ val packet = Sphinx .FailurePacket .create(ss.outerOnionSecret, failure)
404+ val attribution = Sphinx .Attribution .create(previousAttribution_opt = None , Some (packet), holdTime, ss.outerOnionSecret)
405+ (Sphinx .FailurePacket .wrap(packet, ss.outerOnionSecret), Some (attribution))
406+ case FailureReason .LocalTrampolineFailure (failure) =>
407+ // This is a trampoline failure: we try to encrypt it to the node who created the trampoline onion.
408+ ss.trampolineOnionSecret_opt match {
409+ case Some (trampolineOnionSecret) if ! ss.blinded =>
410+ val packet = Sphinx .FailurePacket .create(trampolineOnionSecret, failure)
411+ val trampolinePacket = Sphinx .FailurePacket .wrap(packet, trampolineOnionSecret)
412+ val attributionInner = Sphinx .Attribution .create(previousAttribution_opt = None , Some (packet), holdTime, trampolineOnionSecret)
413+ val attributionOuter = Sphinx .Attribution .create(Some (attributionInner), Some (trampolinePacket), holdTime, ss.outerOnionSecret)
414+ (Sphinx .FailurePacket .wrap(trampolinePacket, ss.outerOnionSecret), Some (attributionOuter))
415+ case Some (trampolineOnionSecret) =>
416+ val packet = Sphinx .FailurePacket .create(trampolineOnionSecret, failure)
417+ val trampolinePacket = Sphinx .FailurePacket .wrap(packet, trampolineOnionSecret)
418+ (Sphinx .FailurePacket .wrap(trampolinePacket, ss.outerOnionSecret), None )
419+ case None =>
420+ // This shouldn't happen, we only generate trampoline failures when there was a trampoline onion.
421+ val packet = Sphinx .FailurePacket .create(ss.outerOnionSecret, failure)
422+ (Sphinx .FailurePacket .wrap(packet, ss.outerOnionSecret), None )
423+ }
386424 }
387- val tlvs : TlvStream [UpdateFailHtlcTlv ] = if (useAttributableFailures) {
388- TlvStream (UpdateFailHtlcTlv .AttributionData (Sphinx .Attribution .create(attribution, Some (packet), holdTime, sharedSecret)))
389- } else {
390- TlvStream .empty
391- }
392- (Sphinx .FailurePacket .wrap(packet, sharedSecret), tlvs)
393425 })
394426 }
395427
428+ private case class HtlcSharedSecrets (outerOnionSecret : ByteVector32 , trampolineOnionSecret_opt : Option [ByteVector32 ], blinded : Boolean )
429+
396430 /**
397431 * We decrypt the onion again to extract the shared secret used to encrypt onion failures.
398432 * We could avoid this by storing the shared secret after the initial onion decryption, but we would have to store it
399433 * in the database since we must be able to fail HTLCs after restarting our node.
400434 * It's simpler to extract it again from the encrypted onion.
401435 */
402- private def extractSharedSecret (nodeSecret : PrivateKey , add : UpdateAddHtlc ): Either [CannotExtractSharedSecret , ByteVector32 ] = {
436+ private def extractSharedSecret (nodeSecret : PrivateKey , add : UpdateAddHtlc ): Either [CannotExtractSharedSecret , HtlcSharedSecrets ] = {
403437 Sphinx .peel(nodeSecret, Some (add.paymentHash), add.onionRoutingPacket) match {
404- case Right (Sphinx .DecryptedPacket (_, _, sharedSecret)) => Right (sharedSecret)
438+ case Right (Sphinx .DecryptedPacket (payload, _, outerOnionSecret)) =>
439+ // Let's look at the onion payload to see if it contains a trampoline onion.
440+ PaymentOnionCodecs .perHopPayloadCodec.decode(payload.bits) match {
441+ case Attempt .Successful (DecodeResult (perHopPayload, _)) =>
442+ // We try to extract the trampoline shared secret, if we can find one.
443+ val trampolineOnionSecret_opt = perHopPayload.get[OnionPaymentPayloadTlv .TrampolineOnion ].map(_.packet).flatMap(trampolinePacket => {
444+ val trampolinePathKey_opt = perHopPayload.get[OnionPaymentPayloadTlv .PathKey ].map(_.publicKey)
445+ val trampolineOnionDecryptionKey = trampolinePathKey_opt.map(pathKey => Sphinx .RouteBlinding .derivePrivateKey(nodeSecret, pathKey)).getOrElse(nodeSecret)
446+ Sphinx .peel(trampolineOnionDecryptionKey, Some (add.paymentHash), trampolinePacket).toOption.map(_.sharedSecret)
447+ })
448+ // We check if we are an intermediate node in a blinded (potentially trampoline) path.
449+ val blinded = trampolineOnionSecret_opt match {
450+ case Some (_) => perHopPayload.get[OnionPaymentPayloadTlv .PathKey ].nonEmpty
451+ case None => add.pathKey_opt.nonEmpty
452+ }
453+ Right (HtlcSharedSecrets (outerOnionSecret, trampolineOnionSecret_opt, blinded))
454+ case Attempt .Failure (_) => Right (HtlcSharedSecrets (outerOnionSecret, None , blinded = false ))
455+ }
405456 case Left (_) => Left (CannotExtractSharedSecret (add.channelId, add))
406457 }
407458 }
@@ -415,24 +466,33 @@ object OutgoingPaymentPacket {
415466 case None =>
416467 // If the htlcReceivedAt was lost (because the node restarted), we use a hold time of 0 which should be ignored by the payer.
417468 val holdTime = cmd.htlcReceivedAt_opt.map(now - _).getOrElse(0 millisecond)
418- buildHtlcFailure(nodeSecret, useAttributableFailures, cmd.reason, add, holdTime).map {
419- case (encryptedReason, tlvs) => UpdateFailHtlc (add.channelId, cmd.id, encryptedReason, tlvs)
469+ buildHtlcFailure(nodeSecret, cmd.reason, add, holdTime).map {
470+ case (encryptedReason, attributionData_opt) =>
471+ val tlvs : Set [UpdateFailHtlcTlv ] = Set (
472+ if (useAttributableFailures) attributionData_opt.map(UpdateFailHtlcTlv .AttributionData (_)) else None
473+ ).flatten
474+ UpdateFailHtlc (add.channelId, cmd.id, encryptedReason, TlvStream (tlvs))
420475 }
421476 }
422477 }
423478
424479 def buildHtlcFulfill (nodeSecret : PrivateKey , useAttributionData : Boolean , cmd : CMD_FULFILL_HTLC , add : UpdateAddHtlc , now : TimestampMilli = TimestampMilli .now()): UpdateFulfillHtlc = {
425480 // If we are part of a blinded route, we must not populate attribution data.
426- val tlvs : TlvStream [UpdateFulfillHtlcTlv ] = if (useAttributionData && add.pathKey_opt.isEmpty) {
427- extractSharedSecret(nodeSecret, add) match {
428- case Left (_) => TlvStream .empty
429- case Right (sharedSecret) =>
430- val holdTime = cmd.htlcReceivedAt_opt.map(now - _).getOrElse(0 millisecond)
431- TlvStream (UpdateFulfillHtlcTlv .AttributionData (Sphinx .Attribution .create(cmd.downstreamAttribution_opt, None , holdTime, sharedSecret)))
432- }
433- } else {
434- TlvStream .empty
481+ val attributionData_opt = add.pathKey_opt match {
482+ case None if useAttributionData =>
483+ extractSharedSecret(nodeSecret, add) match {
484+ case Right (sharedSecret) =>
485+ // Note that we use the outer shared secret: we report our hold time to the previous trampoline node, not
486+ // necessarily to the payer.
487+ val holdTime = cmd.htlcReceivedAt_opt.map(now - _).getOrElse(0 millisecond)
488+ Some (Sphinx .Attribution .create(cmd.downstreamAttribution_opt, None , holdTime, sharedSecret.outerOnionSecret))
489+ case Left (_) => None
490+ }
491+ case _ => None
435492 }
436- UpdateFulfillHtlc (add.channelId, cmd.id, cmd.r, tlvs)
493+ val tlvs : Set [UpdateFulfillHtlcTlv ] = Set (
494+ attributionData_opt.map(UpdateFulfillHtlcTlv .AttributionData (_))
495+ ).flatten
496+ UpdateFulfillHtlc (add.channelId, cmd.id, cmd.r, TlvStream (tlvs))
437497 }
438498}
0 commit comments