Skip to content

Commit 76584b5

Browse files
committed
payments/db: remap legacy zero attempt IDs
Legacy KV payments can contain HTLC attempts with attempt ID zero. The SQL payments schema requires payment_htlc_attempts.attempt_index to be globally unique, so migrating multiple such attempts can fail with a UNIQUE constraint violation. Allocate synthetic attempt indexes for legacy zero-ID attempts from the switch payment ID sequencer horizon. Keep nonzero attempt IDs unchanged and advance the switch sequence once after migration validation succeeds. This preserves the SQL uniqueness invariant and prevents future switch IDs from colliding with migrated attempts. It also wraps HTLC insert errors with the attempted index and payment hash so future migration failures identify the problematic row.
1 parent 50eb0d1 commit 76584b5

3 files changed

Lines changed: 553 additions & 45 deletions

File tree

payments/db/migration1/migration_validation.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,11 @@ func validateMigratedPaymentBatch(ctx context.Context,
7878
return fmt.Errorf("fetch KV payment %x: %w",
7979
hash[:8], err)
8080
}
81+
_, err = failUnresolvedLegacyZeroAttempts(kvPayment)
82+
if err != nil {
83+
return fmt.Errorf("normalize KV payment %x: %w",
84+
hash[:8], err)
85+
}
8186

8287
err = structuralCompare(kvPayment, row)
8388
if err != nil {
@@ -191,6 +196,12 @@ func deepComparePayment(ctx context.Context, cfg *SQLStoreConfig,
191196
paymentHash[:8], err)
192197
}
193198

199+
_, err = failUnresolvedLegacyZeroAttempts(kvPayment)
200+
if err != nil {
201+
return fmt.Errorf("normalize KV payment %x: %w",
202+
paymentHash[:8], err)
203+
}
204+
normalizeLegacyZeroAttemptIDsForCompare(kvPayment, sqlPayment)
194205
normalizePaymentForCompare(kvPayment)
195206
normalizePaymentForCompare(sqlPayment)
196207

@@ -221,6 +232,60 @@ func deepComparePayment(ctx context.Context, cfg *SQLStoreConfig,
221232
return nil
222233
}
223234

235+
// normalizeLegacyZeroAttemptIDsForCompare aligns expected legacy attempt ID
236+
// remaps before deep comparison.
237+
//
238+
// Legacy KV payments can use attempt ID zero to represent an unknown ID. During
239+
// migration those attempts are assigned synthetic SQL attempt indexes, while
240+
// the source KV payment is left unchanged. Match by session key and copy the
241+
// SQL attempt ID into the KV object so fallback deep comparison still reports
242+
// real data mismatches instead of this expected migration repair.
243+
func normalizeLegacyZeroAttemptIDsForCompare(kvPayment,
244+
sqlPayment *MPPayment) {
245+
246+
if kvPayment == nil || sqlPayment == nil {
247+
return
248+
}
249+
250+
sqlAttemptsBySessionKey := make(map[string]uint64)
251+
for i := range sqlPayment.HTLCs {
252+
htlc := &sqlPayment.HTLCs[i]
253+
sessionKey := htlc.SessionKey()
254+
if sessionKey == nil {
255+
// Leave normalization incomplete so the deep comparison
256+
// reports the malformed attempt instead of panicking.
257+
continue
258+
}
259+
260+
sessionKeyBytes := sessionKey.Serialize()
261+
sessionKeyStr := string(sessionKeyBytes)
262+
sqlAttemptsBySessionKey[sessionKeyStr] = htlc.AttemptID
263+
}
264+
265+
for i := range kvPayment.HTLCs {
266+
htlc := &kvPayment.HTLCs[i]
267+
if htlc.AttemptID != 0 {
268+
continue
269+
}
270+
271+
sessionKey := htlc.SessionKey()
272+
if sessionKey == nil {
273+
// Leave normalization incomplete so the deep comparison
274+
// reports the malformed attempt instead of panicking.
275+
continue
276+
}
277+
278+
sessionKeyBytes := sessionKey.Serialize()
279+
sessionKeyStr := string(sessionKeyBytes)
280+
attemptID, ok := sqlAttemptsBySessionKey[sessionKeyStr]
281+
if !ok {
282+
continue
283+
}
284+
285+
htlc.AttemptID = attemptID
286+
}
287+
}
288+
224289
// normalizePaymentForCompare normalizes fields that are expected to differ
225290
// between KV and SQL representations before deep comparison.
226291
func normalizePaymentForCompare(payment *MPPayment) {

0 commit comments

Comments
 (0)