diff --git a/cmd/swarm/swarm-snapshot/create.go b/cmd/swarm/swarm-snapshot/create.go new file mode 100644 index 0000000000..51cda8df59 --- /dev/null +++ b/cmd/swarm/swarm-snapshot/create.go @@ -0,0 +1,280 @@ +// Copyright 2018 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 . + +package main + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "os" + "strings" + "time" + + "github.com/ethereum/go-ethereum/cmd/utils" + "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" + "github.com/ethereum/go-ethereum/swarm/network" + cli "gopkg.in/urfave/cli.v1" +) + +var serviceFuncs = adapters.Services{ + "discovery": newService, +} + +const testMinProxBinSize = 2 + +var discoveryEnabled = true + +func init() { + adapters.RegisterServices(serviceFuncs) +} + +func create(ctx *cli.Context) error { + log.PrintOrigins(true) + log.Root().SetHandler(log.LvlFilterHandler(log.Lvl(verbosity), log.StreamHandler(os.Stdout, log.TerminalFormat(true)))) + if len(ctx.Args()) < 1 { + return errors.New("argument should be the filename to verify or write-to") + } + filename = ctx.Args()[0] + err := ResolvePath() + if err != nil { + return err + } + result, err := discoverySnapshot(10, adapters.NewSimAdapter(serviceFuncs)) + if err != nil { + utils.Fatalf("Setting up simulation failed: %v", err) + } + + if result.Error != nil { + utils.Fatalf("Simulation failed: %s", result.Error) + } + + return err +} +func discoverySnapshot(nodes int, adapter adapters.NodeAdapter) (*simulations.StepResult, error) { + // create network + net := simulations.NewNetwork(adapter, &simulations.NetworkConfig{ + ID: "0", + DefaultService: "discovery", + }) + defer net.Shutdown() + trigger := make(chan enode.ID) + ids := make([]enode.ID, nodes) + + for i := 0; i < nodes; i++ { + conf := adapters.RandomNodeConfig() + node, err := net.NewNodeWithConfig(conf) + if err != nil { + return nil, fmt.Errorf("error starting node: %s", err) + } + if err := net.Start(node.ID()); err != nil { + return nil, fmt.Errorf("error starting node %s: %s", node.ID().TerminalString(), err) + } + if err := triggerChecks(trigger, net, node.ID()); err != nil { + return nil, fmt.Errorf("error triggering checks for node %s: %s", node.ID().TerminalString(), err) + } + ids[i] = node.ID() + } + events := make(chan *simulations.Event) + sub := net.Events().Subscribe(events) + select { + case ev := <-events: + //only catch node up events + if ev.Type == simulations.EventTypeConn { + utils.Fatalf("this shouldn't happen as connections weren't initiated yet") + } + case <-time.After(1 * time.Second): + } + + sub.Unsubscribe() + + if len(net.Conns) > 0 { + utils.Fatalf("no connections should exist after just adding nodes") + } + + var addrs [][]byte + action := func(ctx context.Context) error { + return nil + } + for i := range ids { + // collect the overlay addresses, to + addrs = append(addrs, ids[i].Bytes()) + } + + switch topology { + case "star": + net.SetPivotNode(ids[pivot]) + err := net.ConnectNodesStarPivot(nil) + if err != nil { + utils.Fatalf("had an error connecting the nodes in a star: %v", err) + } + case "ring": + err := net.ConnectNodesRing(nil) + if err != nil { + utils.Fatalf("had an error connecting the nodes in a ring: %v", err) + } + case "chain": + err := net.ConnectNodesChain(nil) + if err != nil { + utils.Fatalf("had an error connecting the nodes in a chain: %v", err) + } + case "full": + err := net.ConnectNodesFull(nil) + if err != nil { + utils.Fatalf("had an error connecting full: %v", err) + } + } + // construct the peer pot, so that kademlia health can be checked + ppmap := network.NewPeerPotMap(testMinProxBinSize, addrs) + check := func(ctx context.Context, id enode.ID) (bool, error) { + select { + case <-ctx.Done(): + return false, ctx.Err() + default: + } + + node := net.GetNode(id) + if node == nil { + return false, fmt.Errorf("unknown node: %s", id) + } + client, err := node.Client() + if err != nil { + return false, fmt.Errorf("error getting node client: %s", err) + } + healthy := &network.Health{} + if err := client.Call(&healthy, "hive_healthy", ppmap[id.String()]); err != nil { + return false, fmt.Errorf("error getting node health: %s", err) + } + log.Debug(fmt.Sprintf("node %4s healthy: got nearest neighbours: %v, know nearest neighbours: %v, saturated: %v\n%v", id, healthy.GotNN, healthy.KnowNN, healthy.Full, healthy.Hive)) + return healthy.KnowNN && healthy.GotNN && healthy.Full, nil + } + + // 64 nodes ~ 1min + // 128 nodes ~ + timeout := 30 * time.Second + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + result := simulations.NewSimulation(net).Run(ctx, &simulations.Step{ + Action: action, + Trigger: trigger, + Expect: &simulations.Expectation{ + Nodes: ids, + Check: check, + }, + }) + if result.Error != nil { + return result, result.Error + } + + var snap *simulations.Snapshot + var err error + if len(services) > 0 { + var addServices []string + var removeServices []string + for _, osvc := range strings.Split(services, ",") { + if strings.Index(osvc, "+") == 0 { + addServices = append(addServices, osvc[1:]) + } else if strings.Index(osvc, "-") == 0 { + removeServices = append(removeServices, osvc[1:]) + } else { + panic("stick to the rules, you know what they are") + } + } + snap, err = net.SnapshotWithServices(addServices, removeServices) + } else { + snap, err = net.Snapshot() + } + + if err != nil { + return nil, errors.New("no shapshot dude") + } + jsonsnapshot, err := json.Marshal(snap) + if err != nil { + return nil, fmt.Errorf("corrupt json snapshot: %v", err) + } + err = ioutil.WriteFile(filename, jsonsnapshot, 0755) + if err != nil { + return nil, err + } + + return result, nil +} + +func newService(ctx *adapters.ServiceContext) (node.Service, error) { + addr := network.NewAddr(ctx.Config.Node()) + + kp := network.NewKadParams() + kp.MinProxBinSize = testMinProxBinSize + + kad := network.NewKademlia(addr.Over(), kp) + hp := network.NewHiveParams() + hp.KeepAliveInterval = time.Duration(200) * time.Millisecond + hp.Discovery = discoveryEnabled + + log.Info(fmt.Sprintf("discovery for nodeID %s is %t", ctx.Config.ID.String(), hp.Discovery)) + + config := &network.BzzConfig{ + OverlayAddr: addr.Over(), + UnderlayAddr: addr.Under(), + HiveParams: hp, + } + + return network.NewBzz(config, kad, nil, nil, nil), nil +} + +func triggerChecks(trigger chan enode.ID, net *simulations.Network, id enode.ID) error { + node := net.GetNode(id) + if node == nil { + return fmt.Errorf("unknown node: %s", id) + } + client, err := node.Client() + if err != nil { + return err + } + events := make(chan *p2p.PeerEvent) + sub, err := client.Subscribe(context.Background(), "admin", events, "peerEvents") + if err != nil { + return fmt.Errorf("error getting peer events for node %v: %s", id, err) + } + go func() { + defer sub.Unsubscribe() + + tick := time.NewTicker(time.Second) + defer tick.Stop() + + for { + select { + case <-events: + trigger <- id + case <-tick.C: + trigger <- id + case err := <-sub.Err(): + if err != nil { + log.Error(fmt.Sprintf("error getting peer events for node %v", id), "err", err) + } + return + } + } + }() + return nil +} diff --git a/cmd/swarm/swarm-snapshot/create_test.go b/cmd/swarm/swarm-snapshot/create_test.go new file mode 100644 index 0000000000..ba57683c51 --- /dev/null +++ b/cmd/swarm/swarm-snapshot/create_test.go @@ -0,0 +1,54 @@ +// Copyright 2018 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 . + +package main + +import ( + "io/ioutil" + "os" + "testing" +) + +//TestSnapshotCreate is a high level e2e test that tests for snapshot generation +func TestSnapshotCreate(t *testing.T) { + file, err := ioutil.TempFile("", "swarm-snapshot") + defer os.Remove(file.Name()) + + if err != nil { + t.Fatal(err) + } + + file.Close() + + snap := runSnapshot(t, + "--verbosity", + "6", + "--topology", + "ring", + "c", + file.Name(), + ) + + _, _ = snap.ExpectRegexp(".") + if snap.ExitStatus() != 0 { + t.Fatal("expected exit code 0") + } + + _, err = os.Stat(file.Name()) + if err != nil { + t.Fatal("could not stat snapshot json") + } +} diff --git a/cmd/swarm/swarm-snapshot/helper.go b/cmd/swarm/swarm-snapshot/helper.go new file mode 100644 index 0000000000..126e1ecb3f --- /dev/null +++ b/cmd/swarm/swarm-snapshot/helper.go @@ -0,0 +1,40 @@ +package main + +import ( + "os" + "path" + "path/filepath" +) + +func ResolvePath() error { + if path.IsAbs(filename) { + if _, err := os.Stat(filename); err == nil { + // path exists, we will override the file + return nil + } + } + + d, f := path.Split(filename) + dir, err := filepath.Abs(filepath.Dir(os.Args[0])) + if err != nil { + return err + } + + _, err = os.Stat(path.Join(dir, filename)) + if err == nil { + // path exists, we will override + return nil + } + + dirPath := path.Join(dir, d) + filePath := path.Join(dirPath, f) + if d != "" { + err = os.MkdirAll(dirPath, os.ModeDir) + if err != nil { + return err + } + } + + filename = filePath + return nil +} diff --git a/cmd/swarm/swarm-snapshot/main.go b/cmd/swarm/swarm-snapshot/main.go new file mode 100644 index 0000000000..b421c79dc8 --- /dev/null +++ b/cmd/swarm/swarm-snapshot/main.go @@ -0,0 +1,110 @@ +// Copyright 2018 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 . + +package main + +import ( + "os" + "sort" + + "github.com/ethereum/go-ethereum/cmd/utils" + "github.com/ethereum/go-ethereum/log" + cli "gopkg.in/urfave/cli.v1" +) + +var ( + gitCommit string // Git SHA1 commit hash of the release (set via linker flags) +) + +var ( + topology string + services string + pivot int + nodes int + verbosity int + filename string +) + +var app = utils.NewApp("", "Swarm Snapshot Util") + +func init() { + app.Name = "swarm-snapshot" + app.Usage = "" + + app.Flags = []cli.Flag{ + cli.StringFlag{ + Name: "topology", + Value: "chain", + Usage: "the desired topology to connect the nodes in (star, ring, chain, full)", + Destination: &topology, + }, + cli.IntFlag{ + Name: "pivot", + Value: 0, + Usage: "pivot node zero-index", + Destination: &pivot, + }, + cli.IntFlag{ + Name: "nodes", + Value: 10, + Usage: "swarm nodes", + Destination: &nodes, + }, + cli.IntFlag{ + Name: "verbosity", + Value: 1, + Usage: "verbosity", + Destination: &verbosity, + }, + cli.StringFlag{ + Name: "services", + Value: "", + Usage: "comma separated list of services to boot the nodes with", + Destination: &services, + }, + } + + app.Commands = []cli.Command{ + { + Name: "create", + Aliases: []string{"c"}, + Usage: "create a swarm snapshot", + Action: create, + }, + { + Name: "verify", + Aliases: []string{"v"}, + Usage: "verify a swarm snapshot", + Action: verify, + }, + } + + sort.Sort(cli.FlagsByName(app.Flags)) + sort.Sort(cli.CommandsByName(app.Commands)) + app.Before = func(ctx *cli.Context) error { + return nil + } +} + +func main() { + err := app.Run(os.Args) + if err != nil { + log.Error(err.Error()) + + os.Exit(1) + } + os.Exit(0) +} diff --git a/cmd/swarm/swarm-snapshot/run_test.go b/cmd/swarm/swarm-snapshot/run_test.go new file mode 100644 index 0000000000..3a66ac987f --- /dev/null +++ b/cmd/swarm/swarm-snapshot/run_test.go @@ -0,0 +1,49 @@ +// Copyright 2018 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 . + +package main + +import ( + "fmt" + "os" + "testing" + + "github.com/docker/docker/pkg/reexec" + "github.com/ethereum/go-ethereum/internal/cmdtest" +) + +func init() { + reexec.Register("swarm-snapshot", func() { + if err := app.Run(os.Args); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + os.Exit(0) + }) +} + +func runSnapshot(t *testing.T, args ...string) *cmdtest.TestCmd { + tt := cmdtest.NewTestCmd(t, nil) + tt.Run("swarm-snapshot", args...) + return tt +} + +func TestMain(m *testing.M) { + if reexec.Init() { + return + } + os.Exit(m.Run()) +} diff --git a/cmd/swarm/swarm-snapshot/verify.go b/cmd/swarm/swarm-snapshot/verify.go new file mode 100644 index 0000000000..12f36969b3 --- /dev/null +++ b/cmd/swarm/swarm-snapshot/verify.go @@ -0,0 +1,30 @@ +// Copyright 2018 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 . + +package main + +import ( + "os" + + "github.com/ethereum/go-ethereum/log" + cli "gopkg.in/urfave/cli.v1" +) + +func verify(c *cli.Context) error { + log.PrintOrigins(true) + log.Root().SetHandler(log.LvlFilterHandler(log.Lvl(verbosity), log.StreamHandler(os.Stdout, log.TerminalFormat(true)))) + return nil +} diff --git a/cmd/swarm/swarm-snapshot/verify_test.go b/cmd/swarm/swarm-snapshot/verify_test.go new file mode 100644 index 0000000000..985e45835b --- /dev/null +++ b/cmd/swarm/swarm-snapshot/verify_test.go @@ -0,0 +1,30 @@ +// Copyright 2018 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 . + +package main + +import ( + "fmt" + "testing" +) + +func TestSnapshotVerify(t *testing.T) { + t.Skip("to be implemented") + snap := runSnapshot(t, "") + _, matches := snap.ExpectRegexp(`.`) + snap.ExpectExit() + fmt.Sprintf("%v", matches) +} diff --git a/swarm/network/simulations/discovery/discovery_test.go b/swarm/network/simulations/discovery/discovery_test.go index cd5456b73e..2e54f70ffb 100644 --- a/swarm/network/simulations/discovery/discovery_test.go +++ b/swarm/network/simulations/discovery/discovery_test.go @@ -23,11 +23,9 @@ import ( "flag" "fmt" "io/ioutil" - "math/rand" "os" "path" "strings" - "sync" "testing" "time" @@ -244,25 +242,14 @@ func discoverySimulation(nodes, conns int, adapter adapters.NodeAdapter) (*simul action := func(ctx context.Context) error { return nil } - wg := sync.WaitGroup{} for i := range ids { // collect the overlay addresses, to addrs = append(addrs, ids[i].Bytes()) - for j := 0; j < conns; j++ { - var k int - if j == 0 { - k = (i + 1) % len(ids) - } else { - k = rand.Intn(len(ids)) - } - wg.Add(1) - go func(i, k int) { - defer wg.Done() - net.Connect(ids[i], ids[k]) - }(i, k) - } } - wg.Wait() + err := net.ConnectNodesChain(nil) + if err != nil { + return nil, err + } log.Debug(fmt.Sprintf("nodes: %v", len(addrs))) // construct the peer pot, so that kademlia health can be checked ppmap := network.NewPeerPotMap(testMinProxBinSize, addrs) @@ -444,23 +431,7 @@ func discoveryPersistenceSimulation(nodes, conns int, adapter adapters.NodeAdapt return nil } - //connects in a chain - wg := sync.WaitGroup{} - //connects in a ring - for i := range ids { - for j := 1; j <= conns; j++ { - k := (i + j) % len(ids) - if k == i { - k = (k + 1) % len(ids) - } - wg.Add(1) - go func(i, k int) { - defer wg.Done() - net.Connect(ids[i], ids[k]) - }(i, k) - } - } - wg.Wait() + net.ConnectNodesChain(nil) log.Debug(fmt.Sprintf("nodes: %v", len(addrs))) // construct the peer pot, so that kademlia health can be checked check := func(ctx context.Context, id enode.ID) (bool, error) {