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/sweeptimelock.go

368 lines
9.8 KiB
Go

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/hdkeychain"
"github.com/guggero/chantools/btc"
"github.com/guggero/chantools/dataformat"
"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 (
defaultFeeSatPerVByte = 30
defaultCsvLimit = 2016
)
type sweepTimeLockCommand struct {
APIURL string
Publish bool
SweepAddr string
MaxCsvLimit uint16
FeeRate uint16
rootKey *rootKey
inputs *inputFlags
cmd *cobra.Command
}
func newSweepTimeLockCommand() *cobra.Command {
cc := &sweepTimeLockCommand{}
cc.cmd = &cobra.Command{
Use: "sweeptimelock",
Short: "Sweep the force-closed state after the time lock has " +
"expired",
Long: `Use this command to sweep the funds from channels that
you force-closed with the forceclose command. You **MUST** use the result file
that was created with the forceclose command, otherwise it won't work. You also
have to wait until the highest time lock (can be up to 2016 blocks which is more
than two weeks) of all the channels has passed. If you only want to sweep
channels that have the default CSV limit of 1 day, you can set the --maxcsvlimit
parameter to 144.`,
Example: `chantools sweeptimelock \
--fromsummary results/forceclose-xxxx-yyyy.json \
--sweepaddr bc1q.....
--publish`,
RunE: cc.Execute,
}
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.MaxCsvLimit, "maxcsvlimit", defaultCsvLimit, "maximum CSV "+
"limit to use",
)
cc.cmd.Flags().Uint16Var(
&cc.FeeRate, "feerate", defaultFeeSatPerVByte, "fee rate to "+
"use for the sweep transaction in sat/vByte",
)
cc.rootKey = newRootKey(cc.cmd, "deriving keys")
cc.inputs = newInputFlags(cc.cmd)
return cc.cmd
}
func (c *sweepTimeLockCommand) 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")
}
// Parse channel entries from any of the possible input files.
entries, err := c.inputs.parseInputType()
if err != nil {
return err
}
// Set default values.
if c.MaxCsvLimit == 0 {
c.MaxCsvLimit = defaultCsvLimit
}
if c.FeeRate == 0 {
c.FeeRate = defaultFeeSatPerVByte
}
return sweepTimeLockFromSummary(
extendedKey, c.APIURL, entries, c.SweepAddr, c.MaxCsvLimit,
c.Publish, c.FeeRate,
)
}
type sweepTarget struct {
channelPoint string
txid chainhash.Hash
index uint32
lockScript []byte
value int64
commitPoint *btcec.PublicKey
revocationBasePoint *btcec.PublicKey
delayBasePointDesc *keychain.KeyDescriptor
}
func sweepTimeLockFromSummary(extendedKey *hdkeychain.ExtendedKey, apiURL string,
entries []*dataformat.SummaryEntry, sweepAddr string,
maxCsvTimeout uint16, publish bool, feeRate uint16) error {
targets := make([]*sweepTarget, 0, len(entries))
for _, entry := range entries {
// Skip entries that can't be swept.
if entry.ForceClose == nil ||
(entry.ClosingTX != nil && entry.ClosingTX.AllOutsSpent) ||
entry.LocalBalance == 0 {
log.Infof("Not sweeping %s, info missing or all spent",
entry.ChannelPoint)
continue
}
fc := entry.ForceClose
// Find index of sweepable output of commitment TX.
txindex := -1
if len(fc.Outs) == 1 {
txindex = 0
if fc.Outs[0].Value != entry.LocalBalance {
log.Errorf("Potential value mismatch! %d vs "+
"%d (%s)",
fc.Outs[0].Value, entry.LocalBalance,
entry.ChannelPoint)
}
} else {
for idx, out := range fc.Outs {
if out.Value == entry.LocalBalance {
txindex = idx
}
}
}
if txindex == -1 {
log.Errorf("Could not find sweep output for chan %s",
entry.ChannelPoint)
continue
}
// Prepare sweep script parameters.
commitPoint, err := pubKeyFromHex(fc.CommitPoint)
if err != nil {
return fmt.Errorf("error parsing commit point: %v", err)
}
revBase, err := pubKeyFromHex(fc.RevocationBasePoint.PubKey)
if err != nil {
return fmt.Errorf("error parsing revocation base "+
"point: %v", err)
}
delayDesc, err := fc.DelayBasePoint.Desc()
if err != nil {
return fmt.Errorf("error parsing delay base point: %v",
err)
}
lockScript, err := hex.DecodeString(fc.Outs[txindex].Script)
if err != nil {
return fmt.Errorf("error parsing target script: %v",
err)
}
// Create the transaction input.
txHash, err := chainhash.NewHashFromStr(fc.TXID)
if err != nil {
return fmt.Errorf("error parsing tx hash: %v", err)
}
targets = append(targets, &sweepTarget{
channelPoint: entry.ChannelPoint,
txid: *txHash,
index: uint32(txindex),
lockScript: lockScript,
value: int64(fc.Outs[txindex].Value),
commitPoint: commitPoint,
revocationBasePoint: revBase,
delayBasePointDesc: delayDesc,
})
}
return sweepTimeLock(
extendedKey, apiURL, targets, sweepAddr, maxCsvTimeout, publish,
feeRate,
)
}
func sweepTimeLock(extendedKey *hdkeychain.ExtendedKey, apiURL string,
targets []*sweepTarget, sweepAddr string, maxCsvTimeout uint16,
publish bool, feeRate uint16) error {
// Create signer and transaction template.
signer := &lnd.Signer{
ExtendedKey: extendedKey,
ChainParams: chainParams,
}
api := &btc.ExplorerAPI{BaseURL: apiURL}
sweepTx := wire.NewMsgTx(2)
totalOutputValue := int64(0)
signDescs := make([]*input.SignDescriptor, 0)
var estimator input.TxWeightEstimator
for _, target := range targets {
// We can't rely on the CSV delay of the channel DB to be
// correct. But it doesn't cost us a lot to just brute force it.
csvTimeout, script, scriptHash, err := bruteForceDelay(
input.TweakPubKey(
target.delayBasePointDesc.PubKey,
target.commitPoint,
), input.DeriveRevocationPubkey(
target.revocationBasePoint,
target.commitPoint,
), target.lockScript, maxCsvTimeout,
)
if err != nil {
log.Errorf("Could not create matching script for %s "+
"or csv too high: %v", target.channelPoint, err)
continue
}
// Create the transaction input.
sweepTx.TxIn = append(sweepTx.TxIn, &wire.TxIn{
PreviousOutPoint: wire.OutPoint{
Hash: target.txid,
Index: target.index,
},
Sequence: input.LockTimeToSequence(
false, uint32(csvTimeout),
),
})
// Create the sign descriptor for the input.
signDesc := &input.SignDescriptor{
KeyDesc: *target.delayBasePointDesc,
SingleTweak: input.SingleTweakBytes(
target.commitPoint,
target.delayBasePointDesc.PubKey,
),
WitnessScript: script,
Output: &wire.TxOut{
PkScript: scriptHash,
Value: target.value,
},
HashType: txscript.SigHashAll,
}
totalOutputValue += target.value
signDescs = append(signDescs, signDesc)
// Account for the input weight.
estimator.AddWitnessInput(input.ToLocalTimeoutWitnessSize)
}
// 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: totalOutputValue - int64(totalFee),
PkScript: sweepScript,
}}
// Sign the transaction now.
sigHashes := txscript.NewTxSigHashes(sweepTx)
for idx, desc := range signDescs {
desc.SigHashes = sigHashes
desc.InputIndex = idx
witness, err := input.CommitSpendTimeout(signer, desc, sweepTx)
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 pubKeyFromHex(pubKeyHex string) (*btcec.PublicKey, error) {
pointBytes, err := hex.DecodeString(pubKeyHex)
if err != nil {
return nil, fmt.Errorf("error hex decoding pub key: %v", err)
}
return btcec.ParsePubKey(pointBytes, btcec.S256())
}
func bruteForceDelay(delayPubkey, revocationPubkey *btcec.PublicKey,
targetScript []byte, maxCsvTimeout uint16) (int32, []byte, []byte,
error) {
if len(targetScript) != 34 {
return 0, nil, nil, fmt.Errorf("invalid target script: %s",
targetScript)
}
for i := uint16(0); i <= maxCsvTimeout; i++ {
s, err := input.CommitScriptToSelf(
uint32(i), delayPubkey, revocationPubkey,
)
if err != nil {
return 0, nil, nil, fmt.Errorf("error creating "+
"script: %v", err)
}
sh, err := input.WitnessScriptHash(s)
if err != nil {
return 0, nil, nil, fmt.Errorf("error hashing script: "+
"%v", err)
}
if bytes.Equal(targetScript[0:8], sh[0:8]) {
return int32(i), s, sh, nil
}
}
return 0, nil, nil, fmt.Errorf("csv timeout not found for target "+
"script %s", targetScript)
}