loop/staticaddr/loopin/manager_test.go
0xfandom 8b89f77529 staticaddr: move address.Parameters to script package
The Parameters struct describes the keys, expiry and pkScript that
define the static address script, so its natural home is the script
package. Moving it there lets staticutil drop its dependency on the
address package and lets callers reuse a single type alongside
script.StaticAddress and script.NewStaticAddress.

No behavior change.

Closes #1056
2026-05-01 15:54:37 +05:30

539 lines
14 KiB
Go

package loopin
import (
"context"
"testing"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/loop/fsm"
"github.com/lightninglabs/loop/staticaddr/deposit"
"github.com/lightninglabs/loop/staticaddr/script"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/stretchr/testify/require"
)
type testCase struct {
name string
deposits []*deposit.Deposit
targetValue btcutil.Amount
csvExpiry uint32
blockHeight uint32
expected []*deposit.Deposit
expectedErr string
}
// TestSelectDeposits tests the selectDeposits function, which selects
// deposits that can cover a target value while respecting the dust limit.
func TestSelectDeposits(t *testing.T) {
d1, d2, d3, d4 := &deposit.Deposit{
Value: 1_000_000,
ConfirmationHeight: 5_000,
}, &deposit.Deposit{
Value: 2_000_000,
ConfirmationHeight: 5_001,
}, &deposit.Deposit{
Value: 3_000_000,
ConfirmationHeight: 5_002,
}, &deposit.Deposit{
Value: 3_000_000,
ConfirmationHeight: 5_003,
}
d1.Hash = chainhash.Hash{1}
d1.Index = 0
d2.Hash = chainhash.Hash{2}
d2.Index = 0
d3.Hash = chainhash.Hash{3}
d3.Index = 0
d4.Hash = chainhash.Hash{4}
d4.Index = 0
testCases := []testCase{
{
name: "single deposit exact target",
deposits: []*deposit.Deposit{d1},
targetValue: 1_000_000,
expected: []*deposit.Deposit{d1},
expectedErr: "",
},
{
name: "prefer larger deposit when both cover",
deposits: []*deposit.Deposit{d1, d2},
targetValue: 1_000_000,
expected: []*deposit.Deposit{d2},
expectedErr: "",
},
{
name: "prefer largest among three when one is enough",
deposits: []*deposit.Deposit{d1, d2, d3},
targetValue: 1_000_000,
expected: []*deposit.Deposit{d3},
expectedErr: "",
},
{
name: "single deposit insufficient by 1",
deposits: []*deposit.Deposit{d1},
targetValue: 1_000_001,
expected: []*deposit.Deposit{},
expectedErr: "not enough deposits to cover",
},
{
name: "target leaves exact dust limit change",
deposits: []*deposit.Deposit{d1},
targetValue: 1_000_000 - dustLimit,
expected: []*deposit.Deposit{d1},
expectedErr: "",
},
{
name: "target leaves dust change (just over)",
deposits: []*deposit.Deposit{d1},
targetValue: 1_000_000 - dustLimit + 1,
expected: []*deposit.Deposit{},
expectedErr: "not enough deposits to cover",
},
{
name: "all deposits exactly match target",
deposits: []*deposit.Deposit{d1, d2, d3},
targetValue: d1.Value + d2.Value + d3.Value,
expected: []*deposit.Deposit{d1, d2, d3},
expectedErr: "",
},
{
name: "sum minus dust limit is allowed (change == dust)",
deposits: []*deposit.Deposit{d1, d2, d3},
targetValue: d1.Value + d2.Value + d3.Value - dustLimit,
expected: []*deposit.Deposit{d1, d2, d3},
expectedErr: "",
},
{
name: "sum minus dust limit plus 1 is not allowed (dust change)",
deposits: []*deposit.Deposit{d1, d2, d3},
targetValue: d1.Value + d2.Value + d3.Value - dustLimit + 1,
expected: []*deposit.Deposit{},
expectedErr: "not enough deposits to cover",
},
{
name: "tie by value, prefer earlier expiry",
deposits: []*deposit.Deposit{d3, d4},
targetValue: d4.Value - dustLimit, // d3/d4 have the
// same value but different expiration.
expected: []*deposit.Deposit{d3},
expectedErr: "",
},
{
name: "prefilter filters deposits close to expiry",
deposits: func() []*deposit.Deposit {
// dClose expires before
// htlcExpiry+DepositHtlcDelta and must be
// filtered out. dOK expires exactly at the
// threshold and must be eligible.
dClose := &deposit.Deposit{
Value: 3_000_000,
ConfirmationHeight: 3000,
}
dClose.Hash = chainhash.Hash{5}
dClose.Index = 0
dOK := &deposit.Deposit{
Value: 2_000_000,
ConfirmationHeight: 3050,
}
dOK.Hash = chainhash.Hash{6}
dOK.Index = 0
return []*deposit.Deposit{dClose, dOK}
}(),
targetValue: 1_000_000,
csvExpiry: 1000,
blockHeight: 3000,
expected: func() []*deposit.Deposit {
// Only dOK should be considered.
// dClose is filtered.
dOK := &deposit.Deposit{
Value: 2_000_000,
ConfirmationHeight: 3050,
}
dOK.Hash = chainhash.Hash{6}
dOK.Index = 0
return []*deposit.Deposit{dOK}
}(),
expectedErr: "",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
selectedDeposits, err := SelectDeposits(
tc.targetValue, tc.deposits, tc.csvExpiry,
tc.blockHeight,
)
if tc.expectedErr == "" {
require.NoError(t, err)
} else {
require.ErrorContains(t, err, tc.expectedErr)
}
require.ElementsMatch(t, tc.expected, selectedDeposits)
})
}
}
// mockDepositManager implements DepositManager for tests.
type mockDepositManager struct {
byOutpoint map[string]*deposit.Deposit
}
func (m *mockDepositManager) GetAllDeposits(_ context.Context) (
[]*deposit.Deposit, error) {
return nil, nil
}
func (m *mockDepositManager) AllStringOutpointsActiveDeposits(_ []string,
_ fsm.StateType) ([]*deposit.Deposit, bool) {
return nil, false
}
func (m *mockDepositManager) TransitionDeposits(_ context.Context,
_ []*deposit.Deposit, _ fsm.EventType, _ fsm.StateType) error {
return nil
}
func (m *mockDepositManager) DepositsForOutpoints(_ context.Context,
outpoints []string, ignoreUnknown bool) ([]*deposit.Deposit, error) {
res := make([]*deposit.Deposit, 0, len(outpoints))
for _, op := range outpoints {
if d, ok := m.byOutpoint[op]; ok {
res = append(res, d)
}
}
return res, nil
}
func (m *mockDepositManager) GetActiveDepositsInState(_ fsm.StateType) (
[]*deposit.Deposit, error) {
return nil, nil
}
// mockStore implements StaticAddressLoopInStore for tests.
type mockStore struct {
loopIns map[lntypes.Hash]*StaticAddressLoopIn
mapIDs map[lntypes.Hash][]deposit.ID
}
func (s *mockStore) CreateLoopIn(_ context.Context,
_ *StaticAddressLoopIn) error {
return nil
}
func (s *mockStore) UpdateLoopIn(_ context.Context,
_ *StaticAddressLoopIn) error {
return nil
}
func (s *mockStore) GetStaticAddressLoopInSwapsByStates(_ context.Context,
_ []fsm.StateType) ([]*StaticAddressLoopIn, error) {
return nil, nil
}
func (s *mockStore) IsStored(_ context.Context, _ lntypes.Hash) (bool, error) {
return false, nil
}
func (s *mockStore) GetLoopInByHash(_ context.Context,
swapHash lntypes.Hash) (*StaticAddressLoopIn, error) {
li, ok := s.loopIns[swapHash]
if !ok {
return nil, nil
}
return li, nil
}
func (s *mockStore) SwapHashesForDepositIDs(_ context.Context,
depositIDs []deposit.ID) (map[lntypes.Hash][]deposit.ID, error) {
// Filter the prepared mapping to only include hashes that reference
// any of the provided deposit IDs.
idSet := make(map[deposit.ID]struct{}, len(depositIDs))
for _, id := range depositIDs {
idSet[id] = struct{}{}
}
res := make(map[lntypes.Hash][]deposit.ID)
for h, ids := range s.mapIDs {
for _, id := range ids {
if _, ok := idSet[id]; ok {
res[h] = ids
break
}
}
}
return res, nil
}
// helper to create a deposit with specific outpoint and value.
func makeDeposit(h byte, index uint32, value btcutil.Amount) *deposit.Deposit {
d := &deposit.Deposit{Value: value}
d.Hash = chainhash.Hash{h}
d.Index = index
var id deposit.ID
id[0] = h
d.ID = id
return d
}
// helper to outpoint string as used by txin.PreviousOutPoint.String().
func outpointString(d *deposit.Deposit) string {
return wire.OutPoint{Hash: d.Hash, Index: d.Index}.String()
}
// build a sweep tx with given inputs and outputs.
func makeSweepTx(inputs []wire.OutPoint, outputs []*wire.TxOut) *wire.MsgTx {
tx := wire.NewMsgTx(2)
for _, in := range inputs {
tx.AddTxIn(&wire.TxIn{PreviousOutPoint: in})
}
for _, out := range outputs {
tx.AddTxOut(out)
}
return tx
}
// TestCheckChange exercises all relevant scenarios for checkChange.
func TestCheckChange(t *testing.T) {
ctx := context.Background()
// Prepare a common change address and an alternate address.
changeAddr := &script.Parameters{PkScript: []byte{0xaa, 0xbb}}
otherAddr := &script.Parameters{PkScript: []byte{0xcc, 0xdd}}
serverAddr := &script.Parameters{PkScript: []byte{0xee, 0xff}}
// Prepare swaps (loop-ins) with varying deposit totals and selections.
// Helper to make a swap with deposits and selected amount.
makeSwap := func(h byte, deposits []*deposit.Deposit,
selected btcutil.Amount) (lntypes.Hash, *StaticAddressLoopIn) {
var hash lntypes.Hash
hash[0] = h
li := &StaticAddressLoopIn{
Deposits: deposits,
SelectedAmount: selected,
AddressParams: changeAddr,
}
return hash, li
}
// Deposits belonging to different swaps.
s1d1 := makeDeposit(1, 0, 1000)
s1d2 := makeDeposit(1, 1, 2000)
s2d1 := makeDeposit(2, 0, 1500)
s3d1 := makeDeposit(3, 0, 800)
s4d1 := makeDeposit(4, 0, 900)
// Swaps:
// A: total 3000, selected 3000 => no change.
hA, liA := makeSwap(10, []*deposit.Deposit{s1d1, s1d2}, 3000)
// B: total 1500, selected 1000 => change 500.
hB, liB := makeSwap(11, []*deposit.Deposit{s2d1}, 1000)
// C: total 800, selected 400 => change 400.
hC, liC := makeSwap(12, []*deposit.Deposit{s3d1}, 400)
// D: total 900, selected 500 => change 400.
hD, liD := makeSwap(13, []*deposit.Deposit{s4d1}, 500)
// Mapping deposits -> swaps (by deposit IDs).
mapIDs := map[lntypes.Hash][]deposit.ID{
hA: {s1d1.ID, s1d2.ID},
hB: {s2d1.ID},
hC: {s3d1.ID},
hD: {s4d1.ID},
}
loopIns := map[lntypes.Hash]*StaticAddressLoopIn{
hA: liA,
hB: liB,
hC: liC,
hD: liD,
}
// Common manager with mocked dependencies; will change inputs per test.
mgr := &Manager{
cfg: &Config{
DepositManager: &mockDepositManager{
byOutpoint: map[string]*deposit.Deposit{},
},
Store: &mockStore{
loopIns: loopIns,
mapIDs: mapIDs,
},
},
}
type testCase struct {
name string
inDeps []*deposit.Deposit // deposits referenced by tx inputs
outputs []*wire.TxOut // outputs in sweep tx
addr *script.Parameters
expectErr bool
expectedErrMsg string
}
cases := []testCase{
{
name: "no change expected (selected == total)",
inDeps: []*deposit.Deposit{s1d1, s1d2},
// No change output required.
outputs: []*wire.TxOut{
{
Value: 1337,
PkScript: serverAddr.PkScript,
},
},
addr: changeAddr,
},
{
name: "single swap change present",
inDeps: []*deposit.Deposit{s2d1}, // B -> change 500
outputs: []*wire.TxOut{
{
Value: 1337,
PkScript: serverAddr.PkScript,
},
{
Value: 500,
PkScript: changeAddr.PkScript,
},
},
addr: changeAddr,
},
{
name: "multiple swaps different change amounts",
inDeps: []*deposit.Deposit{s2d1, s3d1}, // B(500)+C(400)=900
outputs: []*wire.TxOut{
{
Value: 1337,
PkScript: serverAddr.PkScript,
},
{
Value: 900,
PkScript: changeAddr.PkScript,
},
},
addr: changeAddr,
},
{
name: "two swaps with identical change values sum correctly",
inDeps: []*deposit.Deposit{s3d1, s4d1}, // C(400)+D(400)=800
outputs: []*wire.TxOut{
{
Value: 1337,
PkScript: serverAddr.PkScript,
},
{
Value: 800,
PkScript: changeAddr.PkScript,
},
},
addr: changeAddr,
},
{
name: "missing change output results in error",
inDeps: []*deposit.Deposit{s2d1}, // expect 500
outputs: []*wire.TxOut{},
addr: changeAddr,
expectErr: true,
expectedErrMsg: "couldn't find expected change",
},
{
name: "wrong address for change output",
inDeps: []*deposit.Deposit{s2d1}, // expect 500
outputs: []*wire.TxOut{
{
Value: 1337,
PkScript: serverAddr.PkScript,
},
{
Value: 500,
PkScript: otherAddr.PkScript,
},
},
addr: changeAddr,
expectErr: true,
expectedErrMsg: "couldn't find expected change",
},
{
name: "wrong amount for change output",
inDeps: []*deposit.Deposit{s2d1}, // expect 500
outputs: []*wire.TxOut{
{
Value: 1337,
PkScript: serverAddr.PkScript,
},
{
Value: 400,
PkScript: changeAddr.PkScript,
},
},
addr: changeAddr,
expectErr: true,
expectedErrMsg: "couldn't find expected change",
},
{
name: "mixed swaps some with change some without",
inDeps: []*deposit.Deposit{s1d1, s1d2, s3d1}, // A(0)+C(400)=400
outputs: []*wire.TxOut{
{
Value: 1337,
PkScript: serverAddr.PkScript,
},
{
Value: 400,
PkScript: changeAddr.PkScript,
},
{
Value: 1000,
PkScript: otherAddr.PkScript,
},
},
addr: changeAddr,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
// Prepare inputs mapping for deposit manager.
mdm := &mockDepositManager{
byOutpoint: map[string]*deposit.Deposit{},
}
inputs := make([]wire.OutPoint, 0, len(tc.inDeps))
for _, d := range tc.inDeps {
mdm.byOutpoint[outpointString(d)] = d
inputs = append(
inputs, wire.OutPoint{
Hash: d.Hash,
Index: d.Index,
},
)
}
mgr.cfg.DepositManager = mdm
tx := makeSweepTx(inputs, tc.outputs)
err := mgr.checkChange(ctx, tx, tc.addr)
if tc.expectErr {
require.Error(t, err)
if tc.expectedErrMsg != "" {
require.ErrorContains(
t, err, tc.expectedErrMsg,
)
}
} else {
require.NoError(t, err)
}
})
}
}