mirror of https://github.com/ethereum/go-ethereum
p2p/simulations: remove packages (#30250)
Looking at the history of these packages over the past several years, there haven't been any meaningful contributions or usages: https://github.com/ethereum/go-ethereum/commits/master/p2p/simulations?before=de6d5976794a9ed3b626d4eba57bf7f0806fb970+35 Almost all of the commits are part of larger refactors or low-hanging-fruit contributions. Seems like it's not providing much value and taking up team + contributor time.pull/29179/head
parent
32a1e0643c
commit
33a13b6f21
@ -1,443 +0,0 @@ |
|||||||
// 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
// 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/ethereum/go-ethereum/crypto" |
|
||||||
"github.com/ethereum/go-ethereum/internal/flags" |
|
||||||
"github.com/ethereum/go-ethereum/p2p" |
|
||||||
"github.com/ethereum/go-ethereum/p2p/enode" |
|
||||||
"github.com/ethereum/go-ethereum/p2p/simulations" |
|
||||||
"github.com/ethereum/go-ethereum/p2p/simulations/adapters" |
|
||||||
"github.com/ethereum/go-ethereum/rpc" |
|
||||||
"github.com/urfave/cli/v2" |
|
||||||
) |
|
||||||
|
|
||||||
var client *simulations.Client |
|
||||||
|
|
||||||
var ( |
|
||||||
// global command flags
|
|
||||||
apiFlag = &cli.StringFlag{ |
|
||||||
Name: "api", |
|
||||||
Value: "http://localhost:8888", |
|
||||||
Usage: "simulation API URL", |
|
||||||
EnvVars: []string{"P2PSIM_API_URL"}, |
|
||||||
} |
|
||||||
|
|
||||||
// events subcommand flags
|
|
||||||
currentFlag = &cli.BoolFlag{ |
|
||||||
Name: "current", |
|
||||||
Usage: "get existing nodes and conns first", |
|
||||||
} |
|
||||||
filterFlag = &cli.StringFlag{ |
|
||||||
Name: "filter", |
|
||||||
Value: "", |
|
||||||
Usage: "message filter", |
|
||||||
} |
|
||||||
|
|
||||||
// node create subcommand flags
|
|
||||||
nameFlag = &cli.StringFlag{ |
|
||||||
Name: "name", |
|
||||||
Value: "", |
|
||||||
Usage: "node name", |
|
||||||
} |
|
||||||
servicesFlag = &cli.StringFlag{ |
|
||||||
Name: "services", |
|
||||||
Value: "", |
|
||||||
Usage: "node services (comma separated)", |
|
||||||
} |
|
||||||
keyFlag = &cli.StringFlag{ |
|
||||||
Name: "key", |
|
||||||
Value: "", |
|
||||||
Usage: "node private key (hex encoded)", |
|
||||||
} |
|
||||||
|
|
||||||
// node rpc subcommand flags
|
|
||||||
subscribeFlag = &cli.BoolFlag{ |
|
||||||
Name: "subscribe", |
|
||||||
Usage: "method is a subscription", |
|
||||||
} |
|
||||||
) |
|
||||||
|
|
||||||
func main() { |
|
||||||
app := flags.NewApp("devp2p simulation command-line client") |
|
||||||
app.Flags = []cli.Flag{ |
|
||||||
apiFlag, |
|
||||||
} |
|
||||||
app.Before = func(ctx *cli.Context) error { |
|
||||||
client = simulations.NewClient(ctx.String(apiFlag.Name)) |
|
||||||
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{ |
|
||||||
currentFlag, |
|
||||||
filterFlag, |
|
||||||
}, |
|
||||||
}, |
|
||||||
{ |
|
||||||
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{ |
|
||||||
nameFlag, |
|
||||||
servicesFlag, |
|
||||||
keyFlag, |
|
||||||
}, |
|
||||||
}, |
|
||||||
{ |
|
||||||
Name: "show", |
|
||||||
ArgsUsage: "<node>", |
|
||||||
Usage: "show node information", |
|
||||||
Action: showNode, |
|
||||||
}, |
|
||||||
{ |
|
||||||
Name: "start", |
|
||||||
ArgsUsage: "<node>", |
|
||||||
Usage: "start a node", |
|
||||||
Action: startNode, |
|
||||||
}, |
|
||||||
{ |
|
||||||
Name: "stop", |
|
||||||
ArgsUsage: "<node>", |
|
||||||
Usage: "stop a node", |
|
||||||
Action: stopNode, |
|
||||||
}, |
|
||||||
{ |
|
||||||
Name: "connect", |
|
||||||
ArgsUsage: "<node> <peer>", |
|
||||||
Usage: "connect a node to a peer node", |
|
||||||
Action: connectNode, |
|
||||||
}, |
|
||||||
{ |
|
||||||
Name: "disconnect", |
|
||||||
ArgsUsage: "<node> <peer>", |
|
||||||
Usage: "disconnect a node from a peer node", |
|
||||||
Action: disconnectNode, |
|
||||||
}, |
|
||||||
{ |
|
||||||
Name: "rpc", |
|
||||||
ArgsUsage: "<node> <method> [<args>]", |
|
||||||
Usage: "call a node RPC method", |
|
||||||
Action: rpcNode, |
|
||||||
Flags: []cli.Flag{ |
|
||||||
subscribeFlag, |
|
||||||
}, |
|
||||||
}, |
|
||||||
}, |
|
||||||
}, |
|
||||||
} |
|
||||||
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(currentFlag.Name), |
|
||||||
Filter: ctx.String(filterFlag.Name), |
|
||||||
}) |
|
||||||
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(nameFlag.Name) |
|
||||||
if key := ctx.String(keyFlag.Name); key != "" { |
|
||||||
privKey, err := crypto.HexToECDSA(key) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
config.ID = enode.PubkeyToIDV4(&privKey.PublicKey) |
|
||||||
config.PrivateKey = privKey |
|
||||||
} |
|
||||||
if services := ctx.String(servicesFlag.Name); 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 { |
|
||||||
if ctx.NArg() != 1 { |
|
||||||
return cli.ShowCommandHelp(ctx, ctx.Command.Name) |
|
||||||
} |
|
||||||
nodeName := ctx.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 { |
|
||||||
if ctx.NArg() != 1 { |
|
||||||
return cli.ShowCommandHelp(ctx, ctx.Command.Name) |
|
||||||
} |
|
||||||
nodeName := ctx.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 { |
|
||||||
if ctx.NArg() != 1 { |
|
||||||
return cli.ShowCommandHelp(ctx, ctx.Command.Name) |
|
||||||
} |
|
||||||
nodeName := ctx.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 { |
|
||||||
if ctx.NArg() != 2 { |
|
||||||
return cli.ShowCommandHelp(ctx, ctx.Command.Name) |
|
||||||
} |
|
||||||
args := ctx.Args() |
|
||||||
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(subscribeFlag.Name) { |
|
||||||
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 { |
|
||||||
namespace, method, _ := strings.Cut(method, "_") |
|
||||||
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 |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
@ -1,174 +0,0 @@ |
|||||||
# devp2p Simulations |
|
||||||
|
|
||||||
The `p2p/simulations` package implements a simulation framework that supports |
|
||||||
creating a collection of devp2p nodes, connecting them to form a |
|
||||||
simulation network, performing simulation actions in that network and then |
|
||||||
extracting useful information. |
|
||||||
|
|
||||||
## Nodes |
|
||||||
|
|
||||||
Each node in a simulation network runs multiple services by wrapping a collection |
|
||||||
of objects which implement the `node.Service` interface meaning they: |
|
||||||
|
|
||||||
* can be started and stopped |
|
||||||
* run p2p protocols |
|
||||||
* expose RPC APIs |
|
||||||
|
|
||||||
This means that any object which implements the `node.Service` interface can be |
|
||||||
used to run a node in the simulation. |
|
||||||
|
|
||||||
## Services |
|
||||||
|
|
||||||
Before running a simulation, a set of service initializers must be registered |
|
||||||
which can then be used to run nodes in the network. |
|
||||||
|
|
||||||
A service initializer is a function with the following signature: |
|
||||||
|
|
||||||
```go |
|
||||||
func(ctx *adapters.ServiceContext) (node.Service, error) |
|
||||||
``` |
|
||||||
|
|
||||||
These initializers should be registered by calling the `adapters.RegisterServices` |
|
||||||
function in an `init()` hook: |
|
||||||
|
|
||||||
```go |
|
||||||
func init() { |
|
||||||
adapters.RegisterServices(adapters.Services{ |
|
||||||
"service1": initService1, |
|
||||||
"service2": initService2, |
|
||||||
}) |
|
||||||
} |
|
||||||
``` |
|
||||||
|
|
||||||
## Node Adapters |
|
||||||
|
|
||||||
The simulation framework includes multiple "node adapters" which are |
|
||||||
responsible for creating an environment in which a node runs. |
|
||||||
|
|
||||||
### SimAdapter |
|
||||||
|
|
||||||
The `SimAdapter` runs nodes in-memory, connecting them using an in-memory, |
|
||||||
synchronous `net.Pipe` and connecting to their RPC server using an in-memory |
|
||||||
`rpc.Client`. |
|
||||||
|
|
||||||
### ExecAdapter |
|
||||||
|
|
||||||
The `ExecAdapter` runs nodes as child processes of the running simulation. |
|
||||||
|
|
||||||
It does this by executing the binary which is running the simulation but |
|
||||||
setting `argv[0]` (i.e. the program name) to `p2p-node` which is then |
|
||||||
detected by an init hook in the child process which runs the `node.Service` |
|
||||||
using the devp2p node stack rather than executing `main()`. |
|
||||||
|
|
||||||
The nodes listen for devp2p connections and WebSocket RPC clients on random |
|
||||||
localhost ports. |
|
||||||
|
|
||||||
## Network |
|
||||||
|
|
||||||
A simulation network is created with an ID and default service. The default |
|
||||||
service is used if a node is created without an explicit service. The |
|
||||||
network has exposed methods for creating, starting, stopping, connecting |
|
||||||
and disconnecting nodes. It also emits events when certain actions occur. |
|
||||||
|
|
||||||
### Events |
|
||||||
|
|
||||||
A simulation network emits the following events: |
|
||||||
|
|
||||||
* node event - when nodes are created / started / stopped |
|
||||||
* connection event - when nodes are connected / disconnected |
|
||||||
* message event - when a protocol message is sent between two nodes |
|
||||||
|
|
||||||
The events have a "control" flag which when set indicates that the event is the |
|
||||||
outcome of a controlled simulation action (e.g. creating a node or explicitly |
|
||||||
connecting two nodes). |
|
||||||
|
|
||||||
This is in contrast to a non-control event, otherwise called a "live" event, |
|
||||||
which is the outcome of something happening in the network as a result of a |
|
||||||
control event (e.g. a node actually started up or a connection was actually |
|
||||||
established between two nodes). |
|
||||||
|
|
||||||
Live events are detected by the simulation network by subscribing to node peer |
|
||||||
events via RPC when the nodes start up. |
|
||||||
|
|
||||||
## Testing Framework |
|
||||||
|
|
||||||
The `Simulation` type can be used in tests to perform actions in a simulation |
|
||||||
network and then wait for expectations to be met. |
|
||||||
|
|
||||||
With a running simulation network, the `Simulation.Run` method can be called |
|
||||||
with a `Step` which has the following fields: |
|
||||||
|
|
||||||
* `Action` - a function that performs some action in the network |
|
||||||
|
|
||||||
* `Expect` - an expectation function which returns whether or not a |
|
||||||
given node meets the expectation |
|
||||||
|
|
||||||
* `Trigger` - a channel that receives node IDs which then trigger a check |
|
||||||
of the expectation function to be performed against that node |
|
||||||
|
|
||||||
As a concrete example, consider a simulated network of Ethereum nodes. An |
|
||||||
`Action` could be the sending of a transaction, `Expect` it being included in |
|
||||||
a block, and `Trigger` a check for every block that is mined. |
|
||||||
|
|
||||||
On return, the `Simulation.Run` method returns a `StepResult` which can be used |
|
||||||
to determine if all nodes met the expectation, how long it took them to meet |
|
||||||
the expectation and what network events were emitted during the step run. |
|
||||||
|
|
||||||
## HTTP API |
|
||||||
|
|
||||||
The simulation framework includes a HTTP API that can be used to control the |
|
||||||
simulation. |
|
||||||
|
|
||||||
The API is initialised with a particular node adapter and has the following |
|
||||||
endpoints: |
|
||||||
|
|
||||||
``` |
|
||||||
OPTIONS / Response 200 with "Access-Control-Allow-Headers"" header set to "Content-Type"" |
|
||||||
GET / Get network information |
|
||||||
POST /start Start all nodes in the network |
|
||||||
POST /stop Stop all nodes in the network |
|
||||||
POST /mocker/start Start the mocker node simulation |
|
||||||
POST /mocker/stop Stop the mocker node simulation |
|
||||||
GET /mocker Get a list of available mockers |
|
||||||
POST /reset Reset all properties of a network to initial (empty) state |
|
||||||
GET /events Stream network events |
|
||||||
GET /snapshot Take a network snapshot |
|
||||||
POST /snapshot Load a network snapshot |
|
||||||
POST /nodes Create a node |
|
||||||
GET /nodes Get all nodes in the network |
|
||||||
GET /nodes/:nodeid Get node information |
|
||||||
POST /nodes/:nodeid/start Start a node |
|
||||||
POST /nodes/:nodeid/stop Stop a node |
|
||||||
POST /nodes/:nodeid/conn/:peerid Connect two nodes |
|
||||||
DELETE /nodes/:nodeid/conn/:peerid Disconnect two nodes |
|
||||||
GET /nodes/:nodeid/rpc Make RPC requests to a node via WebSocket |
|
||||||
``` |
|
||||||
|
|
||||||
For convenience, `nodeid` in the URL can be the name of a node rather than its |
|
||||||
ID. |
|
||||||
|
|
||||||
## Command line client |
|
||||||
|
|
||||||
`p2psim` is a command line client for the HTTP API, located in |
|
||||||
`cmd/p2psim`. |
|
||||||
|
|
||||||
It provides the following commands: |
|
||||||
|
|
||||||
``` |
|
||||||
p2psim show |
|
||||||
p2psim events [--current] [--filter=FILTER] |
|
||||||
p2psim snapshot |
|
||||||
p2psim load |
|
||||||
p2psim node create [--name=NAME] [--services=SERVICES] [--key=KEY] |
|
||||||
p2psim node list |
|
||||||
p2psim node show <node> |
|
||||||
p2psim node start <node> |
|
||||||
p2psim node stop <node> |
|
||||||
p2psim node connect <node> <peer> |
|
||||||
p2psim node disconnect <node> <peer> |
|
||||||
p2psim node rpc <node> <method> [<args>] [--subscribe] |
|
||||||
``` |
|
||||||
|
|
||||||
## Example |
|
||||||
|
|
||||||
See [p2p/simulations/examples/README.md](examples/README.md). |
|
@ -1,567 +0,0 @@ |
|||||||
// Copyright 2017 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library 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 Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package adapters |
|
||||||
|
|
||||||
import ( |
|
||||||
"bytes" |
|
||||||
"context" |
|
||||||
"encoding/json" |
|
||||||
"errors" |
|
||||||
"fmt" |
|
||||||
"io" |
|
||||||
"log/slog" |
|
||||||
"net" |
|
||||||
"net/http" |
|
||||||
"os" |
|
||||||
"os/exec" |
|
||||||
"os/signal" |
|
||||||
"path/filepath" |
|
||||||
"strings" |
|
||||||
"sync" |
|
||||||
"syscall" |
|
||||||
"time" |
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/internal/reexec" |
|
||||||
"github.com/ethereum/go-ethereum/log" |
|
||||||
"github.com/ethereum/go-ethereum/node" |
|
||||||
"github.com/ethereum/go-ethereum/p2p" |
|
||||||
"github.com/ethereum/go-ethereum/p2p/enode" |
|
||||||
"github.com/ethereum/go-ethereum/rpc" |
|
||||||
"github.com/gorilla/websocket" |
|
||||||
) |
|
||||||
|
|
||||||
func init() { |
|
||||||
// Register a reexec function to start a simulation node when the current binary is
|
|
||||||
// executed as "p2p-node" (rather than whatever the main() function would normally do).
|
|
||||||
reexec.Register("p2p-node", execP2PNode) |
|
||||||
} |
|
||||||
|
|
||||||
// ExecAdapter is a NodeAdapter which runs simulation nodes by executing the current binary
|
|
||||||
// as a child process.
|
|
||||||
type ExecAdapter struct { |
|
||||||
// BaseDir is the directory under which the data directories for each
|
|
||||||
// simulation node are created.
|
|
||||||
BaseDir string |
|
||||||
|
|
||||||
nodes map[enode.ID]*ExecNode |
|
||||||
} |
|
||||||
|
|
||||||
// NewExecAdapter returns an ExecAdapter which stores node data in
|
|
||||||
// subdirectories of the given base directory
|
|
||||||
func NewExecAdapter(baseDir string) *ExecAdapter { |
|
||||||
return &ExecAdapter{ |
|
||||||
BaseDir: baseDir, |
|
||||||
nodes: make(map[enode.ID]*ExecNode), |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// Name returns the name of the adapter for logging purposes
|
|
||||||
func (e *ExecAdapter) Name() string { |
|
||||||
return "exec-adapter" |
|
||||||
} |
|
||||||
|
|
||||||
// NewNode returns a new ExecNode using the given config
|
|
||||||
func (e *ExecAdapter) NewNode(config *NodeConfig) (Node, error) { |
|
||||||
if len(config.Lifecycles) == 0 { |
|
||||||
return nil, errors.New("node must have at least one service lifecycle") |
|
||||||
} |
|
||||||
for _, service := range config.Lifecycles { |
|
||||||
if _, exists := lifecycleConstructorFuncs[service]; !exists { |
|
||||||
return nil, fmt.Errorf("unknown node service %q", service) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// create the node directory using the first 12 characters of the ID
|
|
||||||
// as Unix socket paths cannot be longer than 256 characters
|
|
||||||
dir := filepath.Join(e.BaseDir, config.ID.String()[:12]) |
|
||||||
if err := os.Mkdir(dir, 0755); err != nil { |
|
||||||
return nil, fmt.Errorf("error creating node directory: %s", err) |
|
||||||
} |
|
||||||
|
|
||||||
err := config.initDummyEnode() |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
// generate the config
|
|
||||||
conf := &execNodeConfig{ |
|
||||||
Stack: node.DefaultConfig, |
|
||||||
Node: config, |
|
||||||
} |
|
||||||
if config.DataDir != "" { |
|
||||||
conf.Stack.DataDir = config.DataDir |
|
||||||
} else { |
|
||||||
conf.Stack.DataDir = filepath.Join(dir, "data") |
|
||||||
} |
|
||||||
|
|
||||||
// these parameters are crucial for execadapter node to run correctly
|
|
||||||
conf.Stack.WSHost = "127.0.0.1" |
|
||||||
conf.Stack.WSPort = 0 |
|
||||||
conf.Stack.WSOrigins = []string{"*"} |
|
||||||
conf.Stack.WSExposeAll = true |
|
||||||
conf.Stack.P2P.EnableMsgEvents = config.EnableMsgEvents |
|
||||||
conf.Stack.P2P.NoDiscovery = true |
|
||||||
conf.Stack.P2P.NAT = nil |
|
||||||
|
|
||||||
// Listen on a localhost port, which we set when we
|
|
||||||
// initialise NodeConfig (usually a random port)
|
|
||||||
conf.Stack.P2P.ListenAddr = fmt.Sprintf(":%d", config.Port) |
|
||||||
|
|
||||||
node := &ExecNode{ |
|
||||||
ID: config.ID, |
|
||||||
Dir: dir, |
|
||||||
Config: conf, |
|
||||||
adapter: e, |
|
||||||
} |
|
||||||
node.newCmd = node.execCommand |
|
||||||
e.nodes[node.ID] = node |
|
||||||
return node, nil |
|
||||||
} |
|
||||||
|
|
||||||
// ExecNode starts a simulation node by exec'ing the current binary and
|
|
||||||
// running the configured services
|
|
||||||
type ExecNode struct { |
|
||||||
ID enode.ID |
|
||||||
Dir string |
|
||||||
Config *execNodeConfig |
|
||||||
Cmd *exec.Cmd |
|
||||||
Info *p2p.NodeInfo |
|
||||||
|
|
||||||
adapter *ExecAdapter |
|
||||||
client *rpc.Client |
|
||||||
wsAddr string |
|
||||||
newCmd func() *exec.Cmd |
|
||||||
} |
|
||||||
|
|
||||||
// Addr returns the node's enode URL
|
|
||||||
func (n *ExecNode) Addr() []byte { |
|
||||||
if n.Info == nil { |
|
||||||
return nil |
|
||||||
} |
|
||||||
return []byte(n.Info.Enode) |
|
||||||
} |
|
||||||
|
|
||||||
// Client returns an rpc.Client which can be used to communicate with the
|
|
||||||
// underlying services (it is set once the node has started)
|
|
||||||
func (n *ExecNode) Client() (*rpc.Client, error) { |
|
||||||
return n.client, nil |
|
||||||
} |
|
||||||
|
|
||||||
// Start exec's the node passing the ID and service as command line arguments
|
|
||||||
// and the node config encoded as JSON in an environment variable.
|
|
||||||
func (n *ExecNode) Start(snapshots map[string][]byte) (err error) { |
|
||||||
if n.Cmd != nil { |
|
||||||
return errors.New("already started") |
|
||||||
} |
|
||||||
defer func() { |
|
||||||
if err != nil { |
|
||||||
n.Stop() |
|
||||||
} |
|
||||||
}() |
|
||||||
|
|
||||||
// encode a copy of the config containing the snapshot
|
|
||||||
confCopy := *n.Config |
|
||||||
confCopy.Snapshots = snapshots |
|
||||||
confCopy.PeerAddrs = make(map[string]string) |
|
||||||
for id, node := range n.adapter.nodes { |
|
||||||
confCopy.PeerAddrs[id.String()] = node.wsAddr |
|
||||||
} |
|
||||||
confData, err := json.Marshal(confCopy) |
|
||||||
if err != nil { |
|
||||||
return fmt.Errorf("error generating node config: %s", err) |
|
||||||
} |
|
||||||
// expose the admin namespace via websocket if it's not enabled
|
|
||||||
exposed := confCopy.Stack.WSExposeAll |
|
||||||
if !exposed { |
|
||||||
for _, api := range confCopy.Stack.WSModules { |
|
||||||
if api == "admin" { |
|
||||||
exposed = true |
|
||||||
break |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
if !exposed { |
|
||||||
confCopy.Stack.WSModules = append(confCopy.Stack.WSModules, "admin") |
|
||||||
} |
|
||||||
// start the one-shot server that waits for startup information
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) |
|
||||||
defer cancel() |
|
||||||
statusURL, statusC := n.waitForStartupJSON(ctx) |
|
||||||
|
|
||||||
// start the node
|
|
||||||
cmd := n.newCmd() |
|
||||||
cmd.Stdout = os.Stdout |
|
||||||
cmd.Stderr = os.Stderr |
|
||||||
cmd.Env = append(os.Environ(), |
|
||||||
envStatusURL+"="+statusURL, |
|
||||||
envNodeConfig+"="+string(confData), |
|
||||||
) |
|
||||||
if err := cmd.Start(); err != nil { |
|
||||||
return fmt.Errorf("error starting node: %s", err) |
|
||||||
} |
|
||||||
n.Cmd = cmd |
|
||||||
|
|
||||||
// Wait for the node to start.
|
|
||||||
status := <-statusC |
|
||||||
if status.Err != "" { |
|
||||||
return errors.New(status.Err) |
|
||||||
} |
|
||||||
client, err := rpc.DialWebsocket(ctx, status.WSEndpoint, "") |
|
||||||
if err != nil { |
|
||||||
return fmt.Errorf("can't connect to RPC server: %v", err) |
|
||||||
} |
|
||||||
|
|
||||||
// Node ready :)
|
|
||||||
n.client = client |
|
||||||
n.wsAddr = status.WSEndpoint |
|
||||||
n.Info = status.NodeInfo |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
// waitForStartupJSON runs a one-shot HTTP server to receive a startup report.
|
|
||||||
func (n *ExecNode) waitForStartupJSON(ctx context.Context) (string, chan nodeStartupJSON) { |
|
||||||
var ( |
|
||||||
ch = make(chan nodeStartupJSON, 1) |
|
||||||
quitOnce sync.Once |
|
||||||
srv http.Server |
|
||||||
) |
|
||||||
l, err := net.Listen("tcp", "127.0.0.1:0") |
|
||||||
if err != nil { |
|
||||||
ch <- nodeStartupJSON{Err: err.Error()} |
|
||||||
return "", ch |
|
||||||
} |
|
||||||
quit := func(status nodeStartupJSON) { |
|
||||||
quitOnce.Do(func() { |
|
||||||
l.Close() |
|
||||||
ch <- status |
|
||||||
}) |
|
||||||
} |
|
||||||
srv.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
|
||||||
var status nodeStartupJSON |
|
||||||
if err := json.NewDecoder(r.Body).Decode(&status); err != nil { |
|
||||||
status.Err = fmt.Sprintf("can't decode startup report: %v", err) |
|
||||||
} |
|
||||||
quit(status) |
|
||||||
}) |
|
||||||
// Run the HTTP server, but don't wait forever and shut it down
|
|
||||||
// if the context is canceled.
|
|
||||||
go srv.Serve(l) |
|
||||||
go func() { |
|
||||||
<-ctx.Done() |
|
||||||
quit(nodeStartupJSON{Err: "didn't get startup report"}) |
|
||||||
}() |
|
||||||
|
|
||||||
url := "http://" + l.Addr().String() |
|
||||||
return url, ch |
|
||||||
} |
|
||||||
|
|
||||||
// execCommand returns a command which runs the node locally by exec'ing
|
|
||||||
// the current binary but setting argv[0] to "p2p-node" so that the child
|
|
||||||
// runs execP2PNode
|
|
||||||
func (n *ExecNode) execCommand() *exec.Cmd { |
|
||||||
return &exec.Cmd{ |
|
||||||
Path: reexec.Self(), |
|
||||||
Args: []string{"p2p-node", strings.Join(n.Config.Node.Lifecycles, ","), n.ID.String()}, |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// Stop stops the node by first sending SIGTERM and then SIGKILL if the node
|
|
||||||
// doesn't stop within 5s
|
|
||||||
func (n *ExecNode) Stop() error { |
|
||||||
if n.Cmd == nil { |
|
||||||
return nil |
|
||||||
} |
|
||||||
defer func() { |
|
||||||
n.Cmd = nil |
|
||||||
}() |
|
||||||
|
|
||||||
if n.client != nil { |
|
||||||
n.client.Close() |
|
||||||
n.client = nil |
|
||||||
n.wsAddr = "" |
|
||||||
n.Info = nil |
|
||||||
} |
|
||||||
|
|
||||||
if err := n.Cmd.Process.Signal(syscall.SIGTERM); err != nil { |
|
||||||
return n.Cmd.Process.Kill() |
|
||||||
} |
|
||||||
waitErr := make(chan error, 1) |
|
||||||
go func() { |
|
||||||
waitErr <- n.Cmd.Wait() |
|
||||||
}() |
|
||||||
timer := time.NewTimer(5 * time.Second) |
|
||||||
defer timer.Stop() |
|
||||||
|
|
||||||
select { |
|
||||||
case err := <-waitErr: |
|
||||||
return err |
|
||||||
case <-timer.C: |
|
||||||
return n.Cmd.Process.Kill() |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// NodeInfo returns information about the node
|
|
||||||
func (n *ExecNode) NodeInfo() *p2p.NodeInfo { |
|
||||||
info := &p2p.NodeInfo{ |
|
||||||
ID: n.ID.String(), |
|
||||||
} |
|
||||||
if n.client != nil { |
|
||||||
n.client.Call(&info, "admin_nodeInfo") |
|
||||||
} |
|
||||||
return info |
|
||||||
} |
|
||||||
|
|
||||||
// ServeRPC serves RPC requests over the given connection by dialling the
|
|
||||||
// node's WebSocket address and joining the two connections
|
|
||||||
func (n *ExecNode) ServeRPC(clientConn *websocket.Conn) error { |
|
||||||
conn, _, err := websocket.DefaultDialer.Dial(n.wsAddr, nil) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
var wg sync.WaitGroup |
|
||||||
wg.Add(2) |
|
||||||
go wsCopy(&wg, conn, clientConn) |
|
||||||
go wsCopy(&wg, clientConn, conn) |
|
||||||
wg.Wait() |
|
||||||
conn.Close() |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
func wsCopy(wg *sync.WaitGroup, src, dst *websocket.Conn) { |
|
||||||
defer wg.Done() |
|
||||||
for { |
|
||||||
msgType, r, err := src.NextReader() |
|
||||||
if err != nil { |
|
||||||
return |
|
||||||
} |
|
||||||
w, err := dst.NextWriter(msgType) |
|
||||||
if err != nil { |
|
||||||
return |
|
||||||
} |
|
||||||
if _, err = io.Copy(w, r); err != nil { |
|
||||||
return |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// Snapshots creates snapshots of the services by calling the
|
|
||||||
// simulation_snapshot RPC method
|
|
||||||
func (n *ExecNode) Snapshots() (map[string][]byte, error) { |
|
||||||
if n.client == nil { |
|
||||||
return nil, errors.New("RPC not started") |
|
||||||
} |
|
||||||
var snapshots map[string][]byte |
|
||||||
return snapshots, n.client.Call(&snapshots, "simulation_snapshot") |
|
||||||
} |
|
||||||
|
|
||||||
// execNodeConfig is used to serialize the node configuration so it can be
|
|
||||||
// passed to the child process as a JSON encoded environment variable
|
|
||||||
type execNodeConfig struct { |
|
||||||
Stack node.Config `json:"stack"` |
|
||||||
Node *NodeConfig `json:"node"` |
|
||||||
Snapshots map[string][]byte `json:"snapshots,omitempty"` |
|
||||||
PeerAddrs map[string]string `json:"peer_addrs,omitempty"` |
|
||||||
} |
|
||||||
|
|
||||||
func initLogging() { |
|
||||||
// Initialize the logging by default first.
|
|
||||||
var innerHandler slog.Handler |
|
||||||
innerHandler = slog.NewTextHandler(os.Stderr, nil) |
|
||||||
glogger := log.NewGlogHandler(innerHandler) |
|
||||||
glogger.Verbosity(log.LevelInfo) |
|
||||||
log.SetDefault(log.NewLogger(glogger)) |
|
||||||
|
|
||||||
confEnv := os.Getenv(envNodeConfig) |
|
||||||
if confEnv == "" { |
|
||||||
return |
|
||||||
} |
|
||||||
var conf execNodeConfig |
|
||||||
if err := json.Unmarshal([]byte(confEnv), &conf); err != nil { |
|
||||||
return |
|
||||||
} |
|
||||||
var writer = os.Stderr |
|
||||||
if conf.Node.LogFile != "" { |
|
||||||
logWriter, err := os.Create(conf.Node.LogFile) |
|
||||||
if err != nil { |
|
||||||
return |
|
||||||
} |
|
||||||
writer = logWriter |
|
||||||
} |
|
||||||
var verbosity = log.LevelInfo |
|
||||||
if conf.Node.LogVerbosity <= log.LevelTrace && conf.Node.LogVerbosity >= log.LevelCrit { |
|
||||||
verbosity = log.FromLegacyLevel(int(conf.Node.LogVerbosity)) |
|
||||||
} |
|
||||||
// Reinitialize the logger
|
|
||||||
innerHandler = log.NewTerminalHandler(writer, true) |
|
||||||
glogger = log.NewGlogHandler(innerHandler) |
|
||||||
glogger.Verbosity(verbosity) |
|
||||||
log.SetDefault(log.NewLogger(glogger)) |
|
||||||
} |
|
||||||
|
|
||||||
// execP2PNode starts a simulation node when the current binary is executed with
|
|
||||||
// argv[0] being "p2p-node", reading the service / ID from argv[1] / argv[2]
|
|
||||||
// and the node config from an environment variable.
|
|
||||||
func execP2PNode() { |
|
||||||
initLogging() |
|
||||||
|
|
||||||
statusURL := os.Getenv(envStatusURL) |
|
||||||
if statusURL == "" { |
|
||||||
log.Crit("missing " + envStatusURL) |
|
||||||
} |
|
||||||
|
|
||||||
// Start the node and gather startup report.
|
|
||||||
var status nodeStartupJSON |
|
||||||
stack, stackErr := startExecNodeStack() |
|
||||||
if stackErr != nil { |
|
||||||
status.Err = stackErr.Error() |
|
||||||
} else { |
|
||||||
status.WSEndpoint = stack.WSEndpoint() |
|
||||||
status.NodeInfo = stack.Server().NodeInfo() |
|
||||||
} |
|
||||||
|
|
||||||
// Send status to the host.
|
|
||||||
statusJSON, _ := json.Marshal(status) |
|
||||||
resp, err := http.Post(statusURL, "application/json", bytes.NewReader(statusJSON)) |
|
||||||
if err != nil { |
|
||||||
log.Crit("Can't post startup info", "url", statusURL, "err", err) |
|
||||||
} |
|
||||||
resp.Body.Close() |
|
||||||
if stackErr != nil { |
|
||||||
os.Exit(1) |
|
||||||
} |
|
||||||
|
|
||||||
// Stop the stack if we get a SIGTERM signal.
|
|
||||||
go func() { |
|
||||||
sigc := make(chan os.Signal, 1) |
|
||||||
signal.Notify(sigc, syscall.SIGTERM) |
|
||||||
defer signal.Stop(sigc) |
|
||||||
<-sigc |
|
||||||
log.Info("Received SIGTERM, shutting down...") |
|
||||||
stack.Close() |
|
||||||
}() |
|
||||||
stack.Wait() // Wait for the stack to exit.
|
|
||||||
} |
|
||||||
|
|
||||||
func startExecNodeStack() (*node.Node, error) { |
|
||||||
// read the services from argv
|
|
||||||
serviceNames := strings.Split(os.Args[1], ",") |
|
||||||
|
|
||||||
// decode the config
|
|
||||||
confEnv := os.Getenv(envNodeConfig) |
|
||||||
if confEnv == "" { |
|
||||||
return nil, errors.New("missing " + envNodeConfig) |
|
||||||
} |
|
||||||
var conf execNodeConfig |
|
||||||
if err := json.Unmarshal([]byte(confEnv), &conf); err != nil { |
|
||||||
return nil, fmt.Errorf("error decoding %s: %v", envNodeConfig, err) |
|
||||||
} |
|
||||||
|
|
||||||
// create enode record
|
|
||||||
nodeTcpConn, _ := net.ResolveTCPAddr("tcp", conf.Stack.P2P.ListenAddr) |
|
||||||
if nodeTcpConn.IP == nil { |
|
||||||
nodeTcpConn.IP = net.IPv4(127, 0, 0, 1) |
|
||||||
} |
|
||||||
conf.Node.initEnode(nodeTcpConn.IP, nodeTcpConn.Port, nodeTcpConn.Port) |
|
||||||
conf.Stack.P2P.PrivateKey = conf.Node.PrivateKey |
|
||||||
conf.Stack.Logger = log.New("node.id", conf.Node.ID.String()) |
|
||||||
|
|
||||||
// initialize the devp2p stack
|
|
||||||
stack, err := node.New(&conf.Stack) |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("error creating node stack: %v", err) |
|
||||||
} |
|
||||||
|
|
||||||
// Register the services, collecting them into a map so they can
|
|
||||||
// be accessed by the snapshot API.
|
|
||||||
services := make(map[string]node.Lifecycle, len(serviceNames)) |
|
||||||
for _, name := range serviceNames { |
|
||||||
lifecycleFunc, exists := lifecycleConstructorFuncs[name] |
|
||||||
if !exists { |
|
||||||
return nil, fmt.Errorf("unknown node service %q", err) |
|
||||||
} |
|
||||||
ctx := &ServiceContext{ |
|
||||||
RPCDialer: &wsRPCDialer{addrs: conf.PeerAddrs}, |
|
||||||
Config: conf.Node, |
|
||||||
} |
|
||||||
if conf.Snapshots != nil { |
|
||||||
ctx.Snapshot = conf.Snapshots[name] |
|
||||||
} |
|
||||||
service, err := lifecycleFunc(ctx, stack) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
services[name] = service |
|
||||||
} |
|
||||||
|
|
||||||
// Add the snapshot API.
|
|
||||||
stack.RegisterAPIs([]rpc.API{{ |
|
||||||
Namespace: "simulation", |
|
||||||
Service: SnapshotAPI{services}, |
|
||||||
}}) |
|
||||||
|
|
||||||
if err = stack.Start(); err != nil { |
|
||||||
err = fmt.Errorf("error starting stack: %v", err) |
|
||||||
} |
|
||||||
return stack, err |
|
||||||
} |
|
||||||
|
|
||||||
const ( |
|
||||||
envStatusURL = "_P2P_STATUS_URL" |
|
||||||
envNodeConfig = "_P2P_NODE_CONFIG" |
|
||||||
) |
|
||||||
|
|
||||||
// nodeStartupJSON is sent to the simulation host after startup.
|
|
||||||
type nodeStartupJSON struct { |
|
||||||
Err string |
|
||||||
WSEndpoint string |
|
||||||
NodeInfo *p2p.NodeInfo |
|
||||||
} |
|
||||||
|
|
||||||
// SnapshotAPI provides an RPC method to create snapshots of services
|
|
||||||
type SnapshotAPI struct { |
|
||||||
services map[string]node.Lifecycle |
|
||||||
} |
|
||||||
|
|
||||||
func (api SnapshotAPI) Snapshot() (map[string][]byte, error) { |
|
||||||
snapshots := make(map[string][]byte) |
|
||||||
for name, service := range api.services { |
|
||||||
if s, ok := service.(interface { |
|
||||||
Snapshot() ([]byte, error) |
|
||||||
}); ok { |
|
||||||
snap, err := s.Snapshot() |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
snapshots[name] = snap |
|
||||||
} |
|
||||||
} |
|
||||||
return snapshots, nil |
|
||||||
} |
|
||||||
|
|
||||||
type wsRPCDialer struct { |
|
||||||
addrs map[string]string |
|
||||||
} |
|
||||||
|
|
||||||
// DialRPC implements the RPCDialer interface by creating a WebSocket RPC
|
|
||||||
// client of the given node
|
|
||||||
func (w *wsRPCDialer) DialRPC(id enode.ID) (*rpc.Client, error) { |
|
||||||
addr, ok := w.addrs[id.String()] |
|
||||||
if !ok { |
|
||||||
return nil, fmt.Errorf("unknown node: %s", id) |
|
||||||
} |
|
||||||
return rpc.DialWebsocket(context.Background(), addr, "http://localhost") |
|
||||||
} |
|
@ -1,344 +0,0 @@ |
|||||||
// Copyright 2017 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library 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 Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package adapters |
|
||||||
|
|
||||||
import ( |
|
||||||
"context" |
|
||||||
"errors" |
|
||||||
"fmt" |
|
||||||
"maps" |
|
||||||
"math" |
|
||||||
"net" |
|
||||||
"sync" |
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/event" |
|
||||||
"github.com/ethereum/go-ethereum/log" |
|
||||||
"github.com/ethereum/go-ethereum/node" |
|
||||||
"github.com/ethereum/go-ethereum/p2p" |
|
||||||
"github.com/ethereum/go-ethereum/p2p/enode" |
|
||||||
"github.com/ethereum/go-ethereum/p2p/simulations/pipes" |
|
||||||
"github.com/ethereum/go-ethereum/rpc" |
|
||||||
"github.com/gorilla/websocket" |
|
||||||
) |
|
||||||
|
|
||||||
// SimAdapter is a NodeAdapter which creates in-memory simulation nodes and
|
|
||||||
// connects them using net.Pipe
|
|
||||||
type SimAdapter struct { |
|
||||||
pipe func() (net.Conn, net.Conn, error) |
|
||||||
mtx sync.RWMutex |
|
||||||
nodes map[enode.ID]*SimNode |
|
||||||
lifecycles LifecycleConstructors |
|
||||||
} |
|
||||||
|
|
||||||
// NewSimAdapter creates a SimAdapter which is capable of running in-memory
|
|
||||||
// simulation nodes running any of the given services (the services to run on a
|
|
||||||
// particular node are passed to the NewNode function in the NodeConfig)
|
|
||||||
// the adapter uses a net.Pipe for in-memory simulated network connections
|
|
||||||
func NewSimAdapter(services LifecycleConstructors) *SimAdapter { |
|
||||||
return &SimAdapter{ |
|
||||||
pipe: pipes.NetPipe, |
|
||||||
nodes: make(map[enode.ID]*SimNode), |
|
||||||
lifecycles: services, |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// Name returns the name of the adapter for logging purposes
|
|
||||||
func (s *SimAdapter) Name() string { |
|
||||||
return "sim-adapter" |
|
||||||
} |
|
||||||
|
|
||||||
// NewNode returns a new SimNode using the given config
|
|
||||||
func (s *SimAdapter) NewNode(config *NodeConfig) (Node, error) { |
|
||||||
s.mtx.Lock() |
|
||||||
defer s.mtx.Unlock() |
|
||||||
|
|
||||||
id := config.ID |
|
||||||
// verify that the node has a private key in the config
|
|
||||||
if config.PrivateKey == nil { |
|
||||||
return nil, fmt.Errorf("node is missing private key: %s", id) |
|
||||||
} |
|
||||||
|
|
||||||
// check a node with the ID doesn't already exist
|
|
||||||
if _, exists := s.nodes[id]; exists { |
|
||||||
return nil, fmt.Errorf("node already exists: %s", id) |
|
||||||
} |
|
||||||
|
|
||||||
// check the services are valid
|
|
||||||
if len(config.Lifecycles) == 0 { |
|
||||||
return nil, errors.New("node must have at least one service") |
|
||||||
} |
|
||||||
for _, service := range config.Lifecycles { |
|
||||||
if _, exists := s.lifecycles[service]; !exists { |
|
||||||
return nil, fmt.Errorf("unknown node service %q", service) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
err := config.initDummyEnode() |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
n, err := node.New(&node.Config{ |
|
||||||
P2P: p2p.Config{ |
|
||||||
PrivateKey: config.PrivateKey, |
|
||||||
MaxPeers: math.MaxInt32, |
|
||||||
NoDiscovery: true, |
|
||||||
Dialer: s, |
|
||||||
EnableMsgEvents: config.EnableMsgEvents, |
|
||||||
}, |
|
||||||
ExternalSigner: config.ExternalSigner, |
|
||||||
Logger: log.New("node.id", id.String()), |
|
||||||
}) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
simNode := &SimNode{ |
|
||||||
ID: id, |
|
||||||
config: config, |
|
||||||
node: n, |
|
||||||
adapter: s, |
|
||||||
running: make(map[string]node.Lifecycle), |
|
||||||
} |
|
||||||
s.nodes[id] = simNode |
|
||||||
return simNode, nil |
|
||||||
} |
|
||||||
|
|
||||||
// Dial implements the p2p.NodeDialer interface by connecting to the node using
|
|
||||||
// an in-memory net.Pipe
|
|
||||||
func (s *SimAdapter) Dial(ctx context.Context, dest *enode.Node) (conn net.Conn, err error) { |
|
||||||
node, ok := s.GetNode(dest.ID()) |
|
||||||
if !ok { |
|
||||||
return nil, fmt.Errorf("unknown node: %s", dest.ID()) |
|
||||||
} |
|
||||||
srv := node.Server() |
|
||||||
if srv == nil { |
|
||||||
return nil, fmt.Errorf("node not running: %s", dest.ID()) |
|
||||||
} |
|
||||||
// SimAdapter.pipe is net.Pipe (NewSimAdapter)
|
|
||||||
pipe1, pipe2, err := s.pipe() |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
// this is simulated 'listening'
|
|
||||||
// asynchronously call the dialed destination node's p2p server
|
|
||||||
// to set up connection on the 'listening' side
|
|
||||||
go srv.SetupConn(pipe1, 0, nil) |
|
||||||
return pipe2, nil |
|
||||||
} |
|
||||||
|
|
||||||
// DialRPC implements the RPCDialer interface by creating an in-memory RPC
|
|
||||||
// client of the given node
|
|
||||||
func (s *SimAdapter) DialRPC(id enode.ID) (*rpc.Client, error) { |
|
||||||
node, ok := s.GetNode(id) |
|
||||||
if !ok { |
|
||||||
return nil, fmt.Errorf("unknown node: %s", id) |
|
||||||
} |
|
||||||
return node.node.Attach(), nil |
|
||||||
} |
|
||||||
|
|
||||||
// GetNode returns the node with the given ID if it exists
|
|
||||||
func (s *SimAdapter) GetNode(id enode.ID) (*SimNode, bool) { |
|
||||||
s.mtx.RLock() |
|
||||||
defer s.mtx.RUnlock() |
|
||||||
node, ok := s.nodes[id] |
|
||||||
return node, ok |
|
||||||
} |
|
||||||
|
|
||||||
// SimNode is an in-memory simulation node which connects to other nodes using
|
|
||||||
// net.Pipe (see SimAdapter.Dial), running devp2p protocols directly over that
|
|
||||||
// pipe
|
|
||||||
type SimNode struct { |
|
||||||
lock sync.RWMutex |
|
||||||
ID enode.ID |
|
||||||
config *NodeConfig |
|
||||||
adapter *SimAdapter |
|
||||||
node *node.Node |
|
||||||
running map[string]node.Lifecycle |
|
||||||
client *rpc.Client |
|
||||||
registerOnce sync.Once |
|
||||||
} |
|
||||||
|
|
||||||
// Close closes the underlying node.Node to release
|
|
||||||
// acquired resources.
|
|
||||||
func (sn *SimNode) Close() error { |
|
||||||
return sn.node.Close() |
|
||||||
} |
|
||||||
|
|
||||||
// Addr returns the node's discovery address
|
|
||||||
func (sn *SimNode) Addr() []byte { |
|
||||||
return []byte(sn.Node().String()) |
|
||||||
} |
|
||||||
|
|
||||||
// Node returns a node descriptor representing the SimNode
|
|
||||||
func (sn *SimNode) Node() *enode.Node { |
|
||||||
return sn.config.Node() |
|
||||||
} |
|
||||||
|
|
||||||
// Client returns an rpc.Client which can be used to communicate with the
|
|
||||||
// underlying services (it is set once the node has started)
|
|
||||||
func (sn *SimNode) Client() (*rpc.Client, error) { |
|
||||||
sn.lock.RLock() |
|
||||||
defer sn.lock.RUnlock() |
|
||||||
if sn.client == nil { |
|
||||||
return nil, errors.New("node not started") |
|
||||||
} |
|
||||||
return sn.client, nil |
|
||||||
} |
|
||||||
|
|
||||||
// ServeRPC serves RPC requests over the given connection by creating an
|
|
||||||
// in-memory client to the node's RPC server.
|
|
||||||
func (sn *SimNode) ServeRPC(conn *websocket.Conn) error { |
|
||||||
handler, err := sn.node.RPCHandler() |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
codec := rpc.NewFuncCodec(conn, func(v any, _ bool) error { return conn.WriteJSON(v) }, conn.ReadJSON) |
|
||||||
handler.ServeCodec(codec, 0) |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
// Snapshots creates snapshots of the services by calling the
|
|
||||||
// simulation_snapshot RPC method
|
|
||||||
func (sn *SimNode) Snapshots() (map[string][]byte, error) { |
|
||||||
sn.lock.RLock() |
|
||||||
services := maps.Clone(sn.running) |
|
||||||
sn.lock.RUnlock() |
|
||||||
if len(services) == 0 { |
|
||||||
return nil, errors.New("no running services") |
|
||||||
} |
|
||||||
snapshots := make(map[string][]byte) |
|
||||||
for name, service := range services { |
|
||||||
if s, ok := service.(interface { |
|
||||||
Snapshot() ([]byte, error) |
|
||||||
}); ok { |
|
||||||
snap, err := s.Snapshot() |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
snapshots[name] = snap |
|
||||||
} |
|
||||||
} |
|
||||||
return snapshots, nil |
|
||||||
} |
|
||||||
|
|
||||||
// Start registers the services and starts the underlying devp2p node
|
|
||||||
func (sn *SimNode) Start(snapshots map[string][]byte) error { |
|
||||||
// ensure we only register the services once in the case of the node
|
|
||||||
// being stopped and then started again
|
|
||||||
var regErr error |
|
||||||
sn.registerOnce.Do(func() { |
|
||||||
for _, name := range sn.config.Lifecycles { |
|
||||||
ctx := &ServiceContext{ |
|
||||||
RPCDialer: sn.adapter, |
|
||||||
Config: sn.config, |
|
||||||
} |
|
||||||
if snapshots != nil { |
|
||||||
ctx.Snapshot = snapshots[name] |
|
||||||
} |
|
||||||
serviceFunc := sn.adapter.lifecycles[name] |
|
||||||
service, err := serviceFunc(ctx, sn.node) |
|
||||||
if err != nil { |
|
||||||
regErr = err |
|
||||||
break |
|
||||||
} |
|
||||||
// if the service has already been registered, don't register it again.
|
|
||||||
if _, ok := sn.running[name]; ok { |
|
||||||
continue |
|
||||||
} |
|
||||||
sn.running[name] = service |
|
||||||
} |
|
||||||
}) |
|
||||||
if regErr != nil { |
|
||||||
return regErr |
|
||||||
} |
|
||||||
|
|
||||||
if err := sn.node.Start(); err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
// create an in-process RPC client
|
|
||||||
client := sn.node.Attach() |
|
||||||
sn.lock.Lock() |
|
||||||
sn.client = client |
|
||||||
sn.lock.Unlock() |
|
||||||
|
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
// Stop closes the RPC client and stops the underlying devp2p node
|
|
||||||
func (sn *SimNode) Stop() error { |
|
||||||
sn.lock.Lock() |
|
||||||
if sn.client != nil { |
|
||||||
sn.client.Close() |
|
||||||
sn.client = nil |
|
||||||
} |
|
||||||
sn.lock.Unlock() |
|
||||||
return sn.node.Close() |
|
||||||
} |
|
||||||
|
|
||||||
// Service returns a running service by name
|
|
||||||
func (sn *SimNode) Service(name string) node.Lifecycle { |
|
||||||
sn.lock.RLock() |
|
||||||
defer sn.lock.RUnlock() |
|
||||||
return sn.running[name] |
|
||||||
} |
|
||||||
|
|
||||||
// Services returns a copy of the underlying services
|
|
||||||
func (sn *SimNode) Services() []node.Lifecycle { |
|
||||||
sn.lock.RLock() |
|
||||||
defer sn.lock.RUnlock() |
|
||||||
services := make([]node.Lifecycle, 0, len(sn.running)) |
|
||||||
for _, service := range sn.running { |
|
||||||
services = append(services, service) |
|
||||||
} |
|
||||||
return services |
|
||||||
} |
|
||||||
|
|
||||||
// ServiceMap returns a map by names of the underlying services
|
|
||||||
func (sn *SimNode) ServiceMap() map[string]node.Lifecycle { |
|
||||||
sn.lock.RLock() |
|
||||||
defer sn.lock.RUnlock() |
|
||||||
return maps.Clone(sn.running) |
|
||||||
} |
|
||||||
|
|
||||||
// Server returns the underlying p2p.Server
|
|
||||||
func (sn *SimNode) Server() *p2p.Server { |
|
||||||
return sn.node.Server() |
|
||||||
} |
|
||||||
|
|
||||||
// SubscribeEvents subscribes the given channel to peer events from the
|
|
||||||
// underlying p2p.Server
|
|
||||||
func (sn *SimNode) SubscribeEvents(ch chan *p2p.PeerEvent) event.Subscription { |
|
||||||
srv := sn.Server() |
|
||||||
if srv == nil { |
|
||||||
panic("node not running") |
|
||||||
} |
|
||||||
return srv.SubscribeEvents(ch) |
|
||||||
} |
|
||||||
|
|
||||||
// NodeInfo returns information about the node
|
|
||||||
func (sn *SimNode) NodeInfo() *p2p.NodeInfo { |
|
||||||
server := sn.Server() |
|
||||||
if server == nil { |
|
||||||
return &p2p.NodeInfo{ |
|
||||||
ID: sn.ID.String(), |
|
||||||
Enode: sn.Node().String(), |
|
||||||
} |
|
||||||
} |
|
||||||
return server.NodeInfo() |
|
||||||
} |
|
@ -1,202 +0,0 @@ |
|||||||
// Copyright 2018 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library 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 Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package adapters |
|
||||||
|
|
||||||
import ( |
|
||||||
"bytes" |
|
||||||
"encoding/binary" |
|
||||||
"fmt" |
|
||||||
"sync" |
|
||||||
"testing" |
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/simulations/pipes" |
|
||||||
) |
|
||||||
|
|
||||||
func TestTCPPipe(t *testing.T) { |
|
||||||
c1, c2, err := pipes.TCPPipe() |
|
||||||
if err != nil { |
|
||||||
t.Fatal(err) |
|
||||||
} |
|
||||||
|
|
||||||
msgs := 50 |
|
||||||
size := 1024 |
|
||||||
for i := 0; i < msgs; i++ { |
|
||||||
msg := make([]byte, size) |
|
||||||
binary.PutUvarint(msg, uint64(i)) |
|
||||||
if _, err := c1.Write(msg); err != nil { |
|
||||||
t.Fatal(err) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
for i := 0; i < msgs; i++ { |
|
||||||
msg := make([]byte, size) |
|
||||||
binary.PutUvarint(msg, uint64(i)) |
|
||||||
out := make([]byte, size) |
|
||||||
if _, err := c2.Read(out); err != nil { |
|
||||||
t.Fatal(err) |
|
||||||
} |
|
||||||
if !bytes.Equal(msg, out) { |
|
||||||
t.Fatalf("expected %#v, got %#v", msg, out) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func TestTCPPipeBidirections(t *testing.T) { |
|
||||||
c1, c2, err := pipes.TCPPipe() |
|
||||||
if err != nil { |
|
||||||
t.Fatal(err) |
|
||||||
} |
|
||||||
|
|
||||||
msgs := 50 |
|
||||||
size := 7 |
|
||||||
for i := 0; i < msgs; i++ { |
|
||||||
msg := []byte(fmt.Sprintf("ping %02d", i)) |
|
||||||
if _, err := c1.Write(msg); err != nil { |
|
||||||
t.Fatal(err) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
for i := 0; i < msgs; i++ { |
|
||||||
expected := []byte(fmt.Sprintf("ping %02d", i)) |
|
||||||
out := make([]byte, size) |
|
||||||
if _, err := c2.Read(out); err != nil { |
|
||||||
t.Fatal(err) |
|
||||||
} |
|
||||||
|
|
||||||
if !bytes.Equal(expected, out) { |
|
||||||
t.Fatalf("expected %#v, got %#v", expected, out) |
|
||||||
} else { |
|
||||||
msg := []byte(fmt.Sprintf("pong %02d", i)) |
|
||||||
if _, err := c2.Write(msg); err != nil { |
|
||||||
t.Fatal(err) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
for i := 0; i < msgs; i++ { |
|
||||||
expected := []byte(fmt.Sprintf("pong %02d", i)) |
|
||||||
out := make([]byte, size) |
|
||||||
if _, err := c1.Read(out); err != nil { |
|
||||||
t.Fatal(err) |
|
||||||
} |
|
||||||
if !bytes.Equal(expected, out) { |
|
||||||
t.Fatalf("expected %#v, got %#v", expected, out) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func TestNetPipe(t *testing.T) { |
|
||||||
c1, c2, err := pipes.NetPipe() |
|
||||||
if err != nil { |
|
||||||
t.Fatal(err) |
|
||||||
} |
|
||||||
|
|
||||||
msgs := 50 |
|
||||||
size := 1024 |
|
||||||
var wg sync.WaitGroup |
|
||||||
defer wg.Wait() |
|
||||||
|
|
||||||
// netPipe is blocking, so writes are emitted asynchronously
|
|
||||||
wg.Add(1) |
|
||||||
go func() { |
|
||||||
defer wg.Done() |
|
||||||
|
|
||||||
for i := 0; i < msgs; i++ { |
|
||||||
msg := make([]byte, size) |
|
||||||
binary.PutUvarint(msg, uint64(i)) |
|
||||||
if _, err := c1.Write(msg); err != nil { |
|
||||||
t.Error(err) |
|
||||||
} |
|
||||||
} |
|
||||||
}() |
|
||||||
|
|
||||||
for i := 0; i < msgs; i++ { |
|
||||||
msg := make([]byte, size) |
|
||||||
binary.PutUvarint(msg, uint64(i)) |
|
||||||
out := make([]byte, size) |
|
||||||
if _, err := c2.Read(out); err != nil { |
|
||||||
t.Error(err) |
|
||||||
} |
|
||||||
if !bytes.Equal(msg, out) { |
|
||||||
t.Errorf("expected %#v, got %#v", msg, out) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func TestNetPipeBidirections(t *testing.T) { |
|
||||||
c1, c2, err := pipes.NetPipe() |
|
||||||
if err != nil { |
|
||||||
t.Fatal(err) |
|
||||||
} |
|
||||||
|
|
||||||
msgs := 1000 |
|
||||||
size := 8 |
|
||||||
pingTemplate := "ping %03d" |
|
||||||
pongTemplate := "pong %03d" |
|
||||||
var wg sync.WaitGroup |
|
||||||
defer wg.Wait() |
|
||||||
|
|
||||||
// netPipe is blocking, so writes are emitted asynchronously
|
|
||||||
wg.Add(1) |
|
||||||
go func() { |
|
||||||
defer wg.Done() |
|
||||||
|
|
||||||
for i := 0; i < msgs; i++ { |
|
||||||
msg := []byte(fmt.Sprintf(pingTemplate, i)) |
|
||||||
if _, err := c1.Write(msg); err != nil { |
|
||||||
t.Error(err) |
|
||||||
} |
|
||||||
} |
|
||||||
}() |
|
||||||
|
|
||||||
// netPipe is blocking, so reads for pong are emitted asynchronously
|
|
||||||
wg.Add(1) |
|
||||||
go func() { |
|
||||||
defer wg.Done() |
|
||||||
|
|
||||||
for i := 0; i < msgs; i++ { |
|
||||||
expected := []byte(fmt.Sprintf(pongTemplate, i)) |
|
||||||
out := make([]byte, size) |
|
||||||
if _, err := c1.Read(out); err != nil { |
|
||||||
t.Error(err) |
|
||||||
} |
|
||||||
if !bytes.Equal(expected, out) { |
|
||||||
t.Errorf("expected %#v, got %#v", expected, out) |
|
||||||
} |
|
||||||
} |
|
||||||
}() |
|
||||||
|
|
||||||
// expect to read pings, and respond with pongs to the alternate connection
|
|
||||||
for i := 0; i < msgs; i++ { |
|
||||||
expected := []byte(fmt.Sprintf(pingTemplate, i)) |
|
||||||
|
|
||||||
out := make([]byte, size) |
|
||||||
_, err := c2.Read(out) |
|
||||||
if err != nil { |
|
||||||
t.Fatal(err) |
|
||||||
} |
|
||||||
|
|
||||||
if !bytes.Equal(expected, out) { |
|
||||||
t.Errorf("expected %#v, got %#v", expected, out) |
|
||||||
} else { |
|
||||||
msg := []byte(fmt.Sprintf(pongTemplate, i)) |
|
||||||
if _, err := c2.Write(msg); err != nil { |
|
||||||
t.Fatal(err) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
@ -1,325 +0,0 @@ |
|||||||
// Copyright 2017 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library 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 Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package adapters |
|
||||||
|
|
||||||
import ( |
|
||||||
"crypto/ecdsa" |
|
||||||
"encoding/hex" |
|
||||||
"encoding/json" |
|
||||||
"fmt" |
|
||||||
"log/slog" |
|
||||||
"net" |
|
||||||
"os" |
|
||||||
"strconv" |
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/crypto" |
|
||||||
"github.com/ethereum/go-ethereum/internal/reexec" |
|
||||||
"github.com/ethereum/go-ethereum/log" |
|
||||||
"github.com/ethereum/go-ethereum/node" |
|
||||||
"github.com/ethereum/go-ethereum/p2p" |
|
||||||
"github.com/ethereum/go-ethereum/p2p/enode" |
|
||||||
"github.com/ethereum/go-ethereum/p2p/enr" |
|
||||||
"github.com/ethereum/go-ethereum/rpc" |
|
||||||
"github.com/gorilla/websocket" |
|
||||||
) |
|
||||||
|
|
||||||
// Node represents a node in a simulation network which is created by a
|
|
||||||
// NodeAdapter, for example:
|
|
||||||
//
|
|
||||||
// - SimNode, an in-memory node in the same process
|
|
||||||
// - ExecNode, a child process node
|
|
||||||
type Node interface { |
|
||||||
// Addr returns the node's address (e.g. an Enode URL)
|
|
||||||
Addr() []byte |
|
||||||
|
|
||||||
// Client returns the RPC client which is created once the node is
|
|
||||||
// up and running
|
|
||||||
Client() (*rpc.Client, error) |
|
||||||
|
|
||||||
// ServeRPC serves RPC requests over the given connection
|
|
||||||
ServeRPC(*websocket.Conn) error |
|
||||||
|
|
||||||
// Start starts the node with the given snapshots
|
|
||||||
Start(snapshots map[string][]byte) error |
|
||||||
|
|
||||||
// Stop stops the node
|
|
||||||
Stop() error |
|
||||||
|
|
||||||
// NodeInfo returns information about the node
|
|
||||||
NodeInfo() *p2p.NodeInfo |
|
||||||
|
|
||||||
// Snapshots creates snapshots of the running services
|
|
||||||
Snapshots() (map[string][]byte, error) |
|
||||||
} |
|
||||||
|
|
||||||
// NodeAdapter is used to create Nodes in a simulation network
|
|
||||||
type NodeAdapter interface { |
|
||||||
// Name returns the name of the adapter for logging purposes
|
|
||||||
Name() string |
|
||||||
|
|
||||||
// NewNode creates a new node with the given configuration
|
|
||||||
NewNode(config *NodeConfig) (Node, error) |
|
||||||
} |
|
||||||
|
|
||||||
// NodeConfig is the configuration used to start a node in a simulation
|
|
||||||
// network
|
|
||||||
type NodeConfig struct { |
|
||||||
// ID is the node's ID which is used to identify the node in the
|
|
||||||
// simulation network
|
|
||||||
ID enode.ID |
|
||||||
|
|
||||||
// PrivateKey is the node's private key which is used by the devp2p
|
|
||||||
// stack to encrypt communications
|
|
||||||
PrivateKey *ecdsa.PrivateKey |
|
||||||
|
|
||||||
// Enable peer events for Msgs
|
|
||||||
EnableMsgEvents bool |
|
||||||
|
|
||||||
// Name is a human friendly name for the node like "node01"
|
|
||||||
Name string |
|
||||||
|
|
||||||
// Use an existing database instead of a temporary one if non-empty
|
|
||||||
DataDir string |
|
||||||
|
|
||||||
// Lifecycles are the names of the service lifecycles which should be run when
|
|
||||||
// starting the node (for SimNodes it should be the names of service lifecycles
|
|
||||||
// contained in SimAdapter.lifecycles, for other nodes it should be
|
|
||||||
// service lifecycles registered by calling the RegisterLifecycle function)
|
|
||||||
Lifecycles []string |
|
||||||
|
|
||||||
// Properties are the names of the properties this node should hold
|
|
||||||
// within running services (e.g. "bootnode", "lightnode" or any custom values)
|
|
||||||
// These values need to be checked and acted upon by node Services
|
|
||||||
Properties []string |
|
||||||
|
|
||||||
// ExternalSigner specifies an external URI for a clef-type signer
|
|
||||||
ExternalSigner string |
|
||||||
|
|
||||||
// Enode
|
|
||||||
node *enode.Node |
|
||||||
|
|
||||||
// ENR Record with entries to overwrite
|
|
||||||
Record enr.Record |
|
||||||
|
|
||||||
// function to sanction or prevent suggesting a peer
|
|
||||||
Reachable func(id enode.ID) bool |
|
||||||
|
|
||||||
Port uint16 |
|
||||||
|
|
||||||
// LogFile is the log file name of the p2p node at runtime.
|
|
||||||
//
|
|
||||||
// The default value is empty so that the default log writer
|
|
||||||
// is the system standard output.
|
|
||||||
LogFile string |
|
||||||
|
|
||||||
// LogVerbosity is the log verbosity of the p2p node at runtime.
|
|
||||||
//
|
|
||||||
// The default verbosity is INFO.
|
|
||||||
LogVerbosity slog.Level |
|
||||||
} |
|
||||||
|
|
||||||
// nodeConfigJSON is used to encode and decode NodeConfig as JSON by encoding
|
|
||||||
// all fields as strings
|
|
||||||
type nodeConfigJSON struct { |
|
||||||
ID string `json:"id"` |
|
||||||
PrivateKey string `json:"private_key"` |
|
||||||
Name string `json:"name"` |
|
||||||
Lifecycles []string `json:"lifecycles"` |
|
||||||
Properties []string `json:"properties"` |
|
||||||
EnableMsgEvents bool `json:"enable_msg_events"` |
|
||||||
Port uint16 `json:"port"` |
|
||||||
LogFile string `json:"logfile"` |
|
||||||
LogVerbosity int `json:"log_verbosity"` |
|
||||||
} |
|
||||||
|
|
||||||
// MarshalJSON implements the json.Marshaler interface by encoding the config
|
|
||||||
// fields as strings
|
|
||||||
func (n *NodeConfig) MarshalJSON() ([]byte, error) { |
|
||||||
confJSON := nodeConfigJSON{ |
|
||||||
ID: n.ID.String(), |
|
||||||
Name: n.Name, |
|
||||||
Lifecycles: n.Lifecycles, |
|
||||||
Properties: n.Properties, |
|
||||||
Port: n.Port, |
|
||||||
EnableMsgEvents: n.EnableMsgEvents, |
|
||||||
LogFile: n.LogFile, |
|
||||||
LogVerbosity: int(n.LogVerbosity), |
|
||||||
} |
|
||||||
if n.PrivateKey != nil { |
|
||||||
confJSON.PrivateKey = hex.EncodeToString(crypto.FromECDSA(n.PrivateKey)) |
|
||||||
} |
|
||||||
return json.Marshal(confJSON) |
|
||||||
} |
|
||||||
|
|
||||||
// UnmarshalJSON implements the json.Unmarshaler interface by decoding the json
|
|
||||||
// string values into the config fields
|
|
||||||
func (n *NodeConfig) UnmarshalJSON(data []byte) error { |
|
||||||
var confJSON nodeConfigJSON |
|
||||||
if err := json.Unmarshal(data, &confJSON); err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
if confJSON.ID != "" { |
|
||||||
if err := n.ID.UnmarshalText([]byte(confJSON.ID)); err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
if confJSON.PrivateKey != "" { |
|
||||||
key, err := hex.DecodeString(confJSON.PrivateKey) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
privKey, err := crypto.ToECDSA(key) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
n.PrivateKey = privKey |
|
||||||
} |
|
||||||
|
|
||||||
n.Name = confJSON.Name |
|
||||||
n.Lifecycles = confJSON.Lifecycles |
|
||||||
n.Properties = confJSON.Properties |
|
||||||
n.Port = confJSON.Port |
|
||||||
n.EnableMsgEvents = confJSON.EnableMsgEvents |
|
||||||
n.LogFile = confJSON.LogFile |
|
||||||
n.LogVerbosity = slog.Level(confJSON.LogVerbosity) |
|
||||||
|
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
// Node returns the node descriptor represented by the config.
|
|
||||||
func (n *NodeConfig) Node() *enode.Node { |
|
||||||
return n.node |
|
||||||
} |
|
||||||
|
|
||||||
// RandomNodeConfig returns node configuration with a randomly generated ID and
|
|
||||||
// PrivateKey
|
|
||||||
func RandomNodeConfig() *NodeConfig { |
|
||||||
prvkey, err := crypto.GenerateKey() |
|
||||||
if err != nil { |
|
||||||
panic("unable to generate key") |
|
||||||
} |
|
||||||
|
|
||||||
port, err := assignTCPPort() |
|
||||||
if err != nil { |
|
||||||
panic("unable to assign tcp port") |
|
||||||
} |
|
||||||
|
|
||||||
enodId := enode.PubkeyToIDV4(&prvkey.PublicKey) |
|
||||||
return &NodeConfig{ |
|
||||||
PrivateKey: prvkey, |
|
||||||
ID: enodId, |
|
||||||
Name: fmt.Sprintf("node_%s", enodId.String()), |
|
||||||
Port: port, |
|
||||||
EnableMsgEvents: true, |
|
||||||
LogVerbosity: log.LvlInfo, |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func assignTCPPort() (uint16, error) { |
|
||||||
l, err := net.Listen("tcp", "127.0.0.1:0") |
|
||||||
if err != nil { |
|
||||||
return 0, err |
|
||||||
} |
|
||||||
l.Close() |
|
||||||
_, port, err := net.SplitHostPort(l.Addr().String()) |
|
||||||
if err != nil { |
|
||||||
return 0, err |
|
||||||
} |
|
||||||
p, err := strconv.ParseUint(port, 10, 16) |
|
||||||
if err != nil { |
|
||||||
return 0, err |
|
||||||
} |
|
||||||
return uint16(p), nil |
|
||||||
} |
|
||||||
|
|
||||||
// ServiceContext is a collection of options and methods which can be utilised
|
|
||||||
// when starting services
|
|
||||||
type ServiceContext struct { |
|
||||||
RPCDialer |
|
||||||
|
|
||||||
Config *NodeConfig |
|
||||||
Snapshot []byte |
|
||||||
} |
|
||||||
|
|
||||||
// RPCDialer is used when initialising services which need to connect to
|
|
||||||
// other nodes in the network (for example a simulated Swarm node which needs
|
|
||||||
// to connect to a Geth node to resolve ENS names)
|
|
||||||
type RPCDialer interface { |
|
||||||
DialRPC(id enode.ID) (*rpc.Client, error) |
|
||||||
} |
|
||||||
|
|
||||||
// LifecycleConstructor allows a Lifecycle to be constructed during node start-up.
|
|
||||||
// While the service-specific package usually takes care of Lifecycle creation and registration,
|
|
||||||
// for testing purposes, it is useful to be able to construct a Lifecycle on spot.
|
|
||||||
type LifecycleConstructor func(ctx *ServiceContext, stack *node.Node) (node.Lifecycle, error) |
|
||||||
|
|
||||||
// LifecycleConstructors stores LifecycleConstructor functions to call during node start-up.
|
|
||||||
type LifecycleConstructors map[string]LifecycleConstructor |
|
||||||
|
|
||||||
// lifecycleConstructorFuncs is a map of registered services which are used to boot devp2p
|
|
||||||
// nodes
|
|
||||||
var lifecycleConstructorFuncs = make(LifecycleConstructors) |
|
||||||
|
|
||||||
// RegisterLifecycles registers the given Services which can then be used to
|
|
||||||
// start devp2p nodes using either the Exec or Docker adapters.
|
|
||||||
//
|
|
||||||
// It should be called in an init function so that it has the opportunity to
|
|
||||||
// execute the services before main() is called.
|
|
||||||
func RegisterLifecycles(lifecycles LifecycleConstructors) { |
|
||||||
for name, f := range lifecycles { |
|
||||||
if _, exists := lifecycleConstructorFuncs[name]; exists { |
|
||||||
panic(fmt.Sprintf("node service already exists: %q", name)) |
|
||||||
} |
|
||||||
lifecycleConstructorFuncs[name] = f |
|
||||||
} |
|
||||||
|
|
||||||
// now we have registered the services, run reexec.Init() which will
|
|
||||||
// potentially start one of the services if the current binary has
|
|
||||||
// been exec'd with argv[0] set to "p2p-node"
|
|
||||||
if reexec.Init() { |
|
||||||
os.Exit(0) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// adds the host part to the configuration's ENR, signs it
|
|
||||||
// creates and adds the corresponding enode object to the configuration
|
|
||||||
func (n *NodeConfig) initEnode(ip net.IP, tcpport int, udpport int) error { |
|
||||||
enrIp := enr.IP(ip) |
|
||||||
n.Record.Set(&enrIp) |
|
||||||
enrTcpPort := enr.TCP(tcpport) |
|
||||||
n.Record.Set(&enrTcpPort) |
|
||||||
enrUdpPort := enr.UDP(udpport) |
|
||||||
n.Record.Set(&enrUdpPort) |
|
||||||
|
|
||||||
err := enode.SignV4(&n.Record, n.PrivateKey) |
|
||||||
if err != nil { |
|
||||||
return fmt.Errorf("unable to generate ENR: %v", err) |
|
||||||
} |
|
||||||
nod, err := enode.New(enode.V4ID{}, &n.Record) |
|
||||||
if err != nil { |
|
||||||
return fmt.Errorf("unable to create enode: %v", err) |
|
||||||
} |
|
||||||
log.Trace("simnode new", "record", n.Record) |
|
||||||
n.node = nod |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
func (n *NodeConfig) initDummyEnode() error { |
|
||||||
return n.initEnode(net.IPv4(127, 0, 0, 1), int(n.Port), 0) |
|
||||||
} |
|
@ -1,153 +0,0 @@ |
|||||||
// Copyright 2018 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library 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 Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package simulations |
|
||||||
|
|
||||||
import ( |
|
||||||
"errors" |
|
||||||
"strings" |
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/enode" |
|
||||||
) |
|
||||||
|
|
||||||
var ( |
|
||||||
ErrNodeNotFound = errors.New("node not found") |
|
||||||
) |
|
||||||
|
|
||||||
// ConnectToLastNode connects the node with provided NodeID
|
|
||||||
// to the last node that is up, and avoiding connection to self.
|
|
||||||
// It is useful when constructing a chain network topology
|
|
||||||
// when Network adds and removes nodes dynamically.
|
|
||||||
func (net *Network) ConnectToLastNode(id enode.ID) (err error) { |
|
||||||
net.lock.Lock() |
|
||||||
defer net.lock.Unlock() |
|
||||||
|
|
||||||
ids := net.getUpNodeIDs() |
|
||||||
l := len(ids) |
|
||||||
if l < 2 { |
|
||||||
return nil |
|
||||||
} |
|
||||||
last := ids[l-1] |
|
||||||
if last == id { |
|
||||||
last = ids[l-2] |
|
||||||
} |
|
||||||
return net.connectNotConnected(last, id) |
|
||||||
} |
|
||||||
|
|
||||||
// ConnectToRandomNode connects the node with provided NodeID
|
|
||||||
// to a random node that is up.
|
|
||||||
func (net *Network) ConnectToRandomNode(id enode.ID) (err error) { |
|
||||||
net.lock.Lock() |
|
||||||
defer net.lock.Unlock() |
|
||||||
|
|
||||||
selected := net.getRandomUpNode(id) |
|
||||||
if selected == nil { |
|
||||||
return ErrNodeNotFound |
|
||||||
} |
|
||||||
return net.connectNotConnected(selected.ID(), id) |
|
||||||
} |
|
||||||
|
|
||||||
// ConnectNodesFull connects all nodes one to another.
|
|
||||||
// It provides a complete connectivity in the network
|
|
||||||
// which should be rarely needed.
|
|
||||||
func (net *Network) ConnectNodesFull(ids []enode.ID) (err error) { |
|
||||||
net.lock.Lock() |
|
||||||
defer net.lock.Unlock() |
|
||||||
|
|
||||||
if ids == nil { |
|
||||||
ids = net.getUpNodeIDs() |
|
||||||
} |
|
||||||
for i, lid := range ids { |
|
||||||
for _, rid := range ids[i+1:] { |
|
||||||
if err = net.connectNotConnected(lid, rid); err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
// ConnectNodesChain connects all nodes in a chain topology.
|
|
||||||
// If ids argument is nil, all nodes that are up will be connected.
|
|
||||||
func (net *Network) ConnectNodesChain(ids []enode.ID) (err error) { |
|
||||||
net.lock.Lock() |
|
||||||
defer net.lock.Unlock() |
|
||||||
|
|
||||||
return net.connectNodesChain(ids) |
|
||||||
} |
|
||||||
|
|
||||||
func (net *Network) connectNodesChain(ids []enode.ID) (err error) { |
|
||||||
if ids == nil { |
|
||||||
ids = net.getUpNodeIDs() |
|
||||||
} |
|
||||||
l := len(ids) |
|
||||||
for i := 0; i < l-1; i++ { |
|
||||||
if err := net.connectNotConnected(ids[i], ids[i+1]); err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
} |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
// ConnectNodesRing connects all nodes in a ring topology.
|
|
||||||
// If ids argument is nil, all nodes that are up will be connected.
|
|
||||||
func (net *Network) ConnectNodesRing(ids []enode.ID) (err error) { |
|
||||||
net.lock.Lock() |
|
||||||
defer net.lock.Unlock() |
|
||||||
|
|
||||||
if ids == nil { |
|
||||||
ids = net.getUpNodeIDs() |
|
||||||
} |
|
||||||
l := len(ids) |
|
||||||
if l < 2 { |
|
||||||
return nil |
|
||||||
} |
|
||||||
if err := net.connectNodesChain(ids); err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
return net.connectNotConnected(ids[l-1], ids[0]) |
|
||||||
} |
|
||||||
|
|
||||||
// ConnectNodesStar connects all nodes into a star topology
|
|
||||||
// If ids argument is nil, all nodes that are up will be connected.
|
|
||||||
func (net *Network) ConnectNodesStar(ids []enode.ID, center enode.ID) (err error) { |
|
||||||
net.lock.Lock() |
|
||||||
defer net.lock.Unlock() |
|
||||||
|
|
||||||
if ids == nil { |
|
||||||
ids = net.getUpNodeIDs() |
|
||||||
} |
|
||||||
for _, id := range ids { |
|
||||||
if center == id { |
|
||||||
continue |
|
||||||
} |
|
||||||
if err := net.connectNotConnected(center, id); err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
} |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
func (net *Network) connectNotConnected(oneID, otherID enode.ID) error { |
|
||||||
return ignoreAlreadyConnectedErr(net.connect(oneID, otherID)) |
|
||||||
} |
|
||||||
|
|
||||||
func ignoreAlreadyConnectedErr(err error) error { |
|
||||||
if err == nil || strings.Contains(err.Error(), "already connected") { |
|
||||||
return nil |
|
||||||
} |
|
||||||
return err |
|
||||||
} |
|
@ -1,172 +0,0 @@ |
|||||||
// Copyright 2018 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library 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 Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package simulations |
|
||||||
|
|
||||||
import ( |
|
||||||
"testing" |
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/node" |
|
||||||
"github.com/ethereum/go-ethereum/p2p/enode" |
|
||||||
"github.com/ethereum/go-ethereum/p2p/simulations/adapters" |
|
||||||
) |
|
||||||
|
|
||||||
func newTestNetwork(t *testing.T, nodeCount int) (*Network, []enode.ID) { |
|
||||||
t.Helper() |
|
||||||
adapter := adapters.NewSimAdapter(adapters.LifecycleConstructors{ |
|
||||||
"noopwoop": func(ctx *adapters.ServiceContext, stack *node.Node) (node.Lifecycle, error) { |
|
||||||
return NewNoopService(nil), nil |
|
||||||
}, |
|
||||||
}) |
|
||||||
|
|
||||||
// create network
|
|
||||||
network := NewNetwork(adapter, &NetworkConfig{ |
|
||||||
DefaultService: "noopwoop", |
|
||||||
}) |
|
||||||
|
|
||||||
// create and start nodes
|
|
||||||
ids := make([]enode.ID, nodeCount) |
|
||||||
for i := range ids { |
|
||||||
conf := adapters.RandomNodeConfig() |
|
||||||
node, err := network.NewNodeWithConfig(conf) |
|
||||||
if err != nil { |
|
||||||
t.Fatalf("error creating node: %s", err) |
|
||||||
} |
|
||||||
if err := network.Start(node.ID()); err != nil { |
|
||||||
t.Fatalf("error starting node: %s", err) |
|
||||||
} |
|
||||||
ids[i] = node.ID() |
|
||||||
} |
|
||||||
|
|
||||||
if len(network.Conns) > 0 { |
|
||||||
t.Fatal("no connections should exist after just adding nodes") |
|
||||||
} |
|
||||||
|
|
||||||
return network, ids |
|
||||||
} |
|
||||||
|
|
||||||
func TestConnectToLastNode(t *testing.T) { |
|
||||||
net, ids := newTestNetwork(t, 10) |
|
||||||
defer net.Shutdown() |
|
||||||
|
|
||||||
first := ids[0] |
|
||||||
if err := net.ConnectToLastNode(first); err != nil { |
|
||||||
t.Fatal(err) |
|
||||||
} |
|
||||||
|
|
||||||
last := ids[len(ids)-1] |
|
||||||
for i, id := range ids { |
|
||||||
if id == first || id == last { |
|
||||||
continue |
|
||||||
} |
|
||||||
|
|
||||||
if net.GetConn(first, id) != nil { |
|
||||||
t.Errorf("connection must not exist with node(ind: %v, id: %v)", i, id) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
if net.GetConn(first, last) == nil { |
|
||||||
t.Error("first and last node must be connected") |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func TestConnectToRandomNode(t *testing.T) { |
|
||||||
net, ids := newTestNetwork(t, 10) |
|
||||||
defer net.Shutdown() |
|
||||||
|
|
||||||
err := net.ConnectToRandomNode(ids[0]) |
|
||||||
if err != nil { |
|
||||||
t.Fatal(err) |
|
||||||
} |
|
||||||
|
|
||||||
var cc int |
|
||||||
for i, a := range ids { |
|
||||||
for _, b := range ids[i:] { |
|
||||||
if net.GetConn(a, b) != nil { |
|
||||||
cc++ |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
if cc != 1 { |
|
||||||
t.Errorf("expected one connection, got %v", cc) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func TestConnectNodesFull(t *testing.T) { |
|
||||||
tests := []struct { |
|
||||||
name string |
|
||||||
nodeCount int |
|
||||||
}{ |
|
||||||
{name: "no node", nodeCount: 0}, |
|
||||||
{name: "single node", nodeCount: 1}, |
|
||||||
{name: "2 nodes", nodeCount: 2}, |
|
||||||
{name: "3 nodes", nodeCount: 3}, |
|
||||||
{name: "even number of nodes", nodeCount: 12}, |
|
||||||
{name: "odd number of nodes", nodeCount: 13}, |
|
||||||
} |
|
||||||
for _, test := range tests { |
|
||||||
t.Run(test.name, func(t *testing.T) { |
|
||||||
net, ids := newTestNetwork(t, test.nodeCount) |
|
||||||
defer net.Shutdown() |
|
||||||
|
|
||||||
err := net.ConnectNodesFull(ids) |
|
||||||
if err != nil { |
|
||||||
t.Fatal(err) |
|
||||||
} |
|
||||||
|
|
||||||
VerifyFull(t, net, ids) |
|
||||||
}) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func TestConnectNodesChain(t *testing.T) { |
|
||||||
net, ids := newTestNetwork(t, 10) |
|
||||||
defer net.Shutdown() |
|
||||||
|
|
||||||
err := net.ConnectNodesChain(ids) |
|
||||||
if err != nil { |
|
||||||
t.Fatal(err) |
|
||||||
} |
|
||||||
|
|
||||||
VerifyChain(t, net, ids) |
|
||||||
} |
|
||||||
|
|
||||||
func TestConnectNodesRing(t *testing.T) { |
|
||||||
net, ids := newTestNetwork(t, 10) |
|
||||||
defer net.Shutdown() |
|
||||||
|
|
||||||
err := net.ConnectNodesRing(ids) |
|
||||||
if err != nil { |
|
||||||
t.Fatal(err) |
|
||||||
} |
|
||||||
|
|
||||||
VerifyRing(t, net, ids) |
|
||||||
} |
|
||||||
|
|
||||||
func TestConnectNodesStar(t *testing.T) { |
|
||||||
net, ids := newTestNetwork(t, 10) |
|
||||||
defer net.Shutdown() |
|
||||||
|
|
||||||
pivotIndex := 2 |
|
||||||
|
|
||||||
err := net.ConnectNodesStar(ids, ids[pivotIndex]) |
|
||||||
if err != nil { |
|
||||||
t.Fatal(err) |
|
||||||
} |
|
||||||
|
|
||||||
VerifyStar(t, net, ids, pivotIndex) |
|
||||||
} |
|
@ -1,110 +0,0 @@ |
|||||||
// Copyright 2017 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library 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 Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package simulations |
|
||||||
|
|
||||||
import ( |
|
||||||
"fmt" |
|
||||||
"time" |
|
||||||
) |
|
||||||
|
|
||||||
// EventType is the type of event emitted by a simulation network
|
|
||||||
type EventType string |
|
||||||
|
|
||||||
const ( |
|
||||||
// EventTypeNode is the type of event emitted when a node is either
|
|
||||||
// created, started or stopped
|
|
||||||
EventTypeNode EventType = "node" |
|
||||||
|
|
||||||
// EventTypeConn is the type of event emitted when a connection is
|
|
||||||
// either established or dropped between two nodes
|
|
||||||
EventTypeConn EventType = "conn" |
|
||||||
|
|
||||||
// EventTypeMsg is the type of event emitted when a p2p message it
|
|
||||||
// sent between two nodes
|
|
||||||
EventTypeMsg EventType = "msg" |
|
||||||
) |
|
||||||
|
|
||||||
// Event is an event emitted by a simulation network
|
|
||||||
type Event struct { |
|
||||||
// Type is the type of the event
|
|
||||||
Type EventType `json:"type"` |
|
||||||
|
|
||||||
// Time is the time the event happened
|
|
||||||
Time time.Time `json:"time"` |
|
||||||
|
|
||||||
// Control indicates whether the event is the result of a controlled
|
|
||||||
// action in the network
|
|
||||||
Control bool `json:"control"` |
|
||||||
|
|
||||||
// Node is set if the type is EventTypeNode
|
|
||||||
Node *Node `json:"node,omitempty"` |
|
||||||
|
|
||||||
// Conn is set if the type is EventTypeConn
|
|
||||||
Conn *Conn `json:"conn,omitempty"` |
|
||||||
|
|
||||||
// Msg is set if the type is EventTypeMsg
|
|
||||||
Msg *Msg `json:"msg,omitempty"` |
|
||||||
|
|
||||||
//Optionally provide data (currently for simulation frontends only)
|
|
||||||
Data interface{} `json:"data"` |
|
||||||
} |
|
||||||
|
|
||||||
// NewEvent creates a new event for the given object which should be either a
|
|
||||||
// Node, Conn or Msg.
|
|
||||||
//
|
|
||||||
// The object is copied so that the event represents the state of the object
|
|
||||||
// when NewEvent is called.
|
|
||||||
func NewEvent(v interface{}) *Event { |
|
||||||
event := &Event{Time: time.Now()} |
|
||||||
switch v := v.(type) { |
|
||||||
case *Node: |
|
||||||
event.Type = EventTypeNode |
|
||||||
event.Node = v.copy() |
|
||||||
case *Conn: |
|
||||||
event.Type = EventTypeConn |
|
||||||
conn := *v |
|
||||||
event.Conn = &conn |
|
||||||
case *Msg: |
|
||||||
event.Type = EventTypeMsg |
|
||||||
msg := *v |
|
||||||
event.Msg = &msg |
|
||||||
default: |
|
||||||
panic(fmt.Sprintf("invalid event type: %T", v)) |
|
||||||
} |
|
||||||
return event |
|
||||||
} |
|
||||||
|
|
||||||
// ControlEvent creates a new control event
|
|
||||||
func ControlEvent(v interface{}) *Event { |
|
||||||
event := NewEvent(v) |
|
||||||
event.Control = true |
|
||||||
return event |
|
||||||
} |
|
||||||
|
|
||||||
// String returns the string representation of the event
|
|
||||||
func (e *Event) String() string { |
|
||||||
switch e.Type { |
|
||||||
case EventTypeNode: |
|
||||||
return fmt.Sprintf("<node-event> id: %s up: %t", e.Node.ID().TerminalString(), e.Node.Up()) |
|
||||||
case EventTypeConn: |
|
||||||
return fmt.Sprintf("<conn-event> nodes: %s->%s up: %t", e.Conn.One.TerminalString(), e.Conn.Other.TerminalString(), e.Conn.Up) |
|
||||||
case EventTypeMsg: |
|
||||||
return fmt.Sprintf("<msg-event> nodes: %s->%s proto: %s, code: %d, received: %t", e.Msg.One.TerminalString(), e.Msg.Other.TerminalString(), e.Msg.Protocol, e.Msg.Code, e.Msg.Received) |
|
||||||
default: |
|
||||||
return "" |
|
||||||
} |
|
||||||
} |
|
@ -1,39 +0,0 @@ |
|||||||
# devp2p simulation examples |
|
||||||
|
|
||||||
## ping-pong |
|
||||||
|
|
||||||
`ping-pong.go` implements a simulation network which contains nodes running a |
|
||||||
simple "ping-pong" protocol where nodes send a ping message to all their |
|
||||||
connected peers every 10s and receive pong messages in return. |
|
||||||
|
|
||||||
To run the simulation, run `go run ping-pong.go` in one terminal to start the |
|
||||||
simulation API and `./ping-pong.sh` in another to start and connect the nodes: |
|
||||||
|
|
||||||
``` |
|
||||||
$ go run ping-pong.go |
|
||||||
INFO [08-15|13:53:49] using sim adapter |
|
||||||
INFO [08-15|13:53:49] starting simulation server on 0.0.0.0:8888... |
|
||||||
``` |
|
||||||
|
|
||||||
``` |
|
||||||
$ ./ping-pong.sh |
|
||||||
---> 13:58:12 creating 10 nodes |
|
||||||
Created node01 |
|
||||||
Started node01 |
|
||||||
... |
|
||||||
Created node10 |
|
||||||
Started node10 |
|
||||||
---> 13:58:13 connecting node01 to all other nodes |
|
||||||
Connected node01 to node02 |
|
||||||
... |
|
||||||
Connected node01 to node10 |
|
||||||
---> 13:58:14 done |
|
||||||
``` |
|
||||||
|
|
||||||
Use the `--adapter` flag to choose the adapter type: |
|
||||||
|
|
||||||
``` |
|
||||||
$ go run ping-pong.go --adapter exec |
|
||||||
INFO [08-15|14:01:14] using exec adapter tmpdir=/var/folders/k6/wpsgfg4n23ddbc6f5cnw5qg00000gn/T/p2p-example992833779 |
|
||||||
INFO [08-15|14:01:14] starting simulation server on 0.0.0.0:8888... |
|
||||||
``` |
|
@ -1,173 +0,0 @@ |
|||||||
// Copyright 2017 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library 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 Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package main |
|
||||||
|
|
||||||
import ( |
|
||||||
"flag" |
|
||||||
"fmt" |
|
||||||
"io" |
|
||||||
"net/http" |
|
||||||
"os" |
|
||||||
"sync/atomic" |
|
||||||
"time" |
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/log" |
|
||||||
"github.com/ethereum/go-ethereum/node" |
|
||||||
"github.com/ethereum/go-ethereum/p2p" |
|
||||||
"github.com/ethereum/go-ethereum/p2p/enode" |
|
||||||
"github.com/ethereum/go-ethereum/p2p/simulations" |
|
||||||
"github.com/ethereum/go-ethereum/p2p/simulations/adapters" |
|
||||||
) |
|
||||||
|
|
||||||
var adapterType = flag.String("adapter", "sim", `node adapter to use (one of "sim" or "exec")`) |
|
||||||
|
|
||||||
// main() starts a simulation network which contains nodes running a simple
|
|
||||||
// ping-pong protocol
|
|
||||||
func main() { |
|
||||||
flag.Parse() |
|
||||||
|
|
||||||
// set the log level to Trace
|
|
||||||
log.SetDefault(log.NewLogger(log.NewTerminalHandlerWithLevel(os.Stderr, log.LevelTrace, false))) |
|
||||||
|
|
||||||
// register a single ping-pong service
|
|
||||||
services := map[string]adapters.LifecycleConstructor{ |
|
||||||
"ping-pong": func(ctx *adapters.ServiceContext, stack *node.Node) (node.Lifecycle, error) { |
|
||||||
pps := newPingPongService(ctx.Config.ID) |
|
||||||
stack.RegisterProtocols(pps.Protocols()) |
|
||||||
return pps, nil |
|
||||||
}, |
|
||||||
} |
|
||||||
adapters.RegisterLifecycles(services) |
|
||||||
|
|
||||||
// create the NodeAdapter
|
|
||||||
var adapter adapters.NodeAdapter |
|
||||||
|
|
||||||
switch *adapterType { |
|
||||||
|
|
||||||
case "sim": |
|
||||||
log.Info("using sim adapter") |
|
||||||
adapter = adapters.NewSimAdapter(services) |
|
||||||
|
|
||||||
case "exec": |
|
||||||
tmpdir, err := os.MkdirTemp("", "p2p-example") |
|
||||||
if err != nil { |
|
||||||
log.Crit("error creating temp dir", "err", err) |
|
||||||
} |
|
||||||
defer os.RemoveAll(tmpdir) |
|
||||||
log.Info("using exec adapter", "tmpdir", tmpdir) |
|
||||||
adapter = adapters.NewExecAdapter(tmpdir) |
|
||||||
|
|
||||||
default: |
|
||||||
log.Crit(fmt.Sprintf("unknown node adapter %q", *adapterType)) |
|
||||||
} |
|
||||||
|
|
||||||
// start the HTTP API
|
|
||||||
log.Info("starting simulation server on 0.0.0.0:8888...") |
|
||||||
network := simulations.NewNetwork(adapter, &simulations.NetworkConfig{ |
|
||||||
DefaultService: "ping-pong", |
|
||||||
}) |
|
||||||
if err := http.ListenAndServe(":8888", simulations.NewServer(network)); err != nil { |
|
||||||
log.Crit("error starting simulation server", "err", err) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// pingPongService runs a ping-pong protocol between nodes where each node
|
|
||||||
// sends a ping to all its connected peers every 10s and receives a pong in
|
|
||||||
// return
|
|
||||||
type pingPongService struct { |
|
||||||
id enode.ID |
|
||||||
log log.Logger |
|
||||||
received atomic.Int64 |
|
||||||
} |
|
||||||
|
|
||||||
func newPingPongService(id enode.ID) *pingPongService { |
|
||||||
return &pingPongService{ |
|
||||||
id: id, |
|
||||||
log: log.New("node.id", id), |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func (p *pingPongService) Protocols() []p2p.Protocol { |
|
||||||
return []p2p.Protocol{{ |
|
||||||
Name: "ping-pong", |
|
||||||
Version: 1, |
|
||||||
Length: 2, |
|
||||||
Run: p.Run, |
|
||||||
NodeInfo: p.Info, |
|
||||||
}} |
|
||||||
} |
|
||||||
|
|
||||||
func (p *pingPongService) Start() error { |
|
||||||
p.log.Info("ping-pong service starting") |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
func (p *pingPongService) Stop() error { |
|
||||||
p.log.Info("ping-pong service stopping") |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
func (p *pingPongService) Info() interface{} { |
|
||||||
return struct { |
|
||||||
Received int64 `json:"received"` |
|
||||||
}{ |
|
||||||
p.received.Load(), |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
const ( |
|
||||||
pingMsgCode = iota |
|
||||||
pongMsgCode |
|
||||||
) |
|
||||||
|
|
||||||
// Run implements the ping-pong protocol which sends ping messages to the peer
|
|
||||||
// at 10s intervals, and responds to pings with pong messages.
|
|
||||||
func (p *pingPongService) Run(peer *p2p.Peer, rw p2p.MsgReadWriter) error { |
|
||||||
log := p.log.New("peer.id", peer.ID()) |
|
||||||
|
|
||||||
errC := make(chan error, 1) |
|
||||||
go func() { |
|
||||||
for range time.Tick(10 * time.Second) { |
|
||||||
log.Info("sending ping") |
|
||||||
if err := p2p.Send(rw, pingMsgCode, "PING"); err != nil { |
|
||||||
errC <- err |
|
||||||
return |
|
||||||
} |
|
||||||
} |
|
||||||
}() |
|
||||||
go func() { |
|
||||||
for { |
|
||||||
msg, err := rw.ReadMsg() |
|
||||||
if err != nil { |
|
||||||
errC <- err |
|
||||||
return |
|
||||||
} |
|
||||||
payload, err := io.ReadAll(msg.Payload) |
|
||||||
if err != nil { |
|
||||||
errC <- err |
|
||||||
return |
|
||||||
} |
|
||||||
log.Info("received message", "msg.code", msg.Code, "msg.payload", string(payload)) |
|
||||||
p.received.Add(1) |
|
||||||
if msg.Code == pingMsgCode { |
|
||||||
log.Info("sending pong") |
|
||||||
go p2p.Send(rw, pongMsgCode, "PONG") |
|
||||||
} |
|
||||||
} |
|
||||||
}() |
|
||||||
return <-errC |
|
||||||
} |
|
@ -1,40 +0,0 @@ |
|||||||
#!/bin/bash |
|
||||||
# |
|
||||||
# Boot a ping-pong network simulation using the HTTP API started by ping-pong.go |
|
||||||
|
|
||||||
set -e |
|
||||||
|
|
||||||
main() { |
|
||||||
if ! which p2psim &>/dev/null; then |
|
||||||
fail "missing p2psim binary (you need to build cmd/p2psim and put it in \$PATH)" |
|
||||||
fi |
|
||||||
|
|
||||||
info "creating 10 nodes" |
|
||||||
for i in $(seq 1 10); do |
|
||||||
p2psim node create --name "$(node_name $i)" |
|
||||||
p2psim node start "$(node_name $i)" |
|
||||||
done |
|
||||||
|
|
||||||
info "connecting node01 to all other nodes" |
|
||||||
for i in $(seq 2 10); do |
|
||||||
p2psim node connect "node01" "$(node_name $i)" |
|
||||||
done |
|
||||||
|
|
||||||
info "done" |
|
||||||
} |
|
||||||
|
|
||||||
node_name() { |
|
||||||
local num=$1 |
|
||||||
echo "node$(printf '%02d' $num)" |
|
||||||
} |
|
||||||
|
|
||||||
info() { |
|
||||||
echo -e "\033[1;32m---> $(date +%H:%M:%S) ${@}\033[0m" |
|
||||||
} |
|
||||||
|
|
||||||
fail() { |
|
||||||
echo -e "\033[1;31mERROR: ${@}\033[0m" >&2 |
|
||||||
exit 1 |
|
||||||
} |
|
||||||
|
|
||||||
main "$@" |
|
@ -1,743 +0,0 @@ |
|||||||
// Copyright 2017 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library 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 Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package simulations |
|
||||||
|
|
||||||
import ( |
|
||||||
"bufio" |
|
||||||
"bytes" |
|
||||||
"context" |
|
||||||
"encoding/json" |
|
||||||
"errors" |
|
||||||
"fmt" |
|
||||||
"html" |
|
||||||
"io" |
|
||||||
"net/http" |
|
||||||
"strconv" |
|
||||||
"strings" |
|
||||||
"sync" |
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/event" |
|
||||||
"github.com/ethereum/go-ethereum/p2p" |
|
||||||
"github.com/ethereum/go-ethereum/p2p/enode" |
|
||||||
"github.com/ethereum/go-ethereum/p2p/simulations/adapters" |
|
||||||
"github.com/ethereum/go-ethereum/rpc" |
|
||||||
"github.com/gorilla/websocket" |
|
||||||
"github.com/julienschmidt/httprouter" |
|
||||||
) |
|
||||||
|
|
||||||
// DefaultClient is the default simulation API client which expects the API
|
|
||||||
// to be running at http://localhost:8888
|
|
||||||
var DefaultClient = NewClient("http://localhost:8888") |
|
||||||
|
|
||||||
// Client is a client for the simulation HTTP API which supports creating
|
|
||||||
// and managing simulation networks
|
|
||||||
type Client struct { |
|
||||||
URL string |
|
||||||
|
|
||||||
client *http.Client |
|
||||||
} |
|
||||||
|
|
||||||
// NewClient returns a new simulation API client
|
|
||||||
func NewClient(url string) *Client { |
|
||||||
return &Client{ |
|
||||||
URL: url, |
|
||||||
client: http.DefaultClient, |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// GetNetwork returns details of the network
|
|
||||||
func (c *Client) GetNetwork() (*Network, error) { |
|
||||||
network := &Network{} |
|
||||||
return network, c.Get("/", network) |
|
||||||
} |
|
||||||
|
|
||||||
// StartNetwork starts all existing nodes in the simulation network
|
|
||||||
func (c *Client) StartNetwork() error { |
|
||||||
return c.Post("/start", nil, nil) |
|
||||||
} |
|
||||||
|
|
||||||
// StopNetwork stops all existing nodes in a simulation network
|
|
||||||
func (c *Client) StopNetwork() error { |
|
||||||
return c.Post("/stop", nil, nil) |
|
||||||
} |
|
||||||
|
|
||||||
// CreateSnapshot creates a network snapshot
|
|
||||||
func (c *Client) CreateSnapshot() (*Snapshot, error) { |
|
||||||
snap := &Snapshot{} |
|
||||||
return snap, c.Get("/snapshot", snap) |
|
||||||
} |
|
||||||
|
|
||||||
// LoadSnapshot loads a snapshot into the network
|
|
||||||
func (c *Client) LoadSnapshot(snap *Snapshot) error { |
|
||||||
return c.Post("/snapshot", snap, nil) |
|
||||||
} |
|
||||||
|
|
||||||
// SubscribeOpts is a collection of options to use when subscribing to network
|
|
||||||
// events
|
|
||||||
type SubscribeOpts struct { |
|
||||||
// Current instructs the server to send events for existing nodes and
|
|
||||||
// connections first
|
|
||||||
Current bool |
|
||||||
|
|
||||||
// Filter instructs the server to only send a subset of message events
|
|
||||||
Filter string |
|
||||||
} |
|
||||||
|
|
||||||
// SubscribeNetwork subscribes to network events which are sent from the server
|
|
||||||
// as a server-sent-events stream, optionally receiving events for existing
|
|
||||||
// nodes and connections and filtering message events
|
|
||||||
func (c *Client) SubscribeNetwork(events chan *Event, opts SubscribeOpts) (event.Subscription, error) { |
|
||||||
url := fmt.Sprintf("%s/events?current=%t&filter=%s", c.URL, opts.Current, opts.Filter) |
|
||||||
req, err := http.NewRequest(http.MethodGet, url, nil) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
req.Header.Set("Accept", "text/event-stream") |
|
||||||
res, err := c.client.Do(req) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
if res.StatusCode != http.StatusOK { |
|
||||||
response, _ := io.ReadAll(res.Body) |
|
||||||
res.Body.Close() |
|
||||||
return nil, fmt.Errorf("unexpected HTTP status: %s: %s", res.Status, response) |
|
||||||
} |
|
||||||
|
|
||||||
// define a producer function to pass to event.Subscription
|
|
||||||
// which reads server-sent events from res.Body and sends
|
|
||||||
// them to the events channel
|
|
||||||
producer := func(stop <-chan struct{}) error { |
|
||||||
defer res.Body.Close() |
|
||||||
|
|
||||||
// read lines from res.Body in a goroutine so that we are
|
|
||||||
// always reading from the stop channel
|
|
||||||
lines := make(chan string) |
|
||||||
errC := make(chan error, 1) |
|
||||||
go func() { |
|
||||||
s := bufio.NewScanner(res.Body) |
|
||||||
for s.Scan() { |
|
||||||
select { |
|
||||||
case lines <- s.Text(): |
|
||||||
case <-stop: |
|
||||||
return |
|
||||||
} |
|
||||||
} |
|
||||||
errC <- s.Err() |
|
||||||
}() |
|
||||||
|
|
||||||
// detect any lines which start with "data:", decode the data
|
|
||||||
// into an event and send it to the events channel
|
|
||||||
for { |
|
||||||
select { |
|
||||||
case line := <-lines: |
|
||||||
if !strings.HasPrefix(line, "data:") { |
|
||||||
continue |
|
||||||
} |
|
||||||
data := strings.TrimSpace(strings.TrimPrefix(line, "data:")) |
|
||||||
event := &Event{} |
|
||||||
if err := json.Unmarshal([]byte(data), event); err != nil { |
|
||||||
return fmt.Errorf("error decoding SSE event: %s", err) |
|
||||||
} |
|
||||||
select { |
|
||||||
case events <- event: |
|
||||||
case <-stop: |
|
||||||
return nil |
|
||||||
} |
|
||||||
case err := <-errC: |
|
||||||
return err |
|
||||||
case <-stop: |
|
||||||
return nil |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
return event.NewSubscription(producer), nil |
|
||||||
} |
|
||||||
|
|
||||||
// GetNodes returns all nodes which exist in the network
|
|
||||||
func (c *Client) GetNodes() ([]*p2p.NodeInfo, error) { |
|
||||||
var nodes []*p2p.NodeInfo |
|
||||||
return nodes, c.Get("/nodes", &nodes) |
|
||||||
} |
|
||||||
|
|
||||||
// CreateNode creates a node in the network using the given configuration
|
|
||||||
func (c *Client) CreateNode(config *adapters.NodeConfig) (*p2p.NodeInfo, error) { |
|
||||||
node := &p2p.NodeInfo{} |
|
||||||
return node, c.Post("/nodes", config, node) |
|
||||||
} |
|
||||||
|
|
||||||
// GetNode returns details of a node
|
|
||||||
func (c *Client) GetNode(nodeID string) (*p2p.NodeInfo, error) { |
|
||||||
node := &p2p.NodeInfo{} |
|
||||||
return node, c.Get(fmt.Sprintf("/nodes/%s", nodeID), node) |
|
||||||
} |
|
||||||
|
|
||||||
// StartNode starts a node
|
|
||||||
func (c *Client) StartNode(nodeID string) error { |
|
||||||
return c.Post(fmt.Sprintf("/nodes/%s/start", nodeID), nil, nil) |
|
||||||
} |
|
||||||
|
|
||||||
// StopNode stops a node
|
|
||||||
func (c *Client) StopNode(nodeID string) error { |
|
||||||
return c.Post(fmt.Sprintf("/nodes/%s/stop", nodeID), nil, nil) |
|
||||||
} |
|
||||||
|
|
||||||
// ConnectNode connects a node to a peer node
|
|
||||||
func (c *Client) ConnectNode(nodeID, peerID string) error { |
|
||||||
return c.Post(fmt.Sprintf("/nodes/%s/conn/%s", nodeID, peerID), nil, nil) |
|
||||||
} |
|
||||||
|
|
||||||
// DisconnectNode disconnects a node from a peer node
|
|
||||||
func (c *Client) DisconnectNode(nodeID, peerID string) error { |
|
||||||
return c.Delete(fmt.Sprintf("/nodes/%s/conn/%s", nodeID, peerID)) |
|
||||||
} |
|
||||||
|
|
||||||
// RPCClient returns an RPC client connected to a node
|
|
||||||
func (c *Client) RPCClient(ctx context.Context, nodeID string) (*rpc.Client, error) { |
|
||||||
baseURL := strings.Replace(c.URL, "http", "ws", 1) |
|
||||||
return rpc.DialWebsocket(ctx, fmt.Sprintf("%s/nodes/%s/rpc", baseURL, nodeID), "") |
|
||||||
} |
|
||||||
|
|
||||||
// Get performs a HTTP GET request decoding the resulting JSON response
|
|
||||||
// into "out"
|
|
||||||
func (c *Client) Get(path string, out interface{}) error { |
|
||||||
return c.Send(http.MethodGet, path, nil, out) |
|
||||||
} |
|
||||||
|
|
||||||
// Post performs a HTTP POST request sending "in" as the JSON body and
|
|
||||||
// decoding the resulting JSON response into "out"
|
|
||||||
func (c *Client) Post(path string, in, out interface{}) error { |
|
||||||
return c.Send(http.MethodPost, path, in, out) |
|
||||||
} |
|
||||||
|
|
||||||
// Delete performs a HTTP DELETE request
|
|
||||||
func (c *Client) Delete(path string) error { |
|
||||||
return c.Send(http.MethodDelete, path, nil, nil) |
|
||||||
} |
|
||||||
|
|
||||||
// Send performs a HTTP request, sending "in" as the JSON request body and
|
|
||||||
// decoding the JSON response into "out"
|
|
||||||
func (c *Client) Send(method, path string, in, out interface{}) error { |
|
||||||
var body []byte |
|
||||||
if in != nil { |
|
||||||
var err error |
|
||||||
body, err = json.Marshal(in) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
} |
|
||||||
req, err := http.NewRequest(method, c.URL+path, bytes.NewReader(body)) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
req.Header.Set("Content-Type", "application/json") |
|
||||||
req.Header.Set("Accept", "application/json") |
|
||||||
res, err := c.client.Do(req) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
defer res.Body.Close() |
|
||||||
if res.StatusCode != http.StatusOK && res.StatusCode != http.StatusCreated { |
|
||||||
response, _ := io.ReadAll(res.Body) |
|
||||||
return fmt.Errorf("unexpected HTTP status: %s: %s", res.Status, response) |
|
||||||
} |
|
||||||
if out != nil { |
|
||||||
if err := json.NewDecoder(res.Body).Decode(out); err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
} |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
// Server is an HTTP server providing an API to manage a simulation network
|
|
||||||
type Server struct { |
|
||||||
router *httprouter.Router |
|
||||||
network *Network |
|
||||||
mockerStop chan struct{} // when set, stops the current mocker
|
|
||||||
mockerMtx sync.Mutex // synchronises access to the mockerStop field
|
|
||||||
} |
|
||||||
|
|
||||||
// NewServer returns a new simulation API server
|
|
||||||
func NewServer(network *Network) *Server { |
|
||||||
s := &Server{ |
|
||||||
router: httprouter.New(), |
|
||||||
network: network, |
|
||||||
} |
|
||||||
|
|
||||||
s.OPTIONS("/", s.Options) |
|
||||||
s.GET("/", s.GetNetwork) |
|
||||||
s.POST("/start", s.StartNetwork) |
|
||||||
s.POST("/stop", s.StopNetwork) |
|
||||||
s.POST("/mocker/start", s.StartMocker) |
|
||||||
s.POST("/mocker/stop", s.StopMocker) |
|
||||||
s.GET("/mocker", s.GetMockers) |
|
||||||
s.POST("/reset", s.ResetNetwork) |
|
||||||
s.GET("/events", s.StreamNetworkEvents) |
|
||||||
s.GET("/snapshot", s.CreateSnapshot) |
|
||||||
s.POST("/snapshot", s.LoadSnapshot) |
|
||||||
s.POST("/nodes", s.CreateNode) |
|
||||||
s.GET("/nodes", s.GetNodes) |
|
||||||
s.GET("/nodes/:nodeid", s.GetNode) |
|
||||||
s.POST("/nodes/:nodeid/start", s.StartNode) |
|
||||||
s.POST("/nodes/:nodeid/stop", s.StopNode) |
|
||||||
s.POST("/nodes/:nodeid/conn/:peerid", s.ConnectNode) |
|
||||||
s.DELETE("/nodes/:nodeid/conn/:peerid", s.DisconnectNode) |
|
||||||
s.GET("/nodes/:nodeid/rpc", s.NodeRPC) |
|
||||||
|
|
||||||
return s |
|
||||||
} |
|
||||||
|
|
||||||
// GetNetwork returns details of the network
|
|
||||||
func (s *Server) GetNetwork(w http.ResponseWriter, req *http.Request) { |
|
||||||
s.JSON(w, http.StatusOK, s.network) |
|
||||||
} |
|
||||||
|
|
||||||
// StartNetwork starts all nodes in the network
|
|
||||||
func (s *Server) StartNetwork(w http.ResponseWriter, req *http.Request) { |
|
||||||
if err := s.network.StartAll(); err != nil { |
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError) |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK) |
|
||||||
} |
|
||||||
|
|
||||||
// StopNetwork stops all nodes in the network
|
|
||||||
func (s *Server) StopNetwork(w http.ResponseWriter, req *http.Request) { |
|
||||||
if err := s.network.StopAll(); err != nil { |
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError) |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK) |
|
||||||
} |
|
||||||
|
|
||||||
// StartMocker starts the mocker node simulation
|
|
||||||
func (s *Server) StartMocker(w http.ResponseWriter, req *http.Request) { |
|
||||||
s.mockerMtx.Lock() |
|
||||||
defer s.mockerMtx.Unlock() |
|
||||||
if s.mockerStop != nil { |
|
||||||
http.Error(w, "mocker already running", http.StatusInternalServerError) |
|
||||||
return |
|
||||||
} |
|
||||||
mockerType := req.FormValue("mocker-type") |
|
||||||
mockerFn := LookupMocker(mockerType) |
|
||||||
if mockerFn == nil { |
|
||||||
http.Error(w, fmt.Sprintf("unknown mocker type %q", html.EscapeString(mockerType)), http.StatusBadRequest) |
|
||||||
return |
|
||||||
} |
|
||||||
nodeCount, err := strconv.Atoi(req.FormValue("node-count")) |
|
||||||
if err != nil { |
|
||||||
http.Error(w, "invalid node-count provided", http.StatusBadRequest) |
|
||||||
return |
|
||||||
} |
|
||||||
s.mockerStop = make(chan struct{}) |
|
||||||
go mockerFn(s.network, s.mockerStop, nodeCount) |
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK) |
|
||||||
} |
|
||||||
|
|
||||||
// StopMocker stops the mocker node simulation
|
|
||||||
func (s *Server) StopMocker(w http.ResponseWriter, req *http.Request) { |
|
||||||
s.mockerMtx.Lock() |
|
||||||
defer s.mockerMtx.Unlock() |
|
||||||
if s.mockerStop == nil { |
|
||||||
http.Error(w, "stop channel not initialized", http.StatusInternalServerError) |
|
||||||
return |
|
||||||
} |
|
||||||
close(s.mockerStop) |
|
||||||
s.mockerStop = nil |
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK) |
|
||||||
} |
|
||||||
|
|
||||||
// GetMockers returns a list of available mockers
|
|
||||||
func (s *Server) GetMockers(w http.ResponseWriter, req *http.Request) { |
|
||||||
list := GetMockerList() |
|
||||||
s.JSON(w, http.StatusOK, list) |
|
||||||
} |
|
||||||
|
|
||||||
// ResetNetwork resets all properties of a network to its initial (empty) state
|
|
||||||
func (s *Server) ResetNetwork(w http.ResponseWriter, req *http.Request) { |
|
||||||
s.network.Reset() |
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK) |
|
||||||
} |
|
||||||
|
|
||||||
// StreamNetworkEvents streams network events as a server-sent-events stream
|
|
||||||
func (s *Server) StreamNetworkEvents(w http.ResponseWriter, req *http.Request) { |
|
||||||
events := make(chan *Event) |
|
||||||
sub := s.network.events.Subscribe(events) |
|
||||||
defer sub.Unsubscribe() |
|
||||||
|
|
||||||
// write writes the given event and data to the stream like:
|
|
||||||
//
|
|
||||||
// event: <event>
|
|
||||||
// data: <data>
|
|
||||||
//
|
|
||||||
write := func(event, data string) { |
|
||||||
fmt.Fprintf(w, "event: %s\n", event) |
|
||||||
fmt.Fprintf(w, "data: %s\n\n", data) |
|
||||||
if fw, ok := w.(http.Flusher); ok { |
|
||||||
fw.Flush() |
|
||||||
} |
|
||||||
} |
|
||||||
writeEvent := func(event *Event) error { |
|
||||||
data, err := json.Marshal(event) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
write("network", string(data)) |
|
||||||
return nil |
|
||||||
} |
|
||||||
writeErr := func(err error) { |
|
||||||
write("error", err.Error()) |
|
||||||
} |
|
||||||
|
|
||||||
// check if filtering has been requested
|
|
||||||
var filters MsgFilters |
|
||||||
if filterParam := req.URL.Query().Get("filter"); filterParam != "" { |
|
||||||
var err error |
|
||||||
filters, err = NewMsgFilters(filterParam) |
|
||||||
if err != nil { |
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest) |
|
||||||
return |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "text/event-stream; charset=utf-8") |
|
||||||
w.WriteHeader(http.StatusOK) |
|
||||||
fmt.Fprintf(w, "\n\n") |
|
||||||
if fw, ok := w.(http.Flusher); ok { |
|
||||||
fw.Flush() |
|
||||||
} |
|
||||||
|
|
||||||
// optionally send the existing nodes and connections
|
|
||||||
if req.URL.Query().Get("current") == "true" { |
|
||||||
snap, err := s.network.Snapshot() |
|
||||||
if err != nil { |
|
||||||
writeErr(err) |
|
||||||
return |
|
||||||
} |
|
||||||
for _, node := range snap.Nodes { |
|
||||||
event := NewEvent(&node.Node) |
|
||||||
if err := writeEvent(event); err != nil { |
|
||||||
writeErr(err) |
|
||||||
return |
|
||||||
} |
|
||||||
} |
|
||||||
for _, conn := range snap.Conns { |
|
||||||
conn := conn |
|
||||||
event := NewEvent(&conn) |
|
||||||
if err := writeEvent(event); err != nil { |
|
||||||
writeErr(err) |
|
||||||
return |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
clientGone := req.Context().Done() |
|
||||||
for { |
|
||||||
select { |
|
||||||
case event := <-events: |
|
||||||
// only send message events which match the filters
|
|
||||||
if event.Msg != nil && !filters.Match(event.Msg) { |
|
||||||
continue |
|
||||||
} |
|
||||||
if err := writeEvent(event); err != nil { |
|
||||||
writeErr(err) |
|
||||||
return |
|
||||||
} |
|
||||||
case <-clientGone: |
|
||||||
return |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// NewMsgFilters constructs a collection of message filters from a URL query
|
|
||||||
// parameter.
|
|
||||||
//
|
|
||||||
// The parameter is expected to be a dash-separated list of individual filters,
|
|
||||||
// each having the format '<proto>:<codes>', where <proto> is the name of a
|
|
||||||
// protocol and <codes> is a comma-separated list of message codes.
|
|
||||||
//
|
|
||||||
// A message code of '*' or '-1' is considered a wildcard and matches any code.
|
|
||||||
func NewMsgFilters(filterParam string) (MsgFilters, error) { |
|
||||||
filters := make(MsgFilters) |
|
||||||
for _, filter := range strings.Split(filterParam, "-") { |
|
||||||
proto, codes, found := strings.Cut(filter, ":") |
|
||||||
if !found || proto == "" || codes == "" { |
|
||||||
return nil, fmt.Errorf("invalid message filter: %s", filter) |
|
||||||
} |
|
||||||
|
|
||||||
for _, code := range strings.Split(codes, ",") { |
|
||||||
if code == "*" || code == "-1" { |
|
||||||
filters[MsgFilter{Proto: proto, Code: -1}] = struct{}{} |
|
||||||
continue |
|
||||||
} |
|
||||||
n, err := strconv.ParseUint(code, 10, 64) |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("invalid message code: %s", code) |
|
||||||
} |
|
||||||
filters[MsgFilter{Proto: proto, Code: int64(n)}] = struct{}{} |
|
||||||
} |
|
||||||
} |
|
||||||
return filters, nil |
|
||||||
} |
|
||||||
|
|
||||||
// MsgFilters is a collection of filters which are used to filter message
|
|
||||||
// events
|
|
||||||
type MsgFilters map[MsgFilter]struct{} |
|
||||||
|
|
||||||
// Match checks if the given message matches any of the filters
|
|
||||||
func (m MsgFilters) Match(msg *Msg) bool { |
|
||||||
// check if there is a wildcard filter for the message's protocol
|
|
||||||
if _, ok := m[MsgFilter{Proto: msg.Protocol, Code: -1}]; ok { |
|
||||||
return true |
|
||||||
} |
|
||||||
|
|
||||||
// check if there is a filter for the message's protocol and code
|
|
||||||
if _, ok := m[MsgFilter{Proto: msg.Protocol, Code: int64(msg.Code)}]; ok { |
|
||||||
return true |
|
||||||
} |
|
||||||
|
|
||||||
return false |
|
||||||
} |
|
||||||
|
|
||||||
// MsgFilter is used to filter message events based on protocol and message
|
|
||||||
// code
|
|
||||||
type MsgFilter struct { |
|
||||||
// Proto is matched against a message's protocol
|
|
||||||
Proto string |
|
||||||
|
|
||||||
// Code is matched against a message's code, with -1 matching all codes
|
|
||||||
Code int64 |
|
||||||
} |
|
||||||
|
|
||||||
// CreateSnapshot creates a network snapshot
|
|
||||||
func (s *Server) CreateSnapshot(w http.ResponseWriter, req *http.Request) { |
|
||||||
snap, err := s.network.Snapshot() |
|
||||||
if err != nil { |
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError) |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
s.JSON(w, http.StatusOK, snap) |
|
||||||
} |
|
||||||
|
|
||||||
// LoadSnapshot loads a snapshot into the network
|
|
||||||
func (s *Server) LoadSnapshot(w http.ResponseWriter, req *http.Request) { |
|
||||||
snap := &Snapshot{} |
|
||||||
if err := json.NewDecoder(req.Body).Decode(snap); err != nil { |
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest) |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
if err := s.network.Load(snap); err != nil { |
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError) |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
s.JSON(w, http.StatusOK, s.network) |
|
||||||
} |
|
||||||
|
|
||||||
// CreateNode creates a node in the network using the given configuration
|
|
||||||
func (s *Server) CreateNode(w http.ResponseWriter, req *http.Request) { |
|
||||||
config := &adapters.NodeConfig{} |
|
||||||
|
|
||||||
err := json.NewDecoder(req.Body).Decode(config) |
|
||||||
if err != nil && !errors.Is(err, io.EOF) { |
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest) |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
node, err := s.network.NewNodeWithConfig(config) |
|
||||||
if err != nil { |
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError) |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
s.JSON(w, http.StatusCreated, node.NodeInfo()) |
|
||||||
} |
|
||||||
|
|
||||||
// GetNodes returns all nodes which exist in the network
|
|
||||||
func (s *Server) GetNodes(w http.ResponseWriter, req *http.Request) { |
|
||||||
nodes := s.network.GetNodes() |
|
||||||
|
|
||||||
infos := make([]*p2p.NodeInfo, len(nodes)) |
|
||||||
for i, node := range nodes { |
|
||||||
infos[i] = node.NodeInfo() |
|
||||||
} |
|
||||||
|
|
||||||
s.JSON(w, http.StatusOK, infos) |
|
||||||
} |
|
||||||
|
|
||||||
// GetNode returns details of a node
|
|
||||||
func (s *Server) GetNode(w http.ResponseWriter, req *http.Request) { |
|
||||||
node := req.Context().Value("node").(*Node) |
|
||||||
|
|
||||||
s.JSON(w, http.StatusOK, node.NodeInfo()) |
|
||||||
} |
|
||||||
|
|
||||||
// StartNode starts a node
|
|
||||||
func (s *Server) StartNode(w http.ResponseWriter, req *http.Request) { |
|
||||||
node := req.Context().Value("node").(*Node) |
|
||||||
|
|
||||||
if err := s.network.Start(node.ID()); err != nil { |
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError) |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
s.JSON(w, http.StatusOK, node.NodeInfo()) |
|
||||||
} |
|
||||||
|
|
||||||
// StopNode stops a node
|
|
||||||
func (s *Server) StopNode(w http.ResponseWriter, req *http.Request) { |
|
||||||
node := req.Context().Value("node").(*Node) |
|
||||||
|
|
||||||
if err := s.network.Stop(node.ID()); err != nil { |
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError) |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
s.JSON(w, http.StatusOK, node.NodeInfo()) |
|
||||||
} |
|
||||||
|
|
||||||
// ConnectNode connects a node to a peer node
|
|
||||||
func (s *Server) ConnectNode(w http.ResponseWriter, req *http.Request) { |
|
||||||
node := req.Context().Value("node").(*Node) |
|
||||||
peer := req.Context().Value("peer").(*Node) |
|
||||||
|
|
||||||
if err := s.network.Connect(node.ID(), peer.ID()); err != nil { |
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError) |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
s.JSON(w, http.StatusOK, node.NodeInfo()) |
|
||||||
} |
|
||||||
|
|
||||||
// DisconnectNode disconnects a node from a peer node
|
|
||||||
func (s *Server) DisconnectNode(w http.ResponseWriter, req *http.Request) { |
|
||||||
node := req.Context().Value("node").(*Node) |
|
||||||
peer := req.Context().Value("peer").(*Node) |
|
||||||
|
|
||||||
if err := s.network.Disconnect(node.ID(), peer.ID()); err != nil { |
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError) |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
s.JSON(w, http.StatusOK, node.NodeInfo()) |
|
||||||
} |
|
||||||
|
|
||||||
// Options responds to the OPTIONS HTTP method by returning a 200 OK response
|
|
||||||
// with the "Access-Control-Allow-Headers" header set to "Content-Type"
|
|
||||||
func (s *Server) Options(w http.ResponseWriter, req *http.Request) { |
|
||||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type") |
|
||||||
w.WriteHeader(http.StatusOK) |
|
||||||
} |
|
||||||
|
|
||||||
var wsUpgrade = websocket.Upgrader{ |
|
||||||
CheckOrigin: func(*http.Request) bool { return true }, |
|
||||||
} |
|
||||||
|
|
||||||
// NodeRPC forwards RPC requests to a node in the network via a WebSocket
|
|
||||||
// connection
|
|
||||||
func (s *Server) NodeRPC(w http.ResponseWriter, req *http.Request) { |
|
||||||
conn, err := wsUpgrade.Upgrade(w, req, nil) |
|
||||||
if err != nil { |
|
||||||
return |
|
||||||
} |
|
||||||
defer conn.Close() |
|
||||||
node := req.Context().Value("node").(*Node) |
|
||||||
node.ServeRPC(conn) |
|
||||||
} |
|
||||||
|
|
||||||
// ServeHTTP implements the http.Handler interface by delegating to the
|
|
||||||
// underlying httprouter.Router
|
|
||||||
func (s *Server) ServeHTTP(w http.ResponseWriter, req *http.Request) { |
|
||||||
s.router.ServeHTTP(w, req) |
|
||||||
} |
|
||||||
|
|
||||||
// GET registers a handler for GET requests to a particular path
|
|
||||||
func (s *Server) GET(path string, handle http.HandlerFunc) { |
|
||||||
s.router.GET(path, s.wrapHandler(handle)) |
|
||||||
} |
|
||||||
|
|
||||||
// POST registers a handler for POST requests to a particular path
|
|
||||||
func (s *Server) POST(path string, handle http.HandlerFunc) { |
|
||||||
s.router.POST(path, s.wrapHandler(handle)) |
|
||||||
} |
|
||||||
|
|
||||||
// DELETE registers a handler for DELETE requests to a particular path
|
|
||||||
func (s *Server) DELETE(path string, handle http.HandlerFunc) { |
|
||||||
s.router.DELETE(path, s.wrapHandler(handle)) |
|
||||||
} |
|
||||||
|
|
||||||
// OPTIONS registers a handler for OPTIONS requests to a particular path
|
|
||||||
func (s *Server) OPTIONS(path string, handle http.HandlerFunc) { |
|
||||||
s.router.OPTIONS("/*path", s.wrapHandler(handle)) |
|
||||||
} |
|
||||||
|
|
||||||
// JSON sends "data" as a JSON HTTP response
|
|
||||||
func (s *Server) JSON(w http.ResponseWriter, status int, data interface{}) { |
|
||||||
w.Header().Set("Content-Type", "application/json") |
|
||||||
w.WriteHeader(status) |
|
||||||
json.NewEncoder(w).Encode(data) |
|
||||||
} |
|
||||||
|
|
||||||
// wrapHandler returns an httprouter.Handle which wraps an http.HandlerFunc by
|
|
||||||
// populating request.Context with any objects from the URL params
|
|
||||||
func (s *Server) wrapHandler(handler http.HandlerFunc) httprouter.Handle { |
|
||||||
return func(w http.ResponseWriter, req *http.Request, params httprouter.Params) { |
|
||||||
w.Header().Set("Access-Control-Allow-Origin", "*") |
|
||||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") |
|
||||||
|
|
||||||
ctx := req.Context() |
|
||||||
|
|
||||||
if id := params.ByName("nodeid"); id != "" { |
|
||||||
var nodeID enode.ID |
|
||||||
var node *Node |
|
||||||
if nodeID.UnmarshalText([]byte(id)) == nil { |
|
||||||
node = s.network.GetNode(nodeID) |
|
||||||
} else { |
|
||||||
node = s.network.GetNodeByName(id) |
|
||||||
} |
|
||||||
if node == nil { |
|
||||||
http.NotFound(w, req) |
|
||||||
return |
|
||||||
} |
|
||||||
ctx = context.WithValue(ctx, "node", node) |
|
||||||
} |
|
||||||
|
|
||||||
if id := params.ByName("peerid"); id != "" { |
|
||||||
var peerID enode.ID |
|
||||||
var peer *Node |
|
||||||
if peerID.UnmarshalText([]byte(id)) == nil { |
|
||||||
peer = s.network.GetNode(peerID) |
|
||||||
} else { |
|
||||||
peer = s.network.GetNodeByName(id) |
|
||||||
} |
|
||||||
if peer == nil { |
|
||||||
http.NotFound(w, req) |
|
||||||
return |
|
||||||
} |
|
||||||
ctx = context.WithValue(ctx, "peer", peer) |
|
||||||
} |
|
||||||
|
|
||||||
handler(w, req.WithContext(ctx)) |
|
||||||
} |
|
||||||
} |
|
@ -1,869 +0,0 @@ |
|||||||
// Copyright 2017 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library 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 Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package simulations |
|
||||||
|
|
||||||
import ( |
|
||||||
"context" |
|
||||||
"flag" |
|
||||||
"fmt" |
|
||||||
"log/slog" |
|
||||||
"math/rand" |
|
||||||
"net/http/httptest" |
|
||||||
"os" |
|
||||||
"reflect" |
|
||||||
"sync" |
|
||||||
"sync/atomic" |
|
||||||
"testing" |
|
||||||
"time" |
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/event" |
|
||||||
"github.com/ethereum/go-ethereum/log" |
|
||||||
"github.com/ethereum/go-ethereum/node" |
|
||||||
"github.com/ethereum/go-ethereum/p2p" |
|
||||||
"github.com/ethereum/go-ethereum/p2p/enode" |
|
||||||
"github.com/ethereum/go-ethereum/p2p/simulations/adapters" |
|
||||||
"github.com/ethereum/go-ethereum/rpc" |
|
||||||
"github.com/mattn/go-colorable" |
|
||||||
) |
|
||||||
|
|
||||||
func TestMain(m *testing.M) { |
|
||||||
loglevel := flag.Int("loglevel", 2, "verbosity of logs") |
|
||||||
|
|
||||||
flag.Parse() |
|
||||||
log.SetDefault(log.NewLogger(log.NewTerminalHandlerWithLevel(colorable.NewColorableStderr(), slog.Level(*loglevel), true))) |
|
||||||
os.Exit(m.Run()) |
|
||||||
} |
|
||||||
|
|
||||||
// testService implements the node.Service interface and provides protocols
|
|
||||||
// and APIs which are useful for testing nodes in a simulation network
|
|
||||||
type testService struct { |
|
||||||
id enode.ID |
|
||||||
|
|
||||||
// peerCount is incremented once a peer handshake has been performed
|
|
||||||
peerCount int64 |
|
||||||
|
|
||||||
peers map[enode.ID]*testPeer |
|
||||||
peersMtx sync.Mutex |
|
||||||
|
|
||||||
// state stores []byte which is used to test creating and loading
|
|
||||||
// snapshots
|
|
||||||
state atomic.Value |
|
||||||
} |
|
||||||
|
|
||||||
func newTestService(ctx *adapters.ServiceContext, stack *node.Node) (node.Lifecycle, error) { |
|
||||||
svc := &testService{ |
|
||||||
id: ctx.Config.ID, |
|
||||||
peers: make(map[enode.ID]*testPeer), |
|
||||||
} |
|
||||||
svc.state.Store(ctx.Snapshot) |
|
||||||
|
|
||||||
stack.RegisterProtocols(svc.Protocols()) |
|
||||||
stack.RegisterAPIs(svc.APIs()) |
|
||||||
return svc, nil |
|
||||||
} |
|
||||||
|
|
||||||
type testPeer struct { |
|
||||||
testReady chan struct{} |
|
||||||
dumReady chan struct{} |
|
||||||
} |
|
||||||
|
|
||||||
func (t *testService) peer(id enode.ID) *testPeer { |
|
||||||
t.peersMtx.Lock() |
|
||||||
defer t.peersMtx.Unlock() |
|
||||||
if peer, ok := t.peers[id]; ok { |
|
||||||
return peer |
|
||||||
} |
|
||||||
peer := &testPeer{ |
|
||||||
testReady: make(chan struct{}), |
|
||||||
dumReady: make(chan struct{}), |
|
||||||
} |
|
||||||
t.peers[id] = peer |
|
||||||
return peer |
|
||||||
} |
|
||||||
|
|
||||||
func (t *testService) Protocols() []p2p.Protocol { |
|
||||||
return []p2p.Protocol{ |
|
||||||
{ |
|
||||||
Name: "test", |
|
||||||
Version: 1, |
|
||||||
Length: 3, |
|
||||||
Run: t.RunTest, |
|
||||||
}, |
|
||||||
{ |
|
||||||
Name: "dum", |
|
||||||
Version: 1, |
|
||||||
Length: 1, |
|
||||||
Run: t.RunDum, |
|
||||||
}, |
|
||||||
{ |
|
||||||
Name: "prb", |
|
||||||
Version: 1, |
|
||||||
Length: 1, |
|
||||||
Run: t.RunPrb, |
|
||||||
}, |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func (t *testService) APIs() []rpc.API { |
|
||||||
return []rpc.API{{ |
|
||||||
Namespace: "test", |
|
||||||
Version: "1.0", |
|
||||||
Service: &TestAPI{ |
|
||||||
state: &t.state, |
|
||||||
peerCount: &t.peerCount, |
|
||||||
}, |
|
||||||
}} |
|
||||||
} |
|
||||||
|
|
||||||
func (t *testService) Start() error { |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
func (t *testService) Stop() error { |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
// handshake performs a peer handshake by sending and expecting an empty
|
|
||||||
// message with the given code
|
|
||||||
func (t *testService) handshake(rw p2p.MsgReadWriter, code uint64) error { |
|
||||||
errc := make(chan error, 2) |
|
||||||
go func() { errc <- p2p.SendItems(rw, code) }() |
|
||||||
go func() { errc <- p2p.ExpectMsg(rw, code, struct{}{}) }() |
|
||||||
for i := 0; i < 2; i++ { |
|
||||||
if err := <-errc; err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
} |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
func (t *testService) RunTest(p *p2p.Peer, rw p2p.MsgReadWriter) error { |
|
||||||
peer := t.peer(p.ID()) |
|
||||||
|
|
||||||
// perform three handshakes with three different message codes,
|
|
||||||
// used to test message sending and filtering
|
|
||||||
if err := t.handshake(rw, 2); err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
if err := t.handshake(rw, 1); err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
if err := t.handshake(rw, 0); err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
// close the testReady channel so that other protocols can run
|
|
||||||
close(peer.testReady) |
|
||||||
|
|
||||||
// track the peer
|
|
||||||
atomic.AddInt64(&t.peerCount, 1) |
|
||||||
defer atomic.AddInt64(&t.peerCount, -1) |
|
||||||
|
|
||||||
// block until the peer is dropped
|
|
||||||
for { |
|
||||||
_, err := rw.ReadMsg() |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func (t *testService) RunDum(p *p2p.Peer, rw p2p.MsgReadWriter) error { |
|
||||||
peer := t.peer(p.ID()) |
|
||||||
|
|
||||||
// wait for the test protocol to perform its handshake
|
|
||||||
<-peer.testReady |
|
||||||
|
|
||||||
// perform a handshake
|
|
||||||
if err := t.handshake(rw, 0); err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
// close the dumReady channel so that other protocols can run
|
|
||||||
close(peer.dumReady) |
|
||||||
|
|
||||||
// block until the peer is dropped
|
|
||||||
for { |
|
||||||
_, err := rw.ReadMsg() |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
func (t *testService) RunPrb(p *p2p.Peer, rw p2p.MsgReadWriter) error { |
|
||||||
peer := t.peer(p.ID()) |
|
||||||
|
|
||||||
// wait for the dum protocol to perform its handshake
|
|
||||||
<-peer.dumReady |
|
||||||
|
|
||||||
// perform a handshake
|
|
||||||
if err := t.handshake(rw, 0); err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
// block until the peer is dropped
|
|
||||||
for { |
|
||||||
_, err := rw.ReadMsg() |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func (t *testService) Snapshot() ([]byte, error) { |
|
||||||
return t.state.Load().([]byte), nil |
|
||||||
} |
|
||||||
|
|
||||||
// TestAPI provides a test API to:
|
|
||||||
// * get the peer count
|
|
||||||
// * get and set an arbitrary state byte slice
|
|
||||||
// * get and increment a counter
|
|
||||||
// * subscribe to counter increment events
|
|
||||||
type TestAPI struct { |
|
||||||
state *atomic.Value |
|
||||||
peerCount *int64 |
|
||||||
counter int64 |
|
||||||
feed event.Feed |
|
||||||
} |
|
||||||
|
|
||||||
func (t *TestAPI) PeerCount() int64 { |
|
||||||
return atomic.LoadInt64(t.peerCount) |
|
||||||
} |
|
||||||
|
|
||||||
func (t *TestAPI) Get() int64 { |
|
||||||
return atomic.LoadInt64(&t.counter) |
|
||||||
} |
|
||||||
|
|
||||||
func (t *TestAPI) Add(delta int64) { |
|
||||||
atomic.AddInt64(&t.counter, delta) |
|
||||||
t.feed.Send(delta) |
|
||||||
} |
|
||||||
|
|
||||||
func (t *TestAPI) GetState() []byte { |
|
||||||
return t.state.Load().([]byte) |
|
||||||
} |
|
||||||
|
|
||||||
func (t *TestAPI) SetState(state []byte) { |
|
||||||
t.state.Store(state) |
|
||||||
} |
|
||||||
|
|
||||||
func (t *TestAPI) Events(ctx context.Context) (*rpc.Subscription, error) { |
|
||||||
notifier, supported := rpc.NotifierFromContext(ctx) |
|
||||||
if !supported { |
|
||||||
return nil, rpc.ErrNotificationsUnsupported |
|
||||||
} |
|
||||||
|
|
||||||
rpcSub := notifier.CreateSubscription() |
|
||||||
|
|
||||||
go func() { |
|
||||||
events := make(chan int64) |
|
||||||
sub := t.feed.Subscribe(events) |
|
||||||
defer sub.Unsubscribe() |
|
||||||
|
|
||||||
for { |
|
||||||
select { |
|
||||||
case event := <-events: |
|
||||||
notifier.Notify(rpcSub.ID, event) |
|
||||||
case <-sub.Err(): |
|
||||||
return |
|
||||||
case <-rpcSub.Err(): |
|
||||||
return |
|
||||||
} |
|
||||||
} |
|
||||||
}() |
|
||||||
|
|
||||||
return rpcSub, nil |
|
||||||
} |
|
||||||
|
|
||||||
var testServices = adapters.LifecycleConstructors{ |
|
||||||
"test": newTestService, |
|
||||||
} |
|
||||||
|
|
||||||
func testHTTPServer(t *testing.T) (*Network, *httptest.Server) { |
|
||||||
t.Helper() |
|
||||||
adapter := adapters.NewSimAdapter(testServices) |
|
||||||
network := NewNetwork(adapter, &NetworkConfig{ |
|
||||||
DefaultService: "test", |
|
||||||
}) |
|
||||||
return network, httptest.NewServer(NewServer(network)) |
|
||||||
} |
|
||||||
|
|
||||||
// TestHTTPNetwork tests interacting with a simulation network using the HTTP
|
|
||||||
// API
|
|
||||||
func TestHTTPNetwork(t *testing.T) { |
|
||||||
// start the server
|
|
||||||
network, s := testHTTPServer(t) |
|
||||||
defer s.Close() |
|
||||||
|
|
||||||
// subscribe to events so we can check them later
|
|
||||||
client := NewClient(s.URL) |
|
||||||
events := make(chan *Event, 100) |
|
||||||
var opts SubscribeOpts |
|
||||||
sub, err := client.SubscribeNetwork(events, opts) |
|
||||||
if err != nil { |
|
||||||
t.Fatalf("error subscribing to network events: %s", err) |
|
||||||
} |
|
||||||
defer sub.Unsubscribe() |
|
||||||
|
|
||||||
// check we can retrieve details about the network
|
|
||||||
gotNetwork, err := client.GetNetwork() |
|
||||||
if err != nil { |
|
||||||
t.Fatalf("error getting network: %s", err) |
|
||||||
} |
|
||||||
if gotNetwork.ID != network.ID { |
|
||||||
t.Fatalf("expected network to have ID %q, got %q", network.ID, gotNetwork.ID) |
|
||||||
} |
|
||||||
|
|
||||||
// start a simulation network
|
|
||||||
nodeIDs := startTestNetwork(t, client) |
|
||||||
|
|
||||||
// check we got all the events
|
|
||||||
x := &expectEvents{t, events, sub} |
|
||||||
x.expect( |
|
||||||
x.nodeEvent(nodeIDs[0], false), |
|
||||||
x.nodeEvent(nodeIDs[1], false), |
|
||||||
x.nodeEvent(nodeIDs[0], true), |
|
||||||
x.nodeEvent(nodeIDs[1], true), |
|
||||||
x.connEvent(nodeIDs[0], nodeIDs[1], false), |
|
||||||
x.connEvent(nodeIDs[0], nodeIDs[1], true), |
|
||||||
) |
|
||||||
|
|
||||||
// reconnect the stream and check we get the current nodes and conns
|
|
||||||
events = make(chan *Event, 100) |
|
||||||
opts.Current = true |
|
||||||
sub, err = client.SubscribeNetwork(events, opts) |
|
||||||
if err != nil { |
|
||||||
t.Fatalf("error subscribing to network events: %s", err) |
|
||||||
} |
|
||||||
defer sub.Unsubscribe() |
|
||||||
x = &expectEvents{t, events, sub} |
|
||||||
x.expect( |
|
||||||
x.nodeEvent(nodeIDs[0], true), |
|
||||||
x.nodeEvent(nodeIDs[1], true), |
|
||||||
x.connEvent(nodeIDs[0], nodeIDs[1], true), |
|
||||||
) |
|
||||||
} |
|
||||||
|
|
||||||
func startTestNetwork(t *testing.T, client *Client) []string { |
|
||||||
// create two nodes
|
|
||||||
nodeCount := 2 |
|
||||||
nodeIDs := make([]string, nodeCount) |
|
||||||
for i := 0; i < nodeCount; i++ { |
|
||||||
config := adapters.RandomNodeConfig() |
|
||||||
node, err := client.CreateNode(config) |
|
||||||
if err != nil { |
|
||||||
t.Fatalf("error creating node: %s", err) |
|
||||||
} |
|
||||||
nodeIDs[i] = node.ID |
|
||||||
} |
|
||||||
|
|
||||||
// check both nodes exist
|
|
||||||
nodes, err := client.GetNodes() |
|
||||||
if err != nil { |
|
||||||
t.Fatalf("error getting nodes: %s", err) |
|
||||||
} |
|
||||||
if len(nodes) != nodeCount { |
|
||||||
t.Fatalf("expected %d nodes, got %d", nodeCount, len(nodes)) |
|
||||||
} |
|
||||||
for i, nodeID := range nodeIDs { |
|
||||||
if nodes[i].ID != nodeID { |
|
||||||
t.Fatalf("expected node %d to have ID %q, got %q", i, nodeID, nodes[i].ID) |
|
||||||
} |
|
||||||
node, err := client.GetNode(nodeID) |
|
||||||
if err != nil { |
|
||||||
t.Fatalf("error getting node %d: %s", i, err) |
|
||||||
} |
|
||||||
if node.ID != nodeID { |
|
||||||
t.Fatalf("expected node %d to have ID %q, got %q", i, nodeID, node.ID) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// start both nodes
|
|
||||||
for _, nodeID := range nodeIDs { |
|
||||||
if err := client.StartNode(nodeID); err != nil { |
|
||||||
t.Fatalf("error starting node %q: %s", nodeID, err) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// connect the nodes
|
|
||||||
for i := 0; i < nodeCount-1; i++ { |
|
||||||
peerId := i + 1 |
|
||||||
if i == nodeCount-1 { |
|
||||||
peerId = 0 |
|
||||||
} |
|
||||||
if err := client.ConnectNode(nodeIDs[i], nodeIDs[peerId]); err != nil { |
|
||||||
t.Fatalf("error connecting nodes: %s", err) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
return nodeIDs |
|
||||||
} |
|
||||||
|
|
||||||
type expectEvents struct { |
|
||||||
*testing.T |
|
||||||
|
|
||||||
events chan *Event |
|
||||||
sub event.Subscription |
|
||||||
} |
|
||||||
|
|
||||||
func (t *expectEvents) nodeEvent(id string, up bool) *Event { |
|
||||||
config := &adapters.NodeConfig{ID: enode.HexID(id)} |
|
||||||
return &Event{Type: EventTypeNode, Node: newNode(nil, config, up)} |
|
||||||
} |
|
||||||
|
|
||||||
func (t *expectEvents) connEvent(one, other string, up bool) *Event { |
|
||||||
return &Event{ |
|
||||||
Type: EventTypeConn, |
|
||||||
Conn: &Conn{ |
|
||||||
One: enode.HexID(one), |
|
||||||
Other: enode.HexID(other), |
|
||||||
Up: up, |
|
||||||
}, |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func (t *expectEvents) expectMsgs(expected map[MsgFilter]int) { |
|
||||||
actual := make(map[MsgFilter]int) |
|
||||||
timeout := time.After(10 * time.Second) |
|
||||||
loop: |
|
||||||
for { |
|
||||||
select { |
|
||||||
case event := <-t.events: |
|
||||||
t.Logf("received %s event: %v", event.Type, event) |
|
||||||
|
|
||||||
if event.Type != EventTypeMsg || event.Msg.Received { |
|
||||||
continue loop |
|
||||||
} |
|
||||||
if event.Msg == nil { |
|
||||||
t.Fatal("expected event.Msg to be set") |
|
||||||
} |
|
||||||
filter := MsgFilter{ |
|
||||||
Proto: event.Msg.Protocol, |
|
||||||
Code: int64(event.Msg.Code), |
|
||||||
} |
|
||||||
actual[filter]++ |
|
||||||
if actual[filter] > expected[filter] { |
|
||||||
t.Fatalf("received too many msgs for filter: %v", filter) |
|
||||||
} |
|
||||||
if reflect.DeepEqual(actual, expected) { |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
case err := <-t.sub.Err(): |
|
||||||
t.Fatalf("network stream closed unexpectedly: %s", err) |
|
||||||
|
|
||||||
case <-timeout: |
|
||||||
t.Fatal("timed out waiting for expected events") |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func (t *expectEvents) expect(events ...*Event) { |
|
||||||
t.Helper() |
|
||||||
timeout := time.After(10 * time.Second) |
|
||||||
i := 0 |
|
||||||
for { |
|
||||||
select { |
|
||||||
case event := <-t.events: |
|
||||||
t.Logf("received %s event: %v", event.Type, event) |
|
||||||
|
|
||||||
expected := events[i] |
|
||||||
if event.Type != expected.Type { |
|
||||||
t.Fatalf("expected event %d to have type %q, got %q", i, expected.Type, event.Type) |
|
||||||
} |
|
||||||
|
|
||||||
switch expected.Type { |
|
||||||
case EventTypeNode: |
|
||||||
if event.Node == nil { |
|
||||||
t.Fatal("expected event.Node to be set") |
|
||||||
} |
|
||||||
if event.Node.ID() != expected.Node.ID() { |
|
||||||
t.Fatalf("expected node event %d to have id %q, got %q", i, expected.Node.ID().TerminalString(), event.Node.ID().TerminalString()) |
|
||||||
} |
|
||||||
if event.Node.Up() != expected.Node.Up() { |
|
||||||
t.Fatalf("expected node event %d to have up=%t, got up=%t", i, expected.Node.Up(), event.Node.Up()) |
|
||||||
} |
|
||||||
|
|
||||||
case EventTypeConn: |
|
||||||
if event.Conn == nil { |
|
||||||
t.Fatal("expected event.Conn to be set") |
|
||||||
} |
|
||||||
if event.Conn.One != expected.Conn.One { |
|
||||||
t.Fatalf("expected conn event %d to have one=%q, got one=%q", i, expected.Conn.One.TerminalString(), event.Conn.One.TerminalString()) |
|
||||||
} |
|
||||||
if event.Conn.Other != expected.Conn.Other { |
|
||||||
t.Fatalf("expected conn event %d to have other=%q, got other=%q", i, expected.Conn.Other.TerminalString(), event.Conn.Other.TerminalString()) |
|
||||||
} |
|
||||||
if event.Conn.Up != expected.Conn.Up { |
|
||||||
t.Fatalf("expected conn event %d to have up=%t, got up=%t", i, expected.Conn.Up, event.Conn.Up) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
i++ |
|
||||||
if i == len(events) { |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
case err := <-t.sub.Err(): |
|
||||||
t.Fatalf("network stream closed unexpectedly: %s", err) |
|
||||||
|
|
||||||
case <-timeout: |
|
||||||
t.Fatal("timed out waiting for expected events") |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// TestHTTPNodeRPC tests calling RPC methods on nodes via the HTTP API
|
|
||||||
func TestHTTPNodeRPC(t *testing.T) { |
|
||||||
// start the server
|
|
||||||
_, s := testHTTPServer(t) |
|
||||||
defer s.Close() |
|
||||||
|
|
||||||
// start a node in the network
|
|
||||||
client := NewClient(s.URL) |
|
||||||
|
|
||||||
config := adapters.RandomNodeConfig() |
|
||||||
node, err := client.CreateNode(config) |
|
||||||
if err != nil { |
|
||||||
t.Fatalf("error creating node: %s", err) |
|
||||||
} |
|
||||||
if err := client.StartNode(node.ID); err != nil { |
|
||||||
t.Fatalf("error starting node: %s", err) |
|
||||||
} |
|
||||||
|
|
||||||
// create two RPC clients
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) |
|
||||||
defer cancel() |
|
||||||
rpcClient1, err := client.RPCClient(ctx, node.ID) |
|
||||||
if err != nil { |
|
||||||
t.Fatalf("error getting node RPC client: %s", err) |
|
||||||
} |
|
||||||
rpcClient2, err := client.RPCClient(ctx, node.ID) |
|
||||||
if err != nil { |
|
||||||
t.Fatalf("error getting node RPC client: %s", err) |
|
||||||
} |
|
||||||
|
|
||||||
// subscribe to events using client 1
|
|
||||||
events := make(chan int64, 1) |
|
||||||
sub, err := rpcClient1.Subscribe(ctx, "test", events, "events") |
|
||||||
if err != nil { |
|
||||||
t.Fatalf("error subscribing to events: %s", err) |
|
||||||
} |
|
||||||
defer sub.Unsubscribe() |
|
||||||
|
|
||||||
// call some RPC methods using client 2
|
|
||||||
if err := rpcClient2.CallContext(ctx, nil, "test_add", 10); err != nil { |
|
||||||
t.Fatalf("error calling RPC method: %s", err) |
|
||||||
} |
|
||||||
var result int64 |
|
||||||
if err := rpcClient2.CallContext(ctx, &result, "test_get"); err != nil { |
|
||||||
t.Fatalf("error calling RPC method: %s", err) |
|
||||||
} |
|
||||||
if result != 10 { |
|
||||||
t.Fatalf("expected result to be 10, got %d", result) |
|
||||||
} |
|
||||||
|
|
||||||
// check we got an event from client 1
|
|
||||||
select { |
|
||||||
case event := <-events: |
|
||||||
if event != 10 { |
|
||||||
t.Fatalf("expected event to be 10, got %d", event) |
|
||||||
} |
|
||||||
case <-ctx.Done(): |
|
||||||
t.Fatal(ctx.Err()) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// TestHTTPSnapshot tests creating and loading network snapshots
|
|
||||||
func TestHTTPSnapshot(t *testing.T) { |
|
||||||
// start the server
|
|
||||||
network, s := testHTTPServer(t) |
|
||||||
defer s.Close() |
|
||||||
|
|
||||||
var eventsDone = make(chan struct{}, 1) |
|
||||||
count := 1 |
|
||||||
eventsDoneChan := make(chan *Event) |
|
||||||
eventSub := network.Events().Subscribe(eventsDoneChan) |
|
||||||
go func() { |
|
||||||
defer eventSub.Unsubscribe() |
|
||||||
for event := range eventsDoneChan { |
|
||||||
if event.Type == EventTypeConn && !event.Control { |
|
||||||
count-- |
|
||||||
if count == 0 { |
|
||||||
eventsDone <- struct{}{} |
|
||||||
return |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
}() |
|
||||||
|
|
||||||
// create a two-node network
|
|
||||||
client := NewClient(s.URL) |
|
||||||
nodeCount := 2 |
|
||||||
nodes := make([]*p2p.NodeInfo, nodeCount) |
|
||||||
for i := 0; i < nodeCount; i++ { |
|
||||||
config := adapters.RandomNodeConfig() |
|
||||||
node, err := client.CreateNode(config) |
|
||||||
if err != nil { |
|
||||||
t.Fatalf("error creating node: %s", err) |
|
||||||
} |
|
||||||
if err := client.StartNode(node.ID); err != nil { |
|
||||||
t.Fatalf("error starting node: %s", err) |
|
||||||
} |
|
||||||
nodes[i] = node |
|
||||||
} |
|
||||||
if err := client.ConnectNode(nodes[0].ID, nodes[1].ID); err != nil { |
|
||||||
t.Fatalf("error connecting nodes: %s", err) |
|
||||||
} |
|
||||||
|
|
||||||
// store some state in the test services
|
|
||||||
states := make([]string, nodeCount) |
|
||||||
for i, node := range nodes { |
|
||||||
rpc, err := client.RPCClient(context.Background(), node.ID) |
|
||||||
if err != nil { |
|
||||||
t.Fatalf("error getting RPC client: %s", err) |
|
||||||
} |
|
||||||
defer rpc.Close() |
|
||||||
state := fmt.Sprintf("%x", rand.Int()) |
|
||||||
if err := rpc.Call(nil, "test_setState", []byte(state)); err != nil { |
|
||||||
t.Fatalf("error setting service state: %s", err) |
|
||||||
} |
|
||||||
states[i] = state |
|
||||||
} |
|
||||||
<-eventsDone |
|
||||||
// create a snapshot
|
|
||||||
snap, err := client.CreateSnapshot() |
|
||||||
if err != nil { |
|
||||||
t.Fatalf("error creating snapshot: %s", err) |
|
||||||
} |
|
||||||
for i, state := range states { |
|
||||||
gotState := snap.Nodes[i].Snapshots["test"] |
|
||||||
if string(gotState) != state { |
|
||||||
t.Fatalf("expected snapshot state %q, got %q", state, gotState) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// create another network
|
|
||||||
network2, s := testHTTPServer(t) |
|
||||||
defer s.Close() |
|
||||||
client = NewClient(s.URL) |
|
||||||
count = 1 |
|
||||||
eventSub = network2.Events().Subscribe(eventsDoneChan) |
|
||||||
go func() { |
|
||||||
defer eventSub.Unsubscribe() |
|
||||||
for event := range eventsDoneChan { |
|
||||||
if event.Type == EventTypeConn && !event.Control { |
|
||||||
count-- |
|
||||||
if count == 0 { |
|
||||||
eventsDone <- struct{}{} |
|
||||||
return |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
}() |
|
||||||
|
|
||||||
// subscribe to events so we can check them later
|
|
||||||
events := make(chan *Event, 100) |
|
||||||
var opts SubscribeOpts |
|
||||||
sub, err := client.SubscribeNetwork(events, opts) |
|
||||||
if err != nil { |
|
||||||
t.Fatalf("error subscribing to network events: %s", err) |
|
||||||
} |
|
||||||
defer sub.Unsubscribe() |
|
||||||
|
|
||||||
// load the snapshot
|
|
||||||
if err := client.LoadSnapshot(snap); err != nil { |
|
||||||
t.Fatalf("error loading snapshot: %s", err) |
|
||||||
} |
|
||||||
<-eventsDone |
|
||||||
|
|
||||||
// check the nodes and connection exists
|
|
||||||
net, err := client.GetNetwork() |
|
||||||
if err != nil { |
|
||||||
t.Fatalf("error getting network: %s", err) |
|
||||||
} |
|
||||||
if len(net.Nodes) != nodeCount { |
|
||||||
t.Fatalf("expected network to have %d nodes, got %d", nodeCount, len(net.Nodes)) |
|
||||||
} |
|
||||||
for i, node := range nodes { |
|
||||||
id := net.Nodes[i].ID().String() |
|
||||||
if id != node.ID { |
|
||||||
t.Fatalf("expected node %d to have ID %s, got %s", i, node.ID, id) |
|
||||||
} |
|
||||||
} |
|
||||||
if len(net.Conns) != 1 { |
|
||||||
t.Fatalf("expected network to have 1 connection, got %d", len(net.Conns)) |
|
||||||
} |
|
||||||
conn := net.Conns[0] |
|
||||||
if conn.One.String() != nodes[0].ID { |
|
||||||
t.Fatalf("expected connection to have one=%q, got one=%q", nodes[0].ID, conn.One) |
|
||||||
} |
|
||||||
if conn.Other.String() != nodes[1].ID { |
|
||||||
t.Fatalf("expected connection to have other=%q, got other=%q", nodes[1].ID, conn.Other) |
|
||||||
} |
|
||||||
if !conn.Up { |
|
||||||
t.Fatal("should be up") |
|
||||||
} |
|
||||||
|
|
||||||
// check the node states were restored
|
|
||||||
for i, node := range nodes { |
|
||||||
rpc, err := client.RPCClient(context.Background(), node.ID) |
|
||||||
if err != nil { |
|
||||||
t.Fatalf("error getting RPC client: %s", err) |
|
||||||
} |
|
||||||
defer rpc.Close() |
|
||||||
var state []byte |
|
||||||
if err := rpc.Call(&state, "test_getState"); err != nil { |
|
||||||
t.Fatalf("error getting service state: %s", err) |
|
||||||
} |
|
||||||
if string(state) != states[i] { |
|
||||||
t.Fatalf("expected snapshot state %q, got %q", states[i], state) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// check we got all the events
|
|
||||||
x := &expectEvents{t, events, sub} |
|
||||||
x.expect( |
|
||||||
x.nodeEvent(nodes[0].ID, false), |
|
||||||
x.nodeEvent(nodes[0].ID, true), |
|
||||||
x.nodeEvent(nodes[1].ID, false), |
|
||||||
x.nodeEvent(nodes[1].ID, true), |
|
||||||
x.connEvent(nodes[0].ID, nodes[1].ID, false), |
|
||||||
x.connEvent(nodes[0].ID, nodes[1].ID, true), |
|
||||||
) |
|
||||||
} |
|
||||||
|
|
||||||
// TestMsgFilterPassMultiple tests streaming message events using a filter
|
|
||||||
// with multiple protocols
|
|
||||||
func TestMsgFilterPassMultiple(t *testing.T) { |
|
||||||
// start the server
|
|
||||||
_, s := testHTTPServer(t) |
|
||||||
defer s.Close() |
|
||||||
|
|
||||||
// subscribe to events with a message filter
|
|
||||||
client := NewClient(s.URL) |
|
||||||
events := make(chan *Event, 10) |
|
||||||
opts := SubscribeOpts{ |
|
||||||
Filter: "prb:0-test:0", |
|
||||||
} |
|
||||||
sub, err := client.SubscribeNetwork(events, opts) |
|
||||||
if err != nil { |
|
||||||
t.Fatalf("error subscribing to network events: %s", err) |
|
||||||
} |
|
||||||
defer sub.Unsubscribe() |
|
||||||
|
|
||||||
// start a simulation network
|
|
||||||
startTestNetwork(t, client) |
|
||||||
|
|
||||||
// check we got the expected events
|
|
||||||
x := &expectEvents{t, events, sub} |
|
||||||
x.expectMsgs(map[MsgFilter]int{ |
|
||||||
{"test", 0}: 2, |
|
||||||
{"prb", 0}: 2, |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
// TestMsgFilterPassWildcard tests streaming message events using a filter
|
|
||||||
// with a code wildcard
|
|
||||||
func TestMsgFilterPassWildcard(t *testing.T) { |
|
||||||
// start the server
|
|
||||||
_, s := testHTTPServer(t) |
|
||||||
defer s.Close() |
|
||||||
|
|
||||||
// subscribe to events with a message filter
|
|
||||||
client := NewClient(s.URL) |
|
||||||
events := make(chan *Event, 10) |
|
||||||
opts := SubscribeOpts{ |
|
||||||
Filter: "prb:0,2-test:*", |
|
||||||
} |
|
||||||
sub, err := client.SubscribeNetwork(events, opts) |
|
||||||
if err != nil { |
|
||||||
t.Fatalf("error subscribing to network events: %s", err) |
|
||||||
} |
|
||||||
defer sub.Unsubscribe() |
|
||||||
|
|
||||||
// start a simulation network
|
|
||||||
startTestNetwork(t, client) |
|
||||||
|
|
||||||
// check we got the expected events
|
|
||||||
x := &expectEvents{t, events, sub} |
|
||||||
x.expectMsgs(map[MsgFilter]int{ |
|
||||||
{"test", 2}: 2, |
|
||||||
{"test", 1}: 2, |
|
||||||
{"test", 0}: 2, |
|
||||||
{"prb", 0}: 2, |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
// TestMsgFilterPassSingle tests streaming message events using a filter
|
|
||||||
// with a single protocol and code
|
|
||||||
func TestMsgFilterPassSingle(t *testing.T) { |
|
||||||
// start the server
|
|
||||||
_, s := testHTTPServer(t) |
|
||||||
defer s.Close() |
|
||||||
|
|
||||||
// subscribe to events with a message filter
|
|
||||||
client := NewClient(s.URL) |
|
||||||
events := make(chan *Event, 10) |
|
||||||
opts := SubscribeOpts{ |
|
||||||
Filter: "dum:0", |
|
||||||
} |
|
||||||
sub, err := client.SubscribeNetwork(events, opts) |
|
||||||
if err != nil { |
|
||||||
t.Fatalf("error subscribing to network events: %s", err) |
|
||||||
} |
|
||||||
defer sub.Unsubscribe() |
|
||||||
|
|
||||||
// start a simulation network
|
|
||||||
startTestNetwork(t, client) |
|
||||||
|
|
||||||
// check we got the expected events
|
|
||||||
x := &expectEvents{t, events, sub} |
|
||||||
x.expectMsgs(map[MsgFilter]int{ |
|
||||||
{"dum", 0}: 2, |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
// TestMsgFilterFailBadParams tests streaming message events using an invalid
|
|
||||||
// filter
|
|
||||||
func TestMsgFilterFailBadParams(t *testing.T) { |
|
||||||
// start the server
|
|
||||||
_, s := testHTTPServer(t) |
|
||||||
defer s.Close() |
|
||||||
|
|
||||||
client := NewClient(s.URL) |
|
||||||
events := make(chan *Event, 10) |
|
||||||
opts := SubscribeOpts{ |
|
||||||
Filter: "foo:", |
|
||||||
} |
|
||||||
_, err := client.SubscribeNetwork(events, opts) |
|
||||||
if err == nil { |
|
||||||
t.Fatalf("expected event subscription to fail but succeeded!") |
|
||||||
} |
|
||||||
|
|
||||||
opts.Filter = "bzz:aa" |
|
||||||
_, err = client.SubscribeNetwork(events, opts) |
|
||||||
if err == nil { |
|
||||||
t.Fatalf("expected event subscription to fail but succeeded!") |
|
||||||
} |
|
||||||
|
|
||||||
opts.Filter = "invalid" |
|
||||||
_, err = client.SubscribeNetwork(events, opts) |
|
||||||
if err == nil { |
|
||||||
t.Fatalf("expected event subscription to fail but succeeded!") |
|
||||||
} |
|
||||||
} |
|
@ -1,197 +0,0 @@ |
|||||||
// Copyright 2017 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library 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 Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
// Package simulations simulates p2p networks.
|
|
||||||
// A mocker simulates starting and stopping real nodes in a network.
|
|
||||||
package simulations |
|
||||||
|
|
||||||
import ( |
|
||||||
"fmt" |
|
||||||
"math/rand" |
|
||||||
"sync" |
|
||||||
"time" |
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/log" |
|
||||||
"github.com/ethereum/go-ethereum/p2p/enode" |
|
||||||
"github.com/ethereum/go-ethereum/p2p/simulations/adapters" |
|
||||||
) |
|
||||||
|
|
||||||
// a map of mocker names to its function
|
|
||||||
var mockerList = map[string]func(net *Network, quit chan struct{}, nodeCount int){ |
|
||||||
"startStop": startStop, |
|
||||||
"probabilistic": probabilistic, |
|
||||||
"boot": boot, |
|
||||||
} |
|
||||||
|
|
||||||
// LookupMocker looks a mocker by its name, returns the mockerFn
|
|
||||||
func LookupMocker(mockerType string) func(net *Network, quit chan struct{}, nodeCount int) { |
|
||||||
return mockerList[mockerType] |
|
||||||
} |
|
||||||
|
|
||||||
// GetMockerList returns a list of mockers (keys of the map)
|
|
||||||
// Useful for frontend to build available mocker selection
|
|
||||||
func GetMockerList() []string { |
|
||||||
list := make([]string, 0, len(mockerList)) |
|
||||||
for k := range mockerList { |
|
||||||
list = append(list, k) |
|
||||||
} |
|
||||||
return list |
|
||||||
} |
|
||||||
|
|
||||||
// The boot mockerFn only connects the node in a ring and doesn't do anything else
|
|
||||||
func boot(net *Network, quit chan struct{}, nodeCount int) { |
|
||||||
_, err := connectNodesInRing(net, nodeCount) |
|
||||||
if err != nil { |
|
||||||
panic("Could not startup node network for mocker") |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// The startStop mockerFn stops and starts nodes in a defined period (ticker)
|
|
||||||
func startStop(net *Network, quit chan struct{}, nodeCount int) { |
|
||||||
nodes, err := connectNodesInRing(net, nodeCount) |
|
||||||
if err != nil { |
|
||||||
panic("Could not startup node network for mocker") |
|
||||||
} |
|
||||||
var ( |
|
||||||
tick = time.NewTicker(10 * time.Second) |
|
||||||
timer = time.NewTimer(3 * time.Second) |
|
||||||
) |
|
||||||
defer tick.Stop() |
|
||||||
defer timer.Stop() |
|
||||||
|
|
||||||
for { |
|
||||||
select { |
|
||||||
case <-quit: |
|
||||||
log.Info("Terminating simulation loop") |
|
||||||
return |
|
||||||
case <-tick.C: |
|
||||||
id := nodes[rand.Intn(len(nodes))] |
|
||||||
log.Info("stopping node", "id", id) |
|
||||||
if err := net.Stop(id); err != nil { |
|
||||||
log.Error("error stopping node", "id", id, "err", err) |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
timer.Reset(3 * time.Second) |
|
||||||
select { |
|
||||||
case <-quit: |
|
||||||
log.Info("Terminating simulation loop") |
|
||||||
return |
|
||||||
case <-timer.C: |
|
||||||
} |
|
||||||
|
|
||||||
log.Debug("starting node", "id", id) |
|
||||||
if err := net.Start(id); err != nil { |
|
||||||
log.Error("error starting node", "id", id, "err", err) |
|
||||||
return |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// The probabilistic mocker func has a more probabilistic pattern
|
|
||||||
// (the implementation could probably be improved):
|
|
||||||
// nodes are connected in a ring, then a varying number of random nodes is selected,
|
|
||||||
// mocker then stops and starts them in random intervals, and continues the loop
|
|
||||||
func probabilistic(net *Network, quit chan struct{}, nodeCount int) { |
|
||||||
nodes, err := connectNodesInRing(net, nodeCount) |
|
||||||
if err != nil { |
|
||||||
select { |
|
||||||
case <-quit: |
|
||||||
//error may be due to abortion of mocking; so the quit channel is closed
|
|
||||||
return |
|
||||||
default: |
|
||||||
panic("Could not startup node network for mocker") |
|
||||||
} |
|
||||||
} |
|
||||||
for { |
|
||||||
select { |
|
||||||
case <-quit: |
|
||||||
log.Info("Terminating simulation loop") |
|
||||||
return |
|
||||||
default: |
|
||||||
} |
|
||||||
var lowid, highid int |
|
||||||
var wg sync.WaitGroup |
|
||||||
randWait := time.Duration(rand.Intn(5000)+1000) * time.Millisecond |
|
||||||
rand1 := rand.Intn(nodeCount - 1) |
|
||||||
rand2 := rand.Intn(nodeCount - 1) |
|
||||||
if rand1 <= rand2 { |
|
||||||
lowid = rand1 |
|
||||||
highid = rand2 |
|
||||||
} else if rand1 > rand2 { |
|
||||||
highid = rand1 |
|
||||||
lowid = rand2 |
|
||||||
} |
|
||||||
var steps = highid - lowid |
|
||||||
wg.Add(steps) |
|
||||||
for i := lowid; i < highid; i++ { |
|
||||||
select { |
|
||||||
case <-quit: |
|
||||||
log.Info("Terminating simulation loop") |
|
||||||
return |
|
||||||
case <-time.After(randWait): |
|
||||||
} |
|
||||||
log.Debug(fmt.Sprintf("node %v shutting down", nodes[i])) |
|
||||||
err := net.Stop(nodes[i]) |
|
||||||
if err != nil { |
|
||||||
log.Error("Error stopping node", "node", nodes[i]) |
|
||||||
wg.Done() |
|
||||||
continue |
|
||||||
} |
|
||||||
go func(id enode.ID) { |
|
||||||
time.Sleep(randWait) |
|
||||||
err := net.Start(id) |
|
||||||
if err != nil { |
|
||||||
log.Error("Error starting node", "node", id) |
|
||||||
} |
|
||||||
wg.Done() |
|
||||||
}(nodes[i]) |
|
||||||
} |
|
||||||
wg.Wait() |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// connect nodeCount number of nodes in a ring
|
|
||||||
func connectNodesInRing(net *Network, nodeCount int) ([]enode.ID, error) { |
|
||||||
ids := make([]enode.ID, nodeCount) |
|
||||||
for i := 0; i < nodeCount; i++ { |
|
||||||
conf := adapters.RandomNodeConfig() |
|
||||||
node, err := net.NewNodeWithConfig(conf) |
|
||||||
if err != nil { |
|
||||||
log.Error("Error creating a node!", "err", err) |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
ids[i] = node.ID() |
|
||||||
} |
|
||||||
|
|
||||||
for _, id := range ids { |
|
||||||
if err := net.Start(id); err != nil { |
|
||||||
log.Error("Error starting a node!", "err", err) |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
log.Debug(fmt.Sprintf("node %v starting up", id)) |
|
||||||
} |
|
||||||
for i, id := range ids { |
|
||||||
peerID := ids[(i+1)%len(ids)] |
|
||||||
if err := net.Connect(id, peerID); err != nil { |
|
||||||
log.Error("Error connecting a node to a peer!", "err", err) |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
return ids, nil |
|
||||||
} |
|
@ -1,174 +0,0 @@ |
|||||||
// Copyright 2017 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library 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 Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
// Package simulations simulates p2p networks.
|
|
||||||
// A mocker simulates starting and stopping real nodes in a network.
|
|
||||||
package simulations |
|
||||||
|
|
||||||
import ( |
|
||||||
"encoding/json" |
|
||||||
"net/http" |
|
||||||
"net/url" |
|
||||||
"strconv" |
|
||||||
"sync" |
|
||||||
"testing" |
|
||||||
"time" |
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/enode" |
|
||||||
) |
|
||||||
|
|
||||||
func TestMocker(t *testing.T) { |
|
||||||
//start the simulation HTTP server
|
|
||||||
_, s := testHTTPServer(t) |
|
||||||
defer s.Close() |
|
||||||
|
|
||||||
//create a client
|
|
||||||
client := NewClient(s.URL) |
|
||||||
|
|
||||||
//start the network
|
|
||||||
err := client.StartNetwork() |
|
||||||
if err != nil { |
|
||||||
t.Fatalf("Could not start test network: %s", err) |
|
||||||
} |
|
||||||
//stop the network to terminate
|
|
||||||
defer func() { |
|
||||||
err = client.StopNetwork() |
|
||||||
if err != nil { |
|
||||||
t.Fatalf("Could not stop test network: %s", err) |
|
||||||
} |
|
||||||
}() |
|
||||||
|
|
||||||
//get the list of available mocker types
|
|
||||||
resp, err := http.Get(s.URL + "/mocker") |
|
||||||
if err != nil { |
|
||||||
t.Fatalf("Could not get mocker list: %s", err) |
|
||||||
} |
|
||||||
defer resp.Body.Close() |
|
||||||
|
|
||||||
if resp.StatusCode != 200 { |
|
||||||
t.Fatalf("Invalid Status Code received, expected 200, got %d", resp.StatusCode) |
|
||||||
} |
|
||||||
|
|
||||||
//check the list is at least 1 in size
|
|
||||||
var mockerlist []string |
|
||||||
err = json.NewDecoder(resp.Body).Decode(&mockerlist) |
|
||||||
if err != nil { |
|
||||||
t.Fatalf("Error decoding JSON mockerlist: %s", err) |
|
||||||
} |
|
||||||
|
|
||||||
if len(mockerlist) < 1 { |
|
||||||
t.Fatalf("No mockers available") |
|
||||||
} |
|
||||||
|
|
||||||
nodeCount := 10 |
|
||||||
var wg sync.WaitGroup |
|
||||||
|
|
||||||
events := make(chan *Event, 10) |
|
||||||
var opts SubscribeOpts |
|
||||||
sub, err := client.SubscribeNetwork(events, opts) |
|
||||||
defer sub.Unsubscribe() |
|
||||||
|
|
||||||
// wait until all nodes are started and connected
|
|
||||||
// store every node up event in a map (value is irrelevant, mimic Set datatype)
|
|
||||||
nodemap := make(map[enode.ID]bool) |
|
||||||
nodesComplete := false |
|
||||||
connCount := 0 |
|
||||||
wg.Add(1) |
|
||||||
go func() { |
|
||||||
defer wg.Done() |
|
||||||
|
|
||||||
for connCount < (nodeCount-1)*2 { |
|
||||||
select { |
|
||||||
case event := <-events: |
|
||||||
if isNodeUp(event) { |
|
||||||
//add the correspondent node ID to the map
|
|
||||||
nodemap[event.Node.Config.ID] = true |
|
||||||
//this means all nodes got a nodeUp event, so we can continue the test
|
|
||||||
if len(nodemap) == nodeCount { |
|
||||||
nodesComplete = true |
|
||||||
} |
|
||||||
} else if event.Conn != nil && nodesComplete { |
|
||||||
connCount += 1 |
|
||||||
} |
|
||||||
case <-time.After(30 * time.Second): |
|
||||||
t.Errorf("Timeout waiting for nodes being started up!") |
|
||||||
return |
|
||||||
} |
|
||||||
} |
|
||||||
}() |
|
||||||
|
|
||||||
//take the last element of the mockerlist as the default mocker-type to ensure one is enabled
|
|
||||||
mockertype := mockerlist[len(mockerlist)-1] |
|
||||||
//still, use hardcoded "probabilistic" one if available ;)
|
|
||||||
for _, m := range mockerlist { |
|
||||||
if m == "probabilistic" { |
|
||||||
mockertype = m |
|
||||||
break |
|
||||||
} |
|
||||||
} |
|
||||||
//start the mocker with nodeCount number of nodes
|
|
||||||
resp, err = http.PostForm(s.URL+"/mocker/start", url.Values{"mocker-type": {mockertype}, "node-count": {strconv.Itoa(nodeCount)}}) |
|
||||||
if err != nil { |
|
||||||
t.Fatalf("Could not start mocker: %s", err) |
|
||||||
} |
|
||||||
resp.Body.Close() |
|
||||||
if resp.StatusCode != 200 { |
|
||||||
t.Fatalf("Invalid Status Code received for starting mocker, expected 200, got %d", resp.StatusCode) |
|
||||||
} |
|
||||||
|
|
||||||
wg.Wait() |
|
||||||
|
|
||||||
//check there are nodeCount number of nodes in the network
|
|
||||||
nodesInfo, err := client.GetNodes() |
|
||||||
if err != nil { |
|
||||||
t.Fatalf("Could not get nodes list: %s", err) |
|
||||||
} |
|
||||||
|
|
||||||
if len(nodesInfo) != nodeCount { |
|
||||||
t.Fatalf("Expected %d number of nodes, got: %d", nodeCount, len(nodesInfo)) |
|
||||||
} |
|
||||||
|
|
||||||
//stop the mocker
|
|
||||||
resp, err = http.Post(s.URL+"/mocker/stop", "", nil) |
|
||||||
if err != nil { |
|
||||||
t.Fatalf("Could not stop mocker: %s", err) |
|
||||||
} |
|
||||||
resp.Body.Close() |
|
||||||
if resp.StatusCode != 200 { |
|
||||||
t.Fatalf("Invalid Status Code received for stopping mocker, expected 200, got %d", resp.StatusCode) |
|
||||||
} |
|
||||||
|
|
||||||
//reset the network
|
|
||||||
resp, err = http.Post(s.URL+"/reset", "", nil) |
|
||||||
if err != nil { |
|
||||||
t.Fatalf("Could not reset network: %s", err) |
|
||||||
} |
|
||||||
resp.Body.Close() |
|
||||||
|
|
||||||
//now the number of nodes in the network should be zero
|
|
||||||
nodesInfo, err = client.GetNodes() |
|
||||||
if err != nil { |
|
||||||
t.Fatalf("Could not get nodes list: %s", err) |
|
||||||
} |
|
||||||
|
|
||||||
if len(nodesInfo) != 0 { |
|
||||||
t.Fatalf("Expected empty list of nodes, got: %d", len(nodesInfo)) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func isNodeUp(event *Event) bool { |
|
||||||
return event.Node != nil && event.Node.Up() |
|
||||||
} |
|
File diff suppressed because it is too large
Load Diff
@ -1,872 +0,0 @@ |
|||||||
// Copyright 2017 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library 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 Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package simulations |
|
||||||
|
|
||||||
import ( |
|
||||||
"bytes" |
|
||||||
"context" |
|
||||||
"encoding/json" |
|
||||||
"fmt" |
|
||||||
"reflect" |
|
||||||
"strconv" |
|
||||||
"strings" |
|
||||||
"testing" |
|
||||||
"time" |
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/log" |
|
||||||
"github.com/ethereum/go-ethereum/node" |
|
||||||
"github.com/ethereum/go-ethereum/p2p/enode" |
|
||||||
"github.com/ethereum/go-ethereum/p2p/simulations/adapters" |
|
||||||
) |
|
||||||
|
|
||||||
// Tests that a created snapshot with a minimal service only contains the expected connections
|
|
||||||
// and that a network when loaded with this snapshot only contains those same connections
|
|
||||||
func TestSnapshot(t *testing.T) { |
|
||||||
// PART I
|
|
||||||
// create snapshot from ring network
|
|
||||||
|
|
||||||
// this is a minimal service, whose protocol will take exactly one message OR close of connection before quitting
|
|
||||||
adapter := adapters.NewSimAdapter(adapters.LifecycleConstructors{ |
|
||||||
"noopwoop": func(ctx *adapters.ServiceContext, stack *node.Node) (node.Lifecycle, error) { |
|
||||||
return NewNoopService(nil), nil |
|
||||||
}, |
|
||||||
}) |
|
||||||
|
|
||||||
// create network
|
|
||||||
network := NewNetwork(adapter, &NetworkConfig{ |
|
||||||
DefaultService: "noopwoop", |
|
||||||
}) |
|
||||||
// \todo consider making a member of network, set to true threadsafe when shutdown
|
|
||||||
runningOne := true |
|
||||||
defer func() { |
|
||||||
if runningOne { |
|
||||||
network.Shutdown() |
|
||||||
} |
|
||||||
}() |
|
||||||
|
|
||||||
// create and start nodes
|
|
||||||
nodeCount := 20 |
|
||||||
ids := make([]enode.ID, nodeCount) |
|
||||||
for i := 0; i < nodeCount; i++ { |
|
||||||
conf := adapters.RandomNodeConfig() |
|
||||||
node, err := network.NewNodeWithConfig(conf) |
|
||||||
if err != nil { |
|
||||||
t.Fatalf("error creating node: %s", err) |
|
||||||
} |
|
||||||
if err := network.Start(node.ID()); err != nil { |
|
||||||
t.Fatalf("error starting node: %s", err) |
|
||||||
} |
|
||||||
ids[i] = node.ID() |
|
||||||
} |
|
||||||
|
|
||||||
// subscribe to peer events
|
|
||||||
evC := make(chan *Event) |
|
||||||
sub := network.Events().Subscribe(evC) |
|
||||||
defer sub.Unsubscribe() |
|
||||||
|
|
||||||
// connect nodes in a ring
|
|
||||||
// spawn separate thread to avoid deadlock in the event listeners
|
|
||||||
connectErr := make(chan error, 1) |
|
||||||
go func() { |
|
||||||
for i, id := range ids { |
|
||||||
peerID := ids[(i+1)%len(ids)] |
|
||||||
if err := network.Connect(id, peerID); err != nil { |
|
||||||
connectErr <- err |
|
||||||
return |
|
||||||
} |
|
||||||
} |
|
||||||
}() |
|
||||||
|
|
||||||
// collect connection events up to expected number
|
|
||||||
ctx, cancel := context.WithTimeout(context.TODO(), time.Second) |
|
||||||
defer cancel() |
|
||||||
checkIds := make(map[enode.ID][]enode.ID) |
|
||||||
connEventCount := nodeCount |
|
||||||
OUTER: |
|
||||||
for { |
|
||||||
select { |
|
||||||
case <-ctx.Done(): |
|
||||||
t.Fatal(ctx.Err()) |
|
||||||
case err := <-connectErr: |
|
||||||
t.Fatal(err) |
|
||||||
case ev := <-evC: |
|
||||||
if ev.Type == EventTypeConn && !ev.Control { |
|
||||||
// fail on any disconnect
|
|
||||||
if !ev.Conn.Up { |
|
||||||
t.Fatalf("unexpected disconnect: %v -> %v", ev.Conn.One, ev.Conn.Other) |
|
||||||
} |
|
||||||
checkIds[ev.Conn.One] = append(checkIds[ev.Conn.One], ev.Conn.Other) |
|
||||||
checkIds[ev.Conn.Other] = append(checkIds[ev.Conn.Other], ev.Conn.One) |
|
||||||
connEventCount-- |
|
||||||
log.Debug("ev", "count", connEventCount) |
|
||||||
if connEventCount == 0 { |
|
||||||
break OUTER |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// create snapshot of current network
|
|
||||||
snap, err := network.Snapshot() |
|
||||||
if err != nil { |
|
||||||
t.Fatal(err) |
|
||||||
} |
|
||||||
j, err := json.Marshal(snap) |
|
||||||
if err != nil { |
|
||||||
t.Fatal(err) |
|
||||||
} |
|
||||||
log.Debug("snapshot taken", "nodes", len(snap.Nodes), "conns", len(snap.Conns), "json", string(j)) |
|
||||||
|
|
||||||
// verify that the snap element numbers check out
|
|
||||||
if len(checkIds) != len(snap.Conns) || len(checkIds) != len(snap.Nodes) { |
|
||||||
t.Fatalf("snapshot wrong node,conn counts %d,%d != %d", len(snap.Nodes), len(snap.Conns), len(checkIds)) |
|
||||||
} |
|
||||||
|
|
||||||
// shut down sim network
|
|
||||||
runningOne = false |
|
||||||
sub.Unsubscribe() |
|
||||||
network.Shutdown() |
|
||||||
|
|
||||||
// check that we have all the expected connections in the snapshot
|
|
||||||
for nodid, nodConns := range checkIds { |
|
||||||
for _, nodConn := range nodConns { |
|
||||||
var match bool |
|
||||||
for _, snapConn := range snap.Conns { |
|
||||||
if snapConn.One == nodid && snapConn.Other == nodConn { |
|
||||||
match = true |
|
||||||
break |
|
||||||
} else if snapConn.Other == nodid && snapConn.One == nodConn { |
|
||||||
match = true |
|
||||||
break |
|
||||||
} |
|
||||||
} |
|
||||||
if !match { |
|
||||||
t.Fatalf("snapshot missing conn %v -> %v", nodid, nodConn) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
log.Info("snapshot checked") |
|
||||||
|
|
||||||
// PART II
|
|
||||||
// load snapshot and verify that exactly same connections are formed
|
|
||||||
|
|
||||||
adapter = adapters.NewSimAdapter(adapters.LifecycleConstructors{ |
|
||||||
"noopwoop": func(ctx *adapters.ServiceContext, stack *node.Node) (node.Lifecycle, error) { |
|
||||||
return NewNoopService(nil), nil |
|
||||||
}, |
|
||||||
}) |
|
||||||
network = NewNetwork(adapter, &NetworkConfig{ |
|
||||||
DefaultService: "noopwoop", |
|
||||||
}) |
|
||||||
defer func() { |
|
||||||
network.Shutdown() |
|
||||||
}() |
|
||||||
|
|
||||||
// subscribe to peer events
|
|
||||||
// every node up and conn up event will generate one additional control event
|
|
||||||
// therefore multiply the count by two
|
|
||||||
evC = make(chan *Event, (len(snap.Conns)*2)+(len(snap.Nodes)*2)) |
|
||||||
sub = network.Events().Subscribe(evC) |
|
||||||
defer sub.Unsubscribe() |
|
||||||
|
|
||||||
// load the snapshot
|
|
||||||
// spawn separate thread to avoid deadlock in the event listeners
|
|
||||||
err = network.Load(snap) |
|
||||||
if err != nil { |
|
||||||
t.Fatal(err) |
|
||||||
} |
|
||||||
|
|
||||||
// collect connection events up to expected number
|
|
||||||
ctx, cancel = context.WithTimeout(context.TODO(), time.Second*3) |
|
||||||
defer cancel() |
|
||||||
|
|
||||||
connEventCount = nodeCount |
|
||||||
|
|
||||||
OuterTwo: |
|
||||||
for { |
|
||||||
select { |
|
||||||
case <-ctx.Done(): |
|
||||||
t.Fatal(ctx.Err()) |
|
||||||
case ev := <-evC: |
|
||||||
if ev.Type == EventTypeConn && !ev.Control { |
|
||||||
// fail on any disconnect
|
|
||||||
if !ev.Conn.Up { |
|
||||||
t.Fatalf("unexpected disconnect: %v -> %v", ev.Conn.One, ev.Conn.Other) |
|
||||||
} |
|
||||||
log.Debug("conn", "on", ev.Conn.One, "other", ev.Conn.Other) |
|
||||||
checkIds[ev.Conn.One] = append(checkIds[ev.Conn.One], ev.Conn.Other) |
|
||||||
checkIds[ev.Conn.Other] = append(checkIds[ev.Conn.Other], ev.Conn.One) |
|
||||||
connEventCount-- |
|
||||||
log.Debug("ev", "count", connEventCount) |
|
||||||
if connEventCount == 0 { |
|
||||||
break OuterTwo |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// check that we have all expected connections in the network
|
|
||||||
for _, snapConn := range snap.Conns { |
|
||||||
var match bool |
|
||||||
for nodid, nodConns := range checkIds { |
|
||||||
for _, nodConn := range nodConns { |
|
||||||
if snapConn.One == nodid && snapConn.Other == nodConn { |
|
||||||
match = true |
|
||||||
break |
|
||||||
} else if snapConn.Other == nodid && snapConn.One == nodConn { |
|
||||||
match = true |
|
||||||
break |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
if !match { |
|
||||||
t.Fatalf("network missing conn %v -> %v", snapConn.One, snapConn.Other) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// verify that network didn't generate any other additional connection events after the ones we have collected within a reasonable period of time
|
|
||||||
ctx, cancel = context.WithTimeout(context.TODO(), time.Second) |
|
||||||
defer cancel() |
|
||||||
select { |
|
||||||
case <-ctx.Done(): |
|
||||||
case ev := <-evC: |
|
||||||
if ev.Type == EventTypeConn { |
|
||||||
t.Fatalf("Superfluous conn found %v -> %v", ev.Conn.One, ev.Conn.Other) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// This test validates if all connections from the snapshot
|
|
||||||
// are created in the network.
|
|
||||||
t.Run("conns after load", func(t *testing.T) { |
|
||||||
// Create new network.
|
|
||||||
n := NewNetwork( |
|
||||||
adapters.NewSimAdapter(adapters.LifecycleConstructors{ |
|
||||||
"noopwoop": func(ctx *adapters.ServiceContext, stack *node.Node) (node.Lifecycle, error) { |
|
||||||
return NewNoopService(nil), nil |
|
||||||
}, |
|
||||||
}), |
|
||||||
&NetworkConfig{ |
|
||||||
DefaultService: "noopwoop", |
|
||||||
}, |
|
||||||
) |
|
||||||
defer n.Shutdown() |
|
||||||
|
|
||||||
// Load the same snapshot.
|
|
||||||
err := n.Load(snap) |
|
||||||
if err != nil { |
|
||||||
t.Fatal(err) |
|
||||||
} |
|
||||||
|
|
||||||
// Check every connection from the snapshot
|
|
||||||
// if it is in the network, too.
|
|
||||||
for _, c := range snap.Conns { |
|
||||||
if n.GetConn(c.One, c.Other) == nil { |
|
||||||
t.Errorf("missing connection: %s -> %s", c.One, c.Other) |
|
||||||
} |
|
||||||
} |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
// TestNetworkSimulation creates a multi-node simulation network with each node
|
|
||||||
// connected in a ring topology, checks that all nodes successfully handshake
|
|
||||||
// with each other and that a snapshot fully represents the desired topology
|
|
||||||
func TestNetworkSimulation(t *testing.T) { |
|
||||||
// create simulation network with 20 testService nodes
|
|
||||||
adapter := adapters.NewSimAdapter(adapters.LifecycleConstructors{ |
|
||||||
"test": newTestService, |
|
||||||
}) |
|
||||||
network := NewNetwork(adapter, &NetworkConfig{ |
|
||||||
DefaultService: "test", |
|
||||||
}) |
|
||||||
defer network.Shutdown() |
|
||||||
nodeCount := 20 |
|
||||||
ids := make([]enode.ID, nodeCount) |
|
||||||
for i := 0; i < nodeCount; i++ { |
|
||||||
conf := adapters.RandomNodeConfig() |
|
||||||
node, err := network.NewNodeWithConfig(conf) |
|
||||||
if err != nil { |
|
||||||
t.Fatalf("error creating node: %s", err) |
|
||||||
} |
|
||||||
if err := network.Start(node.ID()); err != nil { |
|
||||||
t.Fatalf("error starting node: %s", err) |
|
||||||
} |
|
||||||
ids[i] = node.ID() |
|
||||||
} |
|
||||||
|
|
||||||
// perform a check which connects the nodes in a ring (so each node is
|
|
||||||
// connected to exactly two peers) and then checks that all nodes
|
|
||||||
// performed two handshakes by checking their peerCount
|
|
||||||
action := func(_ context.Context) error { |
|
||||||
for i, id := range ids { |
|
||||||
peerID := ids[(i+1)%len(ids)] |
|
||||||
if err := network.Connect(id, peerID); err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
} |
|
||||||
return nil |
|
||||||
} |
|
||||||
check := func(ctx context.Context, id enode.ID) (bool, error) { |
|
||||||
// check we haven't run out of time
|
|
||||||
select { |
|
||||||
case <-ctx.Done(): |
|
||||||
return false, ctx.Err() |
|
||||||
default: |
|
||||||
} |
|
||||||
|
|
||||||
// get the node
|
|
||||||
node := network.GetNode(id) |
|
||||||
if node == nil { |
|
||||||
return false, fmt.Errorf("unknown node: %s", id) |
|
||||||
} |
|
||||||
|
|
||||||
// check it has exactly two peers
|
|
||||||
client, err := node.Client() |
|
||||||
if err != nil { |
|
||||||
return false, err |
|
||||||
} |
|
||||||
var peerCount int64 |
|
||||||
if err := client.CallContext(ctx, &peerCount, "test_peerCount"); err != nil { |
|
||||||
return false, err |
|
||||||
} |
|
||||||
switch { |
|
||||||
case peerCount < 2: |
|
||||||
return false, nil |
|
||||||
case peerCount == 2: |
|
||||||
return true, nil |
|
||||||
default: |
|
||||||
return false, fmt.Errorf("unexpected peerCount: %d", peerCount) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
timeout := 30 * time.Second |
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), timeout) |
|
||||||
defer cancel() |
|
||||||
|
|
||||||
// trigger a check every 100ms
|
|
||||||
trigger := make(chan enode.ID) |
|
||||||
go triggerChecks(ctx, ids, trigger, 100*time.Millisecond) |
|
||||||
|
|
||||||
result := NewSimulation(network).Run(ctx, &Step{ |
|
||||||
Action: action, |
|
||||||
Trigger: trigger, |
|
||||||
Expect: &Expectation{ |
|
||||||
Nodes: ids, |
|
||||||
Check: check, |
|
||||||
}, |
|
||||||
}) |
|
||||||
if result.Error != nil { |
|
||||||
t.Fatalf("simulation failed: %s", result.Error) |
|
||||||
} |
|
||||||
|
|
||||||
// take a network snapshot and check it contains the correct topology
|
|
||||||
snap, err := network.Snapshot() |
|
||||||
if err != nil { |
|
||||||
t.Fatal(err) |
|
||||||
} |
|
||||||
if len(snap.Nodes) != nodeCount { |
|
||||||
t.Fatalf("expected snapshot to contain %d nodes, got %d", nodeCount, len(snap.Nodes)) |
|
||||||
} |
|
||||||
if len(snap.Conns) != nodeCount { |
|
||||||
t.Fatalf("expected snapshot to contain %d connections, got %d", nodeCount, len(snap.Conns)) |
|
||||||
} |
|
||||||
for i, id := range ids { |
|
||||||
conn := snap.Conns[i] |
|
||||||
if conn.One != id { |
|
||||||
t.Fatalf("expected conn[%d].One to be %s, got %s", i, id, conn.One) |
|
||||||
} |
|
||||||
peerID := ids[(i+1)%len(ids)] |
|
||||||
if conn.Other != peerID { |
|
||||||
t.Fatalf("expected conn[%d].Other to be %s, got %s", i, peerID, conn.Other) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func createTestNodes(count int, network *Network) (nodes []*Node, err error) { |
|
||||||
for i := 0; i < count; i++ { |
|
||||||
nodeConf := adapters.RandomNodeConfig() |
|
||||||
node, err := network.NewNodeWithConfig(nodeConf) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
if err := network.Start(node.ID()); err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
nodes = append(nodes, node) |
|
||||||
} |
|
||||||
|
|
||||||
return nodes, nil |
|
||||||
} |
|
||||||
|
|
||||||
func createTestNodesWithProperty(property string, count int, network *Network) (propertyNodes []*Node, err error) { |
|
||||||
for i := 0; i < count; i++ { |
|
||||||
nodeConf := adapters.RandomNodeConfig() |
|
||||||
nodeConf.Properties = append(nodeConf.Properties, property) |
|
||||||
|
|
||||||
node, err := network.NewNodeWithConfig(nodeConf) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
if err := network.Start(node.ID()); err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
propertyNodes = append(propertyNodes, node) |
|
||||||
} |
|
||||||
|
|
||||||
return propertyNodes, nil |
|
||||||
} |
|
||||||
|
|
||||||
// TestGetNodeIDs creates a set of nodes and attempts to retrieve their IDs,.
|
|
||||||
// It then tests again whilst excluding a node ID from being returned.
|
|
||||||
// If a node ID is not returned, or more node IDs than expected are returned, the test fails.
|
|
||||||
func TestGetNodeIDs(t *testing.T) { |
|
||||||
adapter := adapters.NewSimAdapter(adapters.LifecycleConstructors{ |
|
||||||
"test": newTestService, |
|
||||||
}) |
|
||||||
network := NewNetwork(adapter, &NetworkConfig{ |
|
||||||
DefaultService: "test", |
|
||||||
}) |
|
||||||
defer network.Shutdown() |
|
||||||
|
|
||||||
numNodes := 5 |
|
||||||
nodes, err := createTestNodes(numNodes, network) |
|
||||||
if err != nil { |
|
||||||
t.Fatalf("Could not create test nodes %v", err) |
|
||||||
} |
|
||||||
|
|
||||||
gotNodeIDs := network.GetNodeIDs() |
|
||||||
if len(gotNodeIDs) != numNodes { |
|
||||||
t.Fatalf("Expected %d nodes, got %d", numNodes, len(gotNodeIDs)) |
|
||||||
} |
|
||||||
|
|
||||||
for _, node1 := range nodes { |
|
||||||
match := false |
|
||||||
for _, node2ID := range gotNodeIDs { |
|
||||||
if bytes.Equal(node1.ID().Bytes(), node2ID.Bytes()) { |
|
||||||
match = true |
|
||||||
break |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
if !match { |
|
||||||
t.Fatalf("A created node was not returned by GetNodes(), ID: %s", node1.ID().String()) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
excludeNodeID := nodes[3].ID() |
|
||||||
gotNodeIDsExcl := network.GetNodeIDs(excludeNodeID) |
|
||||||
if len(gotNodeIDsExcl) != numNodes-1 { |
|
||||||
t.Fatalf("Expected one less node ID to be returned") |
|
||||||
} |
|
||||||
for _, nodeID := range gotNodeIDsExcl { |
|
||||||
if bytes.Equal(excludeNodeID.Bytes(), nodeID.Bytes()) { |
|
||||||
t.Fatalf("GetNodeIDs returned the node ID we excluded, ID: %s", nodeID.String()) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// TestGetNodes creates a set of nodes and attempts to retrieve them again.
|
|
||||||
// It then tests again whilst excluding a node from being returned.
|
|
||||||
// If a node is not returned, or more nodes than expected are returned, the test fails.
|
|
||||||
func TestGetNodes(t *testing.T) { |
|
||||||
adapter := adapters.NewSimAdapter(adapters.LifecycleConstructors{ |
|
||||||
"test": newTestService, |
|
||||||
}) |
|
||||||
network := NewNetwork(adapter, &NetworkConfig{ |
|
||||||
DefaultService: "test", |
|
||||||
}) |
|
||||||
defer network.Shutdown() |
|
||||||
|
|
||||||
numNodes := 5 |
|
||||||
nodes, err := createTestNodes(numNodes, network) |
|
||||||
if err != nil { |
|
||||||
t.Fatalf("Could not create test nodes %v", err) |
|
||||||
} |
|
||||||
|
|
||||||
gotNodes := network.GetNodes() |
|
||||||
if len(gotNodes) != numNodes { |
|
||||||
t.Fatalf("Expected %d nodes, got %d", numNodes, len(gotNodes)) |
|
||||||
} |
|
||||||
|
|
||||||
for _, node1 := range nodes { |
|
||||||
match := false |
|
||||||
for _, node2 := range gotNodes { |
|
||||||
if bytes.Equal(node1.ID().Bytes(), node2.ID().Bytes()) { |
|
||||||
match = true |
|
||||||
break |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
if !match { |
|
||||||
t.Fatalf("A created node was not returned by GetNodes(), ID: %s", node1.ID().String()) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
excludeNodeID := nodes[3].ID() |
|
||||||
gotNodesExcl := network.GetNodes(excludeNodeID) |
|
||||||
if len(gotNodesExcl) != numNodes-1 { |
|
||||||
t.Fatalf("Expected one less node to be returned") |
|
||||||
} |
|
||||||
for _, node := range gotNodesExcl { |
|
||||||
if bytes.Equal(excludeNodeID.Bytes(), node.ID().Bytes()) { |
|
||||||
t.Fatalf("GetNodes returned the node we excluded, ID: %s", node.ID().String()) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// TestGetNodesByID creates a set of nodes and attempts to retrieve a subset of them by ID
|
|
||||||
// If a node is not returned, or more nodes than expected are returned, the test fails.
|
|
||||||
func TestGetNodesByID(t *testing.T) { |
|
||||||
adapter := adapters.NewSimAdapter(adapters.LifecycleConstructors{ |
|
||||||
"test": newTestService, |
|
||||||
}) |
|
||||||
network := NewNetwork(adapter, &NetworkConfig{ |
|
||||||
DefaultService: "test", |
|
||||||
}) |
|
||||||
defer network.Shutdown() |
|
||||||
|
|
||||||
numNodes := 5 |
|
||||||
nodes, err := createTestNodes(numNodes, network) |
|
||||||
if err != nil { |
|
||||||
t.Fatalf("Could not create test nodes: %v", err) |
|
||||||
} |
|
||||||
|
|
||||||
numSubsetNodes := 2 |
|
||||||
subsetNodes := nodes[0:numSubsetNodes] |
|
||||||
var subsetNodeIDs []enode.ID |
|
||||||
for _, node := range subsetNodes { |
|
||||||
subsetNodeIDs = append(subsetNodeIDs, node.ID()) |
|
||||||
} |
|
||||||
|
|
||||||
gotNodesByID := network.GetNodesByID(subsetNodeIDs) |
|
||||||
if len(gotNodesByID) != numSubsetNodes { |
|
||||||
t.Fatalf("Expected %d nodes, got %d", numSubsetNodes, len(gotNodesByID)) |
|
||||||
} |
|
||||||
|
|
||||||
for _, node1 := range subsetNodes { |
|
||||||
match := false |
|
||||||
for _, node2 := range gotNodesByID { |
|
||||||
if bytes.Equal(node1.ID().Bytes(), node2.ID().Bytes()) { |
|
||||||
match = true |
|
||||||
break |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
if !match { |
|
||||||
t.Fatalf("A created node was not returned by GetNodesByID(), ID: %s", node1.ID().String()) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// TestGetNodesByProperty creates a subset of nodes with a property assigned.
|
|
||||||
// GetNodesByProperty is then checked for correctness by comparing the nodes returned to those initially created.
|
|
||||||
// If a node with a property is not found, or more nodes than expected are returned, the test fails.
|
|
||||||
func TestGetNodesByProperty(t *testing.T) { |
|
||||||
adapter := adapters.NewSimAdapter(adapters.LifecycleConstructors{ |
|
||||||
"test": newTestService, |
|
||||||
}) |
|
||||||
network := NewNetwork(adapter, &NetworkConfig{ |
|
||||||
DefaultService: "test", |
|
||||||
}) |
|
||||||
defer network.Shutdown() |
|
||||||
|
|
||||||
numNodes := 3 |
|
||||||
_, err := createTestNodes(numNodes, network) |
|
||||||
if err != nil { |
|
||||||
t.Fatalf("Failed to create nodes: %v", err) |
|
||||||
} |
|
||||||
|
|
||||||
numPropertyNodes := 3 |
|
||||||
propertyTest := "test" |
|
||||||
propertyNodes, err := createTestNodesWithProperty(propertyTest, numPropertyNodes, network) |
|
||||||
if err != nil { |
|
||||||
t.Fatalf("Failed to create nodes with property: %v", err) |
|
||||||
} |
|
||||||
|
|
||||||
gotNodesByProperty := network.GetNodesByProperty(propertyTest) |
|
||||||
if len(gotNodesByProperty) != numPropertyNodes { |
|
||||||
t.Fatalf("Expected %d nodes with a property, got %d", numPropertyNodes, len(gotNodesByProperty)) |
|
||||||
} |
|
||||||
|
|
||||||
for _, node1 := range propertyNodes { |
|
||||||
match := false |
|
||||||
for _, node2 := range gotNodesByProperty { |
|
||||||
if bytes.Equal(node1.ID().Bytes(), node2.ID().Bytes()) { |
|
||||||
match = true |
|
||||||
break |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
if !match { |
|
||||||
t.Fatalf("A created node with property was not returned by GetNodesByProperty(), ID: %s", node1.ID().String()) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// TestGetNodeIDsByProperty creates a subset of nodes with a property assigned.
|
|
||||||
// GetNodeIDsByProperty is then checked for correctness by comparing the node IDs returned to those initially created.
|
|
||||||
// If a node ID with a property is not found, or more nodes IDs than expected are returned, the test fails.
|
|
||||||
func TestGetNodeIDsByProperty(t *testing.T) { |
|
||||||
adapter := adapters.NewSimAdapter(adapters.LifecycleConstructors{ |
|
||||||
"test": newTestService, |
|
||||||
}) |
|
||||||
network := NewNetwork(adapter, &NetworkConfig{ |
|
||||||
DefaultService: "test", |
|
||||||
}) |
|
||||||
defer network.Shutdown() |
|
||||||
|
|
||||||
numNodes := 3 |
|
||||||
_, err := createTestNodes(numNodes, network) |
|
||||||
if err != nil { |
|
||||||
t.Fatalf("Failed to create nodes: %v", err) |
|
||||||
} |
|
||||||
|
|
||||||
numPropertyNodes := 3 |
|
||||||
propertyTest := "test" |
|
||||||
propertyNodes, err := createTestNodesWithProperty(propertyTest, numPropertyNodes, network) |
|
||||||
if err != nil { |
|
||||||
t.Fatalf("Failed to created nodes with property: %v", err) |
|
||||||
} |
|
||||||
|
|
||||||
gotNodeIDsByProperty := network.GetNodeIDsByProperty(propertyTest) |
|
||||||
if len(gotNodeIDsByProperty) != numPropertyNodes { |
|
||||||
t.Fatalf("Expected %d nodes with a property, got %d", numPropertyNodes, len(gotNodeIDsByProperty)) |
|
||||||
} |
|
||||||
|
|
||||||
for _, node1 := range propertyNodes { |
|
||||||
match := false |
|
||||||
id1 := node1.ID() |
|
||||||
for _, id2 := range gotNodeIDsByProperty { |
|
||||||
if bytes.Equal(id1.Bytes(), id2.Bytes()) { |
|
||||||
match = true |
|
||||||
break |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
if !match { |
|
||||||
t.Fatalf("Not all nodes IDs were returned by GetNodeIDsByProperty(), ID: %s", id1.String()) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func triggerChecks(ctx context.Context, ids []enode.ID, trigger chan enode.ID, interval time.Duration) { |
|
||||||
tick := time.NewTicker(interval) |
|
||||||
defer tick.Stop() |
|
||||||
for { |
|
||||||
select { |
|
||||||
case <-tick.C: |
|
||||||
for _, id := range ids { |
|
||||||
select { |
|
||||||
case trigger <- id: |
|
||||||
case <-ctx.Done(): |
|
||||||
return |
|
||||||
} |
|
||||||
} |
|
||||||
case <-ctx.Done(): |
|
||||||
return |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// \todo: refactor to implement snapshots
|
|
||||||
// and connect configuration methods once these are moved from
|
|
||||||
// swarm/network/simulations/connect.go
|
|
||||||
func BenchmarkMinimalService(b *testing.B) { |
|
||||||
b.Run("ring/32", benchmarkMinimalServiceTmp) |
|
||||||
} |
|
||||||
|
|
||||||
func benchmarkMinimalServiceTmp(b *testing.B) { |
|
||||||
// stop timer to discard setup time pollution
|
|
||||||
args := strings.Split(b.Name(), "/") |
|
||||||
nodeCount, err := strconv.ParseInt(args[2], 10, 16) |
|
||||||
if err != nil { |
|
||||||
b.Fatal(err) |
|
||||||
} |
|
||||||
|
|
||||||
for i := 0; i < b.N; i++ { |
|
||||||
// this is a minimal service, whose protocol will close a channel upon run of protocol
|
|
||||||
// making it possible to bench the time it takes for the service to start and protocol actually to be run
|
|
||||||
protoCMap := make(map[enode.ID]map[enode.ID]chan struct{}) |
|
||||||
adapter := adapters.NewSimAdapter(adapters.LifecycleConstructors{ |
|
||||||
"noopwoop": func(ctx *adapters.ServiceContext, stack *node.Node) (node.Lifecycle, error) { |
|
||||||
protoCMap[ctx.Config.ID] = make(map[enode.ID]chan struct{}) |
|
||||||
svc := NewNoopService(protoCMap[ctx.Config.ID]) |
|
||||||
return svc, nil |
|
||||||
}, |
|
||||||
}) |
|
||||||
|
|
||||||
// create network
|
|
||||||
network := NewNetwork(adapter, &NetworkConfig{ |
|
||||||
DefaultService: "noopwoop", |
|
||||||
}) |
|
||||||
defer network.Shutdown() |
|
||||||
|
|
||||||
// create and start nodes
|
|
||||||
ids := make([]enode.ID, nodeCount) |
|
||||||
for i := 0; i < int(nodeCount); i++ { |
|
||||||
conf := adapters.RandomNodeConfig() |
|
||||||
node, err := network.NewNodeWithConfig(conf) |
|
||||||
if err != nil { |
|
||||||
b.Fatalf("error creating node: %s", err) |
|
||||||
} |
|
||||||
if err := network.Start(node.ID()); err != nil { |
|
||||||
b.Fatalf("error starting node: %s", err) |
|
||||||
} |
|
||||||
ids[i] = node.ID() |
|
||||||
} |
|
||||||
|
|
||||||
// ready, set, go
|
|
||||||
b.ResetTimer() |
|
||||||
|
|
||||||
// connect nodes in a ring
|
|
||||||
for i, id := range ids { |
|
||||||
peerID := ids[(i+1)%len(ids)] |
|
||||||
if err := network.Connect(id, peerID); err != nil { |
|
||||||
b.Fatal(err) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// wait for all protocols to signal to close down
|
|
||||||
ctx, cancel := context.WithTimeout(context.TODO(), time.Second) |
|
||||||
defer cancel() |
|
||||||
for nodid, peers := range protoCMap { |
|
||||||
for peerid, peerC := range peers { |
|
||||||
log.Debug("getting ", "node", nodid, "peer", peerid) |
|
||||||
select { |
|
||||||
case <-ctx.Done(): |
|
||||||
b.Fatal(ctx.Err()) |
|
||||||
case <-peerC: |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func TestNode_UnmarshalJSON(t *testing.T) { |
|
||||||
t.Run("up_field", func(t *testing.T) { |
|
||||||
runNodeUnmarshalJSON(t, casesNodeUnmarshalJSONUpField()) |
|
||||||
}) |
|
||||||
t.Run("config_field", func(t *testing.T) { |
|
||||||
runNodeUnmarshalJSON(t, casesNodeUnmarshalJSONConfigField()) |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
func runNodeUnmarshalJSON(t *testing.T, tests []nodeUnmarshalTestCase) { |
|
||||||
t.Helper() |
|
||||||
for _, tt := range tests { |
|
||||||
t.Run(tt.name, func(t *testing.T) { |
|
||||||
var got *Node |
|
||||||
if err := json.Unmarshal([]byte(tt.marshaled), &got); err != nil { |
|
||||||
expectErrorMessageToContain(t, err, tt.wantErr) |
|
||||||
got = nil |
|
||||||
} |
|
||||||
expectNodeEquality(t, got, tt.want) |
|
||||||
}) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
type nodeUnmarshalTestCase struct { |
|
||||||
name string |
|
||||||
marshaled string |
|
||||||
want *Node |
|
||||||
wantErr string |
|
||||||
} |
|
||||||
|
|
||||||
func expectErrorMessageToContain(t *testing.T, got error, want string) { |
|
||||||
t.Helper() |
|
||||||
if got == nil && want == "" { |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
if got == nil && want != "" { |
|
||||||
t.Errorf("error was expected, got: nil, want: %v", want) |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
if !strings.Contains(got.Error(), want) { |
|
||||||
t.Errorf( |
|
||||||
"unexpected error message, got %v, want: %v", |
|
||||||
want, |
|
||||||
got, |
|
||||||
) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func expectNodeEquality(t *testing.T, got, want *Node) { |
|
||||||
t.Helper() |
|
||||||
if !reflect.DeepEqual(got, want) { |
|
||||||
t.Errorf("Node.UnmarshalJSON() = %v, want %v", got, want) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func casesNodeUnmarshalJSONUpField() []nodeUnmarshalTestCase { |
|
||||||
return []nodeUnmarshalTestCase{ |
|
||||||
{ |
|
||||||
name: "empty json", |
|
||||||
marshaled: "{}", |
|
||||||
want: newNode(nil, nil, false), |
|
||||||
}, |
|
||||||
{ |
|
||||||
name: "a stopped node", |
|
||||||
marshaled: "{\"up\": false}", |
|
||||||
want: newNode(nil, nil, false), |
|
||||||
}, |
|
||||||
{ |
|
||||||
name: "a running node", |
|
||||||
marshaled: "{\"up\": true}", |
|
||||||
want: newNode(nil, nil, true), |
|
||||||
}, |
|
||||||
{ |
|
||||||
name: "invalid JSON value on valid key", |
|
||||||
marshaled: "{\"up\": foo}", |
|
||||||
wantErr: "invalid character", |
|
||||||
}, |
|
||||||
{ |
|
||||||
name: "invalid JSON key and value", |
|
||||||
marshaled: "{foo: bar}", |
|
||||||
wantErr: "invalid character", |
|
||||||
}, |
|
||||||
{ |
|
||||||
name: "bool value expected but got something else (string)", |
|
||||||
marshaled: "{\"up\": \"true\"}", |
|
||||||
wantErr: "cannot unmarshal string into Go struct", |
|
||||||
}, |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func casesNodeUnmarshalJSONConfigField() []nodeUnmarshalTestCase { |
|
||||||
// Don't do a big fuss around testing, as adapters.NodeConfig should
|
|
||||||
// handle it's own serialization. Just do a sanity check.
|
|
||||||
return []nodeUnmarshalTestCase{ |
|
||||||
{ |
|
||||||
name: "Config field is omitted", |
|
||||||
marshaled: "{}", |
|
||||||
want: newNode(nil, nil, false), |
|
||||||
}, |
|
||||||
{ |
|
||||||
name: "Config field is nil", |
|
||||||
marshaled: "{\"config\": null}", |
|
||||||
want: newNode(nil, nil, false), |
|
||||||
}, |
|
||||||
{ |
|
||||||
name: "a non default Config field", |
|
||||||
marshaled: "{\"config\":{\"name\":\"node_ecdd0\",\"port\":44665}}", |
|
||||||
want: newNode(nil, &adapters.NodeConfig{Name: "node_ecdd0", Port: 44665}, false), |
|
||||||
}, |
|
||||||
} |
|
||||||
} |
|
@ -1,157 +0,0 @@ |
|||||||
// Copyright 2017 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library 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 Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package simulations |
|
||||||
|
|
||||||
import ( |
|
||||||
"context" |
|
||||||
"time" |
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/enode" |
|
||||||
) |
|
||||||
|
|
||||||
// Simulation provides a framework for running actions in a simulated network
|
|
||||||
// and then waiting for expectations to be met
|
|
||||||
type Simulation struct { |
|
||||||
network *Network |
|
||||||
} |
|
||||||
|
|
||||||
// NewSimulation returns a new simulation which runs in the given network
|
|
||||||
func NewSimulation(network *Network) *Simulation { |
|
||||||
return &Simulation{ |
|
||||||
network: network, |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// Run performs a step of the simulation by performing the step's action and
|
|
||||||
// then waiting for the step's expectation to be met
|
|
||||||
func (s *Simulation) Run(ctx context.Context, step *Step) (result *StepResult) { |
|
||||||
result = newStepResult() |
|
||||||
|
|
||||||
result.StartedAt = time.Now() |
|
||||||
defer func() { result.FinishedAt = time.Now() }() |
|
||||||
|
|
||||||
// watch network events for the duration of the step
|
|
||||||
stop := s.watchNetwork(result) |
|
||||||
defer stop() |
|
||||||
|
|
||||||
// perform the action
|
|
||||||
if err := step.Action(ctx); err != nil { |
|
||||||
result.Error = err |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
// wait for all node expectations to either pass, error or timeout
|
|
||||||
nodes := make(map[enode.ID]struct{}, len(step.Expect.Nodes)) |
|
||||||
for _, id := range step.Expect.Nodes { |
|
||||||
nodes[id] = struct{}{} |
|
||||||
} |
|
||||||
for len(result.Passes) < len(nodes) { |
|
||||||
select { |
|
||||||
case id := <-step.Trigger: |
|
||||||
// skip if we aren't checking the node
|
|
||||||
if _, ok := nodes[id]; !ok { |
|
||||||
continue |
|
||||||
} |
|
||||||
|
|
||||||
// skip if the node has already passed
|
|
||||||
if _, ok := result.Passes[id]; ok { |
|
||||||
continue |
|
||||||
} |
|
||||||
|
|
||||||
// run the node expectation check
|
|
||||||
pass, err := step.Expect.Check(ctx, id) |
|
||||||
if err != nil { |
|
||||||
result.Error = err |
|
||||||
return |
|
||||||
} |
|
||||||
if pass { |
|
||||||
result.Passes[id] = time.Now() |
|
||||||
} |
|
||||||
case <-ctx.Done(): |
|
||||||
result.Error = ctx.Err() |
|
||||||
return |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
func (s *Simulation) watchNetwork(result *StepResult) func() { |
|
||||||
stop := make(chan struct{}) |
|
||||||
done := make(chan struct{}) |
|
||||||
events := make(chan *Event) |
|
||||||
sub := s.network.Events().Subscribe(events) |
|
||||||
go func() { |
|
||||||
defer close(done) |
|
||||||
defer sub.Unsubscribe() |
|
||||||
for { |
|
||||||
select { |
|
||||||
case event := <-events: |
|
||||||
result.NetworkEvents = append(result.NetworkEvents, event) |
|
||||||
case <-stop: |
|
||||||
return |
|
||||||
} |
|
||||||
} |
|
||||||
}() |
|
||||||
return func() { |
|
||||||
close(stop) |
|
||||||
<-done |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
type Step struct { |
|
||||||
// Action is the action to perform for this step
|
|
||||||
Action func(context.Context) error |
|
||||||
|
|
||||||
// Trigger is a channel which receives node ids and triggers an
|
|
||||||
// expectation check for that node
|
|
||||||
Trigger chan enode.ID |
|
||||||
|
|
||||||
// Expect is the expectation to wait for when performing this step
|
|
||||||
Expect *Expectation |
|
||||||
} |
|
||||||
|
|
||||||
type Expectation struct { |
|
||||||
// Nodes is a list of nodes to check
|
|
||||||
Nodes []enode.ID |
|
||||||
|
|
||||||
// Check checks whether a given node meets the expectation
|
|
||||||
Check func(context.Context, enode.ID) (bool, error) |
|
||||||
} |
|
||||||
|
|
||||||
func newStepResult() *StepResult { |
|
||||||
return &StepResult{ |
|
||||||
Passes: make(map[enode.ID]time.Time), |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
type StepResult struct { |
|
||||||
// Error is the error encountered whilst running the step
|
|
||||||
Error error |
|
||||||
|
|
||||||
// StartedAt is the time the step started
|
|
||||||
StartedAt time.Time |
|
||||||
|
|
||||||
// FinishedAt is the time the step finished
|
|
||||||
FinishedAt time.Time |
|
||||||
|
|
||||||
// Passes are the timestamps of the successful node expectations
|
|
||||||
Passes map[enode.ID]time.Time |
|
||||||
|
|
||||||
// NetworkEvents are the network events which occurred during the step
|
|
||||||
NetworkEvents []*Event |
|
||||||
} |
|
@ -1,150 +0,0 @@ |
|||||||
// Copyright 2018 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library 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 Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package simulations |
|
||||||
|
|
||||||
import ( |
|
||||||
"testing" |
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/p2p" |
|
||||||
"github.com/ethereum/go-ethereum/p2p/enode" |
|
||||||
"github.com/ethereum/go-ethereum/p2p/enr" |
|
||||||
"github.com/ethereum/go-ethereum/rpc" |
|
||||||
) |
|
||||||
|
|
||||||
// NoopService is the service that does not do anything
|
|
||||||
// but implements node.Service interface.
|
|
||||||
type NoopService struct { |
|
||||||
c map[enode.ID]chan struct{} |
|
||||||
} |
|
||||||
|
|
||||||
func NewNoopService(ackC map[enode.ID]chan struct{}) *NoopService { |
|
||||||
return &NoopService{ |
|
||||||
c: ackC, |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func (t *NoopService) Protocols() []p2p.Protocol { |
|
||||||
return []p2p.Protocol{ |
|
||||||
{ |
|
||||||
Name: "noop", |
|
||||||
Version: 666, |
|
||||||
Length: 0, |
|
||||||
Run: func(peer *p2p.Peer, rw p2p.MsgReadWriter) error { |
|
||||||
if t.c != nil { |
|
||||||
t.c[peer.ID()] = make(chan struct{}) |
|
||||||
close(t.c[peer.ID()]) |
|
||||||
} |
|
||||||
rw.ReadMsg() |
|
||||||
return nil |
|
||||||
}, |
|
||||||
NodeInfo: func() interface{} { |
|
||||||
return struct{}{} |
|
||||||
}, |
|
||||||
PeerInfo: func(id enode.ID) interface{} { |
|
||||||
return struct{}{} |
|
||||||
}, |
|
||||||
Attributes: []enr.Entry{}, |
|
||||||
}, |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func (t *NoopService) APIs() []rpc.API { |
|
||||||
return []rpc.API{} |
|
||||||
} |
|
||||||
|
|
||||||
func (t *NoopService) Start() error { |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
func (t *NoopService) Stop() error { |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
func VerifyRing(t *testing.T, net *Network, ids []enode.ID) { |
|
||||||
t.Helper() |
|
||||||
n := len(ids) |
|
||||||
for i := 0; i < n; i++ { |
|
||||||
for j := i + 1; j < n; j++ { |
|
||||||
c := net.GetConn(ids[i], ids[j]) |
|
||||||
if i == j-1 || (i == 0 && j == n-1) { |
|
||||||
if c == nil { |
|
||||||
t.Errorf("nodes %v and %v are not connected, but they should be", i, j) |
|
||||||
} |
|
||||||
} else { |
|
||||||
if c != nil { |
|
||||||
t.Errorf("nodes %v and %v are connected, but they should not be", i, j) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func VerifyChain(t *testing.T, net *Network, ids []enode.ID) { |
|
||||||
t.Helper() |
|
||||||
n := len(ids) |
|
||||||
for i := 0; i < n; i++ { |
|
||||||
for j := i + 1; j < n; j++ { |
|
||||||
c := net.GetConn(ids[i], ids[j]) |
|
||||||
if i == j-1 { |
|
||||||
if c == nil { |
|
||||||
t.Errorf("nodes %v and %v are not connected, but they should be", i, j) |
|
||||||
} |
|
||||||
} else { |
|
||||||
if c != nil { |
|
||||||
t.Errorf("nodes %v and %v are connected, but they should not be", i, j) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func VerifyFull(t *testing.T, net *Network, ids []enode.ID) { |
|
||||||
t.Helper() |
|
||||||
n := len(ids) |
|
||||||
var connections int |
|
||||||
for i, lid := range ids { |
|
||||||
for _, rid := range ids[i+1:] { |
|
||||||
if net.GetConn(lid, rid) != nil { |
|
||||||
connections++ |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
want := n * (n - 1) / 2 |
|
||||||
if connections != want { |
|
||||||
t.Errorf("wrong number of connections, got: %v, want: %v", connections, want) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func VerifyStar(t *testing.T, net *Network, ids []enode.ID, centerIndex int) { |
|
||||||
t.Helper() |
|
||||||
n := len(ids) |
|
||||||
for i := 0; i < n; i++ { |
|
||||||
for j := i + 1; j < n; j++ { |
|
||||||
c := net.GetConn(ids[i], ids[j]) |
|
||||||
if i == centerIndex || j == centerIndex { |
|
||||||
if c == nil { |
|
||||||
t.Errorf("nodes %v and %v are not connected, but they should be", i, j) |
|
||||||
} |
|
||||||
} else { |
|
||||||
if c != nil { |
|
||||||
t.Errorf("nodes %v and %v are connected, but they should not be", i, j) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
Loading…
Reference in new issue