From 00025ef3bf3d3b0311972815d8d84b4e8546fc7b Mon Sep 17 00:00:00 2001 From: ziggie Date: Thu, 26 Feb 2026 17:37:01 +0100 Subject: [PATCH] payments/db: normalize LegacyPayload flag in migration comparison MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LegacyPayload was a hint used exclusively by the KV store to decide how to serialize and deserialize the hop payload (legacy format vs TLV). The SQL store does not serialize hop data at all — every hop field is persisted natively in its own column — so this flag has no meaning there and is never stored. Clear LegacyPayload for all hops inside normalizePaymentForCompare so that deep-equality checks between KV and SQL payments succeed even when the KV source data carries LegacyPayload=true. A dedicated test (TestMigrationLegacyPayloadNormalized) is added to verify that a payment with LegacyPayload=true hops migrates and compares correctly. --- .../db/migration1/migration_validation.go | 9 + payments/db/migration1/sql_migration_test.go | 172 ++++++++++++++++++ 2 files changed, 181 insertions(+) diff --git a/payments/db/migration1/migration_validation.go b/payments/db/migration1/migration_validation.go index b2192e944..fe43cf28f 100644 --- a/payments/db/migration1/migration_validation.go +++ b/payments/db/migration1/migration_validation.go @@ -320,6 +320,15 @@ func normalizePaymentForCompare(payment *MPPayment) { htlc.Route.Hops[j].CustomRecords = record.CustomSet{} } + + // LegacyPayload was a hint used by the KV store to + // determine how to serialize and deserialize the hop + // payload (i.e. whether to use the legacy format or + // TLV). The SQL store does not serialize hop data at + // all — each field is stored natively in its own + // column — so this flag has no meaning there and is + // never persisted. + htlc.Route.Hops[j].LegacyPayload = false } } } diff --git a/payments/db/migration1/sql_migration_test.go b/payments/db/migration1/sql_migration_test.go index f1e96f025..f19dd9179 100644 --- a/payments/db/migration1/sql_migration_test.go +++ b/payments/db/migration1/sql_migration_test.go @@ -156,6 +156,178 @@ func TestMigrationDataIntegrity(t *testing.T) { } } +// TestMigrationLegacyPayloadNormalized verifies that a payment whose HTLC +// hops carry LegacyPayload=true in the KV store is migrated and compared +// correctly. LegacyPayload was a hint used by the KV store to choose between +// the legacy and TLV hop-payload serialization formats. The SQL store does not +// serialize hop data at all — each field is stored natively in its own +// column — so the flag is never persisted there. normalizePaymentForCompare +// clears it on both sides before the equality check, so the comparison must +// succeed even when the KV source has LegacyPayload=true. +func TestMigrationLegacyPayloadNormalized(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + kvDB := setupTestKVDB(t) + + hash := createTestPaymentHash(t, 0) + + err := kvdb.Update(kvDB, func(tx kvdb.RwTx) error { + paymentsBucket, err := tx.CreateTopLevelBucket( + paymentsRootBucket, + ) + if err != nil { + return err + } + + indexBucket, err := tx.CreateTopLevelBucket( + paymentsIndexBucket, + ) + if err != nil { + return err + } + + paymentBucket, err := paymentsBucket.CreateBucketIfNotExists( + hash[:], + ) + if err != nil { + return err + } + + var paymentID lntypes.Hash + copy(paymentID[:], hash[:]) + + creationInfo := &PaymentCreationInfo{ + PaymentIdentifier: paymentID, + Value: lnwire.MilliSatoshi(1000000), + CreationTime: time.Now().Add(-24 * time.Hour), + PaymentRequest: []byte("lnbc1test"), + } + + var b bytes.Buffer + if err = serializePaymentCreationInfo( + &b, creationInfo, + ); err != nil { + return err + } + if err = paymentBucket.Put( + paymentCreationInfoKey, b.Bytes(), + ); err != nil { + return err + } + + seqBytes := make([]byte, 8) + byteOrder.PutUint64(seqBytes, 0) + if err = paymentBucket.Put( + paymentSequenceKey, seqBytes, + ); err != nil { + return err + } + + htlcBucket, err := paymentBucket.CreateBucketIfNotExists( + paymentHtlcsBucket, + ) + if err != nil { + return err + } + + hop1Key, err := btcec.NewPrivateKey() + if err != nil { + return err + } + + hop2Key, err := btcec.NewPrivateKey() + if err != nil { + return err + } + + sessionKey, err := btcec.NewPrivateKey() + if err != nil { + return err + } + + var sourcePubKey Vertex + copy(sourcePubKey[:], sessionKey.PubKey().SerializeCompressed()) + + var sessionKeyBytes [32]byte + copy(sessionKeyBytes[:], sessionKey.Serialize()) + + const attemptID = uint64(1) + attemptInfo := &HTLCAttemptInfo{ + AttemptID: attemptID, + sessionKey: sessionKeyBytes, + Route: Route{ + TotalTimeLock: 500000, + TotalAmount: lnwire.MilliSatoshi(1000000), + SourcePubKey: sourcePubKey, + Hops: []*Hop{ + { + PubKeyBytes: NewVertex( + hop1Key.PubKey(), + ), + ChannelID: 123456, + OutgoingTimeLock: 499500, + AmtToForward: 900000, + LegacyPayload: true, + }, + { + PubKeyBytes: NewVertex( + hop2Key.PubKey(), + ), + ChannelID: 789012, + OutgoingTimeLock: 499000, + AmtToForward: 800000, + LegacyPayload: true, + }, + }, + }, + AttemptTime: time.Now().Add(-2 * time.Hour), + Hash: &paymentID, + } + + if err = writeHTLCAttempt( + htlcBucket, attemptID, attemptInfo, + ); err != nil { + return err + } + + settleInfo := &HTLCSettleInfo{ + Preimage: lntypes.Preimage(paymentID), + SettleTime: time.Now().Add(-1 * time.Hour), + } + if err = writeHTLCSettle( + htlcBucket, attemptID, settleInfo, + ); err != nil { + return err + } + + return createIndexEntry(indexBucket, seqBytes, hash) + }, func() {}) + require.NoError(t, err) + + // Fetch the payment from KV and verify LegacyPayload is true so we + // know the test data is correct before migration. + var kvPayment *MPPayment + err = kvdb.View(kvDB, func(tx kvdb.RTx) error { + bucket := tx.ReadBucket(paymentsRootBucket). + NestedReadBucket(hash[:]) + var err error + kvPayment, err = fetchPayment(bucket) + return err + }, func() {}) + require.NoError(t, err) + require.True(t, kvPayment.HTLCs[0].Route.Hops[0].LegacyPayload) + require.True(t, kvPayment.HTLCs[0].Route.Hops[1].LegacyPayload) + + sqlStore := setupTestSQLDB(t) + require.NoError(t, runPaymentsMigration(ctx, kvDB, sqlStore)) + + // comparePaymentData normalizes both sides (clearing LegacyPayload) + // before the equality check, so this must pass. + comparePaymentData(t, ctx, sqlStore, kvPayment) +} + // TestMigrationWithDuplicates tests migration of duplicate payments into // the payment_duplicates table. func TestMigrationWithDuplicates(t *testing.T) {