cmd/devp2p: add support for -limit option in nodeset filter command (#22694)
The new -limit option makes the filter operate on top N nodes by score. This also adds ENR attribute stats in the nodeset info command. Node set commands are now documented in README.
This commit is contained in:
parent
e43ac53264
commit
424656519a
@ -30,6 +30,29 @@ Run `devp2p dns to-route53 <directory>` to publish a tree to Amazon Route53.
|
|||||||
|
|
||||||
You can find more information about these commands in the [DNS Discovery Setup Guide][dns-tutorial].
|
You can find more information about these commands in the [DNS Discovery Setup Guide][dns-tutorial].
|
||||||
|
|
||||||
|
### Node Set Utilities
|
||||||
|
|
||||||
|
There are several commands for working with JSON node set files. These files are generated
|
||||||
|
by the discovery crawlers and DNS client commands. Node sets also used as the input of the
|
||||||
|
DNS deployer commands.
|
||||||
|
|
||||||
|
Run `devp2p nodeset info <nodes.json>` to display statistics of a node set.
|
||||||
|
|
||||||
|
Run `devp2p nodeset filter <nodes.json> <filter flags...>` to write a new, filtered node
|
||||||
|
set to standard output. The following filters are supported:
|
||||||
|
|
||||||
|
- `-limit <N>` limits the output set to N entries, taking the top N nodes by score
|
||||||
|
- `-ip <CIDR>` filters nodes by IP subnet
|
||||||
|
- `-min-age <duration>` filters nodes by 'first seen' time
|
||||||
|
- `-eth-network <mainnet/rinkeby/goerli/ropsten>` filters nodes by "eth" ENR entry
|
||||||
|
- `-les-server` filters nodes by LES server support
|
||||||
|
- `-snap` filters nodes by snap protocol support
|
||||||
|
|
||||||
|
For example, given a node set in `nodes.json`, you could create a filtered set containing
|
||||||
|
up to 20 eth mainnet nodes which also support snap sync using this command:
|
||||||
|
|
||||||
|
devp2p nodeset filter nodes.json -eth-network mainnet -snap -limit 20
|
||||||
|
|
||||||
### Discovery v4 Utilities
|
### Discovery v4 Utilities
|
||||||
|
|
||||||
The `devp2p discv4 ...` command family deals with the [Node Discovery v4][discv4]
|
The `devp2p discv4 ...` command family deals with the [Node Discovery v4][discv4]
|
||||||
|
@ -71,6 +71,7 @@ func writeNodesJSON(file string, nodes nodeSet) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// nodes returns the node records contained in the set.
|
||||||
func (ns nodeSet) nodes() []*enode.Node {
|
func (ns nodeSet) nodes() []*enode.Node {
|
||||||
result := make([]*enode.Node, 0, len(ns))
|
result := make([]*enode.Node, 0, len(ns))
|
||||||
for _, n := range ns {
|
for _, n := range ns {
|
||||||
@ -83,12 +84,37 @@ func (ns nodeSet) nodes() []*enode.Node {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// add ensures the given nodes are present in the set.
|
||||||
func (ns nodeSet) add(nodes ...*enode.Node) {
|
func (ns nodeSet) add(nodes ...*enode.Node) {
|
||||||
for _, n := range nodes {
|
for _, n := range nodes {
|
||||||
ns[n.ID()] = nodeJSON{Seq: n.Seq(), N: n}
|
v := ns[n.ID()]
|
||||||
|
v.N = n
|
||||||
|
v.Seq = n.Seq()
|
||||||
|
ns[n.ID()] = v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// topN returns the top n nodes by score as a new set.
|
||||||
|
func (ns nodeSet) topN(n int) nodeSet {
|
||||||
|
if n >= len(ns) {
|
||||||
|
return ns
|
||||||
|
}
|
||||||
|
|
||||||
|
byscore := make([]nodeJSON, 0, len(ns))
|
||||||
|
for _, v := range ns {
|
||||||
|
byscore = append(byscore, v)
|
||||||
|
}
|
||||||
|
sort.Slice(byscore, func(i, j int) bool {
|
||||||
|
return byscore[i].Score >= byscore[j].Score
|
||||||
|
})
|
||||||
|
result := make(nodeSet, n)
|
||||||
|
for _, v := range byscore[:n] {
|
||||||
|
result[v.N.ID()] = v
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// verify performs integrity checks on the node set.
|
||||||
func (ns nodeSet) verify() error {
|
func (ns nodeSet) verify() error {
|
||||||
for id, n := range ns {
|
for id, n := range ns {
|
||||||
if n.N.ID() != id {
|
if n.N.ID() != id {
|
||||||
|
@ -17,8 +17,12 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/core/forkid"
|
"github.com/ethereum/go-ethereum/core/forkid"
|
||||||
@ -60,25 +64,64 @@ func nodesetInfo(ctx *cli.Context) error {
|
|||||||
|
|
||||||
ns := loadNodesJSON(ctx.Args().First())
|
ns := loadNodesJSON(ctx.Args().First())
|
||||||
fmt.Printf("Set contains %d nodes.\n", len(ns))
|
fmt.Printf("Set contains %d nodes.\n", len(ns))
|
||||||
|
showAttributeCounts(ns)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// showAttributeCounts prints the distribution of ENR attributes in a node set.
|
||||||
|
func showAttributeCounts(ns nodeSet) {
|
||||||
|
attrcount := make(map[string]int)
|
||||||
|
var attrlist []interface{}
|
||||||
|
for _, n := range ns {
|
||||||
|
r := n.N.Record()
|
||||||
|
attrlist = r.AppendElements(attrlist[:0])[1:]
|
||||||
|
for i := 0; i < len(attrlist); i += 2 {
|
||||||
|
key := attrlist[i].(string)
|
||||||
|
attrcount[key]++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var keys []string
|
||||||
|
var maxlength int
|
||||||
|
for key := range attrcount {
|
||||||
|
keys = append(keys, key)
|
||||||
|
if len(key) > maxlength {
|
||||||
|
maxlength = len(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Strings(keys)
|
||||||
|
fmt.Println("ENR attribute counts:")
|
||||||
|
for _, key := range keys {
|
||||||
|
fmt.Printf("%s%s: %d\n", strings.Repeat(" ", maxlength-len(key)+1), key, attrcount[key])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func nodesetFilter(ctx *cli.Context) error {
|
func nodesetFilter(ctx *cli.Context) error {
|
||||||
if ctx.NArg() < 1 {
|
if ctx.NArg() < 1 {
|
||||||
return fmt.Errorf("need nodes file as argument")
|
return fmt.Errorf("need nodes file as argument")
|
||||||
}
|
}
|
||||||
ns := loadNodesJSON(ctx.Args().First())
|
// Parse -limit.
|
||||||
|
limit, err := parseFilterLimit(ctx.Args().Tail())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Parse the filters.
|
||||||
filter, err := andFilter(ctx.Args().Tail())
|
filter, err := andFilter(ctx.Args().Tail())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load nodes and apply filters.
|
||||||
|
ns := loadNodesJSON(ctx.Args().First())
|
||||||
result := make(nodeSet)
|
result := make(nodeSet)
|
||||||
for id, n := range ns {
|
for id, n := range ns {
|
||||||
if filter(n) {
|
if filter(n) {
|
||||||
result[id] = n
|
result[id] = n
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if limit >= 0 {
|
||||||
|
result = result.topN(limit)
|
||||||
|
}
|
||||||
writeNodesJSON("-", result)
|
writeNodesJSON("-", result)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -91,6 +134,7 @@ type nodeFilterC struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var filterFlags = map[string]nodeFilterC{
|
var filterFlags = map[string]nodeFilterC{
|
||||||
|
"-limit": {1, trueFilter}, // needed to skip over -limit
|
||||||
"-ip": {1, ipFilter},
|
"-ip": {1, ipFilter},
|
||||||
"-min-age": {1, minAgeFilter},
|
"-min-age": {1, minAgeFilter},
|
||||||
"-eth-network": {1, ethFilter},
|
"-eth-network": {1, ethFilter},
|
||||||
@ -98,6 +142,7 @@ var filterFlags = map[string]nodeFilterC{
|
|||||||
"-snap": {0, snapFilter},
|
"-snap": {0, snapFilter},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parseFilters parses nodeFilters from args.
|
||||||
func parseFilters(args []string) ([]nodeFilter, error) {
|
func parseFilters(args []string) ([]nodeFilter, error) {
|
||||||
var filters []nodeFilter
|
var filters []nodeFilter
|
||||||
for len(args) > 0 {
|
for len(args) > 0 {
|
||||||
@ -118,6 +163,26 @@ func parseFilters(args []string) ([]nodeFilter, error) {
|
|||||||
return filters, nil
|
return filters, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parseFilterLimit parses the -limit option in args. It returns -1 if there is no limit.
|
||||||
|
func parseFilterLimit(args []string) (int, error) {
|
||||||
|
limit := -1
|
||||||
|
for i, arg := range args {
|
||||||
|
if arg == "-limit" {
|
||||||
|
if i == len(args)-1 {
|
||||||
|
return -1, errors.New("-limit requires an argument")
|
||||||
|
}
|
||||||
|
n, err := strconv.Atoi(args[i+1])
|
||||||
|
if err != nil {
|
||||||
|
return -1, fmt.Errorf("invalid -limit %q", args[i+1])
|
||||||
|
}
|
||||||
|
limit = n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return limit, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// andFilter parses node filters in args and and returns a single filter that requires all
|
||||||
|
// of them to match.
|
||||||
func andFilter(args []string) (nodeFilter, error) {
|
func andFilter(args []string) (nodeFilter, error) {
|
||||||
checks, err := parseFilters(args)
|
checks, err := parseFilters(args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -134,6 +199,10 @@ func andFilter(args []string) (nodeFilter, error) {
|
|||||||
return f, nil
|
return f, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func trueFilter(args []string) (nodeFilter, error) {
|
||||||
|
return func(n nodeJSON) bool { return true }, nil
|
||||||
|
}
|
||||||
|
|
||||||
func ipFilter(args []string) (nodeFilter, error) {
|
func ipFilter(args []string) (nodeFilter, error) {
|
||||||
_, cidr, err := net.ParseCIDR(args[0])
|
_, cidr, err := net.ParseCIDR(args[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
Loading…
Reference in New Issue
Block a user