Skip to content

Commit af35668

Browse files
committed
zombierecovery: add new commands for zombie channel recovery
1 parent 7a3c9a3 commit af35668

29 files changed

+1381
-25
lines changed

README.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ compacting the DB).
185185
don't have a `channel.db` file or because `chantools` couldn't rescue all your
186186
node's channels. There are a few things you can try manually that have some
187187
chance of working:
188-
- Make sure you can connect to all nodes when restoring from SCB: It happens
188+
- Make sure you can connect to all nodes when restoring from SCB: It happens
189189
all the time that nodes change their IP addresses. When restoring from a
190190
static channel backup, your node tries to connect to the node using the IP
191191
address encoded in the backup file. If the address changed, the SCB restore
@@ -194,13 +194,20 @@ compacting the DB).
194194
`lncli connect <node-pubkey>@<updated-ip-address>:<port>` in the recovered
195195
`lnd` node from step 3 and wait a few hours to see if the channel is now
196196
being force closed by the remote node.
197-
- Find out who the node belongs to: Maybe you opened the channel with someone
197+
- Find out who the node belongs to: Maybe you opened the channel with someone
198198
you know. Or maybe their node alias contains some information about who the
199199
node belongs to. If you can find out who operates the remote node, you can
200200
ask them to force-close the channel from their end. If the channel was opened
201201
with the `option_static_remote_key`, (`lnd v0.8.0` and later), the funds can
202202
be swept by your node.
203203

204+
12. **Use Zombie Channel Recovery Matcher**: As a final, last resort, you can
205+
go to [node-recovery.com](https://www.node-recovery.com/) and register your
206+
node's ID for being matched up against other nodes with the same problem.
207+
<br/><br/>
208+
Once you were contacted with a match, follow the instructions on the
209+
[Zombie Channel Recovery Guide](doc/zombierecovery.md) page.
210+
204211
## Seed and passphrase input
205212

206213
All commands that require the seed (and, if set, the seed's passphrase) offer

btc/explorer_api.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"errors"
77
"fmt"
88
"net/http"
9+
"strconv"
910
"strings"
1011
)
1112

@@ -89,6 +90,30 @@ func (a *ExplorerAPI) Outpoint(addr string) (*TX, int, error) {
8990
return nil, 0, fmt.Errorf("no tx found")
9091
}
9192

93+
func (a *ExplorerAPI) Address(outpoint string) (string, error) {
94+
parts := strings.Split(outpoint, ":")
95+
96+
if len(parts) != 2 {
97+
return "", fmt.Errorf("invalid outpoint: %v", outpoint)
98+
}
99+
100+
tx, err := a.Transaction(parts[0])
101+
if err != nil {
102+
return "", err
103+
}
104+
105+
vout, err := strconv.Atoi(parts[1])
106+
if err != nil {
107+
return "", err
108+
}
109+
110+
if len(tx.Vout) <= vout {
111+
return "", fmt.Errorf("invalid output index: %d", vout)
112+
}
113+
114+
return tx.Vout[vout].ScriptPubkeyAddr, nil
115+
}
116+
92117
func (a *ExplorerAPI) PublishTx(rawTxHex string) (string, error) {
93118
url := fmt.Sprintf("%s/tx", a.BaseURL)
94119
resp, err := http.Post(url, "text/plain", strings.NewReader(rawTxHex))

btc/summary.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ func reportOutspend(api *ExplorerAPI,
102102
entry.ClosingTX.ToRemoteAddr = o.ScriptPubkeyAddr
103103
}
104104
}
105-
105+
106106
if couldBeOurs(entry, utxo) {
107107
summaryFile.ChannelsWithPotential++
108108
summaryFile.FundsForceClose += utxo[0].Value

cmd/chantools/dropchannelgraph.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,6 @@ func (c *dropChannelGraphCommand) Execute(_ *cobra.Command, _ []string) error {
6767
if err := rwTx.DeleteTopLevelBucket(graphMetaBucket); err != nil {
6868
return err
6969
}
70-
70+
7171
return rwTx.Commit()
7272
}

cmd/chantools/fakechanbackup.go

Lines changed: 6 additions & 6 deletions
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/tor"
87
"io/ioutil"
98
"net"
109
"strconv"
@@ -21,6 +20,7 @@ import (
2120
"github.com/lightningnetwork/lnd/keychain"
2221
"github.com/lightningnetwork/lnd/lnrpc"
2322
"github.com/lightningnetwork/lnd/lnwire"
23+
"github.com/lightningnetwork/lnd/tor"
2424
"github.com/spf13/cobra"
2525
)
2626

@@ -166,21 +166,21 @@ func (c *fakeChanBackupCommand) Execute(_ *cobra.Command, _ []string) error {
166166
}
167167

168168
// Parse the short channel ID.
169-
splitChanId := strings.Split(c.ShortChanID, "x")
170-
if len(splitChanId) != 3 {
169+
splitChanID := strings.Split(c.ShortChanID, "x")
170+
if len(splitChanID) != 3 {
171171
return fmt.Errorf("--short_channel_id expected in format: " +
172172
"<blockheight>x<transactionindex>x<outputindex>",
173173
)
174174
}
175-
blockHeight, err := strconv.ParseInt(splitChanId[0], 10, 32)
175+
blockHeight, err := strconv.ParseInt(splitChanID[0], 10, 32)
176176
if err != nil {
177177
return fmt.Errorf("could not parse block height: %s", err)
178178
}
179-
txIndex, err := strconv.ParseInt(splitChanId[1], 10, 32)
179+
txIndex, err := strconv.ParseInt(splitChanID[1], 10, 32)
180180
if err != nil {
181181
return fmt.Errorf("could not parse transaction index: %s", err)
182182
}
183-
chanOutputIdx, err := strconv.ParseInt(splitChanId[2], 10, 32)
183+
chanOutputIdx, err := strconv.ParseInt(splitChanID[2], 10, 32)
184184
if err != nil {
185185
return fmt.Errorf("could not parse output index: %s", err)
186186
}

cmd/chantools/rescueclosed.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ func commitPointsFromLogFile(lndLog string) ([]*btcec.PublicKey, error) {
225225
dedupMap[groups[1]] = commitPoint
226226
}
227227

228-
var result []*btcec.PublicKey
228+
result := make([]*btcec.PublicKey, 0, len(dedupMap))
229229
for _, commitPoint := range dedupMap {
230230
result = append(result, commitPoint)
231231
}

cmd/chantools/root.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import (
2626

2727
const (
2828
defaultAPIURL = "https://blockstream.info/api"
29-
version = "0.8.3"
29+
version = "0.8.4"
3030
na = "n/a"
3131

3232
Commit = ""
@@ -102,6 +102,7 @@ func main() {
102102
newSweepTimeLockManualCommand(),
103103
newVanityGenCommand(),
104104
newWalletInfoCommand(),
105+
newZombieRecoveryCommand(),
105106
)
106107

107108
if err := rootCmd.Execute(); err != nil {

cmd/chantools/sweeptimelock.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import (
1919
)
2020

2121
const (
22-
defaultFeeSatPerVByte = 2
22+
defaultFeeSatPerVByte = 30
2323
defaultCsvLimit = 2016
2424
)
2525

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
package main
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"io/ioutil"
7+
"regexp"
8+
"time"
9+
10+
"github.com/btcsuite/btcd/btcec"
11+
"github.com/gogo/protobuf/jsonpb"
12+
"github.com/guggero/chantools/btc"
13+
"github.com/guggero/chantools/lnd"
14+
"github.com/lightningnetwork/lnd/lnrpc"
15+
"github.com/spf13/cobra"
16+
)
17+
18+
var (
19+
patternRegistration = regexp.MustCompile(
20+
"(?m)(?s)ID: ([0-9a-f]{66})\nContact: (.*?)\n" +
21+
"Time: ")
22+
)
23+
24+
type nodeInfo struct {
25+
PubKey string `json:"identity_pubkey"`
26+
Contact string `json:"contact"`
27+
PayoutAddr string `json:"payout_addr,omitempty"`
28+
MultisigKeys []string `json:"multisig_keys,omitempty"`
29+
}
30+
31+
type channel struct {
32+
ChannelID string `json:"short_channel_id"`
33+
ChanPoint string `json:"chan_point"`
34+
Address string `json:"address"`
35+
Capacity int64 `json:"capacity"`
36+
txid string
37+
vout uint32
38+
ourKeyIndex uint32
39+
ourKey *btcec.PublicKey
40+
theirKey *btcec.PublicKey
41+
witnessScript []byte
42+
}
43+
44+
type match struct {
45+
Node1 *nodeInfo `json:"node1"`
46+
Node2 *nodeInfo `json:"node2"`
47+
Channels []*channel `json:"channels"`
48+
}
49+
50+
type zombieRecoveryFindMatchesCommand struct {
51+
APIURL string
52+
Registrations string
53+
ChannelGraph string
54+
55+
cmd *cobra.Command
56+
}
57+
58+
func newZombieRecoveryFindMatchesCommand() *cobra.Command {
59+
cc := &zombieRecoveryFindMatchesCommand{}
60+
cc.cmd = &cobra.Command{
61+
Use: "findmatches",
62+
Short: "[0/3] Match maker only: Find matches between " +
63+
"registered nodes",
64+
Long: `Match maker only: Runs through all the nodes that have
65+
registered their ID on https://www.node-recovery.com and checks whether there
66+
are any matches of channels between them by looking at the whole channel graph.
67+
68+
This command will be run by guggero and the result will be sent to the
69+
registered nodes.`,
70+
Example: `chantools zombierecovery findmatches \
71+
--registrations data.txt \
72+
--channel_graph lncli_describegraph.json`,
73+
RunE: cc.Execute,
74+
}
75+
76+
cc.cmd.Flags().StringVar(
77+
&cc.APIURL, "apiurl", defaultAPIURL, "API URL to use (must "+
78+
"be esplora compatible)",
79+
)
80+
cc.cmd.Flags().StringVar(
81+
&cc.Registrations, "registrations", "", "the raw data.txt "+
82+
"where the registrations are stored in",
83+
)
84+
cc.cmd.Flags().StringVar(
85+
&cc.ChannelGraph, "channel_graph", "", "the full LN channel "+
86+
"graph in the JSON format that the "+
87+
"'lncli describegraph' returns",
88+
)
89+
90+
return cc.cmd
91+
}
92+
93+
func (c *zombieRecoveryFindMatchesCommand) Execute(_ *cobra.Command,
94+
_ []string) error {
95+
96+
api := &btc.ExplorerAPI{BaseURL: c.APIURL}
97+
98+
logFileBytes, err := ioutil.ReadFile(c.Registrations)
99+
if err != nil {
100+
return fmt.Errorf("error reading registrations file %s: %v",
101+
c.Registrations, err)
102+
}
103+
104+
allMatches := patternRegistration.FindAllStringSubmatch(
105+
string(logFileBytes), -1,
106+
)
107+
registrations := make(map[string]string, len(allMatches))
108+
for _, groups := range allMatches {
109+
if _, err := pubKeyFromHex(groups[1]); err != nil {
110+
return fmt.Errorf("error parsing node ID: %v", err)
111+
}
112+
113+
registrations[groups[1]] = groups[2]
114+
115+
log.Infof("%s: %s", groups[1], groups[2])
116+
}
117+
118+
graphBytes, err := ioutil.ReadFile(c.ChannelGraph)
119+
if err != nil {
120+
return fmt.Errorf("error reading graph JSON file %s: "+
121+
"%v", c.ChannelGraph, err)
122+
}
123+
graph := &lnrpc.ChannelGraph{}
124+
err = jsonpb.UnmarshalString(string(graphBytes), graph)
125+
if err != nil {
126+
return fmt.Errorf("error parsing graph JSON: %v", err)
127+
}
128+
129+
// Loop through all nodes now.
130+
matches := make(map[string]map[string]*match)
131+
for node1, contact1 := range registrations {
132+
matches[node1] = make(map[string]*match)
133+
for node2, contact2 := range registrations {
134+
if node1 == node2 {
135+
continue
136+
}
137+
138+
// We've already looked at this pair.
139+
if matches[node2][node1] != nil {
140+
continue
141+
}
142+
143+
edges := lnd.FindCommonEdges(graph, node1, node2)
144+
if len(edges) > 0 {
145+
matches[node1][node2] = &match{
146+
Node1: &nodeInfo{
147+
PubKey: node1,
148+
Contact: contact1,
149+
},
150+
Node2: &nodeInfo{
151+
PubKey: node2,
152+
Contact: contact2,
153+
},
154+
Channels: make([]*channel, len(edges)),
155+
}
156+
157+
for idx, edge := range edges {
158+
cid := fmt.Sprintf("%d", edge.ChannelId)
159+
c := &channel{
160+
ChannelID: cid,
161+
ChanPoint: edge.ChanPoint,
162+
Capacity: edge.Capacity,
163+
}
164+
165+
addr, err := api.Address(c.ChanPoint)
166+
if err == nil {
167+
c.Address = addr
168+
}
169+
170+
matches[node1][node2].Channels[idx] = c
171+
}
172+
}
173+
}
174+
}
175+
176+
// Write the matches to files.
177+
for node1, node1map := range matches {
178+
for node2, match := range node1map {
179+
if match == nil {
180+
continue
181+
}
182+
183+
matchBytes, err := json.MarshalIndent(match, "", " ")
184+
if err != nil {
185+
return err
186+
}
187+
188+
fileName := fmt.Sprintf("results/match-%s-%s-%s.json",
189+
time.Now().Format("2006-01-02"),
190+
node1, node2)
191+
log.Infof("Writing result to %s", fileName)
192+
err = ioutil.WriteFile(fileName, matchBytes, 0644)
193+
if err != nil {
194+
return err
195+
}
196+
}
197+
}
198+
199+
return nil
200+
}

0 commit comments

Comments
 (0)