|
|
|
@ -1,27 +1,91 @@
|
|
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bytes"
|
|
|
|
|
"context"
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"fmt"
|
|
|
|
|
"io/ioutil"
|
|
|
|
|
"os"
|
|
|
|
|
"regexp"
|
|
|
|
|
"strconv"
|
|
|
|
|
"text/template"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"github.com/btcsuite/btcd/btcec/v2"
|
|
|
|
|
"github.com/gogo/protobuf/jsonpb"
|
|
|
|
|
"github.com/guggero/chantools/btc"
|
|
|
|
|
"github.com/guggero/chantools/lnd"
|
|
|
|
|
"github.com/lightningnetwork/lnd/lnrpc"
|
|
|
|
|
"github.com/hasura/go-graphql-client"
|
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
|
"golang.org/x/oauth2"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
var (
|
|
|
|
|
patternRegistration = regexp.MustCompile(
|
|
|
|
|
"(?m)(?s)ID: ([0-9a-f]{66})\nContact: (.*?)\n" +
|
|
|
|
|
"Time: ")
|
|
|
|
|
"(?m)(?s)ID: ([0-9a-f]{66})\nContact: (.*?)\nTime: ",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
ambossQueryDelay = 7 * time.Second
|
|
|
|
|
|
|
|
|
|
initialTemplate = `SEND TO: {{.Contact}}
|
|
|
|
|
|
|
|
|
|
Hi
|
|
|
|
|
|
|
|
|
|
This is Oliver from node-recovery.com.
|
|
|
|
|
You recently registered your node ({{.Node1}}) with my website.
|
|
|
|
|
|
|
|
|
|
I have some good news! I found
|
|
|
|
|
{{- if eq .NumChannels 1}} a match for a channel{{end}}
|
|
|
|
|
{{- if gt .NumChannels 1}} matches for {{.NumChannels}} channels{{end}}.
|
|
|
|
|
Attached you find the JSON files that contain all the info I have about your
|
|
|
|
|
node and the remote node (open with a text editor).
|
|
|
|
|
|
|
|
|
|
With those files you can close the channels and get your funds back. But you
|
|
|
|
|
need the cooperation of the remote peer. But because they also registered to the
|
|
|
|
|
same website, they should be aware of that and be willing to cooperate.
|
|
|
|
|
|
|
|
|
|
Please contact the remote peer with the contact information listed below (this
|
|
|
|
|
is what they registered with, I don't have additional contact information):
|
|
|
|
|
|
|
|
|
|
{{range $i, $peer := .Peers}}
|
|
|
|
|
Peer: {{$peer.PubKey}}
|
|
|
|
|
Contact: {{$peer.Contact}}
|
|
|
|
|
|
|
|
|
|
{{end}}
|
|
|
|
|
The document that describes what to do exactly is located here:
|
|
|
|
|
https://github.com/guggero/chantools/blob/master/doc/zombierecovery.md
|
|
|
|
|
|
|
|
|
|
Good luck!
|
|
|
|
|
|
|
|
|
|
Oliver (guggero)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
P.S.: If you don't want to be notified about future matches, please let me know.
|
|
|
|
|
`
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type gqChannel struct {
|
|
|
|
|
ChanPoint string `graphql:"chan_point"`
|
|
|
|
|
Capacity string `graphql:"capacity"`
|
|
|
|
|
ClosureInfo struct {
|
|
|
|
|
ClosedHeight uint32 `graphql:"closed_height"`
|
|
|
|
|
} `graphql:"closure_info"`
|
|
|
|
|
Node1 string `graphql:"node1_pub"`
|
|
|
|
|
Node2 string `graphql:"node2_pub"`
|
|
|
|
|
ChannelID string `graphql:"long_channel_id"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type gqGraphInfo struct {
|
|
|
|
|
Channels struct {
|
|
|
|
|
ChannelList struct {
|
|
|
|
|
List []*gqChannel `graphql:"list"`
|
|
|
|
|
} `graphql:"channel_list(page:{limit:$limit,offset:$offset})"`
|
|
|
|
|
} `graphql:"channels"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type gqGetNodeQuery struct {
|
|
|
|
|
GetNode struct {
|
|
|
|
|
GraphInfo *gqGraphInfo `graphql:"graph_info"`
|
|
|
|
|
} `graphql:"getNode(pubkey: $pubkey)"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type nodeInfo struct {
|
|
|
|
|
PubKey string `json:"identity_pubkey"`
|
|
|
|
|
Contact string `json:"contact"`
|
|
|
|
@ -48,22 +112,10 @@ type match struct {
|
|
|
|
|
Channels []*channel `json:"channels"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type donePair struct {
|
|
|
|
|
Node1 *nodeInfo `json:"node1"`
|
|
|
|
|
Node2 *nodeInfo `json:"node2"`
|
|
|
|
|
Msg string `json:"msg"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (p *donePair) matches(node1, node2 string) bool {
|
|
|
|
|
return (p.Node1.PubKey == node1 && p.Node2.PubKey == node2) ||
|
|
|
|
|
(p.Node1.PubKey == node2 && p.Node2.PubKey == node1)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type zombieRecoveryFindMatchesCommand struct {
|
|
|
|
|
APIURL string
|
|
|
|
|
Registrations string
|
|
|
|
|
ChannelGraph string
|
|
|
|
|
PairsDone string
|
|
|
|
|
AmbossKey string
|
|
|
|
|
|
|
|
|
|
cmd *cobra.Command
|
|
|
|
|
}
|
|
|
|
@ -96,14 +148,8 @@ registered nodes.`,
|
|
|
|
|
"where the registrations are stored in",
|
|
|
|
|
)
|
|
|
|
|
cc.cmd.Flags().StringVar(
|
|
|
|
|
&cc.ChannelGraph, "channel_graph", "", "the full LN channel "+
|
|
|
|
|
"graph in the JSON format that the "+
|
|
|
|
|
"'lncli describegraph' returns",
|
|
|
|
|
)
|
|
|
|
|
cc.cmd.Flags().StringVar(
|
|
|
|
|
&cc.PairsDone, "pairs_done", "", "an optional file containing "+
|
|
|
|
|
"all pairs that have already been contacted and "+
|
|
|
|
|
"shouldn't be matched again",
|
|
|
|
|
&cc.AmbossKey, "ambosskey", "", "the API key for the Amboss "+
|
|
|
|
|
"GraphQL API",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return cc.cmd
|
|
|
|
@ -112,9 +158,7 @@ registered nodes.`,
|
|
|
|
|
func (c *zombieRecoveryFindMatchesCommand) Execute(_ *cobra.Command,
|
|
|
|
|
_ []string) error {
|
|
|
|
|
|
|
|
|
|
api := &btc.ExplorerAPI{BaseURL: c.APIURL}
|
|
|
|
|
|
|
|
|
|
logFileBytes, err := ioutil.ReadFile(c.Registrations)
|
|
|
|
|
logFileBytes, err := os.ReadFile(c.Registrations)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("error reading registrations file %s: %w",
|
|
|
|
|
c.Registrations, err)
|
|
|
|
@ -129,116 +173,201 @@ func (c *zombieRecoveryFindMatchesCommand) Execute(_ *cobra.Command,
|
|
|
|
|
return fmt.Errorf("error parsing node ID: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
registrations[groups[1]] = groups[2]
|
|
|
|
|
if registrations[groups[1]] != "" {
|
|
|
|
|
registrations[groups[1]] += ", "
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
log.Infof("%s: %s", groups[1], groups[2])
|
|
|
|
|
}
|
|
|
|
|
registrations[groups[1]] += groups[2]
|
|
|
|
|
|
|
|
|
|
graphBytes, err := ioutil.ReadFile(c.ChannelGraph)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("error reading graph JSON file %s: "+
|
|
|
|
|
"%v", c.ChannelGraph, err)
|
|
|
|
|
}
|
|
|
|
|
graph := &lnrpc.ChannelGraph{}
|
|
|
|
|
err = jsonpb.UnmarshalString(string(graphBytes), graph)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("error parsing graph JSON: %w", err)
|
|
|
|
|
log.Infof("%s: %s", groups[1], groups[2])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var donePairs []*donePair
|
|
|
|
|
if c.PairsDone != "" {
|
|
|
|
|
donePairsBytes, err := readInput(c.PairsDone)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("error reading pairs JSON %s: %w",
|
|
|
|
|
c.PairsDone, err)
|
|
|
|
|
}
|
|
|
|
|
decoder := json.NewDecoder(bytes.NewReader(donePairsBytes))
|
|
|
|
|
err = decoder.Decode(&donePairs)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("error parsing pairs JSON %s: %w",
|
|
|
|
|
c.PairsDone, err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
api := &btc.ExplorerAPI{BaseURL: c.APIURL}
|
|
|
|
|
src := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: c.AmbossKey})
|
|
|
|
|
httpClient := oauth2.NewClient(context.Background(), src)
|
|
|
|
|
client := graphql.NewClient(
|
|
|
|
|
"https://api.amboss.space/graphql", httpClient,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Loop through all nodes now.
|
|
|
|
|
matches := make(map[string]map[string]*match)
|
|
|
|
|
idx := 0
|
|
|
|
|
for node1, contact1 := range registrations {
|
|
|
|
|
matches[node1] = make(map[string]*match)
|
|
|
|
|
for node2, contact2 := range registrations {
|
|
|
|
|
if node1 == node2 {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// We've already looked at this pair.
|
|
|
|
|
if matches[node2][node1] != nil {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
time.Sleep(ambossQueryDelay)
|
|
|
|
|
log.Debugf("Fetching channels for node %d of %d", idx,
|
|
|
|
|
len(registrations))
|
|
|
|
|
idx++
|
|
|
|
|
|
|
|
|
|
channels, err := fetchChannels(client, node1)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("error fetching channels for %s: %w",
|
|
|
|
|
node1, err)
|
|
|
|
|
}
|
|
|
|
|
for _, node1Chan := range channels {
|
|
|
|
|
peer := identifyPeer(node1Chan, node1)
|
|
|
|
|
|
|
|
|
|
edges := lnd.FindCommonEdges(graph, node1, node2)
|
|
|
|
|
if len(edges) > 0 {
|
|
|
|
|
matches[node1][node2] = &match{
|
|
|
|
|
Node1: &nodeInfo{
|
|
|
|
|
PubKey: node1,
|
|
|
|
|
Contact: contact1,
|
|
|
|
|
},
|
|
|
|
|
Node2: &nodeInfo{
|
|
|
|
|
PubKey: node2,
|
|
|
|
|
Contact: contact2,
|
|
|
|
|
},
|
|
|
|
|
Channels: make([]*channel, len(edges)),
|
|
|
|
|
for node2, contact2 := range registrations {
|
|
|
|
|
if node1 == node2 || node2 != peer {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for idx, edge := range edges {
|
|
|
|
|
cid := fmt.Sprintf("%d", edge.ChannelId)
|
|
|
|
|
c := &channel{
|
|
|
|
|
ChannelID: cid,
|
|
|
|
|
ChanPoint: edge.ChanPoint,
|
|
|
|
|
Capacity: edge.Capacity,
|
|
|
|
|
}
|
|
|
|
|
if matches[node2][node1] != nil {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
addr, err := api.Address(c.ChanPoint)
|
|
|
|
|
if err == nil {
|
|
|
|
|
c.Address = addr
|
|
|
|
|
log.Debugf("Node 1 (%s, %s) has channel with "+
|
|
|
|
|
"match (%s): %v", node1, contact1, peer,
|
|
|
|
|
node1Chan.ChannelID)
|
|
|
|
|
|
|
|
|
|
// This is a new match.
|
|
|
|
|
if matches[node1][node2] == nil {
|
|
|
|
|
matches[node1][node2] = &match{
|
|
|
|
|
Node1: &nodeInfo{
|
|
|
|
|
PubKey: node1,
|
|
|
|
|
Contact: contact1,
|
|
|
|
|
},
|
|
|
|
|
Node2: &nodeInfo{
|
|
|
|
|
PubKey: node2,
|
|
|
|
|
Contact: contact2,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
matches[node1][node2].Channels[idx] = c
|
|
|
|
|
// Find the address of the channel.
|
|
|
|
|
addr, err := api.Address(node1Chan.ChanPoint)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("error fetching "+
|
|
|
|
|
"address for channel %s: %w",
|
|
|
|
|
node1Chan.ChannelID, err)
|
|
|
|
|
}
|
|
|
|
|
capacity, err := strconv.ParseUint(
|
|
|
|
|
node1Chan.Capacity, 10, 64,
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("error parsing "+
|
|
|
|
|
"capacity for channel %s: %w",
|
|
|
|
|
node1Chan.ChannelID, err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// We've found a new match for this peer.
|
|
|
|
|
newChan := &channel{
|
|
|
|
|
ChannelID: node1Chan.ChannelID,
|
|
|
|
|
ChanPoint: node1Chan.ChanPoint,
|
|
|
|
|
Address: addr,
|
|
|
|
|
Capacity: int64(capacity),
|
|
|
|
|
}
|
|
|
|
|
matches[node1][node2].Channels = append(
|
|
|
|
|
matches[node1][node2].Channels,
|
|
|
|
|
newChan,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Write the matches to files.
|
|
|
|
|
for node1, node1map := range matches {
|
|
|
|
|
for node2, match := range node1map {
|
|
|
|
|
if match == nil || isPairDone(donePairs, node1, node2) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
tpl, err := template.New("initial").Parse(initialTemplate)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("error parsing template: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tplVars := struct {
|
|
|
|
|
Contact string
|
|
|
|
|
Node1 string
|
|
|
|
|
NumChannels int
|
|
|
|
|
Peers []*nodeInfo
|
|
|
|
|
}{
|
|
|
|
|
Contact: registrations[node1],
|
|
|
|
|
Node1: node1,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
folder := fmt.Sprintf("results/match-%s", node1)
|
|
|
|
|
err = os.MkdirAll(folder, 0755)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for node2, match := range node1map {
|
|
|
|
|
matchBytes, err := json.MarshalIndent(match, "", " ")
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fileName := fmt.Sprintf("results/match-%s-%s-%s.json",
|
|
|
|
|
time.Now().Format("2006-01-02"),
|
|
|
|
|
node1, node2)
|
|
|
|
|
fileName := fmt.Sprintf("%s/%s-%s.json",
|
|
|
|
|
folder, node2, time.Now().Format("2006-01-02"))
|
|
|
|
|
log.Infof("Writing result to %s", fileName)
|
|
|
|
|
err = ioutil.WriteFile(fileName, matchBytes, 0644)
|
|
|
|
|
err = os.WriteFile(fileName, matchBytes, 0644)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tplVars.NumChannels += len(match.Channels)
|
|
|
|
|
tplVars.Peers = append(tplVars.Peers, match.Node2)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if tplVars.NumChannels == 0 {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
textFileName := fmt.Sprintf("%s/message.txt", folder)
|
|
|
|
|
file, err := os.OpenFile(
|
|
|
|
|
textFileName, os.O_RDWR|os.O_CREATE, 0644,
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("error opening file %s: %w",
|
|
|
|
|
textFileName, err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
err = tpl.Execute(file, tplVars)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("error executing template: %w", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func isPairDone(donePairs []*donePair, node1, node2 string) bool {
|
|
|
|
|
for _, donePair := range donePairs {
|
|
|
|
|
if donePair.matches(node1, node2) {
|
|
|
|
|
return true
|
|
|
|
|
func fetchChannels(client *graphql.Client, pubkey string) ([]*gqChannel,
|
|
|
|
|
error) {
|
|
|
|
|
|
|
|
|
|
variables := map[string]interface{}{
|
|
|
|
|
"pubkey": pubkey,
|
|
|
|
|
"limit": 50.0,
|
|
|
|
|
"offset": 0.0,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var channels []*gqChannel
|
|
|
|
|
for {
|
|
|
|
|
var query gqGetNodeQuery
|
|
|
|
|
err := client.Query(context.Background(), &query, variables)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(query.GetNode.GraphInfo.Channels.ChannelList.List) == 0 {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
channels = append(
|
|
|
|
|
channels,
|
|
|
|
|
query.GetNode.GraphInfo.Channels.ChannelList.List...,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
variables["offset"] = variables["offset"].(float64) + 50.0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return channels, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func identifyPeer(channel *gqChannel, node1 string) string {
|
|
|
|
|
if channel.Node1 == node1 {
|
|
|
|
|
return channel.Node2
|
|
|
|
|
}
|
|
|
|
|
if channel.Node2 == node1 {
|
|
|
|
|
return channel.Node1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false
|
|
|
|
|
panic("peer not found")
|
|
|
|
|
}
|
|
|
|
|