2
0
mirror of https://github.com/lightninglabs/loop synced 2024-11-04 06:00:21 +00:00
loop/loopdb/store_test.go
carla 1877b7f08b
multi: store loop out htlc confirmations on disk
To allow users to specify differing confirmation targets, we store the
swap conf target per-swap. This makes us restart safe, so we do not
forget confirmation values for swaps that are in flight when we restart.
2020-08-04 20:28:06 +02:00

475 lines
12 KiB
Go

package loopdb
import (
"crypto/sha256"
"io/ioutil"
"os"
"path/filepath"
"reflect"
"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/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,
}
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,
SenderKey: senderKey,
ReceiverKey: receiverKey,
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()),
},
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 = "test label"
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")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tempDirName)
store, err := NewBoltSwapStore(tempDirName, &chaincfg.MainNetParams)
if err != nil {
t.Fatal(err)
}
// First, verify that an empty database has no active swaps.
swaps, err := store.FetchLoopOutSwaps()
if err != nil {
t.Fatal(err)
}
if len(swaps) != 0 {
t.Fatal("expected empty store")
}
// checkSwap is a test helper function that'll assert the state of a
// swap.
checkSwap := func(expectedState SwapState) {
t.Helper()
swaps, err := store.FetchLoopOutSwaps()
if err != nil {
t.Fatal(err)
}
if len(swaps) != 1 {
t.Fatal("expected pending swap in store")
}
swap := swaps[0].Contract
if !reflect.DeepEqual(swap, pendingSwap) {
t.Fatal("invalid pending swap data")
}
if swaps[0].State().State != expectedState {
t.Fatalf("expected state %v, but got %v",
expectedState, swaps[0].State(),
)
}
if expectedState == StatePreimageRevealed {
require.NotNil(t, swaps[0].State().HtlcTxHash)
}
}
hash := pendingSwap.Preimage.Hash()
// If we create a new swap, then it should show up as being initialized
// right after.
if err := store.CreateLoopOut(hash, pendingSwap); err != nil {
t.Fatal(err)
}
checkSwap(StateInitiated)
// Trying to make the same swap again should result in an error.
if err := store.CreateLoopOut(hash, pendingSwap); err == nil {
t.Fatal("expected error on storing duplicate")
}
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(
hash, testTime,
SwapStateData{
State: StatePreimageRevealed,
HtlcTxHash: &chainhash.Hash{1, 6, 2},
},
)
if err != nil {
t.Fatal(err)
}
checkSwap(StatePreimageRevealed)
// Next, we'll update to the final state to ensure that the state is
// properly updated.
err = store.UpdateLoopOut(
hash, testTime,
SwapStateData{
State: StateFailInsufficientValue,
},
)
if err != nil {
t.Fatal(err)
}
checkSwap(StateFailInsufficientValue)
if err := store.Close(); err != nil {
t.Fatal(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)
if err != nil {
t.Fatal(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,
SenderKey: senderKey,
ReceiverKey: receiverKey,
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()),
},
HtlcConfTarget: 2,
LastHop: &lastHop,
ExternalHtlc: true,
}
t.Run("loop in", func(t *testing.T) {
testLoopInStore(t, pendingSwap)
})
labelledSwap := pendingSwap
labelledSwap.Label = "test label"
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")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tempDirName)
store, err := NewBoltSwapStore(tempDirName, &chaincfg.MainNetParams)
if err != nil {
t.Fatal(err)
}
// First, verify that an empty database has no active swaps.
swaps, err := store.FetchLoopInSwaps()
if err != nil {
t.Fatal(err)
}
if len(swaps) != 0 {
t.Fatal("expected empty store")
}
// checkSwap is a test helper function that'll assert the state of a
// swap.
checkSwap := func(expectedState SwapState) {
t.Helper()
swaps, err := store.FetchLoopInSwaps()
if err != nil {
t.Fatal(err)
}
if len(swaps) != 1 {
t.Fatal("expected pending swap in store")
}
swap := swaps[0].Contract
if !reflect.DeepEqual(swap, &pendingSwap) {
t.Fatal("invalid pending swap data")
}
if swaps[0].State().State != expectedState {
t.Fatalf("expected state %v, but got %v",
expectedState, swaps[0].State(),
)
}
}
hash := sha256.Sum256(testPreimage[:])
// If we create a new swap, then it should show up as being initialized
// right after.
if err := store.CreateLoopIn(hash, &pendingSwap); err != nil {
t.Fatal(err)
}
checkSwap(StateInitiated)
// Trying to make the same swap again should result in an error.
if err := store.CreateLoopIn(hash, &pendingSwap); err == nil {
t.Fatal("expected error on storing duplicate")
}
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(
hash, testTime,
SwapStateData{
State: StatePreimageRevealed,
},
)
if err != nil {
t.Fatal(err)
}
checkSwap(StatePreimageRevealed)
// Next, we'll update to the final state to ensure that the state is
// properly updated.
err = store.UpdateLoopIn(
hash, testTime,
SwapStateData{
State: StateFailInsufficientValue,
},
)
if err != nil {
t.Fatal(err)
}
checkSwap(StateFailInsufficientValue)
if err := store.Close(); err != nil {
t.Fatal(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)
if err != nil {
t.Fatal(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")
)
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()
if err != nil {
t.Fatal(err)
}
// Assert that the outgoing channel is read properly.
expectedChannelSet := ChannelSet{5}
if !reflect.DeepEqual(swaps[0].Contract.OutgoingChanSet, expectedChannelSet) {
t.Fatal("invalid outgoing channel")
}
}