mirror of
https://github.com/lightninglabs/loop.git
synced 2026-08-13 12:33:03 +02:00
Merge pull request #1066 from starius/sweephtlc5
Add a command to manually sweep loop-out HTLC
This commit is contained in:
commit
9590fafae8
20 changed files with 2346 additions and 404 deletions
|
|
@ -121,6 +121,9 @@ var loopOutCommand = &cli.Command{
|
|||
verboseFlag,
|
||||
channelFlag,
|
||||
},
|
||||
Commands: []*cli.Command{
|
||||
sweepHtlcCommand,
|
||||
},
|
||||
Action: loopOut,
|
||||
}
|
||||
|
||||
|
|
|
|||
119
cmd/loop/sweephtlc.go
Normal file
119
cmd/loop/sweephtlc.go
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/lightninglabs/loop/looprpc"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
// sweepHtlcCommand exposes HTLC success-path sweeping over loop CLI.
|
||||
var sweepHtlcCommand = &cli.Command{
|
||||
Name: "sweephtlc",
|
||||
Usage: "sweep an HTLC output using the preimage success path",
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "outpoint",
|
||||
Usage: "htlc outpoint to sweep (format: txid:vout)",
|
||||
Required: true,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "htlcaddr",
|
||||
Usage: "htlc address corresponding to the outpoint",
|
||||
Required: true,
|
||||
},
|
||||
&cli.UintFlag{
|
||||
Name: "feerate",
|
||||
Usage: "fee rate to use in sat/vbyte",
|
||||
Required: true,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "destaddr",
|
||||
Usage: "optional destination address; defaults to a " +
|
||||
"new wallet address",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "preimage",
|
||||
Usage: "optional preimage hex to override stored " +
|
||||
"swap preimage",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "publish",
|
||||
Usage: "publish the sweep transaction immediately",
|
||||
Value: false,
|
||||
},
|
||||
},
|
||||
Hidden: true,
|
||||
Action: sweepHtlc,
|
||||
}
|
||||
|
||||
// sweepHtlc executes the SweepHtlc RPC and prints the sweep transaction hex.
|
||||
func sweepHtlc(ctx context.Context, cmd *cli.Command) error {
|
||||
// Loopd connecting client.
|
||||
client, cleanup, err := getClient(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
// Find the preimage if the user passed it.
|
||||
var preimage []byte
|
||||
if cmd.IsSet("preimage") {
|
||||
preimage, err = hex.DecodeString(cmd.String("preimage"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid preimage: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Call SweepHtlc on loopd trying to sweep the HTLC.
|
||||
resp, err := client.SweepHtlc(ctx, &looprpc.SweepHtlcRequest{
|
||||
Outpoint: cmd.String("outpoint"),
|
||||
DestAddress: cmd.String("destaddr"),
|
||||
HtlcAddress: cmd.String("htlcaddr"),
|
||||
SatPerVbyte: uint32(cmd.Uint("feerate")),
|
||||
Preimage: preimage,
|
||||
Publish: cmd.Bool("publish"),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Always display the raw sweep transaction.
|
||||
fmt.Printf("sweep_tx_hex: %x\n", resp.SweepTx)
|
||||
|
||||
// Report publish status in a user-friendly way based on response.
|
||||
switch {
|
||||
case resp.GetNotRequested() != nil:
|
||||
fmt.Println("publish: not requested (pass --publish to " +
|
||||
"broadcast)")
|
||||
|
||||
case resp.GetPublished() != nil:
|
||||
fmt.Println("publish: success")
|
||||
|
||||
case resp.GetFailed() != nil:
|
||||
errMsg := resp.GetFailed().GetError()
|
||||
fmt.Printf("publish: failed: %s\n", errMsg)
|
||||
|
||||
return fmt.Errorf("publish failed: %s", errMsg)
|
||||
|
||||
default:
|
||||
fmt.Println("publish: unknown status")
|
||||
}
|
||||
|
||||
// Print txid if the transaction is valid.
|
||||
var tx wire.MsgTx
|
||||
if err := tx.Deserialize(bytes.NewReader(resp.SweepTx)); err == nil {
|
||||
fmt.Printf("sweep_txid: %s\n", tx.TxHash().String())
|
||||
} else {
|
||||
fmt.Printf("sweep_txid: could not decode tx: %v\n", err)
|
||||
}
|
||||
|
||||
// Print the fee-rate.
|
||||
fmt.Printf("fee_sats: %d\n", resp.FeeSats)
|
||||
|
||||
return nil
|
||||
}
|
||||
22
docs/loop.md
22
docs/loop.md
|
|
@ -53,6 +53,28 @@ The following flags are supported:
|
|||
| `--channel="…"` | the comma-separated list of short channel IDs of the channels to loop out | string |
|
||||
| `--help` (`-h`) | show help | bool | `false` |
|
||||
|
||||
### `out sweephtlc` subcommand
|
||||
|
||||
sweep an HTLC output using the preimage success path.
|
||||
|
||||
Usage:
|
||||
|
||||
```bash
|
||||
$ loop [GLOBAL FLAGS] out sweephtlc [COMMAND FLAGS] [ARGUMENTS...]
|
||||
```
|
||||
|
||||
The following flags are supported:
|
||||
|
||||
| Name | Description | Type | Default value |
|
||||
|------------------|----------------------------------------------------------------|--------|:-------------:|
|
||||
| `--outpoint="…"` | htlc outpoint to sweep (format: txid:vout) | string |
|
||||
| `--htlcaddr="…"` | htlc address corresponding to the outpoint | string |
|
||||
| `--feerate="…"` | fee rate to use in sat/vbyte | uint | `0` |
|
||||
| `--destaddr="…"` | optional destination address; defaults to a new wallet address | string |
|
||||
| `--preimage="…"` | optional preimage hex to override stored swap preimage | string |
|
||||
| `--publish` | publish the sweep transaction immediately | bool | `false` |
|
||||
| `--help` (`-h`) | show help | bool | `false` |
|
||||
|
||||
### `in` command
|
||||
|
||||
perform an on-chain to off-chain swap (loop in).
|
||||
|
|
|
|||
|
|
@ -317,9 +317,16 @@ func (f *FSM) BuildHTLCAction(ctx context.Context,
|
|||
return f.handleErrorAndUnlockReservations(ctx, err)
|
||||
}
|
||||
|
||||
minRelayFee, err := f.cfg.Wallet.MinRelayFee(ctx)
|
||||
if err != nil {
|
||||
return f.handleErrorAndUnlockReservations(ctx, err)
|
||||
}
|
||||
|
||||
// Now that our nonces are set, we can create and sign the htlc
|
||||
// transaction.
|
||||
htlcTx, err := f.InstantOut.createHtlcTransaction(f.cfg.Network)
|
||||
htlcTx, err := f.InstantOut.createHtlcTransaction(
|
||||
f.cfg.Network, minRelayFee,
|
||||
)
|
||||
if err != nil {
|
||||
return f.handleErrorAndUnlockReservations(ctx, err)
|
||||
}
|
||||
|
|
@ -382,6 +389,11 @@ func (f *FSM) PushPreimageAction(ctx context.Context,
|
|||
return f.handleErrorAndUnlockReservations(ctx, err)
|
||||
}
|
||||
|
||||
minRelayFee, err := f.cfg.Wallet.MinRelayFee(ctx)
|
||||
if err != nil {
|
||||
return f.handleErrorAndUnlockReservations(ctx, err)
|
||||
}
|
||||
|
||||
pushPreImageRes, err := f.cfg.InstantOutClient.PushPreimage(
|
||||
ctx,
|
||||
&swapserverrpc.PushPreimageRequest{
|
||||
|
|
@ -400,7 +412,9 @@ func (f *FSM) PushPreimageAction(ctx context.Context,
|
|||
|
||||
// Now that we have the sweepless sweep signatures we can build and
|
||||
// publish the sweepless sweep transaction.
|
||||
sweepTx, err := f.InstantOut.createSweeplessSweepTx(feeRate)
|
||||
sweepTx, err := f.InstantOut.createSweeplessSweepTx(
|
||||
feeRate, minRelayFee,
|
||||
)
|
||||
if err != nil {
|
||||
f.LastActionError = err
|
||||
return OnErrorPublishHtlc
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import (
|
|||
"github.com/lightninglabs/loop/instantout/reservation"
|
||||
"github.com/lightninglabs/loop/loopdb"
|
||||
"github.com/lightninglabs/loop/swap"
|
||||
"github.com/lightninglabs/loop/utils"
|
||||
"github.com/lightningnetwork/lnd/input"
|
||||
"github.com/lightningnetwork/lnd/keychain"
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
|
|
@ -145,8 +146,8 @@ func (i *InstantOut) getInputReservations() (InputReservations, error) {
|
|||
}
|
||||
|
||||
// createHtlcTransaction creates the htlc transaction for the instant out.
|
||||
func (i *InstantOut) createHtlcTransaction(network *chaincfg.Params) (
|
||||
*wire.MsgTx, error) {
|
||||
func (i *InstantOut) createHtlcTransaction(network *chaincfg.Params,
|
||||
minRelayFeeRate chainfee.SatPerKWeight) (*wire.MsgTx, error) {
|
||||
|
||||
if network == nil {
|
||||
return nil, errors.New("no network provided")
|
||||
|
|
@ -170,7 +171,16 @@ func (i *InstantOut) createHtlcTransaction(network *chaincfg.Params) (
|
|||
// Estimate the fee
|
||||
weight := htlcWeight(len(inputReservations))
|
||||
fee := i.htlcFeeRate.FeeForWeight(weight)
|
||||
if fee > i.Value/5 {
|
||||
|
||||
// We cap the fee at 20% of the deposit value.
|
||||
_, clamped, err := utils.ClampSweepFee(
|
||||
fee, i.Value, utils.MaxFeeToAmountRatio, minRelayFeeRate,
|
||||
weight,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if clamped {
|
||||
return nil, errors.New("fee is higher than 20% of " +
|
||||
"sweep value")
|
||||
}
|
||||
|
|
@ -193,8 +203,8 @@ func (i *InstantOut) createHtlcTransaction(network *chaincfg.Params) (
|
|||
|
||||
// createSweeplessSweepTx creates the sweepless sweep transaction for the
|
||||
// instant out.
|
||||
func (i *InstantOut) createSweeplessSweepTx(feerate chainfee.SatPerKWeight) (
|
||||
*wire.MsgTx, error) {
|
||||
func (i *InstantOut) createSweeplessSweepTx(feerate,
|
||||
minRelayFeeRate chainfee.SatPerKWeight) (*wire.MsgTx, error) {
|
||||
|
||||
inputReservations, err := i.getInputReservations()
|
||||
if err != nil {
|
||||
|
|
@ -214,7 +224,14 @@ func (i *InstantOut) createSweeplessSweepTx(feerate chainfee.SatPerKWeight) (
|
|||
// Estimate the fee
|
||||
weight := sweeplessSweepWeight(len(inputReservations))
|
||||
fee := feerate.FeeForWeight(weight)
|
||||
if fee > i.Value/5 {
|
||||
_, clamped, err := utils.ClampSweepFee(
|
||||
fee, i.Value, utils.MaxFeeToAmountRatio, minRelayFeeRate,
|
||||
weight,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if clamped {
|
||||
return nil, errors.New("fee is higher than 20% of " +
|
||||
"sweep value")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1365,6 +1365,17 @@ func (s *swapClientServer) StopDaemon(ctx context.Context,
|
|||
return &looprpc.StopDaemonResponse{}, nil
|
||||
}
|
||||
|
||||
// SweepHtlc spends a Loop HTLC output using the success path and a known
|
||||
// preimage.
|
||||
func (s *swapClientServer) SweepHtlc(ctx context.Context,
|
||||
req *looprpc.SweepHtlcRequest) (*looprpc.SweepHtlcResponse, error) {
|
||||
|
||||
return sweepHtlc(
|
||||
ctx, req, s.lnd.ChainParams, s.impl.Store,
|
||||
s.lnd.ChainNotifier, s.lnd.WalletKit, s.lnd.Signer,
|
||||
)
|
||||
}
|
||||
|
||||
// GetLiquidityParams gets our current liquidity manager's parameters.
|
||||
func (s *swapClientServer) GetLiquidityParams(_ context.Context,
|
||||
_ *looprpc.GetLiquidityParamsRequest) (*looprpc.LiquidityParameters,
|
||||
|
|
|
|||
402
loopd/sweep_htlc.go
Normal file
402
loopd/sweep_htlc.go
Normal file
|
|
@ -0,0 +1,402 @@
|
|||
package loopd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/btcsuite/btcd/chaincfg"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/lightninglabs/lndclient"
|
||||
"github.com/lightninglabs/loop/labels"
|
||||
"github.com/lightninglabs/loop/loopdb"
|
||||
"github.com/lightninglabs/loop/looprpc"
|
||||
"github.com/lightninglabs/loop/swap"
|
||||
"github.com/lightninglabs/loop/sweep"
|
||||
"github.com/lightninglabs/loop/utils"
|
||||
"github.com/lightningnetwork/lnd/chainntnfs"
|
||||
"github.com/lightningnetwork/lnd/input"
|
||||
"github.com/lightningnetwork/lnd/keychain"
|
||||
"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
"github.com/lightningnetwork/lnd/lnwallet"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// loopOutStore abstracts the minimal store API needed to look up loop-out
|
||||
// swaps.
|
||||
type loopOutStore interface {
|
||||
// FetchLoopOutSwaps returns all loop-out swaps currently in the store.
|
||||
FetchLoopOutSwaps(ctx context.Context) ([]*loopdb.LoopOut, error)
|
||||
}
|
||||
|
||||
// htlcChainNotifier defines the minimal notifier API to watch for a tx
|
||||
// confirmation.
|
||||
type htlcChainNotifier interface {
|
||||
RegisterConfirmationsNtfn(ctx context.Context, txid *chainhash.Hash,
|
||||
pkScript []byte, numConfs, heightHint int32,
|
||||
opts ...lndclient.NotifierOption) (
|
||||
chan *chainntnfs.TxConfirmation, chan error, error)
|
||||
}
|
||||
|
||||
// htlcWallet abstracts the wallet calls used for sweeping.
|
||||
type htlcWallet interface {
|
||||
// NextAddr derives the next address from the given account and type.
|
||||
NextAddr(ctx context.Context, account string,
|
||||
addrType walletrpc.AddressType,
|
||||
change bool) (btcutil.Address, error)
|
||||
|
||||
// PublishTransaction broadcasts the transaction with the given label.
|
||||
PublishTransaction(ctx context.Context, tx *wire.MsgTx,
|
||||
label string) error
|
||||
|
||||
// MinRelayFee returns the current minimum relay fee in sat/kw.
|
||||
MinRelayFee(ctx context.Context) (chainfee.SatPerKWeight, error)
|
||||
}
|
||||
|
||||
// htlcSigner signs the success path spend.
|
||||
type htlcSigner interface {
|
||||
SignOutputRaw(ctx context.Context, tx *wire.MsgTx,
|
||||
signDescriptors []*lndclient.SignDescriptor,
|
||||
prevOutputs []*wire.TxOut) ([][]byte, error)
|
||||
}
|
||||
|
||||
// sweepHtlc spends a Loop HTLC output using the success path and a known
|
||||
// preimage.
|
||||
func sweepHtlc(ctx context.Context, req *looprpc.SweepHtlcRequest,
|
||||
chainParams *chaincfg.Params, store loopOutStore,
|
||||
notifier htlcChainNotifier, wallet htlcWallet,
|
||||
signer htlcSigner) (*looprpc.SweepHtlcResponse, error) {
|
||||
|
||||
// Make sure that the request has all required inputs.
|
||||
if req.Outpoint == "" {
|
||||
return nil, status.Error(codes.InvalidArgument,
|
||||
"outpoint required")
|
||||
}
|
||||
if req.HtlcAddress == "" {
|
||||
return nil, status.Error(codes.InvalidArgument,
|
||||
"htlc_address required")
|
||||
}
|
||||
if req.SatPerVbyte == 0 {
|
||||
return nil, status.Error(codes.InvalidArgument,
|
||||
"sat_per_vbyte required")
|
||||
}
|
||||
|
||||
// Parse the inputs.
|
||||
htlcAddr, err := btcutil.DecodeAddress(
|
||||
req.HtlcAddress, chainParams,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument,
|
||||
"invalid htlc_address: %v", err)
|
||||
}
|
||||
|
||||
htlcPkScript, err := txscript.PayToAddrScript(htlcAddr)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument,
|
||||
"invalid htlc_address script: %v", err)
|
||||
}
|
||||
|
||||
htlcOutpoint, err := wire.NewOutPointFromString(req.Outpoint)
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.InvalidArgument, err.Error())
|
||||
}
|
||||
|
||||
// Destination address: honor a provided override or derive a fresh
|
||||
// wallet address from the default account.
|
||||
var sweepAddr btcutil.Address
|
||||
if req.DestAddress != "" {
|
||||
sweepAddr, err = btcutil.DecodeAddress(
|
||||
req.DestAddress, chainParams,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument,
|
||||
"invalid dest_address: %v", err)
|
||||
}
|
||||
} else {
|
||||
sweepAddr, err = wallet.NextAddr(
|
||||
ctx, lnwallet.DefaultAccountName,
|
||||
walletrpc.AddressType_TAPROOT_PUBKEY,
|
||||
false,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal,
|
||||
"derive sweep address: %v", err)
|
||||
}
|
||||
infof("sweephtlc: generated new destination address: %v",
|
||||
sweepAddr.EncodeAddress())
|
||||
}
|
||||
|
||||
sweepPkScript, err := txscript.PayToAddrScript(sweepAddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
infof("sweephtlc: start sweep for %v -> %v", req.Outpoint,
|
||||
sweepAddr.EncodeAddress())
|
||||
|
||||
// Locate the loop-out swap whose HTLC script matches the outpoint so
|
||||
// we can obtain keys and the stored preimage.
|
||||
swaps, err := store.FetchLoopOutSwaps(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var (
|
||||
targetSwap *loopdb.LoopOut
|
||||
targetHtlc *swap.Htlc
|
||||
)
|
||||
|
||||
for _, swp := range swaps {
|
||||
htlc, htlcErr := utils.GetHtlc(
|
||||
swp.Hash, &swp.Contract.SwapContract,
|
||||
chainParams,
|
||||
)
|
||||
if htlcErr != nil {
|
||||
return nil, htlcErr
|
||||
}
|
||||
|
||||
if bytes.Equal(htlc.PkScript, htlcPkScript) {
|
||||
targetSwap = swp
|
||||
targetHtlc = htlc
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if targetSwap == nil || targetHtlc == nil {
|
||||
return nil, status.Error(codes.NotFound,
|
||||
"no matching swap HTLC found")
|
||||
}
|
||||
|
||||
infof("sweephtlc: matched swap %v at height hint %v",
|
||||
targetSwap.Hash, targetSwap.Contract.InitiationHeight)
|
||||
|
||||
if targetSwap.Contract.InitiationHeight <= 0 {
|
||||
return nil, status.Errorf(codes.InvalidArgument,
|
||||
"invalid initiation height %d",
|
||||
targetSwap.Contract.InitiationHeight)
|
||||
}
|
||||
|
||||
// Wait for a confirmation so we can read the full transaction even if
|
||||
// it's not in our wallet.
|
||||
infof("sweephtlc: registering conf ntfn for %v hint=%v",
|
||||
req.Outpoint, targetSwap.Contract.InitiationHeight)
|
||||
confChan, errChan, err := notifier.RegisterConfirmationsNtfn(
|
||||
ctx, &htlcOutpoint.Hash, htlcPkScript, 1,
|
||||
targetSwap.Contract.InitiationHeight,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal,
|
||||
"register conf ntfn: %v", err)
|
||||
}
|
||||
|
||||
var (
|
||||
htlcTxOut *wire.TxOut
|
||||
fundingTx *wire.MsgTx
|
||||
)
|
||||
|
||||
infof("sweephtlc: waiting for confirmation of %v", req.Outpoint)
|
||||
select {
|
||||
case conf := <-confChan:
|
||||
fundingTx = conf.Tx
|
||||
infof("sweephtlc: funding confirmed at height %v",
|
||||
conf.BlockHeight)
|
||||
|
||||
case ntfnErr := <-errChan:
|
||||
infof("sweephtlc: conf ntfn error for %v: %v",
|
||||
req.Outpoint, ntfnErr)
|
||||
|
||||
return nil, status.Errorf(codes.Internal,
|
||||
"conf ntfn: %v", ntfnErr)
|
||||
|
||||
case <-ctx.Done():
|
||||
infof("sweephtlc: context done waiting for %v: %v",
|
||||
req.Outpoint, ctx.Err())
|
||||
|
||||
return nil, status.Errorf(codes.DeadlineExceeded,
|
||||
"waiting for transaction details")
|
||||
}
|
||||
|
||||
if int(htlcOutpoint.Index) >= len(fundingTx.TxOut) {
|
||||
return nil, status.Errorf(codes.InvalidArgument,
|
||||
"vout %d out of range", htlcOutpoint.Index)
|
||||
}
|
||||
|
||||
htlcTxOut = fundingTx.TxOut[htlcOutpoint.Index]
|
||||
|
||||
if !bytes.Equal(htlcTxOut.PkScript, htlcPkScript) {
|
||||
return nil, status.Error(codes.InvalidArgument,
|
||||
"outpoint script does not match HTLC address")
|
||||
}
|
||||
|
||||
infof("sweephtlc: swap hash validated for %v", req.Outpoint)
|
||||
|
||||
// Pick a preimage: prefer the caller-provided override, otherwise use
|
||||
// the swap's stored preimage.
|
||||
var preimage lntypes.Preimage
|
||||
if len(req.Preimage) > 0 {
|
||||
preimage, err = lntypes.MakePreimage(req.Preimage)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument,
|
||||
"invalid preimage: %v", err)
|
||||
}
|
||||
} else {
|
||||
preimage = targetSwap.Contract.Preimage
|
||||
}
|
||||
|
||||
if preimage.Hash() != targetHtlc.Hash {
|
||||
return nil, status.Error(codes.InvalidArgument,
|
||||
"preimage does not match HTLC hash")
|
||||
}
|
||||
|
||||
infof("sweephtlc: sweeping to %v with feerate %v sat/vbyte",
|
||||
sweepAddr.EncodeAddress(), req.SatPerVbyte)
|
||||
|
||||
// Estimate fee for the success-path spend weight.
|
||||
var estimator input.TxWeightEstimator
|
||||
err = targetHtlc.AddSuccessToEstimator(&estimator)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument,
|
||||
"failed to estimate tx input weight: %v", err)
|
||||
}
|
||||
err = sweep.AddOutputEstimate(&estimator, sweepAddr)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument,
|
||||
"failed to estimate tx output weight: %v", err)
|
||||
}
|
||||
|
||||
// Convert the requested fee rate to sat/kw for fee computation.
|
||||
feeRate := chainfee.SatPerVByte(req.SatPerVbyte).FeePerKWeight()
|
||||
fee := feeRate.FeeForWeightRoundUp(estimator.Weight())
|
||||
|
||||
// Make sure the fee is fine.
|
||||
htlcValue := btcutil.Amount(htlcTxOut.Value)
|
||||
if htlcValue <= fee {
|
||||
return nil, status.Error(codes.InvalidArgument,
|
||||
"fee exceeds HTLC value")
|
||||
}
|
||||
|
||||
minRelayFeeRate, err := wallet.MinRelayFee(ctx)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal,
|
||||
"min relay fee: %v", err)
|
||||
}
|
||||
|
||||
fee, clamped, err := utils.ClampSweepFee(
|
||||
fee, htlcValue, utils.MaxFeeToAmountRatio, minRelayFeeRate,
|
||||
estimator.Weight(),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument,
|
||||
"fee too low for relay after clamp: %v", err)
|
||||
}
|
||||
if clamped {
|
||||
return nil, status.Errorf(codes.InvalidArgument,
|
||||
"fee exceeds %.0f%% of HTLC value; lower sat_per_vbyte",
|
||||
utils.MaxFeeToAmountRatio*100,
|
||||
)
|
||||
}
|
||||
|
||||
// Build the sweep transaction spending the HTLC via the success path.
|
||||
sweepTx := wire.NewMsgTx(2)
|
||||
sweepTx.AddTxIn(&wire.TxIn{
|
||||
PreviousOutPoint: *htlcOutpoint,
|
||||
Sequence: targetHtlc.SuccessSequence(),
|
||||
})
|
||||
sweepTx.AddTxOut(&wire.TxOut{
|
||||
PkScript: sweepPkScript,
|
||||
Value: int64(htlcValue - fee),
|
||||
})
|
||||
|
||||
infof("sweephtlc: signing sweep spending %v", req.Outpoint)
|
||||
|
||||
prevOut := &wire.TxOut{
|
||||
Value: int64(htlcValue),
|
||||
PkScript: targetHtlc.PkScript,
|
||||
}
|
||||
signDesc := lndclient.SignDescriptor{
|
||||
WitnessScript: targetHtlc.SuccessScript(),
|
||||
Output: prevOut,
|
||||
HashType: targetHtlc.SigHash(),
|
||||
InputIndex: 0,
|
||||
KeyDesc: keychain.KeyDescriptor{
|
||||
KeyLocator: targetSwap.Contract.HtlcKeys.
|
||||
ClientScriptKeyLocator,
|
||||
},
|
||||
}
|
||||
if targetHtlc.Version == swap.HtlcV3 {
|
||||
signDesc.SignMethod = input.TaprootScriptSpendSignMethod
|
||||
}
|
||||
|
||||
// Sign the HTLC spend.
|
||||
rawSigs, err := signer.SignOutputRaw(
|
||||
ctx, sweepTx, []*lndclient.SignDescriptor{&signDesc},
|
||||
[]*wire.TxOut{prevOut},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sig := rawSigs[0]
|
||||
|
||||
infof("sweephtlc: witness assembled, tx size=%d vbytes",
|
||||
sweepTx.SerializeSize())
|
||||
|
||||
// Assemble the success witness using the signature and preimage.
|
||||
witness, err := targetHtlc.GenSuccessWitness(sig, preimage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sweepTx.TxIn[0].Witness = witness
|
||||
|
||||
var rawBuf bytes.Buffer
|
||||
err = sweepTx.Serialize(&rawBuf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rawTx := rawBuf.Bytes()
|
||||
|
||||
// Optionally publish immediately if requested; otherwise caller can
|
||||
// broadcast the signed tx themselves.
|
||||
if req.Publish {
|
||||
err = wallet.PublishTransaction(
|
||||
ctx, sweepTx,
|
||||
labels.LoopOutSweepSuccess(targetSwap.Hash.String()),
|
||||
)
|
||||
if err != nil {
|
||||
errorf("sweephtlc: publish failed for %v: %v",
|
||||
req.Outpoint, err)
|
||||
|
||||
return &looprpc.SweepHtlcResponse{
|
||||
SweepTx: rawTx,
|
||||
FeeSats: uint64(fee),
|
||||
Publish: &looprpc.SweepHtlcResponse_Failed{
|
||||
Failed: &looprpc.PublishFailed{
|
||||
Error: err.Error(),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
infof("sweephtlc: published sweep %v", sweepTx.TxHash())
|
||||
}
|
||||
|
||||
resp := &looprpc.SweepHtlcResponse{
|
||||
SweepTx: rawTx,
|
||||
FeeSats: uint64(fee),
|
||||
}
|
||||
if req.Publish {
|
||||
resp.Publish = &looprpc.SweepHtlcResponse_Published{
|
||||
Published: &looprpc.PublishSucceeded{},
|
||||
}
|
||||
} else {
|
||||
resp.Publish = &looprpc.SweepHtlcResponse_NotRequested{
|
||||
NotRequested: &looprpc.PublishNotRequested{},
|
||||
}
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
581
loopd/sweep_htlc_test.go
Normal file
581
loopd/sweep_htlc_test.go
Normal file
|
|
@ -0,0 +1,581 @@
|
|||
package loopd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/btcsuite/btclog/v2"
|
||||
"github.com/lightninglabs/loop/loopdb"
|
||||
"github.com/lightninglabs/loop/looprpc"
|
||||
"github.com/lightninglabs/loop/swap"
|
||||
"github.com/lightninglabs/loop/test"
|
||||
"github.com/lightninglabs/loop/utils"
|
||||
"github.com/lightningnetwork/lnd/chainntnfs"
|
||||
"github.com/lightningnetwork/lnd/keychain"
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// sweepHtlcTests is a collection of table tests for TestSweepHtlc.
|
||||
var sweepHtlcTests = []struct {
|
||||
name string
|
||||
amount btcutil.Amount
|
||||
satPerVByte uint32
|
||||
minRelayFee chainfee.SatPerKWeight
|
||||
expectErrMsg string
|
||||
expectLogs []string
|
||||
expectRegister bool
|
||||
noSwap bool
|
||||
publish bool
|
||||
publishErr bool
|
||||
modifyReq func(*looprpc.SweepHtlcRequest)
|
||||
mutateSwap func(*loopdb.LoopOutContract)
|
||||
mutateTxOut func(*wire.TxOut)
|
||||
sendConf func(*test.ConfRegistration)
|
||||
}{
|
||||
{
|
||||
name: "success low fee",
|
||||
amount: 100_000,
|
||||
satPerVByte: 10,
|
||||
expectRegister: true,
|
||||
expectLogs: []string{
|
||||
"sweephtlc: generated new destination address: %v",
|
||||
"sweephtlc: start sweep for %v -> %v",
|
||||
"sweephtlc: matched swap %v at height hint %v",
|
||||
"sweephtlc: registering conf ntfn for %v hint=%v",
|
||||
"sweephtlc: waiting for confirmation of %v",
|
||||
"sweephtlc: funding confirmed at height %v",
|
||||
"sweephtlc: swap hash validated for %v",
|
||||
"sweephtlc: sweeping to %v with feerate %v sat/vbyte",
|
||||
"sweephtlc: signing sweep spending %v",
|
||||
"sweephtlc: witness assembled, tx size=%d vbytes",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "success low fee, publish",
|
||||
amount: 100_000,
|
||||
satPerVByte: 10,
|
||||
expectRegister: true,
|
||||
expectLogs: []string{
|
||||
"sweephtlc: generated new destination address: %v",
|
||||
"sweephtlc: start sweep for %v -> %v",
|
||||
"sweephtlc: matched swap %v at height hint %v",
|
||||
"sweephtlc: registering conf ntfn for %v hint=%v",
|
||||
"sweephtlc: waiting for confirmation of %v",
|
||||
"sweephtlc: funding confirmed at height %v",
|
||||
"sweephtlc: swap hash validated for %v",
|
||||
"sweephtlc: sweeping to %v with feerate %v sat/vbyte",
|
||||
"sweephtlc: signing sweep spending %v",
|
||||
"sweephtlc: witness assembled, tx size=%d vbytes",
|
||||
"sweephtlc: published sweep %v",
|
||||
},
|
||||
publish: true,
|
||||
},
|
||||
{
|
||||
name: "publish failure reported",
|
||||
amount: 100_000,
|
||||
satPerVByte: 10,
|
||||
expectRegister: true,
|
||||
expectLogs: []string{
|
||||
"sweephtlc: generated new destination address: %v",
|
||||
"sweephtlc: start sweep for %v -> %v",
|
||||
"sweephtlc: matched swap %v at height hint %v",
|
||||
"sweephtlc: registering conf ntfn for %v hint=%v",
|
||||
"sweephtlc: waiting for confirmation of %v",
|
||||
"sweephtlc: funding confirmed at height %v",
|
||||
"sweephtlc: swap hash validated for %v",
|
||||
"sweephtlc: sweeping to %v with feerate %v sat/vbyte",
|
||||
"sweephtlc: signing sweep spending %v",
|
||||
"sweephtlc: witness assembled, tx size=%d vbytes",
|
||||
"sweephtlc: publish failed for %v: %v",
|
||||
},
|
||||
publish: true,
|
||||
publishErr: true,
|
||||
},
|
||||
{
|
||||
name: "fee clamped over ratio",
|
||||
amount: 100_000,
|
||||
satPerVByte: 200,
|
||||
expectErrMsg: "fee exceeds",
|
||||
expectRegister: true,
|
||||
expectLogs: []string{
|
||||
"sweephtlc: generated new destination address: %v",
|
||||
"sweephtlc: start sweep for %v -> %v",
|
||||
"sweephtlc: matched swap %v at height hint %v",
|
||||
"sweephtlc: registering conf ntfn for %v hint=%v",
|
||||
"sweephtlc: waiting for confirmation of %v",
|
||||
"sweephtlc: funding confirmed at height %v",
|
||||
"sweephtlc: swap hash validated for %v",
|
||||
"sweephtlc: sweeping to %v with feerate %v sat/vbyte",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "clamped below min relay",
|
||||
amount: 10_000,
|
||||
// Will clamp further.
|
||||
satPerVByte: 5,
|
||||
minRelayFee: chainfee.SatPerKWeight(1_000_000),
|
||||
expectErrMsg: "fee too low for relay after clamp",
|
||||
expectRegister: true,
|
||||
expectLogs: []string{
|
||||
"sweephtlc: generated new destination address: %v",
|
||||
"sweephtlc: start sweep for %v -> %v",
|
||||
"sweephtlc: matched swap %v at height hint %v",
|
||||
"sweephtlc: registering conf ntfn for %v hint=%v",
|
||||
"sweephtlc: waiting for confirmation of %v",
|
||||
"sweephtlc: funding confirmed at height %v",
|
||||
"sweephtlc: swap hash validated for %v",
|
||||
"sweephtlc: sweeping to %v with feerate %v sat/vbyte",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing outpoint",
|
||||
amount: 100_000,
|
||||
satPerVByte: 10,
|
||||
expectErrMsg: "outpoint required",
|
||||
expectLogs: []string{},
|
||||
expectRegister: false,
|
||||
modifyReq: func(req *looprpc.SweepHtlcRequest) {
|
||||
req.Outpoint = ""
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing htlc address",
|
||||
amount: 100_000,
|
||||
satPerVByte: 10,
|
||||
expectErrMsg: "htlc_address required",
|
||||
expectLogs: []string{},
|
||||
expectRegister: false,
|
||||
modifyReq: func(req *looprpc.SweepHtlcRequest) {
|
||||
req.HtlcAddress = ""
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing feerate",
|
||||
amount: 100_000,
|
||||
satPerVByte: 0,
|
||||
expectErrMsg: "sat_per_vbyte required",
|
||||
expectLogs: []string{},
|
||||
expectRegister: false,
|
||||
},
|
||||
{
|
||||
name: "invalid htlc address",
|
||||
amount: 100_000,
|
||||
satPerVByte: 10,
|
||||
expectErrMsg: "invalid htlc_address",
|
||||
expectLogs: []string{},
|
||||
expectRegister: false,
|
||||
modifyReq: func(req *looprpc.SweepHtlcRequest) {
|
||||
req.HtlcAddress = "notanaddress"
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no matching swap",
|
||||
amount: 100_000,
|
||||
satPerVByte: 10,
|
||||
expectErrMsg: "no matching swap",
|
||||
expectRegister: false,
|
||||
noSwap: true,
|
||||
expectLogs: []string{
|
||||
"sweephtlc: generated new destination address: %v",
|
||||
"sweephtlc: start sweep for %v -> %v",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid initiation height",
|
||||
amount: 100_000,
|
||||
satPerVByte: 10,
|
||||
expectErrMsg: "invalid initiation height",
|
||||
expectRegister: false,
|
||||
mutateSwap: func(contract *loopdb.LoopOutContract) {
|
||||
contract.InitiationHeight = 0
|
||||
},
|
||||
expectLogs: []string{
|
||||
"sweephtlc: generated new destination address: %v",
|
||||
"sweephtlc: start sweep for %v -> %v",
|
||||
"sweephtlc: matched swap %v at height hint %v",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "conf ntfn error",
|
||||
amount: 100_000,
|
||||
satPerVByte: 10,
|
||||
expectErrMsg: "conf ntfn",
|
||||
expectRegister: true,
|
||||
sendConf: func(reg *test.ConfRegistration) {
|
||||
reg.ErrChan <- errors.New("boom")
|
||||
},
|
||||
expectLogs: []string{
|
||||
"sweephtlc: generated new destination address: %v",
|
||||
"sweephtlc: start sweep for %v -> %v",
|
||||
"sweephtlc: matched swap %v at height hint %v",
|
||||
"sweephtlc: registering conf ntfn for %v hint=%v",
|
||||
"sweephtlc: waiting for confirmation of %v",
|
||||
"sweephtlc: conf ntfn error for %v: %v",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "outpoint script mismatch",
|
||||
amount: 100_000,
|
||||
satPerVByte: 10,
|
||||
expectErrMsg: "outpoint script does not match HTLC address",
|
||||
expectRegister: true,
|
||||
mutateTxOut: func(txOut *wire.TxOut) {
|
||||
txOut.PkScript = []byte{0x6a}
|
||||
},
|
||||
expectLogs: []string{
|
||||
"sweephtlc: generated new destination address: %v",
|
||||
"sweephtlc: start sweep for %v -> %v",
|
||||
"sweephtlc: matched swap %v at height hint %v",
|
||||
"sweephtlc: registering conf ntfn for %v hint=%v",
|
||||
"sweephtlc: waiting for confirmation of %v",
|
||||
"sweephtlc: funding confirmed at height %v",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "fee exceeds htlc value",
|
||||
amount: 100_000,
|
||||
satPerVByte: 2_000_000,
|
||||
expectErrMsg: "fee exceeds HTLC value",
|
||||
expectRegister: true,
|
||||
expectLogs: []string{
|
||||
"sweephtlc: generated new destination address: %v",
|
||||
"sweephtlc: start sweep for %v -> %v",
|
||||
"sweephtlc: matched swap %v at height hint %v",
|
||||
"sweephtlc: registering conf ntfn for %v hint=%v",
|
||||
"sweephtlc: waiting for confirmation of %v",
|
||||
"sweephtlc: funding confirmed at height %v",
|
||||
"sweephtlc: swap hash validated for %v",
|
||||
"sweephtlc: sweeping to %v with feerate %v sat/vbyte",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "preimage mismatch",
|
||||
amount: 100_000,
|
||||
satPerVByte: 10,
|
||||
expectErrMsg: "preimage does not match HTLC hash",
|
||||
expectRegister: true,
|
||||
modifyReq: func(req *looprpc.SweepHtlcRequest) {
|
||||
req.Preimage = bytes.Repeat([]byte{9}, 32)
|
||||
},
|
||||
expectLogs: []string{
|
||||
"sweephtlc: generated new destination address: %v",
|
||||
"sweephtlc: start sweep for %v -> %v",
|
||||
"sweephtlc: matched swap %v at height hint %v",
|
||||
"sweephtlc: registering conf ntfn for %v hint=%v",
|
||||
"sweephtlc: waiting for confirmation of %v",
|
||||
"sweephtlc: funding confirmed at height %v",
|
||||
"sweephtlc: swap hash validated for %v",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// TestSweepHtlc runs a table of happy-path and fee-related rejection cases for
|
||||
// the sweep helper.
|
||||
func TestSweepHtlc(t *testing.T) {
|
||||
// shortDelay is used to check that nothing is produced from a channel.
|
||||
const shortDelay = 100 * time.Millisecond
|
||||
|
||||
for _, tc := range sweepHtlcTests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// Catch leaked goroutines and constrain test time.
|
||||
defer test.Guard(t)()
|
||||
|
||||
// Fresh logger per test to capture emitted formats.
|
||||
logger := newFormatLogger()
|
||||
setLogger(logger)
|
||||
|
||||
// Base mocks for wallet/notifier/signer.
|
||||
lnd := test.NewMockLnd()
|
||||
if tc.publishErr {
|
||||
lnd.PublishHandler = func(ctx context.Context,
|
||||
_ *wire.MsgTx, _ string) error {
|
||||
|
||||
return errors.New("publish-fail")
|
||||
}
|
||||
}
|
||||
if tc.minRelayFee != 0 {
|
||||
lnd.SetMinRelayFee(tc.minRelayFee)
|
||||
}
|
||||
store := loopdb.NewStoreMock(t)
|
||||
|
||||
preimage := lntypes.Preimage{1, 2, 3, 4}
|
||||
swapHash := preimage.Hash()
|
||||
|
||||
_, senderPub := test.CreateKey(0)
|
||||
_, receiverPub := test.CreateKey(1)
|
||||
|
||||
var senderKey, receiverKey [33]byte
|
||||
copy(senderKey[:], senderPub.SerializeCompressed())
|
||||
copy(receiverKey[:], receiverPub.SerializeCompressed())
|
||||
|
||||
htlcKeys := loopdb.HtlcKeys{
|
||||
SenderScriptKey: senderKey,
|
||||
ReceiverScriptKey: receiverKey,
|
||||
ClientScriptKeyLocator: keychain.KeyLocator{
|
||||
Family: keychain.KeyFamily(
|
||||
swap.KeyFamily,
|
||||
),
|
||||
Index: 0,
|
||||
},
|
||||
}
|
||||
|
||||
swapContract := loopdb.SwapContract{
|
||||
Preimage: preimage,
|
||||
AmountRequested: tc.amount,
|
||||
HtlcKeys: htlcKeys,
|
||||
CltvExpiry: 500,
|
||||
InitiationHeight: 123,
|
||||
ProtocolVersion: loopdb.ProtocolVersionHtlcV2,
|
||||
}
|
||||
|
||||
destAddr, err := btcutil.NewAddressWitnessPubKeyHash(
|
||||
make([]byte, 20), lnd.ChainParams,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
loopOut := &loopdb.LoopOut{
|
||||
Loop: loopdb.Loop{
|
||||
Hash: swapHash,
|
||||
},
|
||||
Contract: &loopdb.LoopOutContract{
|
||||
SwapContract: swapContract,
|
||||
DestAddr: destAddr,
|
||||
},
|
||||
}
|
||||
|
||||
// Store the swap unless this case disables it.
|
||||
if tc.mutateSwap != nil {
|
||||
tc.mutateSwap(loopOut.Contract)
|
||||
}
|
||||
if !tc.noSwap {
|
||||
store.LoopOutSwaps[swapHash] = loopOut.Contract
|
||||
}
|
||||
|
||||
// Build HTLC details and funding tx.
|
||||
htlc, err := utils.GetHtlc(
|
||||
swapHash, &loopOut.Contract.SwapContract,
|
||||
lnd.ChainParams,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
fundingTx := wire.NewMsgTx(2)
|
||||
txOut := &wire.TxOut{
|
||||
Value: int64(
|
||||
loopOut.Contract.AmountRequested,
|
||||
),
|
||||
PkScript: htlc.PkScript,
|
||||
}
|
||||
if tc.mutateTxOut != nil {
|
||||
tc.mutateTxOut(txOut)
|
||||
}
|
||||
fundingTx.AddTxOut(txOut)
|
||||
fundingHash := fundingTx.TxHash()
|
||||
outpoint := wire.OutPoint{Hash: fundingHash, Index: 0}
|
||||
|
||||
ctx, cancel := context.WithTimeout(
|
||||
t.Context(), 5*time.Second,
|
||||
)
|
||||
defer cancel()
|
||||
|
||||
// Drain signer requests to avoid blocking.
|
||||
go func() {
|
||||
select {
|
||||
case <-lnd.SignOutputRawChannel:
|
||||
|
||||
case <-ctx.Done():
|
||||
}
|
||||
}()
|
||||
|
||||
pubChan := make(chan *wire.MsgTx, 1)
|
||||
|
||||
// If publish is requested, drain TxPublishChannel so
|
||||
// the mock PublishTransaction does not block.
|
||||
if tc.publish {
|
||||
go func() {
|
||||
select {
|
||||
case tx := <-lnd.TxPublishChannel:
|
||||
pubChan <- tx
|
||||
|
||||
case <-ctx.Done():
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Handle confirmation registration caused by the call.
|
||||
if tc.expectRegister {
|
||||
// Consume notifier registration.
|
||||
go func() {
|
||||
var reg *test.ConfRegistration
|
||||
select {
|
||||
case reg = <-lnd.RegisterConfChannel:
|
||||
// Got registration.
|
||||
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
|
||||
// Either send an error or a
|
||||
// confirmation.
|
||||
if tc.sendConf != nil {
|
||||
tc.sendConf(reg)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
conf := &chainntnfs.TxConfirmation{
|
||||
Tx: fundingTx,
|
||||
}
|
||||
reg.ConfChan <- conf
|
||||
}()
|
||||
}
|
||||
|
||||
// Build request with optional mutation.
|
||||
req := &looprpc.SweepHtlcRequest{
|
||||
Outpoint: outpoint.String(),
|
||||
SatPerVbyte: tc.satPerVByte,
|
||||
Publish: tc.publish,
|
||||
HtlcAddress: htlc.Address.String(),
|
||||
DestAddress: "",
|
||||
Preimage: nil,
|
||||
}
|
||||
if tc.modifyReq != nil {
|
||||
tc.modifyReq(req)
|
||||
}
|
||||
|
||||
// Invoke sweepHtlc and forward the result.
|
||||
resp, err := sweepHtlc(
|
||||
ctx, req, lnd.ChainParams, store,
|
||||
lnd.ChainNotifier, lnd.WalletKit,
|
||||
lnd.Signer,
|
||||
)
|
||||
|
||||
// Handle confirmation registration caused by the call
|
||||
// when not expected.
|
||||
if !tc.expectRegister {
|
||||
select {
|
||||
case reg := <-lnd.RegisterConfChannel:
|
||||
t.Fatalf("unexpected registration: %+v",
|
||||
reg)
|
||||
|
||||
case <-time.After(shortDelay):
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure it produced the expected logs.
|
||||
logs := logger.formats
|
||||
if logs == nil {
|
||||
logs = []string{}
|
||||
}
|
||||
require.Equal(t, tc.expectLogs, logs)
|
||||
|
||||
// Ensure all mock channels are drained.
|
||||
defer require.NoError(t, lnd.IsDone())
|
||||
|
||||
// Error path.
|
||||
if tc.expectErrMsg != "" {
|
||||
require.ErrorContains(t, err, tc.expectErrMsg)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Success path.
|
||||
require.NoError(t, err)
|
||||
|
||||
// Parse the produced signed transaction.
|
||||
require.NotEmpty(t, resp.SweepTx)
|
||||
var sweepTx wire.MsgTx
|
||||
err = sweepTx.Deserialize(bytes.NewReader(resp.SweepTx))
|
||||
require.NoError(t, err)
|
||||
require.Equal(
|
||||
t, outpoint, sweepTx.TxIn[0].PreviousOutPoint,
|
||||
)
|
||||
require.NotEmpty(t, sweepTx.TxIn[0].Witness)
|
||||
|
||||
if tc.publish {
|
||||
// For publish=true we should see a
|
||||
// publish (or a publish failure
|
||||
// response which skips broadcast).
|
||||
select {
|
||||
case tx := <-pubChan:
|
||||
require.NotNil(t, tx)
|
||||
|
||||
case <-time.After(shortDelay):
|
||||
if !tc.publishErr {
|
||||
t.Fatal("expected publish")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// For publish=false we should not
|
||||
// publish.
|
||||
select {
|
||||
case <-lnd.TxPublishChannel:
|
||||
t.Fatal("unexpected publish")
|
||||
|
||||
case <-time.After(shortDelay):
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// formatLogger captures format strings passed to the logger interface so we
|
||||
// can assert on log invocations.
|
||||
type formatLogger struct {
|
||||
btclog.Logger
|
||||
|
||||
formats []string
|
||||
}
|
||||
|
||||
// newFormatLogger builds a logger that records format strings while discarding
|
||||
// actual log output.
|
||||
func newFormatLogger() *formatLogger {
|
||||
return &formatLogger{Logger: btclog.Disabled}
|
||||
}
|
||||
|
||||
// record stores the raw format string.
|
||||
func (f *formatLogger) record(format string) {
|
||||
f.formats = append(f.formats, format)
|
||||
}
|
||||
|
||||
// Tracef logs a trace and records its format.
|
||||
func (f *formatLogger) Tracef(format string, params ...interface{}) {
|
||||
f.record(format)
|
||||
f.Logger.Tracef(format, params...)
|
||||
}
|
||||
|
||||
// Debugf logs a debug message and records its format.
|
||||
func (f *formatLogger) Debugf(format string, params ...interface{}) {
|
||||
f.record(format)
|
||||
f.Logger.Debugf(format, params...)
|
||||
}
|
||||
|
||||
// Infof logs an info message and records its format.
|
||||
func (f *formatLogger) Infof(format string, params ...interface{}) {
|
||||
f.record(format)
|
||||
f.Logger.Infof(format, params...)
|
||||
}
|
||||
|
||||
// Warnf logs a warning and records its format.
|
||||
func (f *formatLogger) Warnf(format string, params ...interface{}) {
|
||||
f.record(format)
|
||||
f.Logger.Warnf(format, params...)
|
||||
}
|
||||
|
||||
// Errorf logs an error and records its format.
|
||||
func (f *formatLogger) Errorf(format string, params ...interface{}) {
|
||||
f.record(format)
|
||||
f.Logger.Errorf(format, params...)
|
||||
}
|
||||
|
||||
// Criticalf logs a critical message and records its format.
|
||||
func (f *formatLogger) Criticalf(format string, params ...interface{}) {
|
||||
f.record(format)
|
||||
f.Logger.Criticalf(format, params...)
|
||||
}
|
||||
1096
looprpc/client.pb.go
1096
looprpc/client.pb.go
File diff suppressed because it is too large
Load diff
|
|
@ -119,6 +119,32 @@ func local_request_SwapClient_ListSwaps_0(ctx context.Context, marshaler runtime
|
|||
|
||||
}
|
||||
|
||||
func request_SwapClient_SweepHtlc_0(ctx context.Context, marshaler runtime.Marshaler, client SwapClientClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var protoReq SweepHtlcRequest
|
||||
var metadata runtime.ServerMetadata
|
||||
|
||||
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
|
||||
msg, err := client.SweepHtlc(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
|
||||
return msg, metadata, err
|
||||
|
||||
}
|
||||
|
||||
func local_request_SwapClient_SweepHtlc_0(ctx context.Context, marshaler runtime.Marshaler, server SwapClientServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var protoReq SweepHtlcRequest
|
||||
var metadata runtime.ServerMetadata
|
||||
|
||||
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
|
||||
msg, err := server.SweepHtlc(ctx, &protoReq)
|
||||
return msg, metadata, err
|
||||
|
||||
}
|
||||
|
||||
func request_SwapClient_SwapInfo_0(ctx context.Context, marshaler runtime.Marshaler, client SwapClientClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var protoReq SwapInfoRequest
|
||||
var metadata runtime.ServerMetadata
|
||||
|
|
@ -924,6 +950,31 @@ func RegisterSwapClientHandlerServer(ctx context.Context, mux *runtime.ServeMux,
|
|||
|
||||
})
|
||||
|
||||
mux.Handle("POST", pattern_SwapClient_SweepHtlc_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
var stream runtime.ServerTransportStream
|
||||
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
|
||||
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
|
||||
var err error
|
||||
var annotatedContext context.Context
|
||||
annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/looprpc.SwapClient/SweepHtlc", runtime.WithHTTPPathPattern("/v1/loop/out/sweephtlc"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := local_request_SwapClient_SweepHtlc_0(annotatedContext, inboundMarshaler, server, req, pathParams)
|
||||
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
|
||||
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
|
||||
if err != nil {
|
||||
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
forward_SwapClient_SweepHtlc_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
|
||||
})
|
||||
|
||||
mux.Handle("GET", pattern_SwapClient_SwapInfo_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
|
|
@ -1656,6 +1707,28 @@ func RegisterSwapClientHandlerClient(ctx context.Context, mux *runtime.ServeMux,
|
|||
|
||||
})
|
||||
|
||||
mux.Handle("POST", pattern_SwapClient_SweepHtlc_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
|
||||
var err error
|
||||
var annotatedContext context.Context
|
||||
annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/looprpc.SwapClient/SweepHtlc", runtime.WithHTTPPathPattern("/v1/loop/out/sweephtlc"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := request_SwapClient_SweepHtlc_0(annotatedContext, inboundMarshaler, client, req, pathParams)
|
||||
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
|
||||
if err != nil {
|
||||
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
forward_SwapClient_SweepHtlc_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
|
||||
})
|
||||
|
||||
mux.Handle("GET", pattern_SwapClient_SwapInfo_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
|
|
@ -2216,6 +2289,8 @@ var (
|
|||
|
||||
pattern_SwapClient_ListSwaps_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "loop", "swaps"}, ""))
|
||||
|
||||
pattern_SwapClient_SweepHtlc_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"v1", "loop", "out", "sweephtlc"}, ""))
|
||||
|
||||
pattern_SwapClient_SwapInfo_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"v1", "loop", "swap", "id"}, ""))
|
||||
|
||||
pattern_SwapClient_LoopOutTerms_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"v1", "loop", "out", "terms"}, ""))
|
||||
|
|
@ -2274,6 +2349,8 @@ var (
|
|||
|
||||
forward_SwapClient_ListSwaps_0 = runtime.ForwardResponseMessage
|
||||
|
||||
forward_SwapClient_SweepHtlc_0 = runtime.ForwardResponseMessage
|
||||
|
||||
forward_SwapClient_SwapInfo_0 = runtime.ForwardResponseMessage
|
||||
|
||||
forward_SwapClient_LoopOutTerms_0 = runtime.ForwardResponseMessage
|
||||
|
|
|
|||
|
|
@ -38,6 +38,12 @@ service SwapClient {
|
|||
*/
|
||||
rpc ListSwaps (ListSwapsRequest) returns (ListSwapsResponse);
|
||||
|
||||
/* loop: `sweephtlc`
|
||||
SweepHtlc spends a swap HTLC output via the preimage (success) path using
|
||||
the swap's known preimage or an optionally supplied one.
|
||||
*/
|
||||
rpc SweepHtlc (SweepHtlcRequest) returns (SweepHtlcResponse);
|
||||
|
||||
/* loop: `swapinfo`
|
||||
SwapInfo returns all known details about a single swap.
|
||||
*/
|
||||
|
|
@ -739,6 +745,61 @@ message ListSwapsResponse {
|
|||
int64 next_start_time = 2;
|
||||
}
|
||||
|
||||
// SweepHtlcRequest instructs loopd to sweep a swap HTLC via its success path.
|
||||
message SweepHtlcRequest {
|
||||
// Optional override for the sweep destination; defaults to a new address
|
||||
// derived from the connected lnd wallet.
|
||||
string dest_address = 1;
|
||||
|
||||
// Fee rate used for the sweep transaction in sat/vByte.
|
||||
uint32 sat_per_vbyte = 2;
|
||||
|
||||
// HTLC outpoint to sweep, formatted as "txid:vout".
|
||||
string outpoint = 3;
|
||||
|
||||
// Optional override for the stored swap preimage.
|
||||
bytes preimage = 4;
|
||||
|
||||
// If true, publish the sweep transaction immediately.
|
||||
bool publish = 5;
|
||||
|
||||
// The HTLC address whose output is being swept; used to derive the
|
||||
// expected pkScript.
|
||||
string htlc_address = 6;
|
||||
}
|
||||
|
||||
// SweepHtlcResponse returns the broadcast sweep transaction.
|
||||
message SweepHtlcResponse {
|
||||
// Raw sweep transaction bytes.
|
||||
bytes sweep_tx = 1;
|
||||
|
||||
// Miner fee paid by the sweep transaction.
|
||||
uint64 fee_sats = 2;
|
||||
|
||||
// Publish outcome.
|
||||
oneof publish {
|
||||
PublishNotRequested not_requested = 3;
|
||||
PublishSucceeded published = 4;
|
||||
PublishFailed failed = 5;
|
||||
}
|
||||
}
|
||||
|
||||
// PublishNotRequested is returned by SweepHtlc if publishing was not requested
|
||||
// in SweepHtlcRequest.
|
||||
message PublishNotRequested {
|
||||
}
|
||||
|
||||
// PublishSucceeded is returned by SweepHtlc if publishing was requested in
|
||||
// SweepHtlcRequest and it succeeded.
|
||||
message PublishSucceeded {
|
||||
}
|
||||
|
||||
// PublishFailed is returned by SweepHtlc if publishing was requested in
|
||||
// SweepHtlcRequest, but failed. It includes the error message.
|
||||
message PublishFailed {
|
||||
string error = 1;
|
||||
}
|
||||
|
||||
message SwapInfoRequest {
|
||||
/*
|
||||
The swap identifier which currently is the hash that locks the HTLCs. When
|
||||
|
|
|
|||
|
|
@ -660,6 +660,40 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
"/v1/loop/out/sweephtlc": {
|
||||
"post": {
|
||||
"summary": "loop: `sweephtlc`\nSweepHtlc spends a swap HTLC output via the preimage (success) path using\nthe swap's known preimage or an optionally supplied one.",
|
||||
"operationId": "SwapClient_SweepHtlc",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "A successful response.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/looprpcSweepHtlcResponse"
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"description": "An unexpected error response.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/rpcStatus"
|
||||
}
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"name": "body",
|
||||
"description": "SweepHtlcRequest instructs loopd to sweep a swap HTLC via its success path.",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/looprpcSweepHtlcRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"SwapClient"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v1/loop/out/terms": {
|
||||
"get": {
|
||||
"summary": "loop: `terms`\nLoopOutTerms returns the terms that the server enforces for a loop out swap.",
|
||||
|
|
@ -2277,6 +2311,23 @@
|
|||
"looprpcProbeResponse": {
|
||||
"type": "object"
|
||||
},
|
||||
"looprpcPublishFailed": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"description": "PublishFailed is returned by SweepHtlc if publishing was requested in\nSweepHtlcRequest, but failed. It includes the error message."
|
||||
},
|
||||
"looprpcPublishNotRequested": {
|
||||
"type": "object",
|
||||
"description": "PublishNotRequested is returned by SweepHtlc if publishing was not requested\nin SweepHtlcRequest."
|
||||
},
|
||||
"looprpcPublishSucceeded": {
|
||||
"type": "object",
|
||||
"description": "PublishSucceeded is returned by SweepHtlc if publishing was requested in\nSweepHtlcRequest and it succeeded."
|
||||
},
|
||||
"looprpcRouteHint": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
@ -2750,6 +2801,63 @@
|
|||
"default": "LOOP_OUT",
|
||||
"title": "- LOOP_OUT: LOOP_OUT indicates an loop out swap (off-chain to on-chain)\n - LOOP_IN: LOOP_IN indicates a loop in swap (on-chain to off-chain)"
|
||||
},
|
||||
"looprpcSweepHtlcRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"dest_address": {
|
||||
"type": "string",
|
||||
"description": "Optional override for the sweep destination; defaults to a new address\nderived from the connected lnd wallet."
|
||||
},
|
||||
"sat_per_vbyte": {
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"description": "Fee rate used for the sweep transaction in sat/vByte."
|
||||
},
|
||||
"outpoint": {
|
||||
"type": "string",
|
||||
"description": "HTLC outpoint to sweep, formatted as \"txid:vout\"."
|
||||
},
|
||||
"preimage": {
|
||||
"type": "string",
|
||||
"format": "byte",
|
||||
"description": "Optional override for the stored swap preimage."
|
||||
},
|
||||
"publish": {
|
||||
"type": "boolean",
|
||||
"description": "If true, publish the sweep transaction immediately."
|
||||
},
|
||||
"htlc_address": {
|
||||
"type": "string",
|
||||
"description": "The HTLC address whose output is being swept; used to derive the\nexpected pkScript."
|
||||
}
|
||||
},
|
||||
"description": "SweepHtlcRequest instructs loopd to sweep a swap HTLC via its success path."
|
||||
},
|
||||
"looprpcSweepHtlcResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"sweep_tx": {
|
||||
"type": "string",
|
||||
"format": "byte",
|
||||
"description": "Raw sweep transaction bytes."
|
||||
},
|
||||
"fee_sats": {
|
||||
"type": "string",
|
||||
"format": "uint64",
|
||||
"description": "Miner fee paid by the sweep transaction."
|
||||
},
|
||||
"not_requested": {
|
||||
"$ref": "#/definitions/looprpcPublishNotRequested"
|
||||
},
|
||||
"published": {
|
||||
"$ref": "#/definitions/looprpcPublishSucceeded"
|
||||
},
|
||||
"failed": {
|
||||
"$ref": "#/definitions/looprpcPublishFailed"
|
||||
}
|
||||
},
|
||||
"description": "SweepHtlcResponse returns the broadcast sweep transaction."
|
||||
},
|
||||
"looprpcTokensResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,9 @@ http:
|
|||
get: "/v1/loop/out/terms"
|
||||
- selector: looprpc.SwapClient.LoopOutQuote
|
||||
get: "/v1/loop/out/quote/{amt}"
|
||||
- selector: looprpc.SwapClient.SweepHtlc
|
||||
post: "/v1/loop/out/sweephtlc"
|
||||
body: "*"
|
||||
- selector: looprpc.SwapClient.GetLoopInTerms
|
||||
get: "/v1/loop/in/terms"
|
||||
- selector: looprpc.SwapClient.GetLoopInQuote
|
||||
|
|
|
|||
|
|
@ -37,6 +37,10 @@ type SwapClientClient interface {
|
|||
// ListSwaps returns a list of all currently known swaps and their current
|
||||
// status.
|
||||
ListSwaps(ctx context.Context, in *ListSwapsRequest, opts ...grpc.CallOption) (*ListSwapsResponse, error)
|
||||
// loop: `sweephtlc`
|
||||
// SweepHtlc spends a swap HTLC output via the preimage (success) path using
|
||||
// the swap's known preimage or an optionally supplied one.
|
||||
SweepHtlc(ctx context.Context, in *SweepHtlcRequest, opts ...grpc.CallOption) (*SweepHtlcResponse, error)
|
||||
// loop: `swapinfo`
|
||||
// SwapInfo returns all known details about a single swap.
|
||||
SwapInfo(ctx context.Context, in *SwapInfoRequest, opts ...grpc.CallOption) (*SwapStatus, error)
|
||||
|
|
@ -205,6 +209,15 @@ func (c *swapClientClient) ListSwaps(ctx context.Context, in *ListSwapsRequest,
|
|||
return out, nil
|
||||
}
|
||||
|
||||
func (c *swapClientClient) SweepHtlc(ctx context.Context, in *SweepHtlcRequest, opts ...grpc.CallOption) (*SweepHtlcResponse, error) {
|
||||
out := new(SweepHtlcResponse)
|
||||
err := c.cc.Invoke(ctx, "/looprpc.SwapClient/SweepHtlc", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *swapClientClient) SwapInfo(ctx context.Context, in *SwapInfoRequest, opts ...grpc.CallOption) (*SwapStatus, error) {
|
||||
out := new(SwapStatus)
|
||||
err := c.cc.Invoke(ctx, "/looprpc.SwapClient/SwapInfo", in, out, opts...)
|
||||
|
|
@ -471,6 +484,10 @@ type SwapClientServer interface {
|
|||
// ListSwaps returns a list of all currently known swaps and their current
|
||||
// status.
|
||||
ListSwaps(context.Context, *ListSwapsRequest) (*ListSwapsResponse, error)
|
||||
// loop: `sweephtlc`
|
||||
// SweepHtlc spends a swap HTLC output via the preimage (success) path using
|
||||
// the swap's known preimage or an optionally supplied one.
|
||||
SweepHtlc(context.Context, *SweepHtlcRequest) (*SweepHtlcResponse, error)
|
||||
// loop: `swapinfo`
|
||||
// SwapInfo returns all known details about a single swap.
|
||||
SwapInfo(context.Context, *SwapInfoRequest) (*SwapStatus, error)
|
||||
|
|
@ -589,6 +606,9 @@ func (UnimplementedSwapClientServer) Monitor(*MonitorRequest, SwapClient_Monitor
|
|||
func (UnimplementedSwapClientServer) ListSwaps(context.Context, *ListSwapsRequest) (*ListSwapsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListSwaps not implemented")
|
||||
}
|
||||
func (UnimplementedSwapClientServer) SweepHtlc(context.Context, *SweepHtlcRequest) (*SweepHtlcResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method SweepHtlc not implemented")
|
||||
}
|
||||
func (UnimplementedSwapClientServer) SwapInfo(context.Context, *SwapInfoRequest) (*SwapStatus, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method SwapInfo not implemented")
|
||||
}
|
||||
|
|
@ -758,6 +778,24 @@ func _SwapClient_ListSwaps_Handler(srv interface{}, ctx context.Context, dec fun
|
|||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _SwapClient_SweepHtlc_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(SweepHtlcRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(SwapClientServer).SweepHtlc(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/looprpc.SwapClient/SweepHtlc",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(SwapClientServer).SweepHtlc(ctx, req.(*SweepHtlcRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _SwapClient_SwapInfo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(SwapInfoRequest)
|
||||
if err := dec(in); err != nil {
|
||||
|
|
@ -1263,6 +1301,10 @@ var SwapClient_ServiceDesc = grpc.ServiceDesc{
|
|||
MethodName: "ListSwaps",
|
||||
Handler: _SwapClient_ListSwaps_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "SweepHtlc",
|
||||
Handler: _SwapClient_SweepHtlc_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "SwapInfo",
|
||||
Handler: _SwapClient_SwapInfo_Handler,
|
||||
|
|
|
|||
|
|
@ -55,6 +55,13 @@ var RequiredPermissions = map[string][]bakery.Op{
|
|||
Entity: "loop",
|
||||
Action: "out",
|
||||
}},
|
||||
"/looprpc.SwapClient/SweepHtlc": {{
|
||||
Entity: "swap",
|
||||
Action: "read",
|
||||
}, {
|
||||
Entity: "loop",
|
||||
Action: "out",
|
||||
}},
|
||||
"/looprpc.SwapClient/GetLoopInTerms": {{
|
||||
Entity: "terms",
|
||||
Action: "read",
|
||||
|
|
|
|||
|
|
@ -138,6 +138,31 @@ func RegisterSwapClientJSONCallbacks(registry map[string]func(ctx context.Contex
|
|||
callback(string(respBytes), nil)
|
||||
}
|
||||
|
||||
registry["looprpc.SwapClient.SweepHtlc"] = func(ctx context.Context,
|
||||
conn *grpc.ClientConn, reqJSON string, callback func(string, error)) {
|
||||
|
||||
req := &SweepHtlcRequest{}
|
||||
err := marshaler.Unmarshal([]byte(reqJSON), req)
|
||||
if err != nil {
|
||||
callback("", err)
|
||||
return
|
||||
}
|
||||
|
||||
client := NewSwapClientClient(conn)
|
||||
resp, err := client.SweepHtlc(ctx, req)
|
||||
if err != nil {
|
||||
callback("", err)
|
||||
return
|
||||
}
|
||||
|
||||
respBytes, err := marshaler.Marshal(resp)
|
||||
if err != nil {
|
||||
callback("", err)
|
||||
return
|
||||
}
|
||||
callback(string(respBytes), nil)
|
||||
}
|
||||
|
||||
registry["looprpc.SwapClient.SwapInfo"] = func(ctx context.Context,
|
||||
conn *grpc.ClientConn, reqJSON string, callback func(string, error)) {
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import (
|
|||
"github.com/lightninglabs/lndclient"
|
||||
"github.com/lightninglabs/loop/fsm"
|
||||
"github.com/lightninglabs/loop/staticaddr/script"
|
||||
"github.com/lightninglabs/loop/utils"
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
)
|
||||
|
||||
|
|
@ -47,12 +48,25 @@ func (f *FSM) PublishDepositExpirySweepAction(ctx context.Context,
|
|||
"estimation failed: %w", err))
|
||||
}
|
||||
|
||||
minRelayFeeRate, err := f.cfg.WalletKit.MinRelayFee(ctx)
|
||||
if err != nil {
|
||||
return f.HandleError(fmt.Errorf("timeout sweep min relay "+
|
||||
"query failed: %w", err))
|
||||
}
|
||||
|
||||
weight := script.ExpirySpendWeight()
|
||||
|
||||
fee := feeRateEstimator.FeeForWeight(lntypes.WeightUnit(weight))
|
||||
|
||||
// We cap the fee at 20% of the deposit value.
|
||||
if fee > f.deposit.Value/5 {
|
||||
_, clamped, err := utils.ClampSweepFee(
|
||||
fee, f.deposit.Value, utils.MaxFeeToAmountRatio,
|
||||
minRelayFeeRate, lntypes.WeightUnit(weight),
|
||||
)
|
||||
if err != nil {
|
||||
return f.HandleError(err)
|
||||
}
|
||||
if clamped {
|
||||
return f.HandleError(errors.New("fee is greater than 20% of " +
|
||||
"the deposit value"))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,10 +46,6 @@ const (
|
|||
// transaction.
|
||||
batchConfHeight = 3
|
||||
|
||||
// maxFeeToSwapAmtRatio is the maximum fee to swap amount ratio that
|
||||
// we allow for a batch transaction.
|
||||
maxFeeToSwapAmtRatio = 0.2
|
||||
|
||||
// MaxSweepsPerBatch is the maximum number of sweeps in a single batch.
|
||||
// It is needed to prevent sweep tx from becoming non-standard. Max
|
||||
// standard transaction is 400k wu, a non-cooperative input is 393 wu.
|
||||
|
|
@ -1414,9 +1410,9 @@ func constructUnsignedTx(sweeps []sweep, address btcutil.Address,
|
|||
}
|
||||
|
||||
// Clamp the calculated fee to the max allowed fee amount for the batch.
|
||||
fee, err := clampBatchFee(
|
||||
fee, _, err := utils.ClampSweepFee(
|
||||
feeForWeight, batchAmt-btcutil.Amount(sumChange),
|
||||
minRelayFeeRate, weight,
|
||||
utils.MaxFeeToAmountRatio, minRelayFeeRate, weight,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, 0, 0, 0, fmt.Errorf("failed to clamp batch "+
|
||||
|
|
@ -2644,28 +2640,3 @@ func (b *batch) persistConfirmedBatch(ctx context.Context,
|
|||
|
||||
return b.store.ConfirmBatchWithSweeps(ctx, b.dbBatch(), sweeps)
|
||||
}
|
||||
|
||||
// clampBatchFee takes the fee amount and total amount of the sweeps in the
|
||||
// batch and makes sure the fee is not too high. If the fee is too high, it is
|
||||
// clamped to the maximum allowed fee. If the clamped fee results in a fee rate
|
||||
// below the minimum relay fee, an error is returned.
|
||||
func clampBatchFee(fee btcutil.Amount, totalAmount btcutil.Amount,
|
||||
minRelayFeeRate chainfee.SatPerKWeight,
|
||||
weight lntypes.WeightUnit) (btcutil.Amount, error) {
|
||||
|
||||
maxFeeAmount := btcutil.Amount(float64(totalAmount) *
|
||||
maxFeeToSwapAmtRatio)
|
||||
|
||||
clampedFee := fee
|
||||
if fee > maxFeeAmount {
|
||||
clampedFee = maxFeeAmount
|
||||
}
|
||||
|
||||
clampedFeeRate := chainfee.NewSatPerKWeight(clampedFee, weight)
|
||||
if clampedFeeRate < minRelayFeeRate {
|
||||
return 0, fmt.Errorf("clamped fee rate %v is less than "+
|
||||
"minimum relay fee %v", clampedFeeRate, minRelayFeeRate)
|
||||
}
|
||||
|
||||
return clampedFee, nil
|
||||
}
|
||||
|
|
|
|||
44
utils/fees.go
Normal file
44
utils/fees.go
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||
)
|
||||
|
||||
const (
|
||||
// MaxFeeToAmountRatio is the maximum fee to total amount ratio allowed
|
||||
// for a sweep transaction.
|
||||
MaxFeeToAmountRatio = 0.2
|
||||
)
|
||||
|
||||
// ClampSweepFee caps a fee to a percentage of the provided total amount and
|
||||
// verifies the resulting fee rate is not below the minimum relay fee. It
|
||||
// returns the clamped fee, whether it was clamped, or an error if the clamped
|
||||
// fee would fall below the minimum relay fee.
|
||||
func ClampSweepFee(fee btcutil.Amount, totalAmount btcutil.Amount,
|
||||
ratio float64, minRelayFeeRate chainfee.SatPerKWeight,
|
||||
weight lntypes.WeightUnit) (btcutil.Amount, bool, error) {
|
||||
|
||||
maxFeeAmount := btcutil.Amount(float64(totalAmount) * ratio)
|
||||
|
||||
clampedFee := fee
|
||||
clamped := false
|
||||
if fee > maxFeeAmount {
|
||||
clampedFee = maxFeeAmount
|
||||
clamped = true
|
||||
}
|
||||
|
||||
if minRelayFeeRate > 0 {
|
||||
clampedFeeRate := chainfee.NewSatPerKWeight(clampedFee, weight)
|
||||
if clampedFeeRate < minRelayFeeRate {
|
||||
return 0, clamped, fmt.Errorf("clamped fee rate %v is "+
|
||||
"less than minimum relay fee %v",
|
||||
clampedFeeRate, minRelayFeeRate)
|
||||
}
|
||||
}
|
||||
|
||||
return clampedFee, clamped, nil
|
||||
}
|
||||
53
utils/fees_test.go
Normal file
53
utils/fees_test.go
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestClampSweepFee verifies clamping still respects min relay fee
|
||||
// requirements and reports clamping.
|
||||
func TestClampSweepFee(t *testing.T) {
|
||||
weight := lntypes.WeightUnit(400)
|
||||
minRelay := chainfee.SatPerKWeight(253)
|
||||
total := btcutil.Amount(100_000)
|
||||
|
||||
// Fee below the clamp threshold and above min relay should pass
|
||||
// through.
|
||||
fee := chainfee.SatPerKWeight(1_000).FeeForWeight(weight)
|
||||
clamped, clampedFlag, err := ClampSweepFee(
|
||||
fee, total, MaxFeeToAmountRatio, minRelay, weight,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, fee, clamped)
|
||||
require.False(t, clampedFlag)
|
||||
|
||||
// A clamped fee that would fall below min relay should error.
|
||||
// The fee will be clamped to 20 sats.
|
||||
fee = btcutil.Amount(10_000)
|
||||
total = btcutil.Amount(100)
|
||||
_, clampedFlag, err = ClampSweepFee(
|
||||
fee, total, MaxFeeToAmountRatio, minRelay, weight,
|
||||
)
|
||||
require.True(t, clampedFlag)
|
||||
require.Error(t, err)
|
||||
|
||||
// A fee above the clamp threshold should be clamped without error when
|
||||
// still above min relay. The fee is 30% of total, will clamp to 20%.
|
||||
fee = btcutil.Amount(30_000)
|
||||
total = btcutil.Amount(100_000)
|
||||
clamped, clampedFlag, err = ClampSweepFee(
|
||||
fee, total, MaxFeeToAmountRatio, minRelay, weight,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.True(t, clampedFlag)
|
||||
|
||||
expectedClampedFee := btcutil.Amount(
|
||||
float64(total) * MaxFeeToAmountRatio,
|
||||
)
|
||||
require.Equal(t, expectedClampedFee, clamped)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue