@@ -379,30 +379,81 @@ object OutgoingPaymentPacket {
379379 }
380380 }
381381
382- private def buildHtlcFailure (nodeSecret : PrivateKey , useAttributableFailures : Boolean , reason : FailureReason , add : UpdateAddHtlc , holdTime : FiniteDuration ): Either [CannotExtractSharedSecret , (ByteVector , TlvStream [UpdateFailHtlcTlv ])] = {
383- extractSharedSecret(nodeSecret, add).map(sharedSecret => {
384- val (packet, attribution) = reason match {
385- case FailureReason .EncryptedDownstreamFailure (packet, attribution) => (packet, attribution)
386- case FailureReason .LocalFailure (failure) => (Sphinx .FailurePacket .create(sharedSecret, failure), None )
382+ private def buildHtlcFailure (nodeSecret : PrivateKey , reason : FailureReason , add : UpdateAddHtlc , holdTime : FiniteDuration , trampolineHoldTime : FiniteDuration ): Either [CannotExtractSharedSecret , (ByteVector , Option [ByteVector ])] = {
383+ extractSharedSecret(nodeSecret, add).map(ss => {
384+ reason match {
385+ case FailureReason .EncryptedDownstreamFailure (packet, previousAttribution_opt) =>
386+ ss.trampolineOnionSecret_opt match {
387+ case Some (trampolineOnionSecret) if ! ss.blinded =>
388+ // If we are unable to decrypt the downstream failure and the payment is using trampoline, the failure is
389+ // intended for the payer. We encrypt it with the trampoline secret first and then the outer secret.
390+ val trampolinePacket = Sphinx .FailurePacket .wrap(packet, trampolineOnionSecret)
391+ val trampolineAttribution = Sphinx .Attribution .create(previousAttribution_opt, Some (packet), trampolineHoldTime, trampolineOnionSecret)
392+ val outerAttribution = Sphinx .Attribution .create(Some (trampolineAttribution), Some (trampolinePacket), holdTime, ss.outerOnionSecret)
393+ (Sphinx .FailurePacket .wrap(trampolinePacket, ss.outerOnionSecret), Some (outerAttribution))
394+ case Some (trampolineOnionSecret) =>
395+ // When we're inside a blinded path, we don't report our attribution data.
396+ val trampolinePacket = Sphinx .FailurePacket .wrap(packet, trampolineOnionSecret)
397+ (Sphinx .FailurePacket .wrap(trampolinePacket, ss.outerOnionSecret), None )
398+ case None =>
399+ val attribution = Sphinx .Attribution .create(previousAttribution_opt, Some (packet), holdTime, ss.outerOnionSecret)
400+ (Sphinx .FailurePacket .wrap(packet, ss.outerOnionSecret), Some (attribution))
401+ }
402+ case FailureReason .LocalFailure (failure) =>
403+ // This isn't a trampoline failure, so we only encrypt it for the node who created the outer onion.
404+ val packet = Sphinx .FailurePacket .create(ss.outerOnionSecret, failure)
405+ val attribution = Sphinx .Attribution .create(previousAttribution_opt = None , Some (packet), holdTime, ss.outerOnionSecret)
406+ (Sphinx .FailurePacket .wrap(packet, ss.outerOnionSecret), Some (attribution))
407+ case FailureReason .LocalTrampolineFailure (failure) =>
408+ // This is a trampoline failure: we try to encrypt it to the node who created the trampoline onion.
409+ ss.trampolineOnionSecret_opt match {
410+ case Some (trampolineOnionSecret) if ! ss.blinded =>
411+ val packet = Sphinx .FailurePacket .create(trampolineOnionSecret, failure)
412+ val trampolinePacket = Sphinx .FailurePacket .wrap(packet, trampolineOnionSecret)
413+ val trampolineAttribution = Sphinx .Attribution .create(previousAttribution_opt = None , Some (packet), trampolineHoldTime, trampolineOnionSecret)
414+ val outerAttribution = Sphinx .Attribution .create(Some (trampolineAttribution), Some (trampolinePacket), holdTime, ss.outerOnionSecret)
415+ (Sphinx .FailurePacket .wrap(trampolinePacket, ss.outerOnionSecret), Some (outerAttribution))
416+ case Some (trampolineOnionSecret) =>
417+ val packet = Sphinx .FailurePacket .create(trampolineOnionSecret, failure)
418+ val trampolinePacket = Sphinx .FailurePacket .wrap(packet, trampolineOnionSecret)
419+ (Sphinx .FailurePacket .wrap(trampolinePacket, ss.outerOnionSecret), None )
420+ case None =>
421+ // This shouldn't happen, we only generate trampoline failures when there was a trampoline onion.
422+ val packet = Sphinx .FailurePacket .create(ss.outerOnionSecret, failure)
423+ (Sphinx .FailurePacket .wrap(packet, ss.outerOnionSecret), None )
424+ }
387425 }
388- val tlvs : TlvStream [UpdateFailHtlcTlv ] = if (useAttributableFailures) {
389- TlvStream (UpdateFailHtlcTlv .AttributionData (Sphinx .Attribution .create(attribution, Some (packet), holdTime, sharedSecret)))
390- } else {
391- TlvStream .empty
392- }
393- (Sphinx .FailurePacket .wrap(packet, sharedSecret), tlvs)
394426 })
395427 }
396428
429+ private case class HtlcSharedSecrets (outerOnionSecret : ByteVector32 , trampolineOnionSecret_opt : Option [ByteVector32 ], blinded : Boolean )
430+
397431 /**
398432 * We decrypt the onion again to extract the shared secret used to encrypt onion failures.
399433 * We could avoid this by storing the shared secret after the initial onion decryption, but we would have to store it
400434 * in the database since we must be able to fail HTLCs after restarting our node.
401435 * It's simpler to extract it again from the encrypted onion.
402436 */
403- private def extractSharedSecret (nodeSecret : PrivateKey , add : UpdateAddHtlc ): Either [CannotExtractSharedSecret , ByteVector32 ] = {
437+ private def extractSharedSecret (nodeSecret : PrivateKey , add : UpdateAddHtlc ): Either [CannotExtractSharedSecret , HtlcSharedSecrets ] = {
404438 Sphinx .peel(nodeSecret, Some (add.paymentHash), add.onionRoutingPacket) match {
405- case Right (Sphinx .DecryptedPacket (_, _, sharedSecret)) => Right (sharedSecret)
439+ case Right (Sphinx .DecryptedPacket (payload, _, outerOnionSecret)) =>
440+ // Let's look at the onion payload to see if it contains a trampoline onion.
441+ PaymentOnionCodecs .perHopPayloadCodec.decode(payload.bits) match {
442+ case Attempt .Successful (DecodeResult (perHopPayload, _)) =>
443+ // We try to extract the trampoline shared secret, if we can find one.
444+ val trampolineOnionSecret_opt = perHopPayload.get[OnionPaymentPayloadTlv .TrampolineOnion ].map(_.packet).flatMap(trampolinePacket => {
445+ val trampolinePathKey_opt = perHopPayload.get[OnionPaymentPayloadTlv .PathKey ].map(_.publicKey)
446+ val trampolineOnionDecryptionKey = trampolinePathKey_opt.map(pathKey => Sphinx .RouteBlinding .derivePrivateKey(nodeSecret, pathKey)).getOrElse(nodeSecret)
447+ Sphinx .peel(trampolineOnionDecryptionKey, Some (add.paymentHash), trampolinePacket).toOption.map(_.sharedSecret)
448+ })
449+ // We check if we are an intermediate node in a blinded (potentially trampoline) path.
450+ val blinded = trampolineOnionSecret_opt match {
451+ case Some (_) => perHopPayload.get[OnionPaymentPayloadTlv .PathKey ].nonEmpty
452+ case None => add.pathKey_opt.nonEmpty
453+ }
454+ Right (HtlcSharedSecrets (outerOnionSecret, trampolineOnionSecret_opt, blinded))
455+ case Attempt .Failure (_) => Right (HtlcSharedSecrets (outerOnionSecret, None , blinded = false ))
456+ }
406457 case Left (_) => Left (CannotExtractSharedSecret (add.channelId, add))
407458 }
408459 }
@@ -414,26 +465,38 @@ object OutgoingPaymentPacket {
414465 val failure = InvalidOnionBlinding (Sphinx .hash(add.onionRoutingPacket))
415466 Right (UpdateFailMalformedHtlc (add.channelId, add.id, failure.onionHash, failure.code))
416467 case None =>
417- // If the htlcReceivedAt was lost (because the node restarted), we use a hold time of 0 which should be ignored by the payer.
468+ // If the attribution data was lost (because the node restarted), we use a hold time of 0 which should be ignored by the payer.
469+ val trampolineHoldTime = cmd.attribution_opt.flatMap(_.trampolineReceivedAt_opt).map(now - _).getOrElse(0 millisecond)
418470 val holdTime = cmd.attribution_opt.map(now - _.htlcReceivedAt).getOrElse(0 millisecond)
419- buildHtlcFailure(nodeSecret, useAttributableFailures, cmd.reason, add, holdTime).map {
420- case (encryptedReason, tlvs) => UpdateFailHtlc (add.channelId, cmd.id, encryptedReason, tlvs)
471+ buildHtlcFailure(nodeSecret, cmd.reason, add, holdTime, trampolineHoldTime).map {
472+ case (encryptedReason, attributionData_opt) =>
473+ val tlvs : Set [UpdateFailHtlcTlv ] = Set (
474+ if (useAttributableFailures) attributionData_opt.map(UpdateFailHtlcTlv .AttributionData (_)) else None
475+ ).flatten
476+ UpdateFailHtlc (add.channelId, cmd.id, encryptedReason, TlvStream (tlvs))
421477 }
422478 }
423479 }
424480
425481 def buildHtlcFulfill (nodeSecret : PrivateKey , useAttributionData : Boolean , cmd : CMD_FULFILL_HTLC , add : UpdateAddHtlc , now : TimestampMilli = TimestampMilli .now()): UpdateFulfillHtlc = {
426482 // If we are part of a blinded route, we must not populate attribution data.
427- val tlvs : TlvStream [UpdateFulfillHtlcTlv ] = if (useAttributionData && add.pathKey_opt.isEmpty) {
428- extractSharedSecret(nodeSecret, add) match {
429- case Left (_) => TlvStream .empty
430- case Right (sharedSecret) =>
431- val holdTime = cmd.attribution_opt.map(now - _.htlcReceivedAt).getOrElse(0 millisecond)
432- TlvStream (UpdateFulfillHtlcTlv .AttributionData (Sphinx .Attribution .create(cmd.attribution_opt.flatMap(_.downstreamAttribution_opt), None , holdTime, sharedSecret)))
433- }
434- } else {
435- TlvStream .empty
483+ val attributionData_opt = add.pathKey_opt match {
484+ case None if useAttributionData =>
485+ val trampolineHoldTime = cmd.attribution_opt.flatMap(_.trampolineReceivedAt_opt).map(now - _).getOrElse(0 millisecond)
486+ val holdTime = cmd.attribution_opt.map(now - _.htlcReceivedAt).getOrElse(0 millisecond)
487+ extractSharedSecret(nodeSecret, add) match {
488+ case Right (HtlcSharedSecrets (outerOnionSecret, None , _)) =>
489+ Some (Sphinx .Attribution .create(cmd.attribution_opt.flatMap(_.downstreamAttribution_opt), None , holdTime, outerOnionSecret))
490+ case Right (HtlcSharedSecrets (outerOnionSecret, Some (trampolineOnionSecret), blinded)) if ! blinded =>
491+ val trampolineAttribution = Sphinx .Attribution .create(cmd.attribution_opt.flatMap(_.downstreamAttribution_opt), None , trampolineHoldTime, trampolineOnionSecret)
492+ Some (Sphinx .Attribution .create(Some (trampolineAttribution), None , holdTime, outerOnionSecret))
493+ case _ => None
494+ }
495+ case _ => None
436496 }
437- UpdateFulfillHtlc (add.channelId, cmd.id, cmd.r, tlvs)
497+ val tlvs : Set [UpdateFulfillHtlcTlv ] = Set (
498+ attributionData_opt.map(UpdateFulfillHtlcTlv .AttributionData (_))
499+ ).flatten
500+ UpdateFulfillHtlc (add.channelId, cmd.id, cmd.r, TlvStream (tlvs))
438501 }
439502}
0 commit comments