mirror of https://github.com/guggero/chantools
recoverloopin: add new command recoverloopin
This commit adds a new command recoverloopin which allows the user to recover stuck loop in funds.pull/66/head
parent
aa767d3faa
commit
ab0743a3d3
@ -0,0 +1,336 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/guggero/chantools/btc"
|
||||
"github.com/guggero/chantools/lnd"
|
||||
"github.com/lightninglabs/loop"
|
||||
"github.com/lightninglabs/loop/loopdb"
|
||||
"github.com/lightninglabs/loop/swap"
|
||||
"github.com/lightningnetwork/lnd/input"
|
||||
"github.com/lightningnetwork/lnd/keychain"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type recoverLoopInCommand struct {
|
||||
TxID string
|
||||
Vout uint32
|
||||
SwapHash string
|
||||
SweepAddr string
|
||||
FeeRate uint16
|
||||
StartKeyIndex int
|
||||
NumTries int
|
||||
|
||||
APIURL string
|
||||
Publish bool
|
||||
|
||||
LoopDbPath string
|
||||
|
||||
rootKey *rootKey
|
||||
cmd *cobra.Command
|
||||
}
|
||||
|
||||
func newRecoverLoopInCommand() *cobra.Command {
|
||||
cc := &recoverLoopInCommand{}
|
||||
cc.cmd = &cobra.Command{
|
||||
Use: "recoverloopin",
|
||||
Short: "Recover a loop in swap that the loop daemon " +
|
||||
"is not able to sweep",
|
||||
Example: `chantools recoverloopin \
|
||||
--txid abcdef01234... \
|
||||
--vout 0 \
|
||||
--swap_hash abcdef01234... \
|
||||
--loop_db_path /path/to/loop.db \
|
||||
--sweep_addr bc1pxxxxxxx \
|
||||
--feerate 10`,
|
||||
RunE: cc.Execute,
|
||||
}
|
||||
cc.cmd.Flags().StringVar(
|
||||
&cc.TxID, "txid", "", "transaction id of the on-chain "+
|
||||
"transaction that created the HTLC",
|
||||
)
|
||||
cc.cmd.Flags().Uint32Var(
|
||||
&cc.Vout, "vout", 0, "output index of the on-chain "+
|
||||
"transaction that created the HTLC",
|
||||
)
|
||||
cc.cmd.Flags().StringVar(
|
||||
&cc.SwapHash, "swap_hash", "", "swap hash of the loop in "+
|
||||
"swap",
|
||||
)
|
||||
cc.cmd.Flags().StringVar(
|
||||
&cc.LoopDbPath, "loop_db_path", "", "path to the loop "+
|
||||
"database file",
|
||||
)
|
||||
cc.cmd.Flags().StringVar(
|
||||
&cc.SweepAddr, "sweep_addr", "", "address to recover "+
|
||||
"the funds to",
|
||||
)
|
||||
cc.cmd.Flags().Uint16Var(
|
||||
&cc.FeeRate, "feerate", 0, "fee rate to "+
|
||||
"use for the sweep transaction in sat/vByte",
|
||||
)
|
||||
cc.cmd.Flags().IntVar(
|
||||
&cc.NumTries, "num_tries", 1000, "number of tries to "+
|
||||
"try to find the correct key index",
|
||||
)
|
||||
cc.cmd.Flags().IntVar(
|
||||
&cc.StartKeyIndex, "start_key_index", 0, "start key index "+
|
||||
"to try to find the correct key index",
|
||||
)
|
||||
cc.cmd.Flags().StringVar(
|
||||
&cc.APIURL, "apiurl", defaultAPIURL, "API URL to use (must "+
|
||||
"be esplora compatible)",
|
||||
)
|
||||
cc.cmd.Flags().BoolVar(
|
||||
&cc.Publish, "publish", false, "publish sweep TX to the chain "+
|
||||
"API instead of just printing the TX",
|
||||
)
|
||||
|
||||
cc.rootKey = newRootKey(cc.cmd, "deriving starting key")
|
||||
|
||||
return cc.cmd
|
||||
}
|
||||
|
||||
func (c *recoverLoopInCommand) Execute(_ *cobra.Command, _ []string) error {
|
||||
extendedKey, err := c.rootKey.read()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading root key: %w", err)
|
||||
}
|
||||
|
||||
api := &btc.ExplorerAPI{BaseURL: c.APIURL}
|
||||
|
||||
signer := &lnd.Signer{
|
||||
ExtendedKey: extendedKey,
|
||||
ChainParams: chainParams,
|
||||
}
|
||||
|
||||
// Try to fetch the swap from the database.
|
||||
store, err := loopdb.NewBoltSwapStore(c.LoopDbPath, chainParams)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer store.Close()
|
||||
|
||||
swaps, err := store.FetchLoopInSwaps()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var loopIn *loopdb.LoopIn
|
||||
for _, s := range swaps {
|
||||
if s.Hash.String() == c.SwapHash {
|
||||
loopIn = s
|
||||
break
|
||||
}
|
||||
}
|
||||
if loopIn == nil {
|
||||
return fmt.Errorf("swap not found")
|
||||
}
|
||||
|
||||
fmt.Println("Loop expires at block height", loopIn.Contract.CltvExpiry)
|
||||
|
||||
// Get the swaps htlc.
|
||||
htlc, err := loop.GetHtlc(
|
||||
loopIn.Hash, &loopIn.Contract.SwapContract, chainParams,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get the destination address.
|
||||
sweepAddr, err := btcutil.DecodeAddress(c.SweepAddr, chainParams)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Calculate the sweep fee.
|
||||
estimator := &input.TxWeightEstimator{}
|
||||
err = htlc.AddTimeoutToEstimator(estimator)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch sweepAddr.(type) {
|
||||
case *btcutil.AddressWitnessPubKeyHash:
|
||||
estimator.AddP2WKHOutput()
|
||||
|
||||
case *btcutil.AddressTaproot:
|
||||
estimator.AddP2TROutput()
|
||||
|
||||
default:
|
||||
return fmt.Errorf("unsupported address type")
|
||||
}
|
||||
|
||||
feeRateKWeight := chainfee.SatPerKVByte(1000 * c.FeeRate).FeePerKWeight()
|
||||
fee := feeRateKWeight.FeeForWeight(int64(estimator.Weight()))
|
||||
|
||||
txID, err := chainhash.NewHashFromStr(c.TxID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get the htlc outpoint.
|
||||
htlcOutpoint := wire.OutPoint{
|
||||
Hash: *txID,
|
||||
Index: c.Vout,
|
||||
}
|
||||
|
||||
// Compose tx.
|
||||
sweepTx := wire.NewMsgTx(2)
|
||||
|
||||
sweepTx.LockTime = uint32(loopIn.Contract.CltvExpiry)
|
||||
|
||||
// Add HTLC input.
|
||||
sweepTx.AddTxIn(&wire.TxIn{
|
||||
PreviousOutPoint: htlcOutpoint,
|
||||
Sequence: 0,
|
||||
})
|
||||
|
||||
// Add output for the destination address.
|
||||
sweepPkScript, err := txscript.PayToAddrScript(sweepAddr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sweepTx.AddTxOut(&wire.TxOut{
|
||||
PkScript: sweepPkScript,
|
||||
Value: int64(loopIn.Contract.AmountRequested) - int64(fee),
|
||||
})
|
||||
|
||||
// If the htlc is version 2, we need to brute force the key locator, as
|
||||
// it is not stored in the database.
|
||||
var rawTx []byte
|
||||
if htlc.Version == swap.HtlcV2 {
|
||||
fmt.Println("Brute forcing key index...")
|
||||
for i := c.StartKeyIndex; i < c.StartKeyIndex+c.NumTries; i++ {
|
||||
rawTx, err = getSignedTx(
|
||||
signer, loopIn, sweepTx, htlc,
|
||||
keychain.KeyFamily(swap.KeyFamily), uint32(i),
|
||||
)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
if rawTx == nil {
|
||||
return fmt.Errorf("failed to brute force key index, " +
|
||||
"please try again with a higher start key index")
|
||||
}
|
||||
} else {
|
||||
rawTx, err = getSignedTx(
|
||||
signer, loopIn, sweepTx, htlc,
|
||||
loopIn.Contract.HtlcKeys.ClientScriptKeyLocator.Family,
|
||||
loopIn.Contract.HtlcKeys.ClientScriptKeyLocator.Index,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Publish TX.
|
||||
if c.Publish {
|
||||
response, err := api.PublishTx(
|
||||
hex.EncodeToString(rawTx),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Infof("Published TX %s, response: %s",
|
||||
sweepTx.TxHash().String(), response)
|
||||
} else {
|
||||
fmt.Printf("Success, we successfully created the sweep transaction. "+
|
||||
"Please publish this using any bitcoin node:\n\n%x\n\n",
|
||||
rawTx)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getSignedTx(signer *lnd.Signer, loopIn *loopdb.LoopIn, sweepTx *wire.MsgTx,
|
||||
htlc *swap.Htlc, keyFamily keychain.KeyFamily,
|
||||
keyIndex uint32) ([]byte, error) {
|
||||
|
||||
// Create the sign descriptor.
|
||||
prevoutFetcher := txscript.NewCannedPrevOutputFetcher(
|
||||
htlc.PkScript, int64(loopIn.Contract.AmountRequested),
|
||||
)
|
||||
|
||||
signDesc := &input.SignDescriptor{
|
||||
KeyDesc: keychain.KeyDescriptor{
|
||||
KeyLocator: keychain.KeyLocator{
|
||||
Family: keyFamily,
|
||||
Index: keyIndex,
|
||||
},
|
||||
},
|
||||
WitnessScript: htlc.TimeoutScript(),
|
||||
HashType: htlc.SigHash(),
|
||||
InputIndex: 0,
|
||||
PrevOutputFetcher: prevoutFetcher,
|
||||
Output: &wire.TxOut{
|
||||
PkScript: htlc.PkScript,
|
||||
Value: int64(loopIn.Contract.AmountRequested),
|
||||
},
|
||||
}
|
||||
switch htlc.Version {
|
||||
case swap.HtlcV2:
|
||||
signDesc.SignMethod = input.WitnessV0SignMethod
|
||||
|
||||
case swap.HtlcV3:
|
||||
signDesc.SignMethod = input.TaprootScriptSpendSignMethod
|
||||
}
|
||||
|
||||
sig, err := signer.SignOutputRaw(sweepTx, signDesc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
witness, err := htlc.GenTimeoutWitness(sig.Serialize())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sweepTx.TxIn[0].Witness = witness
|
||||
|
||||
rawTx, err := encodeTx(sweepTx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sighashes := txscript.NewTxSigHashes(sweepTx, prevoutFetcher)
|
||||
|
||||
// Verify the signature. This will throw an error if the signature is
|
||||
// invalid and allows us to bruteforce the key index.
|
||||
vm, err := txscript.NewEngine(
|
||||
htlc.PkScript, sweepTx, 0, txscript.StandardVerifyFlags, nil,
|
||||
sighashes, int64(loopIn.Contract.AmountRequested), prevoutFetcher,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = vm.Execute()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return rawTx, nil
|
||||
}
|
||||
|
||||
// encodeTx encodes a tx to raw bytes.
|
||||
func encodeTx(tx *wire.MsgTx) ([]byte, error) {
|
||||
var buffer bytes.Buffer
|
||||
err := tx.BtcEncode(&buffer, 0, wire.WitnessEncoding)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rawTx := buffer.Bytes()
|
||||
|
||||
return rawTx, nil
|
||||
}
|
Loading…
Reference in New Issue