Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 36 additions & 15 deletions staticaddr/deposit/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,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.
Expand All @@ -32,6 +32,10 @@ const (
// DefaultTransitionTimeout is the default timeout for transitions in
// the deposit state machine.
DefaultTransitionTimeout = 5 * time.Second

// PollInterval is the interval in which we poll for new deposits to our
// static address.
PollInterval = 10 * time.Second
)

// ManagerConfig holds the configuration for the address manager.
Expand Down Expand Up @@ -116,14 +120,16 @@ func (m *Manager) Run(ctx context.Context, initChan chan struct{}) error {
return err
}

// Initially reconcile new deposits after a restart, so we catch up with
// missed deposits while we were offline.
if err = m.reconcileDeposits(ctx); err != nil {
// Reconcile immediately on startup so deposits are available
// before the first ticker fires.
err = m.reconcileDeposits(ctx)
if err != nil {
log.Errorf("unable to reconcile deposits: %v", err)

return err
}

// Start the deposit notifier.
m.pollDeposits(ctx)

// Communicate to the caller that the address manager has completed its
// initialization.
close(initChan)
Expand Down Expand Up @@ -151,15 +157,6 @@ func (m *Manager) Run(ctx context.Context, initChan chan struct{}) error {
}
}

// Reconcile new deposits that might have just gotten
// confirmed.
if err = m.reconcileDeposits(ctx); err != nil {
log.Errorf("unable to reconcile deposits: %v",
err)

return err
}

case outpoint := <-m.finalizedDepositChan:
// If deposits notify us about their finalization, flush
// the finalized deposit from memory.
Expand Down Expand Up @@ -224,6 +221,30 @@ func (m *Manager) recoverDeposits(ctx context.Context) error {
return nil
}

// pollDeposits polls new deposits to our static address and notifies the
// manager's event loop about them.
func (m *Manager) pollDeposits(ctx context.Context) {
log.Debugf("Waiting for new static address deposits...")

go func() {
ticker := time.NewTicker(PollInterval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
err := m.reconcileDeposits(ctx)
if err != nil {
log.Errorf("unable to reconcile "+
"deposits: %v", err)
}

case <-ctx.Done():
return
}
}
}()
}

// reconcileDeposits fetches all spends to our static addresses from our lnd
// wallet and matches it against the deposits in our memory that we've seen so
// far. It picks the newly identified deposits and starts a state machine per
Expand Down
31 changes: 31 additions & 0 deletions staticaddr/loopin/actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -910,3 +915,29 @@ 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)
Comment on lines +933 to +934

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For better readability and to avoid string concatenation, you can include the full format string directly in log.Warnf.

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)
Comment on lines +937 to +940

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For better readability and to avoid string concatenation, you can include the full format string directly in log.Warnf.

log.Warnf("  Deposit %s: %d/%d confirmations (need %d more blocks)",
    dep.Outpoint, dep.CurrentConfirmations,
    dep.RequiredConfirmations, dep.BlocksToWait)

}
}
}
43 changes: 29 additions & 14 deletions staticaddr/loopin/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -863,11 +863,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) {
Expand All @@ -888,18 +890,31 @@ 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)
// 3. Expiring sooner first (to use time-sensitive deposits)
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
// Primary: more confirmations first.
iConfs := blockHeight - uint32(deposits[i].ConfirmationHeight)
jConfs := blockHeight - uint32(deposits[j].ConfirmationHeight)
if iConfs != jConfs {
return iConfs > jConfs
}

return iExp < jExp
// Secondary: larger amounts first.
if deposits[i].Value != deposits[j].Value {
return deposits[i].Value > deposits[j].Value
}
return deposits[i].Value > deposits[j].Value

// Tertiary: expiring sooner first.
iExp := uint32(deposits[i].ConfirmationHeight) +
csvExpiry - blockHeight
jExp := uint32(deposits[j].ConfirmationHeight) +
csvExpiry - blockHeight

return iExp < jExp
})

// Select the deposits that are needed to cover the swap amount without
Expand Down
55 changes: 46 additions & 9 deletions staticaddr/loopin/manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand All @@ -49,75 +53,108 @@ 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",
},
{
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: "",
},
{
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",
},
{
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: "",
},
{
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: "",
},
{
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: "",
},
Expand Down
Loading
Loading