@@ -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 , trampolineHoldTime : 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 trampolineAttribution = Sphinx .Attribution .create(previousAttribution_opt, Some (packet), trampolineHoldTime, trampolineOnionSecret)
391+ val outerAttribution = Sphinx .Attribution .create(Some (trampolineAttribution), Some (trampolinePacket), holdTime, ss.outerOnionSecret)
392+ (Sphinx .FailurePacket .wrap(trampolinePacket, ss.outerOnionSecret), Some (outerAttribution))
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 trampolineAttribution = Sphinx .Attribution .create(previousAttribution_opt = None , Some (packet), trampolineHoldTime, trampolineOnionSecret)
413+ val outerAttribution = Sphinx .Attribution .create(Some (trampolineAttribution), Some (trampolinePacket), holdTime, ss.outerOnionSecret)
414+ (Sphinx .FailurePacket .wrap(trampolinePacket, ss.outerOnionSecret), Some (outerAttribution))
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 }
@@ -413,26 +464,38 @@ object OutgoingPaymentPacket {
413464 val failure = InvalidOnionBlinding (Sphinx .hash(add.onionRoutingPacket))
414465 Right (UpdateFailMalformedHtlc (add.channelId, add.id, failure.onionHash, failure.code))
415466 case None =>
416- // If the htlcReceivedAt was lost (because the node restarted), we use a hold time of 0 which should be ignored by the payer.
467+ // If the attribution data was lost (because the node restarted), we use a hold time of 0 which should be ignored by the payer.
468+ val trampolineHoldTime = cmd.attribution_opt.flatMap(_.trampolineReceivedAt_opt).map(now - _).getOrElse(0 millisecond)
417469 val holdTime = cmd.attribution_opt.map(now - _.htlcReceivedAt).getOrElse(0 millisecond)
418- buildHtlcFailure(nodeSecret, useAttributableFailures, cmd.reason, add, holdTime).map {
419- case (encryptedReason, tlvs) => UpdateFailHtlc (add.channelId, cmd.id, encryptedReason, tlvs)
470+ buildHtlcFailure(nodeSecret, cmd.reason, add, holdTime, trampolineHoldTime).map {
471+ case (encryptedReason, attributionData_opt) =>
472+ val tlvs : Set [UpdateFailHtlcTlv ] = Set (
473+ if (useAttributableFailures) attributionData_opt.map(UpdateFailHtlcTlv .AttributionData (_)) else None
474+ ).flatten
475+ UpdateFailHtlc (add.channelId, cmd.id, encryptedReason, TlvStream (tlvs))
420476 }
421477 }
422478 }
423479
424480 def buildHtlcFulfill (nodeSecret : PrivateKey , useAttributionData : Boolean , cmd : CMD_FULFILL_HTLC , add : UpdateAddHtlc , now : TimestampMilli = TimestampMilli .now()): UpdateFulfillHtlc = {
425481 // 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.attribution_opt.map(now - _.htlcReceivedAt).getOrElse(0 millisecond)
431- TlvStream (UpdateFulfillHtlcTlv .AttributionData (Sphinx .Attribution .create(cmd.attribution_opt.flatMap(_.downstreamAttribution_opt), None , holdTime, sharedSecret)))
432- }
433- } else {
434- TlvStream .empty
482+ val attributionData_opt = add.pathKey_opt match {
483+ case None if useAttributionData =>
484+ val trampolineHoldTime = cmd.attribution_opt.flatMap(_.trampolineReceivedAt_opt).map(now - _).getOrElse(0 millisecond)
485+ val holdTime = cmd.attribution_opt.map(now - _.htlcReceivedAt).getOrElse(0 millisecond)
486+ extractSharedSecret(nodeSecret, add) match {
487+ case Right (HtlcSharedSecrets (outerOnionSecret, None , _)) =>
488+ Some (Sphinx .Attribution .create(cmd.attribution_opt.flatMap(_.downstreamAttribution_opt), None , holdTime, outerOnionSecret))
489+ case Right (HtlcSharedSecrets (outerOnionSecret, Some (trampolineOnionSecret), blinded)) if ! blinded =>
490+ val trampolineAttribution = Sphinx .Attribution .create(cmd.attribution_opt.flatMap(_.downstreamAttribution_opt), None , trampolineHoldTime, trampolineOnionSecret)
491+ Some (Sphinx .Attribution .create(Some (trampolineAttribution), None , holdTime, outerOnionSecret))
492+ case _ => None
493+ }
494+ case _ => None
435495 }
436- UpdateFulfillHtlc (add.channelId, cmd.id, cmd.r, tlvs)
496+ val tlvs : Set [UpdateFulfillHtlcTlv ] = Set (
497+ attributionData_opt.map(UpdateFulfillHtlcTlv .AttributionData (_))
498+ ).flatten
499+ UpdateFulfillHtlc (add.channelId, cmd.id, cmd.r, TlvStream (tlvs))
437500 }
438501}
0 commit comments