// Copyright 2017 The go-ethereum Authors // This file is part of go-ethereum. // // go-ethereum is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // go-ethereum is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with go-ethereum. If not, see . // p2psim provides a command-line client for a simulation HTTP API. // // Here is an example of creating a 2 node network with the first node // connected to the second: // // $ p2psim node create // Created node01 // // $ p2psim node start node01 // Started node01 // // $ p2psim node create // Created node02 // // $ p2psim node start node02 // Started node02 // // $ p2psim node connect node01 node02 // Connected node01 to node02 package main import ( "context" "encoding/json" "fmt" "io" "os" "strings" "text/tabwriter" "github.com/urfave/cli/v2" "github.com/ledgerwatch/erigon/crypto" "github.com/ledgerwatch/erigon/p2p" "github.com/ledgerwatch/erigon/p2p/enode" "github.com/ledgerwatch/erigon/p2p/simulations" "github.com/ledgerwatch/erigon/p2p/simulations/adapters" "github.com/ledgerwatch/erigon/rpc" ) var client *simulations.Client func main() { app := cli.NewApp() app.Usage = "devp2p simulation command-line client" app.Flags = []cli.Flag{ &cli.StringFlag{ Name: "api", Value: "http://localhost:8888", Usage: "simulation API URL", EnvVars: []string{"P2PSIM_API_URL"}, }, } app.Before = func(ctx *cli.Context) error { client = simulations.NewClient(ctx.String("api")) return nil } app.Commands = []*cli.Command{ { Name: "show", Usage: "show network information", Action: showNetwork, }, { Name: "events", Usage: "stream network events", Action: streamNetwork, Flags: []cli.Flag{ &cli.BoolFlag{ Name: "current", Usage: "get existing nodes and conns first", }, &cli.StringFlag{ Name: "filter", Value: "", Usage: "message filter", }, }, }, { Name: "snapshot", Usage: "create a network snapshot to stdout", Action: createSnapshot, }, { Name: "load", Usage: "load a network snapshot from stdin", Action: loadSnapshot, }, { Name: "node", Usage: "manage simulation nodes", Action: listNodes, Subcommands: []*cli.Command{ { Name: "list", Usage: "list nodes", Action: listNodes, }, { Name: "create", Usage: "create a node", Action: createNode, Flags: []cli.Flag{ &cli.StringFlag{ Name: "name", Value: "", Usage: "node name", }, &cli.StringFlag{ Name: "services", Value: "", Usage: "node services (comma separated)", }, &cli.StringFlag{ Name: "key", Value: "", Usage: "node private key (hex encoded)", }, }, }, { Name: "show", ArgsUsage: "", Usage: "show node information", Action: showNode, }, { Name: "start", ArgsUsage: "", Usage: "start a node", Action: startNode, }, { Name: "stop", ArgsUsage: "", Usage: "stop a node", Action: stopNode, }, { Name: "connect", ArgsUsage: " ", Usage: "connect a node to a peer node", Action: connectNode, }, { Name: "disconnect", ArgsUsage: " ", Usage: "disconnect a node from a peer node", Action: disconnectNode, }, { Name: "rpc", ArgsUsage: " []", Usage: "call a node RPC method", Action: rpcNode, Flags: []cli.Flag{ &cli.BoolFlag{ Name: "subscribe", Usage: "method is a subscription", }, }, }, }, }, } if err := app.Run(os.Args); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } } func showNetwork(ctx *cli.Context) error { if ctx.NArg() != 0 { return cli.ShowCommandHelp(ctx, ctx.Command.Name) } network, err := client.GetNetwork() if err != nil { return err } w := tabwriter.NewWriter(ctx.App.Writer, 1, 2, 2, ' ', 0) defer w.Flush() fmt.Fprintf(w, "NODES\t%d\n", len(network.Nodes)) fmt.Fprintf(w, "CONNS\t%d\n", len(network.Conns)) return nil } func streamNetwork(ctx *cli.Context) error { if ctx.NArg() != 0 { return cli.ShowCommandHelp(ctx, ctx.Command.Name) } events := make(chan *simulations.Event) sub, err := client.SubscribeNetwork(events, simulations.SubscribeOpts{ Current: ctx.Bool("current"), Filter: ctx.String("filter"), }) if err != nil { return err } defer sub.Unsubscribe() enc := json.NewEncoder(ctx.App.Writer) for { select { case event := <-events: if err := enc.Encode(event); err != nil { return err } case err := <-sub.Err(): return err } } } func createSnapshot(ctx *cli.Context) error { if ctx.NArg() != 0 { return cli.ShowCommandHelp(ctx, ctx.Command.Name) } snap, err := client.CreateSnapshot() if err != nil { return err } return json.NewEncoder(os.Stdout).Encode(snap) } func loadSnapshot(ctx *cli.Context) error { if ctx.NArg() != 0 { return cli.ShowCommandHelp(ctx, ctx.Command.Name) } snap := &simulations.Snapshot{} if err := json.NewDecoder(os.Stdin).Decode(snap); err != nil { return err } return client.LoadSnapshot(snap) } func listNodes(ctx *cli.Context) error { if ctx.NArg() != 0 { return cli.ShowCommandHelp(ctx, ctx.Command.Name) } nodes, err := client.GetNodes() if err != nil { return err } w := tabwriter.NewWriter(ctx.App.Writer, 1, 2, 2, ' ', 0) defer w.Flush() fmt.Fprintf(w, "NAME\tPROTOCOLS\tID\n") for _, node := range nodes { fmt.Fprintf(w, "%s\t%s\t%s\n", node.Name, strings.Join(protocolList(node), ","), node.ID) } return nil } func protocolList(node *p2p.NodeInfo) []string { protos := make([]string, 0, len(node.Protocols)) for name := range node.Protocols { protos = append(protos, name) } return protos } func createNode(ctx *cli.Context) error { if ctx.NArg() != 0 { return cli.ShowCommandHelp(ctx, ctx.Command.Name) } config := adapters.RandomNodeConfig() config.Name = ctx.String("name") if key := ctx.String("key"); key != "" { privKey, err := crypto.HexToECDSA(key) if err != nil { return err } config.ID = enode.PubkeyToIDV4(&privKey.PublicKey) config.PrivateKey = privKey } if services := ctx.String("services"); services != "" { config.Lifecycles = strings.Split(services, ",") } node, err := client.CreateNode(config) if err != nil { return err } fmt.Fprintln(ctx.App.Writer, "Created", node.Name) return nil } func showNode(ctx *cli.Context) error { args := ctx.Args() if args.Len() != 1 { return cli.ShowCommandHelp(ctx, ctx.Command.Name) } nodeName := args.First() node, err := client.GetNode(nodeName) if err != nil { return err } w := tabwriter.NewWriter(ctx.App.Writer, 1, 2, 2, ' ', 0) defer w.Flush() fmt.Fprintf(w, "NAME\t%s\n", node.Name) fmt.Fprintf(w, "PROTOCOLS\t%s\n", strings.Join(protocolList(node), ",")) fmt.Fprintf(w, "ID\t%s\n", node.ID) fmt.Fprintf(w, "ENODE\t%s\n", node.Enode) for name, proto := range node.Protocols { fmt.Fprintln(w) fmt.Fprintf(w, "--- PROTOCOL INFO: %s\n", name) fmt.Fprintf(w, "%v\n", proto) fmt.Fprintf(w, "---\n") } return nil } func startNode(ctx *cli.Context) error { args := ctx.Args() if args.Len() != 1 { return cli.ShowCommandHelp(ctx, ctx.Command.Name) } nodeName := args.First() if err := client.StartNode(nodeName); err != nil { return err } fmt.Fprintln(ctx.App.Writer, "Started", nodeName) return nil } func stopNode(ctx *cli.Context) error { args := ctx.Args() if args.Len() != 1 { return cli.ShowCommandHelp(ctx, ctx.Command.Name) } nodeName := args.First() if err := client.StopNode(nodeName); err != nil { return err } fmt.Fprintln(ctx.App.Writer, "Stopped", nodeName) return nil } func connectNode(ctx *cli.Context) error { args := ctx.Args() if args.Len() != 2 { return cli.ShowCommandHelp(ctx, ctx.Command.Name) } nodeName := args.Get(0) peerName := args.Get(1) if err := client.ConnectNode(nodeName, peerName); err != nil { return err } fmt.Fprintln(ctx.App.Writer, "Connected", nodeName, "to", peerName) return nil } func disconnectNode(ctx *cli.Context) error { args := ctx.Args() if args.Len() != 2 { return cli.ShowCommandHelp(ctx, ctx.Command.Name) } nodeName := args.Get(0) peerName := args.Get(1) if err := client.DisconnectNode(nodeName, peerName); err != nil { return err } fmt.Fprintln(ctx.App.Writer, "Disconnected", nodeName, "from", peerName) return nil } func rpcNode(ctx *cli.Context) error { args := ctx.Args() if args.Len() < 2 { return cli.ShowCommandHelp(ctx, ctx.Command.Name) } nodeName := args.Get(0) method := args.Get(1) rpcClient, err := client.RPCClient(context.Background(), nodeName) if err != nil { return err } if ctx.Bool("subscribe") { return rpcSubscribe(rpcClient, ctx.App.Writer, method, args.Slice()[3:]...) } var result interface{} params := make([]interface{}, len(args.Slice()[3:])) for i, v := range args.Slice()[3:] { params[i] = v } if err := rpcClient.Call(&result, method, params...); err != nil { return err } return json.NewEncoder(ctx.App.Writer).Encode(result) } func rpcSubscribe(client *rpc.Client, out io.Writer, method string, args ...string) error { parts := strings.SplitN(method, "_", 2) namespace := parts[0] method = parts[1] ch := make(chan interface{}) subArgs := make([]interface{}, len(args)+1) subArgs[0] = method for i, v := range args { subArgs[i+1] = v } sub, err := client.Subscribe(context.Background(), namespace, ch, subArgs...) if err != nil { return err } defer sub.Unsubscribe() enc := json.NewEncoder(out) for { select { case v := <-ch: if err := enc.Encode(v); err != nil { return err } case err := <-sub.Err(): return err } } }