Skip to content

Commit 2a19978

Browse files
authored
Merge pull request #70 from sputn1ck/rbfInputs
Add rbfinputs command
2 parents 22a4644 + aad46d3 commit 2a19978

19 files changed

+452
-30
lines changed

cmd/chantools/closepoolaccount.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ type closePoolAccountCommand struct {
4242
AuctioneerKey string
4343
Publish bool
4444
SweepAddr string
45-
FeeRate uint16
45+
FeeRate uint32
4646

4747
MinExpiry uint32
4848
MaxNumBlocks uint32
@@ -91,7 +91,7 @@ obtained by running 'pool accounts list' `,
9191
cc.cmd.Flags().StringVar(
9292
&cc.SweepAddr, "sweepaddr", "", "address to sweep the funds to",
9393
)
94-
cc.cmd.Flags().Uint16Var(
94+
cc.cmd.Flags().Uint32Var(
9595
&cc.FeeRate, "feerate", defaultFeeSatPerVByte, "fee rate to "+
9696
"use for the sweep transaction in sat/vByte",
9797
)
@@ -158,7 +158,7 @@ func (c *closePoolAccountCommand) Execute(_ *cobra.Command, _ []string) error {
158158

159159
func closePoolAccount(extendedKey *hdkeychain.ExtendedKey, apiURL string,
160160
outpoint *wire.OutPoint, auctioneerKey *btcec.PublicKey,
161-
sweepAddr string, publish bool, feeRate uint16, minExpiry,
161+
sweepAddr string, publish bool, feeRate uint32, minExpiry,
162162
maxNumBlocks, maxNumAccounts, maxNumBatchKeys uint32) error {
163163

164164
signer := &lnd.Signer{

cmd/chantools/doublespendinputs.go

Lines changed: 367 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,367 @@
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

Comments
 (0)