From 1e0c336d293367bb75df494a685cabb2029f318e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?P=C3=A9ter=20Szil=C3=A1gyi?= <peterke@gmail.com>
Date: Fri, 20 Oct 2017 11:14:10 +0300
Subject: cmd/puppeth: etherchain light block explorer for PoW nets

---
 cmd/puppeth/genesis.go         | 208 +++++++++++++++++++++++++++++++++++++
 cmd/puppeth/module_ethstats.go |   6 +-
 cmd/puppeth/module_explorer.go | 226 +++++++++++++++++++++++++++++++++++++++++
 cmd/puppeth/module_node.go     |   4 +-
 cmd/puppeth/wizard_explorer.go | 111 ++++++++++++++++++++
 cmd/puppeth/wizard_faucet.go   |   2 +-
 cmd/puppeth/wizard_netstats.go |   8 ++
 cmd/puppeth/wizard_network.go  |  11 +-
 8 files changed, 565 insertions(+), 11 deletions(-)
 create mode 100644 cmd/puppeth/genesis.go
 create mode 100644 cmd/puppeth/module_explorer.go
 create mode 100644 cmd/puppeth/wizard_explorer.go

(limited to 'cmd/puppeth')

diff --git a/cmd/puppeth/genesis.go b/cmd/puppeth/genesis.go
new file mode 100644
index 000000000..2b66df43c
--- /dev/null
+++ b/cmd/puppeth/genesis.go
@@ -0,0 +1,208 @@
+// Copyright 2017 The go-ethereum Authors
+// This file is part of go-ethereum.
+//
+// go-ethereum is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// go-ethereum is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
+
+package main
+
+import (
+	"encoding/binary"
+	"errors"
+	"math"
+
+	"github.com/ethereum/go-ethereum/common"
+	"github.com/ethereum/go-ethereum/common/hexutil"
+	"github.com/ethereum/go-ethereum/consensus/ethash"
+	"github.com/ethereum/go-ethereum/core"
+	"github.com/ethereum/go-ethereum/params"
+)
+
+// parityChainSpec is the chain specification format used by Parity.
+type parityChainSpec struct {
+	Name   string `json:"name"`
+	Engine struct {
+		Ethash struct {
+			Params struct {
+				MinimumDifficulty      *hexutil.Big `json:"minimumDifficulty"`
+				DifficultyBoundDivisor *hexutil.Big `json:"difficultyBoundDivisor"`
+				GasLimitBoundDivisor   *hexutil.Big `json:"gasLimitBoundDivisor"`
+				DurationLimit          *hexutil.Big `json:"durationLimit"`
+				BlockReward            *hexutil.Big `json:"blockReward"`
+				HomesteadTransition    uint64       `json:"homesteadTransition"`
+				EIP150Transition       uint64       `json:"eip150Transition"`
+				EIP160Transition       uint64       `json:"eip160Transition"`
+				EIP161abcTransition    uint64       `json:"eip161abcTransition"`
+				EIP161dTransition      uint64       `json:"eip161dTransition"`
+				EIP649Reward           *hexutil.Big `json:"eip649Reward"`
+				EIP100bTransition      uint64       `json:"eip100bTransition"`
+				EIP649Transition       uint64       `json:"eip649Transition"`
+			} `json:"params"`
+		} `json:"Ethash"`
+	} `json:"engine"`
+
+	Params struct {
+		MaximumExtraDataSize hexutil.Uint64 `json:"maximumExtraDataSize"`
+		MinGasLimit          *hexutil.Big   `json:"minGasLimit"`
+		NetworkID            hexutil.Uint64 `json:"networkID"`
+		MaxCodeSize          uint64         `json:"maxCodeSize"`
+		EIP155Transition     uint64         `json:"eip155Transition"`
+		EIP98Transition      uint64         `json:"eip98Transition"`
+		EIP86Transition      uint64         `json:"eip86Transition"`
+		EIP140Transition     uint64         `json:"eip140Transition"`
+		EIP211Transition     uint64         `json:"eip211Transition"`
+		EIP214Transition     uint64         `json:"eip214Transition"`
+		EIP658Transition     uint64         `json:"eip658Transition"`
+	} `json:"params"`
+
+	Genesis struct {
+		Seal struct {
+			Ethereum struct {
+				Nonce   hexutil.Bytes `json:"nonce"`
+				MixHash hexutil.Bytes `json:"mixHash"`
+			} `json:"ethereum"`
+		} `json:"seal"`
+
+		Difficulty *hexutil.Big   `json:"difficulty"`
+		Author     common.Address `json:"author"`
+		Timestamp  hexutil.Uint64 `json:"timestamp"`
+		ParentHash common.Hash    `json:"parentHash"`
+		ExtraData  hexutil.Bytes  `json:"extraData"`
+		GasLimit   hexutil.Uint64 `json:"gasLimit"`
+	} `json:"genesis"`
+
+	Nodes    []string                                   `json:"nodes"`
+	Accounts map[common.Address]*parityChainSpecAccount `json:"accounts"`
+}
+
+// parityChainSpecAccount is the prefunded genesis account and/or precompiled
+// contract definition.
+type parityChainSpecAccount struct {
+	Balance *hexutil.Big            `json:"balance"`
+	Nonce   uint64                  `json:"nonce,omitempty"`
+	Builtin *parityChainSpecBuiltin `json:"builtin,omitempty"`
+}
+
+// parityChainSpecBuiltin is the precompiled contract definition.
+type parityChainSpecBuiltin struct {
+	Name       string                  `json:"name,omitempty"`
+	ActivateAt uint64                  `json:"activate_at,omitempty"`
+	Pricing    *parityChainSpecPricing `json:"pricing,omitempty"`
+}
+
+// parityChainSpecPricing represents the different pricing models that builtin
+// contracts might advertise using.
+type parityChainSpecPricing struct {
+	Linear       *parityChainSpecLinearPricing       `json:"linear,omitempty"`
+	ModExp       *parityChainSpecModExpPricing       `json:"modexp,omitempty"`
+	AltBnPairing *parityChainSpecAltBnPairingPricing `json:"alt_bn128_pairing,omitempty"`
+}
+
+type parityChainSpecLinearPricing struct {
+	Base uint64 `json:"base"`
+	Word uint64 `json:"word"`
+}
+
+type parityChainSpecModExpPricing struct {
+	Divisor uint64 `json:"divisor"`
+}
+
+type parityChainSpecAltBnPairingPricing struct {
+	Base uint64 `json:"base"`
+	Pair uint64 `json:"pair"`
+}
+
+// newParityChainSpec converts a go-ethereum genesis block into a Parity specific
+// chain specification format.
+func newParityChainSpec(network string, genesis *core.Genesis, bootnodes []string) (*parityChainSpec, error) {
+	// Only ethash is currently supported between go-ethereum and Parity
+	if genesis.Config.Ethash == nil {
+		return nil, errors.New("unsupported consensus engine")
+	}
+	// Reconstruct the chain spec in Parity's format
+	spec := &parityChainSpec{
+		Name:  network,
+		Nodes: bootnodes,
+	}
+	spec.Engine.Ethash.Params.MinimumDifficulty = (*hexutil.Big)(params.MinimumDifficulty)
+	spec.Engine.Ethash.Params.DifficultyBoundDivisor = (*hexutil.Big)(params.DifficultyBoundDivisor)
+	spec.Engine.Ethash.Params.GasLimitBoundDivisor = (*hexutil.Big)(params.GasLimitBoundDivisor)
+	spec.Engine.Ethash.Params.DurationLimit = (*hexutil.Big)(params.DurationLimit)
+	spec.Engine.Ethash.Params.BlockReward = (*hexutil.Big)(ethash.FrontierBlockReward)
+	spec.Engine.Ethash.Params.HomesteadTransition = genesis.Config.HomesteadBlock.Uint64()
+	spec.Engine.Ethash.Params.EIP150Transition = genesis.Config.EIP150Block.Uint64()
+	spec.Engine.Ethash.Params.EIP160Transition = genesis.Config.EIP155Block.Uint64()
+	spec.Engine.Ethash.Params.EIP161abcTransition = genesis.Config.EIP158Block.Uint64()
+	spec.Engine.Ethash.Params.EIP161dTransition = genesis.Config.EIP158Block.Uint64()
+	spec.Engine.Ethash.Params.EIP649Reward = (*hexutil.Big)(ethash.ByzantiumBlockReward)
+	spec.Engine.Ethash.Params.EIP100bTransition = genesis.Config.ByzantiumBlock.Uint64()
+	spec.Engine.Ethash.Params.EIP649Transition = genesis.Config.ByzantiumBlock.Uint64()
+
+	spec.Params.MaximumExtraDataSize = (hexutil.Uint64)(params.MaximumExtraDataSize)
+	spec.Params.MinGasLimit = (*hexutil.Big)(params.MinGasLimit)
+	spec.Params.NetworkID = (hexutil.Uint64)(genesis.Config.ChainId.Uint64())
+	spec.Params.MaxCodeSize = params.MaxCodeSize
+	spec.Params.EIP155Transition = genesis.Config.EIP155Block.Uint64()
+	spec.Params.EIP98Transition = math.MaxUint64
+	spec.Params.EIP86Transition = math.MaxUint64
+	spec.Params.EIP140Transition = genesis.Config.ByzantiumBlock.Uint64()
+	spec.Params.EIP211Transition = genesis.Config.ByzantiumBlock.Uint64()
+	spec.Params.EIP214Transition = genesis.Config.ByzantiumBlock.Uint64()
+	spec.Params.EIP658Transition = genesis.Config.ByzantiumBlock.Uint64()
+
+	spec.Genesis.Seal.Ethereum.Nonce = (hexutil.Bytes)(make([]byte, 8))
+	binary.LittleEndian.PutUint64(spec.Genesis.Seal.Ethereum.Nonce[:], genesis.Nonce)
+
+	spec.Genesis.Seal.Ethereum.MixHash = (hexutil.Bytes)(genesis.Mixhash[:])
+	spec.Genesis.Difficulty = (*hexutil.Big)(genesis.Difficulty)
+	spec.Genesis.Author = genesis.Coinbase
+	spec.Genesis.Timestamp = (hexutil.Uint64)(genesis.Timestamp)
+	spec.Genesis.ParentHash = genesis.ParentHash
+	spec.Genesis.ExtraData = (hexutil.Bytes)(genesis.ExtraData)
+	spec.Genesis.GasLimit = (hexutil.Uint64)(genesis.GasLimit)
+
+	spec.Accounts = make(map[common.Address]*parityChainSpecAccount)
+	for address, account := range genesis.Alloc {
+		spec.Accounts[address] = &parityChainSpecAccount{
+			Balance: (*hexutil.Big)(account.Balance),
+			Nonce:   account.Nonce,
+		}
+	}
+	spec.Accounts[common.BytesToAddress([]byte{1})].Builtin = &parityChainSpecBuiltin{
+		Name: "ecrecover", Pricing: &parityChainSpecPricing{Linear: &parityChainSpecLinearPricing{Base: 3000}},
+	}
+	spec.Accounts[common.BytesToAddress([]byte{2})].Builtin = &parityChainSpecBuiltin{
+		Name: "sha256", Pricing: &parityChainSpecPricing{Linear: &parityChainSpecLinearPricing{Base: 60, Word: 12}},
+	}
+	spec.Accounts[common.BytesToAddress([]byte{3})].Builtin = &parityChainSpecBuiltin{
+		Name: "ripemd160", Pricing: &parityChainSpecPricing{Linear: &parityChainSpecLinearPricing{Base: 600, Word: 120}},
+	}
+	spec.Accounts[common.BytesToAddress([]byte{4})].Builtin = &parityChainSpecBuiltin{
+		Name: "identity", Pricing: &parityChainSpecPricing{Linear: &parityChainSpecLinearPricing{Base: 15, Word: 3}},
+	}
+	if genesis.Config.ByzantiumBlock != nil {
+		spec.Accounts[common.BytesToAddress([]byte{5})].Builtin = &parityChainSpecBuiltin{
+			Name: "modexp", ActivateAt: genesis.Config.ByzantiumBlock.Uint64(), Pricing: &parityChainSpecPricing{ModExp: &parityChainSpecModExpPricing{Divisor: 20}},
+		}
+		spec.Accounts[common.BytesToAddress([]byte{6})].Builtin = &parityChainSpecBuiltin{
+			Name: "alt_bn128_add", ActivateAt: genesis.Config.ByzantiumBlock.Uint64(), Pricing: &parityChainSpecPricing{Linear: &parityChainSpecLinearPricing{Base: 500}},
+		}
+		spec.Accounts[common.BytesToAddress([]byte{7})].Builtin = &parityChainSpecBuiltin{
+			Name: "alt_bn128_mul", ActivateAt: genesis.Config.ByzantiumBlock.Uint64(), Pricing: &parityChainSpecPricing{Linear: &parityChainSpecLinearPricing{Base: 40000}},
+		}
+		spec.Accounts[common.BytesToAddress([]byte{8})].Builtin = &parityChainSpecBuiltin{
+			Name: "alt_bn128_pairing", ActivateAt: genesis.Config.ByzantiumBlock.Uint64(), Pricing: &parityChainSpecPricing{AltBnPairing: &parityChainSpecAltBnPairingPricing{Base: 100000, Pair: 80000}},
+		}
+	}
+	return spec, nil
+}
diff --git a/cmd/puppeth/module_ethstats.go b/cmd/puppeth/module_ethstats.go
index 7ce3ca3cd..b9874cf58 100644
--- a/cmd/puppeth/module_ethstats.go
+++ b/cmd/puppeth/module_ethstats.go
@@ -34,9 +34,9 @@ var ethstatsDockerfile = `
 FROM mhart/alpine-node:latest
 
 RUN \
-  apk add --update git                                         && \
-  git clone --depth=1 https://github.com/karalabe/eth-netstats && \
-	apk del git && rm -rf /var/cache/apk/*                       && \
+  apk add --update git                                        && \
+  git clone --depth=1 https://github.com/puppeth/eth-netstats && \
+	apk del git && rm -rf /var/cache/apk/*                      && \
 	\
   cd /eth-netstats && npm install && npm install -g grunt-cli && grunt
 
diff --git a/cmd/puppeth/module_explorer.go b/cmd/puppeth/module_explorer.go
new file mode 100644
index 000000000..cb7fc5430
--- /dev/null
+++ b/cmd/puppeth/module_explorer.go
@@ -0,0 +1,226 @@
+// Copyright 2017 The go-ethereum Authors
+// This file is part of go-ethereum.
+//
+// go-ethereum is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// go-ethereum is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
+
+package main
+
+import (
+	"bytes"
+	"fmt"
+	"html/template"
+	"math/rand"
+	"path/filepath"
+	"strconv"
+	"strings"
+
+	"github.com/ethereum/go-ethereum/log"
+)
+
+// explorerDockerfile is the Dockerfile required to run a block explorer.
+var explorerDockerfile = `
+FROM parity/parity:stable
+
+RUN \
+	apt-get update && apt-get install -y curl git npm make g++ --no-install-recommends && \
+  npm install -g n pm2 && n stable
+
+RUN \
+  git clone --depth=1 https://github.com/puppeth/eth-net-intelligence-api && \
+	cd eth-net-intelligence-api && npm install
+
+RUN \
+  git clone --depth=1 https://github.com/puppeth/etherchain-light --recursive && \
+	cd etherchain-light && npm install && mv config.js.example config.js        && \
+	sed -i '/this.bootstrapUrl/c\  this.bootstrapUrl = "https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css";' config.js
+
+ADD ethstats.json /ethstats.json
+ADD chain.json /chain.json
+
+RUN \
+  echo '(cd eth-net-intelligence-api && pm2 start /ethstats.json)' >  explorer.sh && \
+	echo '(cd etherchain-light && npm start &)'                      >> explorer.sh && \
+	echo '/parity/parity --chain=/chain.json --port={{.NodePort}} --tracing=on --fat-db=on --pruning=archive' >> explorer.sh
+
+EXPOSE 3000
+
+ENTRYPOINT ["/bin/sh", "explorer.sh"]
+`
+
+// explorerEthstats is the configuration file for the ethstats javascript client.
+var explorerEthstats = `[
+  {
+    "name"              : "node-app",
+    "script"            : "app.js",
+    "log_date_format"   : "YYYY-MM-DD HH:mm Z",
+    "merge_logs"        : false,
+    "watch"             : false,
+    "max_restarts"      : 10,
+    "exec_interpreter"  : "node",
+    "exec_mode"         : "fork_mode",
+    "env":
+    {
+      "NODE_ENV"        : "production",
+      "RPC_HOST"        : "localhost",
+      "RPC_PORT"        : "8545",
+      "LISTENING_PORT"  : "{{.Port}}",
+      "INSTANCE_NAME"   : "{{.Name}}",
+      "CONTACT_DETAILS" : "",
+      "WS_SERVER"       : "{{.Host}}",
+      "WS_SECRET"       : "{{.Secret}}",
+      "VERBOSITY"       : 2
+    }
+  }
+]`
+
+// explorerComposefile is the docker-compose.yml file required to deploy and
+// maintain a block explorer.
+var explorerComposefile = `
+version: '2'
+services:
+  explorer:
+    build: .
+    image: {{.Network}}/explorer
+    ports:
+      - "{{.NodePort}}:{{.NodePort}}"
+      - "{{.NodePort}}:{{.NodePort}}/udp"{{if not .VHost}}
+      - "{{.WebPort}}:3000"{{end}}
+    volumes:
+      - {{.Datadir}}:/root/.local/share/io.parity.ethereum
+    environment:
+      - NODE_PORT={{.NodePort}}/tcp
+      - STATS={{.Ethstats}}{{if .VHost}}
+      - VIRTUAL_HOST={{.VHost}}
+      - VIRTUAL_PORT=3000{{end}}
+    logging:
+      driver: "json-file"
+      options:
+        max-size: "1m"
+        max-file: "10"
+    restart: always
+`
+
+// deployExplorer deploys a new block explorer container to a remote machine via
+// SSH, docker and docker-compose. If an instance with the specified network name
+// already exists there, it will be overwritten!
+func deployExplorer(client *sshClient, network string, chainspec []byte, config *explorerInfos, nocache bool) ([]byte, error) {
+	// Generate the content to upload to the server
+	workdir := fmt.Sprintf("%d", rand.Int63())
+	files := make(map[string][]byte)
+
+	dockerfile := new(bytes.Buffer)
+	template.Must(template.New("").Parse(explorerDockerfile)).Execute(dockerfile, map[string]interface{}{
+		"NodePort": config.nodePort,
+	})
+	files[filepath.Join(workdir, "Dockerfile")] = dockerfile.Bytes()
+
+	ethstats := new(bytes.Buffer)
+	template.Must(template.New("").Parse(explorerEthstats)).Execute(ethstats, map[string]interface{}{
+		"Port":   config.nodePort,
+		"Name":   config.ethstats[:strings.Index(config.ethstats, ":")],
+		"Secret": config.ethstats[strings.Index(config.ethstats, ":")+1 : strings.Index(config.ethstats, "@")],
+		"Host":   config.ethstats[strings.Index(config.ethstats, "@")+1:],
+	})
+	files[filepath.Join(workdir, "ethstats.json")] = ethstats.Bytes()
+
+	composefile := new(bytes.Buffer)
+	template.Must(template.New("").Parse(explorerComposefile)).Execute(composefile, map[string]interface{}{
+		"Datadir":  config.datadir,
+		"Network":  network,
+		"NodePort": config.nodePort,
+		"VHost":    config.webHost,
+		"WebPort":  config.webPort,
+		"Ethstats": config.ethstats[:strings.Index(config.ethstats, ":")],
+	})
+	files[filepath.Join(workdir, "docker-compose.yaml")] = composefile.Bytes()
+
+	files[filepath.Join(workdir, "chain.json")] = []byte(chainspec)
+
+	// Upload the deployment files to the remote server (and clean up afterwards)
+	if out, err := client.Upload(files); err != nil {
+		return out, err
+	}
+	defer client.Run("rm -rf " + workdir)
+
+	// Build and deploy the boot or seal node service
+	if nocache {
+		return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s build --pull --no-cache && docker-compose -p %s up -d --force-recreate", workdir, network, network))
+	}
+	return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s up -d --build --force-recreate", workdir, network))
+}
+
+// explorerInfos is returned from a block explorer status check to allow reporting
+// various configuration parameters.
+type explorerInfos struct {
+	datadir  string
+	ethstats string
+	nodePort int
+	webHost  string
+	webPort  int
+}
+
+// Report converts the typed struct into a plain string->string map, cotnaining
+// most - but not all - fields for reporting to the user.
+func (info *explorerInfos) Report() map[string]string {
+	report := map[string]string{
+		"Data directory":         info.datadir,
+		"Node listener port ":    strconv.Itoa(info.nodePort),
+		"Ethstats username":      info.ethstats,
+		"Website address ":       info.webHost,
+		"Website listener port ": strconv.Itoa(info.webPort),
+	}
+	return report
+}
+
+// checkExplorer does a health-check against an boot or seal node server to verify
+// whether it's running, and if yes, whether it's responsive.
+func checkExplorer(client *sshClient, network string) (*explorerInfos, error) {
+	// Inspect a possible block explorer container on the host
+	infos, err := inspectContainer(client, fmt.Sprintf("%s_explorer_1", network))
+	if err != nil {
+		return nil, err
+	}
+	if !infos.running {
+		return nil, ErrServiceOffline
+	}
+	// Resolve the port from the host, or the reverse proxy
+	webPort := infos.portmap["3000/tcp"]
+	if webPort == 0 {
+		if proxy, _ := checkNginx(client, network); proxy != nil {
+			webPort = proxy.port
+		}
+	}
+	if webPort == 0 {
+		return nil, ErrNotExposed
+	}
+	// Resolve the host from the reverse-proxy and the config values
+	host := infos.envvars["VIRTUAL_HOST"]
+	if host == "" {
+		host = client.server
+	}
+	// Run a sanity check to see if the devp2p is reachable
+	nodePort := infos.portmap[infos.envvars["NODE_PORT"]]
+	if err = checkPort(client.server, nodePort); err != nil {
+		log.Warn(fmt.Sprintf("Explorer devp2p port seems unreachable"), "server", client.server, "port", nodePort, "err", err)
+	}
+	// Assemble and return the useful infos
+	stats := &explorerInfos{
+		datadir:  infos.volumes["/root/.local/share/io.parity.ethereum"],
+		nodePort: nodePort,
+		webHost:  host,
+		webPort:  webPort,
+		ethstats: infos.envvars["STATS"],
+	}
+	return stats, nil
+}
diff --git a/cmd/puppeth/module_node.go b/cmd/puppeth/module_node.go
index 37da770aa..9b8d5f0f7 100644
--- a/cmd/puppeth/module_node.go
+++ b/cmd/puppeth/module_node.go
@@ -40,7 +40,7 @@ ADD genesis.json /genesis.json
 	ADD signer.pass /signer.pass
 {{end}}
 RUN \
-  echo 'geth init /genesis.json' > geth.sh && \{{if .Unlock}}
+  echo 'geth --cache 512 init /genesis.json' > geth.sh && \{{if .Unlock}}
 	echo 'mkdir -p /root/.ethereum/keystore/ && cp /signer.json /root/.ethereum/keystore/' >> geth.sh && \{{end}}
 	echo $'geth --networkid {{.NetworkID}} --cache 512 --port {{.Port}} --maxpeers {{.Peers}} {{.LightFlag}} --ethstats \'{{.Ethstats}}\' {{if .BootV4}}--bootnodesv4 {{.BootV4}}{{end}} {{if .BootV5}}--bootnodesv5 {{.BootV5}}{{end}} {{if .Etherbase}}--etherbase {{.Etherbase}} --mine --minerthreads 1{{end}} {{if .Unlock}}--unlock 0 --password /signer.pass --mine{{end}} --targetgaslimit {{.GasTarget}} --gasprice {{.GasPrice}}' >> geth.sh
 
@@ -131,9 +131,7 @@ func deployNode(client *sshClient, network string, bootv4, bootv5 []string, conf
 	})
 	files[filepath.Join(workdir, "docker-compose.yaml")] = composefile.Bytes()
 
-	//genesisfile, _ := json.MarshalIndent(config.genesis, "", "  ")
 	files[filepath.Join(workdir, "genesis.json")] = config.genesis
-
 	if config.keyJSON != "" {
 		files[filepath.Join(workdir, "signer.json")] = []byte(config.keyJSON)
 		files[filepath.Join(workdir, "signer.pass")] = []byte(config.keyPass)
diff --git a/cmd/puppeth/wizard_explorer.go b/cmd/puppeth/wizard_explorer.go
new file mode 100644
index 000000000..2df77fa5c
--- /dev/null
+++ b/cmd/puppeth/wizard_explorer.go
@@ -0,0 +1,111 @@
+// Copyright 2017 The go-ethereum Authors
+// This file is part of go-ethereum.
+//
+// go-ethereum is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// go-ethereum is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
+
+package main
+
+import (
+	"encoding/json"
+	"fmt"
+	"time"
+
+	"github.com/ethereum/go-ethereum/log"
+)
+
+// deployExplorer creates a new block explorer based on some user input.
+func (w *wizard) deployExplorer() {
+	// Do some sanity check before the user wastes time on input
+	if w.conf.genesis == nil {
+		log.Error("No genesis block configured")
+		return
+	}
+	if w.conf.ethstats == "" {
+		log.Error("No ethstats server configured")
+		return
+	}
+	if w.conf.genesis.Config.Ethash == nil {
+		log.Error("Only ethash network supported")
+		return
+	}
+	// Select the server to interact with
+	server := w.selectServer()
+	if server == "" {
+		return
+	}
+	client := w.servers[server]
+
+	// Retrieve any active node configurations from the server
+	infos, err := checkExplorer(client, w.network)
+	if err != nil {
+		infos = &explorerInfos{nodePort: 30303, webPort: 80, webHost: client.server}
+	}
+	chainspec, err := newParityChainSpec(w.network, w.conf.genesis, w.conf.bootFull)
+	if err != nil {
+		log.Error("Failed to create chain spec for explorer", "err", err)
+		return
+	}
+	chain, _ := json.MarshalIndent(chainspec, "", "  ")
+
+	// Figure out which port to listen on
+	fmt.Println()
+	fmt.Printf("Which port should the explorer listen on? (default = %d)\n", infos.webPort)
+	infos.webPort = w.readDefaultInt(infos.webPort)
+
+	// Figure which virtual-host to deploy ethstats on
+	if infos.webHost, err = w.ensureVirtualHost(client, infos.webPort, infos.webHost); err != nil {
+		log.Error("Failed to decide on explorer host", "err", err)
+		return
+	}
+	// Figure out where the user wants to store the persistent data
+	fmt.Println()
+	if infos.datadir == "" {
+		fmt.Printf("Where should data be stored on the remote machine?\n")
+		infos.datadir = w.readString()
+	} else {
+		fmt.Printf("Where should data be stored on the remote machine? (default = %s)\n", infos.datadir)
+		infos.datadir = w.readDefaultString(infos.datadir)
+	}
+	// Figure out which port to listen on
+	fmt.Println()
+	fmt.Printf("Which TCP/UDP port should the archive node listen on? (default = %d)\n", infos.nodePort)
+	infos.nodePort = w.readDefaultInt(infos.nodePort)
+
+	// Set a proper name to report on the stats page
+	fmt.Println()
+	if infos.ethstats == "" {
+		fmt.Printf("What should the explorer be called on the stats page?\n")
+		infos.ethstats = w.readString() + ":" + w.conf.ethstats
+	} else {
+		fmt.Printf("What should the explorer be called on the stats page? (default = %s)\n", infos.ethstats)
+		infos.ethstats = w.readDefaultString(infos.ethstats) + ":" + w.conf.ethstats
+	}
+	// Try to deploy the explorer on the host
+	fmt.Println()
+	fmt.Printf("Should the explorer be built from scratch (y/n)? (default = no)\n")
+	nocache := w.readDefaultString("n") != "n"
+
+	if out, err := deployExplorer(client, w.network, chain, infos, nocache); err != nil {
+		log.Error("Failed to deploy explorer container", "err", err)
+		if len(out) > 0 {
+			fmt.Printf("%s\n", out)
+		}
+		return
+	}
+	// All ok, run a network scan to pick any changes up
+	log.Info("Waiting for node to finish booting")
+	time.Sleep(3 * time.Second)
+
+	w.networkStats()
+}
diff --git a/cmd/puppeth/wizard_faucet.go b/cmd/puppeth/wizard_faucet.go
index e9d5c6016..dbb0965eb 100644
--- a/cmd/puppeth/wizard_faucet.go
+++ b/cmd/puppeth/wizard_faucet.go
@@ -60,7 +60,7 @@ func (w *wizard) deployFaucet() {
 		log.Error("Failed to decide on faucet host", "err", err)
 		return
 	}
-	// Port and proxy settings retrieved, figure out the funcing amount per perdion configurations
+	// Port and proxy settings retrieved, figure out the funding amount per period configurations
 	fmt.Println()
 	fmt.Printf("How many Ethers to release per request? (default = %d)\n", infos.amount)
 	infos.amount = w.readDefaultInt(infos.amount)
diff --git a/cmd/puppeth/wizard_netstats.go b/cmd/puppeth/wizard_netstats.go
index 906dfeda7..42df501b9 100644
--- a/cmd/puppeth/wizard_netstats.go
+++ b/cmd/puppeth/wizard_netstats.go
@@ -137,6 +137,14 @@ func (w *wizard) gatherStats(server string, pubkey []byte, client *sshClient) *s
 		stat.services["sealnode"] = infos.Report()
 		genesis = string(infos.genesis)
 	}
+	logger.Debug("Checking for explorer availability")
+	if infos, err := checkExplorer(client, w.network); err != nil {
+		if err != ErrServiceUnknown {
+			stat.services["explorer"] = map[string]string{"offline": err.Error()}
+		}
+	} else {
+		stat.services["explorer"] = infos.Report()
+	}
 	logger.Debug("Checking for faucet availability")
 	if infos, err := checkFaucet(client, w.network); err != nil {
 		if err != ErrServiceUnknown {
diff --git a/cmd/puppeth/wizard_network.go b/cmd/puppeth/wizard_network.go
index bf8248e4b..46b52bfcb 100644
--- a/cmd/puppeth/wizard_network.go
+++ b/cmd/puppeth/wizard_network.go
@@ -174,9 +174,10 @@ func (w *wizard) deployComponent() {
 	fmt.Println(" 1. Ethstats  - Network monitoring tool")
 	fmt.Println(" 2. Bootnode  - Entry point of the network")
 	fmt.Println(" 3. Sealer    - Full node minting new blocks")
-	fmt.Println(" 4. Wallet    - Browser wallet for quick sends (todo)")
-	fmt.Println(" 5. Faucet    - Crypto faucet to give away funds")
-	fmt.Println(" 6. Dashboard - Website listing above web-services")
+	fmt.Println(" 4. Explorer  - Chain analysis webservice (ethash only)")
+	fmt.Println(" 5. Wallet    - Browser wallet for quick sends (todo)")
+	fmt.Println(" 6. Faucet    - Crypto faucet to give away funds")
+	fmt.Println(" 7. Dashboard - Website listing above web-services")
 
 	switch w.read() {
 	case "1":
@@ -186,9 +187,11 @@ func (w *wizard) deployComponent() {
 	case "3":
 		w.deployNode(false)
 	case "4":
+		w.deployExplorer()
 	case "5":
-		w.deployFaucet()
 	case "6":
+		w.deployFaucet()
+	case "7":
 		w.deployDashboard()
 	default:
 		log.Error("That's not something I can do")
-- 
cgit v1.2.3