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
sputn1ck 1 year ago
parent aa767d3faa
commit ab0743a3d3
No known key found for this signature in database
GPG Key ID: 671103D881A5F0E4

@ -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
}

@ -95,6 +95,7 @@ func main() {
newForceCloseCommand(),
newGenImportScriptCommand(),
newMigrateDBCommand(),
newRecoverLoopInCommand(),
newRemoveChannelCommand(),
newRescueClosedCommand(),
newRescueFundingCommand(),

@ -17,6 +17,7 @@ require (
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1
github.com/gogo/protobuf v1.3.2
github.com/hasura/go-graphql-client v0.9.1
github.com/lightninglabs/loop v0.23.0-beta
github.com/lightninglabs/pool v0.6.2-beta.0.20230329135228-c3bffb52df3a
github.com/lightningnetwork/lnd v0.16.0-beta
github.com/lightningnetwork/lnd/kvdb v1.4.1
@ -93,8 +94,10 @@ require (
github.com/klauspost/compress v1.16.0 // indirect
github.com/klauspost/pgzip v1.2.5 // indirect
github.com/lib/pq v1.10.3 // indirect
github.com/lightninglabs/aperture v0.1.20-beta // indirect
github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf // indirect
github.com/lightninglabs/lndclient v0.16.0-10 // indirect
github.com/lightninglabs/loop/swapserverrpc v1.0.4 // indirect
github.com/lightninglabs/neutrino v0.15.0 // indirect
github.com/lightninglabs/neutrino/cache v1.1.1 // indirect
github.com/lightninglabs/pool/auctioneerrpc v1.0.7 // indirect

@ -177,6 +177,7 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fergusstrange/embedded-postgres v1.10.0 h1:YnwF6xAQYmKLAXXrrRx4rHDLih47YJwVPvg8jeKfdNg=
github.com/fergusstrange/embedded-postgres v1.10.0/go.mod h1:a008U8/Rws5FtIOTGYDYa7beVWsT3qVKyqExqYYjL+c=
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
github.com/frankban/quicktest v1.0.0/go.mod h1:R98jIehRai+d1/3Hv2//jOVCTJhW1VBavT6B6CuGq2k=
github.com/frankban/quicktest v1.2.2/go.mod h1:Qh/WofXFeiAFII1aEBu529AtJo6Zg2VHscnEsbBnJ20=
github.com/frankban/quicktest v1.11.2 h1:mjwHjStlXWibxOohM7HYieIViKyh56mmt3+6viyhDDI=
@ -463,10 +464,16 @@ github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lib/pq v1.10.3 h1:v9QZf2Sn6AmjXtQeFpdoq/eaNtYP6IN+7lcrygsIAtg=
github.com/lib/pq v1.10.3/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lightninglabs/aperture v0.1.20-beta h1:zMcYtzhaC+LsGpkS4Xkt6Qv2YeMHSL6wXQA0cydER1U=
github.com/lightninglabs/aperture v0.1.20-beta/go.mod h1:81OL9AHa8Wjm1HzRqTa6jkcafyaxJAsHZDIG5jj6RlU=
github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf h1:HZKvJUHlcXI/f/O0Avg7t8sqkPo78HFzjmeYFl6DPnc=
github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf/go.mod h1:vxmQPeIQxPf6Jf9rM8R+B4rKBqLA2AjttNxkFBL2Plk=
github.com/lightninglabs/lndclient v0.16.0-10 h1:cMBJNfssBQtpgYIu23QLP/qw0ijiT5SBZffnXz8zjJk=
github.com/lightninglabs/lndclient v0.16.0-10/go.mod h1:mqY0znSNa+M40HZowwKfno29RyZnmxoqo++BlYP82EY=
github.com/lightninglabs/loop v0.23.0-beta h1:me3g9erjnvoJq5udl7XLOC0e3Pp7+6YGupTNRIbl64E=
github.com/lightninglabs/loop v0.23.0-beta/go.mod h1:rh5c7KZMNV/GOJ79n3x5qrO9h6FZT7ZZ54b6/FPIhQI=
github.com/lightninglabs/loop/swapserverrpc v1.0.4 h1:cEX+mt7xmQlEbmuQ52vOBT7l+a471v94ofdJbB6MmXs=
github.com/lightninglabs/loop/swapserverrpc v1.0.4/go.mod h1:imy1/sqnb70EEyBKMo4pHwwLBPW8uYahWZ8s+1Xcq1o=
github.com/lightninglabs/neutrino v0.15.0 h1:yr3uz36fLAq8hyM0TRUVlef1TRNoWAqpmmNlVtKUDtI=
github.com/lightninglabs/neutrino v0.15.0/go.mod h1:pmjwElN/091TErtSE9Vd5W4hpxoG2/+xlb+HoPm9Gug=
github.com/lightninglabs/neutrino/cache v1.1.1 h1:TllWOSlkABhpgbWJfzsrdUaDH2fBy/54VSIB4vVqV8M=
@ -660,8 +667,8 @@ github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4A
github.com/ulikunitz/xz v0.5.7/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8=
github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/xdg-go/stringprep v1.0.2 h1:6iq84/ryjjeRmMJwxutI51F2GIPlP5BfTvXHeYjyhBc=
github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM=
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8=

Loading…
Cancel
Save