mirror of
https://github.com/lightninglabs/loop
synced 2024-11-19 15:25:33 +00:00
500 lines
13 KiB
Go
500 lines
13 KiB
Go
package loopdb
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"io/ioutil"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/btcsuite/btcd/chaincfg"
|
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
|
"github.com/coreos/bbolt"
|
|
"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"
|
|
)
|
|
|
|
var (
|
|
senderKey = [33]byte{
|
|
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
|
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2,
|
|
}
|
|
|
|
receiverKey = [33]byte{
|
|
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
|
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3,
|
|
}
|
|
|
|
senderInternalKey = [33]byte{
|
|
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
|
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 4,
|
|
}
|
|
|
|
receiverInternalKey = [33]byte{
|
|
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
|
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 5,
|
|
}
|
|
|
|
testPreimage = lntypes.Preimage([32]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,
|
|
})
|
|
|
|
testTime = time.Date(2018, time.January, 9, 14, 00, 00, 0, time.UTC)
|
|
)
|
|
|
|
// TestLoopOutStore tests all the basic functionality of the current bbolt
|
|
// swap store.
|
|
func TestLoopOutStore(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,
|
|
|
|
// Convert to/from unix to remove timezone, so that it
|
|
// doesn't interfere with DeepEqual.
|
|
InitiationTime: time.Unix(0, initiationTime.UnixNano()),
|
|
ProtocolVersion: ProtocolVersionMuSig2,
|
|
},
|
|
MaxPrepayRoutingFee: 40,
|
|
PrepayInvoice: "prepayinvoice",
|
|
DestAddr: destAddr,
|
|
SwapInvoice: "swapinvoice",
|
|
MaxSwapRoutingFee: 30,
|
|
SweepConfTarget: 2,
|
|
HtlcConfirmations: 2,
|
|
SwapPublicationDeadline: time.Unix(0, initiationTime.UnixNano()),
|
|
}
|
|
|
|
t.Run("no outgoing set", func(t *testing.T) {
|
|
testLoopOutStore(t, &unrestrictedSwap)
|
|
})
|
|
|
|
restrictedSwap := unrestrictedSwap
|
|
restrictedSwap.OutgoingChanSet = ChannelSet{1, 2}
|
|
|
|
t.Run("two channel outgoing set", func(t *testing.T) {
|
|
testLoopOutStore(t, &restrictedSwap)
|
|
})
|
|
|
|
labelledSwap := unrestrictedSwap
|
|
labelledSwap.Label = testLabel
|
|
t.Run("labelled swap", func(t *testing.T) {
|
|
testLoopOutStore(t, &labelledSwap)
|
|
})
|
|
}
|
|
|
|
// testLoopOutStore tests the basic functionality of the current bbolt
|
|
// swap store for specific swap parameters.
|
|
func testLoopOutStore(t *testing.T, pendingSwap *LoopOutContract) {
|
|
tempDirName, err := ioutil.TempDir("", "clientstore")
|
|
require.NoError(t, err)
|
|
|
|
defer os.RemoveAll(tempDirName)
|
|
|
|
store, err := NewBoltSwapStore(tempDirName, &chaincfg.MainNetParams)
|
|
require.NoError(t, err)
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
|
|
// If we re-open the same store, then the state of the current swap
|
|
// should be the same.
|
|
store, err = NewBoltSwapStore(tempDirName, &chaincfg.MainNetParams)
|
|
require.NoError(t, err)
|
|
|
|
checkSwap(StateFailInsufficientValue)
|
|
}
|
|
|
|
// TestLoopInStore tests all the basic functionality of the current bbolt
|
|
// swap store.
|
|
func TestLoopInStore(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: time.Unix(0, initiationTime.UnixNano()),
|
|
ProtocolVersion: ProtocolVersionMuSig2,
|
|
},
|
|
HtlcConfTarget: 2,
|
|
LastHop: &lastHop,
|
|
ExternalHtlc: true,
|
|
}
|
|
|
|
t.Run("loop in", func(t *testing.T) {
|
|
testLoopInStore(t, pendingSwap)
|
|
})
|
|
|
|
labelledSwap := pendingSwap
|
|
labelledSwap.Label = testLabel
|
|
t.Run("loop in with label", func(t *testing.T) {
|
|
testLoopInStore(t, labelledSwap)
|
|
})
|
|
}
|
|
|
|
func testLoopInStore(t *testing.T, pendingSwap LoopInContract) {
|
|
tempDirName, err := ioutil.TempDir("", "clientstore")
|
|
require.NoError(t, err)
|
|
defer os.RemoveAll(tempDirName)
|
|
|
|
store, err := NewBoltSwapStore(tempDirName, &chaincfg.MainNetParams)
|
|
require.NoError(t, err)
|
|
|
|
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)
|
|
|
|
// If we re-open the same store, then the state of the current swap
|
|
// should be the same.
|
|
store, err = NewBoltSwapStore(tempDirName, &chaincfg.MainNetParams)
|
|
require.NoError(t, err)
|
|
|
|
checkSwap(StateFailInsufficientValue)
|
|
}
|
|
|
|
// TestVersionNew tests that a new database is initialized with the current
|
|
// version.
|
|
func TestVersionNew(t *testing.T) {
|
|
tempDirName, err := ioutil.TempDir("", "clientstore")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer os.RemoveAll(tempDirName)
|
|
|
|
store, err := NewBoltSwapStore(tempDirName, &chaincfg.MainNetParams)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
ver, err := getDBVersion(store.db)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if ver != latestDBVersion {
|
|
t.Fatal("db not at latest version")
|
|
}
|
|
}
|
|
|
|
// TestVersionNew tests that an existing version zero database is migrated to
|
|
// the latest version.
|
|
func TestVersionMigrated(t *testing.T) {
|
|
tempDirName, err := ioutil.TempDir("", "clientstore")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer os.RemoveAll(tempDirName)
|
|
|
|
createVersionZeroDb(t, tempDirName)
|
|
|
|
store, err := NewBoltSwapStore(tempDirName, &chaincfg.MainNetParams)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
ver, err := getDBVersion(store.db)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if ver != latestDBVersion {
|
|
t.Fatal("db not at latest version")
|
|
}
|
|
}
|
|
|
|
// createVersionZeroDb creates a database with an empty meta bucket. In version
|
|
// zero, there was no version key specified yet.
|
|
func createVersionZeroDb(t *testing.T, dbPath string) {
|
|
path := filepath.Join(dbPath, dbFileName)
|
|
bdb, err := bbolt.Open(path, 0600, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer bdb.Close()
|
|
|
|
err = bdb.Update(func(tx *bbolt.Tx) error {
|
|
_, err := tx.CreateBucket(metaBucketKey)
|
|
return err
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
// TestLegacyOutgoingChannel asserts that a legacy channel restriction is
|
|
// properly mapped onto the newer channel set.
|
|
func TestLegacyOutgoingChannel(t *testing.T) {
|
|
var (
|
|
legacyDbVersion = Hex("00000003")
|
|
legacyOutgoingChannel = Hex("0000000000000005")
|
|
)
|
|
|
|
ctxb := context.Background()
|
|
|
|
legacyDb := map[string]interface{}{
|
|
"loop-in": map[string]interface{}{},
|
|
"metadata": map[string]interface{}{
|
|
"dbp": legacyDbVersion,
|
|
},
|
|
"uncharge-swaps": map[string]interface{}{
|
|
Hex("2a595d79a55168970532805ae20c9b5fac98f04db79ba4c6ae9b9ac0f206359e"): map[string]interface{}{
|
|
"contract": Hex("1562d6fbec140000010101010202020203030303040404040101010102020202030303030404040400000000000000640d707265706179696e766f69636501010101010101010101010101010101010101010101010101010101010101010201010101010101010101010101010101010101010101010101010101010101010300000090000000000000000a0000000000000014000000000000002800000063223347454e556d6e4552745766516374344e65676f6d557171745a757a5947507742530b73776170696e766f69636500000002000000000000001e") + legacyOutgoingChannel + Hex("1562d6fbec140000"),
|
|
"updates": map[string]interface{}{
|
|
Hex("0000000000000001"): Hex("1508290a92d4c00001000000000000000000000000000000000000000000000000"),
|
|
Hex("0000000000000002"): Hex("1508290a92d4c00006000000000000000000000000000000000000000000000000"),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
// Restore a legacy database.
|
|
tempDirName, err := ioutil.TempDir("", "clientstore")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer os.RemoveAll(tempDirName)
|
|
|
|
tempPath := filepath.Join(tempDirName, dbFileName)
|
|
db, err := bbolt.Open(tempPath, 0600, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
err = db.Update(func(tx *bbolt.Tx) error {
|
|
return RestoreDB(tx, legacyDb)
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
db.Close()
|
|
|
|
// Fetch the legacy swap.
|
|
store, err := NewBoltSwapStore(tempDirName, &chaincfg.MainNetParams)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
swaps, err := store.FetchLoopOutSwaps(ctxb)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Assert that the outgoing channel is read properly.
|
|
expectedChannelSet := ChannelSet{5}
|
|
|
|
require.Equal(t, expectedChannelSet, swaps[0].Contract.OutgoingChanSet)
|
|
}
|
|
|
|
// TestLiquidityParams checks that reading and writing to liquidty bucket are
|
|
// as expected.
|
|
func TestLiquidityParams(t *testing.T) {
|
|
tempDirName, err := ioutil.TempDir("", "clientstore")
|
|
require.NoError(t, err, "failed to db")
|
|
defer os.RemoveAll(tempDirName)
|
|
|
|
ctxb := context.Background()
|
|
|
|
store, err := NewBoltSwapStore(tempDirName, &chaincfg.MainNetParams)
|
|
require.NoError(t, err, "failed to create store")
|
|
|
|
// 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)
|
|
|
|
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")
|
|
}
|