Skip to content

Commit bab9486

Browse files
committed
staticaddr: fractional loop-in amount
1 parent 2ce0ba7 commit bab9486

File tree

5 files changed

+132
-12
lines changed

5 files changed

+132
-12
lines changed

interface.go

+2
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,8 @@ type StaticAddressLoopInRequest struct {
338338
// swap payment. If the timeout is reached the swap will be aborted and
339339
// the client can retry the swap if desired with different parameters.
340340
PaymentTimeoutSeconds uint32
341+
342+
SelectedAmount btcutil.Amount
341343
}
342344

343345
// LoopInTerms are the server terms on which it executes loop in swaps.

staticaddr/loopin/actions.go

+8-3
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,14 @@ func (f *FSM) InitHtlcAction(ctx context.Context,
6868
}
6969

7070
// Calculate the swap invoice amount. The server needs to pay us the
71-
// sum of all deposits minus the fees that the server charges for the
72-
// swap.
73-
swapInvoiceAmt := f.loopIn.TotalDepositAmount() - f.loopIn.QuotedSwapFee
71+
// swap amount minus the fees that the server charges for the swap. The
72+
// swap amount is either the total value of the selected deposits, or
73+
// the selected amount if a specific amount was requested.
74+
swapAmount := f.loopIn.TotalDepositAmount()
75+
if f.loopIn.SelectedAmount > 0 {
76+
swapAmount = f.loopIn.SelectedAmount
77+
}
78+
swapInvoiceAmt := swapAmount - f.loopIn.QuotedSwapFee
7479

7580
// Generate random preimage.
7681
var swapPreimage lntypes.Preimage

staticaddr/loopin/loopin.go

+39-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package loopin
22

33
import (
4+
"bytes"
45
"context"
56
"errors"
67
"fmt"
@@ -93,6 +94,8 @@ type StaticAddressLoopIn struct {
9394
// swap.
9495
DepositOutpoints []string
9596

97+
SelectedAmount btcutil.Amount
98+
9699
// state is the current state of the swap.
97100
state fsm.StateType
98101

@@ -287,10 +290,20 @@ func (l *StaticAddressLoopIn) createHtlcTx(chainParams *chaincfg.Params,
287290
weight := l.htlcWeight()
288291
fee := feeRate.FeeForWeight(weight)
289292

290-
// Check if the server breaches our fee limits.
291-
amt := float64(l.TotalDepositAmount())
292-
feeLimit := btcutil.Amount(amt * maxFeePercentage)
293+
// Determine the swap amount. If the user selected a specific amount, we
294+
// use that and use the difference to the total deposit amount as the
295+
// change.
296+
var (
297+
swapAmt = float64(l.TotalDepositAmount())
298+
changeAmount btcutil.Amount
299+
)
300+
if l.SelectedAmount > 0 {
301+
swapAmt = float64(l.SelectedAmount)
302+
changeAmount = l.TotalDepositAmount() - l.SelectedAmount
303+
}
293304

305+
// Check if the server breaches our fee limits.
306+
feeLimit := btcutil.Amount(swapAmt * maxFeePercentage)
294307
if fee > feeLimit {
295308
return nil, fmt.Errorf("htlc tx fee %v exceeds max fee %v",
296309
fee, feeLimit)
@@ -314,6 +327,14 @@ func (l *StaticAddressLoopIn) createHtlcTx(chainParams *chaincfg.Params,
314327

315328
msgTx.AddTxOut(sweepOutput)
316329

330+
// We expect change to be sent back to our static address output script.
331+
if changeAmount > 0 {
332+
msgTx.AddTxOut(&wire.TxOut{
333+
Value: int64(changeAmount),
334+
PkScript: l.AddressParams.PkScript,
335+
})
336+
}
337+
317338
return msgTx, nil
318339
}
319340

@@ -373,11 +394,25 @@ func (l *StaticAddressLoopIn) createHtlcSweepTx(ctx context.Context,
373394
return nil, err
374395
}
375396

397+
// Check if the htlc tx has a change output. If so we need to select the
398+
// non-change output index to construct the sweep with.
399+
htlcInputIndex := uint32(0)
400+
if len(htlcTx.TxOut) == 2 {
401+
// If the first htlc tx output matches our static address
402+
// script we need to select the second output to sweep from.
403+
if bytes.Equal(
404+
htlcTx.TxOut[0].PkScript, l.AddressParams.PkScript,
405+
) {
406+
407+
htlcInputIndex = 1
408+
}
409+
}
410+
376411
// Add the htlc input.
377412
sweepTx.AddTxIn(&wire.TxIn{
378413
PreviousOutPoint: wire.OutPoint{
379414
Hash: htlcTx.TxHash(),
380-
Index: 0,
415+
Index: htlcInputIndex,
381416
},
382417
SignatureScript: htlc.SigScript,
383418
Sequence: htlc.SuccessSequence(),

staticaddr/loopin/manager.go

+81-5
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ import (
44
"bytes"
55
"context"
66
"fmt"
7+
"sort"
78
"sync/atomic"
89
"time"
910

1011
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
12+
"github.com/btcsuite/btcd/btcutil"
1113
"github.com/btcsuite/btcd/btcutil/psbt"
1214
"github.com/btcsuite/btcd/chaincfg"
1315
"github.com/btcsuite/btcd/chaincfg/chainhash"
@@ -20,7 +22,9 @@ import (
2022
"github.com/lightninglabs/loop/staticaddr/deposit"
2123
"github.com/lightninglabs/loop/swapserverrpc"
2224
looprpc "github.com/lightninglabs/loop/swapserverrpc"
25+
"github.com/lightningnetwork/lnd/input"
2326
"github.com/lightningnetwork/lnd/lntypes"
27+
"github.com/lightningnetwork/lnd/lnwallet"
2428
"github.com/lightningnetwork/lnd/routing/route"
2529
)
2630

@@ -205,8 +209,8 @@ func (m *Manager) Run(ctx context.Context, currentHeight uint32) error {
205209
case request.respChan <- resp:
206210

207211
case <-ctx.Done():
208-
// Noify subroutines that the main loop has been
209-
// canceled.
212+
// Notify subroutines that the main loop has
213+
// been canceled.
210214
close(m.exitChan)
211215

212216
return ctx.Err()
@@ -530,14 +534,30 @@ func (m *Manager) initiateLoopIn(ctx context.Context,
530534
req *loop.StaticAddressLoopInRequest) (*StaticAddressLoopIn, error) {
531535

532536
// Validate the loop-in request.
537+
if len(req.DepositOutpoints) == 0 && req.SelectedAmount == 0 {
538+
return nil, fmt.Errorf("no deposit outpoints provided and no " +
539+
"amount selected")
540+
}
541+
542+
var (
543+
err error
544+
selectedOutpoints = req.DepositOutpoints
545+
)
546+
// If there's only an amount selected by the user, we need to find
547+
// deposits that cover this amount.
533548
if len(req.DepositOutpoints) == 0 {
534-
return nil, fmt.Errorf("no deposit outpoints provided")
549+
selectedOutpoints, err = m.selectDeposits(
550+
ctx, req.SelectedAmount,
551+
)
552+
if err != nil {
553+
return nil, err
554+
}
535555
}
536556

537557
// Retrieve all deposits referenced by the outpoints and ensure that
538558
// they are in state Deposited.
539559
deposits, active := m.cfg.DepositManager.AllStringOutpointsActiveDeposits( //nolint:lll
540-
req.DepositOutpoints, deposit.Deposited,
560+
selectedOutpoints, deposit.Deposited,
541561
)
542562
if !active {
543563
return nil, fmt.Errorf("one or more deposits are not in "+
@@ -550,8 +570,17 @@ func (m *Manager) initiateLoopIn(ctx context.Context,
550570
}
551571
totalDepositAmount := tmp.TotalDepositAmount()
552572

573+
// If the selected amount would leave a dust change output or exceeds
574+
// the total deposits value, we return an error.
575+
dustLimit := lnwallet.DustLimitForSize(input.P2TRSize)
576+
if totalDepositAmount-req.SelectedAmount < dustLimit {
577+
return nil, fmt.Errorf("selected amount %v leaves "+
578+
"dust or exceeds total deposit value %v",
579+
req.SelectedAmount, totalDepositAmount)
580+
}
581+
553582
// Check that the label is valid.
554-
err := labels.Validate(req.Label)
583+
err = labels.Validate(req.Label)
555584
if err != nil {
556585
return nil, fmt.Errorf("invalid label: %w", err)
557586
}
@@ -617,6 +646,7 @@ func (m *Manager) initiateLoopIn(ctx context.Context,
617646
}
618647

619648
swap := &StaticAddressLoopIn{
649+
SelectedAmount: req.SelectedAmount,
620650
DepositOutpoints: req.DepositOutpoints,
621651
Deposits: deposits,
622652
Label: req.Label,
@@ -636,6 +666,52 @@ func (m *Manager) initiateLoopIn(ctx context.Context,
636666
return m.startLoopInFsm(ctx, swap)
637667
}
638668

669+
// selectDeposits finds a set of deposits that are ready to be used for a
670+
// loop-in and cover a given amount. It returns the outpoints of the selected
671+
// deposits.
672+
func (m *Manager) selectDeposits(ctx context.Context,
673+
amount btcutil.Amount) ([]string, error) {
674+
675+
// TODO(hieblmi): provide sql query to get all deposits in given state.
676+
allDeposits, err := m.cfg.DepositManager.GetAllDeposits(ctx)
677+
if err != nil {
678+
return nil, err
679+
}
680+
681+
deposits := make([]*deposit.Deposit, 0)
682+
for _, d := range allDeposits {
683+
if d.IsInState(deposit.Deposited) {
684+
deposits = append(deposits, d)
685+
}
686+
}
687+
688+
// Sort deposits by confirmation block in descending order first to pick
689+
// the oldest deposits, then sort by deposit amount in descending order.
690+
sort.Slice(deposits, func(i, j int) bool {
691+
if deposits[i].ConfirmationHeight !=
692+
deposits[j].ConfirmationHeight {
693+
694+
return deposits[i].ConfirmationHeight >
695+
deposits[j].ConfirmationHeight
696+
}
697+
698+
return deposits[i].Value > deposits[j].Value
699+
})
700+
701+
// Now select deposits from the front of the sorted slice until the sum
702+
// satisfies the required amount.
703+
selectedDeposits := make([]string, 0)
704+
for _, d := range deposits {
705+
amount -= d.Value
706+
selectedDeposits = append(selectedDeposits, d.OutPoint.String())
707+
if amount <= 0 {
708+
return selectedDeposits, nil
709+
}
710+
}
711+
712+
return nil, fmt.Errorf("not enough deposits to cover amount")
713+
}
714+
639715
// startLoopInFsm initiates a loop-in state machine based on the user-provided
640716
// swap information, sends that info to the server and waits for the server to
641717
// return htlc signature information. It then creates the loop-in object in the

staticaddr/loopin/sql_store.go

+2
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,7 @@ func (s *SqlStore) CreateLoopIn(ctx context.Context,
230230
HtlcTimeoutSweepAddress: loopIn.HtlcTimeoutSweepAddress.String(),
231231
HtlcTxFeeRateSatKw: int64(loopIn.HtlcTxFeeRate),
232232
DepositOutpoints: joinedOutpoints,
233+
SelectedAmount: int64(loopIn.SelectedAmount),
233234
PaymentTimeoutSeconds: int32(loopIn.PaymentTimeoutSeconds),
234235
}
235236

@@ -378,6 +379,7 @@ func toStaticAddressLoopIn(_ context.Context, network *chaincfg.Params,
378379
LastHop: row.LastHop,
379380
QuotedSwapFee: btcutil.Amount(row.QuotedSwapFeeSatoshis),
380381
DepositOutpoints: depositOutpoints,
382+
SelectedAmount: btcutil.Amount(row.SelectedAmount),
381383
HtlcTxFeeRate: chainfee.SatPerKWeight(
382384
row.HtlcTxFeeRateSatKw,
383385
),

0 commit comments

Comments
 (0)