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()) }