Skip to content

Commit 0d9e9c6

Browse files
committed
staticaddr: arbitrary loop-in amount
In this commit we add a new function SelectDeposits to the loop-in manager. It coin-selects deposits that meet an arbitrary swap amount provided by the client. We have to ensure that the server creates the correct change outputs for the htlc- and sweepless sweep transactions.
1 parent a6f2870 commit 0d9e9c6

File tree

7 files changed

+529
-46
lines changed

7 files changed

+529
-46
lines changed

interface.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,11 @@ 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 is the amount that the user selected for the swap. If
343+
// the user did not select an amount, the amount of the selected
344+
// deposits is used.
345+
SelectedAmount btcutil.Amount
341346
}
342347

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

staticaddr/loopin/actions.go

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,18 @@ 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+
var hasChange bool
76+
if f.loopIn.SelectedAmount > 0 {
77+
swapAmount = f.loopIn.SelectedAmount
78+
remainingAmount := f.loopIn.TotalDepositAmount() - swapAmount
79+
hasChange = remainingAmount > 0 && remainingAmount <
80+
f.loopIn.TotalDepositAmount()
81+
}
82+
swapInvoiceAmt := swapAmount - f.loopIn.QuotedSwapFee
7483

7584
// Generate random preimage.
7685
var swapPreimage lntypes.Preimage
@@ -120,6 +129,7 @@ func (f *FSM) InitHtlcAction(ctx context.Context,
120129
loopInReq := &swapserverrpc.ServerStaticAddressLoopInRequest{
121130
SwapHash: f.loopIn.SwapHash[:],
122131
DepositOutpoints: f.loopIn.DepositOutpoints,
132+
Amount: uint64(f.loopIn.SelectedAmount),
123133
HtlcClientPubKey: f.loopIn.ClientPubkey.SerializeCompressed(),
124134
SwapInvoice: f.loopIn.SwapInvoice,
125135
ProtocolVersion: version.CurrentRPCProtocolVersion(),
@@ -204,15 +214,15 @@ func (f *FSM) InitHtlcAction(ctx context.Context,
204214
// We need to defend against the server setting high fees for the htlc
205215
// tx since we might have to sweep the timeout path. We maximally allow
206216
// a configured percentage of the swap value to be spent on fees.
207-
amt := float64(f.loopIn.TotalDepositAmount())
217+
amt := float64(swapAmount)
208218
maxHtlcTxFee := btcutil.Amount(amt *
209219
f.cfg.MaxStaticAddrHtlcFeePercentage)
210220

211221
maxHtlcTxBackupFee := btcutil.Amount(amt *
212222
f.cfg.MaxStaticAddrHtlcBackupFeePercentage)
213223

214224
feeRate := chainfee.SatPerKWeight(loopInResp.StandardHtlcInfo.FeeRate)
215-
fee := feeRate.FeeForWeight(f.loopIn.htlcWeight())
225+
fee := feeRate.FeeForWeight(f.loopIn.htlcWeight(hasChange))
216226
if fee > maxHtlcTxFee {
217227
// Abort the swap by pushing empty sigs to the server.
218228
pushEmptySigs()
@@ -225,7 +235,7 @@ func (f *FSM) InitHtlcAction(ctx context.Context,
225235
f.loopIn.HtlcTxFeeRate = feeRate
226236

227237
highFeeRate := chainfee.SatPerKWeight(loopInResp.HighFeeHtlcInfo.FeeRate)
228-
fee = highFeeRate.FeeForWeight(f.loopIn.htlcWeight())
238+
fee = highFeeRate.FeeForWeight(f.loopIn.htlcWeight(hasChange))
229239
if fee > maxHtlcTxBackupFee {
230240
// Abort the swap by pushing empty sigs to the server.
231241
pushEmptySigs()
@@ -241,7 +251,7 @@ func (f *FSM) InitHtlcAction(ctx context.Context,
241251
extremelyHighFeeRate := chainfee.SatPerKWeight(
242252
loopInResp.ExtremeFeeHtlcInfo.FeeRate,
243253
)
244-
fee = extremelyHighFeeRate.FeeForWeight(f.loopIn.htlcWeight())
254+
fee = extremelyHighFeeRate.FeeForWeight(f.loopIn.htlcWeight(hasChange))
245255
if fee > maxHtlcTxBackupFee {
246256
// Abort the swap by pushing empty sigs to the server.
247257
pushEmptySigs()
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package loopin
2+
3+
import (
4+
"testing"
5+
6+
"github.com/btcsuite/btcd/btcutil"
7+
"github.com/lightningnetwork/lnd/input"
8+
"github.com/lightningnetwork/lnd/lnwallet"
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
// TestDeduceSwapAmount covers all validation branches of DeduceSwapAmount.
13+
func TestDeduceSwapAmount(t *testing.T) {
14+
dl := lnwallet.DustLimitForSize(input.P2TRSize)
15+
16+
tests := []struct {
17+
name string
18+
total btcutil.Amount
19+
selectAmt btcutil.Amount
20+
wantAmt btcutil.Amount
21+
wantErr string
22+
}{
23+
{
24+
name: "negative selected amount",
25+
total: dl * 10,
26+
selectAmt: -1,
27+
wantErr: "negative",
28+
},
29+
{
30+
name: "selected is dust (>0 < dust)",
31+
total: dl * 10,
32+
selectAmt: dl - 1,
33+
wantErr: "is dust",
34+
},
35+
{
36+
name: "total deposit is dust",
37+
total: dl - 1,
38+
selectAmt: 0,
39+
wantErr: "total deposit value",
40+
},
41+
{
42+
name: "selected exceeds total",
43+
total: dl * 5,
44+
selectAmt: dl*5 + 1,
45+
wantErr: "exceeds total",
46+
},
47+
{
48+
name: "leaves dust change",
49+
total: dl*5 + (dl - 1),
50+
selectAmt: dl * 5,
51+
wantErr: "leaves dust change",
52+
},
53+
{
54+
name: "selected zero => swap total",
55+
total: dl * 7,
56+
selectAmt: 0,
57+
wantAmt: dl * 7,
58+
},
59+
{
60+
name: "selected equals total",
61+
total: dl * 9,
62+
selectAmt: dl * 9,
63+
wantAmt: dl * 9,
64+
},
65+
{
66+
name: "selected and remaining both >= dust",
67+
total: dl*10 + dl, // 11*dust
68+
selectAmt: dl * 10, // remaining = dust
69+
wantAmt: dl * 10,
70+
},
71+
}
72+
73+
for _, tc := range tests {
74+
t.Run(tc.name, func(t *testing.T) {
75+
amt, err := DeduceSwapAmount(tc.total, tc.selectAmt)
76+
if tc.wantErr != "" {
77+
require.Error(t, err)
78+
require.ErrorContains(t, err, tc.wantErr)
79+
return
80+
}
81+
require.NoError(t, err)
82+
require.Equal(t, tc.wantAmt, amt)
83+
})
84+
}
85+
}

staticaddr/loopin/interface.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@ type DepositManager interface {
5353
// outpoints.
5454
DepositsForOutpoints(ctx context.Context, outpoints []string) (
5555
[]*deposit.Deposit, error)
56+
57+
// GetActiveDepositsInState returns all active deposits in the given
58+
// state.
59+
GetActiveDepositsInState(stateFilter fsm.StateType) ([]*deposit.Deposit,
60+
error)
5661
}
5762

5863
// StaticAddressLoopInStore provides access to the static address loop-in DB.

staticaddr/loopin/loopin.go

Lines changed: 52 additions & 7 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"
@@ -91,8 +92,15 @@ type StaticAddressLoopIn struct {
9192

9293
// The outpoints in the format txid:vout that are part of the loop-in
9394
// swap.
95+
// TODO(hieblmi): Replace this with a getter method that fetches the
96+
// outpoints from the deposits.
9497
DepositOutpoints []string
9598

99+
// SelectedAmount is the amount that the user selected for the swap. If
100+
// the user did not select an amount, the amount of all deposits is
101+
// used.
102+
SelectedAmount btcutil.Amount
103+
96104
// state is the current state of the swap.
97105
state fsm.StateType
98106

@@ -283,14 +291,25 @@ func (l *StaticAddressLoopIn) createHtlcTx(chainParams *chaincfg.Params,
283291
})
284292
}
285293

294+
// Determine the swap amount. If the user selected a specific amount, we
295+
// use that and use the difference to the total deposit amount as the
296+
// change.
297+
var (
298+
swapAmt = l.TotalDepositAmount()
299+
changeAmount btcutil.Amount
300+
)
301+
if l.SelectedAmount > 0 {
302+
swapAmt = l.SelectedAmount
303+
changeAmount = l.TotalDepositAmount() - l.SelectedAmount
304+
}
305+
286306
// Calculate htlc tx fee for server provided fee rate.
287-
weight := l.htlcWeight()
307+
hasChange := changeAmount > 0
308+
weight := l.htlcWeight(hasChange)
288309
fee := feeRate.FeeForWeight(weight)
289310

290311
// Check if the server breaches our fee limits.
291-
amt := float64(l.TotalDepositAmount())
292-
feeLimit := btcutil.Amount(amt * maxFeePercentage)
293-
312+
feeLimit := btcutil.Amount(float64(swapAmt) * maxFeePercentage)
294313
if fee > feeLimit {
295314
return nil, fmt.Errorf("htlc tx fee %v exceeds max fee %v",
296315
fee, feeLimit)
@@ -308,12 +327,20 @@ func (l *StaticAddressLoopIn) createHtlcTx(chainParams *chaincfg.Params,
308327

309328
// Create the sweep output
310329
sweepOutput := &wire.TxOut{
311-
Value: int64(l.TotalDepositAmount()) - int64(fee),
330+
Value: int64(swapAmt - fee),
312331
PkScript: pkscript,
313332
}
314333

315334
msgTx.AddTxOut(sweepOutput)
316335

336+
// We expect change to be sent back to our static address output script.
337+
if changeAmount > 0 {
338+
msgTx.AddTxOut(&wire.TxOut{
339+
Value: int64(changeAmount),
340+
PkScript: l.AddressParams.PkScript,
341+
})
342+
}
343+
317344
return msgTx, nil
318345
}
319346

@@ -325,7 +352,7 @@ func (l *StaticAddressLoopIn) isHtlcTimedOut(height int32) bool {
325352
}
326353

327354
// htlcWeight returns the weight for the htlc transaction.
328-
func (l *StaticAddressLoopIn) htlcWeight() lntypes.WeightUnit {
355+
func (l *StaticAddressLoopIn) htlcWeight(hasChange bool) lntypes.WeightUnit {
329356
var weightEstimator input.TxWeightEstimator
330357
for i := 0; i < len(l.Deposits); i++ {
331358
weightEstimator.AddTaprootKeySpendInput(
@@ -335,6 +362,10 @@ func (l *StaticAddressLoopIn) htlcWeight() lntypes.WeightUnit {
335362

336363
weightEstimator.AddP2WSHOutput()
337364

365+
if hasChange {
366+
weightEstimator.AddP2TROutput()
367+
}
368+
338369
return weightEstimator.Weight()
339370
}
340371

@@ -373,11 +404,25 @@ func (l *StaticAddressLoopIn) createHtlcSweepTx(ctx context.Context,
373404
return nil, err
374405
}
375406

407+
// Check if the htlc tx has a change output. If so we need to select the
408+
// non-change output index to construct the sweep with.
409+
htlcInputIndex := uint32(0)
410+
if len(htlcTx.TxOut) == 2 {
411+
// If the first htlc tx output matches our static address
412+
// script we need to select the second output to sweep from.
413+
if bytes.Equal(
414+
htlcTx.TxOut[0].PkScript, l.AddressParams.PkScript,
415+
) {
416+
417+
htlcInputIndex = 1
418+
}
419+
}
420+
376421
// Add the htlc input.
377422
sweepTx.AddTxIn(&wire.TxIn{
378423
PreviousOutPoint: wire.OutPoint{
379424
Hash: htlcTx.TxHash(),
380-
Index: 0,
425+
Index: htlcInputIndex,
381426
},
382427
SignatureScript: htlc.SigScript,
383428
Sequence: htlc.SuccessSequence(),

0 commit comments

Comments
 (0)