diff --git a/container/blockchain.go b/container/blockchain.go index 418d4093..f93d90ad 100644 --- a/container/blockchain.go +++ b/container/blockchain.go @@ -19,6 +19,7 @@ package container import ( "context" "crypto/ecdsa" + "fmt" "log" "net" "os" @@ -147,16 +148,63 @@ func NewDefaultBlockchainWithFaulty(network *DockerNetwork, numOfNormal int, num return bc } +func NewQuorumBlockchain(network *DockerNetwork, ctn ConstellationNetwork, options ...Option) (bc *blockchain) { + bc = &blockchain{opts: options, isQuorum: true, constellationNetwork: ctn} + bc.opts = append(bc.opts, IsQuorum(true)) + bc.opts = append(bc.opts, NoUSB()) + + var err error + bc.dockerClient, err = client.NewEnvClient() + if err != nil { + log.Fatalf("Cannot connect to Docker daemon, err: %v", err) + } + + if network == nil { + bc.defaultNetwork, err = NewDockerNetwork() + if err != nil { + log.Fatalf("Cannot create Docker network, err: %v", err) + } + network = bc.defaultNetwork + } + + bc.dockerNetworkName = network.Name() + bc.getFreeIPAddrs = network.GetFreeIPAddrs + bc.opts = append(bc.opts, DockerNetworkName(bc.dockerNetworkName)) + + bc.addValidators(ctn.NumOfConstellations()) + return bc +} + +func NewDefaultQuorumBlockchain(network *DockerNetwork, ctn ConstellationNetwork) (bc *blockchain) { + return NewQuorumBlockchain(network, + ctn, + ImageRepository("quay.io/amis/quorum"), + ImageTag("latest"), + DataDir("/data"), + WebSocket(), + WebSocketAddress("0.0.0.0"), + WebSocketAPI("admin,eth,net,web3,personal,miner,istanbul"), + WebSocketOrigin("*"), + NAT("any"), + NoDiscover(), + Etherbase("1a9afb711302c5f83b5902843d1c007a1a137632"), + Mine(), + Logging(false), + ) +} + // ---------------------------------------------------------------------------- type blockchain struct { - dockerClient *client.Client - defaultNetwork *DockerNetwork - dockerNetworkName string - getFreeIPAddrs func(int) ([]net.IP, error) - genesisFile string - validators []Ethereum - opts []Option + dockerClient *client.Client + defaultNetwork *DockerNetwork + dockerNetworkName string + getFreeIPAddrs func(int) ([]net.IP, error) + genesisFile string + isQuorum bool + validators []Ethereum + opts []Option + constellationNetwork ConstellationNetwork } func (bc *blockchain) AddValidators(numOfValidators int) ([]Ethereum, error) { @@ -334,7 +382,7 @@ func (bc *blockchain) connectAll(strong bool) error { func (bc *blockchain) setupGenesis(addrs []common.Address) { if bc.genesisFile == "" { - bc.genesisFile = genesis.NewFile( + bc.genesisFile = genesis.NewFile(bc.isQuorum, genesis.Validators(addrs...), ) } @@ -354,6 +402,13 @@ func (bc *blockchain) setupValidators(ips []net.IP, keys []*ecdsa.PrivateKey, op opts = append(opts, HostWebSocketPort(freeport.GetPort())) opts = append(opts, Key(keys[i])) opts = append(opts, HostIP(ips[i])) + // Add PRIVATE_CONFIG for quorum + if bc.isQuorum { + ct := bc.constellationNetwork.GetConstellation(i) + env := fmt.Sprintf("PRIVATE_CONFIG=%s", ct.ConfigPath()) + opts = append(opts, DockerEnv([]string{env})) + opts = append(opts, DockerBinds(ct.Binds())) + } geth := NewEthereum( bc.dockerClient, @@ -386,3 +441,129 @@ func (bc *blockchain) stop(validators []Ethereum, force bool) error { } return nil } + +// Constellation functions ---------------------------------------------------------------------------- +type ConstellationNetwork interface { + Start() error + Stop() error + Finalize() + NumOfConstellations() int + GetConstellation(int) Constellation +} + +func NewConstellationNetwork(network *DockerNetwork, numOfValidators int, options ...ConstellationOption) (ctn *constellationNetwork) { + ctn = &constellationNetwork{opts: options} + + var err error + ctn.dockerClient, err = client.NewEnvClient() + if err != nil { + log.Fatalf("Cannot connect to Docker daemon, err: %v", err) + } + + if network == nil { + log.Fatalf("Network is required") + } + + ctn.dockerNetworkName = network.Name() + ctn.getFreeIPAddrs = network.GetFreeIPAddrs + ctn.opts = append(ctn.opts, CTDockerNetworkName(ctn.dockerNetworkName)) + + ctn.setupConstellations(numOfValidators) + return ctn +} + +func NewDefaultConstellationNetwork(network *DockerNetwork, numOfValidators int) (ctn *constellationNetwork) { + return NewConstellationNetwork(network, numOfValidators, + CTImageRepository("quay.io/amis/constellation"), + CTImageTag("latest"), + CTWorkDir("/ctdata"), + CTLogging(true), + CTKeyName("node"), + CTSocketFilename("node.ipc"), + CTVerbosity(1), + ) +} + +func (ctn *constellationNetwork) setupConstellations(numOfValidators int) { + // Create constellations + ips, ports := ctn.getFreeHosts(numOfValidators) + for i := 0; i < numOfValidators; i++ { + opts := append(ctn.opts, CTHost(ips[i], ports[i])) + othernodes := ctn.getOtherNodes(ips, ports, i) + opts = append(opts, CTOtherNodes(othernodes)) + ct := NewConstellation(ctn.dockerClient, opts...) + // Generate keys + ct.GenerateKey() + ctn.constellations = append(ctn.constellations, ct) + } +} + +func (ctn *constellationNetwork) Start() error { + // Run nodes + for i, ct := range ctn.constellations { + err := ct.Start() + if err != nil { + log.Fatalf("Failed to start constellation %v, err:%v\n", i, err) + return err + } + } + return nil +} + +func (ctn *constellationNetwork) Stop() error { + // Stop nodes + for i, ct := range ctn.constellations { + err := ct.Stop() + if err != nil { + log.Fatalf("Failed to stop constellation %v, err:%v\n", i, err) + return err + } + } + return nil +} + +func (ctn *constellationNetwork) Finalize() { + // Clean up local working directory + for _, ct := range ctn.constellations { + os.RemoveAll(ct.WorkDir()) + } +} + +func (ctn *constellationNetwork) NumOfConstellations() int { + return len(ctn.constellations) +} + +func (ctn *constellationNetwork) GetConstellation(idx int) Constellation { + return ctn.constellations[idx] +} + +func (ctn *constellationNetwork) getFreeHosts(num int) ([]net.IP, []int) { + ips, err := ctn.getFreeIPAddrs(num) + if err != nil { + log.Fatalf("Cannot get free ip, err: %v", err) + } + var ports []int + for i := 0; i < num; i++ { + ports = append(ports, freeport.GetPort()) + } + return ips, ports +} + +func (ctn *constellationNetwork) getOtherNodes(ips []net.IP, ports []int, idx int) []string { + var result []string + for i, ip := range ips { + if i == idx { + continue + } + result = append(result, fmt.Sprintf("http://%s:%d/", ip, ports[i])) + } + return result +} + +type constellationNetwork struct { + dockerClient *client.Client + dockerNetworkName string + getFreeIPAddrs func(int) ([]net.IP, error) + opts []ConstellationOption + constellations []Constellation +} diff --git a/container/constellation.go b/container/constellation.go new file mode 100644 index 00000000..ba8c0e39 --- /dev/null +++ b/container/constellation.go @@ -0,0 +1,389 @@ +// Copyright 2017 AMIS Technologies +// 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 . + +package container + +import ( + "context" + "fmt" + "io" + "io/ioutil" + "log" + "net" + "os" + "path/filepath" + "strings" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/api/types/network" + "github.com/docker/docker/client" + "github.com/docker/go-connections/nat" +) + +//TODO: refactor this with ethereum options? +/** + * Constellation options + **/ +type ConstellationOption func(*constellation) + +func CTImageRepository(repository string) ConstellationOption { + return func(ct *constellation) { + ct.imageRepository = repository + } +} + +func CTImageTag(tag string) ConstellationOption { + return func(ct *constellation) { + ct.imageTag = tag + } +} + +func CTHost(ip net.IP, port int) ConstellationOption { + return func(ct *constellation) { + ct.port = fmt.Sprintf("%d", port) + ct.ip = ip.String() + ct.flags = append(ct.flags, fmt.Sprintf("--port=%d", port)) + ct.flags = append(ct.flags, fmt.Sprintf("--url=%s", ct.Host())) + } +} + +func CTLogging(enabled bool) ConstellationOption { + return func(ct *constellation) { + ct.logging = enabled + } +} + +func CTDockerNetworkName(dockerNetworkName string) ConstellationOption { + return func(ct *constellation) { + ct.dockerNetworkName = dockerNetworkName + } +} + +func CTWorkDir(workDir string) ConstellationOption { + return func(ct *constellation) { + ct.workDir = workDir + ct.flags = append(ct.flags, fmt.Sprintf("--storage=%s", workDir)) + } +} + +func CTKeyName(keyName string) ConstellationOption { + return func(ct *constellation) { + ct.keyName = keyName + ct.flags = append(ct.flags, fmt.Sprintf("--privatekeys=%s", ct.keyPath("key"))) + ct.flags = append(ct.flags, fmt.Sprintf("--publickeys=%s", ct.keyPath("pub"))) + } +} + +func CTSocketFilename(socketFilename string) ConstellationOption { + return func(ct *constellation) { + ct.socketFilename = socketFilename + ct.flags = append(ct.flags, fmt.Sprintf("--socket=%s", filepath.Join(ct.workDir, socketFilename))) + } +} + +func CTVerbosity(verbosity int) ConstellationOption { + return func(ct *constellation) { + ct.flags = append(ct.flags, fmt.Sprintf("--verbosity=%d", verbosity)) + } +} + +func CTOtherNodes(urls []string) ConstellationOption { + return func(ct *constellation) { + ct.flags = append(ct.flags, fmt.Sprintf("--othernodes=%s", strings.Join(urls, ","))) + } +} + +/** + * Constellation interface and constructors + **/ +type Constellation interface { + // GenerateKey() generates private/public key pair + GenerateKey() (string, error) + // Start() starts constellation service + Start() error + // Stop() stops constellation service + Stop() error + // Host() returns constellation service url + Host() string + // Running() returns true if container is running + Running() bool + // WorkDir() returns local working directory + WorkDir() string + // ConfigPath() returns container config path + ConfigPath() string + // Binds() returns volume binding paths + Binds() []string +} + +func NewConstellation(c *client.Client, options ...ConstellationOption) *constellation { + ct := &constellation{ + client: c, + } + + for _, opt := range options { + opt(ct) + } + + filters := filters.NewArgs() + filters.Add("reference", ct.Image()) + + images, err := c.ImageList(context.Background(), types.ImageListOptions{ + Filters: filters, + }) + + if len(images) == 0 || err != nil { + out, err := ct.client.ImagePull(context.Background(), ct.Image(), types.ImagePullOptions{}) + if err != nil { + log.Printf("Cannot pull %s, err: %v", ct.Image(), err) + return nil + } + if ct.logging { + io.Copy(os.Stdout, out) + } else { + io.Copy(ioutil.Discard, out) + } + } + + return ct +} + +/** + * Constellation implementation + **/ +type constellation struct { + flags []string + ip string + port string + containerID string + workDir string + localWorkDir string + keyName string + socketFilename string + + imageRepository string + imageTag string + dockerNetworkName string + + logging bool + client *client.Client +} + +func (ct *constellation) Image() string { + if ct.imageTag == "" { + return ct.imageRepository + ":latest" + } + return ct.imageRepository + ":" + ct.imageTag +} + +func (ct *constellation) GenerateKey() (localWorkDir string, err error) { + // Generate empty password file + ct.localWorkDir, err = generateRandomDir() + if err != nil { + log.Printf("Failed to generate working dir, err: :%v\n", err) + return "", err + } + + // Generate config file + configContent := fmt.Sprintf("socket=\"%s\"\npublickeys=[\"%s\"]\n", + ct.keyPath("ipc"), ct.keyPath("pub")) + localConfigPath := ct.localConfigPath() + err = ioutil.WriteFile(localConfigPath, []byte(configContent), 0600) + if err != nil { + log.Printf("Failed to write config, err: %v\n", err) + return "", err + } + + // Create container and mount working directory + binds := ct.Binds() + config := &container.Config{ + Image: ct.Image(), + Cmd: []string{ + "--generatekeys=" + ct.keyPath(""), + }, + } + hostConfig := &container.HostConfig{ + Binds: binds, + } + resp, err := ct.client.ContainerCreate(context.Background(), config, hostConfig, nil, "") + if err != nil { + log.Printf("Failed to create container, err: %v\n", err) + return "", err + } + id := resp.ID + + // Start container + if err := ct.client.ContainerStart(context.Background(), id, types.ContainerStartOptions{}); err != nil { + log.Printf("Failed to start container, err: %v\n", err) + return "", err + } + + // Attach container: for stdin interaction with the container. + // - constellation-node generatekeys takes stdin as password + hiresp, err := ct.client.ContainerAttach(context.Background(), id, types.ContainerAttachOptions{Stream: true, Stdin: true}) + if err != nil { + log.Printf("Failed to attach container, err: %v\n", err) + return "", err + } + // - write empty string password to container stdin + hiresp.Conn.Write([]byte("")) //Empty password + + // Wait container + resC, errC := ct.client.ContainerWait(context.Background(), id, container.WaitConditionNotRunning) + select { + case <-resC: + case <-errC: + log.Printf("Failed to wait container, err: %v\n", err) + return "", err + } + + if ct.logging { + ct.showLog(context.Background()) + } + + // Stop container + return ct.localWorkDir, ct.client.ContainerRemove(context.Background(), id, types.ContainerRemoveOptions{Force: true}) +} + +func (ct *constellation) Start() error { + defer func() { + if ct.logging { + go ct.showLog(context.Background()) + } + }() + + // container config + exposedPorts := make(map[nat.Port]struct{}) + exposedPorts[nat.Port(ct.port)] = struct{}{} + config := &container.Config{ + Image: ct.Image(), + Cmd: ct.flags, + ExposedPorts: exposedPorts, + } + + // host config + binds := []string{ + ct.localWorkDir + ":" + ct.workDir, + } + hostConfig := &container.HostConfig{ + Binds: binds, + } + + // Setup network config + var networkingConfig *network.NetworkingConfig + if ct.ip != "" && ct.dockerNetworkName != "" { + endpointsConfig := make(map[string]*network.EndpointSettings) + endpointsConfig[ct.dockerNetworkName] = &network.EndpointSettings{ + IPAMConfig: &network.EndpointIPAMConfig{ + IPv4Address: ct.ip, + }, + } + networkingConfig = &network.NetworkingConfig{ + EndpointsConfig: endpointsConfig, + } + } + + // Create container + resp, err := ct.client.ContainerCreate(context.Background(), config, hostConfig, networkingConfig, "") + if err != nil { + log.Printf("Failed to create container, err: %v\n", err) + return err + } + ct.containerID = resp.ID + + // Start container + err = ct.client.ContainerStart(context.Background(), ct.containerID, types.ContainerStartOptions{}) + if err != nil { + log.Printf("Failed to start container, err: %v, ip:%v\n", err, ct.ip) + return err + } + + return nil +} + +func (ct *constellation) Stop() error { + err := ct.client.ContainerStop(context.Background(), ct.containerID, nil) + if err != nil { + return err + } + + defer os.RemoveAll(ct.localWorkDir) + + return ct.client.ContainerRemove(context.Background(), ct.containerID, + types.ContainerRemoveOptions{ + Force: true, + }) +} + +func (ct *constellation) Host() string { + return fmt.Sprintf("http://%s:%s/", ct.ip, ct.port) +} + +func (ct *constellation) Running() bool { + containers, err := ct.client.ContainerList(context.Background(), types.ContainerListOptions{}) + if err != nil { + log.Printf("Failed to list containers, err: %v", err) + return false + } + + for _, c := range containers { + if c.ID == ct.containerID { + return true + } + } + + return false +} + +func (ct *constellation) WorkDir() string { + return ct.localWorkDir +} + +func (ct *constellation) ConfigPath() string { + return ct.keyPath("conf") +} + +func (ct *constellation) Binds() []string { + return []string{ct.localWorkDir + ":" + ct.workDir} +} + +/** + * Constellation internal functions + **/ + +func (ct *constellation) showLog(context context.Context) { + if readCloser, err := ct.client.ContainerLogs(context, ct.containerID, + types.ContainerLogsOptions{ShowStderr: true, Follow: true}); err == nil { + defer readCloser.Close() + _, err = io.Copy(os.Stdout, readCloser) + if err != nil && err != io.EOF { + log.Fatal(err) + } + } +} + +func (ct *constellation) keyPath(extension string) string { + if extension == "" { + return filepath.Join(ct.workDir, ct.keyName) + } else { + return filepath.Join(ct.workDir, fmt.Sprintf("%s.%s", ct.keyName, extension)) + } +} + +func (ct *constellation) localConfigPath() string { + return filepath.Join(ct.localWorkDir, fmt.Sprintf("%s.conf", ct.keyName)) +} diff --git a/container/constellation_test.go b/container/constellation_test.go new file mode 100644 index 00000000..a8137d4f --- /dev/null +++ b/container/constellation_test.go @@ -0,0 +1,80 @@ +// Copyright 2017 AMIS Technologies +// 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 . + +package container + +import ( + "testing" + + "github.com/docker/docker/client" + "github.com/phayes/freeport" +) + +func TestConstellationContainer(t *testing.T) { + dockerClient, err := client.NewEnvClient() + if err != nil { + t.Error(err) + } + + dockerNetwork, err := NewDockerNetwork() + if err != nil { + t.Error(err) + } + + ips, err := dockerNetwork.GetFreeIPAddrs(1) + if err != nil { + t.Error(err) + } + ip := ips[0] + + port := freeport.GetPort() + + ct := NewConstellation(dockerClient, + CTImageRepository("quay.io/amis/constellation"), + CTImageTag("latest"), + CTHost(ip, port), + CTDockerNetworkName(dockerNetwork.Name()), + CTWorkDir("/data"), + CTLogging(true), + CTKeyName("node"), + CTSocketFilename("node.ipc"), + CTVerbosity(3), + ) + + _, err = ct.GenerateKey() + if err != nil { + t.Error(err) + } + + err = ct.Start() + if err != nil { + t.Error(err) + } + + if !ct.Running() { + t.Error("constellation should be running") + } + + err = ct.Stop() + if err != nil { + t.Error(err) + } + + err = dockerNetwork.Remove() + if err != nil { + t.Error(err) + } +} diff --git a/container/ethereum.go b/container/ethereum.go index be25da07..846823d3 100644 --- a/container/ethereum.go +++ b/container/ethereum.go @@ -48,7 +48,7 @@ import ( ) const ( - healthCheckRetryCount = 5 + healthCheckRetryCount = 10 healthCheckRetryDelay = 2 * time.Second ) @@ -82,6 +82,9 @@ type Ethereum interface { StartMining() error StopMining() error + + DockerEnv() []string + DockerBinds() []string } func NewEthereum(c *client.Client, options ...Option) *ethereum { @@ -128,6 +131,11 @@ type ethereum struct { containerID string node *discover.Node + //Quorum only + isQuorum bool + dockerEnv []string + dockerBinds []string + imageRepository string imageTag string dockerNetworkName string @@ -223,6 +231,7 @@ func (eth *ethereum) Start() error { } binds := []string{} + binds = append(binds, eth.dockerBinds...) if eth.dataDir != "" { binds = append(binds, eth.dataDir+":"+utils.DataDirFlag.Value.Value) } @@ -246,6 +255,7 @@ func (eth *ethereum) Start() error { Image: eth.Image(), Cmd: eth.flags, ExposedPorts: exposedPorts, + Env: eth.DockerEnv(), }, &container.HostConfig{ Binds: binds, @@ -308,6 +318,7 @@ func (eth *ethereum) Start() error { func (eth *ethereum) Stop() error { err := eth.client.ContainerStop(context.Background(), eth.containerID, nil) if err != nil { + fmt.Printf("error on stop container:%v", err) return err } @@ -355,6 +366,7 @@ func (eth *ethereum) NewClient() *ethclient.Client { } client, err := ethclient.Dial(scheme + eth.Host() + ":" + port) if err != nil { + log.Printf("Failed to dial eth client, err: %v\n", err) return nil } return client @@ -611,6 +623,14 @@ func (eth *ethereum) StopMining() error { return client.StopMining(context.Background()) } +func (eth *ethereum) DockerEnv() []string { + return eth.dockerEnv +} + +func (eth *ethereum) DockerBinds() []string { + return eth.dockerBinds +} + // ---------------------------------------------------------------------------- func (eth *ethereum) showLog(context context.Context) { diff --git a/container/options.go b/container/options.go index aa7fde84..385ac5cd 100644 --- a/container/options.go +++ b/container/options.go @@ -86,6 +86,24 @@ func Logging(enabled bool) Option { } } +func IsQuorum(isQuorum bool) Option { + return func(eth *ethereum) { + eth.isQuorum = isQuorum + } +} + +func DockerEnv(env []string) Option { + return func(eth *ethereum) { + eth.dockerEnv = env + } +} + +func DockerBinds(binds []string) Option { + return func(eth *ethereum) { + eth.dockerBinds = binds + } +} + // ---------------------------------------------------------------------------- func Key(key *ecdsa.PrivateKey) Option { @@ -263,5 +281,12 @@ func SyncMode(mode string) Option { return func(eth *ethereum) { eth.flags = append(eth.flags, "--"+utils.SyncModeFlag.Name) eth.flags = append(eth.flags, mode) + + } +} + +func NoUSB() Option { + return func(eth *ethereum) { + eth.flags = append(eth.flags, "--"+utils.NoUSBFlag.Name) } } diff --git a/genesis/genesis.go b/genesis/genesis.go index 9f265634..50a6d8c2 100644 --- a/genesis/genesis.go +++ b/genesis/genesis.go @@ -18,10 +18,12 @@ package genesis import ( "encoding/json" + "fmt" "io/ioutil" "log" "math/big" "path/filepath" + "strings" "time" "github.com/ethereum/go-ethereum/consensus/istanbul" @@ -62,20 +64,20 @@ func New(options ...Option) *core.Genesis { return genesis } -func NewFile(options ...Option) string { +func NewFile(isQuorum bool, options ...Option) string { dir, err := generateRandomDir() if err != nil { log.Fatalf("Failed to create random directory, err: %v", err) } genesis := New(options...) - if err := Save(dir, genesis); err != nil { + if err := Save(dir, genesis, isQuorum); err != nil { log.Fatalf("Failed to save genesis to '%s', err: %v", dir, err) } return filepath.Join(dir, FileName) } -func Save(dataDir string, genesis *core.Genesis) error { +func Save(dataDir string, genesis *core.Genesis, isQuorum bool) error { filePath := filepath.Join(dataDir, FileName) raw, err := json.Marshal(genesis) @@ -83,5 +85,12 @@ func Save(dataDir string, genesis *core.Genesis) error { return err } + //Quorum hack: add isQuorum field + if isQuorum { + jsonStr := string(raw) + idx := strings.Index(jsonStr, ",\"istanbul\"") + jsonStr = fmt.Sprintf("%s,\"isQuorum\":true%s", jsonStr[:idx], jsonStr[idx:]) + raw = []byte(jsonStr) + } return ioutil.WriteFile(filePath, raw, 0600) } diff --git a/tests/quorum/functional/general_consensus_test.go b/tests/quorum/functional/general_consensus_test.go new file mode 100644 index 00000000..99e6e446 --- /dev/null +++ b/tests/quorum/functional/general_consensus_test.go @@ -0,0 +1,261 @@ +// Copyright 2017 AMIS Technologies +// 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 . + +package functional + +import ( + "sync" + + "github.com/getamis/istanbul-tools/tests" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "github.com/getamis/istanbul-tools/container" +) + +var _ = Describe("QFS-01: General consensus", func() { + const ( + numberOfValidators = 4 + ) + var ( + constellationNetwork container.ConstellationNetwork + blockchain container.Blockchain + ) + + BeforeEach(func() { + constellationNetwork = container.NewDefaultConstellationNetwork(dockerNetwork, numberOfValidators) + Expect(constellationNetwork.Start()).To(BeNil()) + blockchain = container.NewDefaultQuorumBlockchain(dockerNetwork, constellationNetwork) + Expect(blockchain.Start(true)).To(BeNil()) + }) + + AfterEach(func() { + blockchain.Stop(true) + blockchain.Finalize() + constellationNetwork.Stop() + constellationNetwork.Finalize() + }) + + // FIt("QFS-01-01, QFS-01-02: Blockchain initialization and run", func() { + // fmt.Printf("validators:%v\n", blockchain.Validators()) + // errc := make(chan error, len(blockchain.Validators())) + // valSet := make(map[common.Address]bool, numberOfValidators) + // for _, geth := range blockchain.Validators() { + // valSet[geth.Address()] = true + // } + // for _, geth := range blockchain.Validators() { + // go func(geth container.Ethereum) { + // // 1. Verify genesis block + // c := geth.NewClient() + // header, err := c.HeaderByNumber(context.Background(), big.NewInt(0)) + // if err != nil { + // errc <- err + // return + // } + + // if header.GasLimit.Int64() != genesis.InitGasLimit { + // errStr := fmt.Sprintf("Invalid genesis gas limit. want:%v, got:%v", genesis.InitGasLimit, header.GasLimit.Int64()) + // errc <- errors.New(errStr) + // return + // } + + // if header.Difficulty.Int64() != genesis.InitDifficulty { + // errStr := fmt.Sprintf("Invalid genesis difficulty. want:%v, got:%v", genesis.InitDifficulty, header.Difficulty.Int64()) + // errc <- errors.New(errStr) + // return + // } + + // if header.MixDigest != types.IstanbulDigest { + // errStr := fmt.Sprintf("Invalid block mixhash. want:%v, got:%v", types.IstanbulDigest, header.MixDigest) + // errc <- errors.New(errStr) + // return + + // } + + // // 2. Check validator set + // istClient := geth.NewIstanbulClient() + // vals, err := istClient.GetValidators(context.Background(), big.NewInt(0)) + // if err != nil { + // errc <- err + // return + // } + + // for _, val := range vals { + // if _, ok := valSet[val]; !ok { + // errc <- errors.New("Invalid validator address.") + // return + // } + // } + + // errc <- nil + // }(geth) + // } + + // for i := 0; i < len(blockchain.Validators()); i++ { + // err := <-errc + // Expect(err).To(BeNil()) + // } + + // }) + + It("QFS-01-03: Peer connection", func(done Done) { + expectedPeerCount := len(blockchain.Validators()) - 1 + tests.WaitFor(blockchain.Validators(), func(v container.Ethereum, wg *sync.WaitGroup) { + Expect(v.WaitForPeersConnected(expectedPeerCount)).To(BeNil()) + wg.Done() + }) + + close(done) + }, 20) + + // It("TFS-01-04: Consensus progress", func(done Done) { + // const ( + // targetBlockHeight = 10 + // maxBlockPeriod = 3 + // ) + + // By("Wait for consensus progress", func() { + // tests.WaitFor(blockchain.Validators(), func(geth container.Ethereum, wg *sync.WaitGroup) { + // Expect(geth.WaitForBlockHeight(targetBlockHeight)).To(BeNil()) + // wg.Done() + // }) + // }) + + // By("Check the block period should less than 3 seconds", func() { + // errc := make(chan error, len(blockchain.Validators())) + // for _, geth := range blockchain.Validators() { + // go func(geth container.Ethereum) { + // c := geth.NewClient() + // lastBlockTime := int64(0) + // // The reason to verify block period from block#2 is that + // // the block period from block#1 to block#2 might take long time due to + // // encounter several round changes at the beginning of the consensus progress. + // for i := 2; i <= targetBlockHeight; i++ { + // header, err := c.HeaderByNumber(context.Background(), big.NewInt(int64(i))) + // if err != nil { + // errc <- err + // return + // } + // if lastBlockTime != 0 { + // diff := header.Time.Int64() - lastBlockTime + // if diff > maxBlockPeriod { + // errStr := fmt.Sprintf("Invaild block(%v) period, want:%v, got:%v", header.Number.Int64(), maxBlockPeriod, diff) + // errc <- errors.New(errStr) + // return + // } + // } + // lastBlockTime = header.Time.Int64() + // } + // errc <- nil + // }(geth) + // } + + // for i := 0; i < len(blockchain.Validators()); i++ { + // err := <-errc + // Expect(err).To(BeNil()) + // } + // }) + // close(done) + // }, 60) + + // It("TFS-01-05: Round robin proposer selection", func(done Done) { + // var ( + // timesOfBeProposer = 3 + // targetBlockHeight = timesOfBeProposer * numberOfValidators + // emptyProposer = common.Address{} + // ) + + // By("Wait for consensus progress", func() { + // tests.WaitFor(blockchain.Validators(), func(geth container.Ethereum, wg *sync.WaitGroup) { + // Expect(geth.WaitForBlockHeight(targetBlockHeight)).To(BeNil()) + // wg.Done() + // }) + // }) + + // By("Block proposer selection should follow round-robin policy", func() { + // errc := make(chan error, len(blockchain.Validators())) + // for _, geth := range blockchain.Validators() { + // go func(geth container.Ethereum) { + // c := geth.NewClient() + // istClient := geth.NewIstanbulClient() + + // // get initial validator set + // vals, err := istClient.GetValidators(context.Background(), big.NewInt(0)) + // if err != nil { + // errc <- err + // return + // } + + // lastProposerIdx := -1 + // counts := make(map[common.Address]int, numberOfValidators) + // // initial count map + // for _, addr := range vals { + // counts[addr] = 0 + // } + // for i := 1; i <= targetBlockHeight; i++ { + // header, err := c.HeaderByNumber(context.Background(), big.NewInt(int64(i))) + // if err != nil { + // errc <- err + // return + // } + + // p := container.GetProposer(header) + // if p == emptyProposer { + // errStr := fmt.Sprintf("Empty block(%v) proposer", header.Number.Int64()) + // errc <- errors.New(errStr) + // return + // } + // // count the times to be the proposer + // if count, ok := counts[p]; ok { + // counts[p] = count + 1 + // } + // // check if the proposer is valid + // if lastProposerIdx == -1 { + // for i, val := range vals { + // if p == val { + // lastProposerIdx = i + // break + // } + // } + // } else { + // proposerIdx := (lastProposerIdx + 1) % len(vals) + // if p != vals[proposerIdx] { + // errStr := fmt.Sprintf("Invaild block(%v) proposer, want:%v, got:%v", header.Number.Int64(), vals[proposerIdx], p) + // errc <- errors.New(errStr) + // return + // } + // lastProposerIdx = proposerIdx + // } + // } + // // check times to be proposer + // for _, count := range counts { + // if count != timesOfBeProposer { + // errc <- errors.New("Wrong times to be proposer.") + // return + // } + // } + // errc <- nil + // }(geth) + // } + + // for i := 0; i < len(blockchain.Validators()); i++ { + // err := <-errc + // Expect(err).To(BeNil()) + // } + // }) + // close(done) + // }, 120) +}) diff --git a/tests/quorum/functional/integration_test.go b/tests/quorum/functional/integration_test.go new file mode 100644 index 00000000..c020e44f --- /dev/null +++ b/tests/quorum/functional/integration_test.go @@ -0,0 +1,44 @@ +// Copyright 2017 AMIS Technologies +// 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 . + +package functional + +import ( + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "github.com/getamis/istanbul-tools/container" +) + +var dockerNetwork *container.DockerNetwork + +func TestQuorumIstanbul(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Quorum Istanbul Test Suite") +} + +var _ = BeforeSuite(func() { + var err error + dockerNetwork, err = container.NewDockerNetwork() + Expect(err).To(BeNil()) +}) + +var _ = AfterSuite(func() { + err := dockerNetwork.Remove() + Expect(err).To(BeNil()) +})