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", ð)) != 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 }