Skip to content

Commit 8321cc2

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 8321cc2

File tree

4 files changed

+118
-24
lines changed

4 files changed

+118
-24
lines changed

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: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -850,11 +850,14 @@ 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), 3) expiring sooner first (prioritize time-sensitive deposits).
857+
// Deposits that are too close to expiry are filtered out before sorting.
858+
// It then selects the deposits that are needed to cover the amount requested
859+
// without leaving a dust change. It returns an error if the sum of deposits
860+
// minus dust is less than the requested amount.
858861
func SelectDeposits(targetAmount btcutil.Amount,
859862
unfilteredDeposits []*deposit.Deposit, csvExpiry uint32,
860863
blockHeight uint32) ([]*deposit.Deposit, error) {
@@ -875,18 +878,40 @@ func SelectDeposits(targetAmount btcutil.Amount,
875878
deposits = append(deposits, d)
876879
}
877880

878-
// Sort the deposits by amount in descending order, then by
879-
// blocks-until-expiry in ascending order.
881+
// Sort deposits to optimize for successful swaps with dynamic
882+
// confirmation requirements:
883+
// 1. More confirmations first (higher chance of server acceptance).
884+
// 2. Larger amounts first (to minimize number of deposits used).
885+
// 3. Expiring sooner first (prioritize time-sensitive deposits).
880886
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
887+
// Primary: more confirmations first. Guard against the
888+
// theoretical case where ConfirmationHeight > blockHeight
889+
// (e.g. during a transient reorg inconsistency).
890+
var iConfs, jConfs uint32
891+
if blockHeight > uint32(deposits[i].ConfirmationHeight) {
892+
iConfs = blockHeight -
893+
uint32(deposits[i].ConfirmationHeight)
894+
}
895+
if blockHeight > uint32(deposits[j].ConfirmationHeight) {
896+
jConfs = blockHeight -
897+
uint32(deposits[j].ConfirmationHeight)
898+
}
899+
if iConfs != jConfs {
900+
return iConfs > jConfs
901+
}
886902

887-
return iExp < jExp
903+
// Secondary: larger amounts first.
904+
if deposits[i].Value != deposits[j].Value {
905+
return deposits[i].Value > deposits[j].Value
888906
}
889-
return deposits[i].Value > deposits[j].Value
907+
908+
// Tertiary: expiring sooner first.
909+
iExp := uint32(deposits[i].ConfirmationHeight) +
910+
csvExpiry - blockHeight
911+
jExp := uint32(deposits[j].ConfirmationHeight) +
912+
csvExpiry - blockHeight
913+
914+
return iExp < jExp
890915
})
891916

892917
// 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)