github.com/ethxdao/go-ethereum@v0.0.0-20221218102228-5ae34a9cc189/cmd/devp2p/nodesetcmd.go (about)

     1  // Copyright 2019 The go-ethereum Authors
     2  // This file is part of go-ethereum.
     3  //
     4  // go-ethereum is free software: you can redistribute it and/or modify
     5  // it under the terms of the GNU General Public License as published by
     6  // the Free Software Foundation, either version 3 of the License, or
     7  // (at your option) any later version.
     8  //
     9  // go-ethereum is distributed in the hope that it will be useful,
    10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    12  // GNU General Public License for more details.
    13  //
    14  // You should have received a copy of the GNU General Public License
    15  // along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
    16  
    17  package main
    18  
    19  import (
    20  	"errors"
    21  	"fmt"
    22  	"net"
    23  	"sort"
    24  	"strconv"
    25  	"strings"
    26  	"time"
    27  
    28  	"github.com/ethxdao/go-ethereum/core/forkid"
    29  	"github.com/ethxdao/go-ethereum/p2p/enr"
    30  	"github.com/ethxdao/go-ethereum/params"
    31  	"github.com/ethxdao/go-ethereum/rlp"
    32  )
    33  
    34  var (
    35  	nodesetCommand = &cli.Command{
    36  		Name:  "nodeset",
    37  		Usage: "Node set tools",
    38  		Subcommands: []*cli.Command{
    39  			nodesetInfoCommand,
    40  			nodesetFilterCommand,
    41  		},
    42  	}
    43  	nodesetInfoCommand = &cli.Command{
    44  		Name:      "info",
    45  		Usage:     "Shows statistics about a node set",
    46  		Action:    nodesetInfo,
    47  		ArgsUsage: "<nodes.json>",
    48  	}
    49  	nodesetFilterCommand = &cli.Command{
    50  		Name:      "filter",
    51  		Usage:     "Filters a node set",
    52  		Action:    nodesetFilter,
    53  		ArgsUsage: "<nodes.json> filters..",
    54  
    55  		SkipFlagParsing: true,
    56  	}
    57  )
    58  
    59  func nodesetInfo(ctx *cli.Context) error {
    60  	if ctx.NArg() < 1 {
    61  		return fmt.Errorf("need nodes file as argument")
    62  	}
    63  
    64  	ns := loadNodesJSON(ctx.Args().First())
    65  	fmt.Printf("Set contains %d nodes.\n", len(ns))
    66  	showAttributeCounts(ns)
    67  	return nil
    68  }
    69  
    70  // showAttributeCounts prints the distribution of ENR attributes in a node set.
    71  func showAttributeCounts(ns nodeSet) {
    72  	attrcount := make(map[string]int)
    73  	var attrlist []interface{}
    74  	for _, n := range ns {
    75  		r := n.N.Record()
    76  		attrlist = r.AppendElements(attrlist[:0])[1:]
    77  		for i := 0; i < len(attrlist); i += 2 {
    78  			key := attrlist[i].(string)
    79  			attrcount[key]++
    80  		}
    81  	}
    82  
    83  	var keys []string
    84  	var maxlength int
    85  	for key := range attrcount {
    86  		keys = append(keys, key)
    87  		if len(key) > maxlength {
    88  			maxlength = len(key)
    89  		}
    90  	}
    91  	sort.Strings(keys)
    92  	fmt.Println("ENR attribute counts:")
    93  	for _, key := range keys {
    94  		fmt.Printf("%s%s: %d\n", strings.Repeat(" ", maxlength-len(key)+1), key, attrcount[key])
    95  	}
    96  }
    97  
    98  func nodesetFilter(ctx *cli.Context) error {
    99  	if ctx.NArg() < 1 {
   100  		return fmt.Errorf("need nodes file as argument")
   101  	}
   102  	// Parse -limit.
   103  	limit, err := parseFilterLimit(ctx.Args().Tail())
   104  	if err != nil {
   105  		return err
   106  	}
   107  	// Parse the filters.
   108  	filter, err := andFilter(ctx.Args().Tail())
   109  	if err != nil {
   110  		return err
   111  	}
   112  
   113  	// Load nodes and apply filters.
   114  	ns := loadNodesJSON(ctx.Args().First())
   115  	result := make(nodeSet)
   116  	for id, n := range ns {
   117  		if filter(n) {
   118  			result[id] = n
   119  		}
   120  	}
   121  	if limit >= 0 {
   122  		result = result.topN(limit)
   123  	}
   124  	writeNodesJSON("-", result)
   125  	return nil
   126  }
   127  
   128  type nodeFilter func(nodeJSON) bool
   129  
   130  type nodeFilterC struct {
   131  	narg int
   132  	fn   func([]string) (nodeFilter, error)
   133  }
   134  
   135  var filterFlags = map[string]nodeFilterC{
   136  	"-limit":       {1, trueFilter}, // needed to skip over -limit
   137  	"-ip":          {1, ipFilter},
   138  	"-min-age":     {1, minAgeFilter},
   139  	"-eth-network": {1, ethFilter},
   140  	"-les-server":  {0, lesFilter},
   141  	"-snap":        {0, snapFilter},
   142  }
   143  
   144  // parseFilters parses nodeFilters from args.
   145  func parseFilters(args []string) ([]nodeFilter, error) {
   146  	var filters []nodeFilter
   147  	for len(args) > 0 {
   148  		fc, ok := filterFlags[args[0]]
   149  		if !ok {
   150  			return nil, fmt.Errorf("invalid filter %q", args[0])
   151  		}
   152  		if len(args)-1 < fc.narg {
   153  			return nil, fmt.Errorf("filter %q wants %d arguments, have %d", args[0], fc.narg, len(args)-1)
   154  		}
   155  		filter, err := fc.fn(args[1 : 1+fc.narg])
   156  		if err != nil {
   157  			return nil, fmt.Errorf("%s: %v", args[0], err)
   158  		}
   159  		filters = append(filters, filter)
   160  		args = args[1+fc.narg:]
   161  	}
   162  	return filters, nil
   163  }
   164  
   165  // parseFilterLimit parses the -limit option in args. It returns -1 if there is no limit.
   166  func parseFilterLimit(args []string) (int, error) {
   167  	limit := -1
   168  	for i, arg := range args {
   169  		if arg == "-limit" {
   170  			if i == len(args)-1 {
   171  				return -1, errors.New("-limit requires an argument")
   172  			}
   173  			n, err := strconv.Atoi(args[i+1])
   174  			if err != nil {
   175  				return -1, fmt.Errorf("invalid -limit %q", args[i+1])
   176  			}
   177  			limit = n
   178  		}
   179  	}
   180  	return limit, nil
   181  }
   182  
   183  // andFilter parses node filters in args and and returns a single filter that requires all
   184  // of them to match.
   185  func andFilter(args []string) (nodeFilter, error) {
   186  	checks, err := parseFilters(args)
   187  	if err != nil {
   188  		return nil, err
   189  	}
   190  	f := func(n nodeJSON) bool {
   191  		for _, filter := range checks {
   192  			if !filter(n) {
   193  				return false
   194  			}
   195  		}
   196  		return true
   197  	}
   198  	return f, nil
   199  }
   200  
   201  func trueFilter(args []string) (nodeFilter, error) {
   202  	return func(n nodeJSON) bool { return true }, nil
   203  }
   204  
   205  func ipFilter(args []string) (nodeFilter, error) {
   206  	_, cidr, err := net.ParseCIDR(args[0])
   207  	if err != nil {
   208  		return nil, err
   209  	}
   210  	f := func(n nodeJSON) bool { return cidr.Contains(n.N.IP()) }
   211  	return f, nil
   212  }
   213  
   214  func minAgeFilter(args []string) (nodeFilter, error) {
   215  	minage, err := time.ParseDuration(args[0])
   216  	if err != nil {
   217  		return nil, err
   218  	}
   219  	f := func(n nodeJSON) bool {
   220  		age := n.LastResponse.Sub(n.FirstResponse)
   221  		return age >= minage
   222  	}
   223  	return f, nil
   224  }
   225  
   226  func ethFilter(args []string) (nodeFilter, error) {
   227  	var filter forkid.Filter
   228  	switch args[0] {
   229  	case "mainnet":
   230  		filter = forkid.NewStaticFilter(params.MainnetChainConfig, params.MainnetGenesisHash)
   231  	case "rinkeby":
   232  		filter = forkid.NewStaticFilter(params.RinkebyChainConfig, params.RinkebyGenesisHash)
   233  	case "goerli":
   234  		filter = forkid.NewStaticFilter(params.GoerliChainConfig, params.GoerliGenesisHash)
   235  	case "ropsten":
   236  		filter = forkid.NewStaticFilter(params.RopstenChainConfig, params.RopstenGenesisHash)
   237  	case "sepolia":
   238  		filter = forkid.NewStaticFilter(params.SepoliaChainConfig, params.SepoliaGenesisHash)
   239  	default:
   240  		return nil, fmt.Errorf("unknown network %q", args[0])
   241  	}
   242  
   243  	f := func(n nodeJSON) bool {
   244  		var eth struct {
   245  			ForkID forkid.ID
   246  			Tail   []rlp.RawValue `rlp:"tail"`
   247  		}
   248  		if n.N.Load(enr.WithEntry("eth", &eth)) != nil {
   249  			return false
   250  		}
   251  		return filter(eth.ForkID) == nil
   252  	}
   253  	return f, nil
   254  }
   255  
   256  func lesFilter(args []string) (nodeFilter, error) {
   257  	f := func(n nodeJSON) bool {
   258  		var les struct {
   259  			Tail []rlp.RawValue `rlp:"tail"`
   260  		}
   261  		return n.N.Load(enr.WithEntry("les", &les)) == nil
   262  	}
   263  	return f, nil
   264  }
   265  
   266  func snapFilter(args []string) (nodeFilter, error) {
   267  	f := func(n nodeJSON) bool {
   268  		var snap struct {
   269  			Tail []rlp.RawValue `rlp:"tail"`
   270  		}
   271  		return n.N.Load(enr.WithEntry("snap", &snap)) == nil
   272  	}
   273  	return f, nil
   274  }