mirror of https://github.com/guggero/chantools
sweepremoteclosed: add command for sweeping closed channels
parent
fe9233761e
commit
0821c35442
@ -0,0 +1,358 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/btcec"
|
||||||
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||||
|
"github.com/btcsuite/btcd/txscript"
|
||||||
|
"github.com/btcsuite/btcd/wire"
|
||||||
|
"github.com/btcsuite/btcutil"
|
||||||
|
"github.com/btcsuite/btcutil/hdkeychain"
|
||||||
|
"github.com/guggero/chantools/btc"
|
||||||
|
"github.com/guggero/chantools/lnd"
|
||||||
|
"github.com/lightningnetwork/lnd/input"
|
||||||
|
"github.com/lightningnetwork/lnd/keychain"
|
||||||
|
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
sweepRemoteClosedDefaultRecoveryWindow = 200
|
||||||
|
sweepDustLimit = 600
|
||||||
|
)
|
||||||
|
|
||||||
|
type sweepRemoteClosedCommand struct {
|
||||||
|
RecoveryWindow uint32
|
||||||
|
APIURL string
|
||||||
|
Publish bool
|
||||||
|
SweepAddr string
|
||||||
|
FeeRate uint16
|
||||||
|
|
||||||
|
rootKey *rootKey
|
||||||
|
cmd *cobra.Command
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSweepRemoteClosedCommand() *cobra.Command {
|
||||||
|
cc := &sweepRemoteClosedCommand{}
|
||||||
|
cc.cmd = &cobra.Command{
|
||||||
|
Use: "sweepremoteclosed",
|
||||||
|
Short: "Go through all the addresses that could have funds of " +
|
||||||
|
"channels that were force-closed by the remote party. " +
|
||||||
|
"A public block explorer is queried for each address " +
|
||||||
|
"and if any balance is found, all funds are swept to " +
|
||||||
|
"a given address",
|
||||||
|
Long: `This command helps users sweep funds that are in
|
||||||
|
outputs of channels that were force-closed by the remote party. This command
|
||||||
|
only needs to be used if no channel.backup file is available. By manually
|
||||||
|
contacting the remote peers and asking them to force-close the channels, the
|
||||||
|
funds can be swept after the force-close transaction was confirmed.
|
||||||
|
|
||||||
|
Supported remote force-closed channel types are:
|
||||||
|
- STATIC_REMOTE_KEY (a.k.a. tweakless channels)
|
||||||
|
- ANCHOR (a.k.a. anchor output channels)
|
||||||
|
`,
|
||||||
|
Example: `chantools sweepremoteclosed \
|
||||||
|
--recoverywindow 300 \
|
||||||
|
--feerate 20 \
|
||||||
|
--sweepaddr bc1q..... \
|
||||||
|
--publish`,
|
||||||
|
RunE: cc.Execute,
|
||||||
|
}
|
||||||
|
cc.cmd.Flags().Uint32Var(
|
||||||
|
&cc.RecoveryWindow, "recoverywindow",
|
||||||
|
sweepRemoteClosedDefaultRecoveryWindow, "number of keys to "+
|
||||||
|
"scan per derivation path",
|
||||||
|
)
|
||||||
|
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.cmd.Flags().StringVar(
|
||||||
|
&cc.SweepAddr, "sweepaddr", "", "address to sweep the funds to",
|
||||||
|
)
|
||||||
|
cc.cmd.Flags().Uint16Var(
|
||||||
|
&cc.FeeRate, "feerate", defaultFeeSatPerVByte, "fee rate to "+
|
||||||
|
"use for the sweep transaction in sat/vByte",
|
||||||
|
)
|
||||||
|
|
||||||
|
cc.rootKey = newRootKey(cc.cmd, "sweeping the wallet")
|
||||||
|
|
||||||
|
return cc.cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *sweepRemoteClosedCommand) Execute(_ *cobra.Command, _ []string) error {
|
||||||
|
extendedKey, err := c.rootKey.read()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error reading root key: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure sweep addr is set.
|
||||||
|
if c.SweepAddr == "" {
|
||||||
|
return fmt.Errorf("sweep addr is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set default values.
|
||||||
|
if c.RecoveryWindow == 0 {
|
||||||
|
c.RecoveryWindow = sweepRemoteClosedDefaultRecoveryWindow
|
||||||
|
}
|
||||||
|
if c.FeeRate == 0 {
|
||||||
|
c.FeeRate = defaultFeeSatPerVByte
|
||||||
|
}
|
||||||
|
|
||||||
|
return sweepRemoteClosed(
|
||||||
|
extendedKey, c.APIURL, c.SweepAddr, c.RecoveryWindow, c.FeeRate,
|
||||||
|
c.Publish,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type targetAddr struct {
|
||||||
|
addr btcutil.Address
|
||||||
|
pubKey *btcec.PublicKey
|
||||||
|
path string
|
||||||
|
keyDesc *keychain.KeyDescriptor
|
||||||
|
vouts []*btc.Vout
|
||||||
|
script []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func sweepRemoteClosed(extendedKey *hdkeychain.ExtendedKey, apiURL,
|
||||||
|
sweepAddr string, recoveryWindow uint32, feeRate uint16,
|
||||||
|
publish bool) error {
|
||||||
|
|
||||||
|
var (
|
||||||
|
targets []*targetAddr
|
||||||
|
api = &btc.ExplorerAPI{BaseURL: apiURL}
|
||||||
|
)
|
||||||
|
for index := uint32(0); index < recoveryWindow; index++ {
|
||||||
|
path := fmt.Sprintf("m/1017'/%d'/%d'/0/%d",
|
||||||
|
chainParams.HDCoinType, keychain.KeyFamilyPaymentBase,
|
||||||
|
index)
|
||||||
|
parsedPath, err := lnd.ParsePath(path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error parsing path: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
hdKey, err := lnd.DeriveChildren(
|
||||||
|
extendedKey, parsedPath,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("eror deriving children: %v",
|
||||||
|
err)
|
||||||
|
}
|
||||||
|
|
||||||
|
privKey, err := hdKey.ECPrivKey()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not derive private "+
|
||||||
|
"key: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
foundTargets, err := queryAddressBalances(
|
||||||
|
privKey.PubKey(), path, &keychain.KeyDescriptor{
|
||||||
|
PubKey: privKey.PubKey(),
|
||||||
|
KeyLocator: keychain.KeyLocator{
|
||||||
|
Family: keychain.KeyFamilyPaymentBase,
|
||||||
|
Index: index,
|
||||||
|
},
|
||||||
|
}, api,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not query API for "+
|
||||||
|
"addresses with funds: %v", err)
|
||||||
|
}
|
||||||
|
targets = append(targets, foundTargets...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create estimator and transaction template.
|
||||||
|
var (
|
||||||
|
estimator input.TxWeightEstimator
|
||||||
|
signDescs []*input.SignDescriptor
|
||||||
|
sweepTx = wire.NewMsgTx(2)
|
||||||
|
totalOutputValue = uint64(0)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Add all found target outputs.
|
||||||
|
for _, target := range targets {
|
||||||
|
for _, vout := range target.vouts {
|
||||||
|
totalOutputValue += vout.Value
|
||||||
|
|
||||||
|
txHash, err := chainhash.NewHashFromStr(
|
||||||
|
vout.Outspend.Txid,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error parsing tx hash: %v",
|
||||||
|
err)
|
||||||
|
}
|
||||||
|
pkScript, err := lnd.GetWitnessAddrScript(
|
||||||
|
target.addr, chainParams,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error getting pk script: %v",
|
||||||
|
err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sequence := wire.MaxTxInSequenceNum
|
||||||
|
switch target.addr.(type) {
|
||||||
|
case *btcutil.AddressWitnessPubKeyHash:
|
||||||
|
estimator.AddP2WKHInput()
|
||||||
|
|
||||||
|
case *btcutil.AddressWitnessScriptHash:
|
||||||
|
estimator.AddWitnessInput(
|
||||||
|
input.ToRemoteConfirmedWitnessSize,
|
||||||
|
)
|
||||||
|
sequence = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
sweepTx.TxIn = append(sweepTx.TxIn, &wire.TxIn{
|
||||||
|
PreviousOutPoint: wire.OutPoint{
|
||||||
|
Hash: *txHash,
|
||||||
|
Index: uint32(vout.Outspend.Vin),
|
||||||
|
},
|
||||||
|
Sequence: sequence,
|
||||||
|
})
|
||||||
|
|
||||||
|
signDescs = append(signDescs, &input.SignDescriptor{
|
||||||
|
KeyDesc: *target.keyDesc,
|
||||||
|
WitnessScript: target.script,
|
||||||
|
Output: &wire.TxOut{
|
||||||
|
PkScript: pkScript,
|
||||||
|
Value: int64(vout.Value),
|
||||||
|
},
|
||||||
|
HashType: txscript.SigHashAll,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(targets) == 0 || totalOutputValue < sweepDustLimit {
|
||||||
|
return fmt.Errorf("found %d sweep targets with total value "+
|
||||||
|
"of %d satoshis which is below the dust limit of %d",
|
||||||
|
len(targets), totalOutputValue, sweepDustLimit)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add our sweep destination output.
|
||||||
|
sweepScript, err := lnd.GetP2WPKHScript(sweepAddr, chainParams)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
estimator.AddP2WKHOutput()
|
||||||
|
|
||||||
|
// Calculate the fee based on the given fee rate and our weight
|
||||||
|
// estimation.
|
||||||
|
feeRateKWeight := chainfee.SatPerKVByte(1000 * feeRate).FeePerKWeight()
|
||||||
|
totalFee := feeRateKWeight.FeeForWeight(int64(estimator.Weight()))
|
||||||
|
|
||||||
|
log.Infof("Fee %d sats of %d total amount (estimated weight %d)",
|
||||||
|
totalFee, totalOutputValue, estimator.Weight())
|
||||||
|
|
||||||
|
sweepTx.TxOut = []*wire.TxOut{{
|
||||||
|
Value: int64(totalOutputValue) - int64(totalFee),
|
||||||
|
PkScript: sweepScript,
|
||||||
|
}}
|
||||||
|
|
||||||
|
// Sign the transaction now.
|
||||||
|
var (
|
||||||
|
signer = &lnd.Signer{
|
||||||
|
ExtendedKey: extendedKey,
|
||||||
|
ChainParams: chainParams,
|
||||||
|
}
|
||||||
|
sigHashes = txscript.NewTxSigHashes(sweepTx)
|
||||||
|
)
|
||||||
|
for idx, desc := range signDescs {
|
||||||
|
desc.SigHashes = sigHashes
|
||||||
|
desc.InputIndex = idx
|
||||||
|
|
||||||
|
if len(desc.WitnessScript) > 0 {
|
||||||
|
witness, err := input.CommitSpendToRemoteConfirmed(
|
||||||
|
signer, desc, sweepTx,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
sweepTx.TxIn[idx].Witness = witness
|
||||||
|
} else {
|
||||||
|
// The txscript library expects the witness script of a
|
||||||
|
// P2WKH descriptor to be set to the pkScript of the
|
||||||
|
// output...
|
||||||
|
desc.WitnessScript = desc.Output.PkScript
|
||||||
|
witness, err := input.CommitSpendNoDelay(
|
||||||
|
signer, desc, sweepTx, true,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
sweepTx.TxIn[idx].Witness = witness
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
err = sweepTx.Serialize(&buf)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Publish TX.
|
||||||
|
if publish {
|
||||||
|
response, err := api.PublishTx(
|
||||||
|
hex.EncodeToString(buf.Bytes()),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Infof("Published TX %s, response: %s",
|
||||||
|
sweepTx.TxHash().String(), response)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("Transaction: %x", buf.Bytes())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func queryAddressBalances(pubKey *btcec.PublicKey, path string,
|
||||||
|
keyDesc *keychain.KeyDescriptor, api *btc.ExplorerAPI) ([]*targetAddr,
|
||||||
|
error) {
|
||||||
|
|
||||||
|
var targets []*targetAddr
|
||||||
|
queryAddr := func(address btcutil.Address, script []byte) error {
|
||||||
|
unspent, err := api.Unspent(address.EncodeAddress())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not query unspent: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(unspent) > 0 {
|
||||||
|
log.Infof("Found %d unspent outputs for address %v",
|
||||||
|
len(unspent), address.EncodeAddress())
|
||||||
|
targets = append(targets, &targetAddr{
|
||||||
|
addr: address,
|
||||||
|
pubKey: pubKey,
|
||||||
|
path: path,
|
||||||
|
keyDesc: keyDesc,
|
||||||
|
vouts: unspent,
|
||||||
|
script: script,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
p2wkh, err := lnd.P2WKHAddr(pubKey, chainParams)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := queryAddr(p2wkh, nil); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
p2anchor, script, err := lnd.P2AnchorStaticRemote(pubKey, chainParams)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := queryAddr(p2anchor, script); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return targets, nil
|
||||||
|
}
|
@ -0,0 +1,55 @@
|
|||||||
|
## chantools sweepremoteclosed
|
||||||
|
|
||||||
|
Go through all the addresses that could have funds of channels that were force-closed by the remote party. A public block explorer is queried for each address and if any balance is found, all funds are swept to a given address
|
||||||
|
|
||||||
|
### Synopsis
|
||||||
|
|
||||||
|
This command helps users sweep funds that are in
|
||||||
|
outputs of channels that were force-closed by the remote party. This command
|
||||||
|
only needs to be used if no channel.backup file is available. By manually
|
||||||
|
contacting the remote peers and asking them to force-close the channels, the
|
||||||
|
funds can be swept after the force-close transaction was confirmed.
|
||||||
|
|
||||||
|
Supported remote force-closed channel types are:
|
||||||
|
- STATIC_REMOTE_KEY (a.k.a. tweakless channels)
|
||||||
|
- ANCHOR (a.k.a. anchor output channels)
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
chantools sweepremoteclosed [flags]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
|
||||||
|
```
|
||||||
|
chantools sweepremoteclosed \
|
||||||
|
--recoverywindow 300 \
|
||||||
|
--feerate 20 \
|
||||||
|
--sweepaddr bc1q..... \
|
||||||
|
--publish
|
||||||
|
```
|
||||||
|
|
||||||
|
### Options
|
||||||
|
|
||||||
|
```
|
||||||
|
--apiurl string API URL to use (must be esplora compatible) (default "https://blockstream.info/api")
|
||||||
|
--bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag
|
||||||
|
--feerate uint16 fee rate to use for the sweep transaction in sat/vByte (default 30)
|
||||||
|
-h, --help help for sweepremoteclosed
|
||||||
|
--publish publish sweep TX to the chain API instead of just printing the TX
|
||||||
|
--recoverywindow uint32 number of keys to scan per derivation path (default 200)
|
||||||
|
--rootkey string BIP32 HD root key of the wallet to use for sweeping the wallet; leave empty to prompt for lnd 24 word aezeed
|
||||||
|
--sweepaddr string address to sweep the funds to
|
||||||
|
```
|
||||||
|
|
||||||
|
### Options inherited from parent commands
|
||||||
|
|
||||||
|
```
|
||||||
|
-r, --regtest Indicates if regtest parameters should be used
|
||||||
|
-t, --testnet Indicates if testnet parameters should be used
|
||||||
|
```
|
||||||
|
|
||||||
|
### SEE ALSO
|
||||||
|
|
||||||
|
* [chantools](chantools.md) - Chantools helps recover funds from lightning channels
|
||||||
|
|
Loading…
Reference in New Issue