loop/loopdb/sql_test.go
Boris Nagaev d554b42ef6
multi: migrate to btcd v2 modules
Update LND, Aperture, and Taproot Assets to revisions using the
btcd v2 modules, and update lndclient to v0.21.0-3. Migrate Loop
chain, transaction, and address types to their corresponding v2
packages.

The lndclient release includes the migration from:
https://github.com/lightninglabs/lndclient/pull/280

Taproot Assets is temporarily replaced with its btcd v2 revision
because the v0.8 release branch has not adopted the new modules.

This raises the minimum Go version to 1.26 and changes exported
address types.
2026-08-12 23:39:26 +00:00

620 lines
16 KiB
Go

package loopdb
import (
"context"
"crypto/sha256"
"errors"
"math/rand"
"reflect"
"testing"
"time"
"github.com/btcsuite/btcd/chainhash/v2"
"github.com/lightninglabs/loop/loopdb/sqlc"
"github.com/lightninglabs/loop/test"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/routing/route"
"github.com/stretchr/testify/require"
)
const (
testLabel = "test label"
)
var (
testAssetId = []byte{
1, 1, 1, 1, 2, 2, 2, 2,
3, 3, 3, 3, 4, 4, 4, 4,
1, 1, 1, 1, 2, 2, 2, 2,
3, 3, 3, 3, 4, 4, 4, 4,
}
)
// TestSqliteLoopOutStore tests all the basic functionality of the current
// sqlite swap store.
func TestSqliteLoopOutStore(t *testing.T) {
destAddr := test.GetDestAddr(t, 0)
initiationTime := time.Date(2018, 11, 1, 0, 0, 0, 0, time.UTC)
// Next, we'll make a new pending swap that we'll insert into the
// database shortly.
unrestrictedSwap := LoopOutContract{
SwapContract: SwapContract{
AmountRequested: 100,
Preimage: testPreimage,
CltvExpiry: 144,
HtlcKeys: HtlcKeys{
SenderScriptKey: senderKey,
ReceiverScriptKey: receiverKey,
SenderInternalPubKey: senderInternalKey,
ReceiverInternalPubKey: receiverInternalKey,
ClientScriptKeyLocator: keychain.KeyLocator{
Family: 1,
Index: 2,
},
},
MaxMinerFee: 10,
MaxSwapFee: 20,
InitiationHeight: 99,
InitiationTime: initiationTime,
ProtocolVersion: ProtocolVersionMuSig2,
},
MaxPrepayRoutingFee: 40,
PrepayInvoice: "prepayinvoice",
DestAddr: destAddr,
SwapInvoice: "swapinvoice",
MaxSwapRoutingFee: 30,
SweepConfTarget: 2,
HtlcConfirmations: 2,
SwapPublicationDeadline: initiationTime,
PaymentTimeout: time.Second * 11,
}
t.Run("no outgoing set", func(t *testing.T) {
testSqliteLoopOutStore(t, &unrestrictedSwap)
})
restrictedSwap := unrestrictedSwap
restrictedSwap.OutgoingChanSet = ChannelSet{1, 2}
t.Run("two channel outgoing set", func(t *testing.T) {
testSqliteLoopOutStore(t, &restrictedSwap)
})
labelledSwap := unrestrictedSwap
labelledSwap.Label = testLabel
t.Run("labelled swap", func(t *testing.T) {
testSqliteLoopOutStore(t, &labelledSwap)
})
assetSwap := unrestrictedSwap
assetSwap.AssetSwapInfo = &LoopOutAssetSwap{
AssetId: testAssetId,
PrepayRfqId: testAssetId,
SwapRfqId: testAssetId,
}
t.Run("asset swap", func(t *testing.T) {
testSqliteLoopOutStore(t, &assetSwap)
})
}
// testSqliteLoopOutStore tests the basic functionality of the current sqlite
// swap store for specific swap parameters.
func testSqliteLoopOutStore(t *testing.T, pendingSwap *LoopOutContract) {
store := NewTestDB(t)
ctxb := context.Background()
// First, verify that an empty database has no active swaps.
swaps, err := store.FetchLoopOutSwaps(ctxb)
require.NoError(t, err)
require.Empty(t, swaps)
hash := pendingSwap.Preimage.Hash()
// checkSwap is a test helper function that'll assert the state of a
// swap.
checkSwap := func(expectedState SwapState) {
t.Helper()
swaps, err := store.FetchLoopOutSwaps(ctxb)
require.NoError(t, err)
require.Len(t, swaps, 1)
swap, err := store.FetchLoopOutSwap(ctxb, hash)
require.NoError(t, err)
require.Equal(t, hash, swap.Hash)
require.Equal(t, hash, swaps[0].Hash)
swapContract := swap.Contract
require.Equal(t, swapContract, pendingSwap)
require.Equal(t, expectedState, swap.State().State)
if expectedState == StatePreimageRevealed {
require.NotNil(t, swap.State().HtlcTxHash)
}
require.Equal(t, time.Second*11, swap.Contract.PaymentTimeout)
}
// If we create a new swap, then it should show up as being initialized
// right after.
err = store.CreateLoopOut(ctxb, hash, pendingSwap)
require.NoError(t, err)
checkSwap(StateInitiated)
// Trying to make the same swap again should result in an error.
err = store.CreateLoopOut(ctxb, hash, pendingSwap)
require.Error(t, err)
checkSwap(StateInitiated)
// Next, we'll update to the next state of the pre-image being
// revealed. The state should be reflected here again.
err = store.UpdateLoopOut(
ctxb, hash, testTime,
SwapStateData{
State: StatePreimageRevealed,
HtlcTxHash: &chainhash.Hash{1, 6, 2},
},
)
require.NoError(t, err)
checkSwap(StatePreimageRevealed)
// Next, we'll update to the final state to ensure that the state is
// properly updated.
err = store.UpdateLoopOut(
ctxb, hash, testTime,
SwapStateData{
State: StateFailInsufficientValue,
},
)
require.NoError(t, err)
checkSwap(StateFailInsufficientValue)
err = store.Close()
require.NoError(t, err)
}
// TestSQLliteLoopInStore tests all the basic functionality of the current
// sqlite swap store.
func TestSQLliteLoopInStore(t *testing.T) {
initiationTime := time.Date(2018, 11, 1, 0, 0, 0, 0, time.UTC)
// Next, we'll make a new pending swap that we'll insert into the
// database shortly.
lastHop := route.Vertex{1, 2, 3}
pendingSwap := LoopInContract{
SwapContract: SwapContract{
AmountRequested: 100,
Preimage: testPreimage,
CltvExpiry: 144,
HtlcKeys: HtlcKeys{
SenderScriptKey: senderKey,
ReceiverScriptKey: receiverKey,
SenderInternalPubKey: senderInternalKey,
ReceiverInternalPubKey: receiverInternalKey,
ClientScriptKeyLocator: keychain.KeyLocator{
Family: 1,
Index: 2,
},
},
MaxMinerFee: 10,
MaxSwapFee: 20,
InitiationHeight: 99,
// Convert to/from unix to remove timezone, so that it
// doesn't interfere with DeepEqual.
InitiationTime: initiationTime,
ProtocolVersion: ProtocolVersionMuSig2,
},
HtlcConfTarget: 2,
LastHop: &lastHop,
ExternalHtlc: true,
}
t.Run("loop in", func(t *testing.T) {
testSqliteLoopInStore(t, pendingSwap)
})
labelledSwap := pendingSwap
labelledSwap.Label = testLabel
t.Run("loop in with label", func(t *testing.T) {
testSqliteLoopInStore(t, labelledSwap)
})
}
func testSqliteLoopInStore(t *testing.T, pendingSwap LoopInContract) {
store := NewTestDB(t)
ctxb := context.Background()
// First, verify that an empty database has no active swaps.
swaps, err := store.FetchLoopInSwaps(ctxb)
require.NoError(t, err)
require.Empty(t, swaps)
hash := sha256.Sum256(testPreimage[:])
// checkSwap is a test helper function that'll assert the state of a
// swap.
checkSwap := func(expectedState SwapState) {
t.Helper()
swaps, err := store.FetchLoopInSwaps(ctxb)
require.NoError(t, err)
require.Len(t, swaps, 1)
swap := swaps[0].Contract
require.Equal(t, swap, &pendingSwap)
require.Equal(t, swaps[0].State().State, expectedState)
}
// If we create a new swap, then it should show up as being initialized
// right after.
err = store.CreateLoopIn(ctxb, hash, &pendingSwap)
require.NoError(t, err)
checkSwap(StateInitiated)
// Trying to make the same swap again should result in an error.
err = store.CreateLoopIn(ctxb, hash, &pendingSwap)
require.Error(t, err)
checkSwap(StateInitiated)
// Next, we'll update to the next state of the pre-image being
// revealed. The state should be reflected here again.
err = store.UpdateLoopIn(
ctxb, hash, testTime,
SwapStateData{
State: StatePreimageRevealed,
},
)
require.NoError(t, err)
checkSwap(StatePreimageRevealed)
// Next, we'll update to the final state to ensure that the state is
// properly updated.
err = store.UpdateLoopIn(
ctxb, hash, testTime,
SwapStateData{
State: StateFailInsufficientValue,
},
)
require.NoError(t, err)
checkSwap(StateFailInsufficientValue)
err = store.Close()
require.NoError(t, err)
}
// TestSqliteLiquidityParams checks that reading and writing to liquidty bucket are
// as expected.
func TestSqliteLiquidityParams(t *testing.T) {
ctxb := context.Background()
store := NewTestDB(t)
// Test when there's no params saved before, an empty bytes is
// returned.
params, err := store.FetchLiquidityParams(ctxb)
require.NoError(t, err, "failed to fetch params")
require.Empty(t, params, "expect empty bytes")
require.Nil(t, params, "expected nil byte array")
params = []byte("test")
// Test we can save the params.
err = store.PutLiquidityParams(ctxb, params)
require.NoError(t, err, "failed to put params")
// Now fetch the db again should return the above saved bytes.
paramsRead, err := store.FetchLiquidityParams(ctxb)
require.NoError(t, err, "failed to fetch params")
require.Equal(t, params, paramsRead, "unexpected return value")
}
// TestSqliteTypeConversion is a small test that checks that we can safely
// convert between the :one and :many types from sqlc.
func TestSqliteTypeConversion(t *testing.T) {
loopOutSwapRow := sqlc.GetLoopOutSwapRow{}
err := randomStruct(&loopOutSwapRow)
require.NoError(t, err)
require.NotNil(t, loopOutSwapRow.DestAddress)
loopOutSwapsRow := sqlc.GetLoopOutSwapsRow(loopOutSwapRow)
require.EqualValues(t, loopOutSwapRow, loopOutSwapsRow)
}
// TestIssue615 tests that on faulty timestamps, the database will be fixed.
// Reference: https://github.com/lightninglabs/lightning-terminal/issues/615
func TestIssue615(t *testing.T) {
ctxb := context.Background()
// Create an invoice to get the timestamp from.
invoice := "lnbc5u1pje2dyusp5qs356crpns9u3we8hw7w9gntfz89zkcaxu6w6h6a" +
"pw6jlgc0cynqpp5y2xdzu4eqasuttxp3nrk72vqdzce3wead7nmf693uqpgx" +
"2hd533qdpcyfnx2etyyp3ks6trddjkuueqw3hkketwwv7kgvrd0py95d6vvv" +
"65z0fzxqzfvcqpjrzjqd82srutzjx82prr234anxdlwvs6peklcc92lp9aqs" +
"q296xnwmqd2rrf9gqqtwqqqqqqqqqqqqqqqqqq9q9qxpqysgq768236z7cx6" +
"gyy766wajrmpnpt6wavkf5nypwyj6r3dcxm89aggq2jm2kznaxvr0lrsqgv7" +
"592upfh5ruyrwzy5tethpzere78xfgwqp64jrpa"
// Create a new sqlite store for testing.
sqlDB := NewTestDB(t)
// Create a faulty loopout swap.
destAddr := test.GetDestAddr(t, 0)
// Corresponds to 55563-06-27 02:09:24 +0000 UTC.
faultyTime := time.Unix(1691247002964, 0)
t.Log(faultyTime.Unix())
unrestrictedSwap := LoopOutContract{
SwapContract: SwapContract{
AmountRequested: 100,
Preimage: testPreimage,
CltvExpiry: 144,
HtlcKeys: HtlcKeys{
SenderScriptKey: senderKey,
ReceiverScriptKey: receiverKey,
SenderInternalPubKey: senderInternalKey,
ReceiverInternalPubKey: receiverInternalKey,
ClientScriptKeyLocator: keychain.KeyLocator{
Family: 1,
Index: 2,
},
},
MaxMinerFee: 10,
MaxSwapFee: 20,
InitiationHeight: 99,
InitiationTime: time.Now(),
ProtocolVersion: ProtocolVersionMuSig2,
},
MaxPrepayRoutingFee: 40,
PrepayInvoice: "prepayinvoice",
DestAddr: destAddr,
SwapInvoice: invoice,
MaxSwapRoutingFee: 30,
SweepConfTarget: 2,
HtlcConfirmations: 2,
SwapPublicationDeadline: faultyTime,
}
err := sqlDB.CreateLoopOut(ctxb, testPreimage.Hash(), &unrestrictedSwap)
require.NoError(t, err)
// This should fail because of the faulty timestamp.
_, err = sqlDB.GetLoopOutSwaps(ctxb)
// If we're using sqlite, we expect an error.
if testDBType == "sqlite" {
require.Error(t, err)
} else {
require.NoError(t, err)
}
// Fix the faulty timestamp.
err = sqlDB.FixFaultyTimestamps(ctxb)
require.NoError(t, err)
_, err = sqlDB.GetLoopOutSwaps(ctxb)
require.NoError(t, err)
}
// TestBatchUpdateCost tests that we can batch update the cost of multiple swaps
// at once.
func TestBatchUpdateCost(t *testing.T) {
// Create a new sqlite store for testing.
store := NewTestDB(t)
destAddr := test.GetDestAddr(t, 0)
initiationTime := time.Date(2018, 11, 1, 0, 0, 0, 0, time.UTC)
testContract := LoopOutContract{
SwapContract: SwapContract{
AmountRequested: 100,
CltvExpiry: 144,
HtlcKeys: HtlcKeys{
SenderScriptKey: senderKey,
ReceiverScriptKey: receiverKey,
SenderInternalPubKey: senderInternalKey,
ReceiverInternalPubKey: receiverInternalKey,
ClientScriptKeyLocator: keychain.KeyLocator{
Family: 1,
Index: 2,
},
},
MaxMinerFee: 10,
MaxSwapFee: 20,
InitiationHeight: 99,
InitiationTime: initiationTime,
ProtocolVersion: ProtocolVersionMuSig2,
},
MaxPrepayRoutingFee: 40,
PrepayInvoice: "prepayinvoice",
DestAddr: destAddr,
SwapInvoice: "swapinvoice",
MaxSwapRoutingFee: 30,
SweepConfTarget: 2,
HtlcConfirmations: 2,
SwapPublicationDeadline: initiationTime,
PaymentTimeout: time.Second * 11,
}
makeSwap := func(preimage lntypes.Preimage) *LoopOutContract {
contract := testContract
contract.Preimage = preimage
return &contract
}
// Next, we'll add two swaps to the database.
preimage1 := testPreimage
preimage2 := lntypes.Preimage{4, 4, 4}
ctxb := context.Background()
swap1 := makeSwap(preimage1)
swap2 := makeSwap(preimage2)
hash1 := swap1.Preimage.Hash()
err := store.CreateLoopOut(ctxb, hash1, swap1)
require.NoError(t, err)
hash2 := swap2.Preimage.Hash()
err = store.CreateLoopOut(ctxb, hash2, swap2)
require.NoError(t, err)
// Add an update to both swaps containing the cost.
err = store.UpdateLoopOut(
ctxb, hash1, testTime,
SwapStateData{
State: StateSuccess,
Cost: SwapCost{
Server: 1,
Onchain: 2,
Offchain: 3,
},
},
)
require.NoError(t, err)
err = store.UpdateLoopOut(
ctxb, hash2, testTime,
SwapStateData{
State: StateSuccess,
Cost: SwapCost{
Server: 4,
Onchain: 5,
Offchain: 6,
},
},
)
require.NoError(t, err)
updateMap := map[lntypes.Hash]SwapCost{
hash1: {
Server: 2,
Onchain: 3,
Offchain: 4,
},
hash2: {
Server: 6,
Onchain: 7,
Offchain: 8,
},
}
require.NoError(t, store.BatchUpdateLoopOutSwapCosts(ctxb, updateMap))
swaps, err := store.FetchLoopOutSwaps(ctxb)
require.NoError(t, err)
require.Len(t, swaps, 2)
swapsMap := make(map[lntypes.Hash]*LoopOut)
swapsMap[swaps[0].Hash] = swaps[0]
swapsMap[swaps[1].Hash] = swaps[1]
require.Equal(t, updateMap[hash1], swapsMap[hash1].State().Cost)
require.Equal(t, updateMap[hash2], swapsMap[hash2].State().Cost)
}
// TestMigrationTracker tests the migration tracker functionality.
func TestMigrationTracker(t *testing.T) {
ctxb := context.Background()
// Create a new sqlite store for testing.
sqlDB := NewTestDB(t)
hasMigration, err := sqlDB.HasMigration(ctxb, "test")
require.NoError(t, err)
require.False(t, hasMigration)
require.NoError(t, sqlDB.SetMigration(ctxb, "test"))
hasMigration, err = sqlDB.HasMigration(ctxb, "test")
require.NoError(t, err)
require.True(t, hasMigration)
}
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
func randomString(length int) string {
b := make([]byte, length)
for i := range b {
b[i] = charset[rand.Intn(len(charset))]
}
return string(b)
}
func randomBytes(length int) []byte {
b := make([]byte, length)
for i := range b {
b[i] = byte(rand.Intn(256))
}
return b
}
func randomStruct(v any) error {
val := reflect.ValueOf(v)
if val.Kind() != reflect.Ptr || val.Elem().Kind() != reflect.Struct {
return errors.New("Input should be a pointer to a struct type")
}
val = val.Elem()
for _, field := range val.Fields() {
switch field.Kind() {
case reflect.Int64:
if field.CanSet() {
field.SetInt(rand.Int63())
}
case reflect.String:
if field.CanSet() {
field.SetString(randomString(10))
}
case reflect.Slice:
if field.Type().Elem().Kind() == reflect.Uint8 {
if field.CanSet() {
field.SetBytes(randomBytes(32))
}
}
case reflect.Struct:
if field.Type() == reflect.TypeFor[time.Time]() {
if field.CanSet() {
field.Set(reflect.ValueOf(time.Now()))
}
}
if field.Type() == reflect.TypeFor[route.Vertex]() {
if field.CanSet() {
vertex, err := route.NewVertexFromBytes(
randomBytes(route.VertexSize),
)
if err != nil {
return err
}
field.Set(reflect.ValueOf(vertex))
}
}
}
}
return nil
}