payments/migration1: handle legacy payments with nil HTLC hash
What changed, and why it matters
This commit fixes a database migration bug in LND's payment storage upgrade. Older Lightning payments sometimes didn't store a payment hash on each individual HTLC (payment attempt). The migration to a new SQL database previously failed when it encountered these legacy records. The fix simply falls back to the parent payment's hash, matching how the rest of LND already treats legacy payments. This is a reliability fix, not an exploitable security vulnerability.
Treat as a normal bug-fix/maintenance patch. Include in release notes as a migration reliability improvement. No urgent security response required.
Security signals we found
Migration failure could block node upgrades or leave databases in a partially migrated state
Fix aligns data handling with existing router behavior for legacy payments
No input validation bypass, privilege escalation, or cryptographic weakness introduced
Evidence from the diff
The patch modifies the bbolt-to-SQL migration for payment HTLC attempts. In sql_migration.go, migrateHTLCAttempt no longer returns an error when htlc.Hash is nil; instead it uses the parent payment hash. In migration_validation.go, normalizePaymentForCompare applies the same fallback so post-migration validation compares equivalent values. The commit message explicitly references patchLegacyPaymentHash in payment_lifecycle.go as the existing precedent.
Changed components
payments/db/migration1/sql_migration.gopayments/db/migration1/migration_validation.golegacy bbolt payment records with nil HTLC HashInspect captured patch +21 / −7
diff --git a/payments/db/migration1/migration_validation.go b/payments/db/migration1/migration_validation.go
index e97c3db..e6f8bdd 100644
--- a/payments/db/migration1/migration_validation.go
+++ b/payments/db/migration1/migration_validation.go
@@ -299,6 +299,17 @@ func normalizePaymentForCompare(payment *MPPayment) {
htlc.circuit = nil
htlc.cachedSessionKey = nil
+ // For legacy payments, the HTLC Hash field may be nil in the
+ // bbolt backend. During migration, the SQL code uses the
+ // parent payment hash as fallback. To ensure the comparison
+ // between bbolt and SQL data succeeds, we apply the same
+ // fallback here.
+ //
+ // See also: patchLegacyPaymentHash in payment_lifecycle.go.
+ if htlc.Hash == nil && payment.Info != nil {
+ htlc.Hash = &payment.Info.PaymentIdentifier
+ }
+
if len(htlc.Route.FirstHopWireCustomRecords) == 0 {
htlc.Route.FirstHopWireCustomRecords =
lnwire.CustomRecords{}
diff --git a/payments/db/migration1/sql_migration.go b/payments/db/migration1/sql_migration.go
index a7a02af..626b25a 100644
--- a/payments/db/migration1/sql_migration.go
+++ b/payments/db/migration1/sql_migration.go
@@ -364,20 +364,23 @@ func migrateHTLCAttempt(ctx context.Context, paymentID int64,
parentPaymentHash lntypes.Hash, htlc *HTLCAttempt, sqlDB SQLQueries,
stats *MigrationStats) error {
- // Validate that we have a payment hash for the attempt.
+ // Determine the payment hash for this HTLC attempt.
//
- // NOTE: We always require an attempt payment hash. A missing hash is an
- // unrecoverable inconsistency. All payments should have a payment hash
- // (AMP,MPP,Legacy)
+ // For AMP payments, each HTLC has its own unique hash. For non-AMP
+ // payments (MPP, Legacy), all HTLCs use the same hash as the parent
+ // payment. Older payment attempts may not have the hash stored
+ // explicitly, in which case we fall back to the parent payment hash
+ // which is ok since non-AMP payments have a single hash for all HTLCs.
var paymentHash []byte
switch {
case htlc.Hash != nil:
paymentHash = (*htlc.Hash)[:]
default:
- return fmt.Errorf("HTLC attempt %d missing payment hash "+
- "(parent payment hash=%x)", htlc.AttemptID,
- parentPaymentHash[:])
+ // For older payments where Hash is nil, use the parent payment
+ // hash. This is consistent with how the router handles these
+ // legacy payments.
+ paymentHash = parentPaymentHash[:]
}
firstHopAmountMsat := int64(htlc.Route.FirstHopAmount.Val.Int())
Why this scored 27/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.