@@ -175,6 +175,20 @@ func (h *HTLCAttemptInfo) SessionKey() *btcec.PrivateKey {
175175 return h .cachedSessionKey
176176}
177177
178+ // setSessionKey sets the session key for the htlc attempt.
179+ //
180+ // NOTE: Only used for testing.
181+ //
182+ //nolint:unused
183+ func (h * HTLCAttemptInfo ) setSessionKey (sessionKey * btcec.PrivateKey ) {
184+ h .cachedSessionKey = sessionKey
185+
186+ // Also set the session key as a raw bytes.
187+ var scratch [btcec .PrivKeyBytesLen ]byte
188+ copy (scratch [:], sessionKey .Serialize ())
189+ h .sessionKey = scratch
190+ }
191+
178192// OnionBlob returns the onion blob created from the sphinx construction.
179193func (h * HTLCAttemptInfo ) OnionBlob () ([lnwire .OnionPacketSize ]byte , error ) {
180194 var zeroBytes [lnwire .OnionPacketSize ]byte
@@ -712,3 +726,95 @@ func generateSphinxPacket(rt *route.Route, paymentHash []byte,
712726 PaymentPath : sphinxPath .NodeKeys (),
713727 }, nil
714728}
729+
730+ // verifyAttempt validates that a new HTLC attempt is compatible with the
731+ // existing payment and its in-flight HTLCs. This function checks:
732+ // 1. MPP (Multi-Path Payment) compatibility between attempts
733+ // 2. Blinded payment consistency
734+ // 3. Amount validation
735+ // 4. Total payment amount limits
736+ func verifyAttempt (payment * MPPayment , attempt * HTLCAttemptInfo ) error {
737+ // If the final hop has encrypted data, then we know this is a
738+ // blinded payment. In blinded payments, MPP records are not set
739+ // for split payments and the recipient is responsible for using
740+ // a consistent PathID across the various encrypted data
741+ // payloads that we received from them for this payment. All we
742+ // need to check is that the total amount field for each HTLC
743+ // in the split payment is correct.
744+ isBlinded := len (attempt .Route .FinalHop ().EncryptedData ) != 0
745+
746+ // Make sure any existing shards match the new one with regards
747+ // to MPP options.
748+ mpp := attempt .Route .FinalHop ().MPP
749+
750+ // MPP records should not be set for attempts to blinded paths.
751+ if isBlinded && mpp != nil {
752+ return ErrMPPRecordInBlindedPayment
753+ }
754+
755+ for _ , h := range payment .InFlightHTLCs () {
756+ hMpp := h .Route .FinalHop ().MPP
757+
758+ // If this is a blinded payment, then no existing HTLCs
759+ // should have MPP records.
760+ if isBlinded && hMpp != nil {
761+ return ErrMPPRecordInBlindedPayment
762+ }
763+
764+ // If this is a blinded payment, then we just need to
765+ // check that the TotalAmtMsat field for this shard
766+ // is equal to that of any other shard in the same
767+ // payment.
768+ if isBlinded {
769+ if attempt .Route .FinalHop ().TotalAmtMsat !=
770+ h .Route .FinalHop ().TotalAmtMsat {
771+
772+ return ErrBlindedPaymentTotalAmountMismatch
773+ }
774+
775+ continue
776+ }
777+
778+ switch {
779+ // We tried to register a non-MPP attempt for a MPP
780+ // payment.
781+ case mpp == nil && hMpp != nil :
782+ return ErrMPPayment
783+
784+ // We tried to register a MPP shard for a non-MPP
785+ // payment.
786+ case mpp != nil && hMpp == nil :
787+ return ErrNonMPPayment
788+
789+ // Non-MPP payment, nothing more to validate.
790+ case mpp == nil :
791+ continue
792+ }
793+
794+ // Check that MPP options match.
795+ if mpp .PaymentAddr () != hMpp .PaymentAddr () {
796+ return ErrMPPPaymentAddrMismatch
797+ }
798+
799+ if mpp .TotalMsat () != hMpp .TotalMsat () {
800+ return ErrMPPTotalAmountMismatch
801+ }
802+ }
803+
804+ // If this is a non-MPP attempt, it must match the total amount
805+ // exactly. Note that a blinded payment is considered an MPP
806+ // attempt.
807+ amt := attempt .Route .ReceiverAmt ()
808+ if ! isBlinded && mpp == nil && amt != payment .Info .Value {
809+ return ErrValueMismatch
810+ }
811+
812+ // Ensure we aren't sending more than the total payment amount.
813+ sentAmt , _ := payment .SentAmt ()
814+ if sentAmt + amt > payment .Info .Value {
815+ return fmt .Errorf ("%w: attempted=%v, payment amount=%v" ,
816+ ErrValueExceedsAmt , sentAmt + amt , payment .Info .Value )
817+ }
818+
819+ return nil
820+ }
0 commit comments