|
| 1 | +package main |
| 2 | + |
| 3 | +import ( |
| 4 | + "bytes" |
| 5 | + "encoding/hex" |
| 6 | + "fmt" |
| 7 | + "strconv" |
| 8 | + |
| 9 | + "github.com/btcsuite/btcd/btcec/v2/schnorr" |
| 10 | + "github.com/btcsuite/btcd/btcutil" |
| 11 | + "github.com/btcsuite/btcd/btcutil/hdkeychain" |
| 12 | + "github.com/btcsuite/btcd/chaincfg/chainhash" |
| 13 | + "github.com/btcsuite/btcd/txscript" |
| 14 | + "github.com/btcsuite/btcd/wire" |
| 15 | + "github.com/decred/dcrd/dcrec/secp256k1/v4" |
| 16 | + "github.com/guggero/chantools/btc" |
| 17 | + "github.com/guggero/chantools/lnd" |
| 18 | + "github.com/lightningnetwork/lnd/input" |
| 19 | + "github.com/lightningnetwork/lnd/lnwallet/chainfee" |
| 20 | + "github.com/spf13/cobra" |
| 21 | +) |
| 22 | + |
| 23 | +type doubleSpendInputs struct { |
| 24 | + APIURL string |
| 25 | + InputOutpoints []string |
| 26 | + Publish bool |
| 27 | + SweepAddr string |
| 28 | + FeeRate uint32 |
| 29 | + RecoveryWindow uint32 |
| 30 | + |
| 31 | + rootKey *rootKey |
| 32 | + cmd *cobra.Command |
| 33 | +} |
| 34 | + |
| 35 | +func newDoubleSpendInputsCommand() *cobra.Command { |
| 36 | + cc := &doubleSpendInputs{} |
| 37 | + cc.cmd = &cobra.Command{ |
| 38 | + Use: "doublespendinputs", |
| 39 | + Short: "Tries to double spend the given inputs by deriving the " + |
| 40 | + "private for the address and sweeping the funds to the given " + |
| 41 | + "address. This can only be used with inputs that belong to " + |
| 42 | + "an lnd wallet.", |
| 43 | + Example: `chantools doublespendinputs \ |
| 44 | + --inputoutpoints xxxxxxxxx:y,xxxxxxxxx:y \ |
| 45 | + --sweepaddr bc1q..... \ |
| 46 | + --feerate 10 \ |
| 47 | + --publish`, |
| 48 | + RunE: cc.Execute, |
| 49 | + } |
| 50 | + cc.cmd.Flags().StringVar( |
| 51 | + &cc.APIURL, "apiurl", defaultAPIURL, "API URL to use (must "+ |
| 52 | + "be esplora compatible)", |
| 53 | + ) |
| 54 | + cc.cmd.Flags().StringSliceVar( |
| 55 | + &cc.InputOutpoints, "inputoutpoints", []string{}, |
| 56 | + "list of outpoints to double spend in the format txid:vout", |
| 57 | + ) |
| 58 | + cc.cmd.Flags().StringVar( |
| 59 | + &cc.SweepAddr, "sweepaddr", "", "address to sweep the funds to", |
| 60 | + ) |
| 61 | + cc.cmd.Flags().Uint32Var( |
| 62 | + &cc.FeeRate, "feerate", defaultFeeSatPerVByte, "fee rate to "+ |
| 63 | + "use for the sweep transaction in sat/vByte", |
| 64 | + ) |
| 65 | + cc.cmd.Flags().Uint32Var( |
| 66 | + &cc.RecoveryWindow, "recoverywindow", defaultRecoveryWindow, |
| 67 | + "number of keys to scan per internal/external branch; output "+ |
| 68 | + "will consist of double this amount of keys", |
| 69 | + ) |
| 70 | + cc.cmd.Flags().BoolVar( |
| 71 | + &cc.Publish, "publish", false, "publish replacement TX to "+ |
| 72 | + "the chain API instead of just printing the TX", |
| 73 | + ) |
| 74 | + |
| 75 | + cc.rootKey = newRootKey(cc.cmd, "deriving the input keys") |
| 76 | + |
| 77 | + return cc.cmd |
| 78 | +} |
| 79 | + |
| 80 | +func (c *doubleSpendInputs) Execute(_ *cobra.Command, _ []string) error { |
| 81 | + extendedKey, err := c.rootKey.read() |
| 82 | + if err != nil { |
| 83 | + return fmt.Errorf("error reading root key: %w", err) |
| 84 | + } |
| 85 | + |
| 86 | + // Make sure sweep addr is set. |
| 87 | + if c.SweepAddr == "" { |
| 88 | + return fmt.Errorf("sweep addr is required") |
| 89 | + } |
| 90 | + |
| 91 | + // Make sure we have at least one input. |
| 92 | + if len(c.InputOutpoints) == 0 { |
| 93 | + return fmt.Errorf("inputoutpoints are required") |
| 94 | + } |
| 95 | + |
| 96 | + api := &btc.ExplorerAPI{BaseURL: c.APIURL} |
| 97 | + |
| 98 | + addresses := make([]btcutil.Address, 0, len(c.InputOutpoints)) |
| 99 | + outpoints := make([]*wire.OutPoint, 0, len(c.InputOutpoints)) |
| 100 | + privKeys := make([]*secp256k1.PrivateKey, 0, len(c.InputOutpoints)) |
| 101 | + |
| 102 | + // Get the addresses for the inputs. |
| 103 | + for _, input := range c.InputOutpoints { |
| 104 | + addrString, err := api.Address(input) |
| 105 | + if err != nil { |
| 106 | + return err |
| 107 | + } |
| 108 | + |
| 109 | + addr, err := btcutil.DecodeAddress(addrString, chainParams) |
| 110 | + if err != nil { |
| 111 | + return err |
| 112 | + } |
| 113 | + |
| 114 | + addresses = append(addresses, addr) |
| 115 | + |
| 116 | + txHash, err := chainhash.NewHashFromStr(input[:64]) |
| 117 | + if err != nil { |
| 118 | + return err |
| 119 | + } |
| 120 | + |
| 121 | + vout, err := strconv.Atoi(input[65:]) |
| 122 | + if err != nil { |
| 123 | + return err |
| 124 | + } |
| 125 | + outpoint := wire.NewOutPoint(txHash, uint32(vout)) |
| 126 | + |
| 127 | + outpoints = append(outpoints, outpoint) |
| 128 | + } |
| 129 | + |
| 130 | + // Create the paths for the addresses. |
| 131 | + p2wkhPath, err := lnd.ParsePath(lnd.WalletDefaultDerivationPath) |
| 132 | + if err != nil { |
| 133 | + return err |
| 134 | + } |
| 135 | + |
| 136 | + p2trPath, err := lnd.ParsePath(lnd.WalletBIP86DerivationPath) |
| 137 | + if err != nil { |
| 138 | + return err |
| 139 | + } |
| 140 | + |
| 141 | + // Start with the txweight estimator. |
| 142 | + estimator := input.TxWeightEstimator{} |
| 143 | + |
| 144 | + // Find the key for the given addresses and add their |
| 145 | + // output weight to the tx estimator. |
| 146 | + for _, addr := range addresses { |
| 147 | + var key *hdkeychain.ExtendedKey |
| 148 | + switch addr.(type) { |
| 149 | + case *btcutil.AddressWitnessPubKeyHash: |
| 150 | + key, err = iterateOverPath( |
| 151 | + extendedKey, addr, p2wkhPath, c.RecoveryWindow, |
| 152 | + ) |
| 153 | + if err != nil { |
| 154 | + return err |
| 155 | + } |
| 156 | + |
| 157 | + estimator.AddP2WKHInput() |
| 158 | + |
| 159 | + case *btcutil.AddressTaproot: |
| 160 | + key, err = iterateOverPath( |
| 161 | + extendedKey, addr, p2trPath, c.RecoveryWindow, |
| 162 | + ) |
| 163 | + if err != nil { |
| 164 | + return err |
| 165 | + } |
| 166 | + |
| 167 | + estimator.AddTaprootKeySpendInput(txscript.SigHashDefault) |
| 168 | + |
| 169 | + default: |
| 170 | + return fmt.Errorf("address type %T not supported", addr) |
| 171 | + } |
| 172 | + |
| 173 | + // Get the private key. |
| 174 | + privKey, err := key.ECPrivKey() |
| 175 | + if err != nil { |
| 176 | + return err |
| 177 | + } |
| 178 | + |
| 179 | + privKeys = append(privKeys, privKey) |
| 180 | + } |
| 181 | + |
| 182 | + // Now that we have the keys, we can create the transaction. |
| 183 | + prevOuts := make(map[wire.OutPoint]*wire.TxOut) |
| 184 | + |
| 185 | + // Next get the full value of the inputs. |
| 186 | + var totalInput btcutil.Amount |
| 187 | + for _, input := range outpoints { |
| 188 | + // Get the transaction. |
| 189 | + tx, err := api.Transaction(input.Hash.String()) |
| 190 | + if err != nil { |
| 191 | + return err |
| 192 | + } |
| 193 | + |
| 194 | + value := tx.Vout[input.Index].Value |
| 195 | + |
| 196 | + // Get the output index. |
| 197 | + totalInput += btcutil.Amount(value) |
| 198 | + |
| 199 | + scriptPubkey, err := hex.DecodeString(tx.Vout[input.Index].ScriptPubkey) |
| 200 | + if err != nil { |
| 201 | + return err |
| 202 | + } |
| 203 | + |
| 204 | + // Add the output to the map. |
| 205 | + prevOuts[*input] = &wire.TxOut{ |
| 206 | + Value: int64(value), |
| 207 | + PkScript: scriptPubkey, |
| 208 | + } |
| 209 | + } |
| 210 | + |
| 211 | + // Calculate the fee. |
| 212 | + sweepAddr, err := btcutil.DecodeAddress(c.SweepAddr, chainParams) |
| 213 | + if err != nil { |
| 214 | + return err |
| 215 | + } |
| 216 | + |
| 217 | + switch sweepAddr.(type) { |
| 218 | + case *btcutil.AddressWitnessPubKeyHash: |
| 219 | + estimator.AddP2WKHOutput() |
| 220 | + |
| 221 | + case *btcutil.AddressTaproot: |
| 222 | + estimator.AddP2TROutput() |
| 223 | + |
| 224 | + default: |
| 225 | + return fmt.Errorf("address type %T not supported", sweepAddr) |
| 226 | + } |
| 227 | + |
| 228 | + // Calculate the fee. |
| 229 | + feeRateKWeight := chainfee.SatPerKVByte(1000 * c.FeeRate).FeePerKWeight() |
| 230 | + totalFee := feeRateKWeight.FeeForWeight(int64(estimator.Weight())) |
| 231 | + |
| 232 | + // Create the transaction. |
| 233 | + tx := wire.NewMsgTx(2) |
| 234 | + |
| 235 | + // Add the inputs. |
| 236 | + for _, input := range outpoints { |
| 237 | + tx.AddTxIn(wire.NewTxIn(input, nil, nil)) |
| 238 | + } |
| 239 | + |
| 240 | + // Add the output. |
| 241 | + sweepScript, err := txscript.PayToAddrScript(sweepAddr) |
| 242 | + if err != nil { |
| 243 | + return err |
| 244 | + } |
| 245 | + |
| 246 | + tx.AddTxOut(wire.NewTxOut(int64(totalInput-totalFee), sweepScript)) |
| 247 | + |
| 248 | + // Calculate the signature hash. |
| 249 | + prevOutFetcher := txscript.NewMultiPrevOutFetcher(prevOuts) |
| 250 | + sigHashes := txscript.NewTxSigHashes(tx, prevOutFetcher) |
| 251 | + |
| 252 | + // Sign the inputs depending on the address type. |
| 253 | + for i, outpoint := range outpoints { |
| 254 | + switch addresses[i].(type) { |
| 255 | + case *btcutil.AddressWitnessPubKeyHash: |
| 256 | + witness, err := txscript.WitnessSignature( |
| 257 | + tx, sigHashes, i, prevOuts[*outpoint].Value, |
| 258 | + prevOuts[*outpoint].PkScript, |
| 259 | + txscript.SigHashAll, privKeys[i], true, |
| 260 | + ) |
| 261 | + if err != nil { |
| 262 | + return err |
| 263 | + } |
| 264 | + |
| 265 | + tx.TxIn[i].Witness = witness |
| 266 | + |
| 267 | + case *btcutil.AddressTaproot: |
| 268 | + rawTxSig, err := txscript.RawTxInTaprootSignature( |
| 269 | + tx, sigHashes, i, |
| 270 | + prevOuts[*outpoint].Value, |
| 271 | + prevOuts[*outpoint].PkScript, |
| 272 | + []byte{}, txscript.SigHashDefault, privKeys[i], |
| 273 | + ) |
| 274 | + if err != nil { |
| 275 | + return err |
| 276 | + } |
| 277 | + |
| 278 | + tx.TxIn[i].Witness = wire.TxWitness{ |
| 279 | + rawTxSig, |
| 280 | + } |
| 281 | + |
| 282 | + default: |
| 283 | + return fmt.Errorf("address type %T not supported", addresses[i]) |
| 284 | + } |
| 285 | + } |
| 286 | + |
| 287 | + // Serialize the transaction. |
| 288 | + var txBuf bytes.Buffer |
| 289 | + if err := tx.Serialize(&txBuf); err != nil { |
| 290 | + return err |
| 291 | + } |
| 292 | + |
| 293 | + // Print the transaction. |
| 294 | + fmt.Printf("Sweeping transaction:\n%s\n", hex.EncodeToString(txBuf.Bytes())) |
| 295 | + |
| 296 | + // Publish the transaction. |
| 297 | + if c.Publish { |
| 298 | + txid, err := api.PublishTx(hex.EncodeToString(txBuf.Bytes())) |
| 299 | + if err != nil { |
| 300 | + return err |
| 301 | + } |
| 302 | + |
| 303 | + fmt.Printf("Published transaction with txid %s\n", txid) |
| 304 | + } |
| 305 | + |
| 306 | + return nil |
| 307 | +} |
| 308 | + |
| 309 | +// iterateOverPath iterates over the given key path and tries to find the |
| 310 | +// private key that corresponds to the given address. |
| 311 | +func iterateOverPath(baseKey *hdkeychain.ExtendedKey, addr btcutil.Address, |
| 312 | + path []uint32, maxTries uint32) (*hdkeychain.ExtendedKey, error) { |
| 313 | + |
| 314 | + for i := uint32(0); i < maxTries; i++ { |
| 315 | + // Check for both the external and internal branch. |
| 316 | + for _, branch := range []uint32{0, 1} { |
| 317 | + // Create the path to derive the key. |
| 318 | + addrPath := append(path, branch, i) //nolint:gocritic |
| 319 | + |
| 320 | + // Derive the key. |
| 321 | + derivedKey, err := lnd.DeriveChildren(baseKey, addrPath) |
| 322 | + if err != nil { |
| 323 | + return nil, err |
| 324 | + } |
| 325 | + |
| 326 | + var address btcutil.Address |
| 327 | + switch addr.(type) { |
| 328 | + case *btcutil.AddressWitnessPubKeyHash: |
| 329 | + // Get the address for the derived key. |
| 330 | + derivedAddr, err := derivedKey.Address(chainParams) |
| 331 | + if err != nil { |
| 332 | + return nil, err |
| 333 | + } |
| 334 | + |
| 335 | + address, err = btcutil.NewAddressWitnessPubKeyHash( |
| 336 | + derivedAddr.ScriptAddress(), chainParams, |
| 337 | + ) |
| 338 | + if err != nil { |
| 339 | + return nil, err |
| 340 | + } |
| 341 | + |
| 342 | + case *btcutil.AddressTaproot: |
| 343 | + |
| 344 | + pubkey, err := derivedKey.ECPubKey() |
| 345 | + if err != nil { |
| 346 | + return nil, err |
| 347 | + } |
| 348 | + |
| 349 | + pubkey = txscript.ComputeTaprootKeyNoScript(pubkey) |
| 350 | + |
| 351 | + address, err = btcutil.NewAddressTaproot( |
| 352 | + schnorr.SerializePubKey(pubkey), chainParams, |
| 353 | + ) |
| 354 | + if err != nil { |
| 355 | + return nil, err |
| 356 | + } |
| 357 | + } |
| 358 | + |
| 359 | + // Compare the addresses. |
| 360 | + if address.String() == addr.String() { |
| 361 | + return derivedKey, nil |
| 362 | + } |
| 363 | + } |
| 364 | + } |
| 365 | + |
| 366 | + return nil, fmt.Errorf("could not find key for address %s", addr.String()) |
| 367 | +} |
0 commit comments