Skip to content

Commit 0a42561

Browse files
committed
staticaddr: support dynamic deposit confirmation requirements
Reduce MinConfs from 6 to 3 to allow faster swap attempts while the server enforces risk-based confirmation requirements. Update SelectDeposits to prioritize more-confirmed deposits first, increasing the likelihood of server acceptance. Add client-side logging of insufficient confirmation details from server error responses.
1 parent f46a2c4 commit 0a42561

4 files changed

Lines changed: 109 additions & 24 deletions

File tree

staticaddr/deposit/manager.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ const (
2323
// MinConfs is the minimum number of confirmations we require for a
2424
// deposit to be considered available for loop-ins, coop-spends and
2525
// timeouts.
26-
MinConfs = 6
26+
MinConfs = 3
2727

2828
// MaxConfs is unset since we don't require a max number of
2929
// confirmations for deposits.

staticaddr/loopin/actions.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import (
3030
"github.com/lightningnetwork/lnd/lnwallet"
3131
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
3232
"github.com/lightningnetwork/lnd/lnwire"
33+
"google.golang.org/grpc/status"
3334
)
3435

3536
const (
@@ -146,6 +147,10 @@ func (f *FSM) InitHtlcAction(ctx context.Context,
146147
ctx, loopInReq,
147148
)
148149
if err != nil {
150+
// Check if this is an insufficient confirmations error and log
151+
// the details to help the user understand what's needed.
152+
logInsufficientConfirmationsDetails(err)
153+
149154
err = fmt.Errorf("unable to initiate the loop-in with the "+
150155
"server: %w", err)
151156

@@ -909,3 +914,31 @@ func byteSliceTo66ByteSlice(b []byte) ([musig2.PubNonceSize]byte, error) {
909914

910915
return res, nil
911916
}
917+
918+
// logInsufficientConfirmationsDetails extracts and logs the per-deposit
919+
// confirmation details from a gRPC error if present.
920+
func logInsufficientConfirmationsDetails(err error) {
921+
st, ok := status.FromError(err)
922+
if !ok {
923+
return
924+
}
925+
926+
for _, detail := range st.Details() {
927+
confDetails, ok :=
928+
detail.(*swapserverrpc.InsufficientConfirmationsDetails)
929+
930+
if !ok {
931+
continue
932+
}
933+
934+
log.Warnf("Insufficient deposit confirmations, max wait: "+
935+
"%d blocks", confDetails.MaxBlocksToWait)
936+
937+
for _, dep := range confDetails.Deposits {
938+
log.Warnf(" Deposit %s: %d/%d confirmations "+
939+
"(need %d more blocks)",
940+
dep.Outpoint, dep.CurrentConfirmations,
941+
dep.RequiredConfirmations, dep.BlocksToWait)
942+
}
943+
}
944+
}

staticaddr/loopin/manager.go

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -865,11 +865,13 @@ func (m *Manager) GetAllSwaps(ctx context.Context) ([]*StaticAddressLoopIn,
865865
return swaps, nil
866866
}
867867

868-
// SelectDeposits sorts the deposits by amount in descending order, then by
869-
// blocks-until-expiry in ascending order. It then selects the deposits that
870-
// are needed to cover the amount requested without leaving a dust change. It
871-
// returns an error if the sum of deposits minus dust is less than the requested
872-
// amount.
868+
// SelectDeposits sorts the deposits to optimize for successful swaps with
869+
// dynamic confirmation requirements: 1) more confirmations first (higher chance
870+
// of server acceptance), 2) larger amounts first (to minimize number of deposits
871+
// used), 3) expiring sooner first (to use time-sensitive deposits). It then
872+
// selects the deposits that are needed to cover the amount requested without
873+
// leaving a dust change. It returns an error if the sum of deposits minus dust
874+
// is less than the requested amount.
873875
func SelectDeposits(targetAmount btcutil.Amount,
874876
unfilteredDeposits []*deposit.Deposit, csvExpiry uint32,
875877
blockHeight uint32) ([]*deposit.Deposit, error) {
@@ -890,18 +892,31 @@ func SelectDeposits(targetAmount btcutil.Amount,
890892
deposits = append(deposits, d)
891893
}
892894

893-
// Sort the deposits by amount in descending order, then by
894-
// blocks-until-expiry in ascending order.
895+
// Sort deposits to optimize for successful swaps with dynamic
896+
// confirmation requirements:
897+
// 1. More confirmations first (higher chance of server acceptance)
898+
// 2. Larger amounts first (to minimize number of deposits used)
899+
// 3. Expiring sooner first (to use time-sensitive deposits)
895900
sort.Slice(deposits, func(i, j int) bool {
896-
if deposits[i].Value == deposits[j].Value {
897-
iExp := uint32(deposits[i].ConfirmationHeight) +
898-
csvExpiry - blockHeight
899-
jExp := uint32(deposits[j].ConfirmationHeight) +
900-
csvExpiry - blockHeight
901+
// Primary: more confirmations first.
902+
iConfs := blockHeight - uint32(deposits[i].ConfirmationHeight)
903+
jConfs := blockHeight - uint32(deposits[j].ConfirmationHeight)
904+
if iConfs != jConfs {
905+
return iConfs > jConfs
906+
}
901907

902-
return iExp < jExp
908+
// Secondary: larger amounts first.
909+
if deposits[i].Value != deposits[j].Value {
910+
return deposits[i].Value > deposits[j].Value
903911
}
904-
return deposits[i].Value > deposits[j].Value
912+
913+
// Tertiary: expiring sooner first.
914+
iExp := uint32(deposits[i].ConfirmationHeight) +
915+
csvExpiry - blockHeight
916+
jExp := uint32(deposits[j].ConfirmationHeight) +
917+
csvExpiry - blockHeight
918+
919+
return iExp < jExp
905920
})
906921

907922
// Select the deposits that are needed to cover the swap amount without

staticaddr/loopin/manager_test.go

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,14 @@ type testCase struct {
2626

2727
// TestSelectDeposits tests the selectDeposits function, which selects
2828
// deposits that can cover a target value while respecting the dust limit.
29+
// Sorting priority: 1) more confirmations first, 2) larger amounts first,
30+
// 3) expiring sooner first.
2931
func TestSelectDeposits(t *testing.T) {
32+
// Note: confirmations = blockHeight - ConfirmationHeight
33+
// Lower ConfirmationHeight means more confirmations at a given block.
3034
d1, d2, d3, d4 := &deposit.Deposit{
3135
Value: 1_000_000,
32-
ConfirmationHeight: 5_000,
36+
ConfirmationHeight: 5_000, // most confs at height 5100
3337
}, &deposit.Deposit{
3438
Value: 2_000_000,
3539
ConfirmationHeight: 5_001,
@@ -38,7 +42,7 @@ func TestSelectDeposits(t *testing.T) {
3842
ConfirmationHeight: 5_002,
3943
}, &deposit.Deposit{
4044
Value: 3_000_000,
41-
ConfirmationHeight: 5_003,
45+
ConfirmationHeight: 5_003, // fewest confs at height 5100
4246
}
4347
d1.Hash = chainhash.Hash{1}
4448
d1.Index = 0
@@ -49,75 +53,108 @@ func TestSelectDeposits(t *testing.T) {
4953
d4.Hash = chainhash.Hash{4}
5054
d4.Index = 0
5155

56+
// Use a realistic block height and csv expiry for all standard
57+
// test cases. csvExpiry must be large enough that deposits remain
58+
// swappable at this block height.
59+
const (
60+
testBlockHeight uint32 = 5_100
61+
testCsvExpiry uint32 = 2_500
62+
)
63+
5264
testCases := []testCase{
5365
{
5466
name: "single deposit exact target",
5567
deposits: []*deposit.Deposit{d1},
5668
targetValue: 1_000_000,
69+
csvExpiry: testCsvExpiry,
70+
blockHeight: testBlockHeight,
5771
expected: []*deposit.Deposit{d1},
5872
expectedErr: "",
5973
},
6074
{
61-
name: "prefer larger deposit when both cover",
75+
// d1 has more confirmations, so it's preferred even
76+
// though d2 is larger.
77+
name: "prefer more confirmed deposit over larger",
6278
deposits: []*deposit.Deposit{d1, d2},
6379
targetValue: 1_000_000,
64-
expected: []*deposit.Deposit{d2},
80+
csvExpiry: testCsvExpiry,
81+
blockHeight: testBlockHeight,
82+
expected: []*deposit.Deposit{d1},
6583
expectedErr: "",
6684
},
6785
{
68-
name: "prefer largest among three when one is enough",
86+
// d1 has the most confirmations among d1, d2, d3.
87+
name: "prefer most confirmed among three",
6988
deposits: []*deposit.Deposit{d1, d2, d3},
7089
targetValue: 1_000_000,
71-
expected: []*deposit.Deposit{d3},
90+
csvExpiry: testCsvExpiry,
91+
blockHeight: testBlockHeight,
92+
expected: []*deposit.Deposit{d1},
7293
expectedErr: "",
7394
},
7495
{
7596
name: "single deposit insufficient by 1",
7697
deposits: []*deposit.Deposit{d1},
7798
targetValue: 1_000_001,
99+
csvExpiry: testCsvExpiry,
100+
blockHeight: testBlockHeight,
78101
expected: []*deposit.Deposit{},
79102
expectedErr: "not enough deposits to cover",
80103
},
81104
{
82105
name: "target leaves exact dust limit change",
83106
deposits: []*deposit.Deposit{d1},
84107
targetValue: 1_000_000 - dustLimit,
108+
csvExpiry: testCsvExpiry,
109+
blockHeight: testBlockHeight,
85110
expected: []*deposit.Deposit{d1},
86111
expectedErr: "",
87112
},
88113
{
89114
name: "target leaves dust change (just over)",
90115
deposits: []*deposit.Deposit{d1},
91116
targetValue: 1_000_000 - dustLimit + 1,
117+
csvExpiry: testCsvExpiry,
118+
blockHeight: testBlockHeight,
92119
expected: []*deposit.Deposit{},
93120
expectedErr: "not enough deposits to cover",
94121
},
95122
{
96123
name: "all deposits exactly match target",
97124
deposits: []*deposit.Deposit{d1, d2, d3},
98125
targetValue: d1.Value + d2.Value + d3.Value,
126+
csvExpiry: testCsvExpiry,
127+
blockHeight: testBlockHeight,
99128
expected: []*deposit.Deposit{d1, d2, d3},
100129
expectedErr: "",
101130
},
102131
{
103132
name: "sum minus dust limit is allowed (change == dust)",
104133
deposits: []*deposit.Deposit{d1, d2, d3},
105134
targetValue: d1.Value + d2.Value + d3.Value - dustLimit,
135+
csvExpiry: testCsvExpiry,
136+
blockHeight: testBlockHeight,
106137
expected: []*deposit.Deposit{d1, d2, d3},
107138
expectedErr: "",
108139
},
109140
{
110141
name: "sum minus dust limit plus 1 is not allowed (dust change)",
111142
deposits: []*deposit.Deposit{d1, d2, d3},
112143
targetValue: d1.Value + d2.Value + d3.Value - dustLimit + 1,
144+
csvExpiry: testCsvExpiry,
145+
blockHeight: testBlockHeight,
113146
expected: []*deposit.Deposit{},
114147
expectedErr: "not enough deposits to cover",
115148
},
116149
{
117-
name: "tie by value, prefer earlier expiry",
150+
// d3 and d4 have the same value but d3 has more
151+
// confirmations (lower ConfirmationHeight), so it
152+
// wins at the primary sort level.
153+
name: "same value, prefer more confirmed",
118154
deposits: []*deposit.Deposit{d3, d4},
119-
targetValue: d4.Value - dustLimit, // d3/d4 have the
120-
// same value but different expiration.
155+
targetValue: d4.Value - dustLimit,
156+
csvExpiry: testCsvExpiry,
157+
blockHeight: testBlockHeight,
121158
expected: []*deposit.Deposit{d3},
122159
expectedErr: "",
123160
},

0 commit comments

Comments
 (0)