You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
chantools/cmd/chantools/sweepremoteclosed.go

442 lines
11 KiB
Go

package main
import (
"bytes"
"encoding/hex"
"fmt"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/btcutil/hdkeychain"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/chantools/btc"
"github.com/lightninglabs/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 uint32
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)
- SIMPLE_TAPROOT (a.k.a. simple taproot 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 recover the funds "+
"to; specify '"+lnd.AddressDeriveFromWallet+"' to "+
"derive a new address from the seed automatically",
)
cc.cmd.Flags().Uint32Var(
&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: %w", err)
}
// Make sure sweep addr is set.
err = lnd.CheckAddress(
c.SweepAddr, chainParams, true, "sweep", lnd.AddrTypeP2WKH,
lnd.AddrTypeP2TR,
)
if err != nil {
return err
}
// 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
scriptTree *input.CommitScriptTree
}
func sweepRemoteClosed(extendedKey *hdkeychain.ExtendedKey, apiURL,
sweepAddr string, recoveryWindow uint32, feeRate uint32,
publish bool) error {
var estimator input.TxWeightEstimator
sweepScript, err := lnd.PrepareWalletAddress(
sweepAddr, chainParams, &estimator, extendedKey, "sweep",
)
if err != nil {
return err
}
var (
targets []*targetAddr
api = newExplorerAPI(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: %w", err)
}
hdKey, err := lnd.DeriveChildren(
extendedKey, parsedPath,
)
if err != nil {
return fmt.Errorf("eror deriving children: %w", err)
}
privKey, err := hdKey.ECPrivKey()
if err != nil {
return fmt.Errorf("could not derive private "+
"key: %w", 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: %w", err)
}
targets = append(targets, foundTargets...)
}
// Create estimator and transaction template.
var (
signDescs []*input.SignDescriptor
sweepTx = wire.NewMsgTx(2)
totalOutputValue = uint64(0)
prevOutFetcher = txscript.NewMultiPrevOutFetcher(nil)
)
// 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: %w",
err)
}
pkScript, err := lnd.GetWitnessAddrScript(
target.addr, chainParams,
)
if err != nil {
return fmt.Errorf("error getting pk script: %w",
err)
}
prevOutPoint := wire.OutPoint{
Hash: *txHash,
Index: uint32(vout.Outspend.Vin),
}
prevTxOut := &wire.TxOut{
PkScript: pkScript,
Value: int64(vout.Value),
}
prevOutFetcher.AddPrevOut(prevOutPoint, prevTxOut)
txIn := &wire.TxIn{
PreviousOutPoint: prevOutPoint,
Sequence: wire.MaxTxInSequenceNum,
}
sweepTx.TxIn = append(sweepTx.TxIn, txIn)
inputIndex := len(sweepTx.TxIn) - 1
var signDesc *input.SignDescriptor
switch target.addr.(type) {
case *btcutil.AddressWitnessPubKeyHash:
estimator.AddP2WKHInput()
signDesc = &input.SignDescriptor{
KeyDesc: *target.keyDesc,
WitnessScript: target.script,
Output: prevTxOut,
HashType: txscript.SigHashAll,
PrevOutputFetcher: prevOutFetcher,
InputIndex: inputIndex,
}
case *btcutil.AddressWitnessScriptHash:
estimator.AddWitnessInput(
input.ToRemoteConfirmedWitnessSize,
)
txIn.Sequence = 1
signDesc = &input.SignDescriptor{
KeyDesc: *target.keyDesc,
WitnessScript: target.script,
Output: prevTxOut,
HashType: txscript.SigHashAll,
PrevOutputFetcher: prevOutFetcher,
InputIndex: inputIndex,
}
case *btcutil.AddressTaproot:
estimator.AddWitnessInput(
input.TaprootToRemoteWitnessSize,
)
txIn.Sequence = 1
tree := target.scriptTree
controlBlock, err := tree.CtrlBlockForPath(
input.ScriptPathSuccess,
)
if err != nil {
return err
}
controlBlockBytes, err := controlBlock.ToBytes()
if err != nil {
return err
}
script := tree.SettleLeaf.Script
signMethod := input.TaprootScriptSpendSignMethod
signDesc = &input.SignDescriptor{
KeyDesc: *target.keyDesc,
WitnessScript: script,
Output: prevTxOut,
HashType: txscript.SigHashDefault,
PrevOutputFetcher: prevOutFetcher,
ControlBlock: controlBlockBytes,
InputIndex: inputIndex,
SignMethod: signMethod,
TapTweak: tree.TapscriptRoot,
}
}
signDescs = append(signDescs, signDesc)
}
}
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)
}
// 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, prevOutFetcher)
)
for idx, desc := range signDescs {
desc.SigHashes = sigHashes
desc.InputIndex = idx
switch {
// Simple Taproot Channels.
case desc.SignMethod == input.TaprootScriptSpendSignMethod:
witness, err := input.TaprootCommitSpendSuccess(
signer, desc, sweepTx, nil,
)
if err != nil {
return err
}
sweepTx.TxIn[idx].Witness = witness
// Anchor Channels.
case len(desc.WitnessScript) > 0:
witness, err := input.CommitSpendToRemoteConfirmed(
signer, desc, sweepTx,
)
if err != nil {
return err
}
sweepTx.TxIn[idx].Witness = witness
// Static Remote Key Channels.
default:
// 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,
scriptTree *input.CommitScriptTree) error {
unspent, err := api.Unspent(address.EncodeAddress())
if err != nil {
return fmt.Errorf("could not query unspent: %w", 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,
scriptTree: scriptTree,
})
}
return nil
}
p2wkh, err := lnd.P2WKHAddr(pubKey, chainParams)
if err != nil {
return nil, err
}
if err := queryAddr(p2wkh, nil, nil); err != nil {
return nil, err
}
p2anchor, script, err := lnd.P2AnchorStaticRemote(pubKey, chainParams)
if err != nil {
return nil, err
}
if err := queryAddr(p2anchor, script, nil); err != nil {
return nil, err
}
p2tr, scriptTree, err := lnd.P2TaprootStaticRemote(pubKey, chainParams)
if err != nil {
return nil, err
}
if err := queryAddr(p2tr, nil, scriptTree); err != nil {
return nil, err
}
return targets, nil
}