mirror of
https://github.com/lightninglabs/loop
synced 2024-11-13 13:10:30 +00:00
1877b7f08b
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.
475 lines
12 KiB
Go
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")
|
|
}
|
|
}
|