diff --git a/cli/dvoting-libp2p/mod.go b/cli/dvoting-libp2p/mod.go new file mode 100644 index 000000000..c895e0651 --- /dev/null +++ b/cli/dvoting-libp2p/mod.go @@ -0,0 +1,99 @@ +// Package main implements the dvoting backend +// +// Unix example: +// +// # Expect GOPATH to be correctly set to have dvoting available. +// go install +// +// dvoting --config /tmp/node1 start --port 2001 & +// dvoting --config /tmp/node2 start --port 2002 & +// dvoting --config /tmp/node3 start --port 2003 & +// +// # Share the different certificates among the participants. +// dvoting --config /tmp/node2 minogrpc join --address 127.0.0.1:2001\ +// $(dvoting --config /tmp/node1 minogrpc token) +// dvoting --config /tmp/node3 minogrpc join --address 127.0.0.1:2001\ +// $(dvoting --config /tmp/node1 minogrpc token) +// +// # Create a chain with two members. +// dvoting --config /tmp/node1 ordering setup\ +// --member $(dvoting --config /tmp/node1 ordering export)\ +// --member $(dvoting --config /tmp/node2 ordering export) +// +// # Add the third after the chain is set up. +// dvoting --config /tmp/node1 ordering roster add\ +// --member $(dvoting --config /tmp/node3 ordering export) +package main + +import ( + "fmt" + "io" + "os" + + dkg "github.com/c4dt/d-voting/services/dkg/pedersen/controller" + "github.com/c4dt/d-voting/services/dkg/pedersen/json" + shuffle "github.com/c4dt/d-voting/services/shuffle/neff/controller" + + cosipbft "github.com/c4dt/d-voting/cli/cosipbftcontroller" + "github.com/c4dt/d-voting/cli/postinstall" + evoting "github.com/c4dt/d-voting/contracts/evoting/controller" + metrics "github.com/c4dt/d-voting/metrics/controller" + "go.dedis.ch/dela/cli/node" + access "go.dedis.ch/dela/contracts/access/controller" + db "go.dedis.ch/dela/core/store/kv/controller" + pool "go.dedis.ch/dela/core/txn/pool/controller" + signed "go.dedis.ch/dela/core/txn/signed/controller" + mino "go.dedis.ch/dela/mino/minows" + proxy "go.dedis.ch/dela/mino/proxy/http/controller" + + _ "github.com/c4dt/d-voting/services/shuffle/neff/json" + + gapi "go.dedis.ch/dela-apps/gapi/controller" +) + +func main() { + err := run(os.Args) + if err != nil { + fmt.Printf("%+v\n", err) + } +} + +func run(args []string) error { + return runWithCfg(args, config{Writer: os.Stdout}) +} + +type config struct { + Channel chan os.Signal + Writer io.Writer +} + +func runWithCfg(args []string, cfg config) error { + json.Register() + + builder := node.NewBuilderWithCfg( + cfg.Channel, + cfg.Writer, + db.NewController(), + mino.NewController(), + cosipbft.NewController(), + dkg.NewController(), + signed.NewManagerController(), + pool.NewController(), + access.NewController(), + proxy.NewController(), + shuffle.NewController(), + evoting.NewController(), + gapi.NewController(), + metrics.NewController(), + postinstall.NewController(), + ) + + app := builder.Build() + + err := app.Run(args) + if err != nil { + return err + } + + return nil +} diff --git a/cli/dvoting-libp2p/mod_test.go b/cli/dvoting-libp2p/mod_test.go new file mode 100644 index 000000000..08919672f --- /dev/null +++ b/cli/dvoting-libp2p/mod_test.go @@ -0,0 +1,297 @@ +package main + +import ( + "bytes" + "encoding/base64" + "fmt" + "io" + "net" + "os" + "path/filepath" + "strconv" + "strings" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/require" + "go.dedis.ch/kyber/v3/pairing/bn256" +) + +func TestDvoting_Main(t *testing.T) { + main() +} + +// This test creates a chain with initially 3 nodes. It then adds node 4 and 5 +// in two blocks. Node 4 does not share its certificate which means others won't +// be able to communicate, but the chain should proceed because of the +// threshold. +func TestDvoting_Scenario_SetupAndTransactions(t *testing.T) { + dir, err := os.MkdirTemp(os.TempDir(), "dvoting1") + require.NoError(t, err) + + defer os.RemoveAll(dir) + + sigs := make(chan os.Signal) + wg := sync.WaitGroup{} + wg.Add(5) + + node1 := filepath.Join(dir, "node1") + node2 := filepath.Join(dir, "node2") + node3 := filepath.Join(dir, "node3") + node4 := filepath.Join(dir, "node4") + node5 := filepath.Join(dir, "node5") + + cfg := config{Channel: sigs, Writer: io.Discard} + + runNode(t, node1, cfg, 2111, &wg) + runNode(t, node2, cfg, 2112, &wg) + runNode(t, node3, cfg, 2113, &wg) + runNode(t, node4, cfg, 2114, &wg) + runNode(t, node5, cfg, 2115, &wg) + + defer func() { + // Simulate a Ctrl+C + close(sigs) + wg.Wait() + }() + + require.True(t, waitDaemon(t, []string{node1, node2, node3}), "daemon failed to start") + + // Share the certificates. + shareCert(t, node2, node1, "//127.0.0.1:2111") + shareCert(t, node3, node1, "//127.0.0.1:2111") + shareCert(t, node5, node1, "//127.0.0.1:2111") + + // Set up the chain with nodes 1 and 2. + args := append(append( + append( + []string{os.Args[0], "--config", node1, "ordering", "setup"}, + getExport(t, node1)..., + ), + getExport(t, node2)...), + getExport(t, node3)...) + + err = run(args) + require.NoError(t, err) + + // Add node 4 to the current chain. This node is not reachable from the + // others but transactions should work as the threshold is correct. + args = append([]string{ + os.Args[0], + "--config", node1, "ordering", "roster", "add", + "--wait", "60s"}, + getExport(t, node4)..., + ) + + err = run(args) + require.NoError(t, err) + + // Add the certificate and push two new blocks to make sure node4 is + // fully participating + shareCert(t, node4, node1, "//127.0.0.1:2111") + publicKey, err := bn256.NewSuiteG2().Point().MarshalBinary() + require.NoError(t, err) + publicKeyHex := base64.StdEncoding.EncodeToString(publicKey) + argsAccess := []string{ + os.Args[0], + "--config", node1, "access", "add", + "--identity", publicKeyHex, + } + for i := 0; i < 2; i++ { + err = runWithCfg(argsAccess, config{}) + require.NoError(t, err) + } + + // Add node 5 which should be participating. + // This makes sure that node 4 is actually participating and caught up. + // If node 4 is not participating, there would be too many faulty nodes + // after adding node 5. + args = append([]string{ + os.Args[0], + "--config", node1, "ordering", "roster", "add", + "--wait", "60s"}, + getExport(t, node5)..., + ) + + err = run(args) + require.NoError(t, err) + + // Run 2 new transactions + for i := 0; i < 2; i++ { + err = runWithCfg(argsAccess, config{}) + require.NoError(t, err) + } + + // Test a timeout waiting for a transaction. + args[7] = "1ns" + err = runWithCfg(args, config{}) + require.EqualError(t, err, "command error: transaction not found after timeout") + + // Test a bad command. + err = runWithCfg([]string{os.Args[0], "ordering", "setup"}, cfg) + require.EqualError(t, err, `Required flag "member" not set`) +} + +// This test creates a chain with two nodes, then gracefully close them. It +// finally restarts both of them to make sure the chain can proceed after the +// restart. It basically tests if the components are correctly loaded from the +// persisten storage. +func TestDvoting_Scenario_RestartNode(t *testing.T) { + dir, err := os.MkdirTemp(os.TempDir(), "dvoting2") + require.NoError(t, err) + + defer os.RemoveAll(dir) + + node1 := filepath.Join(dir, "node1") + node2 := filepath.Join(dir, "node2") + + // Setup the chain and closes the node. + setupChain(t, []string{node1, node2}, []uint16{2210, 2211}) + + sigs := make(chan os.Signal) + wg := sync.WaitGroup{} + wg.Add(2) + + cfg := config{Channel: sigs, Writer: io.Discard} + + // Now the node are restarted. It should correctly follow the existing chain + // and then participate to new blocks. + runNode(t, node1, cfg, 2210, &wg) + runNode(t, node2, cfg, 2211, &wg) + + defer func() { + // Simulate a Ctrl+C + close(sigs) + wg.Wait() + }() + + require.True(t, waitDaemon(t, []string{node1, node2}), "daemon failed to start") + + args := append([]string{ + os.Args[0], + "--config", node1, "ordering", "roster", "add", + "--wait", "60s"}, + getExport(t, node1)..., + ) + + err = run(args) + require.EqualError(t, err, "command error: transaction refused: duplicate in roster: grpcs://127.0.0.1:2210") +} + +// ----------------------------------------------------------------------------- +// Utility functions + +const testDialTimeout = 500 * time.Millisecond + +func runNode(t *testing.T, node string, cfg config, port uint16, wg *sync.WaitGroup) { + go func() { + defer wg.Done() + + err := runWithCfg(makeNodeArg(node, port), cfg) + require.NoError(t, err) + }() +} + +func setupChain(t *testing.T, nodes []string, ports []uint16) { + sigs := make(chan os.Signal) + wg := sync.WaitGroup{} + wg.Add(len(nodes)) + + cfg := config{Channel: sigs, Writer: io.Discard} + + for i, node := range nodes { + runNode(t, node, cfg, ports[i], &wg) + } + + defer func() { + // Simulate a Ctrl+C + close(sigs) + wg.Wait() + }() + + waitDaemon(t, nodes) + + shareCert(t, nodes[1], nodes[0], fmt.Sprintf("//127.0.0.1:%d", ports[0])) + + args := append(append( + []string{os.Args[0], "--config", nodes[0], "ordering", "setup"}, + getExport(t, nodes[0])...), + getExport(t, nodes[1])..., + ) + + err := run(args) + require.NoError(t, err) +} + +func waitDaemon(t *testing.T, daemons []string) bool { + num := 50 + + for _, daemon := range daemons { + path := filepath.Join(daemon, "daemon.sock") + + for i := 0; i < num; i++ { + // Windows: we have to check the file as Dial on Windows creates the + // file and prevent to listen. + _, err := os.Stat(path) + if !os.IsNotExist(err) { + conn, err := net.DialTimeout("unix", path, testDialTimeout) + if err == nil { + conn.Close() + break + } + } + + time.Sleep(100 * time.Millisecond) + + if i+1 >= num { + return false + } + } + } + + return true +} + +func makeNodeArg(path string, port uint16) []string { + return []string{ + os.Args[0], "--config", path, "start", "--listen", "tcp://127.0.0.1:" + strconv.Itoa(int(port)), + } +} + +func shareCert(t *testing.T, path string, src string, addr string) { + args := append( + []string{os.Args[0], "--config", path, "minogrpc", "join", "--address", addr}, + getToken(t, src)..., + ) + + err := run(args) + require.NoError(t, err) +} + +func getToken(t *testing.T, path string) []string { + buffer := new(bytes.Buffer) + cfg := config{ + Writer: buffer, + } + + args := []string{os.Args[0], "--config", path, "minogrpc", "token"} + err := runWithCfg(args, cfg) + require.NoError(t, err) + + return strings.Split(buffer.String(), " ") +} + +func getExport(t *testing.T, path string) []string { + buffer := bytes.NewBufferString("--member ") + cfg := config{ + Writer: buffer, + } + + args := []string{os.Args[0], "--config", path, "ordering", "export"} + + err := runWithCfg(args, cfg) + require.NoError(t, err) + + return strings.Split(buffer.String(), " ") +}