mirror of
https://github.com/lightninglabs/loop.git
synced 2026-08-13 12:33:03 +02:00
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.
508 lines
14 KiB
Go
508 lines
14 KiB
Go
package loopdb
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/btcsuite/btcd/chaincfg/v2"
|
|
"github.com/btcsuite/btcd/chainhash/v2"
|
|
"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"
|
|
"go.etcd.io/bbolt"
|
|
bbolterrors "go.etcd.io/bbolt/errors"
|
|
)
|
|
|
|
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)
|
|
)
|
|
|
|
// TestNewBoltSwapStoreTimeout ensures a wrapped bbolt timeout is detected
|
|
// correctly when opening the store.
|
|
func TestNewBoltSwapStoreTimeout(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
|
|
// Override the bbolt open function to return a wrapped timeout.
|
|
origOpen := bboltOpen
|
|
t.Cleanup(func() {
|
|
bboltOpen = origOpen
|
|
})
|
|
|
|
wrappedErr := fmt.Errorf("wrapped: %w", bbolterrors.ErrTimeout)
|
|
bboltOpen = func(path string, mode os.FileMode,
|
|
options *bbolt.Options) (*bbolt.DB, error) {
|
|
|
|
require.NotNil(t, options)
|
|
require.Equal(t, filepath.Join(tempDir, dbFileName), path)
|
|
|
|
return nil, wrappedErr
|
|
}
|
|
|
|
store, err := NewBoltSwapStore(tempDir, &chaincfg.MainNetParams)
|
|
require.Nil(t, store)
|
|
require.ErrorIs(t, err, bbolterrors.ErrTimeout)
|
|
require.ErrorContains(t, err, "couldn't obtain exclusive lock")
|
|
}
|
|
|
|
// 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 := t.TempDir()
|
|
|
|
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 := t.TempDir()
|
|
|
|
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 := t.TempDir()
|
|
|
|
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")
|
|
}
|
|
}
|
|
|
|
// TestVersionMigrated tests that an existing version zero database is migrated
|
|
// to the latest version.
|
|
func TestVersionMigrated(t *testing.T) {
|
|
tempDirName := t.TempDir()
|
|
|
|
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]any{
|
|
"loop-in": map[string]any{},
|
|
"metadata": map[string]any{
|
|
"dbp": legacyDbVersion,
|
|
},
|
|
"uncharge-swaps": map[string]any{
|
|
Hex("2a595d79a55168970532805ae20c9b5fac98f04db79ba4c6ae9b9ac0f206359e"): map[string]any{
|
|
"contract": Hex("1562d6fbec140000010101010202020203030303040404040101010102020202030303030404040400000000000000640d707265706179696e766f69636501010101010101010101010101010101010101010101010101010101010101010201010101010101010101010101010101010101010101010101010101010101010300000090000000000000000a0000000000000014000000000000002800000063223347454e556d6e4552745766516374344e65676f6d557171745a757a5947507742530b73776170696e766f69636500000002000000000000001e") + legacyOutgoingChannel + Hex("1562d6fbec140000"),
|
|
"updates": map[string]any{
|
|
Hex("0000000000000001"): Hex("1508290a92d4c00001000000000000000000000000000000000000000000000000"),
|
|
Hex("0000000000000002"): Hex("1508290a92d4c00006000000000000000000000000000000000000000000000000"),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
// Restore a legacy database.
|
|
tempDirName := t.TempDir()
|
|
|
|
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 := t.TempDir()
|
|
|
|
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")
|
|
}
|