Skip to content

Commit 203c29a

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. (cherry picked from commit 66a17f4)
1 parent e2f3511 commit 203c29a

4 files changed

Lines changed: 105 additions & 24 deletions

File tree

staticaddr/deposit/manager.go

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

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

staticaddr/loopin/actions.go

Lines changed: 32 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

@@ -914,3 +919,30 @@ func byteSliceTo66ByteSlice(b []byte) ([musig2.PubNonceSize]byte, error) {
914919

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

staticaddr/loopin/manager.go

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -850,11 +850,13 @@ func (m *Manager) GetAllSwaps(ctx context.Context) ([]*StaticAddressLoopIn,
850850
return swaps, nil
851851
}
852852

853-
// SelectDeposits sorts the deposits by amount in descending order, then by
854-
// blocks-until-expiry in ascending order. It then selects the deposits that
855-
// are needed to cover the amount requested without leaving a dust change. It
856-
// returns an error if the sum of deposits minus dust is less than the requested
857-
// amount.
853+
// SelectDeposits sorts the deposits to optimize for successful swaps with
854+
// dynamic confirmation requirements: 1) more confirmations first (higher chance
855+
// of server acceptance), 2) larger amounts first (to minimize number of deposits
856+
// used). Deposits that are too close to expiry are filtered out before sorting.
857+
// It then selects the deposits that are needed to cover the amount requested
858+
// without leaving a dust change. It returns an error if the sum of deposits
859+
// minus dust is less than the requested amount.
858860
func SelectDeposits(targetAmount btcutil.Amount,
859861
unfilteredDeposits []*deposit.Deposit, csvExpiry uint32,
860862
blockHeight uint32) ([]*deposit.Deposit, error) {
@@ -875,17 +877,28 @@ func SelectDeposits(targetAmount btcutil.Amount,
875877
deposits = append(deposits, d)
876878
}
877879

878-
// Sort the deposits by amount in descending order, then by
879-
// blocks-until-expiry in ascending order.
880+
// Sort deposits to optimize for successful swaps with dynamic
881+
// confirmation requirements:
882+
// 1. More confirmations first (higher chance of server acceptance)
883+
// 2. Larger amounts first (to minimize number of deposits used)
880884
sort.Slice(deposits, func(i, j int) bool {
881-
if deposits[i].Value == deposits[j].Value {
882-
iExp := uint32(deposits[i].ConfirmationHeight) +
883-
csvExpiry - blockHeight
884-
jExp := uint32(deposits[j].ConfirmationHeight) +
885-
csvExpiry - blockHeight
886-
887-
return iExp < jExp
885+
// Primary: more confirmations first. Guard against the
886+
// theoretical case where ConfirmationHeight > blockHeight
887+
// (e.g. during a transient reorg inconsistency).
888+
var iConfs, jConfs uint32
889+
if blockHeight > uint32(deposits[i].ConfirmationHeight) {
890+
iConfs = blockHeight -
891+
uint32(deposits[i].ConfirmationHeight)
892+
}
893+
if blockHeight > uint32(deposits[j].ConfirmationHeight) {
894+
jConfs = blockHeight -
895+
uint32(deposits[j].ConfirmationHeight)
888896
}
897+
if iConfs != jConfs {
898+
return iConfs > jConfs
899+
}
900+
901+
// Secondary: larger amounts first.
889902
return deposits[i].Value > deposits[j].Value
890903
})
891904

staticaddr/loopin/manager_test.go

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,13 @@ 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.
2930
func TestSelectDeposits(t *testing.T) {
31+
// Note: confirmations = blockHeight - ConfirmationHeight
32+
// Lower ConfirmationHeight means more confirmations at a given block.
3033
d1, d2, d3, d4 := &deposit.Deposit{
3134
Value: 1_000_000,
32-
ConfirmationHeight: 5_000,
35+
ConfirmationHeight: 5_000, // most confs at height 5100
3336
}, &deposit.Deposit{
3437
Value: 2_000_000,
3538
ConfirmationHeight: 5_001,
@@ -38,7 +41,7 @@ func TestSelectDeposits(t *testing.T) {
3841
ConfirmationHeight: 5_002,
3942
}, &deposit.Deposit{
4043
Value: 3_000_000,
41-
ConfirmationHeight: 5_003,
44+
ConfirmationHeight: 5_003, // fewest confs at height 5100
4245
}
4346
d1.Hash = chainhash.Hash{1}
4447
d1.Index = 0
@@ -49,75 +52,108 @@ func TestSelectDeposits(t *testing.T) {
4952
d4.Hash = chainhash.Hash{4}
5053
d4.Index = 0
5154

55+
// Use a realistic block height and csv expiry for all standard
56+
// test cases. csvExpiry must be large enough that deposits remain
57+
// swappable at this block height.
58+
const (
59+
testBlockHeight uint32 = 5_100
60+
testCsvExpiry uint32 = 2_500
61+
)
62+
5263
testCases := []testCase{
5364
{
5465
name: "single deposit exact target",
5566
deposits: []*deposit.Deposit{d1},
5667
targetValue: 1_000_000,
68+
csvExpiry: testCsvExpiry,
69+
blockHeight: testBlockHeight,
5770
expected: []*deposit.Deposit{d1},
5871
expectedErr: "",
5972
},
6073
{
61-
name: "prefer larger deposit when both cover",
74+
// d1 has more confirmations, so it's preferred even
75+
// though d2 is larger.
76+
name: "prefer more confirmed deposit over larger",
6277
deposits: []*deposit.Deposit{d1, d2},
6378
targetValue: 1_000_000,
64-
expected: []*deposit.Deposit{d2},
79+
csvExpiry: testCsvExpiry,
80+
blockHeight: testBlockHeight,
81+
expected: []*deposit.Deposit{d1},
6582
expectedErr: "",
6683
},
6784
{
68-
name: "prefer largest among three when one is enough",
85+
// d1 has the most confirmations among d1, d2, d3.
86+
name: "prefer most confirmed among three",
6987
deposits: []*deposit.Deposit{d1, d2, d3},
7088
targetValue: 1_000_000,
71-
expected: []*deposit.Deposit{d3},
89+
csvExpiry: testCsvExpiry,
90+
blockHeight: testBlockHeight,
91+
expected: []*deposit.Deposit{d1},
7292
expectedErr: "",
7393
},
7494
{
7595
name: "single deposit insufficient by 1",
7696
deposits: []*deposit.Deposit{d1},
7797
targetValue: 1_000_001,
98+
csvExpiry: testCsvExpiry,
99+
blockHeight: testBlockHeight,
78100
expected: []*deposit.Deposit{},
79101
expectedErr: "not enough deposits to cover",
80102
},
81103
{
82104
name: "target leaves exact dust limit change",
83105
deposits: []*deposit.Deposit{d1},
84106
targetValue: 1_000_000 - dustLimit,
107+
csvExpiry: testCsvExpiry,
108+
blockHeight: testBlockHeight,
85109
expected: []*deposit.Deposit{d1},
86110
expectedErr: "",
87111
},
88112
{
89113
name: "target leaves dust change (just over)",
90114
deposits: []*deposit.Deposit{d1},
91115
targetValue: 1_000_000 - dustLimit + 1,
116+
csvExpiry: testCsvExpiry,
117+
blockHeight: testBlockHeight,
92118
expected: []*deposit.Deposit{},
93119
expectedErr: "not enough deposits to cover",
94120
},
95121
{
96122
name: "all deposits exactly match target",
97123
deposits: []*deposit.Deposit{d1, d2, d3},
98124
targetValue: d1.Value + d2.Value + d3.Value,
125+
csvExpiry: testCsvExpiry,
126+
blockHeight: testBlockHeight,
99127
expected: []*deposit.Deposit{d1, d2, d3},
100128
expectedErr: "",
101129
},
102130
{
103131
name: "sum minus dust limit is allowed (change == dust)",
104132
deposits: []*deposit.Deposit{d1, d2, d3},
105133
targetValue: d1.Value + d2.Value + d3.Value - dustLimit,
134+
csvExpiry: testCsvExpiry,
135+
blockHeight: testBlockHeight,
106136
expected: []*deposit.Deposit{d1, d2, d3},
107137
expectedErr: "",
108138
},
109139
{
110140
name: "sum minus dust limit plus 1 is not allowed (dust change)",
111141
deposits: []*deposit.Deposit{d1, d2, d3},
112142
targetValue: d1.Value + d2.Value + d3.Value - dustLimit + 1,
143+
csvExpiry: testCsvExpiry,
144+
blockHeight: testBlockHeight,
113145
expected: []*deposit.Deposit{},
114146
expectedErr: "not enough deposits to cover",
115147
},
116148
{
117-
name: "tie by value, prefer earlier expiry",
149+
// d3 and d4 have the same value but d3 has more
150+
// confirmations (lower ConfirmationHeight), so it
151+
// wins at the primary sort level.
152+
name: "same value, prefer more confirmed",
118153
deposits: []*deposit.Deposit{d3, d4},
119-
targetValue: d4.Value - dustLimit, // d3/d4 have the
120-
// same value but different expiration.
154+
targetValue: d4.Value - dustLimit,
155+
csvExpiry: testCsvExpiry,
156+
blockHeight: testBlockHeight,
121157
expected: []*deposit.Deposit{d3},
122158
expectedErr: "",
123159
},

0 commit comments

Comments
 (0)