sweepremoteclosed: add command for sweeping closed channels

pull/32/head
Oliver Gugger 3 years ago
parent fe9233761e
commit 0821c35442
No known key found for this signature in database
GPG Key ID: 8E4256593F177720

@ -316,6 +316,7 @@ Quick access:
+ [showrootkey](doc/chantools_showrootkey.md)
+ [signrescuefunding](doc/chantools_signrescuefunding.md)
+ [summary](doc/chantools_summary.md)
+ [sweepremoteclosed](doc/chantools_sweepremoteclosed.md)
+ [sweeptimelock](doc/chantools_sweeptimelock.md)
+ [sweeptimelockmanual](doc/chantools_sweeptimelockmanual.md)
+ [vanitygen](doc/chantools_vanitygen.md)

@ -6,7 +6,6 @@ import (
"time"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
"github.com/btcsuite/btcutil/hdkeychain"
@ -193,24 +192,15 @@ func (i *ImportWallet) Format(hdKey *hdkeychain.ExtendedKey,
if err != nil {
return "", fmt.Errorf("could not encode WIF: %v", err)
}
pubKey, err := hdKey.ECPubKey()
if err != nil {
return "", fmt.Errorf("could not derive private key: %v", err)
}
hash160 := btcutil.Hash160(pubKey.SerializeCompressed())
addrP2PKH, err := btcutil.NewAddressPubKeyHash(hash160, params)
addrP2PKH, err := lnd.P2PKHAddr(privKey.PubKey(), params)
if err != nil {
return "", fmt.Errorf("could not create address: %v", err)
}
addrP2WKH, err := btcutil.NewAddressWitnessPubKeyHash(hash160, params)
addrP2WKH, err := lnd.P2WKHAddr(privKey.PubKey(), params)
if err != nil {
return "", fmt.Errorf("could not create address: %v", err)
}
script, err := txscript.PayToAddrScript(addrP2WKH)
if err != nil {
return "", fmt.Errorf("could not create script: %v", err)
}
addrNP2WKH, err := btcutil.NewAddressScriptHash(script, params)
addrNP2WKH, err := lnd.NP2WKHAddr(privKey.PubKey(), params)
if err != nil {
return "", fmt.Errorf("could not create address: %v", err)
}

@ -53,6 +53,20 @@ type Status struct {
BlockHash string `json:"block_hash"`
}
type Stats struct {
FundedTXOCount uint32 `json:"funded_txo_count"`
FundedTXOSum uint64 `json:"funded_txo_sum"`
SpentTXOCount uint32 `json:"spent_txo_count"`
SpentTXOSum uint64 `json:"spent_txo_sum"`
TXCount uint32 `json:"tx_count"`
}
type AddressStats struct {
Address string `json:"address"`
ChainStats *Stats `json:"chain_stats"`
MempoolStats *Stats `json:"mempool_stats"`
}
func (a *ExplorerAPI) Transaction(txid string) (*TX, error) {
tx := &TX{}
err := fetchJSON(fmt.Sprintf("%s/tx/%s", a.BaseURL, txid), tx)
@ -90,6 +104,46 @@ func (a *ExplorerAPI) Outpoint(addr string) (*TX, int, error) {
return nil, 0, fmt.Errorf("no tx found")
}
func (a *ExplorerAPI) Unspent(addr string) ([]*Vout, error) {
var (
stats = &AddressStats{}
outputs []*Vout
txs []*TX
err error
)
err = fetchJSON(fmt.Sprintf("%s/address/%s", a.BaseURL, addr), &stats)
if err != nil {
return nil, err
}
confirmedUnspent := stats.ChainStats.FundedTXOSum -
stats.ChainStats.SpentTXOSum
unconfirmedUnspent := stats.MempoolStats.FundedTXOSum -
stats.MempoolStats.SpentTXOSum
if confirmedUnspent+unconfirmedUnspent == 0 {
return nil, nil
}
err = fetchJSON(fmt.Sprintf("%s/address/%s/txs", a.BaseURL, addr), &txs)
if err != nil {
return nil, err
}
for _, tx := range txs {
for voutIdx, vout := range tx.Vout {
if vout.ScriptPubkeyAddr == addr {
vout.Outspend = &Outspend{
Txid: tx.TXID,
Vin: voutIdx,
}
outputs = append(outputs, vout)
}
}
}
return outputs, nil
}
func (a *ExplorerAPI) Address(outpoint string) (string, error) {
parts := strings.Split(outpoint, ":")

@ -101,6 +101,7 @@ func main() {
newSummaryCommand(),
newSweepTimeLockCommand(),
newSweepTimeLockManualCommand(),
newSweepRemoteClosedCommand(),
newVanityGenCommand(),
newWalletInfoCommand(),
newZombieRecoveryCommand(),

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

@ -37,6 +37,7 @@ Complete documentation is available at https://github.com/guggero/chantools/.
* [chantools showrootkey](chantools_showrootkey.md) - Extract and show the BIP32 HD root key from the 24 word lnd aezeed
* [chantools signrescuefunding](chantools_signrescuefunding.md) - Rescue funds locked in a funding multisig output that never resulted in a proper channel; this is the command the remote node (the non-initiator) of the channel needs to run
* [chantools summary](chantools_summary.md) - Compile a summary about the current state of channels
* [chantools sweepremoteclosed](chantools_sweepremoteclosed.md) - 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
* [chantools sweeptimelock](chantools_sweeptimelock.md) - Sweep the force-closed state after the time lock has expired
* [chantools sweeptimelockmanual](chantools_sweeptimelockmanual.md) - Sweep the force-closed state of a single channel manually if only a channel backup file is available
* [chantools vanitygen](chantools_vanitygen.md) - Generate a seed with a custom lnd node identity public key that starts with the given prefix

@ -61,7 +61,7 @@ chantools fakechanbackup --from_channel_graph lncli_describegraph.json \
--channelpoint string funding transaction outpoint of the channel to rescue (<txid>:<txindex>) as it is displayed on 1ml.com
--from_channel_graph string the full LN channel graph in the JSON format that the 'lncli describegraph' returns
-h, --help help for fakechanbackup
--multi_file string the fake channel backup file to create (default "results/fake-2021-08-13-13-09-39.backup")
--multi_file string the fake channel backup file to create (default "results/fake-2021-08-29-18-21-11.backup")
--remote_node_addr string the remote node connection information in the format pubkey@host:port
--rootkey string BIP32 HD root key of the wallet to use for encrypting the backup; leave empty to prompt for lnd 24 word aezeed
--short_channel_id string the short channel ID in the format <blockheight>x<transactionindex>x<outputindex>

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

@ -1,6 +1,7 @@
package lnd
import (
"crypto/sha256"
"fmt"
"strconv"
"strings"
@ -11,6 +12,7 @@ import (
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcutil"
"github.com/btcsuite/btcutil/hdkeychain"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/shachain"
)
@ -18,6 +20,7 @@ import (
const (
HardenedKeyStart = uint32(hdkeychain.HardenedKeyStart)
WalletDefaultDerivationPath = "m/84'/0'/0'"
WalletBIP49DerivationPath = "m/49'/0'/0'"
LndDerivationPath = "m/1017'/%d'/%d'"
)
@ -220,6 +223,21 @@ func DecodeAddressHash(addr string, chainParams *chaincfg.Params) ([]byte, bool,
return targetHash, isScriptHash, nil
}
func GetWitnessAddrScript(addr btcutil.Address,
chainParams *chaincfg.Params) ([]byte, error) {
if !addr.IsForNet(chainParams) {
return nil, fmt.Errorf("address %v is not for net %v", addr,
chainParams.Name)
}
builder := txscript.NewScriptBuilder()
builder.AddOp(txscript.OP_0)
builder.AddData(addr.ScriptAddress())
return builder.Script()
}
// GetP2WPKHScript creates a P2WKH output script from an address. If the address
// is not a P2WKH address, an error is returned.
func GetP2WPKHScript(addr string, chainParams *chaincfg.Params) ([]byte,
@ -268,6 +286,53 @@ func GetP2WSHScript(addr string, chainParams *chaincfg.Params) ([]byte,
return builder.Script()
}
func P2PKHAddr(pubKey *btcec.PublicKey,
params *chaincfg.Params) (*btcutil.AddressPubKeyHash, error) {
hash160 := btcutil.Hash160(pubKey.SerializeCompressed())
addrP2PKH, err := btcutil.NewAddressPubKeyHash(hash160, params)
if err != nil {
return nil, fmt.Errorf("could not create address: %v", err)
}
return addrP2PKH, nil
}
func P2WKHAddr(pubKey *btcec.PublicKey,
params *chaincfg.Params) (*btcutil.AddressWitnessPubKeyHash, error) {
hash160 := btcutil.Hash160(pubKey.SerializeCompressed())
return btcutil.NewAddressWitnessPubKeyHash(hash160, params)
}
func NP2WKHAddr(pubKey *btcec.PublicKey,
params *chaincfg.Params) (*btcutil.AddressScriptHash, error) {
hash160 := btcutil.Hash160(pubKey.SerializeCompressed())
addrP2WKH, err := btcutil.NewAddressWitnessPubKeyHash(hash160, params)
if err != nil {
return nil, fmt.Errorf("could not create address: %v", err)
}
script, err := txscript.PayToAddrScript(addrP2WKH)
if err != nil {
return nil, fmt.Errorf("could not create script: %v", err)
}
return btcutil.NewAddressScriptHash(script, params)
}
func P2AnchorStaticRemote(pubKey *btcec.PublicKey,
params *chaincfg.Params) (*btcutil.AddressWitnessScriptHash, []byte,
error) {
commitScript, err := input.CommitScriptToRemoteConfirmed(pubKey)
if err != nil {
return nil, nil, fmt.Errorf("could not create script: %v", err)
}
scriptHash := sha256.Sum256(commitScript)
p2wsh, err := btcutil.NewAddressWitnessScriptHash(scriptHash[:], params)
return p2wsh, commitScript, err
}
type HDKeyRing struct {
ExtendedKey *hdkeychain.ExtendedKey
ChainParams *chaincfg.Params

Loading…
Cancel
Save