Skip to content

Commit 9590faf

Browse files
authored
Merge pull request #1066 from starius/sweephtlc5
Add a command to manually sweep loop-out HTLC
2 parents 1f1f668 + 579ea31 commit 9590faf

File tree

20 files changed

+2346
-404
lines changed

20 files changed

+2346
-404
lines changed

cmd/loop/loopout.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,9 @@ var loopOutCommand = &cli.Command{
121121
verboseFlag,
122122
channelFlag,
123123
},
124+
Commands: []*cli.Command{
125+
sweepHtlcCommand,
126+
},
124127
Action: loopOut,
125128
}
126129

cmd/loop/sweephtlc.go

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"encoding/hex"
7+
"fmt"
8+
9+
"github.com/btcsuite/btcd/wire"
10+
"github.com/lightninglabs/loop/looprpc"
11+
"github.com/urfave/cli/v3"
12+
)
13+
14+
// sweepHtlcCommand exposes HTLC success-path sweeping over loop CLI.
15+
var sweepHtlcCommand = &cli.Command{
16+
Name: "sweephtlc",
17+
Usage: "sweep an HTLC output using the preimage success path",
18+
Flags: []cli.Flag{
19+
&cli.StringFlag{
20+
Name: "outpoint",
21+
Usage: "htlc outpoint to sweep (format: txid:vout)",
22+
Required: true,
23+
},
24+
&cli.StringFlag{
25+
Name: "htlcaddr",
26+
Usage: "htlc address corresponding to the outpoint",
27+
Required: true,
28+
},
29+
&cli.UintFlag{
30+
Name: "feerate",
31+
Usage: "fee rate to use in sat/vbyte",
32+
Required: true,
33+
},
34+
&cli.StringFlag{
35+
Name: "destaddr",
36+
Usage: "optional destination address; defaults to a " +
37+
"new wallet address",
38+
},
39+
&cli.StringFlag{
40+
Name: "preimage",
41+
Usage: "optional preimage hex to override stored " +
42+
"swap preimage",
43+
},
44+
&cli.BoolFlag{
45+
Name: "publish",
46+
Usage: "publish the sweep transaction immediately",
47+
Value: false,
48+
},
49+
},
50+
Hidden: true,
51+
Action: sweepHtlc,
52+
}
53+
54+
// sweepHtlc executes the SweepHtlc RPC and prints the sweep transaction hex.
55+
func sweepHtlc(ctx context.Context, cmd *cli.Command) error {
56+
// Loopd connecting client.
57+
client, cleanup, err := getClient(cmd)
58+
if err != nil {
59+
return err
60+
}
61+
defer cleanup()
62+
63+
// Find the preimage if the user passed it.
64+
var preimage []byte
65+
if cmd.IsSet("preimage") {
66+
preimage, err = hex.DecodeString(cmd.String("preimage"))
67+
if err != nil {
68+
return fmt.Errorf("invalid preimage: %w", err)
69+
}
70+
}
71+
72+
// Call SweepHtlc on loopd trying to sweep the HTLC.
73+
resp, err := client.SweepHtlc(ctx, &looprpc.SweepHtlcRequest{
74+
Outpoint: cmd.String("outpoint"),
75+
DestAddress: cmd.String("destaddr"),
76+
HtlcAddress: cmd.String("htlcaddr"),
77+
SatPerVbyte: uint32(cmd.Uint("feerate")),
78+
Preimage: preimage,
79+
Publish: cmd.Bool("publish"),
80+
})
81+
if err != nil {
82+
return err
83+
}
84+
85+
// Always display the raw sweep transaction.
86+
fmt.Printf("sweep_tx_hex: %x\n", resp.SweepTx)
87+
88+
// Report publish status in a user-friendly way based on response.
89+
switch {
90+
case resp.GetNotRequested() != nil:
91+
fmt.Println("publish: not requested (pass --publish to " +
92+
"broadcast)")
93+
94+
case resp.GetPublished() != nil:
95+
fmt.Println("publish: success")
96+
97+
case resp.GetFailed() != nil:
98+
errMsg := resp.GetFailed().GetError()
99+
fmt.Printf("publish: failed: %s\n", errMsg)
100+
101+
return fmt.Errorf("publish failed: %s", errMsg)
102+
103+
default:
104+
fmt.Println("publish: unknown status")
105+
}
106+
107+
// Print txid if the transaction is valid.
108+
var tx wire.MsgTx
109+
if err := tx.Deserialize(bytes.NewReader(resp.SweepTx)); err == nil {
110+
fmt.Printf("sweep_txid: %s\n", tx.TxHash().String())
111+
} else {
112+
fmt.Printf("sweep_txid: could not decode tx: %v\n", err)
113+
}
114+
115+
// Print the fee-rate.
116+
fmt.Printf("fee_sats: %d\n", resp.FeeSats)
117+
118+
return nil
119+
}

docs/loop.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,28 @@ The following flags are supported:
5353
| `--channel="…"` | the comma-separated list of short channel IDs of the channels to loop out | string |
5454
| `--help` (`-h`) | show help | bool | `false` |
5555

56+
### `out sweephtlc` subcommand
57+
58+
sweep an HTLC output using the preimage success path.
59+
60+
Usage:
61+
62+
```bash
63+
$ loop [GLOBAL FLAGS] out sweephtlc [COMMAND FLAGS] [ARGUMENTS...]
64+
```
65+
66+
The following flags are supported:
67+
68+
| Name | Description | Type | Default value |
69+
|------------------|----------------------------------------------------------------|--------|:-------------:|
70+
| `--outpoint="…"` | htlc outpoint to sweep (format: txid:vout) | string |
71+
| `--htlcaddr="…"` | htlc address corresponding to the outpoint | string |
72+
| `--feerate="…"` | fee rate to use in sat/vbyte | uint | `0` |
73+
| `--destaddr="…"` | optional destination address; defaults to a new wallet address | string |
74+
| `--preimage="…"` | optional preimage hex to override stored swap preimage | string |
75+
| `--publish` | publish the sweep transaction immediately | bool | `false` |
76+
| `--help` (`-h`) | show help | bool | `false` |
77+
5678
### `in` command
5779

5880
perform an on-chain to off-chain swap (loop in).

instantout/actions.go

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -317,9 +317,16 @@ func (f *FSM) BuildHTLCAction(ctx context.Context,
317317
return f.handleErrorAndUnlockReservations(ctx, err)
318318
}
319319

320+
minRelayFee, err := f.cfg.Wallet.MinRelayFee(ctx)
321+
if err != nil {
322+
return f.handleErrorAndUnlockReservations(ctx, err)
323+
}
324+
320325
// Now that our nonces are set, we can create and sign the htlc
321326
// transaction.
322-
htlcTx, err := f.InstantOut.createHtlcTransaction(f.cfg.Network)
327+
htlcTx, err := f.InstantOut.createHtlcTransaction(
328+
f.cfg.Network, minRelayFee,
329+
)
323330
if err != nil {
324331
return f.handleErrorAndUnlockReservations(ctx, err)
325332
}
@@ -382,6 +389,11 @@ func (f *FSM) PushPreimageAction(ctx context.Context,
382389
return f.handleErrorAndUnlockReservations(ctx, err)
383390
}
384391

392+
minRelayFee, err := f.cfg.Wallet.MinRelayFee(ctx)
393+
if err != nil {
394+
return f.handleErrorAndUnlockReservations(ctx, err)
395+
}
396+
385397
pushPreImageRes, err := f.cfg.InstantOutClient.PushPreimage(
386398
ctx,
387399
&swapserverrpc.PushPreimageRequest{
@@ -400,7 +412,9 @@ func (f *FSM) PushPreimageAction(ctx context.Context,
400412

401413
// Now that we have the sweepless sweep signatures we can build and
402414
// publish the sweepless sweep transaction.
403-
sweepTx, err := f.InstantOut.createSweeplessSweepTx(feeRate)
415+
sweepTx, err := f.InstantOut.createSweeplessSweepTx(
416+
feeRate, minRelayFee,
417+
)
404418
if err != nil {
405419
f.LastActionError = err
406420
return OnErrorPublishHtlc

instantout/instantout.go

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"github.com/lightninglabs/loop/instantout/reservation"
1919
"github.com/lightninglabs/loop/loopdb"
2020
"github.com/lightninglabs/loop/swap"
21+
"github.com/lightninglabs/loop/utils"
2122
"github.com/lightningnetwork/lnd/input"
2223
"github.com/lightningnetwork/lnd/keychain"
2324
"github.com/lightningnetwork/lnd/lntypes"
@@ -145,8 +146,8 @@ func (i *InstantOut) getInputReservations() (InputReservations, error) {
145146
}
146147

147148
// createHtlcTransaction creates the htlc transaction for the instant out.
148-
func (i *InstantOut) createHtlcTransaction(network *chaincfg.Params) (
149-
*wire.MsgTx, error) {
149+
func (i *InstantOut) createHtlcTransaction(network *chaincfg.Params,
150+
minRelayFeeRate chainfee.SatPerKWeight) (*wire.MsgTx, error) {
150151

151152
if network == nil {
152153
return nil, errors.New("no network provided")
@@ -170,7 +171,16 @@ func (i *InstantOut) createHtlcTransaction(network *chaincfg.Params) (
170171
// Estimate the fee
171172
weight := htlcWeight(len(inputReservations))
172173
fee := i.htlcFeeRate.FeeForWeight(weight)
173-
if fee > i.Value/5 {
174+
175+
// We cap the fee at 20% of the deposit value.
176+
_, clamped, err := utils.ClampSweepFee(
177+
fee, i.Value, utils.MaxFeeToAmountRatio, minRelayFeeRate,
178+
weight,
179+
)
180+
if err != nil {
181+
return nil, err
182+
}
183+
if clamped {
174184
return nil, errors.New("fee is higher than 20% of " +
175185
"sweep value")
176186
}
@@ -193,8 +203,8 @@ func (i *InstantOut) createHtlcTransaction(network *chaincfg.Params) (
193203

194204
// createSweeplessSweepTx creates the sweepless sweep transaction for the
195205
// instant out.
196-
func (i *InstantOut) createSweeplessSweepTx(feerate chainfee.SatPerKWeight) (
197-
*wire.MsgTx, error) {
206+
func (i *InstantOut) createSweeplessSweepTx(feerate,
207+
minRelayFeeRate chainfee.SatPerKWeight) (*wire.MsgTx, error) {
198208

199209
inputReservations, err := i.getInputReservations()
200210
if err != nil {
@@ -214,7 +224,14 @@ func (i *InstantOut) createSweeplessSweepTx(feerate chainfee.SatPerKWeight) (
214224
// Estimate the fee
215225
weight := sweeplessSweepWeight(len(inputReservations))
216226
fee := feerate.FeeForWeight(weight)
217-
if fee > i.Value/5 {
227+
_, clamped, err := utils.ClampSweepFee(
228+
fee, i.Value, utils.MaxFeeToAmountRatio, minRelayFeeRate,
229+
weight,
230+
)
231+
if err != nil {
232+
return nil, err
233+
}
234+
if clamped {
218235
return nil, errors.New("fee is higher than 20% of " +
219236
"sweep value")
220237
}

loopd/swapclient_server.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1365,6 +1365,17 @@ func (s *swapClientServer) StopDaemon(ctx context.Context,
13651365
return &looprpc.StopDaemonResponse{}, nil
13661366
}
13671367

1368+
// SweepHtlc spends a Loop HTLC output using the success path and a known
1369+
// preimage.
1370+
func (s *swapClientServer) SweepHtlc(ctx context.Context,
1371+
req *looprpc.SweepHtlcRequest) (*looprpc.SweepHtlcResponse, error) {
1372+
1373+
return sweepHtlc(
1374+
ctx, req, s.lnd.ChainParams, s.impl.Store,
1375+
s.lnd.ChainNotifier, s.lnd.WalletKit, s.lnd.Signer,
1376+
)
1377+
}
1378+
13681379
// GetLiquidityParams gets our current liquidity manager's parameters.
13691380
func (s *swapClientServer) GetLiquidityParams(_ context.Context,
13701381
_ *looprpc.GetLiquidityParamsRequest) (*looprpc.LiquidityParameters,

0 commit comments

Comments
 (0)