Also freeze the lnwire and record packages used by the migration. Copy the minimal subset of lnwire files (16) into payments/db/migration1/lnwire/ and all record files (6) into payments/db/migration1/record/. Three lnwire files are trimmed to avoid pulling in the full message-type dispatch tree — all changes are purely subtractive and can be verified with: diff lnwire/message.go payments/db/migration1/lnwire/message.go diff lnwire/writer.go payments/db/migration1/lnwire/writer.go diff lnwire/lnwire.go payments/db/migration1/lnwire/lnwire.go All migration1 files now import only the frozen packages, removing the live dependency on lnwire and record so future changes to those packages cannot affect migration correctness.
2795 lines
67 KiB
Go
2795 lines
67 KiB
Go
//go:build test_db_postgres || test_db_sqlite
|
|
|
|
package migration1
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/sha256"
|
|
"fmt"
|
|
"io"
|
|
"sort"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/btcsuite/btcd/btcec/v2"
|
|
"github.com/lightningnetwork/lnd/kvdb"
|
|
"github.com/lightningnetwork/lnd/lntypes"
|
|
"github.com/lightningnetwork/lnd/payments/db/migration1/lnwire"
|
|
"github.com/lightningnetwork/lnd/payments/db/migration1/record"
|
|
"github.com/lightningnetwork/lnd/payments/db/migration1/sqlc"
|
|
"github.com/lightningnetwork/lnd/sqldb"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// TestMigrationKVToSQL tests the basic payment migration from KV to SQL.
|
|
func TestMigrationKVToSQL(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Setup KV database and populate with test data.
|
|
kvDB := setupTestKVDB(t)
|
|
populateTestPayments(t, kvDB, 5)
|
|
|
|
sqlStore := setupTestSQLDB(t)
|
|
|
|
// Run migration in a single transaction.
|
|
err := runPaymentsMigration(ctx, kvDB, sqlStore)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// TestMigrationSequenceOrder ensures the migration follows sequence order
|
|
// rather than lexicographic hash order.
|
|
func TestMigrationSequenceOrder(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
|
|
kvDB := setupTestKVDB(t)
|
|
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
|
|
}
|
|
|
|
var globalAttemptID uint64
|
|
hash0 := [32]byte{}
|
|
hash1 := [32]byte{}
|
|
hash2 := [32]byte{}
|
|
hash0[0] = 3
|
|
hash1[0] = 2
|
|
hash2[0] = 1
|
|
|
|
if err := createTestPaymentInKV(
|
|
t, paymentsBucket, indexBucket, 0, hash0,
|
|
&globalAttemptID,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
|
|
// We make sure that the duplicate payment is skipped because
|
|
// it will be migrated separately into payment_duplicates.
|
|
if err := createTestDuplicatePaymentWithIndex(
|
|
t, paymentsBucket, indexBucket, hash0, 1, false,
|
|
&globalAttemptID,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
if err := createTestPaymentInKV(
|
|
t, paymentsBucket, indexBucket, 2, hash1,
|
|
&globalAttemptID,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
if err := createTestPaymentInKV(
|
|
t, paymentsBucket, indexBucket, 3, hash2,
|
|
&globalAttemptID,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}, func() {})
|
|
require.NoError(t, err)
|
|
|
|
sqlStore := setupTestSQLDB(t)
|
|
|
|
err = runPaymentsMigration(ctx, kvDB, sqlStore)
|
|
require.NoError(t, err)
|
|
|
|
resp, err := sqlStore.QueryPayments(ctx, Query{
|
|
MaxPayments: 10,
|
|
IncludeIncomplete: true,
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, resp.Payments, 3)
|
|
|
|
var (
|
|
exp0 lntypes.Hash
|
|
exp1 lntypes.Hash
|
|
exp2 lntypes.Hash
|
|
)
|
|
exp0[0] = 3
|
|
exp1[0] = 2
|
|
exp2[0] = 1
|
|
|
|
require.Equal(t, exp0, resp.Payments[0].Info.PaymentIdentifier)
|
|
require.Equal(t, exp1, resp.Payments[1].Info.PaymentIdentifier)
|
|
require.Equal(t, exp2, resp.Payments[2].Info.PaymentIdentifier)
|
|
}
|
|
|
|
// TestMigrationDataIntegrity verifies that migrated payment data exactly
|
|
// matches the original KV data when fetched through the SQLStore
|
|
// (SQLStore.FetchPayment). This covers the SQLStore query path separately
|
|
// from the migration's own batch validation.
|
|
func TestMigrationDataIntegrity(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Setup KV database with test data.
|
|
kvDB := setupTestKVDB(t)
|
|
numPayments := populateTestPayments(t, kvDB, 5)
|
|
|
|
// Fetch all payments from KV before migration.
|
|
kvPayments := fetchAllPaymentsFromKV(t, kvDB)
|
|
require.Len(t, kvPayments, numPayments)
|
|
|
|
// Setup SQL database and run migration.
|
|
sqlStore := setupTestSQLDB(t)
|
|
|
|
err := runPaymentsMigration(ctx, kvDB, sqlStore)
|
|
require.NoError(t, err)
|
|
|
|
// Compare each KV payment with its SQL counterpart using deep equality.
|
|
// This ensures that ALL fields match, not just a few selected ones.
|
|
for _, kvPayment := range kvPayments {
|
|
comparePaymentData(t, ctx, sqlStore, kvPayment)
|
|
}
|
|
}
|
|
|
|
// 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) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Setup KV database.
|
|
kvDB := setupTestKVDB(t)
|
|
|
|
// Create a payment with duplicates.
|
|
hash := createTestPaymentHash(t, 0)
|
|
err := kvdb.Update(kvDB, func(tx kvdb.RwTx) error {
|
|
// Create root buckets.
|
|
paymentsBucket, err := tx.CreateTopLevelBucket(
|
|
paymentsRootBucket,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
indexBucket, err := tx.CreateTopLevelBucket(paymentsIndexBucket)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Create primary payment with sequence 0 and globally unique
|
|
// attempt ID.
|
|
var globalAttemptID uint64
|
|
err = createTestPaymentInKV(
|
|
t, paymentsBucket, indexBucket, 0, hash,
|
|
&globalAttemptID,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Add 2 duplicate payments for the same hash.
|
|
paymentBucket := paymentsBucket.NestedReadWriteBucket(hash[:])
|
|
require.NotNil(t, paymentBucket)
|
|
|
|
dupBucket, err := paymentBucket.CreateBucketIfNotExists(
|
|
duplicatePaymentsBucket,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Create duplicate with sequence 1 using global attempt ID.
|
|
err = createTestDuplicatePayment(
|
|
t, dupBucket, hash, 1, true, &globalAttemptID,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Create duplicate with sequence 2 using global attempt ID.
|
|
err = createTestDuplicatePayment(
|
|
t, dupBucket, hash, 2, false, &globalAttemptID,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}, func() {})
|
|
require.NoError(t, err)
|
|
|
|
sqlStore := setupTestSQLDB(t)
|
|
|
|
// Run migration.
|
|
err = runPaymentsMigration(ctx, kvDB, sqlStore)
|
|
require.NoError(t, err)
|
|
|
|
// Verify in SQL database.
|
|
var count int64
|
|
err = sqlStore.db.ExecTx(
|
|
ctx, sqldb.ReadTxOpt(), func(q SQLQueries) error {
|
|
var err error
|
|
count, err = q.CountPayments(ctx)
|
|
return err
|
|
}, sqldb.NoOpReset,
|
|
)
|
|
require.NoError(t, err)
|
|
require.Equal(
|
|
t, int64(1), count, "SQL DB should have 1 payment",
|
|
)
|
|
|
|
var (
|
|
dbPayment sqlc.FetchPaymentRow
|
|
duplicates []sqlc.PaymentDuplicate
|
|
)
|
|
err = sqlStore.db.ExecTx(
|
|
ctx, sqldb.ReadTxOpt(), func(q SQLQueries) error {
|
|
var err error
|
|
dbPayment, err = q.FetchPayment(ctx, hash[:])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
duplicates, err = q.FetchPaymentDuplicates(
|
|
ctx, dbPayment.Payment.ID,
|
|
)
|
|
return err
|
|
}, sqldb.NoOpReset,
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
require.Len(t, duplicates, 2)
|
|
sort.SliceStable(duplicates, func(i, j int) bool {
|
|
return duplicates[i].AmountMsat < duplicates[j].AmountMsat
|
|
})
|
|
|
|
require.Equal(t, int64(2001000), duplicates[0].AmountMsat)
|
|
require.False(t, duplicates[0].FailReason.Valid)
|
|
require.NotEmpty(t, duplicates[0].SettlePreimage)
|
|
|
|
require.Equal(t, int64(2002000), duplicates[1].AmountMsat)
|
|
require.True(t, duplicates[1].FailReason.Valid)
|
|
require.Equal(
|
|
t, int32(FailureReasonError),
|
|
duplicates[1].FailReason.Int32,
|
|
)
|
|
require.Empty(t, duplicates[1].SettlePreimage)
|
|
}
|
|
|
|
// TestDuplicatePaymentsWithoutAttemptInfo verifies duplicate payments without
|
|
// attempt info are migrated with terminal failure reasons.
|
|
func TestDuplicatePaymentsWithoutAttemptInfo(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
|
|
}
|
|
|
|
var globalAttemptID uint64
|
|
err = createTestPaymentInKV(
|
|
t, paymentsBucket, indexBucket, 1, hash,
|
|
&globalAttemptID,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
paymentBucket := paymentsBucket.NestedReadWriteBucket(
|
|
hash[:],
|
|
)
|
|
require.NotNil(t, paymentBucket)
|
|
|
|
dupBucket, err := paymentBucket.CreateBucketIfNotExists(
|
|
duplicatePaymentsBucket,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := createDuplicateWithoutAttemptInfo(
|
|
t, dupBucket, hash, 2, true, false,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
if err := createDuplicateWithoutAttemptInfo(
|
|
t, dupBucket, hash, 3, false, true,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
if err := createDuplicateWithoutAttemptInfo(
|
|
t, dupBucket, hash, 4, false, false,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}, func() {})
|
|
require.NoError(t, err)
|
|
|
|
sqlStore := setupTestSQLDB(t)
|
|
err = runPaymentsMigration(ctx, kvDB, sqlStore)
|
|
require.NoError(t, err)
|
|
|
|
var (
|
|
dbPayment sqlc.FetchPaymentRow
|
|
duplicates []sqlc.PaymentDuplicate
|
|
)
|
|
err = sqlStore.db.ExecTx(
|
|
ctx, sqldb.ReadTxOpt(), func(q SQLQueries) error {
|
|
var err error
|
|
dbPayment, err = q.FetchPayment(ctx, hash[:])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
duplicates, err = q.FetchPaymentDuplicates(
|
|
ctx, dbPayment.Payment.ID,
|
|
)
|
|
return err
|
|
}, sqldb.NoOpReset,
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
require.Len(t, duplicates, 3)
|
|
sort.SliceStable(duplicates, func(i, j int) bool {
|
|
return duplicates[i].AmountMsat < duplicates[j].AmountMsat
|
|
})
|
|
|
|
require.Equal(t, int64(2002000), duplicates[0].AmountMsat)
|
|
require.NotEmpty(t, duplicates[0].SettlePreimage)
|
|
require.False(t, duplicates[0].FailReason.Valid)
|
|
|
|
require.Equal(t, int64(2003000), duplicates[1].AmountMsat)
|
|
require.True(t, duplicates[1].FailReason.Valid)
|
|
require.Equal(
|
|
t, int32(FailureReasonNoRoute),
|
|
duplicates[1].FailReason.Int32,
|
|
)
|
|
require.Empty(t, duplicates[1].SettlePreimage)
|
|
|
|
require.Equal(t, int64(2004000), duplicates[2].AmountMsat)
|
|
require.True(t, duplicates[2].FailReason.Valid)
|
|
require.Equal(
|
|
t, int32(FailureReasonError),
|
|
duplicates[2].FailReason.Int32,
|
|
)
|
|
require.Empty(t, duplicates[2].SettlePreimage)
|
|
}
|
|
|
|
// TestMigratePaymentWithMPP tests migration of a payment with MPP (multi-path
|
|
// payment) records.
|
|
func TestMigratePaymentWithMPP(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
kvDB := setupTestKVDB(t)
|
|
|
|
// Create a payment with MPP.
|
|
var paymentHash [32]byte
|
|
copy(paymentHash[:], []byte("test_mpp_payment_hash_12345"))
|
|
|
|
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
|
|
}
|
|
|
|
return createPaymentWithMPP(
|
|
t, paymentsBucket, indexBucket, paymentHash,
|
|
)
|
|
}, func() {})
|
|
require.NoError(t, err)
|
|
|
|
// Run migration.
|
|
sqlStore := setupTestSQLDB(t)
|
|
|
|
err = runPaymentsMigration(ctx, kvDB, sqlStore)
|
|
require.NoError(t, err)
|
|
|
|
// Verify payment matches.
|
|
assertPaymentDataMatches(t, ctx, kvDB, sqlStore, paymentHash)
|
|
}
|
|
|
|
// TestMigratePaymentWithAMP tests migration of a payment with AMP (atomic
|
|
// multi-path) records.
|
|
func TestMigratePaymentWithAMP(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
kvDB := setupTestKVDB(t)
|
|
|
|
var paymentHash [32]byte
|
|
copy(paymentHash[:], []byte("test_amp_payment_hash_12345"))
|
|
|
|
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
|
|
}
|
|
|
|
return createPaymentWithAMP(
|
|
t, paymentsBucket, indexBucket, paymentHash,
|
|
)
|
|
}, func() {})
|
|
require.NoError(t, err)
|
|
|
|
sqlStore := setupTestSQLDB(t)
|
|
|
|
err = runPaymentsMigration(ctx, kvDB, sqlStore)
|
|
require.NoError(t, err)
|
|
|
|
assertPaymentDataMatches(t, ctx, kvDB, sqlStore, paymentHash)
|
|
}
|
|
|
|
// TestMigratePaymentWithAMPSignedChildIndex tests migration of an AMP payment
|
|
// where the child index has the signed bit set.
|
|
func TestMigratePaymentWithAMPSignedChildIndex(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
kvDB := setupTestKVDB(t)
|
|
|
|
var paymentHash [32]byte
|
|
copy(paymentHash[:], []byte("test_amp_child_idx_8000"))
|
|
|
|
const childIndex = uint32(0x80000001)
|
|
|
|
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
|
|
}
|
|
|
|
return createPaymentWithAMPChildIndex(
|
|
t, paymentsBucket, indexBucket, paymentHash, childIndex,
|
|
)
|
|
}, func() {})
|
|
require.NoError(t, err)
|
|
|
|
sqlStore := setupTestSQLDB(t)
|
|
|
|
err = runPaymentsMigration(ctx, kvDB, sqlStore)
|
|
require.NoError(t, err)
|
|
|
|
assertPaymentDataMatches(t, ctx, kvDB, sqlStore, paymentHash)
|
|
}
|
|
|
|
// TestMigratePaymentWithCustomRecords tests migration of a payment with custom
|
|
// records.
|
|
func TestMigratePaymentWithCustomRecords(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
kvDB := setupTestKVDB(t)
|
|
|
|
var paymentHash [32]byte
|
|
copy(paymentHash[:], []byte("test_custom_records_hash_12"))
|
|
|
|
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
|
|
}
|
|
|
|
return createPaymentWithCustomRecords(
|
|
t, paymentsBucket, indexBucket, paymentHash,
|
|
)
|
|
}, func() {})
|
|
require.NoError(t, err)
|
|
|
|
sqlStore := setupTestSQLDB(t)
|
|
|
|
err = runPaymentsMigration(ctx, kvDB, sqlStore)
|
|
require.NoError(t, err)
|
|
|
|
assertPaymentDataMatches(t, ctx, kvDB, sqlStore, paymentHash)
|
|
}
|
|
|
|
// TestMigratePaymentWithBlindedRoute tests migration of a payment with blinded
|
|
// route.
|
|
func TestMigratePaymentWithBlindedRoute(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
kvDB := setupTestKVDB(t)
|
|
|
|
var paymentHash [32]byte
|
|
copy(paymentHash[:], []byte("test_blinded_route_hash_123"))
|
|
|
|
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
|
|
}
|
|
|
|
return createPaymentWithBlindedRoute(
|
|
t, paymentsBucket, indexBucket, paymentHash,
|
|
)
|
|
}, func() {})
|
|
require.NoError(t, err)
|
|
|
|
sqlStore := setupTestSQLDB(t)
|
|
|
|
err = runPaymentsMigration(ctx, kvDB, sqlStore)
|
|
require.NoError(t, err)
|
|
|
|
assertPaymentDataMatches(t, ctx, kvDB, sqlStore, paymentHash)
|
|
}
|
|
|
|
// TestMigratePaymentWithMetadata tests migration of a payment with hop
|
|
// metadata.
|
|
func TestMigratePaymentWithMetadata(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
kvDB := setupTestKVDB(t)
|
|
|
|
var paymentHash [32]byte
|
|
copy(paymentHash[:], []byte("test_metadata_payment_hash_"))
|
|
|
|
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
|
|
}
|
|
|
|
return createPaymentWithMetadata(
|
|
t, paymentsBucket, indexBucket, paymentHash,
|
|
)
|
|
}, func() {})
|
|
require.NoError(t, err)
|
|
|
|
sqlStore := setupTestSQLDB(t)
|
|
|
|
err = runPaymentsMigration(ctx, kvDB, sqlStore)
|
|
require.NoError(t, err)
|
|
|
|
assertPaymentDataMatches(t, ctx, kvDB, sqlStore, paymentHash)
|
|
}
|
|
|
|
// TestMigratePaymentWithAllFeatures tests migration with all optional
|
|
// features enabled.
|
|
func TestMigratePaymentWithAllFeatures(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
kvDB := setupTestKVDB(t)
|
|
|
|
var paymentHash [32]byte
|
|
copy(paymentHash[:], []byte("test_all_features_hash_1234"))
|
|
|
|
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
|
|
}
|
|
|
|
return createPaymentWithAllFeatures(
|
|
t, paymentsBucket, indexBucket, paymentHash,
|
|
)
|
|
}, func() {})
|
|
require.NoError(t, err)
|
|
|
|
sqlStore := setupTestSQLDB(t)
|
|
|
|
err = runPaymentsMigration(ctx, kvDB, sqlStore)
|
|
require.NoError(t, err)
|
|
|
|
assertPaymentDataMatches(t, ctx, kvDB, sqlStore, paymentHash)
|
|
}
|
|
|
|
// TestMigratePaymentFeatureCombinations tests selected feature combinations
|
|
// in a single migration to cover interactions without random data.
|
|
func TestMigratePaymentFeatureCombinations(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
kvDB := setupTestKVDB(t)
|
|
|
|
cases := []paymentFeatureSet{
|
|
{
|
|
name: "mpp_custom",
|
|
mpp: true,
|
|
customRecords: true,
|
|
},
|
|
{
|
|
name: "amp_blinded",
|
|
amp: true,
|
|
blindedRoute: true,
|
|
},
|
|
{
|
|
name: "custom_metadata",
|
|
customRecords: true,
|
|
hopMetadata: true,
|
|
},
|
|
{
|
|
name: "blinded_metadata",
|
|
blindedRoute: true,
|
|
hopMetadata: true,
|
|
},
|
|
{
|
|
name: "mpp_metadata",
|
|
mpp: true,
|
|
hopMetadata: true,
|
|
},
|
|
}
|
|
|
|
hashes := make([][32]byte, 0, len(cases))
|
|
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
|
|
}
|
|
|
|
var globalAttemptID uint64
|
|
for i, c := range cases {
|
|
hash := sha256.Sum256([]byte(c.name))
|
|
hashes = append(hashes, hash)
|
|
|
|
err := createPaymentWithFeatureSet(
|
|
t, paymentsBucket, indexBucket, hash,
|
|
uint64(10+i), c, &globalAttemptID,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}, func() {})
|
|
require.NoError(t, err)
|
|
|
|
sqlStore := setupTestSQLDB(t)
|
|
|
|
err = runPaymentsMigration(ctx, kvDB, sqlStore)
|
|
require.NoError(t, err)
|
|
|
|
for _, hash := range hashes {
|
|
assertPaymentDataMatches(t, ctx, kvDB, sqlStore, hash)
|
|
}
|
|
}
|
|
|
|
// TestMigratePaymentWithFailureMessage tests migration of a payment with a
|
|
// failed HTLC that includes a failure message.
|
|
func TestMigratePaymentWithFailureMessage(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
kvDB := setupTestKVDB(t)
|
|
|
|
var paymentHash [32]byte
|
|
copy(paymentHash[:], []byte("test_fail_msg_hash_123456789"))
|
|
|
|
// Create a payment with a failed HTLC.
|
|
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
|
|
}
|
|
|
|
// Create payment bucket.
|
|
paymentBucket, err := paymentsBucket.CreateBucketIfNotExists(
|
|
paymentHash[:],
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Add creation info.
|
|
var paymentID lntypes.Hash
|
|
copy(paymentID[:], paymentHash[:])
|
|
|
|
creationInfo := &PaymentCreationInfo{
|
|
PaymentIdentifier: paymentID,
|
|
Value: lnwire.MilliSatoshi(1000000),
|
|
CreationTime: time.Now().Add(-24 * time.Hour),
|
|
PaymentRequest: []byte("lnbc10utest"),
|
|
}
|
|
|
|
// Use a separate buffer for payment creation info to avoid
|
|
// reuse issues when serializing HTLC attempts later.
|
|
var creationInfoBuf bytes.Buffer
|
|
err = serializePaymentCreationInfo(
|
|
&creationInfoBuf, creationInfo,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
serialized := creationInfoBuf.Bytes()
|
|
|
|
err = paymentBucket.Put(
|
|
paymentCreationInfoKey, serialized,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Add sequence number.
|
|
seqBytes := make([]byte, 8)
|
|
byteOrder.PutUint64(seqBytes, 50)
|
|
err = paymentBucket.Put(paymentSequenceKey, seqBytes)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Add payment-level failure reason.
|
|
failReasonBytes := []byte{byte(FailureReasonNoRoute)}
|
|
err = paymentBucket.Put(
|
|
paymentFailInfoKey, failReasonBytes,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Create HTLC bucket with one failed attempt.
|
|
htlcBucket, err := paymentBucket.CreateBucketIfNotExists(
|
|
paymentHtlcsBucket,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Create the failed attempt with a failure message.
|
|
attemptID := uint64(500)
|
|
sessionKey, err := btcec.NewPrivateKey()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var sessionKeyBytes [32]byte
|
|
copy(sessionKeyBytes[:], sessionKey.Serialize())
|
|
|
|
var sourcePubKey Vertex
|
|
copy(sourcePubKey[:], sessionKey.PubKey().SerializeCompressed())
|
|
|
|
hopKey, err := btcec.NewPrivateKey()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Create a proper copy of the hash instead of referencing
|
|
// the local variable directly.
|
|
attemptHash := new(lntypes.Hash)
|
|
copy(attemptHash[:], paymentHash[:])
|
|
|
|
//nolint:ll
|
|
attemptInfo := &HTLCAttemptInfo{
|
|
AttemptID: attemptID,
|
|
sessionKey: sessionKeyBytes,
|
|
Route: Route{
|
|
TotalTimeLock: 500000,
|
|
TotalAmount: 900,
|
|
SourcePubKey: sourcePubKey,
|
|
Hops: []*Hop{
|
|
{
|
|
PubKeyBytes: NewVertex(hopKey.PubKey()),
|
|
ChannelID: 12345,
|
|
OutgoingTimeLock: 499500,
|
|
AmtToForward: 850,
|
|
},
|
|
},
|
|
},
|
|
AttemptTime: time.Now().Add(-2 * time.Hour),
|
|
Hash: attemptHash,
|
|
}
|
|
|
|
// Write attempt info.
|
|
attemptKey := make([]byte, len(htlcAttemptInfoKey)+8)
|
|
copy(attemptKey, htlcAttemptInfoKey)
|
|
byteOrder.PutUint64(
|
|
attemptKey[len(htlcAttemptInfoKey):], attemptID,
|
|
)
|
|
|
|
var b bytes.Buffer
|
|
err = serializeHTLCAttemptInfo(&b, attemptInfo)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = htlcBucket.Put(attemptKey, b.Bytes())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Add failure info with a message.
|
|
//nolint:ll
|
|
failInfo := &HTLCFailInfo{
|
|
FailTime: time.Now().Add(-1 * time.Hour),
|
|
Message: &lnwire.FailTemporaryChannelFailure{},
|
|
Reason: HTLCFailMessage,
|
|
FailureSourceIndex: 1,
|
|
}
|
|
|
|
failKey := make([]byte, len(htlcFailInfoKey)+8)
|
|
copy(failKey, htlcFailInfoKey)
|
|
byteOrder.PutUint64(failKey[len(htlcFailInfoKey):], attemptID)
|
|
|
|
b.Reset()
|
|
if err := serializeHTLCFailInfo(&b, failInfo); err != nil {
|
|
return err
|
|
}
|
|
if err := htlcBucket.Put(failKey, b.Bytes()); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Create index entry.
|
|
var idx bytes.Buffer
|
|
if err := WriteElements(
|
|
&idx, paymentIndexTypeHash, paymentHash[:],
|
|
); err != nil {
|
|
return err
|
|
}
|
|
return indexBucket.Put(seqBytes, idx.Bytes())
|
|
}, func() {})
|
|
require.NoError(t, err)
|
|
|
|
// Migrate to SQL.
|
|
sqlStore := setupTestSQLDB(t)
|
|
|
|
err = runPaymentsMigration(ctx, kvDB, sqlStore)
|
|
require.NoError(t, err)
|
|
|
|
// Verify data matches.
|
|
assertPaymentDataMatches(t, ctx, kvDB, sqlStore, paymentHash)
|
|
}
|
|
|
|
// setupTestKVDB creates a temporary KV database for testing.
|
|
func setupTestKVDB(t *testing.T) kvdb.Backend {
|
|
t.Helper()
|
|
|
|
backend, cleanup, err := kvdb.GetTestBackend(t.TempDir(), "payments")
|
|
require.NoError(t, err)
|
|
t.Cleanup(cleanup)
|
|
|
|
return backend
|
|
}
|
|
|
|
// populateTestPayments populates the KV database with test payment data.
|
|
func populateTestPayments(t *testing.T, db kvdb.Backend, numPayments int) int {
|
|
t.Helper()
|
|
|
|
err := kvdb.Update(db, func(tx kvdb.RwTx) error {
|
|
// Create root buckets.
|
|
paymentsBucket, err := tx.CreateTopLevelBucket(
|
|
paymentsRootBucket,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
indexBucket, err := tx.CreateTopLevelBucket(paymentsIndexBucket)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Create test payments with globally unique attempt IDs.
|
|
var globalAttemptID uint64
|
|
for i := 0; i < numPayments; i++ {
|
|
hash := createTestPaymentHash(t, i)
|
|
|
|
err := createTestPaymentInKV(
|
|
t, paymentsBucket, indexBucket, uint64(i), hash,
|
|
&globalAttemptID,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}, func() {})
|
|
|
|
require.NoError(t, err)
|
|
return numPayments
|
|
}
|
|
|
|
// serializeDuplicatePaymentCreationInfo serializes PaymentCreationInfo for
|
|
// duplicate payments. The time is stored in seconds (not nanoseconds) to match
|
|
// the format used by deserializeDuplicatePaymentCreationInfo in the KV store.
|
|
func serializeDuplicatePaymentCreationInfo(w io.Writer,
|
|
c *PaymentCreationInfo) error {
|
|
|
|
var scratch [8]byte
|
|
|
|
if _, err := w.Write(c.PaymentIdentifier[:]); err != nil {
|
|
return err
|
|
}
|
|
|
|
byteOrder.PutUint64(scratch[:], uint64(c.Value))
|
|
if _, err := w.Write(scratch[:]); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Store time in seconds (not nanoseconds) for duplicate payments.
|
|
// This matches the deserialization format used in
|
|
// deserializeDuplicatePaymentCreationInfo.
|
|
var unixSec int64
|
|
if !c.CreationTime.IsZero() {
|
|
unixSec = c.CreationTime.Unix()
|
|
}
|
|
byteOrder.PutUint64(scratch[:], uint64(unixSec))
|
|
if _, err := w.Write(scratch[:]); err != nil {
|
|
return err
|
|
}
|
|
|
|
byteOrder.PutUint32(scratch[:4], uint32(len(c.PaymentRequest)))
|
|
if _, err := w.Write(scratch[:4]); err != nil {
|
|
return err
|
|
}
|
|
|
|
if _, err := w.Write(c.PaymentRequest); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// createTestPaymentHash creates a deterministic payment hash for testing.
|
|
func createTestPaymentHash(t *testing.T, seed int) [32]byte {
|
|
t.Helper()
|
|
|
|
hash := sha256.Sum256([]byte{byte(seed)})
|
|
return hash
|
|
}
|
|
|
|
// createTestPaymentInKV creates a single payment in the KV store.
|
|
func createTestPaymentInKV(t *testing.T, paymentsBucket,
|
|
indexBucket kvdb.RwBucket, seqNum uint64, hash [32]byte,
|
|
globalAttemptID *uint64) error {
|
|
|
|
t.Helper()
|
|
|
|
// Create payment bucket.
|
|
paymentBucket, err := paymentsBucket.CreateBucketIfNotExists(hash[:])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Create payment creation info.
|
|
var paymentID lntypes.Hash
|
|
copy(paymentID[:], hash[:])
|
|
|
|
creationInfo := &PaymentCreationInfo{
|
|
PaymentIdentifier: paymentID,
|
|
Value: lnwire.MilliSatoshi(1000000 + seqNum*1000),
|
|
CreationTime: time.Now().Add(-24 * time.Hour),
|
|
PaymentRequest: []byte("lnbc1test"),
|
|
}
|
|
|
|
// Serialize and write creation info.
|
|
var b bytes.Buffer
|
|
err = serializePaymentCreationInfo(&b, creationInfo)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = paymentBucket.Put(paymentCreationInfoKey, b.Bytes())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Store sequence number.
|
|
seqBytes := make([]byte, 8)
|
|
byteOrder.PutUint64(seqBytes, seqNum)
|
|
err = paymentBucket.Put(paymentSequenceKey, seqBytes)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Add one HTLC attempt for each payment with globally unique ID.
|
|
htlcBucket, err := paymentBucket.CreateBucketIfNotExists(
|
|
paymentHtlcsBucket,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Increment global attempt ID and create HTLC attempt. So we have a
|
|
// globally unique attempt ID for the HTLC attempt.
|
|
*globalAttemptID++
|
|
err = createTestHTLCAttempt(
|
|
t, htlcBucket, hash, *globalAttemptID, seqNum%3 == 0,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var idx bytes.Buffer
|
|
err = WriteElements(&idx, paymentIndexTypeHash, hash[:])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return indexBucket.Put(seqBytes, idx.Bytes())
|
|
}
|
|
|
|
// createTestHTLCAttempt creates a test HTLC attempt in the KV store.
|
|
func createTestHTLCAttempt(t *testing.T, htlcBucket kvdb.RwBucket,
|
|
paymentHash [32]byte, attemptID uint64, shouldSettle bool) error {
|
|
t.Helper()
|
|
|
|
// Generate a session key.
|
|
sessionKey, err := btcec.NewPrivateKey()
|
|
require.NoError(t, err)
|
|
|
|
// Create a simple 2-hop route.
|
|
hop1Key, err := btcec.NewPrivateKey()
|
|
require.NoError(t, err)
|
|
|
|
hop2Key, err := btcec.NewPrivateKey()
|
|
require.NoError(t, err)
|
|
|
|
var sourcePubKey Vertex
|
|
copy(sourcePubKey[:], sessionKey.PubKey().SerializeCompressed())
|
|
|
|
// Convert session key to [32]byte.
|
|
var sessionKeyBytes [32]byte
|
|
copy(sessionKeyBytes[:], sessionKey.Serialize())
|
|
|
|
attemptInfo := &HTLCAttemptInfo{
|
|
AttemptID: attemptID,
|
|
sessionKey: sessionKeyBytes,
|
|
Route: Route{
|
|
TotalTimeLock: 500000,
|
|
TotalAmount: 900,
|
|
SourcePubKey: sourcePubKey,
|
|
Hops: []*Hop{
|
|
{
|
|
PubKeyBytes: NewVertex(
|
|
hop1Key.PubKey(),
|
|
),
|
|
ChannelID: 12345,
|
|
OutgoingTimeLock: 499500,
|
|
AmtToForward: 850,
|
|
},
|
|
{
|
|
PubKeyBytes: NewVertex(
|
|
hop2Key.PubKey(),
|
|
),
|
|
ChannelID: 67890,
|
|
OutgoingTimeLock: 499000,
|
|
AmtToForward: 800,
|
|
},
|
|
},
|
|
},
|
|
AttemptTime: time.Now().Add(-2 * time.Hour),
|
|
Hash: (*lntypes.Hash)(&paymentHash),
|
|
}
|
|
|
|
// Serialize and write attempt info.
|
|
attemptKey := make([]byte, len(htlcAttemptInfoKey)+8)
|
|
copy(attemptKey, htlcAttemptInfoKey)
|
|
byteOrder.PutUint64(attemptKey[len(htlcAttemptInfoKey):], attemptID)
|
|
|
|
var b bytes.Buffer
|
|
err = serializeHTLCAttemptInfo(&b, attemptInfo)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = htlcBucket.Put(attemptKey, b.Bytes())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Add settlement if requested.
|
|
if shouldSettle {
|
|
settleInfo := &HTLCSettleInfo{
|
|
Preimage: lntypes.Preimage(paymentHash),
|
|
SettleTime: time.Now().Add(-1 * time.Hour),
|
|
}
|
|
|
|
settleKey := make([]byte, len(htlcSettleInfoKey)+8)
|
|
copy(settleKey, htlcSettleInfoKey)
|
|
byteOrder.PutUint64(
|
|
settleKey[len(htlcSettleInfoKey):], attemptID,
|
|
)
|
|
|
|
var sb bytes.Buffer
|
|
err = serializeHTLCSettleInfo(&sb, settleInfo)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = htlcBucket.Put(settleKey, sb.Bytes())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// createTestDuplicatePaymentWithIndex creates a duplicate payment and adds
|
|
// a matching entry into the global payment sequence index.
|
|
func createTestDuplicatePaymentWithIndex(t *testing.T,
|
|
paymentsBucket kvdb.RwBucket, indexBucket kvdb.RwBucket,
|
|
paymentHash [32]byte, seqNum uint64, shouldSettle bool,
|
|
globalAttemptID *uint64) error {
|
|
t.Helper()
|
|
|
|
paymentBucket, err := paymentsBucket.CreateBucketIfNotExists(
|
|
paymentHash[:],
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
dupBucket, err := paymentBucket.CreateBucketIfNotExists(
|
|
duplicatePaymentsBucket,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := createTestDuplicatePayment(
|
|
t, dupBucket, paymentHash, seqNum, shouldSettle,
|
|
globalAttemptID,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
|
|
seqBytes := make([]byte, 8)
|
|
byteOrder.PutUint64(seqBytes, seqNum)
|
|
var idx bytes.Buffer
|
|
if err := WriteElements(
|
|
&idx, paymentIndexTypeHash, paymentHash[:],
|
|
); err != nil {
|
|
return err
|
|
}
|
|
|
|
return indexBucket.Put(seqBytes, idx.Bytes())
|
|
}
|
|
|
|
// createTestDuplicatePayment creates a duplicate payment in the KV store.
|
|
func createTestDuplicatePayment(t *testing.T,
|
|
dupBucket kvdb.RwBucket, paymentHash [32]byte, seqNum uint64,
|
|
shouldSettle bool, globalAttemptID *uint64) error {
|
|
|
|
t.Helper()
|
|
|
|
// Create bucket for this duplicate using sequence number as key.
|
|
seqBytes := make([]byte, 8)
|
|
byteOrder.PutUint64(seqBytes, seqNum)
|
|
|
|
dupPaymentBucket, err := dupBucket.CreateBucketIfNotExists(seqBytes)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Store sequence number.
|
|
err = dupPaymentBucket.Put(duplicatePaymentSequenceKey, seqBytes)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Create payment creation info.
|
|
var paymentID lntypes.Hash
|
|
copy(paymentID[:], paymentHash[:])
|
|
|
|
creationInfo := &PaymentCreationInfo{
|
|
PaymentIdentifier: paymentID,
|
|
Value: lnwire.MilliSatoshi(2000000 + seqNum*1000),
|
|
CreationTime: time.Now().Add(-48 * time.Hour),
|
|
PaymentRequest: []byte("lnbc1duplicate"),
|
|
}
|
|
|
|
var b bytes.Buffer
|
|
err = serializeDuplicatePaymentCreationInfo(&b, creationInfo)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = dupPaymentBucket.Put(duplicatePaymentCreationInfoKey, b.Bytes())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Generate a session key for the duplicate attempt.
|
|
sessionKey, err := btcec.NewPrivateKey()
|
|
require.NoError(t, err)
|
|
|
|
// Create route for duplicate.
|
|
hop1Key, err := btcec.NewPrivateKey()
|
|
require.NoError(t, err)
|
|
|
|
hop2Key, err := btcec.NewPrivateKey()
|
|
require.NoError(t, err)
|
|
|
|
var sourcePubKey Vertex
|
|
copy(sourcePubKey[:], sessionKey.PubKey().SerializeCompressed())
|
|
|
|
var sessionKeyBytes [32]byte
|
|
copy(sessionKeyBytes[:], sessionKey.Serialize())
|
|
|
|
// Use globally unique attempt ID.
|
|
*globalAttemptID++
|
|
attemptID := *globalAttemptID
|
|
|
|
duplicateAttempt := &duplicateHTLCAttemptInfo{
|
|
attemptID: attemptID,
|
|
sessionKey: sessionKeyBytes,
|
|
route: Route{
|
|
TotalTimeLock: 500000,
|
|
TotalAmount: 900,
|
|
SourcePubKey: sourcePubKey,
|
|
Hops: []*Hop{
|
|
{
|
|
PubKeyBytes: NewVertex(
|
|
hop1Key.PubKey(),
|
|
),
|
|
ChannelID: 12345,
|
|
OutgoingTimeLock: 499500,
|
|
AmtToForward: 850,
|
|
},
|
|
{
|
|
PubKeyBytes: NewVertex(
|
|
hop2Key.PubKey(),
|
|
),
|
|
ChannelID: 67890,
|
|
OutgoingTimeLock: 499000,
|
|
AmtToForward: 800,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
// Serialize and write attempt info (using existing WriteElements
|
|
// and SerializeRoute).
|
|
var ab bytes.Buffer
|
|
if err := WriteElements(
|
|
&ab, duplicateAttempt.attemptID,
|
|
duplicateAttempt.sessionKey,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
if err := SerializeRoute(&ab, duplicateAttempt.route); err != nil {
|
|
return err
|
|
}
|
|
err = dupPaymentBucket.Put(duplicatePaymentAttemptInfoKey, ab.Bytes())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Add settlement if requested.
|
|
if shouldSettle {
|
|
settleInfo := &HTLCSettleInfo{
|
|
Preimage: lntypes.Preimage(paymentHash),
|
|
SettleTime: time.Now().Add(-1 * time.Hour),
|
|
}
|
|
|
|
var sb bytes.Buffer
|
|
err = serializeHTLCSettleInfo(&sb, settleInfo)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = dupPaymentBucket.Put(
|
|
duplicatePaymentSettleInfoKey, sb.Bytes(),
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// createDuplicateWithoutAttemptInfo creates a duplicate payment bucket with
|
|
// settle/fail info but without attempt info.
|
|
func createDuplicateWithoutAttemptInfo(t *testing.T,
|
|
dupBucket kvdb.RwBucket, paymentHash [32]byte, seqNum uint64,
|
|
shouldSettle bool, shouldFail bool) error {
|
|
t.Helper()
|
|
|
|
seqBytes := make([]byte, 8)
|
|
byteOrder.PutUint64(seqBytes, seqNum)
|
|
|
|
dupPaymentBucket, err := dupBucket.CreateBucketIfNotExists(seqBytes)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := dupPaymentBucket.Put(
|
|
duplicatePaymentSequenceKey, seqBytes,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
|
|
var paymentID lntypes.Hash
|
|
copy(paymentID[:], paymentHash[:])
|
|
|
|
creationInfo := &PaymentCreationInfo{
|
|
PaymentIdentifier: paymentID,
|
|
Value: lnwire.MilliSatoshi(2000000 + seqNum*1000),
|
|
CreationTime: time.Now().Add(-48 * time.Hour),
|
|
PaymentRequest: []byte("lnbc1duplicate"),
|
|
}
|
|
|
|
var b bytes.Buffer
|
|
err = serializeDuplicatePaymentCreationInfo(&b, creationInfo)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := dupPaymentBucket.Put(
|
|
duplicatePaymentCreationInfoKey, b.Bytes(),
|
|
); err != nil {
|
|
return err
|
|
}
|
|
|
|
switch {
|
|
case shouldSettle && shouldFail:
|
|
return fmt.Errorf("invalid duplicate state")
|
|
case shouldSettle:
|
|
settleInfo := &HTLCSettleInfo{
|
|
Preimage: lntypes.Preimage(paymentHash),
|
|
SettleTime: time.Now().Add(-1 * time.Hour),
|
|
}
|
|
|
|
var sb bytes.Buffer
|
|
err = serializeHTLCSettleInfo(&sb, settleInfo)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := dupPaymentBucket.Put(
|
|
duplicatePaymentSettleInfoKey, sb.Bytes(),
|
|
); err != nil {
|
|
return err
|
|
}
|
|
case shouldFail:
|
|
failReasonBytes := []byte{byte(FailureReasonNoRoute)}
|
|
if err := dupPaymentBucket.Put(
|
|
duplicatePaymentFailInfoKey, failReasonBytes,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// fetchAllPaymentsFromKV fetches all payments from the KV store using the
|
|
// KVStore implementation.
|
|
func fetchAllPaymentsFromKV(t *testing.T, kvDB kvdb.Backend) []*MPPayment {
|
|
t.Helper()
|
|
|
|
kvStore, err := NewKVStore(kvDB, WithNoMigration(true))
|
|
require.NoError(t, err)
|
|
|
|
payments, err := kvStore.FetchPayments()
|
|
require.NoError(t, err)
|
|
|
|
return payments
|
|
}
|
|
|
|
// normalizePaymentData makes sure that the payment data is normalized for
|
|
// comparison using the same logic as in-migration validation.
|
|
func normalizePaymentData(payment *MPPayment) {
|
|
normalizePaymentForCompare(payment)
|
|
}
|
|
|
|
// comparePaymentData compares a KV payment with its SQL counterpart using
|
|
// deep equality check (similar to invoice migration).
|
|
func comparePaymentData(t *testing.T, ctx context.Context, sqlStore *SQLStore,
|
|
kvPayment *MPPayment) {
|
|
|
|
t.Helper()
|
|
|
|
// Fetch the SQL payment as MPPayment using SQLStore.
|
|
var paymentHash lntypes.Hash
|
|
copy(paymentHash[:], kvPayment.Info.PaymentIdentifier[:])
|
|
|
|
sqlPayment, err := sqlStore.FetchPayment(ctx, paymentHash)
|
|
require.NoError(t, err, "SQL payment should exist for %x",
|
|
paymentHash[:8])
|
|
|
|
// Normalize time precision to microseconds.
|
|
normalizePaymentData(kvPayment)
|
|
normalizePaymentData(sqlPayment)
|
|
|
|
// Deep equality check - compares all fields recursively.
|
|
require.Equal(t, kvPayment, sqlPayment,
|
|
"KV and SQL payments should be equal for %x", paymentHash[:8])
|
|
}
|
|
|
|
// runPaymentsMigration executes the payment migration from KV to SQL within
|
|
// a SQL transaction.
|
|
func runPaymentsMigration(ctx context.Context, kvDB kvdb.Backend,
|
|
sqlStore *SQLStore) error {
|
|
|
|
return sqlStore.db.ExecTx(
|
|
ctx, sqldb.WriteTxOpt(), func(tx SQLQueries) error {
|
|
migTx, ok := tx.(SQLMigrationQueries)
|
|
if !ok {
|
|
return fmt.Errorf("db does not implement " +
|
|
"SQLMigrationQueries")
|
|
}
|
|
|
|
return MigratePaymentsKVToSQL(
|
|
ctx, kvDB, migTx, &SQLStoreConfig{
|
|
QueryCfg: sqlStore.cfg.QueryCfg,
|
|
},
|
|
)
|
|
}, sqldb.NoOpReset,
|
|
)
|
|
}
|
|
|
|
// assertPaymentDataMatches verifies a payment in KV matches its SQL counterpart
|
|
// using deep equality check.
|
|
func assertPaymentDataMatches(t *testing.T, ctx context.Context,
|
|
kvDB kvdb.Backend, sqlStore *SQLStore, hash [32]byte) {
|
|
t.Helper()
|
|
|
|
// Fetch from KV.
|
|
var kvPayment *MPPayment
|
|
err := kvdb.View(kvDB, func(tx kvdb.RTx) error {
|
|
paymentsBucket := tx.ReadBucket(paymentsRootBucket)
|
|
if paymentsBucket == nil {
|
|
return nil
|
|
}
|
|
|
|
paymentBucket := paymentsBucket.NestedReadBucket(hash[:])
|
|
if paymentBucket == nil {
|
|
return nil
|
|
}
|
|
|
|
var err error
|
|
kvPayment, err = fetchPayment(paymentBucket)
|
|
return err
|
|
}, func() {})
|
|
require.NoError(t, err)
|
|
|
|
if kvPayment == nil {
|
|
// Payment doesn't exist in KV, should not exist in SQL
|
|
// either.
|
|
var paymentHash lntypes.Hash
|
|
copy(paymentHash[:], hash[:])
|
|
_, err := sqlStore.FetchPayment(ctx, paymentHash)
|
|
require.Error(
|
|
t, err, "payment should not exist in SQL if not "+
|
|
"in KV",
|
|
)
|
|
return
|
|
}
|
|
|
|
// Use the deep comparison function.
|
|
comparePaymentData(t, ctx, sqlStore, kvPayment)
|
|
}
|
|
|
|
// paymentTestConfig holds configuration for creating test payments with various
|
|
// features.
|
|
type paymentTestConfig struct {
|
|
hash [32]byte
|
|
seqNum uint64
|
|
value lnwire.MilliSatoshi
|
|
creationTime time.Time
|
|
paymentRequest string
|
|
attemptID uint64
|
|
numHops int
|
|
baseChannelID uint64
|
|
baseTimeLock uint32
|
|
paymentCustomRecs lnwire.CustomRecords
|
|
attemptCustomRecs lnwire.CustomRecords
|
|
hopConfigurator func(hop *Hop, index int, isFinal bool)
|
|
}
|
|
|
|
// serializeAndPut serializes data using the provided serializer function and
|
|
// writes it to the bucket.
|
|
func serializeAndPut(bucket kvdb.RwBucket, key []byte,
|
|
serializer func(io.Writer) error) error {
|
|
|
|
var b bytes.Buffer
|
|
if err := serializer(&b); err != nil {
|
|
return err
|
|
}
|
|
return bucket.Put(key, b.Bytes())
|
|
}
|
|
|
|
// generateSessionKey creates a new session key and returns the private key,
|
|
// source public key vertex, and serialized key bytes.
|
|
func generateSessionKey(t *testing.T) (*btcec.PrivateKey, Vertex,
|
|
[32]byte, error) {
|
|
|
|
t.Helper()
|
|
|
|
sessionKey, err := btcec.NewPrivateKey()
|
|
if err != nil {
|
|
return nil, Vertex{}, [32]byte{}, err
|
|
}
|
|
|
|
var sourcePubKey Vertex
|
|
copy(sourcePubKey[:], sessionKey.PubKey().SerializeCompressed())
|
|
|
|
var sessionKeyBytes [32]byte
|
|
copy(sessionKeyBytes[:], sessionKey.Serialize())
|
|
|
|
return sessionKey, sourcePubKey, sessionKeyBytes, nil
|
|
}
|
|
|
|
// createTestHops creates the specified number of test hops with the given
|
|
// parameters. The configurator function is called for each hop to allow
|
|
// feature-specific customization.
|
|
func createTestHops(t *testing.T, numHops int, baseAmount lnwire.MilliSatoshi,
|
|
baseChannelID uint64, baseTimeLock uint32,
|
|
configurator func(*Hop, int, bool)) ([]*Hop,
|
|
lnwire.MilliSatoshi, error) {
|
|
|
|
t.Helper()
|
|
|
|
hops := make([]*Hop, numHops)
|
|
currentAmt := baseAmount
|
|
|
|
for i := 0; i < numHops; i++ {
|
|
hopKey, err := btcec.NewPrivateKey()
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
amt := baseAmount - lnwire.MilliSatoshi(uint64(i)*100)
|
|
hop := &Hop{
|
|
PubKeyBytes: NewVertex(hopKey.PubKey()),
|
|
ChannelID: baseChannelID + uint64(i),
|
|
OutgoingTimeLock: baseTimeLock - uint32(i*40),
|
|
AmtToForward: amt,
|
|
}
|
|
|
|
// Apply feature-specific configuration.
|
|
if configurator != nil {
|
|
configurator(hop, i, i == numHops-1)
|
|
}
|
|
|
|
hops[i] = hop
|
|
currentAmt = amt
|
|
}
|
|
|
|
return hops, currentAmt, nil
|
|
}
|
|
|
|
// writeHTLCAttempt writes the HTLC attempt info to the bucket.
|
|
func writeHTLCAttempt(bucket kvdb.RwBucket, attemptID uint64,
|
|
info *HTLCAttemptInfo) error {
|
|
|
|
attemptKey := make([]byte, len(htlcAttemptInfoKey)+8)
|
|
copy(attemptKey, htlcAttemptInfoKey)
|
|
byteOrder.PutUint64(attemptKey[len(htlcAttemptInfoKey):], attemptID)
|
|
|
|
return serializeAndPut(bucket, attemptKey, func(w io.Writer) error {
|
|
return serializeHTLCAttemptInfo(w, info)
|
|
})
|
|
}
|
|
|
|
// writeHTLCSettle writes the HTLC settle info to the bucket.
|
|
func writeHTLCSettle(bucket kvdb.RwBucket, attemptID uint64,
|
|
info *HTLCSettleInfo) error {
|
|
|
|
settleKey := make([]byte, len(htlcSettleInfoKey)+8)
|
|
copy(settleKey, htlcSettleInfoKey)
|
|
byteOrder.PutUint64(settleKey[len(htlcSettleInfoKey):], attemptID)
|
|
|
|
return serializeAndPut(bucket, settleKey, func(w io.Writer) error {
|
|
return serializeHTLCSettleInfo(w, info)
|
|
})
|
|
}
|
|
|
|
// createIndexEntry creates a payment index entry in the index bucket.
|
|
func createIndexEntry(indexBucket kvdb.RwBucket, seqBytes []byte,
|
|
hash [32]byte) error {
|
|
|
|
var idx bytes.Buffer
|
|
if err := WriteElements(
|
|
&idx, paymentIndexTypeHash, hash[:],
|
|
); err != nil {
|
|
return err
|
|
}
|
|
|
|
return indexBucket.Put(seqBytes, idx.Bytes())
|
|
}
|
|
|
|
// createTestPayment creates a test payment with the specified configuration,
|
|
// handling all common boilerplate code.
|
|
func createTestPayment(t *testing.T, paymentsBucket, indexBucket kvdb.RwBucket,
|
|
cfg paymentTestConfig) error {
|
|
|
|
t.Helper()
|
|
|
|
// Create payment bucket.
|
|
paymentBucket, err := paymentsBucket.CreateBucketIfNotExists(
|
|
cfg.hash[:],
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Create payment ID.
|
|
var paymentID lntypes.Hash
|
|
copy(paymentID[:], cfg.hash[:])
|
|
|
|
// Create and serialize payment creation info.
|
|
creationInfo := &PaymentCreationInfo{
|
|
PaymentIdentifier: paymentID,
|
|
Value: cfg.value,
|
|
CreationTime: cfg.creationTime,
|
|
PaymentRequest: []byte(cfg.paymentRequest),
|
|
FirstHopCustomRecords: cfg.paymentCustomRecs,
|
|
}
|
|
|
|
err = serializeAndPut(
|
|
paymentBucket, paymentCreationInfoKey,
|
|
func(w io.Writer) error {
|
|
return serializePaymentCreationInfo(w, creationInfo)
|
|
},
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Store sequence number.
|
|
seqBytes := make([]byte, 8)
|
|
byteOrder.PutUint64(seqBytes, cfg.seqNum)
|
|
if err := paymentBucket.Put(paymentSequenceKey, seqBytes); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Create HTLC bucket.
|
|
htlcBucket, err := paymentBucket.CreateBucketIfNotExists(
|
|
paymentHtlcsBucket,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Generate session key.
|
|
_, sourcePubKey, sessionKeyBytes, err := generateSessionKey(t)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Create route with hops.
|
|
hops, totalAmount, err := createTestHops(
|
|
t, cfg.numHops, cfg.value, cfg.baseChannelID,
|
|
cfg.baseTimeLock, cfg.hopConfigurator,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Create and serialize attempt info.
|
|
attemptInfo := &HTLCAttemptInfo{
|
|
AttemptID: cfg.attemptID,
|
|
sessionKey: sessionKeyBytes,
|
|
Route: Route{
|
|
TotalTimeLock: cfg.baseTimeLock,
|
|
TotalAmount: totalAmount,
|
|
SourcePubKey: sourcePubKey,
|
|
Hops: hops,
|
|
FirstHopWireCustomRecords: cfg.attemptCustomRecs,
|
|
},
|
|
AttemptTime: cfg.creationTime.Add(time.Minute),
|
|
Hash: (*lntypes.Hash)(&cfg.hash),
|
|
}
|
|
|
|
if err = writeHTLCAttempt(
|
|
htlcBucket, cfg.attemptID, attemptInfo,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Add settlement.
|
|
settleInfo := &HTLCSettleInfo{
|
|
Preimage: lntypes.Preimage(cfg.hash),
|
|
SettleTime: cfg.creationTime.Add(2 * time.Minute),
|
|
}
|
|
|
|
if err := writeHTLCSettle(
|
|
htlcBucket, cfg.attemptID, settleInfo,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Create index entry.
|
|
return createIndexEntry(indexBucket, seqBytes, cfg.hash)
|
|
}
|
|
|
|
// createPaymentWithMPP creates a payment with MPP records on the final hop.
|
|
func createPaymentWithMPP(t *testing.T, paymentsBucket,
|
|
indexBucket kvdb.RwBucket, hash [32]byte) error {
|
|
|
|
t.Helper()
|
|
|
|
return createTestPayment(
|
|
t, paymentsBucket, indexBucket,
|
|
paymentTestConfig{
|
|
hash: hash,
|
|
seqNum: 1,
|
|
value: 50000,
|
|
creationTime: time.Date(
|
|
2024, 1, 1, 12, 0, 0, 0, time.UTC,
|
|
),
|
|
paymentRequest: "lnbc500n1test_mpp",
|
|
attemptID: 1,
|
|
numHops: 3,
|
|
baseChannelID: 100000,
|
|
baseTimeLock: 500000,
|
|
hopConfigurator: func(hop *Hop, index int,
|
|
isFinal bool) {
|
|
|
|
if isFinal {
|
|
var paymentAddr [32]byte
|
|
copy(
|
|
paymentAddr[:],
|
|
[]byte(
|
|
"test_mpp_payment_"+
|
|
"address_32",
|
|
),
|
|
)
|
|
hop.MPP = record.NewMPP(
|
|
lnwire.MilliSatoshi(50000),
|
|
paymentAddr,
|
|
)
|
|
}
|
|
},
|
|
},
|
|
)
|
|
}
|
|
|
|
// createPaymentWithAMP creates a payment with AMP records on the final hop.
|
|
func createPaymentWithAMP(t *testing.T, paymentsBucket,
|
|
indexBucket kvdb.RwBucket, hash [32]byte) error {
|
|
t.Helper()
|
|
|
|
return createPaymentWithAMPChildIndex(
|
|
t, paymentsBucket, indexBucket, hash, 0,
|
|
)
|
|
}
|
|
|
|
// createPaymentWithAMPChildIndex creates a payment with AMP records on the
|
|
// final hop and a specific child index.
|
|
func createPaymentWithAMPChildIndex(t *testing.T, paymentsBucket,
|
|
indexBucket kvdb.RwBucket, hash [32]byte, childIndex uint32) error {
|
|
|
|
t.Helper()
|
|
|
|
return createTestPayment(
|
|
t, paymentsBucket, indexBucket,
|
|
paymentTestConfig{
|
|
hash: hash,
|
|
seqNum: 2,
|
|
value: 75000,
|
|
creationTime: time.Date(
|
|
2024, 2, 1, 10, 0, 0, 0, time.UTC,
|
|
),
|
|
paymentRequest: "lnbc750n1test_amp",
|
|
attemptID: 1,
|
|
numHops: 2,
|
|
baseChannelID: 200000,
|
|
baseTimeLock: 600000,
|
|
hopConfigurator: func(hop *Hop, index int,
|
|
isFinal bool) {
|
|
|
|
if isFinal {
|
|
var rootShare [32]byte
|
|
copy(
|
|
rootShare[:],
|
|
[]byte(
|
|
"test_amp_root_share"+
|
|
"_12345678",
|
|
),
|
|
)
|
|
var setID [32]byte
|
|
copy(
|
|
setID[:],
|
|
[]byte(
|
|
"test_amp_set_id_"+
|
|
"123456789012",
|
|
),
|
|
)
|
|
hop.AMP = record.NewAMP(
|
|
rootShare, setID, childIndex,
|
|
)
|
|
}
|
|
},
|
|
},
|
|
)
|
|
}
|
|
|
|
// createPaymentWithCustomRecords creates a payment with custom records at all
|
|
// levels.
|
|
func createPaymentWithCustomRecords(t *testing.T, paymentsBucket,
|
|
indexBucket kvdb.RwBucket, hash [32]byte) error {
|
|
|
|
t.Helper()
|
|
|
|
return createTestPayment(
|
|
t, paymentsBucket, indexBucket,
|
|
paymentTestConfig{
|
|
hash: hash,
|
|
seqNum: 3,
|
|
value: 100000,
|
|
creationTime: time.Date(
|
|
2024, 3, 1, 14, 0, 0, 0, time.UTC,
|
|
),
|
|
paymentRequest: "lnbc1m1test_custom",
|
|
attemptID: 1,
|
|
numHops: 3,
|
|
baseChannelID: 300000,
|
|
baseTimeLock: 700000,
|
|
paymentCustomRecs: lnwire.CustomRecords{
|
|
65536: []byte("payment_level_value_1"),
|
|
65537: []byte("payment_level_value_2"),
|
|
},
|
|
attemptCustomRecs: lnwire.CustomRecords{
|
|
65541: []byte("attempt_custom_value_1"),
|
|
65542: []byte("attempt_custom_value_2"),
|
|
},
|
|
hopConfigurator: func(hop *Hop, index int,
|
|
isFinal bool) {
|
|
|
|
hop.CustomRecords = record.CustomSet{
|
|
65538 + uint64(index): []byte(
|
|
fmt.Sprintf(
|
|
"hop_%d_custom_value",
|
|
index,
|
|
),
|
|
),
|
|
}
|
|
},
|
|
},
|
|
)
|
|
}
|
|
|
|
// createPaymentWithBlindedRoute creates a payment with blinded route data.
|
|
func createPaymentWithBlindedRoute(t *testing.T, paymentsBucket,
|
|
indexBucket kvdb.RwBucket, hash [32]byte) error {
|
|
|
|
t.Helper()
|
|
|
|
return createTestPayment(
|
|
t, paymentsBucket, indexBucket,
|
|
paymentTestConfig{
|
|
hash: hash,
|
|
seqNum: 4,
|
|
value: 120000,
|
|
creationTime: time.Date(
|
|
2024, 4, 1, 16, 0, 0, 0, time.UTC,
|
|
),
|
|
paymentRequest: "lnbc1200n1test_blinded",
|
|
attemptID: 1,
|
|
numHops: 4,
|
|
baseChannelID: 400000,
|
|
baseTimeLock: 800000,
|
|
hopConfigurator: func(hop *Hop, index int,
|
|
isFinal bool) {
|
|
|
|
if isFinal {
|
|
blindingKey, err := btcec.
|
|
NewPrivateKey()
|
|
|
|
require.NoError(t, err)
|
|
|
|
hop.BlindingPoint = blindingKey.PubKey()
|
|
hop.EncryptedData = []byte(
|
|
"encrypted_blinded_route_" +
|
|
"data_test_value_12345",
|
|
)
|
|
hop.TotalAmtMsat = lnwire.MilliSatoshi(
|
|
119400,
|
|
)
|
|
}
|
|
},
|
|
},
|
|
)
|
|
}
|
|
|
|
// createPaymentWithMetadata creates a payment with hop metadata.
|
|
func createPaymentWithMetadata(t *testing.T, paymentsBucket,
|
|
indexBucket kvdb.RwBucket, hash [32]byte) error {
|
|
|
|
t.Helper()
|
|
|
|
return createTestPayment(
|
|
t, paymentsBucket, indexBucket,
|
|
paymentTestConfig{
|
|
hash: hash,
|
|
seqNum: 5,
|
|
value: 80000,
|
|
creationTime: time.Date(
|
|
2024, 5, 1, 18, 0, 0, 0, time.UTC,
|
|
),
|
|
paymentRequest: "lnbc800n1test_metadata",
|
|
attemptID: 1,
|
|
numHops: 3,
|
|
baseChannelID: 500000,
|
|
baseTimeLock: 900000,
|
|
hopConfigurator: func(hop *Hop, index int,
|
|
isFinal bool) {
|
|
|
|
hop.Metadata = []byte(
|
|
fmt.Sprintf(
|
|
"hop_%d_metadata_value", index,
|
|
),
|
|
)
|
|
},
|
|
},
|
|
)
|
|
}
|
|
|
|
// createPaymentWithAllFeatures creates a payment with all optional features
|
|
// enabled.
|
|
func createPaymentWithAllFeatures(t *testing.T, paymentsBucket,
|
|
indexBucket kvdb.RwBucket, hash [32]byte) error {
|
|
|
|
t.Helper()
|
|
|
|
return createTestPayment(
|
|
t, paymentsBucket, indexBucket,
|
|
paymentTestConfig{
|
|
hash: hash,
|
|
seqNum: 6,
|
|
value: 150000,
|
|
creationTime: time.Date(
|
|
2024, 6, 1, 20, 0, 0, 0, time.UTC,
|
|
),
|
|
paymentRequest: "lnbc1500n1test_all_features",
|
|
attemptID: 1,
|
|
numHops: 4,
|
|
baseChannelID: 600000,
|
|
baseTimeLock: 1000000,
|
|
paymentCustomRecs: lnwire.CustomRecords{
|
|
65543: []byte("all_features_payment_custom_1"),
|
|
65544: []byte("all_features_payment_custom_2"),
|
|
},
|
|
attemptCustomRecs: lnwire.CustomRecords{
|
|
65549: []byte("all_feat_attempt_custom_1"),
|
|
65550: []byte("all_feat_attempt_custom_2"),
|
|
},
|
|
hopConfigurator: func(hop *Hop, index int,
|
|
isFinal bool) {
|
|
|
|
// Add custom records and metadata to all hops.
|
|
hop.CustomRecords = record.CustomSet{
|
|
65545 + uint64(index): []byte(
|
|
fmt.Sprintf(
|
|
"all_feat_hop_%d",
|
|
index,
|
|
),
|
|
),
|
|
}
|
|
hop.Metadata = []byte(
|
|
fmt.Sprintf(
|
|
"all_feat_metadata_%d", index,
|
|
),
|
|
)
|
|
|
|
// Add MPP and blinded route data to final hop.
|
|
if isFinal {
|
|
var paymentAddr [32]byte
|
|
copy(
|
|
paymentAddr[:],
|
|
[]byte(
|
|
"all_features_mpp_"+
|
|
"addr_123456",
|
|
),
|
|
)
|
|
hop.MPP = record.NewMPP(
|
|
lnwire.MilliSatoshi(149250),
|
|
paymentAddr,
|
|
)
|
|
|
|
blindingKey, err := btcec.
|
|
NewPrivateKey()
|
|
|
|
require.NoError(t, err)
|
|
hop.BlindingPoint = blindingKey.PubKey()
|
|
hop.EncryptedData = []byte(
|
|
"all_features_encrypted_" +
|
|
"blinded_data_123456",
|
|
)
|
|
hop.TotalAmtMsat = lnwire.MilliSatoshi(
|
|
149250,
|
|
)
|
|
}
|
|
},
|
|
},
|
|
)
|
|
}
|
|
|
|
// paymentFeatureSet defines a combination of optional payment features for
|
|
// testing feature interactions.
|
|
type paymentFeatureSet struct {
|
|
name string
|
|
mpp bool
|
|
amp bool
|
|
customRecords bool
|
|
blindedRoute bool
|
|
hopMetadata bool
|
|
}
|
|
|
|
// TestMigrateLegacyPaymentWithNilHash tests migration of a legacy payment where
|
|
// the HTLC attempt's Hash field is nil. In older payments, the Hash wasn't
|
|
// stored on individual HTLCs; instead, the parent payment hash should be used.
|
|
func TestMigrateLegacyPaymentWithNilHash(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
kvDB := setupTestKVDB(t)
|
|
|
|
var paymentHash [32]byte
|
|
copy(paymentHash[:], []byte("test_legacy_nil_hash_payment"))
|
|
|
|
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
|
|
}
|
|
|
|
return createLegacyPaymentWithNilHash(
|
|
t, paymentsBucket, indexBucket, paymentHash,
|
|
)
|
|
}, func() {})
|
|
require.NoError(t, err)
|
|
|
|
sqlStore := setupTestSQLDB(t)
|
|
|
|
err = runPaymentsMigration(ctx, kvDB, sqlStore)
|
|
require.NoError(t, err)
|
|
|
|
// Verify the payment was migrated correctly.
|
|
var paymentID lntypes.Hash
|
|
copy(paymentID[:], paymentHash[:])
|
|
|
|
payment, err := sqlStore.FetchPayment(ctx, paymentID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, payment)
|
|
require.Len(t, payment.HTLCs, 1)
|
|
|
|
// The HTLC should have the parent payment hash since its own Hash was
|
|
// nil.
|
|
require.NotNil(t, payment.HTLCs[0].Hash)
|
|
require.Equal(t, paymentID, *payment.HTLCs[0].Hash)
|
|
}
|
|
|
|
// createLegacyPaymentWithNilHash creates a payment with an HTLC attempt that
|
|
// has a nil Hash field, simulating older payment data format.
|
|
func createLegacyPaymentWithNilHash(t *testing.T, paymentsBucket,
|
|
indexBucket kvdb.RwBucket, hash [32]byte) error {
|
|
|
|
t.Helper()
|
|
|
|
paymentBucket, err := paymentsBucket.CreateBucketIfNotExists(hash[:])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var paymentID lntypes.Hash
|
|
copy(paymentID[:], hash[:])
|
|
|
|
creationInfo := &PaymentCreationInfo{
|
|
PaymentIdentifier: paymentID,
|
|
Value: lnwire.MilliSatoshi(50000),
|
|
CreationTime: time.Now().Add(-24 * time.Hour),
|
|
PaymentRequest: []byte("lnbc500n1legacy_payment"),
|
|
}
|
|
|
|
var creationBuf bytes.Buffer
|
|
err = serializePaymentCreationInfo(&creationBuf, creationInfo)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = paymentBucket.Put(paymentCreationInfoKey, creationBuf.Bytes())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
seqBytes := make([]byte, 8)
|
|
byteOrder.PutUint64(seqBytes, 100)
|
|
err = paymentBucket.Put(paymentSequenceKey, seqBytes)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
htlcBucket, err := paymentBucket.CreateBucketIfNotExists(
|
|
paymentHtlcsBucket,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Create HTLC attempt with nil Hash (legacy format).
|
|
sessionKey, err := btcec.NewPrivateKey()
|
|
require.NoError(t, err)
|
|
|
|
var sessionKeyBytes [32]byte
|
|
copy(sessionKeyBytes[:], sessionKey.Serialize())
|
|
|
|
var sourcePubKey Vertex
|
|
copy(sourcePubKey[:], sessionKey.PubKey().SerializeCompressed())
|
|
|
|
hopKey, err := btcec.NewPrivateKey()
|
|
require.NoError(t, err)
|
|
|
|
attemptInfo := &HTLCAttemptInfo{
|
|
AttemptID: 1,
|
|
sessionKey: sessionKeyBytes,
|
|
Route: Route{
|
|
TotalTimeLock: 500000,
|
|
TotalAmount: 50000,
|
|
SourcePubKey: sourcePubKey,
|
|
Hops: []*Hop{
|
|
{
|
|
PubKeyBytes: NewVertex(hopKey.PubKey()),
|
|
ChannelID: 12345,
|
|
OutgoingTimeLock: 499500,
|
|
AmtToForward: 49000,
|
|
},
|
|
},
|
|
},
|
|
AttemptTime: time.Now().Add(-2 * time.Hour),
|
|
Hash: nil, // Legacy: Hash is nil
|
|
}
|
|
|
|
// Serialize the attempt info. Since Hash is nil, the serialization
|
|
// will not write the hash, simulating the legacy format.
|
|
attemptKey := make([]byte, len(htlcAttemptInfoKey)+8)
|
|
copy(attemptKey, htlcAttemptInfoKey)
|
|
byteOrder.PutUint64(attemptKey[len(htlcAttemptInfoKey):], 1)
|
|
|
|
var attemptBuf bytes.Buffer
|
|
err = serializeHTLCAttemptInfo(&attemptBuf, attemptInfo)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = htlcBucket.Put(attemptKey, attemptBuf.Bytes())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Add settlement info.
|
|
settleInfo := &HTLCSettleInfo{
|
|
Preimage: lntypes.Preimage(hash),
|
|
SettleTime: time.Now().Add(-1 * time.Hour),
|
|
}
|
|
|
|
settleKey := make([]byte, len(htlcSettleInfoKey)+8)
|
|
copy(settleKey, htlcSettleInfoKey)
|
|
byteOrder.PutUint64(settleKey[len(htlcSettleInfoKey):], 1)
|
|
|
|
var settleBuf bytes.Buffer
|
|
err = serializeHTLCSettleInfo(&settleBuf, settleInfo)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = htlcBucket.Put(settleKey, settleBuf.Bytes())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Create index entry.
|
|
var idx bytes.Buffer
|
|
err = WriteElements(&idx, paymentIndexTypeHash, hash[:])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return indexBucket.Put(seqBytes, idx.Bytes())
|
|
}
|
|
|
|
// createPaymentWithFeatureSet creates a payment with a selected set of
|
|
// optional features for combination testing.
|
|
func createPaymentWithFeatureSet(t *testing.T, paymentsBucket,
|
|
indexBucket kvdb.RwBucket, hash [32]byte, seqNum uint64,
|
|
features paymentFeatureSet, globalAttemptID *uint64) error {
|
|
t.Helper()
|
|
|
|
if features.mpp && features.amp {
|
|
return fmt.Errorf("invalid feature set: mpp and amp")
|
|
}
|
|
|
|
paymentBucket, err := paymentsBucket.CreateBucketIfNotExists(hash[:])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var paymentID lntypes.Hash
|
|
copy(paymentID[:], hash[:])
|
|
|
|
creationTime := time.Date(2024, 7, 1, 12, 0, 0, 0, time.UTC).
|
|
Add(time.Duration(seqNum) * time.Minute)
|
|
creationInfo := &PaymentCreationInfo{
|
|
PaymentIdentifier: paymentID,
|
|
Value: lnwire.MilliSatoshi(100000),
|
|
CreationTime: creationTime,
|
|
PaymentRequest: []byte(
|
|
fmt.Sprintf("lnbc_test_%s", features.name),
|
|
),
|
|
}
|
|
if features.customRecords {
|
|
creationInfo.FirstHopCustomRecords = lnwire.CustomRecords{
|
|
65560: []byte("combo_payment_custom_1"),
|
|
65561: []byte("combo_payment_custom_2"),
|
|
}
|
|
}
|
|
|
|
var b bytes.Buffer
|
|
err = serializePaymentCreationInfo(&b, creationInfo)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = paymentBucket.Put(paymentCreationInfoKey, b.Bytes())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
seqBytes := make([]byte, 8)
|
|
byteOrder.PutUint64(seqBytes, seqNum)
|
|
err = paymentBucket.Put(paymentSequenceKey, seqBytes)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
htlcBucket, err := paymentBucket.CreateBucketIfNotExists(
|
|
paymentHtlcsBucket,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
sessionKey, err := btcec.NewPrivateKey()
|
|
require.NoError(t, err)
|
|
|
|
var sourcePubKey Vertex
|
|
copy(sourcePubKey[:], sessionKey.PubKey().SerializeCompressed())
|
|
|
|
var sessionKeyBytes [32]byte
|
|
copy(sessionKeyBytes[:], sessionKey.Serialize())
|
|
|
|
baseAmt := lnwire.MilliSatoshi(100000)
|
|
hops := make([]*Hop, 3)
|
|
for i := 0; i < 3; i++ {
|
|
hopKey, err := btcec.NewPrivateKey()
|
|
require.NoError(t, err)
|
|
|
|
amt := baseAmt - lnwire.MilliSatoshi(uint64(i)*100)
|
|
hop := &Hop{
|
|
PubKeyBytes: NewVertex(hopKey.PubKey()),
|
|
ChannelID: uint64(700000 + i),
|
|
OutgoingTimeLock: uint32(700000 - i*40),
|
|
AmtToForward: amt,
|
|
}
|
|
if features.customRecords {
|
|
hop.CustomRecords = record.CustomSet{
|
|
65562 + uint64(i): []byte(fmt.Sprintf(
|
|
"combo_hop_%d", i,
|
|
)),
|
|
}
|
|
}
|
|
if features.hopMetadata {
|
|
hop.Metadata = []byte(
|
|
fmt.Sprintf("combo_metadata_%d", i),
|
|
)
|
|
}
|
|
|
|
if i == 2 {
|
|
if features.mpp {
|
|
var paymentAddr [32]byte
|
|
copy(
|
|
paymentAddr[:],
|
|
[]byte("combo_mpp_payment_addr_1234"),
|
|
)
|
|
hop.MPP = record.NewMPP(
|
|
baseAmt-200, paymentAddr,
|
|
)
|
|
}
|
|
if features.amp {
|
|
var rootShare [32]byte
|
|
copy(
|
|
rootShare[:],
|
|
[]byte("combo_amp_root_share_123456"),
|
|
)
|
|
var setID [32]byte
|
|
copy(
|
|
setID[:],
|
|
[]byte("combo_amp_set_id_12345678"),
|
|
)
|
|
hop.AMP = record.NewAMP(rootShare, setID, 0)
|
|
}
|
|
if features.blindedRoute {
|
|
blindingKey, err := btcec.NewPrivateKey()
|
|
require.NoError(t, err)
|
|
hop.BlindingPoint = blindingKey.PubKey()
|
|
hop.EncryptedData = []byte(
|
|
"combo_encrypted_blinded_data",
|
|
)
|
|
hop.TotalAmtMsat = baseAmt - 200
|
|
}
|
|
}
|
|
|
|
hops[i] = hop
|
|
}
|
|
|
|
routeInfo := Route{
|
|
TotalTimeLock: 700000,
|
|
TotalAmount: baseAmt - 200,
|
|
SourcePubKey: sourcePubKey,
|
|
Hops: hops,
|
|
}
|
|
if features.customRecords {
|
|
routeInfo.FirstHopWireCustomRecords = lnwire.CustomRecords{
|
|
65565: []byte("combo_attempt_custom_1"),
|
|
65566: []byte("combo_attempt_custom_2"),
|
|
}
|
|
}
|
|
|
|
*globalAttemptID++
|
|
attemptID := *globalAttemptID
|
|
attemptInfo := &HTLCAttemptInfo{
|
|
AttemptID: attemptID,
|
|
sessionKey: sessionKeyBytes,
|
|
Route: routeInfo,
|
|
AttemptTime: creationTime.Add(time.Minute),
|
|
Hash: (*lntypes.Hash)(&hash),
|
|
}
|
|
|
|
attemptKey := make([]byte, len(htlcAttemptInfoKey)+8)
|
|
copy(attemptKey, htlcAttemptInfoKey)
|
|
byteOrder.PutUint64(attemptKey[len(htlcAttemptInfoKey):], attemptID)
|
|
|
|
var ab bytes.Buffer
|
|
err = serializeHTLCAttemptInfo(&ab, attemptInfo)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = htlcBucket.Put(attemptKey, ab.Bytes())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
settleInfo := &HTLCSettleInfo{
|
|
Preimage: lntypes.Preimage(hash),
|
|
SettleTime: creationTime.Add(2 * time.Minute),
|
|
}
|
|
|
|
settleKey := make([]byte, len(htlcSettleInfoKey)+8)
|
|
copy(settleKey, htlcSettleInfoKey)
|
|
byteOrder.PutUint64(settleKey[len(htlcSettleInfoKey):], attemptID)
|
|
|
|
var sb bytes.Buffer
|
|
err = serializeHTLCSettleInfo(&sb, settleInfo)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = htlcBucket.Put(settleKey, sb.Bytes())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var idx bytes.Buffer
|
|
err = WriteElements(&idx, paymentIndexTypeHash, hash[:])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return indexBucket.Put(seqBytes, idx.Bytes())
|
|
}
|
|
|
|
// TestMigrateInFlightPayment tests that a payment with an active (in-flight)
|
|
// HTLC attempt — i.e. a node that upgrades while a payment is still pending on
|
|
// the network — is migrated correctly. The HTLC attempt must appear in SQL
|
|
// without a settlement or failure resolution, and FetchInFlightPayments must
|
|
// return the payment after migration.
|
|
func TestMigrateInFlightPayment(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
kvDB := setupTestKVDB(t)
|
|
|
|
var paymentHash [32]byte
|
|
copy(paymentHash[:], []byte("test_inflight_payment_hash_1234"))
|
|
|
|
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
|
|
}
|
|
|
|
return createInFlightPayment(
|
|
t, paymentsBucket, indexBucket, paymentHash,
|
|
)
|
|
}, func() {})
|
|
require.NoError(t, err)
|
|
|
|
sqlStore := setupTestSQLDB(t)
|
|
|
|
err = runPaymentsMigration(ctx, kvDB, sqlStore)
|
|
require.NoError(t, err)
|
|
|
|
// Verify the payment data matches between KV and SQL.
|
|
assertPaymentDataMatches(t, ctx, kvDB, sqlStore, paymentHash)
|
|
|
|
// Additionally verify that FetchInFlightPayments returns this payment,
|
|
// confirming it is treated as in-flight by the SQL store.
|
|
inFlight, err := sqlStore.FetchInFlightPayments(ctx)
|
|
require.NoError(t, err)
|
|
require.Len(t, inFlight, 1)
|
|
|
|
var paymentID lntypes.Hash
|
|
copy(paymentID[:], paymentHash[:])
|
|
require.Equal(t, paymentID, inFlight[0].Info.PaymentIdentifier)
|
|
require.Equal(t, StatusInFlight, inFlight[0].Status)
|
|
require.Len(t, inFlight[0].HTLCs, 1)
|
|
require.Nil(t, inFlight[0].HTLCs[0].Settle)
|
|
require.Nil(t, inFlight[0].HTLCs[0].Failure)
|
|
}
|
|
|
|
// createInFlightPayment creates a payment with an active (in-flight) HTLC
|
|
// attempt in the KV store. Unlike settled payments, no settle or fail
|
|
// resolution is written for the HTLC, simulating a payment that is still
|
|
// pending on the network at the time of migration.
|
|
func createInFlightPayment(t *testing.T, paymentsBucket,
|
|
indexBucket kvdb.RwBucket, hash [32]byte) error {
|
|
|
|
t.Helper()
|
|
|
|
paymentBucket, err := paymentsBucket.CreateBucketIfNotExists(hash[:])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var paymentID lntypes.Hash
|
|
copy(paymentID[:], hash[:])
|
|
|
|
creationInfo := &PaymentCreationInfo{
|
|
PaymentIdentifier: paymentID,
|
|
Value: lnwire.MilliSatoshi(100000),
|
|
CreationTime: time.Now().Add(-1 * time.Hour),
|
|
PaymentRequest: []byte("lnbc1inflight_payment"),
|
|
}
|
|
|
|
err = serializeAndPut(
|
|
paymentBucket, paymentCreationInfoKey,
|
|
func(w io.Writer) error {
|
|
return serializePaymentCreationInfo(w, creationInfo)
|
|
},
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
seqBytes := make([]byte, 8)
|
|
byteOrder.PutUint64(seqBytes, 42)
|
|
if err := paymentBucket.Put(paymentSequenceKey, seqBytes); err != nil {
|
|
return err
|
|
}
|
|
|
|
htlcBucket, err := paymentBucket.CreateBucketIfNotExists(
|
|
paymentHtlcsBucket,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, sourcePubKey, sessionKeyBytes, err := generateSessionKey(t)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
hopKey, err := btcec.NewPrivateKey()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
const attemptID = 1
|
|
attemptInfo := &HTLCAttemptInfo{
|
|
AttemptID: attemptID,
|
|
sessionKey: sessionKeyBytes,
|
|
Route: Route{
|
|
TotalTimeLock: 500000,
|
|
TotalAmount: 100000,
|
|
SourcePubKey: sourcePubKey,
|
|
Hops: []*Hop{
|
|
{
|
|
PubKeyBytes: NewVertex(
|
|
hopKey.PubKey(),
|
|
),
|
|
ChannelID: 12345,
|
|
OutgoingTimeLock: 499500,
|
|
AmtToForward: 99000,
|
|
},
|
|
},
|
|
},
|
|
AttemptTime: time.Now().Add(-30 * time.Minute),
|
|
Hash: (*lntypes.Hash)(&hash),
|
|
}
|
|
|
|
if err = writeHTLCAttempt(htlcBucket, attemptID, attemptInfo); err != nil {
|
|
return err
|
|
}
|
|
|
|
return createIndexEntry(indexBucket, seqBytes, hash)
|
|
}
|