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

304 lines
8.0 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"
)
const (
feeSatPerByte = 2
)
type sweepTimeLockCommand struct {
RootKey string `long:"rootkey" description:"BIP32 HD root key to use. Leave empty to prompt for lnd 24 word aezeed."`
Publish bool `long:"publish" description:"Should the sweep TX be published to the chain API?"`
SweepAddr string `long:"sweepaddr" description:"The address the funds should be sweeped to"`
MaxCsvLimit int `long:"maxcsvlimit" description:"Maximum CSV limit to use. (default 2000)"`
}
func (c *sweepTimeLockCommand) Execute(_ []string) error {
setupChainParams(cfg)
var (
extendedKey *hdkeychain.ExtendedKey
err error
)
// Check that root key is valid or fall back to console input.
switch {
case c.RootKey != "":
extendedKey, err = hdkeychain.NewKeyFromString(c.RootKey)
default:
extendedKey, _, err = rootKeyFromConsole()
}
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 := parseInputType(cfg)
if err != nil {
return err
}
// Set default value
if c.MaxCsvLimit == 0 {
c.MaxCsvLimit = 2000
}
return sweepTimeLock(
extendedKey, cfg.APIURL, entries, c.SweepAddr, c.MaxCsvLimit,
c.Publish,
)
}
func sweepTimeLock(extendedKey *hdkeychain.ExtendedKey, apiURL string,
entries []*dataformat.SummaryEntry, sweepAddr string, maxCsvTimeout int,
publish bool) 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)
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 commit point: %v", err)
}
delayDesc := fc.DelayBasePoint.Desc()
delayPrivKey, err := signer.FetchPrivKey(delayDesc)
if err != nil {
return fmt.Errorf("error getting private key: %v", err)
}
delayBase := delayPrivKey.PubKey()
// 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(delayBase, commitPoint),
input.DeriveRevocationPubkey(revBase, commitPoint),
fc.Outs[txindex].Script, maxCsvTimeout,
)
if err != nil {
log.Errorf("Could not create matching script for %s "+
"or csv too high: %v", entry.ChannelPoint,
err)
continue
}
// Create the transaction input.
txHash, err := chainhash.NewHashFromStr(fc.TXID)
if err != nil {
return fmt.Errorf("error parsing tx hash: %v", err)
}
sweepTx.TxIn = append(sweepTx.TxIn, &wire.TxIn{
PreviousOutPoint: wire.OutPoint{
Hash: *txHash,
Index: uint32(txindex),
},
Sequence: input.LockTimeToSequence(
false, uint32(csvTimeout),
),
})
// Create the sign descriptor for the input.
signDesc := &input.SignDescriptor{
KeyDesc: *delayDesc,
SingleTweak: input.SingleTweakBytes(
commitPoint, delayBase,
),
WitnessScript: script,
Output: &wire.TxOut{
PkScript: scriptHash,
Value: int64(fc.Outs[txindex].Value),
},
HashType: txscript.SigHashAll,
}
totalOutputValue += int64(fc.Outs[txindex].Value)
signDescs = append(signDescs, signDesc)
}
// Add our sweep destination output.
sweepScript, err := getWP2PKHScript(sweepAddr)
if err != nil {
return err
}
sweepTx.TxOut = []*wire.TxOut{{
Value: totalOutputValue,
PkScript: sweepScript,
}}
// Very naive fee estimation algorithm: Sign a first time as if we would
// send the whole amount with zero fee, just to estimate how big the
// transaction would get in bytes. Then adjust the fee and sign again.
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
}
// Calculate a fee. This won't be very accurate so the feeSatPerByte
// should at least be 2 to not risk falling below the 1 sat/byte limit.
size := sweepTx.SerializeSize()
fee := int64(size * feeSatPerByte)
sweepTx.TxOut[0].Value = totalOutputValue - fee
// Sign again after output fixing.
sigHashes = txscript.NewTxSigHashes(sweepTx)
for idx, desc := range signDescs {
desc.SigHashes = sigHashes
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
}
log.Infof("Fee %d sats of %d total amount (for size %d)",
fee, totalOutputValue, sweepTx.SerializeSize())
// 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 getWP2PKHScript(addr string) ([]byte, error) {
targetPubKeyHash, err := parseAddr(addr)
if err != nil {
return nil, err
}
builder := txscript.NewScriptBuilder()
builder.AddOp(txscript.OP_0)
builder.AddData(targetPubKeyHash)
return builder.Script()
}
func bruteForceDelay(delayPubkey, revocationPubkey *btcec.PublicKey,
targetScriptHex string, maxCsvTimeout int) (int32, []byte, []byte,
error) {
targetScript, err := hex.DecodeString(targetScriptHex)
if err != nil {
return 0, nil, nil, fmt.Errorf("error parsing target script: "+
"%v", err)
}
if len(targetScript) != 34 {
return 0, nil, nil, fmt.Errorf("invalid target script: %s",
targetScriptHex)
}
for i := 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", targetScriptHex)
}