From 66a17f47e69c2913f4fac55f9deba0adc20bd3ec Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Fri, 9 Jan 2026 12:42:44 +0100 Subject: [PATCH] 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. --- staticaddr/deposit/manager.go | 2 +- staticaddr/loopin/actions.go | 32 ++++++++++++++++++ staticaddr/loopin/manager.go | 41 +++++++++++++++-------- staticaddr/loopin/manager_test.go | 55 ++++++++++++++++++++++++++----- 4 files changed, 106 insertions(+), 24 deletions(-) diff --git a/staticaddr/deposit/manager.go b/staticaddr/deposit/manager.go index af882030..2f023901 100644 --- a/staticaddr/deposit/manager.go +++ b/staticaddr/deposit/manager.go @@ -20,7 +20,7 @@ const ( // MinConfs is the minimum number of confirmations we require for a // deposit to be considered available for loop-ins, coop-spends and // timeouts. - MinConfs = 6 + MinConfs = 3 // MaxConfs is unset since we don't require a max number of // confirmations for deposits. diff --git a/staticaddr/loopin/actions.go b/staticaddr/loopin/actions.go index 70a27811..eadf7ba5 100644 --- a/staticaddr/loopin/actions.go +++ b/staticaddr/loopin/actions.go @@ -30,6 +30,7 @@ import ( "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/lnwire" + "google.golang.org/grpc/status" ) const ( @@ -146,6 +147,10 @@ func (f *FSM) InitHtlcAction(ctx context.Context, ctx, loopInReq, ) if err != nil { + // Check if this is an insufficient confirmations error and log + // the details to help the user understand what's needed. + logInsufficientConfirmationsDetails(err) + err = fmt.Errorf("unable to initiate the loop-in with the "+ "server: %w", err) @@ -914,3 +919,30 @@ func byteSliceTo66ByteSlice(b []byte) ([musig2.PubNonceSize]byte, error) { return res, nil } + +// logInsufficientConfirmationsDetails extracts and logs the per-deposit +// confirmation details from a gRPC error if present. +func logInsufficientConfirmationsDetails(err error) { + st, ok := status.FromError(err) + if !ok { + return + } + + for _, detail := range st.Details() { + confDetails, ok := + detail.(*swapserverrpc.InsufficientConfirmationsDetails) + + if !ok { + continue + } + + log.Warnf("Insufficient deposit confirmations, max wait: %d blocks", + confDetails.MaxBlocksToWait) + + for _, dep := range confDetails.Deposits { + log.Warnf(" Deposit %s: %d/%d confirmations (need %d more blocks)", + dep.Outpoint, dep.CurrentConfirmations, + dep.RequiredConfirmations, dep.BlocksToWait) + } + } +} diff --git a/staticaddr/loopin/manager.go b/staticaddr/loopin/manager.go index 444ab585..97f6e41d 100644 --- a/staticaddr/loopin/manager.go +++ b/staticaddr/loopin/manager.go @@ -850,11 +850,13 @@ func (m *Manager) GetAllSwaps(ctx context.Context) ([]*StaticAddressLoopIn, return swaps, nil } -// SelectDeposits sorts the deposits by amount in descending order, then by -// blocks-until-expiry in ascending order. It then selects the deposits that -// are needed to cover the amount requested without leaving a dust change. It -// returns an error if the sum of deposits minus dust is less than the requested -// amount. +// SelectDeposits sorts the deposits to optimize for successful swaps with +// dynamic confirmation requirements: 1) more confirmations first (higher chance +// of server acceptance), 2) larger amounts first (to minimize number of deposits +// used), 3) expiring sooner first (to use time-sensitive deposits). It then +// selects the deposits that are needed to cover the amount requested without +// leaving a dust change. It returns an error if the sum of deposits minus dust +// is less than the requested amount. func SelectDeposits(targetAmount btcutil.Amount, unfilteredDeposits []*deposit.Deposit, csvExpiry uint32, blockHeight uint32) ([]*deposit.Deposit, error) { @@ -875,17 +877,28 @@ func SelectDeposits(targetAmount btcutil.Amount, deposits = append(deposits, d) } - // Sort the deposits by amount in descending order, then by - // blocks-until-expiry in ascending order. + // Sort deposits to optimize for successful swaps with dynamic + // confirmation requirements: + // 1. More confirmations first (higher chance of server acceptance) + // 2. Larger amounts first (to minimize number of deposits used) sort.Slice(deposits, func(i, j int) bool { - if deposits[i].Value == deposits[j].Value { - iExp := uint32(deposits[i].ConfirmationHeight) + - csvExpiry - blockHeight - jExp := uint32(deposits[j].ConfirmationHeight) + - csvExpiry - blockHeight - - return iExp < jExp + // Primary: more confirmations first. Guard against the + // theoretical case where ConfirmationHeight > blockHeight + // (e.g. during a transient reorg inconsistency). + var iConfs, jConfs uint32 + if blockHeight > uint32(deposits[i].ConfirmationHeight) { + iConfs = blockHeight - + uint32(deposits[i].ConfirmationHeight) } + if blockHeight > uint32(deposits[j].ConfirmationHeight) { + jConfs = blockHeight - + uint32(deposits[j].ConfirmationHeight) + } + if iConfs != jConfs { + return iConfs > jConfs + } + + // Secondary: larger amounts first. return deposits[i].Value > deposits[j].Value }) diff --git a/staticaddr/loopin/manager_test.go b/staticaddr/loopin/manager_test.go index d908a9e1..d367a3dc 100644 --- a/staticaddr/loopin/manager_test.go +++ b/staticaddr/loopin/manager_test.go @@ -26,10 +26,14 @@ type testCase struct { // TestSelectDeposits tests the selectDeposits function, which selects // deposits that can cover a target value while respecting the dust limit. +// Sorting priority: 1) more confirmations first, 2) larger amounts first, +// 3) expiring sooner first. func TestSelectDeposits(t *testing.T) { + // Note: confirmations = blockHeight - ConfirmationHeight + // Lower ConfirmationHeight means more confirmations at a given block. d1, d2, d3, d4 := &deposit.Deposit{ Value: 1_000_000, - ConfirmationHeight: 5_000, + ConfirmationHeight: 5_000, // most confs at height 5100 }, &deposit.Deposit{ Value: 2_000_000, ConfirmationHeight: 5_001, @@ -38,7 +42,7 @@ func TestSelectDeposits(t *testing.T) { ConfirmationHeight: 5_002, }, &deposit.Deposit{ Value: 3_000_000, - ConfirmationHeight: 5_003, + ConfirmationHeight: 5_003, // fewest confs at height 5100 } d1.Hash = chainhash.Hash{1} d1.Index = 0 @@ -49,32 +53,51 @@ func TestSelectDeposits(t *testing.T) { d4.Hash = chainhash.Hash{4} d4.Index = 0 + // Use a realistic block height and csv expiry for all standard + // test cases. csvExpiry must be large enough that deposits remain + // swappable at this block height. + const ( + testBlockHeight uint32 = 5_100 + testCsvExpiry uint32 = 2_500 + ) + testCases := []testCase{ { name: "single deposit exact target", deposits: []*deposit.Deposit{d1}, targetValue: 1_000_000, + csvExpiry: testCsvExpiry, + blockHeight: testBlockHeight, expected: []*deposit.Deposit{d1}, expectedErr: "", }, { - name: "prefer larger deposit when both cover", + // d1 has more confirmations, so it's preferred even + // though d2 is larger. + name: "prefer more confirmed deposit over larger", deposits: []*deposit.Deposit{d1, d2}, targetValue: 1_000_000, - expected: []*deposit.Deposit{d2}, + csvExpiry: testCsvExpiry, + blockHeight: testBlockHeight, + expected: []*deposit.Deposit{d1}, expectedErr: "", }, { - name: "prefer largest among three when one is enough", + // d1 has the most confirmations among d1, d2, d3. + name: "prefer most confirmed among three", deposits: []*deposit.Deposit{d1, d2, d3}, targetValue: 1_000_000, - expected: []*deposit.Deposit{d3}, + csvExpiry: testCsvExpiry, + blockHeight: testBlockHeight, + expected: []*deposit.Deposit{d1}, expectedErr: "", }, { name: "single deposit insufficient by 1", deposits: []*deposit.Deposit{d1}, targetValue: 1_000_001, + csvExpiry: testCsvExpiry, + blockHeight: testBlockHeight, expected: []*deposit.Deposit{}, expectedErr: "not enough deposits to cover", }, @@ -82,6 +105,8 @@ func TestSelectDeposits(t *testing.T) { name: "target leaves exact dust limit change", deposits: []*deposit.Deposit{d1}, targetValue: 1_000_000 - dustLimit, + csvExpiry: testCsvExpiry, + blockHeight: testBlockHeight, expected: []*deposit.Deposit{d1}, expectedErr: "", }, @@ -89,6 +114,8 @@ func TestSelectDeposits(t *testing.T) { name: "target leaves dust change (just over)", deposits: []*deposit.Deposit{d1}, targetValue: 1_000_000 - dustLimit + 1, + csvExpiry: testCsvExpiry, + blockHeight: testBlockHeight, expected: []*deposit.Deposit{}, expectedErr: "not enough deposits to cover", }, @@ -96,6 +123,8 @@ func TestSelectDeposits(t *testing.T) { name: "all deposits exactly match target", deposits: []*deposit.Deposit{d1, d2, d3}, targetValue: d1.Value + d2.Value + d3.Value, + csvExpiry: testCsvExpiry, + blockHeight: testBlockHeight, expected: []*deposit.Deposit{d1, d2, d3}, expectedErr: "", }, @@ -103,6 +132,8 @@ func TestSelectDeposits(t *testing.T) { name: "sum minus dust limit is allowed (change == dust)", deposits: []*deposit.Deposit{d1, d2, d3}, targetValue: d1.Value + d2.Value + d3.Value - dustLimit, + csvExpiry: testCsvExpiry, + blockHeight: testBlockHeight, expected: []*deposit.Deposit{d1, d2, d3}, expectedErr: "", }, @@ -110,14 +141,20 @@ func TestSelectDeposits(t *testing.T) { 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, + csvExpiry: testCsvExpiry, + blockHeight: testBlockHeight, expected: []*deposit.Deposit{}, expectedErr: "not enough deposits to cover", }, { - name: "tie by value, prefer earlier expiry", + // d3 and d4 have the same value but d3 has more + // confirmations (lower ConfirmationHeight), so it + // wins at the primary sort level. + name: "same value, prefer more confirmed", deposits: []*deposit.Deposit{d3, d4}, - targetValue: d4.Value - dustLimit, // d3/d4 have the - // same value but different expiration. + targetValue: d4.Value - dustLimit, + csvExpiry: testCsvExpiry, + blockHeight: testBlockHeight, expected: []*deposit.Deposit{d3}, expectedErr: "", },