mirror of
https://github.com/lightninglabs/loop.git
synced 2026-08-13 12:33:03 +02:00
2241 lines
60 KiB
Go
2241 lines
60 KiB
Go
package loopd
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
|
"github.com/btcsuite/btcd/btcutil"
|
|
"github.com/btcsuite/btcd/chaincfg"
|
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
|
"github.com/btcsuite/btcd/wire"
|
|
"github.com/btcsuite/btclog/v2"
|
|
"github.com/lightninglabs/lndclient"
|
|
"github.com/lightninglabs/loop"
|
|
"github.com/lightninglabs/loop/fsm"
|
|
"github.com/lightninglabs/loop/labels"
|
|
"github.com/lightninglabs/loop/liquidity"
|
|
"github.com/lightninglabs/loop/loopdb"
|
|
"github.com/lightninglabs/loop/looprpc"
|
|
"github.com/lightninglabs/loop/staticaddr/address"
|
|
"github.com/lightninglabs/loop/staticaddr/deposit"
|
|
"github.com/lightninglabs/loop/staticaddr/loopin"
|
|
"github.com/lightninglabs/loop/staticaddr/script"
|
|
"github.com/lightninglabs/loop/swap"
|
|
mock_lnd "github.com/lightninglabs/loop/test"
|
|
"github.com/lightningnetwork/lnd/input"
|
|
"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
|
|
"github.com/lightningnetwork/lnd/lntypes"
|
|
"github.com/lightningnetwork/lnd/lnwallet"
|
|
"github.com/lightningnetwork/lnd/lnwire"
|
|
"github.com/lightningnetwork/lnd/routing/route"
|
|
"github.com/stretchr/testify/require"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/metadata"
|
|
"google.golang.org/grpc/status"
|
|
)
|
|
|
|
var (
|
|
testnetAddr, _ = btcutil.NewAddressScriptHash(
|
|
[]byte{123}, &chaincfg.TestNet3Params,
|
|
)
|
|
|
|
mainnetAddr, _ = btcutil.NewAddressScriptHash(
|
|
[]byte{123}, &chaincfg.MainNetParams,
|
|
)
|
|
|
|
nodepubkeyAddr, _ = btcutil.DecodeAddress(
|
|
mock_lnd.NewMockLnd().NodePubkey, &chaincfg.MainNetParams,
|
|
)
|
|
|
|
chanID1 = lnwire.NewShortChanIDFromInt(1)
|
|
chanID2 = lnwire.NewShortChanIDFromInt(2)
|
|
chanID3 = lnwire.NewShortChanIDFromInt(3)
|
|
chanID4 = lnwire.NewShortChanIDFromInt(4)
|
|
|
|
peer1 = route.Vertex{1}
|
|
peer2 = route.Vertex{2}
|
|
|
|
channel1 = lndclient.ChannelInfo{
|
|
Active: false,
|
|
ChannelID: chanID1.ToUint64(),
|
|
PubKeyBytes: peer1,
|
|
LocalBalance: 10000,
|
|
RemoteBalance: 0,
|
|
Capacity: 10000,
|
|
}
|
|
|
|
channel2 = lndclient.ChannelInfo{
|
|
Active: true,
|
|
ChannelID: chanID2.ToUint64(),
|
|
PubKeyBytes: peer2,
|
|
LocalBalance: 10000,
|
|
RemoteBalance: 0,
|
|
Capacity: 10000,
|
|
}
|
|
|
|
channel3 = lndclient.ChannelInfo{
|
|
Active: true,
|
|
ChannelID: chanID3.ToUint64(),
|
|
PubKeyBytes: peer2,
|
|
LocalBalance: 10000,
|
|
RemoteBalance: 0,
|
|
Capacity: 10000,
|
|
}
|
|
|
|
channel4 = lndclient.ChannelInfo{
|
|
Active: true,
|
|
ChannelID: chanID4.ToUint64(),
|
|
PubKeyBytes: peer2,
|
|
LocalBalance: 1000,
|
|
RemoteBalance: 0,
|
|
Capacity: 1000,
|
|
}
|
|
)
|
|
|
|
// TestValidateConfTarget tests all failure and success cases for our conf
|
|
// target validation function, including the case where we replace a zero
|
|
// target with the default provided.
|
|
func TestValidateConfTarget(t *testing.T) {
|
|
const (
|
|
// Various input confirmation values for tests.
|
|
zeroConf int32 = 0
|
|
oneConf int32 = 1
|
|
twoConf int32 = 2
|
|
fiveConf int32 = 5
|
|
|
|
// defaultConf is the default confirmation target we use for
|
|
// all tests.
|
|
defaultConf = 6
|
|
)
|
|
|
|
tests := []struct {
|
|
name string
|
|
confTarget int32
|
|
expectedTarget int32
|
|
expectErr bool
|
|
}{
|
|
{
|
|
name: "zero conf, get default",
|
|
confTarget: zeroConf,
|
|
expectedTarget: defaultConf,
|
|
expectErr: false,
|
|
},
|
|
{
|
|
name: "one conf, get error",
|
|
confTarget: oneConf,
|
|
expectErr: true,
|
|
},
|
|
{
|
|
name: "two conf, ok",
|
|
confTarget: twoConf,
|
|
expectedTarget: twoConf,
|
|
expectErr: false,
|
|
},
|
|
{
|
|
name: "five conf, ok",
|
|
confTarget: fiveConf,
|
|
expectedTarget: fiveConf,
|
|
expectErr: false,
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
target, err := validateConfTarget(
|
|
test.confTarget, defaultConf,
|
|
)
|
|
|
|
if test.expectErr {
|
|
require.Error(t, err)
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
require.Equal(t, test.expectedTarget, target)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestValidateLoopInRequest tests validation of loop in requests.
|
|
func TestValidateLoopInRequest(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
amount int64
|
|
numDeposits uint32
|
|
external bool
|
|
confTarget int32
|
|
autoSelectDeposits bool
|
|
expectErr bool
|
|
expectedTarget int32
|
|
}{
|
|
{
|
|
name: "external and htlc conf set",
|
|
amount: 100_000,
|
|
external: true,
|
|
confTarget: 1,
|
|
expectErr: true,
|
|
expectedTarget: 0,
|
|
},
|
|
{
|
|
name: "external and no conf",
|
|
amount: 100_000,
|
|
external: true,
|
|
confTarget: 0,
|
|
expectErr: false,
|
|
expectedTarget: 0,
|
|
},
|
|
{
|
|
name: "not external, zero conf",
|
|
amount: 100_000,
|
|
external: false,
|
|
confTarget: 0,
|
|
expectErr: false,
|
|
expectedTarget: loop.DefaultHtlcConfTarget,
|
|
},
|
|
{
|
|
name: "not external, bad conf",
|
|
amount: 100_000,
|
|
external: false,
|
|
confTarget: 1,
|
|
expectErr: true,
|
|
expectedTarget: 0,
|
|
},
|
|
{
|
|
name: "not external, ok conf",
|
|
amount: 100_000,
|
|
external: false,
|
|
confTarget: 5,
|
|
expectErr: false,
|
|
expectedTarget: 5,
|
|
},
|
|
{
|
|
name: "not external, amount no deposit",
|
|
amount: 100_000,
|
|
numDeposits: 0,
|
|
external: false,
|
|
expectErr: false,
|
|
expectedTarget: loop.DefaultHtlcConfTarget,
|
|
},
|
|
{
|
|
name: "not external, deposit no amount",
|
|
amount: 100_000,
|
|
numDeposits: 1,
|
|
external: false,
|
|
expectErr: false,
|
|
},
|
|
|
|
{
|
|
name: "not external, deposit fractional amount",
|
|
amount: 100_000,
|
|
numDeposits: 1,
|
|
external: false,
|
|
expectErr: false,
|
|
},
|
|
{
|
|
name: "amount with deposit coin select",
|
|
amount: 100_000,
|
|
autoSelectDeposits: true,
|
|
external: false,
|
|
expectErr: false,
|
|
},
|
|
{
|
|
name: "amount with deposit coin select",
|
|
numDeposits: 1,
|
|
autoSelectDeposits: true,
|
|
external: false,
|
|
expectErr: true,
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
external := test.external
|
|
conf, err := validateLoopInRequest(
|
|
test.confTarget, external, test.numDeposits,
|
|
btcutil.Amount(test.amount),
|
|
test.autoSelectDeposits,
|
|
)
|
|
|
|
if test.expectErr {
|
|
require.Error(t, err)
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
require.Equal(t, test.expectedTarget, conf)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestStaticAddressLoopInRejectsReservedLabel verifies that external static
|
|
// loop-in requests still reject reserved autoloop labels at the RPC boundary.
|
|
func TestStaticAddressLoopInRejectsReservedLabel(t *testing.T) {
|
|
logger := btclog.NewSLogger(
|
|
btclog.NewDefaultHandler(os.Stdout),
|
|
)
|
|
setLogger(logger.SubSystem(Subsystem))
|
|
|
|
server := &swapClientServer{}
|
|
|
|
_, err := server.StaticAddressLoopIn(
|
|
t.Context(), &looprpc.StaticAddressLoopInRequest{
|
|
Label: labels.AutoloopLabel(swap.TypeIn),
|
|
},
|
|
)
|
|
require.ErrorContains(t, err, labels.ErrReservedPrefix.Error())
|
|
}
|
|
|
|
// TestSetLiquidityParamsRejectsStaticAutoloopWithoutExperimental verifies that
|
|
// users must restart loopd with --experimental before enabling static-address
|
|
// autoloop.
|
|
func TestSetLiquidityParamsRejectsStaticAutoloopWithoutExperimental(
|
|
t *testing.T) {
|
|
|
|
server := &swapClientServer{
|
|
config: &Config{},
|
|
}
|
|
|
|
_, err := server.SetLiquidityParams(
|
|
t.Context(), &looprpc.SetLiquidityParamsRequest{
|
|
Parameters: &looprpc.LiquidityParameters{
|
|
LoopInSource: looprpc.
|
|
LoopInSource_LOOP_IN_SOURCE_STATIC_ADDRESS,
|
|
},
|
|
},
|
|
)
|
|
require.Error(t, err)
|
|
require.Equal(t, codes.FailedPrecondition, status.Code(err))
|
|
require.ErrorContains(t, err, "--experimental")
|
|
}
|
|
|
|
// TestStaticAddressLoopInTimestamp verifies that zero timestamps are omitted
|
|
// from static loop-in responses instead of passing a zero time to UnixNano.
|
|
func TestStaticAddressLoopInTimestamp(t *testing.T) {
|
|
require.Zero(t, staticAddressLoopInTimestamp(time.Time{}))
|
|
|
|
timestamp := time.Unix(1_234, 567).UTC()
|
|
require.Equal(
|
|
t, timestamp.UnixNano(),
|
|
staticAddressLoopInTimestamp(timestamp),
|
|
)
|
|
}
|
|
|
|
// TestStaticAddressLoopInSwapServerCost verifies that static loop-in server
|
|
// costs are only reported once the invoice payment was received. Timeout path
|
|
// costs are not persisted today, so they are intentionally not estimated here.
|
|
func TestStaticAddressLoopInSwapServerCost(t *testing.T) {
|
|
const quoteFee = btcutil.Amount(1_234)
|
|
|
|
tests := []struct {
|
|
name string
|
|
state fsm.StateType
|
|
wantServer int64
|
|
}{
|
|
{
|
|
name: "pending before payment",
|
|
state: loopin.SignHtlcTx,
|
|
},
|
|
{
|
|
name: "payment received",
|
|
state: loopin.PaymentReceived,
|
|
wantServer: int64(quoteFee),
|
|
},
|
|
{
|
|
name: "succeeded",
|
|
state: loopin.Succeeded,
|
|
wantServer: int64(quoteFee),
|
|
},
|
|
{
|
|
name: "succeeded transition failed",
|
|
state: loopin.SucceededTransitioningFailed,
|
|
wantServer: int64(quoteFee),
|
|
},
|
|
{
|
|
name: "timeout swept",
|
|
state: loopin.HtlcTimeoutSwept,
|
|
},
|
|
{
|
|
name: "failed",
|
|
state: loopin.Failed,
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
swap := &loopin.StaticAddressLoopIn{
|
|
QuotedSwapFee: quoteFee,
|
|
}
|
|
swap.SetState(test.state)
|
|
|
|
costServer := staticAddressLoopInSwapServerCost(swap)
|
|
|
|
require.Equal(t, test.wantServer, costServer)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestListStaticAddressSwapsPopulatesTimingAndCosts verifies that the RPC
|
|
// response maps stored static loop-in timing fields and cost fields.
|
|
func TestListStaticAddressSwapsPopulatesTimingAndCosts(t *testing.T) {
|
|
ctx := t.Context()
|
|
lnd := mock_lnd.NewMockLnd()
|
|
|
|
const (
|
|
paymentRequestAmount = btcutil.Amount(50_000)
|
|
quotedSwapFee = btcutil.Amount(1_234)
|
|
depositValue = btcutil.Amount(51_234)
|
|
depositConfHeight = int64(590)
|
|
staticAddressExpiry = uint32(25)
|
|
)
|
|
|
|
_, swapInvoice, err := lnd.Client.AddInvoice(
|
|
ctx, &invoicesrpc.AddInvoiceData{
|
|
Value: lnwire.NewMSatFromSatoshis(paymentRequestAmount),
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
swapHash := lntypes.Hash{1, 2, 3}
|
|
depositID := deposit.ID{4, 5, 6}
|
|
depositOutpoint := wire.OutPoint{
|
|
Hash: chainhash.Hash{7, 8, 9},
|
|
Index: 1,
|
|
}
|
|
testDeposit := &deposit.Deposit{
|
|
ID: depositID,
|
|
OutPoint: depositOutpoint,
|
|
Value: depositValue,
|
|
ConfirmationHeight: depositConfHeight,
|
|
SwapHash: &swapHash,
|
|
}
|
|
testDeposit.SetState(deposit.LoopedIn)
|
|
|
|
_, clientPubkey := mock_lnd.CreateKey(1)
|
|
_, serverPubkey := mock_lnd.CreateKey(2)
|
|
staticAddressParams := &script.Parameters{
|
|
ID: 1,
|
|
ClientPubkey: clientPubkey,
|
|
ServerPubkey: serverPubkey,
|
|
Expiry: staticAddressExpiry,
|
|
PkScript: []byte("pkscript"),
|
|
}
|
|
testDeposit.AddressParams = staticAddressParams
|
|
|
|
initiationTime := time.Unix(1_234, 567).UTC()
|
|
lastUpdateTime := time.Unix(2_345, 678).UTC()
|
|
staticLoopIn := &loopin.StaticAddressLoopIn{
|
|
SwapHash: swapHash,
|
|
SwapInvoice: swapInvoice,
|
|
InitiationTime: initiationTime,
|
|
LastUpdateTime: lastUpdateTime,
|
|
QuotedSwapFee: quotedSwapFee,
|
|
DepositOutpoints: []string{depositOutpoint.String()},
|
|
Deposits: []*deposit.Deposit{testDeposit},
|
|
}
|
|
staticLoopIn.SetState(loopin.Succeeded)
|
|
|
|
depositStore := &mockDepositStore{
|
|
byOutpoint: map[string]*deposit.Deposit{
|
|
depositOutpoint.String(): testDeposit,
|
|
},
|
|
}
|
|
depositMgr := deposit.NewManager(&deposit.ManagerConfig{
|
|
Store: depositStore,
|
|
})
|
|
|
|
staticLoopInMgr, err := loopin.NewManager(&loopin.Config{
|
|
Store: &mockStaticAddressLoopInStore{
|
|
swaps: []*loopin.StaticAddressLoopIn{staticLoopIn},
|
|
},
|
|
DepositManager: depositMgr,
|
|
}, 1)
|
|
require.NoError(t, err)
|
|
|
|
addrStore := &mockAddressStore{
|
|
params: []*script.Parameters{staticAddressParams},
|
|
}
|
|
addrMgr, err := address.NewManager(&address.ManagerConfig{
|
|
Store: addrStore,
|
|
WalletKit: lnd.WalletKit,
|
|
ChainParams: lnd.ChainParams,
|
|
}, 1)
|
|
require.NoError(t, err)
|
|
|
|
server := &swapClientServer{
|
|
network: lndclient.NetworkTestnet,
|
|
lnd: &lnd.LndServices,
|
|
staticAddressManager: addrMgr,
|
|
depositManager: depositMgr,
|
|
staticLoopInManager: staticLoopInMgr,
|
|
}
|
|
resp, err := server.ListStaticAddressSwaps(
|
|
ctx, &looprpc.ListStaticAddressSwapsRequest{},
|
|
)
|
|
require.NoError(t, err)
|
|
require.Len(t, resp.Swaps, 1)
|
|
|
|
swap := resp.Swaps[0]
|
|
require.Equal(t, swapHash[:], swap.SwapHash)
|
|
require.Equal(t, []string{depositOutpoint.String()}, swap.DepositOutpoints)
|
|
require.Equal(
|
|
t, looprpc.StaticAddressLoopInSwapState_SUCCEEDED, swap.State,
|
|
)
|
|
require.Equal(t, int64(depositValue), swap.SwapAmountSatoshis)
|
|
require.Equal(
|
|
t, int64(paymentRequestAmount), swap.PaymentRequestAmountSatoshis,
|
|
)
|
|
require.Equal(t, initiationTime.UnixNano(), swap.InitiationTime)
|
|
require.Equal(t, lastUpdateTime.UnixNano(), swap.LastUpdateTime)
|
|
require.Equal(t, int64(quotedSwapFee), swap.CostServer)
|
|
require.Zero(t, swap.CostOnchain)
|
|
require.Zero(t, swap.CostOffchain)
|
|
require.Len(t, swap.Deposits, 1)
|
|
|
|
rpcDeposit := swap.Deposits[0]
|
|
require.Equal(t, depositID[:], rpcDeposit.Id)
|
|
require.Equal(t, depositOutpoint.String(), rpcDeposit.Outpoint)
|
|
require.Equal(t, int64(depositValue), rpcDeposit.Value)
|
|
require.Equal(t, depositConfHeight, rpcDeposit.ConfirmationHeight)
|
|
require.Equal(t, swapHash[:], rpcDeposit.SwapHash)
|
|
require.Equal(t, looprpc.DepositState_LOOPED_IN, rpcDeposit.State)
|
|
require.Equal(
|
|
t, depositConfHeight+int64(staticAddressExpiry)-600,
|
|
rpcDeposit.BlocksUntilExpiry,
|
|
)
|
|
}
|
|
|
|
// TestStaticAddressLoopInMarshallUsesStaticTypeAndP2WSH protects the RPC
|
|
// mapping invariant that static loop-ins expose their static type, static
|
|
// state, and P2WSH HTLC address without leaking a taproot HTLC address.
|
|
func TestStaticAddressLoopInMarshallUsesStaticTypeAndP2WSH(t *testing.T) {
|
|
server := &swapClientServer{}
|
|
loopSwap := &loop.SwapInfo{
|
|
SwapStateData: loopdb.SwapStateData{
|
|
State: loopdb.StateInitiated,
|
|
},
|
|
SwapContract: loopdb.SwapContract{
|
|
InitiationTime: time.Now(),
|
|
},
|
|
LastUpdate: time.Now(),
|
|
SwapHash: lntypes.Hash{1},
|
|
SwapType: swap.TypeStaticAddressLoopIn,
|
|
StaticAddressLoopInState: loopin.SignHtlcTx,
|
|
HtlcAddressP2WSH: testnetAddr,
|
|
}
|
|
|
|
rpcSwap, err := server.marshallSwap(t.Context(), loopSwap)
|
|
require.NoError(t, err)
|
|
require.Equal(t, looprpc.SwapType_STATIC_LOOP_IN, rpcSwap.Type)
|
|
require.Equal(
|
|
t, looprpc.StaticAddressLoopInSwapState_SIGN_HTLC_TX,
|
|
rpcSwap.GetStaticLoopInState(),
|
|
)
|
|
require.Equal(t, looprpc.SwapState_INITIATED, rpcSwap.State)
|
|
require.Equal(t, testnetAddr.EncodeAddress(), rpcSwap.HtlcAddressP2Wsh)
|
|
require.Empty(t, rpcSwap.HtlcAddressP2Tr)
|
|
}
|
|
|
|
// TestStaticAddressLoopInMarshallFailuresLeaveLegacyFieldsDefault asserts that
|
|
// static loop-in failures keep default legacy fields while preserving the
|
|
// precise static state.
|
|
func TestStaticAddressLoopInMarshallFailuresLeaveLegacyFieldsDefault(
|
|
t *testing.T) {
|
|
|
|
tests := []struct {
|
|
name string
|
|
state fsm.StateType
|
|
wantStaticState looprpc.StaticAddressLoopInSwapState
|
|
}{
|
|
{
|
|
name: "failed",
|
|
state: loopin.Failed,
|
|
wantStaticState: looprpc.
|
|
StaticAddressLoopInSwapState_FAILED_STATIC_ADDRESS_SWAP,
|
|
},
|
|
{
|
|
name: "succeeded transitioning failed",
|
|
state: loopin.SucceededTransitioningFailed,
|
|
wantStaticState: looprpc.
|
|
StaticAddressLoopInSwapState_SUCCEEDED_TRANSITIONING_FAILED,
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
server, staticLoopIn := newGenericStaticLoopInServer(t)
|
|
staticLoopIn.SetState(test.state)
|
|
loopSwap, err := server.staticAddressLoopInSwapInfo(
|
|
t.Context(), staticLoopIn,
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
rpcSwap, err := server.marshallSwap(t.Context(), loopSwap)
|
|
|
|
require.NoError(t, err)
|
|
require.Equal(t, looprpc.SwapState_INITIATED, rpcSwap.State)
|
|
require.Equal(
|
|
t, looprpc.FailureReason_FAILURE_REASON_NONE,
|
|
rpcSwap.FailureReason,
|
|
)
|
|
require.Equal(
|
|
t, test.wantStaticState,
|
|
rpcSwap.GetStaticLoopInState(),
|
|
)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestStaticAddressLoopInMarshallRejectsMissingHtlcAddress protects the
|
|
// fail-closed HTLC-address invariant for static loop-ins missing the P2WSH
|
|
// address required by the client-facing RPC representation.
|
|
func TestStaticAddressLoopInMarshallRejectsMissingHtlcAddress(t *testing.T) {
|
|
_, taprootAddress := newTestStaticAddressParams(t)
|
|
server := &swapClientServer{}
|
|
loopSwap := &loop.SwapInfo{
|
|
SwapStateData: loopdb.SwapStateData{
|
|
State: loopdb.StateInitiated,
|
|
},
|
|
SwapContract: loopdb.SwapContract{
|
|
InitiationTime: time.Now(),
|
|
},
|
|
LastUpdate: time.Now(),
|
|
SwapHash: lntypes.Hash{1},
|
|
SwapType: swap.TypeStaticAddressLoopIn,
|
|
StaticAddressLoopInState: loopin.SignHtlcTx,
|
|
HtlcAddressP2TR: taprootAddress,
|
|
}
|
|
|
|
_, err := server.marshallSwap(t.Context(), loopSwap)
|
|
require.ErrorContains(t, err, "missing static address loop-in P2WSH HTLC address")
|
|
}
|
|
|
|
// TestStaticAddressLoopInSwapInfoFailsClosedWhenHtlcKeysMissing protects the
|
|
// HTLC-address construction invariant that missing cooperative keys must not
|
|
// produce monitorable swap info.
|
|
func TestStaticAddressLoopInSwapInfoFailsClosedWhenHtlcKeysMissing(t *testing.T) {
|
|
server, staticLoopIn := newGenericStaticLoopInServer(t)
|
|
staticLoopIn.ClientPubkey = nil
|
|
|
|
_, err := server.staticAddressLoopInSwapInfo(t.Context(), staticLoopIn)
|
|
require.ErrorContains(
|
|
t, err, "missing static address loop-in client HTLC key",
|
|
)
|
|
}
|
|
|
|
// TestMonitorSnapshotIncludesStaticAddressLoopIns protects the monitor snapshot
|
|
// invariant that pending static loop-ins are included alongside cached generic
|
|
// swaps with their static state and swap-specific HTLC address.
|
|
func TestMonitorSnapshotIncludesStaticAddressLoopIns(t *testing.T) {
|
|
ctx := t.Context()
|
|
server, staticLoopIn := newGenericStaticLoopInServer(t)
|
|
|
|
pendingSwaps, completedSwaps := server.monitorCachedSwaps()
|
|
err := server.appendStaticAddressLoopInMonitorSnapshot(
|
|
ctx, &pendingSwaps, &completedSwaps,
|
|
)
|
|
require.NoError(t, err)
|
|
require.Empty(t, completedSwaps)
|
|
require.Len(t, pendingSwaps, 1)
|
|
require.Equal(t, staticLoopIn.SwapHash, pendingSwaps[0].SwapHash)
|
|
require.Equal(t, swap.TypeStaticAddressLoopIn, pendingSwaps[0].SwapType)
|
|
require.Equal(
|
|
t, staticLoopIn.GetState(),
|
|
pendingSwaps[0].StaticAddressLoopInState,
|
|
)
|
|
assertStaticLoopInUsesSwapHtlcAddress(t, staticLoopIn, pendingSwaps[0])
|
|
}
|
|
|
|
// TestMonitorSnapshotIncludesFinalStaticAddressLoopIns protects the monitor
|
|
// snapshot invariant that exact final static loop-in states are completed swaps.
|
|
func TestMonitorSnapshotIncludesFinalStaticAddressLoopIns(t *testing.T) {
|
|
server, staticLoopIn := newGenericStaticLoopInServer(t)
|
|
staticLoopIn.SetState(loopin.Succeeded)
|
|
|
|
pendingSwaps, completedSwaps := server.monitorCachedSwaps()
|
|
err := server.appendStaticAddressLoopInMonitorSnapshot(
|
|
t.Context(), &pendingSwaps, &completedSwaps,
|
|
)
|
|
|
|
require.NoError(t, err)
|
|
require.Empty(t, pendingSwaps)
|
|
require.Len(t, completedSwaps, 1)
|
|
}
|
|
|
|
// TestStaticLoopInStatusUpdaterUsesSwapHtlcAddress protects the live-update
|
|
// invariant that static loop-in status events derive the HTLC address from the
|
|
// swap, not from reusable static address parameters.
|
|
func TestStaticLoopInStatusUpdaterUsesSwapHtlcAddress(t *testing.T) {
|
|
ctx := t.Context()
|
|
_, staticLoopIn := newGenericStaticLoopInServer(t)
|
|
staticLoopIn.AddressParams = nil
|
|
statusChan := make(chan loop.SwapInfo, 1)
|
|
updater := &staticLoopInStatusUpdater{
|
|
statusChan: statusChan,
|
|
mainCtx: ctx,
|
|
chainParams: &chaincfg.TestNet3Params,
|
|
}
|
|
|
|
err := updater.sendUpdate(ctx, staticLoopIn)
|
|
require.NoError(t, err)
|
|
swapInfo := <-statusChan
|
|
assertStaticLoopInUsesSwapHtlcAddress(t, staticLoopIn, swapInfo)
|
|
}
|
|
|
|
// TestMonitorSuppressesStaticAddressLoopInSnapshotLiveDuplicate protects the
|
|
// monitor race invariant that live static loop-in updates arriving during the
|
|
// initial snapshot are deduplicated without dropping newer progress.
|
|
func TestMonitorSuppressesStaticAddressLoopInSnapshotLiveDuplicate(t *testing.T) {
|
|
logger := btclog.NewSLogger(
|
|
btclog.NewDefaultHandler(os.Stdout),
|
|
)
|
|
setLogger(logger.SubSystem(Subsystem))
|
|
|
|
ctx, cancel := context.WithCancel(t.Context())
|
|
defer cancel()
|
|
mainCtx, mainCancel := context.WithCancel(t.Context())
|
|
defer mainCancel()
|
|
server, staticLoopIn, store := newGenericStaticLoopInServerWithStore(t)
|
|
server.statusChan = make(chan loop.SwapInfo)
|
|
server.subscribers = make(map[int]chan<- any)
|
|
server.mainCtx = mainCtx
|
|
|
|
snapshotStarted := make(chan struct{}, 1)
|
|
releaseSnapshot := make(chan struct{})
|
|
store.beforeGet = func() {
|
|
select {
|
|
case snapshotStarted <- struct{}{}:
|
|
default:
|
|
}
|
|
}
|
|
store.waitGet = releaseSnapshot
|
|
|
|
go server.processStatusUpdates(mainCtx)
|
|
|
|
monitorServer := &testMonitorServer{
|
|
ctx: ctx,
|
|
sent: make(chan *looprpc.SwapStatus, 3),
|
|
}
|
|
errChan := make(chan error, 1)
|
|
go func() {
|
|
errChan <- server.Monitor(&looprpc.MonitorRequest{}, monitorServer)
|
|
}()
|
|
|
|
select {
|
|
case <-snapshotStarted:
|
|
case <-ctx.Done():
|
|
t.Fatal(ctx.Err())
|
|
}
|
|
|
|
staticUpdate, err := server.staticAddressLoopInSwapInfo(ctx, staticLoopIn)
|
|
require.NoError(t, err)
|
|
staleUpdate := *staticUpdate
|
|
staleUpdate.State = loopdb.StateInitiated
|
|
staleUpdate.LastUpdate = staticUpdate.LastUpdate.Add(-time.Second)
|
|
server.statusChan <- staleUpdate
|
|
|
|
server.statusChan <- *staticUpdate
|
|
close(releaseSnapshot)
|
|
|
|
first := receiveMonitorUpdate(t, ctx, monitorServer.sent)
|
|
require.Equal(t, staticLoopIn.SwapHash[:], first.IdBytes)
|
|
require.Equal(t, looprpc.SwapType_STATIC_LOOP_IN, first.Type)
|
|
require.Equal(
|
|
t, looprpc.StaticAddressLoopInSwapState_PAYMENT_RECEIVED,
|
|
first.GetStaticLoopInState(),
|
|
)
|
|
|
|
nextUpdate := *staticUpdate
|
|
nextUpdate.State = loopdb.StateSuccess
|
|
nextUpdate.StaticAddressLoopInState = loopin.Succeeded
|
|
nextUpdate.LastUpdate = staticUpdate.LastUpdate.Add(time.Second)
|
|
server.statusChan <- nextUpdate
|
|
|
|
second := receiveMonitorUpdate(t, ctx, monitorServer.sent)
|
|
require.Equal(t, staticLoopIn.SwapHash[:], second.IdBytes)
|
|
require.Equal(t, looprpc.SwapType_STATIC_LOOP_IN, second.Type)
|
|
require.Equal(
|
|
t, looprpc.StaticAddressLoopInSwapState_SUCCEEDED,
|
|
second.GetStaticLoopInState(),
|
|
)
|
|
|
|
cancel()
|
|
require.NoError(t, <-errChan)
|
|
}
|
|
|
|
// TestStaticAddressLoopInHighWaterSuppressesExactDuplicate protects the
|
|
// high-water dedup invariant that an initial live update identical to the
|
|
// snapshot is treated as stale.
|
|
func TestStaticAddressLoopInHighWaterSuppressesExactDuplicate(t *testing.T) {
|
|
swapHash := lntypes.Hash{1, 2, 3}
|
|
lastUpdate := time.Unix(100, 0).UTC()
|
|
snapshot := loop.SwapInfo{
|
|
SwapHash: swapHash,
|
|
SwapType: swap.TypeStaticAddressLoopIn,
|
|
LastUpdate: lastUpdate,
|
|
StaticAddressLoopInState: loopin.PaymentReceived,
|
|
}
|
|
highWater := staticAddressLoopInMonitorHighWater([]loop.SwapInfo{
|
|
snapshot,
|
|
})
|
|
|
|
isStale := isInitialStaticAddressLoopInStale(highWater, snapshot)
|
|
|
|
require.True(t, isStale)
|
|
}
|
|
|
|
// TestStaticAddressLoopInHighWaterKeepsSameTimeDifferentState protects the
|
|
// high-water timing invariant that equal timestamps do not hide a distinct
|
|
// static loop-in state transition.
|
|
func TestStaticAddressLoopInHighWaterKeepsSameTimeDifferentState(t *testing.T) {
|
|
swapHash := lntypes.Hash{1, 2, 3}
|
|
lastUpdate := time.Unix(100, 0).UTC()
|
|
snapshot := loop.SwapInfo{
|
|
SwapHash: swapHash,
|
|
SwapType: swap.TypeStaticAddressLoopIn,
|
|
LastUpdate: lastUpdate,
|
|
StaticAddressLoopInState: loopin.PaymentReceived,
|
|
}
|
|
liveUpdate := snapshot
|
|
liveUpdate.StaticAddressLoopInState = loopin.Succeeded
|
|
highWater := staticAddressLoopInMonitorHighWater([]loop.SwapInfo{
|
|
snapshot,
|
|
})
|
|
|
|
isStale := isInitialStaticAddressLoopInStale(highWater, liveUpdate)
|
|
|
|
require.False(t, isStale)
|
|
}
|
|
|
|
// TestStaticAddressLoopInHighWaterSuppressesOlderStaticOnly protects the
|
|
// high-water cache invariant that stale suppression applies only to static
|
|
// loop-ins and cannot filter generic swap updates.
|
|
func TestStaticAddressLoopInHighWaterSuppressesOlderStaticOnly(t *testing.T) {
|
|
swapHash := lntypes.Hash{1, 2, 3}
|
|
lastUpdate := time.Unix(100, 0).UTC()
|
|
snapshot := loop.SwapInfo{
|
|
SwapHash: swapHash,
|
|
SwapType: swap.TypeStaticAddressLoopIn,
|
|
LastUpdate: lastUpdate,
|
|
StaticAddressLoopInState: loopin.PaymentReceived,
|
|
}
|
|
highWater := staticAddressLoopInMonitorHighWater([]loop.SwapInfo{
|
|
snapshot,
|
|
})
|
|
olderStatic := snapshot
|
|
olderStatic.LastUpdate = lastUpdate.Add(-time.Second)
|
|
olderStatic.StaticAddressLoopInState = loopin.Succeeded
|
|
nonStatic := olderStatic
|
|
nonStatic.SwapType = swap.TypeOut
|
|
|
|
staticStale := isInitialStaticAddressLoopInStale(highWater, olderStatic)
|
|
nonStaticStale := isInitialStaticAddressLoopInStale(
|
|
highWater, nonStatic,
|
|
)
|
|
|
|
require.True(t, staticStale)
|
|
require.False(t, nonStaticStale)
|
|
}
|
|
|
|
// TestStaticAddressLoopInStatusUpdateDoesNotEnterGenericSwapCache protects the
|
|
// cache isolation invariant that static loop-in live updates reach subscribers
|
|
// without entering the generic swap cache.
|
|
func TestStaticAddressLoopInStatusUpdateDoesNotEnterGenericSwapCache(t *testing.T) {
|
|
ctx := t.Context()
|
|
server, staticLoopIn := newGenericStaticLoopInServer(t)
|
|
server.statusChan = make(chan loop.SwapInfo)
|
|
updates := make(chan any, 1)
|
|
server.subscribers = map[int]chan<- any{0: updates}
|
|
mainCtx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
go server.processStatusUpdates(mainCtx)
|
|
|
|
staticUpdate, err := server.staticAddressLoopInSwapInfo(ctx, staticLoopIn)
|
|
require.NoError(t, err)
|
|
server.statusChan <- *staticUpdate
|
|
|
|
select {
|
|
case update := <-updates:
|
|
require.Equal(t, staticLoopIn.SwapHash, update.(loop.SwapInfo).SwapHash)
|
|
|
|
case <-ctx.Done():
|
|
t.Fatal(ctx.Err())
|
|
}
|
|
|
|
server.swapsLock.Lock()
|
|
_, cached := server.swaps[staticLoopIn.SwapHash]
|
|
server.swapsLock.Unlock()
|
|
require.False(t, cached)
|
|
}
|
|
|
|
func newGenericStaticLoopInServer(t *testing.T) (*swapClientServer,
|
|
*loopin.StaticAddressLoopIn) {
|
|
|
|
server, staticLoopIn, _ := newGenericStaticLoopInServerWithStore(t)
|
|
|
|
return server, staticLoopIn
|
|
}
|
|
|
|
func newTestStaticAddressParams(t *testing.T) (*script.Parameters,
|
|
*btcutil.AddressTaproot) {
|
|
|
|
t.Helper()
|
|
|
|
const staticAddressExpiry = uint32(25)
|
|
|
|
_, staticClientPubkey := mock_lnd.CreateKey(12)
|
|
_, staticServerPubkey := mock_lnd.CreateKey(13)
|
|
staticAddress, err := script.NewStaticAddress(
|
|
input.MuSig2Version100RC2, int64(staticAddressExpiry),
|
|
staticClientPubkey, staticServerPubkey,
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
staticPkScript, err := staticAddress.StaticAddressScript()
|
|
require.NoError(t, err)
|
|
|
|
taprootAddress, err := btcutil.NewAddressTaproot(
|
|
schnorr.SerializePubKey(staticAddress.TaprootKey),
|
|
&chaincfg.TestNet3Params,
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
return &script.Parameters{
|
|
ClientPubkey: staticClientPubkey,
|
|
ServerPubkey: staticServerPubkey,
|
|
Expiry: staticAddressExpiry,
|
|
PkScript: staticPkScript,
|
|
}, taprootAddress
|
|
}
|
|
|
|
func newGenericStaticLoopInServerWithStore(t *testing.T) (*swapClientServer,
|
|
*loopin.StaticAddressLoopIn, *mockStaticAddressLoopInStore) {
|
|
|
|
t.Helper()
|
|
|
|
_, clientPubkey := mock_lnd.CreateKey(10)
|
|
_, serverPubkey := mock_lnd.CreateKey(11)
|
|
addressParams, _ := newTestStaticAddressParams(t)
|
|
depositOutpoint := wire.OutPoint{
|
|
Hash: chainhash.Hash{12, 13, 14},
|
|
Index: 2,
|
|
}
|
|
staticDeposit := &deposit.Deposit{
|
|
OutPoint: depositOutpoint,
|
|
Value: 51_000,
|
|
}
|
|
lastHop := route.Vertex{7, 8, 9}
|
|
|
|
staticLoopIn := &loopin.StaticAddressLoopIn{
|
|
SwapHash: lntypes.Hash{1, 2, 3},
|
|
HtlcCltvExpiry: 700,
|
|
InitiationTime: time.Unix(100, 0).UTC(),
|
|
LastUpdateTime: time.Unix(200, 0).UTC(),
|
|
Label: "static-loop-in",
|
|
ClientPubkey: clientPubkey,
|
|
ServerPubkey: serverPubkey,
|
|
LastHop: lastHop[:],
|
|
QuotedSwapFee: 1_111,
|
|
SelectedAmount: 50_000,
|
|
DepositOutpoints: []string{depositOutpoint.String()},
|
|
Deposits: []*deposit.Deposit{staticDeposit},
|
|
AddressParams: addressParams,
|
|
}
|
|
staticLoopIn.SetState(loopin.PaymentReceived)
|
|
|
|
depositStore := &mockDepositStore{
|
|
byOutpoint: map[string]*deposit.Deposit{
|
|
depositOutpoint.String(): staticDeposit,
|
|
},
|
|
}
|
|
loopInStore := &mockStaticAddressLoopInStore{
|
|
swaps: []*loopin.StaticAddressLoopIn{staticLoopIn},
|
|
}
|
|
staticLoopInManager, err := loopin.NewManager(&loopin.Config{
|
|
Store: loopInStore,
|
|
DepositManager: deposit.NewManager(&deposit.ManagerConfig{
|
|
Store: depositStore,
|
|
}),
|
|
}, 1)
|
|
require.NoError(t, err)
|
|
|
|
return &swapClientServer{
|
|
network: lndclient.NetworkTestnet,
|
|
swaps: make(map[lntypes.Hash]loop.SwapInfo),
|
|
staticLoopInManager: staticLoopInManager,
|
|
}, staticLoopIn, loopInStore
|
|
}
|
|
|
|
// assertStaticLoopInUsesSwapHtlcAddress verifies the static loop-in uses the
|
|
// swap HTLC P2WSH address expected by the fixture.
|
|
func assertStaticLoopInUsesSwapHtlcAddress(t *testing.T,
|
|
staticLoopIn *loopin.StaticAddressLoopIn, swapInfo loop.SwapInfo) {
|
|
|
|
t.Helper()
|
|
|
|
expectedAddress, err := staticAddressLoopInHtlcAddress(
|
|
staticLoopIn, &chaincfg.TestNet3Params,
|
|
)
|
|
require.NoError(t, err)
|
|
require.Nil(t, swapInfo.HtlcAddressP2TR)
|
|
require.NotNil(t, swapInfo.HtlcAddressP2WSH)
|
|
require.Equal(
|
|
t, expectedAddress.EncodeAddress(),
|
|
swapInfo.HtlcAddressP2WSH.EncodeAddress(),
|
|
)
|
|
}
|
|
|
|
// testMonitorServer implements the monitor stream interface for tests.
|
|
type testMonitorServer struct {
|
|
ctx context.Context
|
|
sent chan *looprpc.SwapStatus
|
|
}
|
|
|
|
// Send forwards monitor updates to the test channel until the context is canceled.
|
|
func (s *testMonitorServer) Send(swapStatus *looprpc.SwapStatus) error {
|
|
select {
|
|
case s.sent <- swapStatus:
|
|
return nil
|
|
|
|
case <-s.ctx.Done():
|
|
return s.ctx.Err()
|
|
}
|
|
}
|
|
|
|
// SetHeader is a no-op stub that satisfies the monitor stream interface in tests.
|
|
func (s *testMonitorServer) SetHeader(metadata.MD) error {
|
|
return nil
|
|
}
|
|
|
|
// SendHeader is a no-op stub that satisfies the monitor stream interface in tests.
|
|
func (s *testMonitorServer) SendHeader(metadata.MD) error {
|
|
return nil
|
|
}
|
|
|
|
// SetTrailer is a no-op stub that satisfies the monitor stream interface in tests.
|
|
func (s *testMonitorServer) SetTrailer(metadata.MD) {}
|
|
|
|
// Context returns the stream context used by the test monitor server.
|
|
func (s *testMonitorServer) Context() context.Context {
|
|
return s.ctx
|
|
}
|
|
|
|
// SendMsg is a no-op stub that satisfies the monitor stream interface in tests.
|
|
func (s *testMonitorServer) SendMsg(any) error {
|
|
return nil
|
|
}
|
|
|
|
// RecvMsg is a no-op stub that satisfies the monitor stream interface in tests.
|
|
func (s *testMonitorServer) RecvMsg(any) error {
|
|
return nil
|
|
}
|
|
|
|
// receiveMonitorUpdate waits for a monitor update or fails if the context is canceled.
|
|
func receiveMonitorUpdate(t *testing.T, ctx context.Context,
|
|
updates <-chan *looprpc.SwapStatus) *looprpc.SwapStatus {
|
|
|
|
t.Helper()
|
|
|
|
select {
|
|
case update := <-updates:
|
|
return update
|
|
|
|
case <-ctx.Done():
|
|
t.Fatal(ctx.Err())
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// mockStaticAddressLoopInStore is a minimal in-memory loop-in store for RPC
|
|
// response mapping tests.
|
|
type mockStaticAddressLoopInStore struct {
|
|
swaps []*loopin.StaticAddressLoopIn
|
|
beforeGet func()
|
|
waitGet <-chan struct{}
|
|
}
|
|
|
|
// CreateLoopIn satisfies the static loop-in store interface.
|
|
func (s *mockStaticAddressLoopInStore) CreateLoopIn(_ context.Context,
|
|
_ *loopin.StaticAddressLoopIn) error {
|
|
|
|
return nil
|
|
}
|
|
|
|
// UpdateLoopIn satisfies the static loop-in store interface.
|
|
func (s *mockStaticAddressLoopInStore) UpdateLoopIn(_ context.Context,
|
|
_ *loopin.StaticAddressLoopIn) error {
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetStaticAddressLoopInSwapsByStates returns the configured loop-ins.
|
|
func (s *mockStaticAddressLoopInStore) GetStaticAddressLoopInSwapsByStates(
|
|
ctx context.Context, _ []fsm.StateType) ([]*loopin.StaticAddressLoopIn,
|
|
error) {
|
|
|
|
if s.beforeGet != nil {
|
|
s.beforeGet()
|
|
}
|
|
if s.waitGet != nil {
|
|
select {
|
|
case <-s.waitGet:
|
|
case <-ctx.Done():
|
|
return nil, ctx.Err()
|
|
}
|
|
}
|
|
|
|
return s.swaps, nil
|
|
}
|
|
|
|
// IsStored satisfies the static loop-in store interface.
|
|
func (s *mockStaticAddressLoopInStore) IsStored(_ context.Context,
|
|
_ lntypes.Hash) (bool, error) {
|
|
|
|
return false, nil
|
|
}
|
|
|
|
// RecordStaticAddressRiskDecision satisfies the static loop-in store interface.
|
|
func (s *mockStaticAddressLoopInStore) RecordStaticAddressRiskDecision(
|
|
_ context.Context, _ lntypes.Hash,
|
|
_ loopin.ConfirmationRiskDecision) error {
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetLoopInByHash returns the configured loop-in with the given hash.
|
|
func (s *mockStaticAddressLoopInStore) GetLoopInByHash(_ context.Context,
|
|
swapHash lntypes.Hash) (*loopin.StaticAddressLoopIn, error) {
|
|
|
|
for _, swp := range s.swaps {
|
|
if swp.SwapHash == swapHash {
|
|
return swp, nil
|
|
}
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
// SwapHashesForDepositIDs satisfies the static loop-in store interface.
|
|
func (s *mockStaticAddressLoopInStore) SwapHashesForDepositIDs(
|
|
_ context.Context, _ []deposit.ID) (map[lntypes.Hash][]deposit.ID,
|
|
error) {
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
// TestRPCAutoloopReasonStaticLoopInNoCandidate verifies that the new planner
|
|
// reason is exposed over rpc.
|
|
func TestRPCAutoloopReasonStaticLoopInNoCandidate(t *testing.T) {
|
|
reason, err := rpcAutoloopReason(
|
|
liquidity.ReasonStaticLoopInNoCandidate,
|
|
)
|
|
require.NoError(t, err)
|
|
require.Equal(
|
|
t,
|
|
looprpc.AutoReason_AUTO_REASON_STATIC_LOOP_IN_NO_CANDIDATE,
|
|
reason,
|
|
)
|
|
}
|
|
|
|
// TestRPCAutoloopReasonCustomChannelData verifies that custom-channel
|
|
// disqualifications are exposed over rpc instead of failing the whole dry run.
|
|
func TestRPCAutoloopReasonCustomChannelData(t *testing.T) {
|
|
reason, err := rpcAutoloopReason(liquidity.ReasonCustomChannelData)
|
|
require.NoError(t, err)
|
|
require.Equal(
|
|
t, looprpc.AutoReason_AUTO_REASON_CUSTOM_CHANNEL_DATA, reason,
|
|
)
|
|
}
|
|
|
|
// TestSwapClientServerStopDaemon ensures that calling StopDaemon triggers the
|
|
// daemon shutdown.
|
|
func TestSwapClientServerStopDaemon(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Prepare a server instance that tracks whether shutdown is requested.
|
|
var stopCalled bool
|
|
server := &swapClientServer{
|
|
stopDaemon: func() {
|
|
stopCalled = true
|
|
},
|
|
}
|
|
|
|
// Request the daemon to stop and assert the callback executed.
|
|
_, err := server.StopDaemon(
|
|
context.Background(), &looprpc.StopDaemonRequest{},
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
// Ensure our shutdown callback executed.
|
|
require.True(t, stopCalled)
|
|
}
|
|
|
|
// TestValidateLoopOutRequest tests validation of loop out requests.
|
|
func TestValidateLoopOutRequest(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
chain chaincfg.Params
|
|
confTarget int32
|
|
destAddr btcutil.Address
|
|
label string
|
|
channels []lndclient.ChannelInfo
|
|
outgoingChanSet []uint64
|
|
amount int64
|
|
maxRoutingFee int64
|
|
maxParts uint32
|
|
err error
|
|
expectedTarget int32
|
|
}{
|
|
{
|
|
name: "mainnet address with mainnet backend",
|
|
chain: chaincfg.MainNetParams,
|
|
destAddr: mainnetAddr,
|
|
label: "label ok",
|
|
confTarget: 2,
|
|
channels: []lndclient.ChannelInfo{
|
|
channel2,
|
|
},
|
|
amount: 10000,
|
|
maxParts: 5,
|
|
err: nil,
|
|
expectedTarget: 2,
|
|
},
|
|
{
|
|
name: "mainnet address with testnet backend",
|
|
chain: chaincfg.TestNet3Params,
|
|
destAddr: mainnetAddr,
|
|
label: "label ok",
|
|
confTarget: 2,
|
|
channels: []lndclient.ChannelInfo{
|
|
channel2,
|
|
},
|
|
amount: 10000,
|
|
maxParts: 5,
|
|
err: errIncorrectChain,
|
|
expectedTarget: 0,
|
|
},
|
|
{
|
|
name: "testnet address with testnet backend",
|
|
chain: chaincfg.TestNet3Params,
|
|
destAddr: testnetAddr,
|
|
label: "label ok",
|
|
confTarget: 2,
|
|
channels: []lndclient.ChannelInfo{
|
|
channel2,
|
|
},
|
|
amount: 10000,
|
|
maxParts: 5,
|
|
err: nil,
|
|
expectedTarget: 2,
|
|
},
|
|
{
|
|
name: "testnet address with mainnet backend",
|
|
chain: chaincfg.MainNetParams,
|
|
destAddr: testnetAddr,
|
|
label: "label ok",
|
|
confTarget: 2,
|
|
channels: []lndclient.ChannelInfo{
|
|
channel2,
|
|
},
|
|
amount: 10000,
|
|
maxParts: 5,
|
|
err: errIncorrectChain,
|
|
expectedTarget: 0,
|
|
},
|
|
{
|
|
name: "invalid label",
|
|
chain: chaincfg.MainNetParams,
|
|
destAddr: mainnetAddr,
|
|
label: labels.Reserved,
|
|
confTarget: 2,
|
|
channels: []lndclient.ChannelInfo{
|
|
channel2,
|
|
},
|
|
amount: 10000,
|
|
maxParts: 5,
|
|
err: labels.ErrReservedPrefix,
|
|
expectedTarget: 0,
|
|
},
|
|
{
|
|
name: "invalid conf target",
|
|
chain: chaincfg.MainNetParams,
|
|
destAddr: mainnetAddr,
|
|
label: "label ok",
|
|
confTarget: 1,
|
|
channels: []lndclient.ChannelInfo{
|
|
channel2,
|
|
},
|
|
amount: 10000,
|
|
maxParts: 5,
|
|
err: errConfTargetTooLow,
|
|
expectedTarget: 0,
|
|
},
|
|
{
|
|
name: "default conf target",
|
|
chain: chaincfg.MainNetParams,
|
|
destAddr: mainnetAddr,
|
|
label: "label ok",
|
|
confTarget: 0,
|
|
channels: []lndclient.ChannelInfo{
|
|
channel2,
|
|
},
|
|
amount: 10000,
|
|
maxParts: 5,
|
|
err: nil,
|
|
expectedTarget: 9,
|
|
},
|
|
{
|
|
name: "valid amount for default channel set",
|
|
chain: chaincfg.MainNetParams,
|
|
destAddr: mainnetAddr,
|
|
label: "label ok",
|
|
confTarget: 2,
|
|
channels: []lndclient.ChannelInfo{
|
|
channel1, channel2, channel3,
|
|
},
|
|
amount: 20000,
|
|
maxParts: 5,
|
|
err: nil,
|
|
expectedTarget: 2,
|
|
},
|
|
{
|
|
name: "invalid amount for default channel set",
|
|
chain: chaincfg.MainNetParams,
|
|
destAddr: mainnetAddr,
|
|
label: "label ok",
|
|
confTarget: 2,
|
|
channels: []lndclient.ChannelInfo{
|
|
channel1, channel2, channel3,
|
|
},
|
|
amount: 25000,
|
|
maxParts: 5,
|
|
err: errBalanceTooLow,
|
|
expectedTarget: 0,
|
|
},
|
|
{
|
|
name: "inactive channel in outgoing channel set",
|
|
chain: chaincfg.MainNetParams,
|
|
destAddr: mainnetAddr,
|
|
label: "label ok",
|
|
confTarget: 2,
|
|
channels: []lndclient.ChannelInfo{
|
|
channel1, channel2, channel3,
|
|
},
|
|
outgoingChanSet: []uint64{
|
|
chanID1.ToUint64(),
|
|
},
|
|
amount: 1000,
|
|
maxParts: 5,
|
|
err: errBalanceTooLow,
|
|
expectedTarget: 0,
|
|
},
|
|
{
|
|
name: "outgoing channel set balance is enough",
|
|
chain: chaincfg.MainNetParams,
|
|
destAddr: mainnetAddr,
|
|
label: "label ok",
|
|
confTarget: 2,
|
|
channels: []lndclient.ChannelInfo{
|
|
channel1, channel2, channel3,
|
|
},
|
|
outgoingChanSet: []uint64{
|
|
chanID2.ToUint64(),
|
|
},
|
|
amount: 1000,
|
|
maxParts: 5,
|
|
err: nil,
|
|
expectedTarget: 2,
|
|
},
|
|
{
|
|
name: "outgoing channel set balance not sufficient",
|
|
chain: chaincfg.MainNetParams,
|
|
destAddr: mainnetAddr,
|
|
label: "label ok",
|
|
confTarget: 2,
|
|
channels: []lndclient.ChannelInfo{
|
|
channel1, channel2, channel3,
|
|
},
|
|
outgoingChanSet: []uint64{
|
|
chanID2.ToUint64(),
|
|
},
|
|
amount: 20000,
|
|
maxParts: 5,
|
|
err: errBalanceTooLow,
|
|
expectedTarget: 0,
|
|
},
|
|
{
|
|
name: "amount with routing fee too high",
|
|
chain: chaincfg.MainNetParams,
|
|
destAddr: mainnetAddr,
|
|
label: "label ok",
|
|
confTarget: 2,
|
|
channels: []lndclient.ChannelInfo{
|
|
channel2,
|
|
},
|
|
amount: 10000,
|
|
maxRoutingFee: 100,
|
|
maxParts: 5,
|
|
err: errBalanceTooLow,
|
|
expectedTarget: 0,
|
|
},
|
|
{
|
|
name: "channel reserve leaves balance one sat short",
|
|
chain: chaincfg.MainNetParams,
|
|
destAddr: mainnetAddr,
|
|
label: "label ok",
|
|
confTarget: 2,
|
|
channels: []lndclient.ChannelInfo{
|
|
{
|
|
Active: true,
|
|
ChannelID: chanID2.ToUint64(),
|
|
LocalBalance: 10100,
|
|
LocalConstraints: &lndclient.ChannelConstraints{
|
|
Reserve: 100,
|
|
},
|
|
},
|
|
},
|
|
amount: 10000,
|
|
maxRoutingFee: 1,
|
|
maxParts: 1,
|
|
err: errBalanceTooLow,
|
|
expectedTarget: 0,
|
|
},
|
|
{
|
|
name: "channel reserve leaves exact balance",
|
|
chain: chaincfg.MainNetParams,
|
|
destAddr: mainnetAddr,
|
|
label: "label ok",
|
|
confTarget: 2,
|
|
channels: []lndclient.ChannelInfo{
|
|
{
|
|
Active: true,
|
|
ChannelID: chanID2.ToUint64(),
|
|
LocalBalance: 10101,
|
|
LocalConstraints: &lndclient.ChannelConstraints{
|
|
Reserve: 100,
|
|
},
|
|
},
|
|
},
|
|
amount: 10000,
|
|
maxRoutingFee: 1,
|
|
maxParts: 1,
|
|
err: nil,
|
|
expectedTarget: 2,
|
|
},
|
|
{
|
|
name: "can split between channels",
|
|
chain: chaincfg.MainNetParams,
|
|
destAddr: mainnetAddr,
|
|
label: "label ok",
|
|
confTarget: 2,
|
|
channels: []lndclient.ChannelInfo{
|
|
channel2, channel4,
|
|
},
|
|
amount: 11000,
|
|
maxParts: 16,
|
|
err: nil,
|
|
expectedTarget: 2,
|
|
},
|
|
{
|
|
name: "can't split between channels",
|
|
chain: chaincfg.MainNetParams,
|
|
destAddr: mainnetAddr,
|
|
label: "label ok",
|
|
confTarget: 2,
|
|
channels: []lndclient.ChannelInfo{
|
|
channel2, channel4,
|
|
},
|
|
amount: 11000,
|
|
maxParts: 5,
|
|
err: errBalanceTooLow,
|
|
expectedTarget: 0,
|
|
},
|
|
{
|
|
name: "node pubkey as dest addr",
|
|
chain: chaincfg.MainNetParams,
|
|
destAddr: nodepubkeyAddr,
|
|
err: errInvalidAddress,
|
|
expectedTarget: 0,
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
|
|
lnd := mock_lnd.NewMockLnd()
|
|
lnd.Channels = test.channels
|
|
|
|
req := &looprpc.LoopOutRequest{
|
|
Amt: test.amount,
|
|
MaxSwapRoutingFee: test.maxRoutingFee,
|
|
OutgoingChanSet: test.outgoingChanSet,
|
|
Label: test.label,
|
|
SweepConfTarget: test.confTarget,
|
|
}
|
|
|
|
logger := btclog.NewSLogger(
|
|
btclog.NewDefaultHandler(os.Stdout),
|
|
)
|
|
setLogger(logger.SubSystem(Subsystem))
|
|
|
|
conf, err := validateLoopOutRequest(
|
|
ctx, lnd.Client, &test.chain, req,
|
|
test.destAddr, test.maxParts,
|
|
)
|
|
require.ErrorIs(t, err, test.err)
|
|
require.Equal(t, test.expectedTarget, conf)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestHasBandwidth tests that the hasBandwidth function correctly simulates
|
|
// the MPP logic used by LND.
|
|
func TestHasBandwidth(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
channels []lndclient.ChannelInfo
|
|
maxParts int
|
|
amt btcutil.Amount
|
|
expectedRes bool
|
|
expectedShards int
|
|
}{
|
|
{
|
|
name: "can route due to high number of parts",
|
|
channels: []lndclient.ChannelInfo{
|
|
{
|
|
LocalBalance: 100,
|
|
},
|
|
{
|
|
LocalBalance: 10,
|
|
},
|
|
},
|
|
maxParts: 11,
|
|
amt: 110,
|
|
expectedRes: true,
|
|
expectedShards: 8,
|
|
},
|
|
{
|
|
name: "can't route due to low number of parts",
|
|
channels: []lndclient.ChannelInfo{
|
|
{
|
|
LocalBalance: 100,
|
|
},
|
|
{
|
|
LocalBalance: 10,
|
|
},
|
|
},
|
|
maxParts: 5,
|
|
amt: 110,
|
|
expectedRes: false,
|
|
},
|
|
{
|
|
name: "can route",
|
|
channels: []lndclient.ChannelInfo{
|
|
{
|
|
LocalBalance: 1000,
|
|
},
|
|
{
|
|
LocalBalance: 1000,
|
|
},
|
|
},
|
|
maxParts: 5,
|
|
amt: 2000,
|
|
expectedRes: true,
|
|
expectedShards: 2,
|
|
},
|
|
{
|
|
name: "can route",
|
|
channels: []lndclient.ChannelInfo{
|
|
{
|
|
LocalBalance: 100,
|
|
},
|
|
{
|
|
LocalBalance: 100,
|
|
},
|
|
{
|
|
LocalBalance: 100,
|
|
},
|
|
},
|
|
maxParts: 10,
|
|
amt: 300,
|
|
expectedRes: true,
|
|
expectedShards: 10,
|
|
},
|
|
{
|
|
name: "can't route due to empty channel set",
|
|
maxParts: 10,
|
|
amt: 300,
|
|
expectedRes: false,
|
|
expectedShards: 0,
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
res, shards := hasBandwidth(test.channels, test.amt,
|
|
test.maxParts)
|
|
require.Equal(t, test.expectedRes, res)
|
|
require.Equal(t, test.expectedShards, shards)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestListSwapsFilterAndPagination tests the filtering and
|
|
// paging of the ListSwaps command.
|
|
func TestListSwapsFilterAndPagination(t *testing.T) {
|
|
unixTime := time.Unix(0, 0)
|
|
firstSwapStartTime := unixTime.Add(10 * time.Minute)
|
|
secondSwapStartTime := unixTime.Add(20 * time.Minute)
|
|
thirdSwapStartTime := unixTime.Add(30 * time.Minute)
|
|
|
|
// Create a set of test swaps of various types which contain the minimal
|
|
// viable amount of info to successfully be run through marshallSwap.
|
|
swapInOrder0 := loop.SwapInfo{
|
|
SwapStateData: loopdb.SwapStateData{
|
|
State: loopdb.StateInitiated,
|
|
Cost: loopdb.SwapCost{},
|
|
},
|
|
SwapContract: loopdb.SwapContract{
|
|
InitiationTime: firstSwapStartTime,
|
|
},
|
|
LastUpdate: time.Now(),
|
|
SwapHash: lntypes.Hash{1},
|
|
SwapType: swap.TypeIn,
|
|
HtlcAddressP2WSH: testnetAddr,
|
|
HtlcAddressP2TR: testnetAddr,
|
|
}
|
|
|
|
swapOutOrder1 := loop.SwapInfo{
|
|
SwapStateData: loopdb.SwapStateData{
|
|
State: loopdb.StateInitiated,
|
|
Cost: loopdb.SwapCost{},
|
|
},
|
|
SwapContract: loopdb.SwapContract{
|
|
InitiationTime: secondSwapStartTime,
|
|
},
|
|
LastUpdate: time.Now(),
|
|
SwapHash: lntypes.Hash{2},
|
|
SwapType: swap.TypeOut,
|
|
HtlcAddressP2WSH: testnetAddr,
|
|
HtlcAddressP2TR: testnetAddr,
|
|
}
|
|
|
|
swapOutOrder2 := loop.SwapInfo{
|
|
SwapStateData: loopdb.SwapStateData{
|
|
State: loopdb.StateInitiated,
|
|
Cost: loopdb.SwapCost{},
|
|
},
|
|
SwapContract: loopdb.SwapContract{
|
|
InitiationTime: thirdSwapStartTime,
|
|
},
|
|
LastUpdate: time.Now(),
|
|
SwapHash: lntypes.Hash{3},
|
|
SwapType: swap.TypeOut,
|
|
HtlcAddressP2WSH: testnetAddr,
|
|
HtlcAddressP2TR: testnetAddr,
|
|
}
|
|
|
|
mockSwaps := []loop.SwapInfo{swapInOrder0, swapOutOrder1, swapOutOrder2}
|
|
|
|
tests := []struct {
|
|
name string
|
|
// Define the mock swaps that will be stored in the mock client.
|
|
mockSwaps []loop.SwapInfo
|
|
req *looprpc.ListSwapsRequest
|
|
// These hashes must be in the correct return order as the response.
|
|
expectedReturnedSwaps []lntypes.Hash
|
|
expectedNextStartTime int64
|
|
}{
|
|
{
|
|
name: "fetch with defaults",
|
|
mockSwaps: mockSwaps,
|
|
req: &looprpc.ListSwapsRequest{},
|
|
expectedReturnedSwaps: []lntypes.Hash{
|
|
swapInOrder0.SwapHash,
|
|
swapOutOrder1.SwapHash,
|
|
swapOutOrder2.SwapHash,
|
|
},
|
|
expectedNextStartTime: 0,
|
|
},
|
|
{
|
|
name: "fetch with swaptype=loopin filter",
|
|
mockSwaps: mockSwaps,
|
|
req: &looprpc.ListSwapsRequest{
|
|
ListSwapFilter: &looprpc.ListSwapsFilter{
|
|
SwapType: looprpc.ListSwapsFilter_LOOP_IN},
|
|
},
|
|
expectedReturnedSwaps: []lntypes.Hash{
|
|
swapInOrder0.SwapHash,
|
|
},
|
|
expectedNextStartTime: 0,
|
|
},
|
|
{
|
|
name: "fetch with swaptype=loopout filter",
|
|
mockSwaps: mockSwaps,
|
|
req: &looprpc.ListSwapsRequest{
|
|
ListSwapFilter: &looprpc.ListSwapsFilter{
|
|
SwapType: looprpc.ListSwapsFilter_LOOP_OUT,
|
|
},
|
|
},
|
|
expectedReturnedSwaps: []lntypes.Hash{
|
|
swapOutOrder1.SwapHash,
|
|
swapOutOrder2.SwapHash,
|
|
},
|
|
expectedNextStartTime: 0,
|
|
},
|
|
{
|
|
name: "fetch with limit",
|
|
mockSwaps: mockSwaps,
|
|
req: &looprpc.ListSwapsRequest{
|
|
MaxSwaps: 2,
|
|
},
|
|
expectedReturnedSwaps: []lntypes.Hash{
|
|
swapInOrder0.SwapHash,
|
|
swapOutOrder1.SwapHash,
|
|
},
|
|
expectedNextStartTime: secondSwapStartTime.UnixNano() + 1,
|
|
},
|
|
{
|
|
name: "fetch with limit set to default",
|
|
mockSwaps: mockSwaps,
|
|
req: &looprpc.ListSwapsRequest{
|
|
MaxSwaps: 0,
|
|
},
|
|
expectedReturnedSwaps: []lntypes.Hash{
|
|
swapInOrder0.SwapHash,
|
|
swapOutOrder1.SwapHash,
|
|
swapOutOrder2.SwapHash,
|
|
},
|
|
expectedNextStartTime: 0,
|
|
},
|
|
{
|
|
name: "fetch with time filter #1",
|
|
mockSwaps: mockSwaps,
|
|
req: &looprpc.ListSwapsRequest{
|
|
ListSwapFilter: &looprpc.ListSwapsFilter{
|
|
StartTimestampNs: unixTime.Add(25 * time.Minute).UnixNano(),
|
|
},
|
|
},
|
|
expectedReturnedSwaps: []lntypes.Hash{
|
|
swapOutOrder2.SwapHash,
|
|
},
|
|
expectedNextStartTime: 0,
|
|
},
|
|
{
|
|
name: "fetch with time filter #2",
|
|
mockSwaps: mockSwaps,
|
|
req: &looprpc.ListSwapsRequest{
|
|
ListSwapFilter: &looprpc.ListSwapsFilter{
|
|
StartTimestampNs: unixTime.Add(5 * time.Minute).UnixNano(),
|
|
},
|
|
},
|
|
expectedReturnedSwaps: []lntypes.Hash{
|
|
swapInOrder0.SwapHash,
|
|
swapOutOrder1.SwapHash,
|
|
swapOutOrder2.SwapHash,
|
|
},
|
|
expectedNextStartTime: 0,
|
|
},
|
|
{
|
|
name: "fetch with swaptype=loopout filter, time filter, limit set",
|
|
mockSwaps: mockSwaps,
|
|
req: &looprpc.ListSwapsRequest{
|
|
ListSwapFilter: &looprpc.ListSwapsFilter{
|
|
SwapType: looprpc.ListSwapsFilter_LOOP_OUT,
|
|
StartTimestampNs: unixTime.Add(15 * time.Minute).UnixNano(),
|
|
},
|
|
MaxSwaps: 1,
|
|
},
|
|
expectedReturnedSwaps: []lntypes.Hash{
|
|
swapOutOrder1.SwapHash,
|
|
},
|
|
expectedNextStartTime: secondSwapStartTime.UnixNano() + 1,
|
|
},
|
|
{
|
|
name: "fetch with time filter, limit set",
|
|
mockSwaps: mockSwaps,
|
|
req: &looprpc.ListSwapsRequest{
|
|
ListSwapFilter: &looprpc.ListSwapsFilter{
|
|
StartTimestampNs: unixTime.UnixNano(),
|
|
},
|
|
MaxSwaps: 2,
|
|
},
|
|
expectedReturnedSwaps: []lntypes.Hash{
|
|
swapInOrder0.SwapHash,
|
|
swapOutOrder1.SwapHash,
|
|
},
|
|
expectedNextStartTime: secondSwapStartTime.UnixNano() + 1,
|
|
},
|
|
{
|
|
name: "fetch with time filter, limit set 2",
|
|
mockSwaps: mockSwaps,
|
|
req: &looprpc.ListSwapsRequest{
|
|
ListSwapFilter: &looprpc.ListSwapsFilter{
|
|
StartTimestampNs: unixTime.UnixNano(),
|
|
},
|
|
MaxSwaps: 3,
|
|
},
|
|
expectedReturnedSwaps: []lntypes.Hash{
|
|
swapInOrder0.SwapHash,
|
|
swapOutOrder1.SwapHash,
|
|
swapOutOrder2.SwapHash,
|
|
},
|
|
expectedNextStartTime: 0,
|
|
},
|
|
{
|
|
name: "fetch with time filter edge case 1",
|
|
mockSwaps: mockSwaps,
|
|
req: &looprpc.ListSwapsRequest{
|
|
ListSwapFilter: &looprpc.ListSwapsFilter{
|
|
StartTimestampNs: secondSwapStartTime.UnixNano(),
|
|
},
|
|
},
|
|
expectedReturnedSwaps: []lntypes.Hash{
|
|
swapOutOrder1.SwapHash,
|
|
swapOutOrder2.SwapHash,
|
|
},
|
|
expectedNextStartTime: 0,
|
|
},
|
|
{
|
|
name: "fetch with time filter edge case 2",
|
|
mockSwaps: mockSwaps,
|
|
req: &looprpc.ListSwapsRequest{
|
|
ListSwapFilter: &looprpc.ListSwapsFilter{
|
|
StartTimestampNs: secondSwapStartTime.UnixNano() + 1,
|
|
},
|
|
},
|
|
expectedReturnedSwaps: []lntypes.Hash{
|
|
swapOutOrder2.SwapHash,
|
|
},
|
|
expectedNextStartTime: 0,
|
|
},
|
|
{
|
|
name: "fetch with time filter edge case 3",
|
|
mockSwaps: mockSwaps,
|
|
req: &looprpc.ListSwapsRequest{
|
|
ListSwapFilter: &looprpc.ListSwapsFilter{
|
|
StartTimestampNs: secondSwapStartTime.UnixNano() - 1,
|
|
},
|
|
},
|
|
expectedReturnedSwaps: []lntypes.Hash{
|
|
swapOutOrder1.SwapHash,
|
|
swapOutOrder2.SwapHash,
|
|
},
|
|
expectedNextStartTime: 0,
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Create the swap client server with our mock client.
|
|
server := &swapClientServer{
|
|
swaps: make(map[lntypes.Hash]loop.SwapInfo),
|
|
}
|
|
|
|
// Populate the server's swap cache with our mock swaps.
|
|
for _, swap := range test.mockSwaps {
|
|
server.swaps[swap.SwapHash] = swap
|
|
}
|
|
|
|
// Call the ListSwaps method.
|
|
resp, err := server.ListSwaps(context.Background(), test.req)
|
|
require.NoError(t, err)
|
|
|
|
require.Len(
|
|
t,
|
|
resp.Swaps,
|
|
len(test.expectedReturnedSwaps),
|
|
"incorrect returned count",
|
|
)
|
|
|
|
// Check order of returned swaps is exactly as expected.
|
|
for idx, aswap := range resp.Swaps {
|
|
newhash, err := lntypes.MakeHash(aswap.GetIdBytes())
|
|
require.NoError(t, err)
|
|
require.Equal(
|
|
t,
|
|
test.expectedReturnedSwaps[idx],
|
|
newhash,
|
|
"iteration order mismatch",
|
|
)
|
|
}
|
|
|
|
require.Equal(
|
|
t,
|
|
test.expectedNextStartTime,
|
|
resp.NextStartTime,
|
|
"incorrect next start time",
|
|
)
|
|
})
|
|
}
|
|
}
|
|
|
|
// mockAddressStore is a minimal in-memory store for address parameters.
|
|
type mockAddressStore struct {
|
|
params []*script.Parameters
|
|
}
|
|
|
|
func (s *mockAddressStore) CreateStaticAddress(_ context.Context,
|
|
p *script.Parameters) error {
|
|
|
|
if p.ID == 0 {
|
|
p.ID = int32(len(s.params) + 1)
|
|
}
|
|
s.params = append(s.params, p)
|
|
return nil
|
|
}
|
|
|
|
func (s *mockAddressStore) GetStaticAddressID(_ context.Context,
|
|
pkScript []byte) (int32, error) {
|
|
|
|
for _, p := range s.params {
|
|
if bytes.Equal(p.PkScript, pkScript) {
|
|
return p.ID, nil
|
|
}
|
|
}
|
|
|
|
return 0, sql.ErrNoRows
|
|
}
|
|
|
|
func (s *mockAddressStore) GetStaticAddress(_ context.Context, _ []byte) (
|
|
*script.Parameters, error) {
|
|
|
|
if len(s.params) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
return s.params[0], nil
|
|
}
|
|
|
|
func (s *mockAddressStore) GetAllStaticAddresses(_ context.Context) (
|
|
[]*script.Parameters, error) {
|
|
|
|
return s.params, nil
|
|
}
|
|
|
|
func (s *mockAddressStore) GetLegacyParameters(_ context.Context) (
|
|
*address.Parameters, error) {
|
|
|
|
if len(s.params) == 0 {
|
|
return nil, sql.ErrNoRows
|
|
}
|
|
|
|
return s.params[0], nil
|
|
}
|
|
|
|
// mockDepositStore implements deposit.Store minimally for DepositsForOutpoints.
|
|
type mockDepositStore struct {
|
|
byOutpoint map[string]*deposit.Deposit
|
|
}
|
|
|
|
func (s *mockDepositStore) CreateDeposit(_ context.Context,
|
|
_ *deposit.Deposit) error {
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *mockDepositStore) UpdateDeposit(_ context.Context,
|
|
_ *deposit.Deposit) error {
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *mockDepositStore) GetDeposit(_ context.Context,
|
|
_ deposit.ID) (*deposit.Deposit, error) {
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
func (s *mockDepositStore) DepositForOutpoint(_ context.Context,
|
|
outpoint string) (*deposit.Deposit, error) {
|
|
|
|
if d, ok := s.byOutpoint[outpoint]; ok {
|
|
return d, nil
|
|
}
|
|
return nil, deposit.ErrDepositNotFound
|
|
}
|
|
|
|
func (s *mockDepositStore) AllDeposits(_ context.Context) ([]*deposit.Deposit,
|
|
error) {
|
|
|
|
deposits := make([]*deposit.Deposit, 0, len(s.byOutpoint))
|
|
for _, d := range s.byOutpoint {
|
|
deposits = append(deposits, d)
|
|
}
|
|
|
|
return deposits, nil
|
|
}
|
|
|
|
// listUnspentDepositManager backs ListUnspentDeposits tests without requiring
|
|
// the full deposit manager event loop.
|
|
type listUnspentDepositManager struct {
|
|
byOutpoint map[string]*deposit.Deposit
|
|
|
|
ensureDepositsFreshCalls int
|
|
onEnsureDepositsFresh func(*listUnspentDepositManager)
|
|
}
|
|
|
|
func (m *listUnspentDepositManager) EnsureDepositsFresh(
|
|
context.Context) error {
|
|
|
|
m.ensureDepositsFreshCalls++
|
|
if m.onEnsureDepositsFresh != nil {
|
|
m.onEnsureDepositsFresh(m)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (m *listUnspentDepositManager) GetActiveDepositsInState(
|
|
state fsm.StateType) ([]*deposit.Deposit, error) {
|
|
|
|
deposits := make([]*deposit.Deposit, 0, len(m.byOutpoint))
|
|
for _, d := range m.byOutpoint {
|
|
if !d.IsInState(state) {
|
|
continue
|
|
}
|
|
|
|
deposits = append(deposits, d)
|
|
}
|
|
|
|
return deposits, nil
|
|
}
|
|
|
|
func (m *listUnspentDepositManager) DepositsForOutpoints(
|
|
_ context.Context, outpoints []string, ignoreUnknown bool) (
|
|
[]*deposit.Deposit, error) {
|
|
|
|
deposits := make([]*deposit.Deposit, 0, len(outpoints))
|
|
seen := make(map[string]struct{}, len(outpoints))
|
|
for i, outpoint := range outpoints {
|
|
if _, ok := seen[outpoint]; ok {
|
|
return nil, fmt.Errorf("duplicate outpoint %s "+
|
|
"at index %d", outpoint, i)
|
|
}
|
|
seen[outpoint] = struct{}{}
|
|
|
|
d, ok := m.byOutpoint[outpoint]
|
|
if !ok {
|
|
if ignoreUnknown {
|
|
continue
|
|
}
|
|
|
|
return nil, deposit.ErrDepositNotFound
|
|
}
|
|
|
|
deposits = append(deposits, d)
|
|
}
|
|
|
|
return deposits, nil
|
|
}
|
|
|
|
func (m *listUnspentDepositManager) GetVisibleDeposits(
|
|
context.Context) ([]*deposit.Deposit, error) {
|
|
|
|
return m.allDeposits(), nil
|
|
}
|
|
|
|
func (m *listUnspentDepositManager) GetAllDeposits(
|
|
context.Context) ([]*deposit.Deposit, error) {
|
|
|
|
return m.allDeposits(), nil
|
|
}
|
|
|
|
func (m *listUnspentDepositManager) allDeposits() []*deposit.Deposit {
|
|
deposits := make([]*deposit.Deposit, 0, len(m.byOutpoint))
|
|
for _, d := range m.byOutpoint {
|
|
deposits = append(deposits, d)
|
|
}
|
|
|
|
return deposits
|
|
}
|
|
|
|
// TestListUnspentDeposits tests filtering behavior of ListUnspentDeposits.
|
|
func TestListUnspentDeposits(t *testing.T) {
|
|
ctx := context.Background()
|
|
mock := mock_lnd.NewMockLnd()
|
|
|
|
// Prepare a single static address parameter set.
|
|
_, client := mock_lnd.CreateKey(1)
|
|
_, server := mock_lnd.CreateKey(2)
|
|
staticAddress, err := script.NewStaticAddress(
|
|
input.MuSig2Version100RC2, 10, client, server,
|
|
)
|
|
require.NoError(t, err)
|
|
pkScript, err := staticAddress.StaticAddressScript()
|
|
require.NoError(t, err)
|
|
addrParams := &script.Parameters{
|
|
ClientPubkey: client,
|
|
ServerPubkey: server,
|
|
Expiry: 10,
|
|
PkScript: pkScript,
|
|
}
|
|
|
|
addrStore := &mockAddressStore{params: []*script.Parameters{addrParams}}
|
|
|
|
// Build an address manager using our mock lnd and fake address store.
|
|
addrMgr, err := address.NewManager(&address.ManagerConfig{
|
|
Store: addrStore,
|
|
WalletKit: mock.WalletKit,
|
|
ChainParams: mock.ChainParams,
|
|
// ChainNotifier and AddressClient are not needed for this test.
|
|
}, 1)
|
|
require.NoError(t, err)
|
|
_, err = addrMgr.EnsureStaticAddressSeed(ctx)
|
|
require.NoError(t, err)
|
|
|
|
// Construct several UTXOs with different confirmation counts.
|
|
makeUtxo := func(idx uint32, confs int64) *lnwallet.Utxo {
|
|
return &lnwallet.Utxo{
|
|
AddressType: lnwallet.TaprootPubkey,
|
|
Value: btcutil.Amount(250_000 + int64(idx)),
|
|
Confirmations: confs,
|
|
PkScript: pkScript,
|
|
OutPoint: wire.OutPoint{
|
|
Hash: chainhash.Hash{byte(idx + 1)},
|
|
Index: idx,
|
|
},
|
|
}
|
|
}
|
|
|
|
utxoUnknown := makeUtxo(0, 0)
|
|
utxoDeposited := makeUtxo(1, 1)
|
|
utxoWithdrawn := makeUtxo(2, 2)
|
|
utxoLoopingIn := makeUtxo(3, 5)
|
|
utxoConfirmedUnknown := makeUtxo(4, 3)
|
|
|
|
// Helper to build the deposit manager with specific states.
|
|
buildDepositMgr := func(
|
|
states map[wire.OutPoint]fsm.StateType) *listUnspentDepositManager {
|
|
|
|
depMgr := &listUnspentDepositManager{
|
|
byOutpoint: make(map[string]*deposit.Deposit),
|
|
}
|
|
for op, state := range states {
|
|
d := &deposit.Deposit{OutPoint: op}
|
|
d.SetState(state)
|
|
depMgr.byOutpoint[op.String()] = d
|
|
}
|
|
|
|
return depMgr
|
|
}
|
|
|
|
// Only known Deposited records are available. Unknown deposits and
|
|
// known non-Deposited states are excluded.
|
|
t.Run("only known Deposited included",
|
|
func(t *testing.T) {
|
|
mock.SetListUnspent([]*lnwallet.Utxo{
|
|
utxoUnknown, utxoDeposited, utxoWithdrawn,
|
|
utxoLoopingIn,
|
|
})
|
|
|
|
depMgr := buildDepositMgr(map[wire.OutPoint]fsm.StateType{
|
|
utxoDeposited.OutPoint: deposit.Deposited,
|
|
utxoWithdrawn.OutPoint: deposit.Withdrawn,
|
|
utxoLoopingIn.OutPoint: deposit.LoopingIn,
|
|
})
|
|
|
|
server := &swapClientServer{
|
|
staticAddressManager: addrMgr,
|
|
depositManager: depMgr,
|
|
}
|
|
|
|
resp, err := server.ListUnspentDeposits(
|
|
ctx, &looprpc.ListUnspentDepositsRequest{},
|
|
)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 1, depMgr.ensureDepositsFreshCalls)
|
|
|
|
// Expect the Deposited utxo only.
|
|
require.Len(t, resp.Utxos, 1)
|
|
got := map[string]struct{}{}
|
|
for _, u := range resp.Utxos {
|
|
got[u.Outpoint] = struct{}{}
|
|
// Confirm address string is non-empty and the
|
|
// same across utxos.
|
|
require.NotEmpty(t, u.StaticAddress)
|
|
}
|
|
_, ok := got[utxoDeposited.OutPoint.String()]
|
|
require.True(t, ok)
|
|
})
|
|
|
|
// Confirmation depth no longer changes availability; state does.
|
|
t.Run("availability ignores conf depth once deposit state is known",
|
|
func(t *testing.T) {
|
|
mock.SetListUnspent(
|
|
[]*lnwallet.Utxo{
|
|
utxoUnknown, utxoDeposited,
|
|
utxoWithdrawn, utxoLoopingIn,
|
|
})
|
|
|
|
depMgr := buildDepositMgr(map[wire.OutPoint]fsm.StateType{
|
|
utxoDeposited.OutPoint: deposit.Deposited,
|
|
utxoWithdrawn.OutPoint: deposit.Withdrawn,
|
|
utxoLoopingIn.OutPoint: deposit.LoopingIn,
|
|
})
|
|
|
|
server := &swapClientServer{
|
|
staticAddressManager: addrMgr,
|
|
depositManager: depMgr,
|
|
}
|
|
|
|
resp, err := server.ListUnspentDeposits(
|
|
ctx, &looprpc.ListUnspentDepositsRequest{},
|
|
)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 1, depMgr.ensureDepositsFreshCalls)
|
|
|
|
require.Len(t, resp.Utxos, 1)
|
|
got := map[string]struct{}{}
|
|
for _, u := range resp.Utxos {
|
|
got[u.Outpoint] = struct{}{}
|
|
}
|
|
_, ok := got[utxoDeposited.OutPoint.String()]
|
|
require.True(t, ok)
|
|
})
|
|
|
|
// A wallet-visible UTXO reconciled by EnsureDepositsFresh should be
|
|
// returned in the same ListUnspentDeposits call.
|
|
t.Run("freshly reconciled wallet utxo is included", func(t *testing.T) {
|
|
mock.SetListUnspent([]*lnwallet.Utxo{utxoConfirmedUnknown})
|
|
|
|
depMgr := buildDepositMgr(map[wire.OutPoint]fsm.StateType{})
|
|
depMgr.onEnsureDepositsFresh = func(
|
|
m *listUnspentDepositManager) {
|
|
|
|
d := &deposit.Deposit{
|
|
OutPoint: utxoConfirmedUnknown.OutPoint,
|
|
}
|
|
d.SetState(deposit.Deposited)
|
|
m.byOutpoint[d.OutPoint.String()] = d
|
|
}
|
|
|
|
server := &swapClientServer{
|
|
staticAddressManager: addrMgr,
|
|
depositManager: depMgr,
|
|
}
|
|
|
|
resp, err := server.ListUnspentDeposits(
|
|
ctx, &looprpc.ListUnspentDepositsRequest{},
|
|
)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 1, depMgr.ensureDepositsFreshCalls)
|
|
|
|
require.Len(t, resp.Utxos, 1)
|
|
require.Equal(
|
|
t, utxoConfirmedUnknown.OutPoint.String(),
|
|
resp.Utxos[0].Outpoint,
|
|
)
|
|
})
|
|
}
|