2
0
mirror of https://github.com/lightninglabs/loop synced 2024-11-17 21:25:56 +00:00
loop/loopdb/sql_test.go
2024-06-03 14:54:18 +02:00

603 lines
15 KiB
Go

package loopdb
import (
"context"
"crypto/sha256"
"errors"
"math/rand"
"reflect"
"testing"
"time"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"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"
)
// 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)
})
}
// 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)
}
// TestLiquidityParams 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 interface{}) 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 i := 0; i < val.NumField(); i++ {
field := val.Field(i)
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.TypeOf(time.Time{}) {
if field.CanSet() {
field.Set(reflect.ValueOf(time.Now()))
}
}
if field.Type() == reflect.TypeOf(route.Vertex{}) {
if field.CanSet() {
vertex, err := route.NewVertexFromBytes(
randomBytes(route.VertexSize),
)
if err != nil {
return err
}
field.Set(reflect.ValueOf(vertex))
}
}
}
}
return nil
}