You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
chantools/cmd/chantools/zombierecovery_findmatches.go

245 lines
6.0 KiB
Go

package main
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"regexp"
"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/spf13/cobra"
)
var (
patternRegistration = regexp.MustCompile(
"(?m)(?s)ID: ([0-9a-f]{66})\nContact: (.*?)\n" +
"Time: ")
)
type nodeInfo struct {
PubKey string `json:"identity_pubkey"`
Contact string `json:"contact"`
PayoutAddr string `json:"payout_addr,omitempty"`
MultisigKeys []string `json:"multisig_keys,omitempty"`
}
type channel struct {
ChannelID string `json:"short_channel_id"`
ChanPoint string `json:"chan_point"`
Address string `json:"address"`
Capacity int64 `json:"capacity"`
txid string
vout uint32
ourKeyIndex uint32
ourKey *btcec.PublicKey
theirKey *btcec.PublicKey
witnessScript []byte
}
type match struct {
Node1 *nodeInfo `json:"node1"`
Node2 *nodeInfo `json:"node2"`
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
cmd *cobra.Command
}
func newZombieRecoveryFindMatchesCommand() *cobra.Command {
cc := &zombieRecoveryFindMatchesCommand{}
cc.cmd = &cobra.Command{
Use: "findmatches",
Short: "[0/3] Match maker only: Find matches between " +
"registered nodes",
Long: `Match maker only: Runs through all the nodes that have
registered their ID on https://www.node-recovery.com and checks whether there
are any matches of channels between them by looking at the whole channel graph.
This command will be run by guggero and the result will be sent to the
registered nodes.`,
Example: `chantools zombierecovery findmatches \
--registrations data.txt \
--channel_graph lncli_describegraph.json \
--pairs_done pairs-done.json`,
RunE: cc.Execute,
}
cc.cmd.Flags().StringVar(
&cc.APIURL, "apiurl", defaultAPIURL, "API URL to use (must "+
"be esplora compatible)",
)
cc.cmd.Flags().StringVar(
&cc.Registrations, "registrations", "", "the raw data.txt "+
"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",
)
return cc.cmd
}
func (c *zombieRecoveryFindMatchesCommand) Execute(_ *cobra.Command,
_ []string) error {
api := &btc.ExplorerAPI{BaseURL: c.APIURL}
logFileBytes, err := ioutil.ReadFile(c.Registrations)
if err != nil {
return fmt.Errorf("error reading registrations file %s: %v",
c.Registrations, err)
}
allMatches := patternRegistration.FindAllStringSubmatch(
string(logFileBytes), -1,
)
registrations := make(map[string]string, len(allMatches))
for _, groups := range allMatches {
if _, err := pubKeyFromHex(groups[1]); err != nil {
return fmt.Errorf("error parsing node ID: %v", err)
}
registrations[groups[1]] = groups[2]
log.Infof("%s: %s", 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: %v", err)
}
var donePairs []*donePair
if c.PairsDone != "" {
donePairsBytes, err := readInput(c.PairsDone)
if err != nil {
return fmt.Errorf("error reading pairs JSON %s: %v",
c.PairsDone, err)
}
decoder := json.NewDecoder(bytes.NewReader(donePairsBytes))
err = decoder.Decode(&donePairs)
if err != nil {
return fmt.Errorf("error parsing pairs JSON %s: %v",
c.PairsDone, err)
}
}
// Loop through all nodes now.
matches := make(map[string]map[string]*match)
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
}
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 idx, edge := range edges {
cid := fmt.Sprintf("%d", edge.ChannelId)
c := &channel{
ChannelID: cid,
ChanPoint: edge.ChanPoint,
Capacity: edge.Capacity,
}
addr, err := api.Address(c.ChanPoint)
if err == nil {
c.Address = addr
}
matches[node1][node2].Channels[idx] = c
}
}
}
}
// Write the matches to files.
for node1, node1map := range matches {
for node2, match := range node1map {
if match == nil || isPairDone(donePairs, node1, node2) {
continue
}
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)
log.Infof("Writing result to %s", fileName)
err = ioutil.WriteFile(fileName, matchBytes, 0644)
if err != nil {
return err
}
}
}
return nil
}
func isPairDone(donePairs []*donePair, node1, node2 string) bool {
for _, donePair := range donePairs {
if donePair.matches(node1, node2) {
return true
}
}
return false
}