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

365 lines
8.9 KiB
Go

package main
import (
"bytes"
"encoding/hex"
"fmt"
"strconv"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/btcutil/hdkeychain"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/mempool"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/lightninglabs/chantools/lnd"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
"github.com/spf13/cobra"
)
type doubleSpendInputs struct {
APIURL string
InputOutpoints []string
Publish bool
SweepAddr string
FeeRate uint32
RecoveryWindow uint32
rootKey *rootKey
cmd *cobra.Command
}
func newDoubleSpendInputsCommand() *cobra.Command {
cc := &doubleSpendInputs{}
cc.cmd = &cobra.Command{
Use: "doublespendinputs",
Short: "Replace a transaction by double spending its input",
Long: `Tries to double spend the given inputs by deriving the
private for the address and sweeping the funds to the given address. This can
only be used with inputs that belong to an lnd wallet.`,
Example: `chantools doublespendinputs \
--inputoutpoints xxxxxxxxx:y,xxxxxxxxx:y \
--sweepaddr bc1q..... \
--feerate 10 \
--publish`,
RunE: cc.Execute,
}
cc.cmd.Flags().StringVar(
&cc.APIURL, "apiurl", defaultAPIURL, "API URL to use (must "+
"be esplora compatible)",
)
cc.cmd.Flags().StringSliceVar(
&cc.InputOutpoints, "inputoutpoints", []string{},
"list of outpoints to double spend in the format txid:vout",
)
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.cmd.Flags().Uint32Var(
&cc.RecoveryWindow, "recoverywindow", defaultRecoveryWindow,
"number of keys to scan per internal/external branch; output "+
"will consist of double this amount of keys",
)
cc.cmd.Flags().BoolVar(
&cc.Publish, "publish", false, "publish replacement TX to "+
"the chain API instead of just printing the TX",
)
cc.rootKey = newRootKey(cc.cmd, "deriving the input keys")
return cc.cmd
}
func (c *doubleSpendInputs) 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
}
// Make sure we have at least one input.
if len(c.InputOutpoints) == 0 {
return fmt.Errorf("inputoutpoints are required")
}
api := newExplorerAPI(c.APIURL)
addresses := make([]btcutil.Address, 0, len(c.InputOutpoints))
outpoints := make([]*wire.OutPoint, 0, len(c.InputOutpoints))
privKeys := make([]*secp256k1.PrivateKey, 0, len(c.InputOutpoints))
// Get the addresses for the inputs.
for _, inputOutpoint := range c.InputOutpoints {
addrString, err := api.Address(inputOutpoint)
if err != nil {
return err
}
addr, err := btcutil.DecodeAddress(addrString, chainParams)
if err != nil {
return err
}
addresses = append(addresses, addr)
txHash, err := chainhash.NewHashFromStr(inputOutpoint[:64])
if err != nil {
return err
}
vout, err := strconv.Atoi(inputOutpoint[65:])
if err != nil {
return err
}
outpoint := wire.NewOutPoint(txHash, uint32(vout))
outpoints = append(outpoints, outpoint)
}
// Create the paths for the addresses.
p2wkhPath, err := lnd.ParsePath(lnd.WalletDefaultDerivationPath)
if err != nil {
return err
}
p2trPath, err := lnd.ParsePath(lnd.WalletBIP86DerivationPath)
if err != nil {
return err
}
// Start with the txweight estimator.
var estimator input.TxWeightEstimator
sweepScript, err := lnd.PrepareWalletAddress(
c.SweepAddr, chainParams, &estimator, extendedKey, "sweep",
)
if err != nil {
return err
}
// Find the key for the given addresses and add their
// output weight to the tx estimator.
for _, addr := range addresses {
var key *hdkeychain.ExtendedKey
switch addr.(type) {
case *btcutil.AddressWitnessPubKeyHash:
key, err = iterateOverPath(
extendedKey, addr, p2wkhPath, c.RecoveryWindow,
)
if err != nil {
return err
}
estimator.AddP2WKHInput()
case *btcutil.AddressTaproot:
key, err = iterateOverPath(
extendedKey, addr, p2trPath, c.RecoveryWindow,
)
if err != nil {
return err
}
estimator.AddTaprootKeySpendInput(
txscript.SigHashDefault,
)
default:
return fmt.Errorf("address type %T not supported", addr)
}
// Get the private key.
privKey, err := key.ECPrivKey()
if err != nil {
return err
}
privKeys = append(privKeys, privKey)
}
// Now that we have the keys, we can create the transaction.
prevOuts := make(map[wire.OutPoint]*wire.TxOut)
// Next get the full value of the inputs.
var totalInput btcutil.Amount
for _, outpoint := range outpoints {
// Get the transaction.
tx, err := api.Transaction(outpoint.Hash.String())
if err != nil {
return err
}
value := tx.Vout[outpoint.Index].Value
// Get the output index.
totalInput += btcutil.Amount(value)
scriptPubkey, err := hex.DecodeString(
tx.Vout[outpoint.Index].ScriptPubkey,
)
if err != nil {
return err
}
// Add the output to the map.
prevOuts[*outpoint] = &wire.TxOut{
Value: int64(value),
PkScript: scriptPubkey,
}
}
// Calculate the fee.
feeRateKWeight := chainfee.SatPerKVByte(1000 * c.FeeRate).FeePerKWeight()
totalFee := feeRateKWeight.FeeForWeight(int64(estimator.Weight()))
// Create the transaction.
tx := wire.NewMsgTx(2)
// Add the inputs.
for _, outpoint := range outpoints {
tx.AddTxIn(&wire.TxIn{
PreviousOutPoint: *outpoint,
Sequence: mempool.MaxRBFSequence,
})
}
tx.AddTxOut(wire.NewTxOut(int64(totalInput-totalFee), sweepScript))
// Calculate the signature hash.
prevOutFetcher := txscript.NewMultiPrevOutFetcher(prevOuts)
sigHashes := txscript.NewTxSigHashes(tx, prevOutFetcher)
// Sign the inputs depending on the address type.
for i, outpoint := range outpoints {
switch addresses[i].(type) {
case *btcutil.AddressWitnessPubKeyHash:
witness, err := txscript.WitnessSignature(
tx, sigHashes, i, prevOuts[*outpoint].Value,
prevOuts[*outpoint].PkScript,
txscript.SigHashAll, privKeys[i], true,
)
if err != nil {
return err
}
tx.TxIn[i].Witness = witness
case *btcutil.AddressTaproot:
rawTxSig, err := txscript.RawTxInTaprootSignature(
tx, sigHashes, i,
prevOuts[*outpoint].Value,
prevOuts[*outpoint].PkScript,
[]byte{}, txscript.SigHashDefault, privKeys[i],
)
if err != nil {
return err
}
tx.TxIn[i].Witness = wire.TxWitness{
rawTxSig,
}
default:
return fmt.Errorf("address type %T not supported",
addresses[i])
}
}
// Serialize the transaction.
var txBuf bytes.Buffer
if err := tx.Serialize(&txBuf); err != nil {
return err
}
// Print the transaction.
fmt.Printf("Sweeping transaction:\n%x\n", txBuf.Bytes())
// Publish the transaction.
if c.Publish {
txid, err := api.PublishTx(hex.EncodeToString(txBuf.Bytes()))
if err != nil {
return err
}
fmt.Printf("Published transaction with txid %s\n", txid)
}
return nil
}
// iterateOverPath iterates over the given key path and tries to find the
// private key that corresponds to the given address.
func iterateOverPath(baseKey *hdkeychain.ExtendedKey, addr btcutil.Address,
path []uint32, maxTries uint32) (*hdkeychain.ExtendedKey, error) {
for i := uint32(0); i < maxTries; i++ {
// Check for both the external and internal branch.
for _, branch := range []uint32{0, 1} {
// Create the path to derive the key.
addrPath := append(path, branch, i) //nolint:gocritic
// Derive the key.
derivedKey, err := lnd.DeriveChildren(baseKey, addrPath)
if err != nil {
return nil, err
}
var address btcutil.Address
switch addr.(type) {
case *btcutil.AddressWitnessPubKeyHash:
// Get the address for the derived key.
derivedAddr, err := derivedKey.Address(chainParams)
if err != nil {
return nil, err
}
address, err = btcutil.NewAddressWitnessPubKeyHash(
derivedAddr.ScriptAddress(), chainParams,
)
if err != nil {
return nil, err
}
case *btcutil.AddressTaproot:
pubkey, err := derivedKey.ECPubKey()
if err != nil {
return nil, err
}
pubkey = txscript.ComputeTaprootKeyNoScript(pubkey)
address, err = btcutil.NewAddressTaproot(
schnorr.SerializePubKey(pubkey), chainParams,
)
if err != nil {
return nil, err
}
}
// Compare the addresses.
if address.String() == addr.String() {
return derivedKey, nil
}
}
}
return nil, fmt.Errorf("could not find key for address %s", addr.String())
}