paymentsdb: verify total amount for last hop in the blinded path
What changed, and why it matters
This commit adds a validation check in LND's payment database to ensure that blinded (privacy-preserving) Lightning payments always include a total amount on the final hop. Without this check, a missing total amount could lead to incorrect accounting or routing behavior for blinded payments. The change is defensive and does not appear to fix an active exploit, but it closes a gap where invalid payment data could be stored.
Review whether any other blinded-payment path validations (e.g., total amount consistency across MPP shards, or bounds checking) are also needed. Ensure the new error is handled gracefully by callers and does not cause unintended payment failures.
Security signals we found
Input validation added for blinded payment attempts
New error condition for missing total amount on final hop
Unit test added to enforce the new validation rule
Defensive hardening in payment state management
Evidence from the diff
The patch modifies verifyAttempt in payments/db/payment.go to reject HTLC attempts for blinded payments (identified by EncryptedData being non-empty on the final hop) when FinalHop().TotalAmtMsat is zero. A new error ErrBlindedPaymentMissingTotalAmount is defined, and a unit test confirms the validation works. This is a local input-validation hardening change in the payments DB layer.
Changed components
payments/db/payment.gopayments/db/errors.gopayments/db/payment_test.goInspect captured patch +52 / −0
diff --git a/payments/db/errors.go b/payments/db/errors.go
index fee71b0..0457db6 100644
--- a/payments/db/errors.go
+++ b/payments/db/errors.go
@@ -84,6 +84,12 @@ var (
ErrMixedBlindedAndNonBlindedPayments = errors.New("mixed blinded and " +
"non-blinded payments")
+ // ErrBlindedPaymentMissingTotalAmount is returned if we try to
+ // register a blinded payment attempt where the final hop doesn't set
+ // the total amount.
+ ErrBlindedPaymentMissingTotalAmount = errors.New("blinded payment " +
+ "final hop must set total amount")
+
// ErrMPPPaymentAddrMismatch is returned if we try to register an MPP
// shard where the payment address doesn't match existing shards.
ErrMPPPaymentAddrMismatch = errors.New("payment address mismatch")
diff --git a/payments/db/payment.go b/payments/db/payment.go
index 147ccdb..ddceedf 100644
--- a/payments/db/payment.go
+++ b/payments/db/payment.go
@@ -744,6 +744,13 @@ func verifyAttempt(payment *MPPayment, attempt *HTLCAttemptInfo) error {
// in the split payment is correct.
isBlinded := len(attempt.Route.FinalHop().EncryptedData) != 0
+ // For blinded payments, the last hop must set the total amount.
+ if isBlinded {
+ if attempt.Route.FinalHop().TotalAmtMsat == 0 {
+ return ErrBlindedPaymentMissingTotalAmount
+ }
+ }
+
// Make sure any existing shards match the new one with regards
// to MPP options.
mpp := attempt.Route.FinalHop().MPP
diff --git a/payments/db/payment_test.go b/payments/db/payment_test.go
index a7369c1..e6a2e73 100644
--- a/payments/db/payment_test.go
+++ b/payments/db/payment_test.go
@@ -1388,6 +1388,45 @@ func TestVerifyAttemptBlindedValidation(t *testing.T) {
require.NoError(t, verifyAttempt(payment, &matching))
}
+// TestVerifyAttemptBlindedMissingTotalAmount tests that we return an error if
+// we try to register a blinded payment attempt where the final hop doesn't set
+// the total amount.
+func TestVerifyAttemptBlindedMissingTotalAmount(t *testing.T) {
+ t.Parallel()
+
+ total := lnwire.MilliSatoshi(5000)
+
+ // Payment with no existing attempts.
+ payment := makePayment(total)
+
+ // Attempt with encrypted data (blinded payment) but missing total
+ // amount.
+ attemptMissingTotal := makeLastHopAttemptInfo(
+ 1,
+ lastHopArgs{
+ amt: 2500,
+ total: 0,
+ encrypted: []byte{1, 2, 3},
+ },
+ )
+ require.ErrorIs(
+ t,
+ verifyAttempt(payment, &attemptMissingTotal),
+ ErrBlindedPaymentMissingTotalAmount,
+ )
+
+ // Attempt with encrypted data and valid total amount should succeed.
+ attemptWithTotal := makeLastHopAttemptInfo(
+ 2,
+ lastHopArgs{
+ amt: 2500,
+ total: total,
+ encrypted: []byte{4, 5, 6},
+ },
+ )
+ require.NoError(t, verifyAttempt(payment, &attemptWithTotal))
+}
+
// TestVerifyAttemptBlindedMixedWithNonBlinded tests that we return an error if
// we try to register a non-MPP attempt for a blinded payment.
func TestVerifyAttemptBlindedMixedWithNonBlinded(t *testing.T) {
Why this scored 44/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.