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