Skip to content

Commit d4846b0

Browse files
committed
staticaddr: fractional loop-in amount
1 parent 7b67125 commit d4846b0

File tree

6 files changed

+167
-38
lines changed

6 files changed

+167
-38
lines changed

interface.go

Lines changed: 2 additions & 0 deletions
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.

loopd/swapclient_server.go

Lines changed: 40 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,10 @@ import (
3434
"github.com/lightninglabs/loop/swap"
3535
"github.com/lightninglabs/loop/swapserverrpc"
3636
"github.com/lightninglabs/taproot-assets/rfqmath"
37+
"github.com/lightningnetwork/lnd/input"
3738
"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
3839
"github.com/lightningnetwork/lnd/lntypes"
40+
"github.com/lightningnetwork/lnd/lnwallet"
3941
"github.com/lightningnetwork/lnd/queue"
4042
"github.com/lightningnetwork/lnd/routing/route"
4143
"github.com/lightningnetwork/lnd/zpay32"
@@ -843,21 +845,26 @@ func (s *swapClientServer) GetLoopInQuote(ctx context.Context,
843845
infof("Loop in quote request received")
844846

845847
var (
846-
numDeposits = uint32(len(req.DepositOutpoints))
847-
err error
848+
selectedAmount = btcutil.Amount(req.Amt)
849+
totalDepositAmount btcutil.Amount
850+
numDeposits = len(req.DepositOutpoints)
851+
err error
848852
)
849853

850854
htlcConfTarget, err := validateLoopInRequest(
851-
req.ConfTarget, req.ExternalHtlc, numDeposits, req.Amt,
855+
req.ConfTarget, req.ExternalHtlc, uint32(numDeposits),
856+
int64(selectedAmount),
852857
)
853858
if err != nil {
854859
return nil, err
855860
}
856861

857862
// Retrieve deposits to calculate their total value.
858863
var depositList *looprpc.ListStaticAddressDepositsResponse
859-
amount := btcutil.Amount(req.Amt)
860-
if len(req.DepositOutpoints) > 0 {
864+
865+
// If deposits are selected, we need to retrieve them to calculate the
866+
// total value which we request a quote for.
867+
if numDeposits > 0 {
861868
depositList, err = s.ListStaticAddressDeposits(
862869
ctx, &looprpc.ListStaticAddressDepositsRequest{
863870
Outpoints: req.DepositOutpoints,
@@ -872,20 +879,34 @@ func (s *swapClientServer) GetLoopInQuote(ctx context.Context,
872879
"deposit outpoints")
873880
}
874881

875-
// The requested amount should be 0 here if the request
876-
// contained deposit outpoints.
877-
if amount != 0 && len(depositList.FilteredDeposits) > 0 {
878-
return nil, fmt.Errorf("amount should be 0 for " +
879-
"deposit quotes")
882+
if numDeposits != len(depositList.FilteredDeposits) {
883+
return nil, fmt.Errorf("expected %d deposits, got %d",
884+
numDeposits, len(depositList.FilteredDeposits))
880885
}
881886

882887
// In case we quote for deposits we send the server both the
883-
// total value and the number of deposits. This is so the server
884-
// can probe the total amount and calculate the per input fee.
885-
if amount == 0 && len(depositList.FilteredDeposits) > 0 {
886-
for _, deposit := range depositList.FilteredDeposits {
887-
amount += btcutil.Amount(deposit.Value)
888-
}
888+
// selected value and the number of deposits. This is so the
889+
// server can probe the selected value and calculate the per
890+
// input fee.
891+
for _, deposit := range depositList.FilteredDeposits {
892+
totalDepositAmount += btcutil.Amount(
893+
deposit.Value,
894+
)
895+
}
896+
897+
// If the selected amount would leave a dust change output or
898+
// exceeds the total deposits value, we return an error.
899+
dustLimit := lnwallet.DustLimitForSize(input.P2TRSize)
900+
if totalDepositAmount-selectedAmount < dustLimit {
901+
return nil, fmt.Errorf("selected amount %v leaves "+
902+
"dust or exceeds total deposit value %v",
903+
selectedAmount, totalDepositAmount)
904+
}
905+
906+
// If the client didn't select an amount we quote for the total
907+
// deposits value.
908+
if selectedAmount == 0 {
909+
selectedAmount = totalDepositAmount
889910
}
890911
}
891912

@@ -912,14 +933,14 @@ func (s *swapClientServer) GetLoopInQuote(ctx context.Context,
912933
}
913934

914935
quote, err := s.impl.LoopInQuote(ctx, &loop.LoopInQuoteRequest{
915-
Amount: amount,
936+
Amount: selectedAmount,
916937
HtlcConfTarget: htlcConfTarget,
917938
ExternalHtlc: req.ExternalHtlc,
918939
LastHop: lastHop,
919940
RouteHints: routeHints,
920941
Private: req.Private,
921942
Initiator: defaultLoopdInitiator,
922-
NumDeposits: numDeposits,
943+
NumDeposits: uint32(numDeposits),
923944
})
924945
if err != nil {
925946
return nil, err
@@ -1762,6 +1783,7 @@ func (s *swapClientServer) StaticAddressLoopIn(ctx context.Context,
17621783
}
17631784

17641785
req := &loop.StaticAddressLoopInRequest{
1786+
SelectedAmount: btcutil.Amount(in.Amount),
17651787
DepositOutpoints: in.Outpoints,
17661788
MaxSwapFee: btcutil.Amount(in.MaxSwapFeeSatoshis),
17671789
Label: in.Label,

staticaddr/loopin/actions.go

Lines changed: 10 additions & 4 deletions
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
@@ -120,6 +125,7 @@ func (f *FSM) InitHtlcAction(ctx context.Context,
120125
loopInReq := &looprpc.ServerStaticAddressLoopInRequest{
121126
SwapHash: f.loopIn.SwapHash[:],
122127
DepositOutpoints: f.loopIn.DepositOutpoints,
128+
Amount: uint64(f.loopIn.SelectedAmount),
123129
HtlcClientPubKey: f.loopIn.ClientPubkey.SerializeCompressed(),
124130
SwapInvoice: f.loopIn.SwapInvoice,
125131
ProtocolVersion: version.CurrentRPCProtocolVersion(),
@@ -204,7 +210,7 @@ func (f *FSM) InitHtlcAction(ctx context.Context,
204210
// We need to defend against the server setting high fees for the htlc
205211
// tx since we might have to sweep the timeout path. We maximally allow
206212
// a configured percentage of the swap value to be spent on fees.
207-
amt := float64(f.loopIn.TotalDepositAmount())
213+
amt := float64(swapAmount)
208214
maxHtlcTxFee := btcutil.Amount(amt *
209215
f.cfg.MaxStaticAddrHtlcFeePercentage)
210216

staticaddr/loopin/loopin.go

Lines changed: 40 additions & 5 deletions
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 = l.TotalDepositAmount()
298+
changeAmount btcutil.Amount
299+
)
300+
if l.SelectedAmount > 0 {
301+
swapAmt = l.SelectedAmount
302+
changeAmount = l.TotalDepositAmount() - l.SelectedAmount
303+
}
293304

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

309322
// Create the sweep output
310323
sweepOutput := &wire.TxOut{
311-
Value: int64(l.TotalDepositAmount()) - int64(fee),
324+
Value: int64(swapAmt - fee),
312325
PkScript: pkscript,
313326
}
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

Lines changed: 68 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ import (
2020
"github.com/lightninglabs/loop/staticaddr/deposit"
2121
"github.com/lightninglabs/loop/swapserverrpc"
2222
looprpc "github.com/lightninglabs/loop/swapserverrpc"
23+
"github.com/lightningnetwork/lnd/input"
2324
"github.com/lightningnetwork/lnd/lntypes"
25+
"github.com/lightningnetwork/lnd/lnwallet"
2426
"github.com/lightningnetwork/lnd/routing/route"
2527
)
2628

@@ -206,8 +208,8 @@ func (m *Manager) Run(ctx context.Context) error {
206208
case request.respChan <- resp:
207209

208210
case <-ctx.Done():
209-
// Noify subroutines that the main loop has been
210-
// canceled.
211+
// Notify subroutines that the main loop has
212+
// been canceled.
211213
close(m.exitChan)
212214

213215
return ctx.Err()
@@ -272,6 +274,14 @@ func (m *Manager) handleLoopInSweepReq(ctx context.Context,
272274
return err
273275
}
274276

277+
deposits, err := m.cfg.DepositManager.DepositsForOutpoints(
278+
ctx, loopIn.DepositOutpoints,
279+
)
280+
if err != nil {
281+
return err
282+
}
283+
loopIn.Deposits = deposits
284+
275285
reader := bytes.NewReader(req.SweepTxPsbt)
276286
sweepPacket, err := psbt.NewFromRawBytes(reader, false)
277287
if err != nil {
@@ -297,6 +307,33 @@ func (m *Manager) handleLoopInSweepReq(ctx context.Context,
297307
len(req.PrevoutInfo), len(sweepTx.TxIn))
298308
}
299309

310+
// If the user selected an amount we'll check that the server sends us
311+
// the correct change amount back to our static address.
312+
if loopIn.SelectedAmount > 0 {
313+
var foundChange bool
314+
totalDepositAmount := loopIn.TotalDepositAmount()
315+
changeAmt := totalDepositAmount - loopIn.SelectedAmount
316+
changePkScript := loopIn.AddressParams.PkScript
317+
318+
for _, out := range sweepTx.TxOut {
319+
if out.Value == int64(changeAmt) &&
320+
bytes.Equal(out.PkScript, changePkScript) {
321+
322+
foundChange = true
323+
break
324+
}
325+
}
326+
327+
if !foundChange {
328+
return fmt.Errorf("expected change output to our "+
329+
"static address, total_deposit_amount=%v, "+
330+
"selected_amount=%v, "+
331+
"expected_change_amount=%v ",
332+
totalDepositAmount, loopIn.SelectedAmount,
333+
changeAmt)
334+
}
335+
}
336+
300337
// Check if all the deposits requested are part of the loop-in and
301338
// find them in the requested sweep.
302339
depositToIdxMap, err := mapDepositsToIndices(req, loopIn, sweepTx)
@@ -531,14 +568,20 @@ func (m *Manager) initiateLoopIn(ctx context.Context,
531568
req *loop.StaticAddressLoopInRequest) (*StaticAddressLoopIn, error) {
532569

533570
// Validate the loop-in request.
534-
if len(req.DepositOutpoints) == 0 {
535-
return nil, fmt.Errorf("no deposit outpoints provided")
571+
if len(req.DepositOutpoints) == 0 && req.SelectedAmount == 0 {
572+
return nil, fmt.Errorf("no deposit outpoints provided and no " +
573+
"amount selected")
536574
}
537575

576+
var (
577+
err error
578+
selectedOutpoints = req.DepositOutpoints
579+
)
580+
538581
// Retrieve all deposits referenced by the outpoints and ensure that
539582
// they are in state Deposited.
540583
deposits, active := m.cfg.DepositManager.AllStringOutpointsActiveDeposits( //nolint:lll
541-
req.DepositOutpoints, deposit.Deposited,
584+
selectedOutpoints, deposit.Deposited,
542585
)
543586
if !active {
544587
return nil, fmt.Errorf("one or more deposits are not in "+
@@ -551,8 +594,22 @@ func (m *Manager) initiateLoopIn(ctx context.Context,
551594
}
552595
totalDepositAmount := tmp.TotalDepositAmount()
553596

597+
// If the selected amount would leave a dust change output or exceeds
598+
// the total deposits value, we return an error.
599+
dustLimit := lnwallet.DustLimitForSize(input.P2TRSize)
600+
if totalDepositAmount-req.SelectedAmount < dustLimit {
601+
return nil, fmt.Errorf("selected amount %v leaves "+
602+
"dust or exceeds total deposit value %v",
603+
req.SelectedAmount, totalDepositAmount)
604+
}
605+
606+
swapAmount := totalDepositAmount
607+
if req.SelectedAmount > 0 {
608+
swapAmount = req.SelectedAmount
609+
}
610+
554611
// Check that the label is valid.
555-
err := labels.Validate(req.Label)
612+
err = labels.Validate(req.Label)
556613
if err != nil {
557614
return nil, fmt.Errorf("invalid label: %w", err)
558615
}
@@ -576,7 +633,7 @@ func (m *Manager) initiateLoopIn(ctx context.Context,
576633
// Because the Private flag is set, we'll generate our own set
577634
// of hop hints.
578635
req.RouteHints, err = loop.SelectHopHints(
579-
ctx, m.cfg.LndClient, totalDepositAmount,
636+
ctx, m.cfg.LndClient, swapAmount,
580637
loop.DefaultMaxHopHints, includeNodes,
581638
)
582639
if err != nil {
@@ -593,11 +650,11 @@ func (m *Manager) initiateLoopIn(ctx context.Context,
593650
// directly anyway and there they have the option to add specific route
594651
// hints.
595652
// The quote call will also request a probe from the server to ensure
596-
// feasibility of a loop-in for the totalDepositAmount.
653+
// feasibility of a loop-in for the selected.
597654
numDeposits := uint32(len(deposits))
598655
quote, err := m.cfg.QuoteGetter.GetLoopInQuote(
599-
ctx, totalDepositAmount, m.cfg.NodePubkey, req.LastHop,
600-
req.RouteHints, req.Initiator, numDeposits,
656+
ctx, swapAmount, m.cfg.NodePubkey, req.LastHop, req.RouteHints,
657+
req.Initiator, numDeposits,
601658
)
602659
if err != nil {
603660
return nil, fmt.Errorf("unable to get loop in quote: %w", err)
@@ -618,6 +675,7 @@ func (m *Manager) initiateLoopIn(ctx context.Context,
618675
}
619676

620677
swap := &StaticAddressLoopIn{
678+
SelectedAmount: req.SelectedAmount,
621679
DepositOutpoints: req.DepositOutpoints,
622680
Deposits: deposits,
623681
Label: req.Label,

0 commit comments

Comments
 (0)