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:
Felix Lange 2021-04-19 14:54:38 +02:00 committed by GitHub
parent e43ac53264
commit 424656519a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 122 additions and 4 deletions

@ -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 {