Skip to content

Commit 338c22f

Browse files
committed
Begin implementing rescuefunding command
1 parent 4c92de5 commit 338c22f

File tree

9 files changed

+255
-56
lines changed

9 files changed

+255
-56
lines changed

README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
+ [genimportscript](#genimportscript)
1616
+ [forceclose](#forceclose)
1717
+ [rescueclosed](#rescueclosed)
18+
+ [rescuefunding](#rescuefunding)
1819
+ [showrootkey](#showrootkey)
1920
+ [summary](#summary)
2021
+ [sweeptimelock](#sweeptimelock)
@@ -69,6 +70,7 @@ Available commands:
6970
forceclose Force-close the last state that is in the channel.db provided.
7071
genimportscript Generate a script containing the on-chain keys of an lnd wallet that can be imported into other software like bitcoind.
7172
rescueclosed Try finding the private keys for funds that are in outputs of remotely force-closed channels.
73+
rescuefunding Rescue funds locked in a funding multisig output that never resulted in a proper channel.
7274
showrootkey Extract and show the BIP32 HD root key from the 24 word lnd aezeed.
7375
summary Compile a summary about the current state of channels.
7476
sweeptimelock Sweep the force-closed state after the time lock has expired.
@@ -330,6 +332,24 @@ chantools --fromsummary results/summary-xxxx-yyyy.json \
330332
--rootkey xprvxxxxxxxxxx
331333
```
332334

335+
### rescuefunding
336+
337+
```text
338+
Usage:
339+
chantools [OPTIONS] rescuefunding [rescuefunding-OPTIONS]
340+
341+
[rescuefunding command options]
342+
--rootkey= BIP32 HD root (m/) key to derive the key for our node from.
343+
--othernodepub= The extended public key (xpub) of the other node's multisig branch (m/1017'/<coin_type>'/0'/0).
344+
--fundingaddr= The bech32 script address of the funding output where the coins to be spent are locked in.
345+
--fundingoutpoint= The funding transaction outpoint (<txid>:<txindex>).
346+
--fundingamount= The exact amount in satoshis that is locked in the funding output.
347+
--sweepaddr= The address to sweep the rescued funds to.
348+
--satperbyte= The fee rate to use in satoshis/vByte.
349+
```
350+
351+
**This command is not fully implemented yet and only listed here as a placeholder.**
352+
333353
### showrootkey
334354

335355
This command converts the 24 word `lnd` aezeed phrase and password to the BIP32

cmd/chantools/derivekey.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,20 @@ func deriveKey(extendedKey *hdkeychain.ExtendedKey, path string,
4040
neuter bool) error {
4141

4242
fmt.Printf("Deriving path %s for network %s.\n", path, chainParams.Name)
43-
pubKey, wif, err := lnd.DeriveKey(extendedKey, path, chainParams)
43+
child, pubKey, wif, err := lnd.DeriveKey(extendedKey, path, chainParams)
4444
if err != nil {
4545
return fmt.Errorf("could not derive keys: %v", err)
4646
}
47+
neutered, err := child.Neuter()
48+
if err != nil {
49+
return fmt.Errorf("could not neuter child key: %v", err)
50+
}
4751
fmt.Printf("Public key: %x\n", pubKey.SerializeCompressed())
52+
fmt.Printf("Extended public key (xpub): %s\n", neutered.String())
4853

4954
if !neuter {
5055
fmt.Printf("Private key (WIF): %s\n", wif.String())
56+
fmt.Printf("Extended private key (xprv): %s\n", child.String())
5157
}
5258

5359
return nil

cmd/chantools/genimportscript.go

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -65,16 +65,6 @@ func (c *genImportScriptCommand) Execute(_ []string) error {
6565

6666
// Decide what derivation path(s) to use.
6767
switch {
68-
case c.LndPaths && c.DerivationPath != "":
69-
return fmt.Errorf("cannot use --lndpaths and --derivationpath " +
70-
"at the same time")
71-
72-
case c.LndPaths:
73-
strPaths, paths, err = lnd.AllDerivationPaths(chainParams)
74-
if err != nil {
75-
return fmt.Errorf("error getting lnd paths: %v", err)
76-
}
77-
7868
default:
7969
c.DerivationPath = lnd.WalletDefaultDerivationPath
8070
fallthrough
@@ -86,6 +76,16 @@ func (c *genImportScriptCommand) Execute(_ []string) error {
8676
}
8777
strPaths = []string{c.DerivationPath}
8878
paths = [][]uint32{derivationPath}
79+
80+
case c.LndPaths && c.DerivationPath != "":
81+
return fmt.Errorf("cannot use --lndpaths and --derivationpath " +
82+
"at the same time")
83+
84+
case c.LndPaths:
85+
strPaths, paths, err = lnd.AllDerivationPaths(chainParams)
86+
if err != nil {
87+
return fmt.Errorf("error getting lnd paths: %v", err)
88+
}
8989
}
9090

9191
exporter := btc.ParseFormat(c.Format)

cmd/chantools/main.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,11 @@ func runCommandParser() error {
128128
"compacting it in the process.", "",
129129
&compactDBCommand{},
130130
)
131+
_, _ = parser.AddCommand(
132+
"rescuefunding", "Rescue funds locked in a funding multisig "+
133+
"output that never resulted in a proper channel.", "",
134+
&rescueFundingCommand{},
135+
)
131136

132137
_, err := parser.Parse()
133138
return err
@@ -221,6 +226,7 @@ func rootKeyFromConsole() (*hdkeychain.ExtendedKey, time.Time, error) {
221226
if err != nil {
222227
return nil, time.Unix(0, 0), err
223228
}
229+
fmt.Println()
224230

225231
var mnemonic aezeed.Mnemonic
226232
copy(mnemonic[:], cipherSeedMnemonic)

cmd/chantools/rescueclosed.go

Lines changed: 6 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -154,10 +154,15 @@ func rescueClosedChannels(extendedKey *hdkeychain.ExtendedKey,
154154
}
155155

156156
func addrInCache(addr string, perCommitPoint *btcec.PublicKey) (string, error) {
157-
targetPubKeyHash, err := parseAddr(addr)
157+
targetPubKeyHash, scriptHash, err := lnd.DecodeAddressHash(
158+
addr, chainParams,
159+
)
158160
if err != nil {
159161
return "", fmt.Errorf("error parsing addr: %v", err)
160162
}
163+
if scriptHash {
164+
return "", fmt.Errorf("address must be a P2WPKH address")
165+
}
161166

162167
// Loop through all cached payment base point keys, tweak each of it
163168
// with the per_commit_point and see if the hashed public key
@@ -229,30 +234,3 @@ func fillCache(extendedKey *hdkeychain.ExtendedKey) error {
229234
}
230235
return nil
231236
}
232-
233-
func parseAddr(addr string) ([]byte, error) {
234-
// First parse address to get targetPubKeyHash from it later.
235-
targetAddr, err := btcutil.DecodeAddress(addr, chainParams)
236-
if err != nil {
237-
return nil, err
238-
}
239-
240-
var targetPubKeyHash []byte
241-
// Make the check on the decoded address according to the active
242-
// network (testnet or mainnet only).
243-
if !targetAddr.IsForNet(chainParams) {
244-
return nil, fmt.Errorf(
245-
"address: %v is not valid for this network: %v",
246-
targetAddr.String(), chainParams.Name,
247-
)
248-
}
249-
250-
// Must be a bech32 native SegWit address.
251-
switch targetAddr.(type) {
252-
case *btcutil.AddressWitnessPubKeyHash:
253-
targetPubKeyHash = targetAddr.ScriptAddress()
254-
default:
255-
return nil, fmt.Errorf("address: must be a bech32 P2WPKH address")
256-
}
257-
return targetPubKeyHash, nil
258-
}

cmd/chantools/rescuefunding.go

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
7+
"github.com/btcsuite/btcutil/hdkeychain"
8+
"github.com/guggero/chantools/lnd"
9+
"github.com/lightningnetwork/lnd/input"
10+
"github.com/lightningnetwork/lnd/keychain"
11+
)
12+
13+
const (
14+
MaxChannelLookup = 5000
15+
)
16+
17+
type rescueFundingCommand struct {
18+
RootKey string `long:"rootkey" description:"BIP32 HD root (m/) key to derive the key for our node from."`
19+
OtherNodePub string `long:"othernodepub" description:"The extended public key (xpub) of the other node's multisig branch (m/1017'/<coin_type>'/0'/0)."`
20+
FundingAddr string `long:"fundingaddr" description:"The bech32 script address of the funding output where the coins to be spent are locked in."`
21+
FundingOutpoint string `long:"fundingoutpoint" description:"The funding transaction outpoint (<txid>:<txindex>)."`
22+
FundingAmount int64 `long:"fundingamount" description:"The exact amount in satoshis that is locked in the funding output."`
23+
SweepAddr string `long:"sweepaddr" description:"The address to sweep the rescued funds to."`
24+
SatPerByte int64 `long:"satperbyte" description:"The fee rate to use in satoshis/vByte."`
25+
}
26+
27+
func (c *rescueFundingCommand) Execute(_ []string) error {
28+
setupChainParams(cfg)
29+
30+
var (
31+
extendedKey *hdkeychain.ExtendedKey
32+
otherPub *hdkeychain.ExtendedKey
33+
err error
34+
)
35+
36+
// Check that root key is valid or fall back to console input.
37+
switch {
38+
case c.RootKey != "":
39+
extendedKey, err = hdkeychain.NewKeyFromString(c.RootKey)
40+
41+
default:
42+
extendedKey, _, err = rootKeyFromConsole()
43+
}
44+
if err != nil {
45+
return fmt.Errorf("error reading root key: %v", err)
46+
}
47+
48+
// Read other node's xpub.
49+
otherPub, err = hdkeychain.NewKeyFromString(c.OtherNodePub)
50+
if err != nil {
51+
return fmt.Errorf("error parsing other node's xpub: %v", err)
52+
}
53+
54+
// Decode target funding address.
55+
hash, isScript, err := lnd.DecodeAddressHash(c.FundingAddr, chainParams)
56+
if err != nil {
57+
return fmt.Errorf("error decoding funding address: %v", err)
58+
}
59+
if !isScript {
60+
return fmt.Errorf("funding address must be a P2WSH address")
61+
}
62+
63+
return rescueFunding(extendedKey, otherPub, hash)
64+
}
65+
66+
func rescueFunding(localNodeKey *hdkeychain.ExtendedKey,
67+
otherNodekey *hdkeychain.ExtendedKey, scriptHash []byte) error {
68+
69+
// First, we need to derive the correct branch from the local root key.
70+
localMultisig, err := lnd.DeriveChildren(localNodeKey, []uint32{
71+
lnd.HardenedKeyStart + uint32(keychain.BIP0043Purpose),
72+
lnd.HardenedKeyStart + chainParams.HDCoinType,
73+
lnd.HardenedKeyStart + uint32(keychain.KeyFamilyMultiSig),
74+
0,
75+
})
76+
if err != nil {
77+
return fmt.Errorf("could not derive local multisig key: %v",
78+
err)
79+
}
80+
81+
log.Infof("Looking for matching multisig keys, this will take a while")
82+
localIndex, otherIndex, script, err := findMatchingIndices(
83+
localMultisig, otherNodekey, scriptHash,
84+
)
85+
if err != nil {
86+
return fmt.Errorf("could not derive keys: %v", err)
87+
}
88+
89+
log.Infof("Found local key with index %d and other key with index %d "+
90+
"for witness script %x", localIndex, otherIndex, script)
91+
92+
// TODO(guggero):
93+
// * craft PSBT with input, sweep output and partial signature
94+
// * do fee estimation based on full amount
95+
// * create `signpsbt` command for the other node operator
96+
return nil
97+
}
98+
99+
func findMatchingIndices(localNodeKey *hdkeychain.ExtendedKey,
100+
otherNodekey *hdkeychain.ExtendedKey, scriptHash []byte) (uint32,
101+
uint32, []byte, error) {
102+
103+
// Loop through both the local and the remote indices of the branches up
104+
// to MaxChannelLookup.
105+
for local := uint32(0); local < MaxChannelLookup; local++ {
106+
for other := uint32(0); other < MaxChannelLookup; other++ {
107+
localKey, err := localNodeKey.Child(local)
108+
if err != nil {
109+
return 0, 0, nil, fmt.Errorf("error "+
110+
"deriving local key: %v", err)
111+
}
112+
localPub, err := localKey.ECPubKey()
113+
if err != nil {
114+
return 0, 0, nil, fmt.Errorf("error "+
115+
"deriving local pubkey: %v", err)
116+
}
117+
otherKey, err := otherNodekey.Child(other)
118+
if err != nil {
119+
return 0, 0, nil, fmt.Errorf("error "+
120+
"deriving other key: %v", err)
121+
}
122+
otherPub, err := otherKey.ECPubKey()
123+
if err != nil {
124+
return 0, 0, nil, fmt.Errorf("error "+
125+
"deriving other pubkey: %v", err)
126+
}
127+
script, out, err := input.GenFundingPkScript(
128+
localPub.SerializeCompressed(),
129+
otherPub.SerializeCompressed(), 123,
130+
)
131+
if err != nil {
132+
return 0, 0, nil, fmt.Errorf("error "+
133+
"generating funding script: %v", err)
134+
}
135+
if bytes.Contains(out.PkScript, scriptHash) {
136+
return local, other, script, nil
137+
}
138+
}
139+
if local > 0 && local%100 == 0 {
140+
log.Infof("Checked %d of %d local keys", local,
141+
MaxChannelLookup)
142+
}
143+
}
144+
return 0, 0, nil, fmt.Errorf("no matching pubkeys found")
145+
}

cmd/chantools/sweeptimelock.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ func sweepTimeLock(extendedKey *hdkeychain.ExtendedKey, apiURL string,
182182
}
183183

184184
// Add our sweep destination output.
185-
sweepScript, err := getWP2PKHScript(sweepAddr)
185+
sweepScript, err := getP2WPKHScript(sweepAddr)
186186
if err != nil {
187187
return err
188188
}
@@ -256,8 +256,10 @@ func pubKeyFromHex(pubKeyHex string) (*btcec.PublicKey, error) {
256256
)
257257
}
258258

259-
func getWP2PKHScript(addr string) ([]byte, error) {
260-
targetPubKeyHash, err := parseAddr(addr)
259+
func getP2WPKHScript(addr string) ([]byte, error) {
260+
targetPubKeyHash, _, err := lnd.DecodeAddressHash(
261+
addr, chainParams,
262+
)
261263
if err != nil {
262264
return nil, err
263265
}

dump/dump.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"bytes"
55
"encoding/hex"
66
"fmt"
7-
"github.com/lightningnetwork/lnd/input"
87
"net"
98

109
"github.com/btcsuite/btcd/btcec"
@@ -13,6 +12,7 @@ import (
1312
"github.com/btcsuite/btcutil"
1413
"github.com/lightningnetwork/lnd/chanbackup"
1514
"github.com/lightningnetwork/lnd/channeldb"
15+
"github.com/lightningnetwork/lnd/input"
1616
"github.com/lightningnetwork/lnd/keychain"
1717
"github.com/lightningnetwork/lnd/lnwire"
1818
)

0 commit comments

Comments
 (0)