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.
This commit is contained in:
lightclient 2024-08-12 02:36:48 -06:00 committed by GitHub
parent 32a1e0643c
commit 33a13b6f21
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 5 additions and 7013 deletions

3
.github/CODEOWNERS vendored
View File

@ -21,7 +21,4 @@ light/ @zsfelfoldi @rjl493456442
node/ @fjl
p2p/ @fjl @zsfelfoldi
rpc/ @fjl @holiman
p2p/simulations @fjl
p2p/protocols @fjl
p2p/testing @fjl
signer/ @holiman

View File

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

View File

@ -1,4 +1,4 @@
// Copyright 2018 The go-ethereum Authors
// Copyright 2024 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
@ -16,17 +16,9 @@
package pipes
import (
"net"
)
import "net"
// NetPipe wraps net.Pipe in a signature returning an error
func NetPipe() (net.Conn, net.Conn, error) {
p1, p2 := net.Pipe()
return p1, p2, nil
}
// TCPPipe creates an in process full duplex pipe based on a localhost TCP socket
// TCPPipe creates an in process full duplex pipe based on a localhost TCP socket.
func TCPPipe() (net.Conn, net.Conn, error) {
l, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {

View File

@ -31,7 +31,7 @@ import (
"github.com/davecgh/go-spew/spew"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/crypto/ecies"
"github.com/ethereum/go-ethereum/p2p/simulations/pipes"
"github.com/ethereum/go-ethereum/p2p/pipes"
"github.com/ethereum/go-ethereum/rlp"
"github.com/stretchr/testify/assert"
)

View File

@ -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).

View File

@ -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")
}

View File

@ -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()
}

View File

@ -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)
}
}
}
}

View File

@ -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)
}

View File

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

View File

@ -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)
}

View File

@ -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 ""
}
}

View File

@ -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...
```

View File

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

View File

@ -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 "$@"

View File

@ -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))
}
}

View File

@ -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!")
}
}

View File

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

View File

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

View File

@ -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),
},
}
}

View File

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

View File

@ -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)
}
}
}
}
}

View File

@ -24,7 +24,7 @@ import (
"github.com/davecgh/go-spew/spew"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/p2p/simulations/pipes"
"github.com/ethereum/go-ethereum/p2p/pipes"
)
func TestProtocolHandshake(t *testing.T) {