Merge pull request #15390 from karalabe/puppeth-devcon3

cmd/puppeth: new version as presented at devcon3
This commit is contained in:
Péter Szilágyi 2017-11-24 10:56:33 +02:00 committed by GitHub
commit f9569f3cd8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 1886 additions and 472 deletions

@ -83,6 +83,7 @@ var (
captchaToken = flag.String("captcha.token", "", "Recaptcha site key to authenticate client side") captchaToken = flag.String("captcha.token", "", "Recaptcha site key to authenticate client side")
captchaSecret = flag.String("captcha.secret", "", "Recaptcha secret key to authenticate server side") captchaSecret = flag.String("captcha.secret", "", "Recaptcha secret key to authenticate server side")
noauthFlag = flag.Bool("noauth", false, "Enables funding requests without authentication")
logFlag = flag.Int("loglevel", 3, "Log level to use for Ethereum and the faucet") logFlag = flag.Int("loglevel", 3, "Log level to use for Ethereum and the faucet")
) )
@ -132,6 +133,7 @@ func main() {
"Amounts": amounts, "Amounts": amounts,
"Periods": periods, "Periods": periods,
"Recaptcha": *captchaToken, "Recaptcha": *captchaToken,
"NoAuth": *noauthFlag,
}) })
if err != nil { if err != nil {
log.Crit("Failed to render the faucet template", "err", err) log.Crit("Failed to render the faucet template", "err", err)
@ -374,7 +376,7 @@ func (f *faucet) apiHandler(conn *websocket.Conn) {
if err = websocket.JSON.Receive(conn, &msg); err != nil { if err = websocket.JSON.Receive(conn, &msg); err != nil {
return return
} }
if !strings.HasPrefix(msg.URL, "https://gist.github.com/") && !strings.HasPrefix(msg.URL, "https://twitter.com/") && if !*noauthFlag && !strings.HasPrefix(msg.URL, "https://gist.github.com/") && !strings.HasPrefix(msg.URL, "https://twitter.com/") &&
!strings.HasPrefix(msg.URL, "https://plus.google.com/") && !strings.HasPrefix(msg.URL, "https://www.facebook.com/") { !strings.HasPrefix(msg.URL, "https://plus.google.com/") && !strings.HasPrefix(msg.URL, "https://www.facebook.com/") {
if err = sendError(conn, errors.New("URL doesn't link to supported services")); err != nil { if err = sendError(conn, errors.New("URL doesn't link to supported services")); err != nil {
log.Warn("Failed to send URL error to client", "err", err) log.Warn("Failed to send URL error to client", "err", err)
@ -435,13 +437,19 @@ func (f *faucet) apiHandler(conn *websocket.Conn) {
) )
switch { switch {
case strings.HasPrefix(msg.URL, "https://gist.github.com/"): case strings.HasPrefix(msg.URL, "https://gist.github.com/"):
username, avatar, address, err = authGitHub(msg.URL) if err = sendError(conn, errors.New("GitHub authentication discontinued at the official request of GitHub")); err != nil {
log.Warn("Failed to send GitHub deprecation to client", "err", err)
return
}
continue
case strings.HasPrefix(msg.URL, "https://twitter.com/"): case strings.HasPrefix(msg.URL, "https://twitter.com/"):
username, avatar, address, err = authTwitter(msg.URL) username, avatar, address, err = authTwitter(msg.URL)
case strings.HasPrefix(msg.URL, "https://plus.google.com/"): case strings.HasPrefix(msg.URL, "https://plus.google.com/"):
username, avatar, address, err = authGooglePlus(msg.URL) username, avatar, address, err = authGooglePlus(msg.URL)
case strings.HasPrefix(msg.URL, "https://www.facebook.com/"): case strings.HasPrefix(msg.URL, "https://www.facebook.com/"):
username, avatar, address, err = authFacebook(msg.URL) username, avatar, address, err = authFacebook(msg.URL)
case *noauthFlag:
username, avatar, address, err = authNoAuth(msg.URL)
default: default:
err = errors.New("Something funky happened, please open an issue at https://github.com/ethereum/go-ethereum/issues") err = errors.New("Something funky happened, please open an issue at https://github.com/ethereum/go-ethereum/issues")
} }
@ -776,3 +784,14 @@ func authFacebook(url string) (string, string, common.Address, error) {
} }
return username + "@facebook", avatar, address, nil return username + "@facebook", avatar, address, nil
} }
// authNoAuth tries to interpret a faucet request as a plain Ethereum address,
// without actually performing any remote authentication. This mode is prone to
// Byzantine attack, so only ever use for truly private networks.
func authNoAuth(url string) (string, string, common.Address, error) {
address := common.HexToAddress(regexp.MustCompile("0x[0-9a-fA-F]{40}").FindString(url))
if address == (common.Address{}) {
return "", "", common.Address{}, errors.New("No Ethereum address found to fund")
}
return address.Hex() + "@noauth", "", address, nil
}

@ -80,11 +80,8 @@
<div class="row" style="margin-top: 32px;"> <div class="row" style="margin-top: 32px;">
<div class="col-lg-12"> <div class="col-lg-12">
<h3>How does this work?</h3> <h3>How does this work?</h3>
<p>This Ether faucet is running on the {{.Network}} network. To prevent malicious actors from exhausting all available funds or accumulating enough Ether to mount long running spam attacks, requests are tied to certain common 3rd party accounts. Anyone having a GitHub, Twitter, Google+ or Facebook account may request funds within the permitted limits.</p> <p>This Ether faucet is running on the {{.Network}} network. To prevent malicious actors from exhausting all available funds or accumulating enough Ether to mount long running spam attacks, requests are tied to common 3rd party social network accounts. Anyone having a Twitter, Google+ or Facebook account may request funds within the permitted limits.</p>
<dl class="dl-horizontal"> <dl class="dl-horizontal">
<dt style="width: auto; margin-left: 40px;"><i class="fa fa-github-alt" aria-hidden="true" style="font-size: 36px;"></i></dt>
<dd style="margin-left: 88px; margin-bottom: 10px;"></i> To request funds via GitHub, create a <a href="https://gist.github.com/" target="_about:blank">gist</a> with your Ethereum address embedded into the content (the file name doesn't matter).<br/>Copy-paste the gists URL into the above input box and fire away!</dd>
<dt style="width: auto; margin-left: 40px;"><i class="fa fa-twitter" aria-hidden="true" style="font-size: 36px;"></i></dt> <dt style="width: auto; margin-left: 40px;"><i class="fa fa-twitter" aria-hidden="true" style="font-size: 36px;"></i></dt>
<dd style="margin-left: 88px; margin-bottom: 10px;"></i> To request funds via Twitter, make a <a href="https://twitter.com/intent/tweet?text=Requesting%20faucet%20funds%20into%200x0000000000000000000000000000000000000000%20on%20the%20%23{{.Network}}%20%23Ethereum%20test%20network." target="_about:blank">tweet</a> with your Ethereum address pasted into the contents (surrounding text doesn't matter).<br/>Copy-paste the <a href="https://support.twitter.com/articles/80586" target="_about:blank">tweets URL</a> into the above input box and fire away!</dd> <dd style="margin-left: 88px; margin-bottom: 10px;"></i> To request funds via Twitter, make a <a href="https://twitter.com/intent/tweet?text=Requesting%20faucet%20funds%20into%200x0000000000000000000000000000000000000000%20on%20the%20%23{{.Network}}%20%23Ethereum%20test%20network." target="_about:blank">tweet</a> with your Ethereum address pasted into the contents (surrounding text doesn't matter).<br/>Copy-paste the <a href="https://support.twitter.com/articles/80586" target="_about:blank">tweets URL</a> into the above input box and fire away!</dd>
@ -93,6 +90,11 @@
<dt style="width: auto; margin-left: 40px;"><i class="fa fa-facebook" aria-hidden="true" style="font-size: 36px;"></i></dt> <dt style="width: auto; margin-left: 40px;"><i class="fa fa-facebook" aria-hidden="true" style="font-size: 36px;"></i></dt>
<dd style="margin-left: 88px; margin-bottom: 10px;"></i> To request funds via Facebook, publish a new <strong>public</strong> post with your Ethereum address embedded into the content (surrounding text doesn't matter).<br/>Copy-paste the <a href="https://www.facebook.com/help/community/question/?id=282662498552845" target="_about:blank">posts URL</a> into the above input box and fire away!</dd> <dd style="margin-left: 88px; margin-bottom: 10px;"></i> To request funds via Facebook, publish a new <strong>public</strong> post with your Ethereum address embedded into the content (surrounding text doesn't matter).<br/>Copy-paste the <a href="https://www.facebook.com/help/community/question/?id=282662498552845" target="_about:blank">posts URL</a> into the above input box and fire away!</dd>
{{if .NoAuth}}
<dt class="text-danger" style="width: auto; margin-left: 40px;"><i class="fa fa-unlock-alt" aria-hidden="true" style="font-size: 36px;"></i></dt>
<dd class="text-danger" style="margin-left: 88px; margin-bottom: 10px;"></i> To request funds <strong>without authentication</strong>, simply copy-paste your Ethereum address into the above input box (surrounding text doesn't matter) and fire away.<br/>This mode is susceptible to Byzantine attacks. Only use for debugging or private networks!</dd>
{{end}}
</dl> </dl>
<p>You can track the current pending requests below the input field to see how much you have to wait until your turn comes.</p> <p>You can track the current pending requests below the input field to see how much you have to wait until your turn comes.</p>
{{if .Recaptcha}}<em>The faucet is running invisible reCaptcha protection against bots.</em>{{end}} {{if .Recaptcha}}<em>The faucet is running invisible reCaptcha protection against bots.</em>{{end}}
@ -126,12 +128,7 @@
}; };
// Define a method to reconnect upon server loss // Define a method to reconnect upon server loss
var reconnect = function() { var reconnect = function() {
if (attempt % 2 == 0) { server = new WebSocket(((window.location.protocol === "https:") ? "wss://" : "ws://") + window.location.host + "/api");
server = new WebSocket("wss://" + location.host + "/api");
} else {
server = new WebSocket("ws://" + location.host + "/api");
}
attempt++;
server.onmessage = function(event) { server.onmessage = function(event) {
var msg = JSON.parse(event.data); var msg = JSON.parse(event.data);

File diff suppressed because one or more lines are too long

379
cmd/puppeth/genesis.go Normal file

@ -0,0 +1,379 @@
// 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"
)
// cppEthereumGenesisSpec represents the genesis specification format used by the
// C++ Ethereum implementation.
type cppEthereumGenesisSpec struct {
SealEngine string `json:"sealEngine"`
Params struct {
AccountStartNonce hexutil.Uint64 `json:"accountStartNonce"`
HomesteadForkBlock hexutil.Uint64 `json:"homesteadForkBlock"`
EIP150ForkBlock hexutil.Uint64 `json:"EIP150ForkBlock"`
EIP158ForkBlock hexutil.Uint64 `json:"EIP158ForkBlock"`
ByzantiumForkBlock hexutil.Uint64 `json:"byzantiumForkBlock"`
ConstantinopleForkBlock hexutil.Uint64 `json:"constantinopleForkBlock"`
NetworkID hexutil.Uint64 `json:"networkID"`
ChainID hexutil.Uint64 `json:"chainID"`
MaximumExtraDataSize hexutil.Uint64 `json:"maximumExtraDataSize"`
MinGasLimit hexutil.Uint64 `json:"minGasLimit"`
MaxGasLimit hexutil.Uint64 `json:"maxGasLimit"`
GasLimitBoundDivisor *hexutil.Big `json:"gasLimitBoundDivisor"`
MinimumDifficulty *hexutil.Big `json:"minimumDifficulty"`
DifficultyBoundDivisor *hexutil.Big `json:"difficultyBoundDivisor"`
DurationLimit *hexutil.Big `json:"durationLimit"`
BlockReward *hexutil.Big `json:"blockReward"`
} `json:"params"`
Genesis struct {
Nonce hexutil.Bytes `json:"nonce"`
Difficulty *hexutil.Big `json:"difficulty"`
MixHash common.Hash `json:"mixHash"`
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"`
Accounts map[common.Address]*cppEthereumGenesisSpecAccount `json:"accounts"`
}
// cppEthereumGenesisSpecAccount is the prefunded genesis account and/or precompiled
// contract definition.
type cppEthereumGenesisSpecAccount struct {
Balance *hexutil.Big `json:"balance"`
Nonce uint64 `json:"nonce,omitempty"`
Precompiled *cppEthereumGenesisSpecBuiltin `json:"precompiled,omitempty"`
}
// cppEthereumGenesisSpecBuiltin is the precompiled contract definition.
type cppEthereumGenesisSpecBuiltin struct {
Name string `json:"name,omitempty"`
StartingBlock hexutil.Uint64 `json:"startingBlock,omitempty"`
Linear *cppEthereumGenesisSpecLinearPricing `json:"linear,omitempty"`
}
type cppEthereumGenesisSpecLinearPricing struct {
Base uint64 `json:"base"`
Word uint64 `json:"word"`
}
// newCppEthereumGenesisSpec converts a go-ethereum genesis block into a Parity specific
// chain specification format.
func newCppEthereumGenesisSpec(network string, genesis *core.Genesis) (*cppEthereumGenesisSpec, error) {
// Only ethash is currently supported between go-ethereum and cpp-ethereum
if genesis.Config.Ethash == nil {
return nil, errors.New("unsupported consensus engine")
}
// Reconstruct the chain spec in Parity's format
spec := &cppEthereumGenesisSpec{
SealEngine: "Ethash",
}
spec.Params.AccountStartNonce = 0
spec.Params.HomesteadForkBlock = (hexutil.Uint64)(genesis.Config.HomesteadBlock.Uint64())
spec.Params.EIP150ForkBlock = (hexutil.Uint64)(genesis.Config.EIP150Block.Uint64())
spec.Params.EIP158ForkBlock = (hexutil.Uint64)(genesis.Config.EIP158Block.Uint64())
spec.Params.ByzantiumForkBlock = (hexutil.Uint64)(genesis.Config.ByzantiumBlock.Uint64())
spec.Params.ConstantinopleForkBlock = (hexutil.Uint64)(math.MaxUint64)
spec.Params.NetworkID = (hexutil.Uint64)(genesis.Config.ChainId.Uint64())
spec.Params.ChainID = (hexutil.Uint64)(genesis.Config.ChainId.Uint64())
spec.Params.MaximumExtraDataSize = (hexutil.Uint64)(params.MaximumExtraDataSize)
spec.Params.MinGasLimit = (hexutil.Uint64)(params.MinGasLimit.Uint64())
spec.Params.MaxGasLimit = (hexutil.Uint64)(math.MaxUint64)
spec.Params.MinimumDifficulty = (*hexutil.Big)(params.MinimumDifficulty)
spec.Params.DifficultyBoundDivisor = (*hexutil.Big)(params.DifficultyBoundDivisor)
spec.Params.GasLimitBoundDivisor = (*hexutil.Big)(params.GasLimitBoundDivisor)
spec.Params.DurationLimit = (*hexutil.Big)(params.DurationLimit)
spec.Params.BlockReward = (*hexutil.Big)(ethash.FrontierBlockReward)
spec.Genesis.Nonce = (hexutil.Bytes)(make([]byte, 8))
binary.LittleEndian.PutUint64(spec.Genesis.Nonce[:], genesis.Nonce)
spec.Genesis.MixHash = 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]*cppEthereumGenesisSpecAccount)
for address, account := range genesis.Alloc {
spec.Accounts[address] = &cppEthereumGenesisSpecAccount{
Balance: (*hexutil.Big)(account.Balance),
Nonce: account.Nonce,
}
}
spec.Accounts[common.BytesToAddress([]byte{1})].Precompiled = &cppEthereumGenesisSpecBuiltin{
Name: "ecrecover", Linear: &cppEthereumGenesisSpecLinearPricing{Base: 3000},
}
spec.Accounts[common.BytesToAddress([]byte{2})].Precompiled = &cppEthereumGenesisSpecBuiltin{
Name: "sha256", Linear: &cppEthereumGenesisSpecLinearPricing{Base: 60, Word: 12},
}
spec.Accounts[common.BytesToAddress([]byte{3})].Precompiled = &cppEthereumGenesisSpecBuiltin{
Name: "ripemd160", Linear: &cppEthereumGenesisSpecLinearPricing{Base: 600, Word: 120},
}
spec.Accounts[common.BytesToAddress([]byte{4})].Precompiled = &cppEthereumGenesisSpecBuiltin{
Name: "identity", Linear: &cppEthereumGenesisSpecLinearPricing{Base: 15, Word: 3},
}
if genesis.Config.ByzantiumBlock != nil {
spec.Accounts[common.BytesToAddress([]byte{5})].Precompiled = &cppEthereumGenesisSpecBuiltin{
Name: "modexp", StartingBlock: (hexutil.Uint64)(genesis.Config.ByzantiumBlock.Uint64()),
}
spec.Accounts[common.BytesToAddress([]byte{6})].Precompiled = &cppEthereumGenesisSpecBuiltin{
Name: "alt_bn128_G1_add", StartingBlock: (hexutil.Uint64)(genesis.Config.ByzantiumBlock.Uint64()), Linear: &cppEthereumGenesisSpecLinearPricing{Base: 500},
}
spec.Accounts[common.BytesToAddress([]byte{7})].Precompiled = &cppEthereumGenesisSpecBuiltin{
Name: "alt_bn128_G1_mul", StartingBlock: (hexutil.Uint64)(genesis.Config.ByzantiumBlock.Uint64()), Linear: &cppEthereumGenesisSpecLinearPricing{Base: 40000},
}
spec.Accounts[common.BytesToAddress([]byte{8})].Precompiled = &cppEthereumGenesisSpecBuiltin{
Name: "alt_bn128_pairing_product", StartingBlock: (hexutil.Uint64)(genesis.Config.ByzantiumBlock.Uint64()),
}
}
return spec, nil
}
// 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
}
// pyEthereumGenesisSpec represents the genesis specification format used by the
// Python Ethereum implementation.
type pyEthereumGenesisSpec struct {
Nonce hexutil.Bytes `json:"nonce"`
Timestamp hexutil.Uint64 `json:"timestamp"`
ExtraData hexutil.Bytes `json:"extraData"`
GasLimit hexutil.Uint64 `json:"gasLimit"`
Difficulty *hexutil.Big `json:"difficulty"`
Mixhash common.Hash `json:"mixhash"`
Coinbase common.Address `json:"coinbase"`
Alloc core.GenesisAlloc `json:"alloc"`
ParentHash common.Hash `json:"parentHash"`
}
// newPyEthereumGenesisSpec converts a go-ethereum genesis block into a Parity specific
// chain specification format.
func newPyEthereumGenesisSpec(network string, genesis *core.Genesis) (*pyEthereumGenesisSpec, error) {
// Only ethash is currently supported between go-ethereum and pyethereum
if genesis.Config.Ethash == nil {
return nil, errors.New("unsupported consensus engine")
}
spec := &pyEthereumGenesisSpec{
Timestamp: (hexutil.Uint64)(genesis.Timestamp),
ExtraData: genesis.ExtraData,
GasLimit: (hexutil.Uint64)(genesis.GasLimit),
Difficulty: (*hexutil.Big)(genesis.Difficulty),
Mixhash: genesis.Mixhash,
Coinbase: genesis.Coinbase,
Alloc: genesis.Alloc,
ParentHash: genesis.ParentHash,
}
spec.Nonce = (hexutil.Bytes)(make([]byte, 8))
binary.LittleEndian.PutUint64(spec.Nonce[:], genesis.Nonce)
return spec, nil
}

@ -18,10 +18,12 @@ package main
import ( import (
"bytes" "bytes"
"encoding/json"
"fmt" "fmt"
"html/template" "html/template"
"math/rand" "math/rand"
"path/filepath" "path/filepath"
"strconv"
"strings" "strings"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
@ -76,25 +78,26 @@ var dashboardContent = `
<div id="sidebar-menu" class="main_menu_side hidden-print main_menu"> <div id="sidebar-menu" class="main_menu_side hidden-print main_menu">
<div class="menu_section"> <div class="menu_section">
<ul class="nav side-menu"> <ul class="nav side-menu">
{{if .EthstatsPage}}<li><a onclick="load('//{{.EthstatsPage}}')"><i class="fa fa-tachometer"></i> Network Stats</a></li>{{end}} {{if .EthstatsPage}}<li id="stats_menu"><a onclick="load('#stats')"><i class="fa fa-tachometer"></i> Network Stats</a></li>{{end}}
{{if .ExplorerPage}}<li><a onclick="load('//{{.ExplorerPage}}')"><i class="fa fa-database"></i> Block Explorer</a></li>{{end}} {{if .ExplorerPage}}<li id="explorer_menu"><a onclick="load('#explorer')"><i class="fa fa-database"></i> Block Explorer</a></li>{{end}}
{{if .WalletPage}}<li><a onclick="load('//{{.WalletPage}}')"><i class="fa fa-address-book-o"></i> Browser Wallet</a></li>{{end}} {{if .WalletPage}}<li id="wallet_menu"><a onclick="load('#wallet')"><i class="fa fa-address-book-o"></i> Browser Wallet</a></li>{{end}}
{{if .FaucetPage}}<li><a onclick="load('//{{.FaucetPage}}')"><i class="fa fa-bath"></i> Crypto Faucet</a></li>{{end}} {{if .FaucetPage}}<li id="faucet_menu"><a onclick="load('#faucet')"><i class="fa fa-bath"></i> Crypto Faucet</a></li>{{end}}
<li id="connect"><a><i class="fa fa-plug"></i> Connect Yourself</a> <li id="connect_menu"><a><i class="fa fa-plug"></i> Connect Yourself</a>
<ul id="connect_list" class="nav child_menu"> <ul id="connect_list" class="nav child_menu">
<li><a onclick="$('#connect').removeClass('active'); $('#connect_list').toggle(); load('#connect-go-ethereum-geth')">Go Ethereum: Geth</a></li> <li><a onclick="$('#connect_menu').removeClass('active'); $('#connect_list').toggle(); load('#geth')">Go Ethereum: Geth</a></li>
<li><a onclick="$('#connect').removeClass('active'); $('#connect_list').toggle(); load('#connect-go-ethereum-mist')">Go Ethereum: Wallet & Mist</a></li> <li><a onclick="$('#connect_menu').removeClass('active'); $('#connect_list').toggle(); load('#mist')">Go Ethereum: Wallet & Mist</a></li>
<li><a onclick="$('#connect').removeClass('active'); $('#connect_list').toggle(); load('#connect-go-ethereum-mobile')">Go Ethereum: Android & iOS</a></li> <li><a onclick="$('#connect_menu').removeClass('active'); $('#connect_list').toggle(); load('#mobile')">Go Ethereum: Android & iOS</a></li>{{if .Ethash}}
<li><a onclick="$('#connect_menu').removeClass('active'); $('#connect_list').toggle(); load('#other')">Other Ethereum Clients</a></li>{{end}}
</ul> </ul>
</li> </li>
<li><a onclick="load('#about')"><i class="fa fa-heartbeat"></i> About Puppeth</a></li> <li id="about_menu"><a onclick="load('#about')"><i class="fa fa-heartbeat"></i> About Puppeth</a></li>
</ul> </ul>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="right_col" role="main" style="padding: 0"> <div class="right_col" role="main" style="padding: 0 !important">
<div id="connect-go-ethereum-geth" hidden style="padding: 16px;"> <div id="geth" hidden style="padding: 16px;">
<div class="page-title"> <div class="page-title">
<div class="title_left"> <div class="title_left">
<h3>Connect Yourself &ndash; Go Ethereum: Geth</h3> <h3>Connect Yourself &ndash; Go Ethereum: Geth</h3>
@ -154,7 +157,7 @@ var dashboardContent = `
<p>Initial processing required to synchronize is light, as it only verifies the validity of the headers; similarly required disk capacity is small, tallying around 500 bytes per header. Low end machines with arbitrary storage, weak CPUs and 512MB+ RAM should cope well.</p> <p>Initial processing required to synchronize is light, as it only verifies the validity of the headers; similarly required disk capacity is small, tallying around 500 bytes per header. Low end machines with arbitrary storage, weak CPUs and 512MB+ RAM should cope well.</p>
<br/> <br/>
<p>To run a light node, download <a href="/{{.GethGenesis}}"><code>{{.GethGenesis}}</code></a> and start Geth with: <p>To run a light node, download <a href="/{{.GethGenesis}}"><code>{{.GethGenesis}}</code></a> and start Geth with:
<pre>geth --datadir=$HOME/.{{.Network}} --light init {{.GethGenesis}}</pre> <pre>geth --datadir=$HOME/.{{.Network}} init {{.GethGenesis}}</pre>
<pre>geth --networkid={{.NetworkID}} --datadir=$HOME/.{{.Network}} --syncmode=light{{if .Ethstats}} --ethstats='{{.Ethstats}}'{{end}} --bootnodes={{.BootnodesLightFlat}}</pre> <pre>geth --networkid={{.NetworkID}} --datadir=$HOME/.{{.Network}} --syncmode=light{{if .Ethstats}} --ethstats='{{.Ethstats}}'{{end}} --bootnodes={{.BootnodesLightFlat}}</pre>
</p> </p>
<br/> <br/>
@ -173,8 +176,8 @@ var dashboardContent = `
<p>Initial processing required to synchronize is light, as it only verifies the validity of the headers; similarly required disk capacity is small, tallying around 500 bytes per header. Embedded machines with arbitrary storage, low power CPUs and 128MB+ RAM may work.</p> <p>Initial processing required to synchronize is light, as it only verifies the validity of the headers; similarly required disk capacity is small, tallying around 500 bytes per header. Embedded machines with arbitrary storage, low power CPUs and 128MB+ RAM may work.</p>
<br/> <br/>
<p>To run an embedded node, download <a href="/{{.GethGenesis}}"><code>{{.GethGenesis}}</code></a> and start Geth with: <p>To run an embedded node, download <a href="/{{.GethGenesis}}"><code>{{.GethGenesis}}</code></a> and start Geth with:
<pre>geth --datadir=$HOME/.{{.Network}} --light init {{.GethGenesis}}</pre> <pre>geth --datadir=$HOME/.{{.Network}} init {{.GethGenesis}}</pre>
<pre>geth --networkid={{.NetworkID}} --datadir=$HOME/.{{.Network}} --cache=32 --syncmode=light{{if .Ethstats}} --ethstats='{{.Ethstats}}'{{end}} --bootnodes={{.BootnodesLightFlat}}</pre> <pre>geth --networkid={{.NetworkID}} --datadir=$HOME/.{{.Network}} --cache=16 --ethash.cachesinmem=1 --syncmode=light{{if .Ethstats}} --ethstats='{{.Ethstats}}'{{end}} --bootnodes={{.BootnodesLightFlat}}</pre>
</p> </p>
<br/> <br/>
<p>You can download Geth from <a href="https://geth.ethereum.org/downloads/" target="about:blank">https://geth.ethereum.org/downloads/</a>.</p> <p>You can download Geth from <a href="https://geth.ethereum.org/downloads/" target="about:blank">https://geth.ethereum.org/downloads/</a>.</p>
@ -183,7 +186,7 @@ var dashboardContent = `
</div> </div>
</div> </div>
</div> </div>
<div id="connect-go-ethereum-mist" hidden style="padding: 16px;"> <div id="mist" hidden style="padding: 16px;">
<div class="page-title"> <div class="page-title">
<div class="title_left"> <div class="title_left">
<h3>Connect Yourself &ndash; Go Ethereum: Wallet &amp; Mist</h3> <h3>Connect Yourself &ndash; Go Ethereum: Wallet &amp; Mist</h3>
@ -235,7 +238,7 @@ var dashboardContent = `
</div> </div>
</div> </div>
</div> </div>
<div id="connect-go-ethereum-mobile" hidden style="padding: 16px;"> <div id="mobile" hidden style="padding: 16px;">
<div class="page-title"> <div class="page-title">
<div class="title_left"> <div class="title_left">
<h3>Connect Yourself &ndash; Go Ethereum: Android &amp; iOS</h3> <h3>Connect Yourself &ndash; Go Ethereum: Android &amp; iOS</h3>
@ -309,7 +312,101 @@ try! node?.start();
</div> </div>
</div> </div>
</div> </div>
</div>{{if .Ethash}}
<div id="other" hidden style="padding: 16px;">
<div class="page-title">
<div class="title_left">
<h3>Connect Yourself &ndash; Other Ethereum Clients</h3>
</div> </div>
</div>
<div class="clearfix"></div>
<div class="row">
<div class="col-md-6">
<div class="x_panel">
<div class="x_title">
<h2>
<svg height="14px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 115 115"><path fill="#5C8DBC" d="M9.7 83.3V35.5s0-3.4 3.3-5.2c3.3-1.8 39.6-23.5 39.6-23.5s4.6-3.1 9.4 0c0 0 43.1 23.9 42.4 25.3L85.3 43.3s-3.6-8.4-13.1-13c-11.3-5.5-29.7-6.2-42.9 13.3 0 0-8.6 13.5.3 31.6l-19 10.7s-.9-.6-.9-2.6z"/><path fill="#5C8DBC" d="M71 51.3c-2.8-4.7-7.9-7.9-13.8-7.9-8.8 0-16 7.2-16 16 0 2.8.7 5.4 2 7.7L71 51.3z"/><path fill="#194674" d="M43.1 67c2.8 4.7 7.9 7.9 13.8 7.9 8.8 0 16-7.2 16-16 0-2.8-.7-5.4-2-7.7L43.1 67z"/><path fill="#1B598E" d="M104.4 32.1s1.3 52.6-.3 53.6L58 58.6l46.4-26.5z"/><path fill="#FFF" d="M90 57h-3.9v-4.1h-4.2V57h-4v4.1h4V65h4.2v-3.9H90zm13.6 0h-3.9v-4.1h-4.2V57h-4v4.1h4V65h4.2v-3.9h3.9z"/><path fill="#194674" d="M29.5 75.1s9.2 17 28.5 16.1 27.3-16.6 27.3-16.6L104 85.4s4.1.8-41.6 25.7c0 0-4.9 3.3-10.2 0 0 0-41.3-23.1-41.6-25.3l18.9-10.7z"/></svg>
C++ Ethereum <small>Official C++ client from the Ethereum Foundation</small>
</h2>
<div class="clearfix"></div>
</div>
<div class="x_content">
<p>C++ Ethereum is the third most popular of the Ethereum clients, focusing on code portability to a broad range of operating systems and hardware. The client is currently a full node with transaction processing based synchronization.</p>
<br/>
<p>To run a cpp-ethereum node, download <a href="/{{.CppGenesis}}"><code>{{.CppGenesis}}</code></a> and start the node with:
<pre>eth --config {{.CppGenesis}} --datadir $HOME/.{{.Network}} --peerset "{{.CppBootnodes}}"</pre>
</p>
<br/>
<p>You can find cpp-ethereum at <a href="https://github.com/ethereum/cpp-ethereum/" target="about:blank">https://github.com/ethereum/cpp-ethereum/</a>.</p>
</div>
</div>
</div>
<div class="col-md-6">
<div class="x_panel">
<div class="x_title">
<h2>
<svg height="14px" version="1.1" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><path d="M46.42,13.07S24.51,18.54,35,30.6c3.09,3.55-.81,6.75-0.81,6.75s7.84-4,4.24-9.11C35,23.51,32.46,21.17,46.42,13.07ZM32.1,16.88C45.05,6.65,38.4,0,38.4,0c2.68,10.57-9.46,13.76-13.84,20.34-3,4.48,1.46,9.3,7.53,14.77C29.73,29.77,21.71,25.09,32.1,16.88Z" transform="translate(-8.4)" fill="#e57125"/><path d="M23.6,49.49c-9.84,2.75,6,8.43,18.51,3.06a23.06,23.06,0,0,1-3.52-1.72,36.62,36.62,0,0,1-13.25.56C21.16,50.92,23.6,49.49,23.6,49.49Zm17-5.36a51.7,51.7,0,0,1-17.1.82c-4.19-.43-1.45-2.46-1.45-2.46-10.84,3.6,6,7.68,21.18,3.25A7.59,7.59,0,0,1,40.62,44.13ZM51.55,54.68s1.81,1.49-2,2.64c-7.23,2.19-30.1,2.85-36.45.09-2.28-1,2-2.37,3.35-2.66a8.69,8.69,0,0,1,2.21-.25c-2.54-1.79-16.41,3.51-7,5C37.15,63.67,58.17,57.67,51.55,54.68ZM42.77,39.12a20.42,20.42,0,0,1,2.93-1.57s-4.83.86-9.65,1.27A87.37,87.37,0,0,1,20.66,39c-7.51-1,4.12-3.77,4.12-3.77A22,22,0,0,0,14.7,37.61C8.14,40.79,31,42.23,42.77,39.12Zm2.88,7.77a1,1,0,0,1-.24.31C61.44,43,55.54,32.35,47.88,35a2.19,2.19,0,0,0-1,.79,9,9,0,0,1,1.37-.37C52.1,34.66,57.65,40.65,45.64,46.89Zm0.43,14.75a94.76,94.76,0,0,1-29.17.45s1.47,1.22,9,1.7c11.53,0.74,29.22-.41,29.64-5.86C55.6,57.94,54.79,60,46.08,61.65Z" transform="translate(-8.4)" fill="#5482a2"/></svg>
Ethereum Harmony<small>Third party Java client from EtherCamp</small>
</h2>
<div class="clearfix"></div>
</div>
<div class="x_content">
<p>Ethereum Harmony is a web user-interface based graphical Ethereum client built on top of the EthereumJ Java implementation of the Ethereum protocol. The client currently is a full node with state download based synchronization.</p>
<br/>
<p>To run an Ethereum Harmony node, download <a href="/{{.HarmonyGenesis}}"><code>{{.HarmonyGenesis}}</code></a> and start the node with:
<pre>./gradlew runCustom -DgenesisFile={{.HarmonyGenesis}} -Dpeer.networkId={{.NetworkID}} -Ddatabase.dir=$HOME/.harmony/{{.Network}} {{.HarmonyBootnodes}} </pre>
</p>
<br/>
<p>You can find Ethereum Harmony at <a href="https://github.com/ether-camp/ethereum-harmony/" target="about:blank">https://github.com/ether-camp/ethereum-harmony/</a>.</p>
</div>
</div>
</div>
</div>
<div class="clearfix"></div>
<div class="row">
<div class="col-md-6">
<div class="x_panel">
<div class="x_title">
<h2>
<svg height="14px" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 104.56749 104.56675" version="1.1" viewbox="0 0 144 144" y="0px" x="0px"><metadata id="metadata10"><rdf:RDF><cc:Work rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs id="defs8" /><path style="fill:#676767;" id="path2" d="m 49.0125,12.3195 a 3.108,3.108 0 0 1 6.216,0 3.108,3.108 0 0 1 -6.216,0 m -37.077,28.14 a 3.108,3.108 0 0 1 6.216,0 3.108,3.108 0 0 1 -6.216,0 m 74.153,0.145 a 3.108,3.108 0 0 1 6.216,0 3.108,3.108 0 0 1 -6.216,0 m -65.156,4.258 c 1.43,-0.635 2.076,-2.311 1.441,-3.744 l -1.379,-3.118 h 5.423 v 24.444 h -10.941 a 38.265,38.265 0 0 1 -1.239,-14.607 z m 22.685,0.601 v -7.205 h 12.914 c 0.667,0 4.71,0.771 4.71,3.794 0,2.51 -3.101,3.41 -5.651,3.41 z m -17.631,38.793 a 3.108,3.108 0 0 1 6.216,0 3.108,3.108 0 0 1 -6.216,0 m 46.051,0.145 a 3.108,3.108 0 0 1 6.216,0 3.108,3.108 0 0 1 -6.216,0 m 0.961,-7.048 c -1.531,-0.328 -3.037,0.646 -3.365,2.18 l -1.56,7.28 a 38.265,38.265 0 0 1 -31.911,-0.153 l -1.559,-7.28 c -0.328,-1.532 -1.834,-2.508 -3.364,-2.179 l -6.427,1.38 a 38.265,38.265 0 0 1 -3.323,-3.917 h 31.272 c 0.354,0 0.59,-0.064 0.59,-0.386 v -11.062 c 0,-0.322 -0.236,-0.386 -0.59,-0.386 h -9.146 v -7.012 h 9.892 c 0.903,0 4.828,0.258 6.083,5.275 0.393,1.543 1.256,6.562 1.846,8.169 0.588,1.802 2.982,5.402 5.533,5.402 h 16.146 a 38.265,38.265 0 0 1 -3.544,4.102 z m 17.365,-29.207 a 38.265,38.265 0 0 1 0.081,6.643 h -3.926 c -0.393,0 -0.551,0.258 -0.551,0.643 v 1.803 c 0,4.244 -2.393,5.167 -4.49,5.402 -1.997,0.225 -4.211,-0.836 -4.484,-2.058 -1.178,-6.626 -3.141,-8.041 -6.241,-10.486 3.847,-2.443 7.85,-6.047 7.85,-10.871 0,-5.209 -3.571,-8.49 -6.005,-10.099 -3.415,-2.251 -7.196,-2.702 -8.216,-2.702 h -40.603 a 38.265,38.265 0 0 1 21.408,-12.082 l 4.786,5.021 c 1.082,1.133 2.874,1.175 4.006,0.092 l 5.355,-5.122 a 38.265,38.265 0 0 1 26.196,18.657 l -3.666,8.28 c -0.633,1.433 0.013,3.109 1.442,3.744 z m 9.143,0.134 -0.125,-1.28 3.776,-3.522 c 0.768,-0.716 0.481,-2.157 -0.501,-2.523 l -4.827,-1.805 -0.378,-1.246 3.011,-4.182 c 0.614,-0.85 0.05,-2.207 -0.984,-2.377 l -5.09,-0.828 -0.612,-1.143 2.139,-4.695 c 0.438,-0.956 -0.376,-2.179 -1.428,-2.139 l -5.166,0.18 -0.816,-0.99 1.187,-5.032 c 0.24,-1.022 -0.797,-2.06 -1.819,-1.82 l -5.031,1.186 -0.992,-0.816 0.181,-5.166 c 0.04,-1.046 -1.184,-1.863 -2.138,-1.429 l -4.694,2.14 -1.143,-0.613 -0.83,-5.091 c -0.168,-1.032 -1.526,-1.596 -2.376,-0.984 l -4.185,3.011 -1.244,-0.377 -1.805,-4.828 c -0.366,-0.984 -1.808,-1.267 -2.522,-0.503 l -3.522,3.779 -1.28,-0.125 -2.72,-4.395 c -0.55,-0.89 -2.023,-0.89 -2.571,0 l -2.72,4.395 -1.281,0.125 -3.523,-3.779 c -0.714,-0.764 -2.156,-0.481 -2.522,0.503 l -1.805,4.828 -1.245,0.377 -4.184,-3.011 c -0.85,-0.614 -2.209,-0.048 -2.377,0.984 l -0.83,5.091 -1.143,0.613 -4.694,-2.14 c -0.954,-0.436 -2.178,0.383 -2.138,1.429 l 0.18,5.166 -0.992,0.816 -5.031,-1.186 c -1.022,-0.238 -2.06,0.798 -1.82,1.82 l 1.185,5.032 -0.814,0.99 -5.166,-0.18 c -1.042,-0.03 -1.863,1.183 -1.429,2.139 l 2.14,4.695 -0.613,1.143 -5.09,0.828 c -1.034,0.168 -1.594,1.527 -0.984,2.377 l 3.011,4.182 -0.378,1.246 -4.828,1.805 c -0.98,0.366 -1.267,1.807 -0.501,2.523 l 3.777,3.522 -0.125,1.28 -4.394,2.72 c -0.89,0.55 -0.89,2.023 0,2.571 l 4.394,2.72 0.125,1.28 -3.777,3.523 c -0.766,0.714 -0.479,2.154 0.501,2.522 l 4.828,1.805 0.378,1.246 -3.011,4.183 c -0.612,0.852 -0.049,2.21 0.985,2.376 l 5.089,0.828 0.613,1.145 -2.14,4.693 c -0.436,0.954 0.387,2.181 1.429,2.139 l 5.164,-0.181 0.816,0.992 -1.185,5.033 c -0.24,1.02 0.798,2.056 1.82,1.816 l 5.031,-1.185 0.992,0.814 -0.18,5.167 c -0.04,1.046 1.184,1.864 2.138,1.428 l 4.694,-2.139 1.143,0.613 0.83,5.088 c 0.168,1.036 1.527,1.596 2.377,0.986 l 4.182,-3.013 1.246,0.379 1.805,4.826 c 0.366,0.98 1.808,1.269 2.522,0.501 l 3.523,-3.777 1.281,0.128 2.72,4.394 c 0.548,0.886 2.021,0.888 2.571,0 l 2.72,-4.394 1.28,-0.128 3.522,3.777 c 0.714,0.768 2.156,0.479 2.522,-0.501 l 1.805,-4.826 1.246,-0.379 4.183,3.013 c 0.85,0.61 2.208,0.048 2.376,-0.986 l 0.83,-5.088 1.143,-0.613 4.694,2.139 c 0.954,0.436 2.176,-0.38 2.138,-1.428 l -0.18,-5.167 0.991,-0.814 5.031,1.185 c 1.022,0.24 2.059,-0.796 1.819,-1.816 l -1.185,-5.033 0.814,-0.992 5.166,0.181 c 1.042,0.042 1.866,-1.185 1.428,-2.139 l -2.139,-4.693 0.612,-1.145 5.09,-0.828 c 1.036,-0.166 1.598,-1.524 0.984,-2.376 l -3.011,-4.183 0.378,-1.246 4.827,-1.805 c 0.982,-0.368 1.269,-1.808 0.501,-2.522 l -3.776,-3.523 0.125,-1.28 4.394,-2.72 c 0.89,-0.548 0.891,-2.021 10e-4,-2.571 z" /></svg>
Parity<small>Third party Rust client from Parity Technologies</small>
</h2>
<div class="clearfix"></div>
</div>
<div class="x_content">
<p>Parity is a fast, light and secure Ethereum client, supporting both headless mode of operation as well as a web user interface for direct manual interaction. The client is currently a full node with transaction processing based synchronization and state pruning enabled.</p>
<br/>
<p>To run a Parity node, download <a href="/{{.ParityGenesis}}"><code>{{.ParityGenesis}}</code></a> and start the node with:
<pre>parity --chain={{.ParityGenesis}}</pre>
</p>
<br/>
<p>You can find Parity at <a href="https://parity.io/" target="about:blank">https://parity.io/</a>.</p>
</div>
</div>
</div>
<div class="col-md-6">
<div class="x_panel">
<div class="x_title">
<h2>
<svg height="14px" version="1.1" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><defs><linearGradient id="a" x1="13.79" y1="38.21" x2="75.87" y2="-15.2" gradientTransform="matrix(0.56, 0, 0, -0.57, -8.96, 23.53)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#5c9fd3"/><stop offset="1" stop-color="#316a99"/></linearGradient><linearGradient id="b" x1="99.87" y1="-47.53" x2="77.7" y2="-16.16" gradientTransform="matrix(0.56, 0, 0, -0.57, -8.96, 23.53)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ffd43d"/><stop offset="1" stop-color="#fee875"/></linearGradient></defs><g><path d="M31.62,0a43.6,43.6,0,0,0-7.3.62c-6.46,1.14-7.63,3.53-7.63,7.94v5.82H32v1.94H11a9.53,9.53,0,0,0-9.54,7.74,28.54,28.54,0,0,0,0,15.52c1.09,4.52,3.68,7.74,8.11,7.74h5.25v-7a9.7,9.7,0,0,1,9.54-9.48H39.58a7.69,7.69,0,0,0,7.63-7.76V8.56c0-4.14-3.49-7.25-7.63-7.94A47.62,47.62,0,0,0,31.62,0ZM23.37,4.68A2.91,2.91,0,1,1,20.5,7.6,2.9,2.9,0,0,1,23.37,4.68Z" transform="translate(-0.35)" fill="url(#a)"/><path d="M49.12,16.32V23.1a9.79,9.79,0,0,1-9.54,9.68H24.33a7.79,7.79,0,0,0-7.63,7.76V55.08c0,4.14,3.6,6.57,7.63,7.76a25.55,25.55,0,0,0,15.25,0c3.84-1.11,7.63-3.35,7.63-7.76V49.26H32V47.32H54.85c4.44,0,6.09-3.1,7.63-7.74s1.53-9.38,0-15.52c-1.1-4.42-3.19-7.74-7.63-7.74H49.12ZM40.54,53.14A2.91,2.91,0,1,1,37.67,56,2.88,2.88,0,0,1,40.54,53.14Z" transform="translate(-0.35)" fill="url(#b)"/></g></svg>
PyEthApp<small>Official Python client from the Ethereum Foundation</small>
</h2>
<div class="clearfix"></div>
</div>
<div class="x_content">
<p>Pyethapp is the Ethereum Foundation's research client, aiming to provide an easily hackable and extendable codebase. The client is currently a full node with transaction processing based synchronization and state pruning enabled.</p>
<br/>
<p>To run a pyethapp node, download <a href="/{{.PythonGenesis}}"><code>{{.PythonGenesis}}</code></a> and start the node with:
<pre>mkdir -p $HOME/.config/pyethapp/{{.Network}}</pre>
<pre>pyethapp -c eth.genesis="$(cat {{.PythonGenesis}})" -c eth.network_id={{.NetworkID}} -c data_dir=$HOME/.config/pyethapp/{{.Network}} -c discovery.bootstrap_nodes="[{{.PythonBootnodes}}]" -c eth.block.HOMESTEAD_FORK_BLKNUM={{.Homestead}} -c eth.block.ANTI_DOS_FORK_BLKNUM={{.Tangerine}} -c eth.block.SPURIOUS_DRAGON_FORK_BLKNUM={{.Spurious}} -c eth.block.METROPOLIS_FORK_BLKNUM={{.Byzantium}} -c eth.block.DAO_FORK_BLKNUM=18446744073709551615 run --console</pre>
</p>
<br/>
<p>You can find pyethapp at <a href="https://github.com/ethereum/pyethapp/" target="about:blank">https://github.com/ethereum/pyethapp/</a>.</p>
</div>
</div>
</div>
</div>
</div>{{end}}
<div id="about" hidden> <div id="about" hidden>
<div class="row vertical-center"> <div class="row vertical-center">
<div style="margin: 0 auto;"> <div style="margin: 0 auto;">
@ -344,13 +441,33 @@ try! node?.start();
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gentelella/1.3.0/js/custom.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/gentelella/1.3.0/js/custom.min.js"></script>
<script> <script>
var load = function(url) { var load = function(hash) {
$("#connect-go-ethereum-geth").fadeOut(300) window.location.hash = hash;
$("#connect-go-ethereum-mist").fadeOut(300)
$("#connect-go-ethereum-mobile").fadeOut(300) // Fade out all possible pages (yes, ugly, no, don't care)
$("#geth").fadeOut(300)
$("#mist").fadeOut(300)
$("#mobile").fadeOut(300)
$("#other").fadeOut(300)
$("#about").fadeOut(300) $("#about").fadeOut(300)
$("#frame-wrapper").fadeOut(300); $("#frame-wrapper").fadeOut(300);
// Depending on the hash, resolve it into a local or remote URL
var url = hash;
switch (hash) {
case "#stats":
url = "//{{.EthstatsPage}}";
break;
case "#explorer":
url = "//{{.ExplorerPage}}";
break;
case "#wallet":
url = "//{{.WalletPage}}";
break;
case "#faucet":
url = "//{{.FaucetPage}}";
break;
}
setTimeout(function() { setTimeout(function() {
if (url.substring(0, 1) == "#") { if (url.substring(0, 1) == "#") {
$('.body').css({overflowY: 'auto'}); $('.body').css({overflowY: 'auto'});
@ -364,13 +481,10 @@ try! node?.start();
} }
var resize = function() { var resize = function() {
var sidebar = $($(".navbar")[0]).width(); var sidebar = $($(".navbar")[0]).width();
var content = 1920;
var limit = document.body.clientWidth - sidebar; var limit = document.body.clientWidth - sidebar;
var scale = limit / content; var scale = limit / 1920;
console.log(document.body.clientHeight); $("#frame-wrapper").width(limit);
$("#frame-wrapper").width(content / scale);
$("#frame-wrapper").height(document.body.clientHeight / scale); $("#frame-wrapper").height(document.body.clientHeight / scale);
$("#frame-wrapper").css({ $("#frame-wrapper").css({
transform: 'scale(' + (scale) + ')', transform: 'scale(' + (scale) + ')',
@ -379,9 +493,17 @@ try! node?.start();
}; };
$(window).resize(resize); $(window).resize(resize);
if (window.location.hash == "") {
var item = $(".side-menu").children()[0]; var item = $(".side-menu").children()[0];
$(item).children()[0].click(); $(item).children()[0].click();
$(item).addClass("active"); $(item).addClass("active");
} else {
load(window.location.hash);
var menu = $(window.location.hash + "_menu");
if (menu !== undefined) {
$(menu).addClass("active");
}
}
</script> </script>
</body> </body>
</html> </html>
@ -405,6 +527,10 @@ RUN \
echo '});' >> server.js echo '});' >> server.js
ADD {{.Network}}.json /dashboard/{{.Network}}.json ADD {{.Network}}.json /dashboard/{{.Network}}.json
ADD {{.Network}}-cpp.json /dashboard/{{.Network}}-cpp.json
ADD {{.Network}}-harmony.json /dashboard/{{.Network}}-harmony.json
ADD {{.Network}}-parity.json /dashboard/{{.Network}}-parity.json
ADD {{.Network}}-python.json /dashboard/{{.Network}}-python.json
ADD index.html /dashboard/index.html ADD index.html /dashboard/index.html
ADD puppeth.png /dashboard/puppeth.png ADD puppeth.png /dashboard/puppeth.png
@ -422,8 +548,12 @@ services:
build: . build: .
image: {{.Network}}/dashboard{{if not .VHost}} image: {{.Network}}/dashboard{{if not .VHost}}
ports: ports:
- "{{.Port}}:80"{{else}} - "{{.Port}}:80"{{end}}
environment: environment:
- ETHSTATS_PAGE={{.EthstatsPage}}
- EXPLORER_PAGE={{.ExplorerPage}}
- WALLET_PAGE={{.WalletPage}}
- FAUCET_PAGE={{.FaucetPage}}{{if .VHost}}
- VIRTUAL_HOST={{.VHost}}{{end}} - VIRTUAL_HOST={{.VHost}}{{end}}
logging: logging:
driver: "json-file" driver: "json-file"
@ -436,7 +566,7 @@ services:
// deployDashboard deploys a new dashboard container to a remote machine via SSH, // deployDashboard deploys a new dashboard container to a remote machine via SSH,
// docker and docker-compose. If an instance with the specified network name // docker and docker-compose. If an instance with the specified network name
// already exists there, it will be overwritten! // already exists there, it will be overwritten!
func deployDashboard(client *sshClient, network string, port int, vhost string, services map[string]string, conf *config, ethstats bool) ([]byte, error) { func deployDashboard(client *sshClient, network string, conf *config, config *dashboardInfos, nocache bool) ([]byte, error) {
// Generate the content to upload to the server // Generate the content to upload to the server
workdir := fmt.Sprintf("%d", rand.Int63()) workdir := fmt.Sprintf("%d", rand.Int63())
files := make(map[string][]byte) files := make(map[string][]byte)
@ -450,36 +580,94 @@ func deployDashboard(client *sshClient, network string, port int, vhost string,
composefile := new(bytes.Buffer) composefile := new(bytes.Buffer)
template.Must(template.New("").Parse(dashboardComposefile)).Execute(composefile, map[string]interface{}{ template.Must(template.New("").Parse(dashboardComposefile)).Execute(composefile, map[string]interface{}{
"Network": network, "Network": network,
"Port": port, "Port": config.port,
"VHost": vhost, "VHost": config.host,
"EthstatsPage": config.ethstats,
"ExplorerPage": config.explorer,
"WalletPage": config.wallet,
"FaucetPage": config.faucet,
}) })
files[filepath.Join(workdir, "docker-compose.yaml")] = composefile.Bytes() files[filepath.Join(workdir, "docker-compose.yaml")] = composefile.Bytes()
statsLogin := fmt.Sprintf("yournode:%s", conf.ethstats) statsLogin := fmt.Sprintf("yournode:%s", conf.ethstats)
if !ethstats { if !config.trusted {
statsLogin = "" statsLogin = ""
} }
indexfile := new(bytes.Buffer) indexfile := new(bytes.Buffer)
bootCpp := make([]string, len(conf.bootFull))
for i, boot := range conf.bootFull {
bootCpp[i] = "required:" + strings.TrimPrefix(boot, "enode://")
}
bootHarmony := make([]string, len(conf.bootFull))
for i, boot := range conf.bootFull {
bootHarmony[i] = fmt.Sprintf("-Dpeer.active.%d.url=%s", i, boot)
}
bootPython := make([]string, len(conf.bootFull))
for i, boot := range conf.bootFull {
bootPython[i] = "'" + boot + "'"
}
template.Must(template.New("").Parse(dashboardContent)).Execute(indexfile, map[string]interface{}{ template.Must(template.New("").Parse(dashboardContent)).Execute(indexfile, map[string]interface{}{
"Network": network, "Network": network,
"NetworkID": conf.genesis.Config.ChainId, "NetworkID": conf.Genesis.Config.ChainId,
"NetworkTitle": strings.Title(network), "NetworkTitle": strings.Title(network),
"EthstatsPage": services["ethstats"], "EthstatsPage": config.ethstats,
"ExplorerPage": services["explorer"], "ExplorerPage": config.explorer,
"WalletPage": services["wallet"], "WalletPage": config.wallet,
"FaucetPage": services["faucet"], "FaucetPage": config.faucet,
"GethGenesis": network + ".json", "GethGenesis": network + ".json",
"BootnodesFull": conf.bootFull, "BootnodesFull": conf.bootFull,
"BootnodesLight": conf.bootLight, "BootnodesLight": conf.bootLight,
"BootnodesFullFlat": strings.Join(conf.bootFull, ","), "BootnodesFullFlat": strings.Join(conf.bootFull, ","),
"BootnodesLightFlat": strings.Join(conf.bootLight, ","), "BootnodesLightFlat": strings.Join(conf.bootLight, ","),
"Ethstats": statsLogin, "Ethstats": statsLogin,
"Ethash": conf.Genesis.Config.Ethash != nil,
"CppGenesis": network + "-cpp.json",
"CppBootnodes": strings.Join(bootCpp, " "),
"HarmonyGenesis": network + "-harmony.json",
"HarmonyBootnodes": strings.Join(bootHarmony, " "),
"ParityGenesis": network + "-parity.json",
"PythonGenesis": network + "-python.json",
"PythonBootnodes": strings.Join(bootPython, ","),
"Homestead": conf.Genesis.Config.HomesteadBlock,
"Tangerine": conf.Genesis.Config.EIP150Block,
"Spurious": conf.Genesis.Config.EIP155Block,
"Byzantium": conf.Genesis.Config.ByzantiumBlock,
}) })
files[filepath.Join(workdir, "index.html")] = indexfile.Bytes() files[filepath.Join(workdir, "index.html")] = indexfile.Bytes()
genesis, _ := conf.genesis.MarshalJSON() // Marshal the genesis spec files for go-ethereum and all the other clients
genesis, _ := conf.Genesis.MarshalJSON()
files[filepath.Join(workdir, network+".json")] = genesis files[filepath.Join(workdir, network+".json")] = genesis
if conf.Genesis.Config.Ethash != nil {
cppSpec, err := newCppEthereumGenesisSpec(network, conf.Genesis)
if err != nil {
return nil, err
}
cppSpecJSON, _ := json.Marshal(cppSpec)
files[filepath.Join(workdir, network+"-cpp.json")] = cppSpecJSON
harmonySpecJSON, _ := conf.Genesis.MarshalJSON()
files[filepath.Join(workdir, network+"-harmony.json")] = harmonySpecJSON
paritySpec, err := newParityChainSpec(network, conf.Genesis, conf.bootFull)
if err != nil {
return nil, err
}
paritySpecJSON, _ := json.Marshal(paritySpec)
files[filepath.Join(workdir, network+"-parity.json")] = paritySpecJSON
pyethSpec, err := newPyEthereumGenesisSpec(network, conf.Genesis)
if err != nil {
return nil, err
}
pyethSpecJSON, _ := json.Marshal(pyethSpec)
files[filepath.Join(workdir, network+"-python.json")] = pyethSpecJSON
} else {
for _, client := range []string{"cpp", "harmony", "parity", "python"} {
files[filepath.Join(workdir, network+"-"+client+".json")] = []byte{}
}
}
files[filepath.Join(workdir, "puppeth.png")] = dashboardMascot files[filepath.Join(workdir, "puppeth.png")] = dashboardMascot
// Upload the deployment files to the remote server (and clean up afterwards) // Upload the deployment files to the remote server (and clean up afterwards)
@ -489,7 +677,10 @@ func deployDashboard(client *sshClient, network string, port int, vhost string,
defer client.Run("rm -rf " + workdir) defer client.Run("rm -rf " + workdir)
// Build and deploy the dashboard service // Build and deploy the dashboard service
return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s up -d --build", workdir, network)) 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))
} }
// dashboardInfos is returned from an dashboard status check to allow reporting // dashboardInfos is returned from an dashboard status check to allow reporting
@ -497,11 +688,25 @@ func deployDashboard(client *sshClient, network string, port int, vhost string,
type dashboardInfos struct { type dashboardInfos struct {
host string host string
port int port int
trusted bool
ethstats string
explorer string
wallet string
faucet string
} }
// String implements the stringer interface. // Report converts the typed struct into a plain string->string map, containing
func (info *dashboardInfos) String() string { // most - but not all - fields for reporting to the user.
return fmt.Sprintf("host=%s, port=%d", info.host, info.port) func (info *dashboardInfos) Report() map[string]string {
return map[string]string{
"Website address": info.host,
"Website listener port": strconv.Itoa(info.port),
"Ethstats service": info.ethstats,
"Explorer service": info.explorer,
"Wallet service": info.wallet,
"Faucet service": info.faucet,
}
} }
// checkDashboard does a health-check against a dashboard container to verify if // checkDashboard does a health-check against a dashboard container to verify if
@ -538,5 +743,9 @@ func checkDashboard(client *sshClient, network string) (*dashboardInfos, error)
return &dashboardInfos{ return &dashboardInfos{
host: host, host: host,
port: port, port: port,
ethstats: infos.envvars["ETHSTATS_PAGE"],
explorer: infos.envvars["EXPLORER_PAGE"],
wallet: infos.envvars["WALLET_PAGE"],
faucet: infos.envvars["FAUCET_PAGE"],
}, nil }, nil
} }

@ -21,6 +21,7 @@ import (
"fmt" "fmt"
"math/rand" "math/rand"
"path/filepath" "path/filepath"
"strconv"
"strings" "strings"
"text/template" "text/template"
@ -30,21 +31,9 @@ import (
// ethstatsDockerfile is the Dockerfile required to build an ethstats backend // ethstatsDockerfile is the Dockerfile required to build an ethstats backend
// and associated monitoring site. // and associated monitoring site.
var ethstatsDockerfile = ` var ethstatsDockerfile = `
FROM mhart/alpine-node:latest FROM puppeth/ethstats:latest
RUN \
apk add --update git && \
git clone --depth=1 https://github.com/karalabe/eth-netstats && \
apk del git && rm -rf /var/cache/apk/* && \
\
cd /eth-netstats && npm install && npm install -g grunt-cli && grunt
WORKDIR /eth-netstats
EXPOSE 3000
RUN echo 'module.exports = {trusted: [{{.Trusted}}], banned: [{{.Banned}}], reserved: ["yournode"]};' > lib/utils/config.js RUN echo 'module.exports = {trusted: [{{.Trusted}}], banned: [{{.Banned}}], reserved: ["yournode"]};' > lib/utils/config.js
CMD ["npm", "start"]
` `
// ethstatsComposefile is the docker-compose.yml file required to deploy and // ethstatsComposefile is the docker-compose.yml file required to deploy and
@ -72,7 +61,7 @@ services:
// deployEthstats deploys a new ethstats container to a remote machine via SSH, // deployEthstats deploys a new ethstats container to a remote machine via SSH,
// docker and docker-compose. If an instance with the specified network name // docker and docker-compose. If an instance with the specified network name
// already exists there, it will be overwritten! // already exists there, it will be overwritten!
func deployEthstats(client *sshClient, network string, port int, secret string, vhost string, trusted []string, banned []string) ([]byte, error) { func deployEthstats(client *sshClient, network string, port int, secret string, vhost string, trusted []string, banned []string, nocache bool) ([]byte, error) {
// Generate the content to upload to the server // Generate the content to upload to the server
workdir := fmt.Sprintf("%d", rand.Int63()) workdir := fmt.Sprintf("%d", rand.Int63())
files := make(map[string][]byte) files := make(map[string][]byte)
@ -110,7 +99,10 @@ func deployEthstats(client *sshClient, network string, port int, secret string,
defer client.Run("rm -rf " + workdir) defer client.Run("rm -rf " + workdir)
// Build and deploy the ethstats service // Build and deploy the ethstats service
return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s up -d --build", workdir, network)) 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))
} }
// ethstatsInfos is returned from an ethstats status check to allow reporting // ethstatsInfos is returned from an ethstats status check to allow reporting
@ -123,9 +115,15 @@ type ethstatsInfos struct {
banned []string banned []string
} }
// String implements the stringer interface. // Report converts the typed struct into a plain string->string map, containing
func (info *ethstatsInfos) String() string { // most - but not all - fields for reporting to the user.
return fmt.Sprintf("host=%s, port=%d, secret=%s, banned=%v", info.host, info.port, info.secret, info.banned) func (info *ethstatsInfos) Report() map[string]string {
return map[string]string{
"Website address": info.host,
"Website listener port": strconv.Itoa(info.port),
"Login secret": info.secret,
"Banned addresses": fmt.Sprintf("%v", info.banned),
}
} }
// checkEthstats does a health-check against an ethstats server to verify whether // checkEthstats does a health-check against an ethstats server to verify whether

@ -0,0 +1,211 @@
// 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 puppeth/explorer:latest
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
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")] = 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, containing
// 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 block explorer 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
}

@ -18,6 +18,7 @@ package main
import ( import (
"bytes" "bytes"
"encoding/json"
"fmt" "fmt"
"html/template" "html/template"
"math/rand" "math/rand"
@ -25,36 +26,24 @@ import (
"strconv" "strconv"
"strings" "strings"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
) )
// faucetDockerfile is the Dockerfile required to build an faucet container to // faucetDockerfile is the Dockerfile required to build an faucet container to
// grant crypto tokens based on GitHub authentications. // grant crypto tokens based on GitHub authentications.
var faucetDockerfile = ` var faucetDockerfile = `
FROM alpine:latest FROM ethereum/client-go:alltools-latest
RUN mkdir /go
ENV GOPATH /go
RUN \
apk add --update git go make gcc musl-dev ca-certificates linux-headers && \
mkdir -p $GOPATH/src/github.com/ethereum && \
(cd $GOPATH/src/github.com/ethereum && git clone --depth=1 https://github.com/ethereum/go-ethereum) && \
go build -v github.com/ethereum/go-ethereum/cmd/faucet && \
apk del git go make gcc musl-dev linux-headers && \
rm -rf $GOPATH && rm -rf /var/cache/apk/*
ADD genesis.json /genesis.json ADD genesis.json /genesis.json
ADD account.json /account.json ADD account.json /account.json
ADD account.pass /account.pass ADD account.pass /account.pass
EXPOSE 8080 ENTRYPOINT [ \
"faucet", "--genesis", "/genesis.json", "--network", "{{.NetworkID}}", "--bootnodes", "{{.Bootnodes}}", "--ethstats", "{{.Ethstats}}", "--ethport", "{{.EthPort}}", \
CMD [ \
"/faucet", "--genesis", "/genesis.json", "--network", "{{.NetworkID}}", "--bootnodes", "{{.Bootnodes}}", "--ethstats", "{{.Ethstats}}", "--ethport", "{{.EthPort}}", \
"--faucet.name", "{{.FaucetName}}", "--faucet.amount", "{{.FaucetAmount}}", "--faucet.minutes", "{{.FaucetMinutes}}", "--faucet.tiers", "{{.FaucetTiers}}", \ "--faucet.name", "{{.FaucetName}}", "--faucet.amount", "{{.FaucetAmount}}", "--faucet.minutes", "{{.FaucetMinutes}}", "--faucet.tiers", "{{.FaucetTiers}}", \
"--github.user", "{{.GitHubUser}}", "--github.token", "{{.GitHubToken}}", "--account.json", "/account.json", "--account.pass", "/account.pass" \ "--account.json", "/account.json", "--account.pass", "/account.pass" \
{{if .CaptchaToken}}, "--captcha.token", "{{.CaptchaToken}}", "--captcha.secret", "{{.CaptchaSecret}}"{{end}} \ {{if .CaptchaToken}}, "--captcha.token", "{{.CaptchaToken}}", "--captcha.secret", "{{.CaptchaSecret}}"{{end}}{{if .NoAuth}}, "--noauth"{{end}} \
]` ]`
// faucetComposefile is the docker-compose.yml file required to deploy and maintain // faucetComposefile is the docker-compose.yml file required to deploy and maintain
@ -76,10 +65,9 @@ services:
- FAUCET_AMOUNT={{.FaucetAmount}} - FAUCET_AMOUNT={{.FaucetAmount}}
- FAUCET_MINUTES={{.FaucetMinutes}} - FAUCET_MINUTES={{.FaucetMinutes}}
- FAUCET_TIERS={{.FaucetTiers}} - FAUCET_TIERS={{.FaucetTiers}}
- GITHUB_USER={{.GitHubUser}}
- GITHUB_TOKEN={{.GitHubToken}}
- CAPTCHA_TOKEN={{.CaptchaToken}} - CAPTCHA_TOKEN={{.CaptchaToken}}
- CAPTCHA_SECRET={{.CaptchaSecret}}{{if .VHost}} - CAPTCHA_SECRET={{.CaptchaSecret}}
- NO_AUTH={{.NoAuth}}{{if .VHost}}
- VIRTUAL_HOST={{.VHost}} - VIRTUAL_HOST={{.VHost}}
- VIRTUAL_PORT=8080{{end}} - VIRTUAL_PORT=8080{{end}}
logging: logging:
@ -93,7 +81,7 @@ services:
// deployFaucet deploys a new faucet container to a remote machine via SSH, // deployFaucet deploys a new faucet container to a remote machine via SSH,
// docker and docker-compose. If an instance with the specified network name // docker and docker-compose. If an instance with the specified network name
// already exists there, it will be overwritten! // already exists there, it will be overwritten!
func deployFaucet(client *sshClient, network string, bootnodes []string, config *faucetInfos) ([]byte, error) { func deployFaucet(client *sshClient, network string, bootnodes []string, config *faucetInfos, nocache bool) ([]byte, error) {
// Generate the content to upload to the server // Generate the content to upload to the server
workdir := fmt.Sprintf("%d", rand.Int63()) workdir := fmt.Sprintf("%d", rand.Int63())
files := make(map[string][]byte) files := make(map[string][]byte)
@ -104,14 +92,13 @@ func deployFaucet(client *sshClient, network string, bootnodes []string, config
"Bootnodes": strings.Join(bootnodes, ","), "Bootnodes": strings.Join(bootnodes, ","),
"Ethstats": config.node.ethstats, "Ethstats": config.node.ethstats,
"EthPort": config.node.portFull, "EthPort": config.node.portFull,
"GitHubUser": config.githubUser,
"GitHubToken": config.githubToken,
"CaptchaToken": config.captchaToken, "CaptchaToken": config.captchaToken,
"CaptchaSecret": config.captchaSecret, "CaptchaSecret": config.captchaSecret,
"FaucetName": strings.Title(network), "FaucetName": strings.Title(network),
"FaucetAmount": config.amount, "FaucetAmount": config.amount,
"FaucetMinutes": config.minutes, "FaucetMinutes": config.minutes,
"FaucetTiers": config.tiers, "FaucetTiers": config.tiers,
"NoAuth": config.noauth,
}) })
files[filepath.Join(workdir, "Dockerfile")] = dockerfile.Bytes() files[filepath.Join(workdir, "Dockerfile")] = dockerfile.Bytes()
@ -123,13 +110,12 @@ func deployFaucet(client *sshClient, network string, bootnodes []string, config
"ApiPort": config.port, "ApiPort": config.port,
"EthPort": config.node.portFull, "EthPort": config.node.portFull,
"EthName": config.node.ethstats[:strings.Index(config.node.ethstats, ":")], "EthName": config.node.ethstats[:strings.Index(config.node.ethstats, ":")],
"GitHubUser": config.githubUser,
"GitHubToken": config.githubToken,
"CaptchaToken": config.captchaToken, "CaptchaToken": config.captchaToken,
"CaptchaSecret": config.captchaSecret, "CaptchaSecret": config.captchaSecret,
"FaucetAmount": config.amount, "FaucetAmount": config.amount,
"FaucetMinutes": config.minutes, "FaucetMinutes": config.minutes,
"FaucetTiers": config.tiers, "FaucetTiers": config.tiers,
"NoAuth": config.noauth,
}) })
files[filepath.Join(workdir, "docker-compose.yaml")] = composefile.Bytes() files[filepath.Join(workdir, "docker-compose.yaml")] = composefile.Bytes()
@ -144,7 +130,10 @@ func deployFaucet(client *sshClient, network string, bootnodes []string, config
defer client.Run("rm -rf " + workdir) defer client.Run("rm -rf " + workdir)
// Build and deploy the faucet service // Build and deploy the faucet service
return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s up -d --build", workdir, network)) 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))
} }
// faucetInfos is returned from an faucet status check to allow reporting various // faucetInfos is returned from an faucet status check to allow reporting various
@ -156,15 +145,38 @@ type faucetInfos struct {
amount int amount int
minutes int minutes int
tiers int tiers int
githubUser string noauth bool
githubToken string
captchaToken string captchaToken string
captchaSecret string captchaSecret string
} }
// String implements the stringer interface. // Report converts the typed struct into a plain string->string map, containing
func (info *faucetInfos) String() string { // most - but not all - fields for reporting to the user.
return fmt.Sprintf("host=%s, api=%d, eth=%d, amount=%d, minutes=%d, tiers=%d, github=%s, captcha=%v, ethstats=%s", info.host, info.port, info.node.portFull, info.amount, info.minutes, info.tiers, info.githubUser, info.captchaToken != "", info.node.ethstats) func (info *faucetInfos) Report() map[string]string {
report := map[string]string{
"Website address": info.host,
"Website listener port": strconv.Itoa(info.port),
"Ethereum listener port": strconv.Itoa(info.node.portFull),
"Funding amount (base tier)": fmt.Sprintf("%d Ethers", info.amount),
"Funding cooldown (base tier)": fmt.Sprintf("%d mins", info.minutes),
"Funding tiers": strconv.Itoa(info.tiers),
"Captha protection": fmt.Sprintf("%v", info.captchaToken != ""),
"Ethstats username": info.node.ethstats,
}
if info.noauth {
report["Debug mode (no auth)"] = "enabled"
}
if info.node.keyJSON != "" {
var key struct {
Address string `json:"address"`
}
if err := json.Unmarshal([]byte(info.node.keyJSON), &key); err == nil {
report["Funding account"] = common.HexToAddress(key.Address).Hex()
} else {
log.Error("Failed to retrieve signer address", "err", err)
}
}
return report
} }
// checkFaucet does a health-check against an faucet server to verify whether // checkFaucet does a health-check against an faucet server to verify whether
@ -224,9 +236,8 @@ func checkFaucet(client *sshClient, network string) (*faucetInfos, error) {
amount: amount, amount: amount,
minutes: minutes, minutes: minutes,
tiers: tiers, tiers: tiers,
githubUser: infos.envvars["GITHUB_USER"],
githubToken: infos.envvars["GITHUB_TOKEN"],
captchaToken: infos.envvars["CAPTCHA_TOKEN"], captchaToken: infos.envvars["CAPTCHA_TOKEN"],
captchaSecret: infos.envvars["CAPTCHA_SECRET"], captchaSecret: infos.envvars["CAPTCHA_SECRET"],
noauth: infos.envvars["NO_AUTH"] == "true",
}, nil }, nil
} }

@ -22,6 +22,7 @@ import (
"html/template" "html/template"
"math/rand" "math/rand"
"path/filepath" "path/filepath"
"strconv"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
) )
@ -54,7 +55,7 @@ services:
// deployNginx deploys a new nginx reverse-proxy container to expose one or more // deployNginx deploys a new nginx reverse-proxy container to expose one or more
// HTTP services running on a single host. If an instance with the specified // HTTP services running on a single host. If an instance with the specified
// network name already exists there, it will be overwritten! // network name already exists there, it will be overwritten!
func deployNginx(client *sshClient, network string, port int) ([]byte, error) { func deployNginx(client *sshClient, network string, port int, nocache bool) ([]byte, error) {
log.Info("Deploying nginx reverse-proxy", "server", client.server, "port", port) log.Info("Deploying nginx reverse-proxy", "server", client.server, "port", port)
// Generate the content to upload to the server // Generate the content to upload to the server
@ -78,8 +79,11 @@ func deployNginx(client *sshClient, network string, port int) ([]byte, error) {
} }
defer client.Run("rm -rf " + workdir) defer client.Run("rm -rf " + workdir)
// Build and deploy the ethstats service // Build and deploy the reverse-proxy service
return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s up -d --build", workdir, network)) 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))
} }
// nginxInfos is returned from an nginx reverse-proxy status check to allow // nginxInfos is returned from an nginx reverse-proxy status check to allow
@ -88,9 +92,12 @@ type nginxInfos struct {
port int port int
} }
// String implements the stringer interface. // Report converts the typed struct into a plain string->string map, containing
func (info *nginxInfos) String() string { // most - but not all - fields for reporting to the user.
return fmt.Sprintf("port=%d", info.port) func (info *nginxInfos) Report() map[string]string {
return map[string]string{
"Shared listener port": strconv.Itoa(info.port),
}
} }
// checkNginx does a health-check against an nginx reverse-proxy to verify whether // checkNginx does a health-check against an nginx reverse-proxy to verify whether

@ -18,6 +18,7 @@ package main
import ( import (
"bytes" "bytes"
"encoding/json"
"fmt" "fmt"
"math/rand" "math/rand"
"path/filepath" "path/filepath"
@ -25,6 +26,7 @@ import (
"strings" "strings"
"text/template" "text/template"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
) )
@ -38,9 +40,9 @@ ADD genesis.json /genesis.json
ADD signer.pass /signer.pass ADD signer.pass /signer.pass
{{end}} {{end}}
RUN \ 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 '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{{end}}{{if .Unlock}}--unlock 0 --password /signer.pass --mine{{end}} --targetgaslimit {{.GasTarget}} --gasprice {{.GasPrice}}' >> geth.sh 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
ENTRYPOINT ["/bin/sh", "geth.sh"] ENTRYPOINT ["/bin/sh", "geth.sh"]
` `
@ -58,7 +60,8 @@ services:
- "{{.FullPort}}:{{.FullPort}}/udp"{{if .Light}} - "{{.FullPort}}:{{.FullPort}}/udp"{{if .Light}}
- "{{.LightPort}}:{{.LightPort}}/udp"{{end}} - "{{.LightPort}}:{{.LightPort}}/udp"{{end}}
volumes: volumes:
- {{.Datadir}}:/root/.ethereum - {{.Datadir}}:/root/.ethereum{{if .Ethashdir}}
- {{.Ethashdir}}:/root/.ethash{{end}}
environment: environment:
- FULL_PORT={{.FullPort}}/tcp - FULL_PORT={{.FullPort}}/tcp
- LIGHT_PORT={{.LightPort}}/udp - LIGHT_PORT={{.LightPort}}/udp
@ -79,7 +82,7 @@ services:
// deployNode deploys a new Ethereum node container to a remote machine via SSH, // deployNode deploys a new Ethereum node container to a remote machine via SSH,
// docker and docker-compose. If an instance with the specified network name // docker and docker-compose. If an instance with the specified network name
// already exists there, it will be overwritten! // already exists there, it will be overwritten!
func deployNode(client *sshClient, network string, bootv4, bootv5 []string, config *nodeInfos) ([]byte, error) { func deployNode(client *sshClient, network string, bootv4, bootv5 []string, config *nodeInfos, nocache bool) ([]byte, error) {
kind := "sealnode" kind := "sealnode"
if config.keyJSON == "" && config.etherbase == "" { if config.keyJSON == "" && config.etherbase == "" {
kind = "bootnode" kind = "bootnode"
@ -114,6 +117,7 @@ func deployNode(client *sshClient, network string, bootv4, bootv5 []string, conf
template.Must(template.New("").Parse(nodeComposefile)).Execute(composefile, map[string]interface{}{ template.Must(template.New("").Parse(nodeComposefile)).Execute(composefile, map[string]interface{}{
"Type": kind, "Type": kind,
"Datadir": config.datadir, "Datadir": config.datadir,
"Ethashdir": config.ethashdir,
"Network": network, "Network": network,
"FullPort": config.portFull, "FullPort": config.portFull,
"TotalPeers": config.peersTotal, "TotalPeers": config.peersTotal,
@ -127,9 +131,7 @@ func deployNode(client *sshClient, network string, bootv4, bootv5 []string, conf
}) })
files[filepath.Join(workdir, "docker-compose.yaml")] = composefile.Bytes() files[filepath.Join(workdir, "docker-compose.yaml")] = composefile.Bytes()
//genesisfile, _ := json.MarshalIndent(config.genesis, "", " ")
files[filepath.Join(workdir, "genesis.json")] = config.genesis files[filepath.Join(workdir, "genesis.json")] = config.genesis
if config.keyJSON != "" { if config.keyJSON != "" {
files[filepath.Join(workdir, "signer.json")] = []byte(config.keyJSON) files[filepath.Join(workdir, "signer.json")] = []byte(config.keyJSON)
files[filepath.Join(workdir, "signer.pass")] = []byte(config.keyPass) files[filepath.Join(workdir, "signer.pass")] = []byte(config.keyPass)
@ -141,7 +143,10 @@ func deployNode(client *sshClient, network string, bootv4, bootv5 []string, conf
defer client.Run("rm -rf " + workdir) defer client.Run("rm -rf " + workdir)
// Build and deploy the boot or seal node service // Build and deploy the boot or seal node service
return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s up -d --build", workdir, network)) 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))
} }
// nodeInfos is returned from a boot or seal node status check to allow reporting // nodeInfos is returned from a boot or seal node status check to allow reporting
@ -150,6 +155,7 @@ type nodeInfos struct {
genesis []byte genesis []byte
network int64 network int64
datadir string datadir string
ethashdir string
ethstats string ethstats string
portFull int portFull int
portLight int portLight int
@ -164,14 +170,43 @@ type nodeInfos struct {
gasPrice float64 gasPrice float64
} }
// String implements the stringer interface. // Report converts the typed struct into a plain string->string map, containing
func (info *nodeInfos) String() string { // most - but not all - fields for reporting to the user.
discv5 := "" func (info *nodeInfos) Report() map[string]string {
if info.peersLight > 0 { report := map[string]string{
discv5 = fmt.Sprintf(", portv5=%d", info.portLight) "Data directory": info.datadir,
"Listener port (full nodes)": strconv.Itoa(info.portFull),
"Peer count (all total)": strconv.Itoa(info.peersTotal),
"Peer count (light nodes)": strconv.Itoa(info.peersLight),
"Ethstats username": info.ethstats,
} }
return fmt.Sprintf("port=%d%s, datadir=%s, peers=%d, lights=%d, ethstats=%s, gastarget=%0.3f MGas, gasprice=%0.3f GWei", if info.peersLight > 0 {
info.portFull, discv5, info.datadir, info.peersTotal, info.peersLight, info.ethstats, info.gasTarget, info.gasPrice) // Light server enabled
report["Listener port (light nodes)"] = strconv.Itoa(info.portLight)
}
if info.gasTarget > 0 {
// Miner or signer node
report["Gas limit (baseline target)"] = fmt.Sprintf("%0.3f MGas", info.gasTarget)
report["Gas price (minimum accepted)"] = fmt.Sprintf("%0.3f GWei", info.gasPrice)
if info.etherbase != "" {
// Ethash proof-of-work miner
report["Ethash directory"] = info.ethashdir
report["Miner account"] = info.etherbase
}
if info.keyJSON != "" {
// Clique proof-of-authority signer
var key struct {
Address string `json:"address"`
}
if err := json.Unmarshal([]byte(info.keyJSON), &key); err == nil {
report["Signer account"] = common.HexToAddress(key.Address).Hex()
} else {
log.Error("Failed to retrieve signer address", "err", err)
}
}
}
return report
} }
// checkNode does a health-check against an boot or seal node server to verify // checkNode does a health-check against an boot or seal node server to verify
@ -223,6 +258,7 @@ func checkNode(client *sshClient, network string, boot bool) (*nodeInfos, error)
stats := &nodeInfos{ stats := &nodeInfos{
genesis: genesis, genesis: genesis,
datadir: infos.volumes["/root/.ethereum"], datadir: infos.volumes["/root/.ethereum"],
ethashdir: infos.volumes["/root/.ethash"],
portFull: infos.portmap[infos.envvars["FULL_PORT"]], portFull: infos.portmap[infos.envvars["FULL_PORT"]],
portLight: infos.portmap[infos.envvars["LIGHT_PORT"]], portLight: infos.portmap[infos.envvars["LIGHT_PORT"]],
peersTotal: totalPeers, peersTotal: totalPeers,

@ -0,0 +1,200 @@
// 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"
)
// walletDockerfile is the Dockerfile required to run a web wallet.
var walletDockerfile = `
FROM puppeth/wallet:latest
ADD genesis.json /genesis.json
RUN \
echo 'node server.js &' > wallet.sh && \
echo 'geth --cache 512 init /genesis.json' >> wallet.sh && \
echo $'geth --networkid {{.NetworkID}} --port {{.NodePort}} --bootnodes {{.Bootnodes}} --ethstats \'{{.Ethstats}}\' --cache=512 --rpc --rpcaddr=0.0.0.0 --rpccorsdomain "*"' >> wallet.sh
RUN \
sed -i 's/PuppethNetworkID/{{.NetworkID}}/g' dist/js/etherwallet-master.js && \
sed -i 's/PuppethNetwork/{{.Network}}/g' dist/js/etherwallet-master.js && \
sed -i 's/PuppethDenom/{{.Denom}}/g' dist/js/etherwallet-master.js && \
sed -i 's/PuppethHost/{{.Host}}/g' dist/js/etherwallet-master.js && \
sed -i 's/PuppethRPCPort/{{.RPCPort}}/g' dist/js/etherwallet-master.js
ENTRYPOINT ["/bin/sh", "wallet.sh"]
`
// walletComposefile is the docker-compose.yml file required to deploy and
// maintain a web wallet.
var walletComposefile = `
version: '2'
services:
wallet:
build: .
image: {{.Network}}/wallet
ports:
- "{{.NodePort}}:{{.NodePort}}"
- "{{.NodePort}}:{{.NodePort}}/udp"
- "{{.RPCPort}}:8545"{{if not .VHost}}
- "{{.WebPort}}:80"{{end}}
volumes:
- {{.Datadir}}:/root/.ethereum
environment:
- NODE_PORT={{.NodePort}}/tcp
- STATS={{.Ethstats}}{{if .VHost}}
- VIRTUAL_HOST={{.VHost}}
- VIRTUAL_PORT=80{{end}}
logging:
driver: "json-file"
options:
max-size: "1m"
max-file: "10"
restart: always
`
// deployWallet deploys a new web wallet 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 deployWallet(client *sshClient, network string, bootnodes []string, config *walletInfos, 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(walletDockerfile)).Execute(dockerfile, map[string]interface{}{
"Network": strings.ToTitle(network),
"Denom": strings.ToUpper(network),
"NetworkID": config.network,
"NodePort": config.nodePort,
"RPCPort": config.rpcPort,
"Bootnodes": strings.Join(bootnodes, ","),
"Ethstats": config.ethstats,
"Host": client.address,
})
files[filepath.Join(workdir, "Dockerfile")] = dockerfile.Bytes()
composefile := new(bytes.Buffer)
template.Must(template.New("").Parse(walletComposefile)).Execute(composefile, map[string]interface{}{
"Datadir": config.datadir,
"Network": network,
"NodePort": config.nodePort,
"RPCPort": config.rpcPort,
"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, "genesis.json")] = config.genesis
// 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))
}
// walletInfos is returned from a web wallet status check to allow reporting
// various configuration parameters.
type walletInfos struct {
genesis []byte
network int64
datadir string
ethstats string
nodePort int
rpcPort int
webHost string
webPort int
}
// Report converts the typed struct into a plain string->string map, containing
// most - but not all - fields for reporting to the user.
func (info *walletInfos) Report() map[string]string {
report := map[string]string{
"Data directory": info.datadir,
"Ethstats username": info.ethstats,
"Node listener port ": strconv.Itoa(info.nodePort),
"RPC listener port ": strconv.Itoa(info.rpcPort),
"Website address ": info.webHost,
"Website listener port ": strconv.Itoa(info.webPort),
}
return report
}
// checkWallet does a health-check against web wallet server to verify whether
// it's running, and if yes, whether it's responsive.
func checkWallet(client *sshClient, network string) (*walletInfos, error) {
// Inspect a possible web wallet container on the host
infos, err := inspectContainer(client, fmt.Sprintf("%s_wallet_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["80/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 and RPC ports are reachable
nodePort := infos.portmap[infos.envvars["NODE_PORT"]]
if err = checkPort(client.server, nodePort); err != nil {
log.Warn(fmt.Sprintf("Wallet devp2p port seems unreachable"), "server", client.server, "port", nodePort, "err", err)
}
rpcPort := infos.portmap["8545/tcp"]
if err = checkPort(client.server, rpcPort); err != nil {
log.Warn(fmt.Sprintf("Wallet RPC port seems unreachable"), "server", client.server, "port", rpcPort, "err", err)
}
// Assemble and return the useful infos
stats := &walletInfos{
datadir: infos.volumes["/root/.ethereum"],
nodePort: nodePort,
rpcPort: rpcPort,
webHost: host,
webPort: webPort,
ethstats: infos.envvars["STATS"],
}
return stats, nil
}

@ -38,7 +38,7 @@ func main() {
}, },
cli.IntFlag{ cli.IntFlag{
Name: "loglevel", Name: "loglevel",
Value: 4, Value: 3,
Usage: "log level to emit to the screen", Usage: "log level to emit to the screen",
}, },
} }

@ -116,6 +116,7 @@ func dial(server string, pubkey []byte) (*sshClient, error) {
keycheck := func(hostname string, remote net.Addr, key ssh.PublicKey) error { keycheck := func(hostname string, remote net.Addr, key ssh.PublicKey) error {
// If no public key is known for SSH, ask the user to confirm // If no public key is known for SSH, ask the user to confirm
if pubkey == nil { if pubkey == nil {
fmt.Println()
fmt.Printf("The authenticity of host '%s (%s)' can't be established.\n", hostname, remote) fmt.Printf("The authenticity of host '%s (%s)' can't be established.\n", hostname, remote)
fmt.Printf("SSH key fingerprint is %s [MD5]\n", ssh.FingerprintLegacyMD5(key)) fmt.Printf("SSH key fingerprint is %s [MD5]\n", ssh.FingerprintLegacyMD5(key))
fmt.Printf("Are you sure you want to continue connecting (yes/no)? ") fmt.Printf("Are you sure you want to continue connecting (yes/no)? ")

@ -28,6 +28,7 @@ import (
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
"sync"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core"
@ -39,11 +40,11 @@ import (
// between sessions. // between sessions.
type config struct { type config struct {
path string // File containing the configuration values path string // File containing the configuration values
genesis *core.Genesis // Genesis block to cache for node deploys
bootFull []string // Bootnodes to always connect to by full nodes bootFull []string // Bootnodes to always connect to by full nodes
bootLight []string // Bootnodes to always connect to by light nodes bootLight []string // Bootnodes to always connect to by light nodes
ethstats string // Ethstats settings to cache for node deploys ethstats string // Ethstats settings to cache for node deploys
Genesis *core.Genesis `json:"genesis,omitempty"` // Genesis block to cache for node deploys
Servers map[string][]byte `json:"servers,omitempty"` Servers map[string][]byte `json:"servers,omitempty"`
} }
@ -76,6 +77,7 @@ type wizard struct {
services map[string][]string // Ethereum services known to be running on servers services map[string][]string // Ethereum services known to be running on servers
in *bufio.Reader // Wrapper around stdin to allow reading user input in *bufio.Reader // Wrapper around stdin to allow reading user input
lock sync.Mutex // Lock to protect configs during concurrent service discovery
} }
// read reads a single line from stdin, trimming if from spaces. // read reads a single line from stdin, trimming if from spaces.

@ -40,6 +40,8 @@ func (w *wizard) deployDashboard() {
host: client.server, host: client.server,
} }
} }
existed := err == nil
// Figure out which port to listen on // Figure out which port to listen on
fmt.Println() fmt.Println()
fmt.Printf("Which port should the dashboard listen on? (default = %d)\n", infos.port) fmt.Printf("Which port should the dashboard listen on? (default = %d)\n", infos.port)
@ -58,7 +60,6 @@ func (w *wizard) deployDashboard() {
available[service] = append(available[service], server) available[service] = append(available[service], server)
} }
} }
listing := make(map[string]string)
for _, service := range []string{"ethstats", "explorer", "wallet", "faucet"} { for _, service := range []string{"ethstats", "explorer", "wallet", "faucet"} {
// Gather all the locally hosted pages of this type // Gather all the locally hosted pages of this type
var pages []string var pages []string
@ -74,6 +75,14 @@ func (w *wizard) deployDashboard() {
if infos, err := checkEthstats(client, w.network); err == nil { if infos, err := checkEthstats(client, w.network); err == nil {
port = infos.port port = infos.port
} }
case "explorer":
if infos, err := checkExplorer(client, w.network); err == nil {
port = infos.webPort
}
case "wallet":
if infos, err := checkWallet(client, w.network); err == nil {
port = infos.webPort
}
case "faucet": case "faucet":
if infos, err := checkFaucet(client, w.network); err == nil { if infos, err := checkFaucet(client, w.network); err == nil {
port = infos.port port = infos.port
@ -101,26 +110,43 @@ func (w *wizard) deployDashboard() {
log.Error("Invalid listing choice, aborting") log.Error("Invalid listing choice, aborting")
return return
} }
var page string
switch { switch {
case choice <= len(pages): case choice <= len(pages):
listing[service] = pages[choice-1] page = pages[choice-1]
case choice == len(pages)+1: case choice == len(pages)+1:
fmt.Println() fmt.Println()
fmt.Printf("Which address is the external %s service at?\n", service) fmt.Printf("Which address is the external %s service at?\n", service)
listing[service] = w.readString() page = w.readString()
default: default:
// No service hosting for this // No service hosting for this
} }
// Save the users choice
switch service {
case "ethstats":
infos.ethstats = page
case "explorer":
infos.explorer = page
case "wallet":
infos.wallet = page
case "faucet":
infos.faucet = page
}
} }
// If we have ethstats running, ask whether to make the secret public or not // If we have ethstats running, ask whether to make the secret public or not
var ethstats bool
if w.conf.ethstats != "" { if w.conf.ethstats != "" {
fmt.Println() fmt.Println()
fmt.Println("Include ethstats secret on dashboard (y/n)? (default = yes)") fmt.Println("Include ethstats secret on dashboard (y/n)? (default = yes)")
ethstats = w.readDefaultString("y") == "y" infos.trusted = w.readDefaultString("y") == "y"
} }
// Try to deploy the dashboard container on the host // Try to deploy the dashboard container on the host
if out, err := deployDashboard(client, w.network, infos.port, infos.host, listing, &w.conf, ethstats); err != nil { nocache := false
if existed {
fmt.Println()
fmt.Printf("Should the dashboard be built from scratch (y/n)? (default = no)\n")
nocache = w.readDefaultString("n") != "n"
}
if out, err := deployDashboard(client, w.network, &w.conf, infos, nocache); err != nil {
log.Error("Failed to deploy dashboard container", "err", err) log.Error("Failed to deploy dashboard container", "err", err)
if len(out) > 0 { if len(out) > 0 {
fmt.Printf("%s\n", out) fmt.Printf("%s\n", out)
@ -128,5 +154,5 @@ func (w *wizard) deployDashboard() {
return return
} }
// All ok, run a network scan to pick any changes up // All ok, run a network scan to pick any changes up
w.networkStats(false) w.networkStats()
} }

@ -42,6 +42,8 @@ func (w *wizard) deployEthstats() {
secret: "", secret: "",
} }
} }
existed := err == nil
// Figure out which port to listen on // Figure out which port to listen on
fmt.Println() fmt.Println()
fmt.Printf("Which port should ethstats listen on? (default = %d)\n", infos.port) fmt.Printf("Which port should ethstats listen on? (default = %d)\n", infos.port)
@ -62,6 +64,7 @@ func (w *wizard) deployEthstats() {
infos.secret = w.readDefaultString(infos.secret) infos.secret = w.readDefaultString(infos.secret)
} }
// Gather any blacklists to ban from reporting // Gather any blacklists to ban from reporting
if existed {
fmt.Println() fmt.Println()
fmt.Printf("Keep existing IP %v blacklist (y/n)? (default = yes)\n", infos.banned) fmt.Printf("Keep existing IP %v blacklist (y/n)? (default = yes)\n", infos.banned)
if w.readDefaultString("y") != "y" { if w.readDefaultString("y") != "y" {
@ -97,14 +100,21 @@ func (w *wizard) deployEthstats() {
} }
sort.Strings(infos.banned) sort.Strings(infos.banned)
} }
}
// Try to deploy the ethstats server on the host // Try to deploy the ethstats server on the host
nocache := false
if existed {
fmt.Println()
fmt.Printf("Should the ethstats be built from scratch (y/n)? (default = no)\n")
nocache = w.readDefaultString("n") != "n"
}
trusted := make([]string, 0, len(w.servers)) trusted := make([]string, 0, len(w.servers))
for _, client := range w.servers { for _, client := range w.servers {
if client != nil { if client != nil {
trusted = append(trusted, client.address) trusted = append(trusted, client.address)
} }
} }
if out, err := deployEthstats(client, w.network, infos.port, infos.secret, infos.host, trusted, infos.banned); err != nil { if out, err := deployEthstats(client, w.network, infos.port, infos.secret, infos.host, trusted, infos.banned, nocache); err != nil {
log.Error("Failed to deploy ethstats container", "err", err) log.Error("Failed to deploy ethstats container", "err", err)
if len(out) > 0 { if len(out) > 0 {
fmt.Printf("%s\n", out) fmt.Printf("%s\n", out)
@ -112,5 +122,5 @@ func (w *wizard) deployEthstats() {
return return
} }
// All ok, run a network scan to pick any changes up // All ok, run a network scan to pick any changes up
w.networkStats(false) w.networkStats()
} }

@ -0,0 +1,117 @@
// 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,
}
}
existed := err == nil
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
nocache := false
if existed {
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()
}

@ -19,7 +19,6 @@ package main
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/http"
"github.com/ethereum/go-ethereum/accounts/keystore" "github.com/ethereum/go-ethereum/accounts/keystore"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
@ -47,8 +46,10 @@ func (w *wizard) deployFaucet() {
tiers: 3, tiers: 3,
} }
} }
infos.node.genesis, _ = json.MarshalIndent(w.conf.genesis, "", " ") existed := err == nil
infos.node.network = w.conf.genesis.Config.ChainId.Int64()
infos.node.genesis, _ = json.MarshalIndent(w.conf.Genesis, "", " ")
infos.node.network = w.conf.Genesis.Config.ChainId.Int64()
// Figure out which port to listen on // Figure out which port to listen on
fmt.Println() fmt.Println()
@ -60,7 +61,7 @@ func (w *wizard) deployFaucet() {
log.Error("Failed to decide on faucet host", "err", err) log.Error("Failed to decide on faucet host", "err", err)
return 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.Println()
fmt.Printf("How many Ethers to release per request? (default = %d)\n", infos.amount) fmt.Printf("How many Ethers to release per request? (default = %d)\n", infos.amount)
infos.amount = w.readDefaultInt(infos.amount) infos.amount = w.readDefaultInt(infos.amount)
@ -76,47 +77,6 @@ func (w *wizard) deployFaucet() {
log.Error("At least one funding tier must be set") log.Error("At least one funding tier must be set")
return return
} }
// Accessing GitHub gists requires API authorization, retrieve it
if infos.githubUser != "" {
fmt.Println()
fmt.Printf("Reuse previous (%s) GitHub API authorization (y/n)? (default = yes)\n", infos.githubUser)
if w.readDefaultString("y") != "y" {
infos.githubUser, infos.githubToken = "", ""
}
}
if infos.githubUser == "" {
// No previous authorization (or new one requested)
fmt.Println()
fmt.Println("Which GitHub user to verify Gists through?")
infos.githubUser = w.readString()
fmt.Println()
fmt.Println("What is the GitHub personal access token of the user? (won't be echoed)")
infos.githubToken = w.readPassword()
// Do a sanity check query against github to ensure it's valid
req, _ := http.NewRequest("GET", "https://api.github.com/user", nil)
req.SetBasicAuth(infos.githubUser, infos.githubToken)
res, err := http.DefaultClient.Do(req)
if err != nil {
log.Error("Failed to verify GitHub authentication", "err", err)
return
}
defer res.Body.Close()
var msg struct {
Login string `json:"login"`
Message string `json:"message"`
}
if err = json.NewDecoder(res.Body).Decode(&msg); err != nil {
log.Error("Failed to decode authorization response", "err", err)
return
}
if msg.Login != infos.githubUser {
log.Error("GitHub authorization failed", "user", infos.githubUser, "message", msg.Message)
return
}
}
// Accessing the reCaptcha service requires API authorizations, request it // Accessing the reCaptcha service requires API authorizations, request it
if infos.captchaToken != "" { if infos.captchaToken != "" {
fmt.Println() fmt.Println()
@ -129,7 +89,9 @@ func (w *wizard) deployFaucet() {
// No previous authorization (or old one discarded) // No previous authorization (or old one discarded)
fmt.Println() fmt.Println()
fmt.Println("Enable reCaptcha protection against robots (y/n)? (default = no)") fmt.Println("Enable reCaptcha protection against robots (y/n)? (default = no)")
if w.readDefaultString("n") == "y" { if w.readDefaultString("n") == "n" {
log.Warn("Users will be able to requests funds via automated scripts")
} else {
// Captcha protection explicitly requested, read the site and secret keys // Captcha protection explicitly requested, read the site and secret keys
fmt.Println() fmt.Println()
fmt.Printf("What is the reCaptcha site key to authenticate human users?\n") fmt.Printf("What is the reCaptcha site key to authenticate human users?\n")
@ -175,7 +137,7 @@ func (w *wizard) deployFaucet() {
} }
} }
} }
if infos.node.keyJSON == "" { for i := 0; i < 3 && infos.node.keyJSON == ""; i++ {
fmt.Println() fmt.Println()
fmt.Println("Please paste the faucet's funding account key JSON:") fmt.Println("Please paste the faucet's funding account key JSON:")
infos.node.keyJSON = w.readJSON() infos.node.keyJSON = w.readJSON()
@ -186,11 +148,27 @@ func (w *wizard) deployFaucet() {
if _, err := keystore.DecryptKey([]byte(infos.node.keyJSON), infos.node.keyPass); err != nil { if _, err := keystore.DecryptKey([]byte(infos.node.keyJSON), infos.node.keyPass); err != nil {
log.Error("Failed to decrypt key with given passphrase") log.Error("Failed to decrypt key with given passphrase")
return infos.node.keyJSON = ""
infos.node.keyPass = ""
} }
} }
// Check if the user wants to run the faucet in debug mode (noauth)
noauth := "n"
if infos.noauth {
noauth = "y"
}
fmt.Println()
fmt.Printf("Permit non-authenticated funding requests (y/n)? (default = %v)\n", infos.noauth)
infos.noauth = w.readDefaultString(noauth) != "n"
// Try to deploy the faucet server on the host // Try to deploy the faucet server on the host
if out, err := deployFaucet(client, w.network, w.conf.bootLight, infos); err != nil { nocache := false
if existed {
fmt.Println()
fmt.Printf("Should the faucet be built from scratch (y/n)? (default = no)\n")
nocache = w.readDefaultString("n") != "n"
}
if out, err := deployFaucet(client, w.network, w.conf.bootLight, infos, nocache); err != nil {
log.Error("Failed to deploy faucet container", "err", err) log.Error("Failed to deploy faucet container", "err", err)
if len(out) > 0 { if len(out) > 0 {
fmt.Printf("%s\n", out) fmt.Printf("%s\n", out)
@ -198,5 +176,5 @@ func (w *wizard) deployFaucet() {
return return
} }
// All ok, run a network scan to pick any changes up // All ok, run a network scan to pick any changes up
w.networkStats(false) w.networkStats()
} }

@ -37,7 +37,7 @@ func (w *wizard) makeGenesis() {
genesis := &core.Genesis{ genesis := &core.Genesis{
Timestamp: uint64(time.Now().Unix()), Timestamp: uint64(time.Now().Unix()),
GasLimit: 4700000, GasLimit: 4700000,
Difficulty: big.NewInt(1048576), Difficulty: big.NewInt(524288),
Alloc: make(core.GenesisAlloc), Alloc: make(core.GenesisAlloc),
Config: &params.ChainConfig{ Config: &params.ChainConfig{
HomesteadBlock: big.NewInt(1), HomesteadBlock: big.NewInt(1),
@ -118,24 +118,16 @@ func (w *wizard) makeGenesis() {
for i := int64(0); i < 256; i++ { for i := int64(0); i < 256; i++ {
genesis.Alloc[common.BigToAddress(big.NewInt(i))] = core.GenesisAccount{Balance: big.NewInt(1)} genesis.Alloc[common.BigToAddress(big.NewInt(i))] = core.GenesisAccount{Balance: big.NewInt(1)}
} }
fmt.Println()
// Query the user for some custom extras // Query the user for some custom extras
fmt.Println() fmt.Println()
fmt.Println("Specify your chain/network ID if you want an explicit one (default = random)") fmt.Println("Specify your chain/network ID if you want an explicit one (default = random)")
genesis.Config.ChainId = new(big.Int).SetUint64(uint64(w.readDefaultInt(rand.Intn(65536)))) genesis.Config.ChainId = new(big.Int).SetUint64(uint64(w.readDefaultInt(rand.Intn(65536))))
fmt.Println()
fmt.Println("Anything fun to embed into the genesis block? (max 32 bytes)")
extra := w.read()
if len(extra) > 32 {
extra = extra[:32]
}
genesis.ExtraData = append([]byte(extra), genesis.ExtraData[len(extra):]...)
// All done, store the genesis and flush to disk // All done, store the genesis and flush to disk
w.conf.genesis = genesis log.Info("Configured new genesis block")
w.conf.Genesis = genesis
w.conf.flush()
} }
// manageGenesis permits the modification of chain configuration parameters in // manageGenesis permits the modification of chain configuration parameters in
@ -145,44 +137,56 @@ func (w *wizard) manageGenesis() {
fmt.Println() fmt.Println()
fmt.Println(" 1. Modify existing fork rules") fmt.Println(" 1. Modify existing fork rules")
fmt.Println(" 2. Export genesis configuration") fmt.Println(" 2. Export genesis configuration")
fmt.Println(" 3. Remove genesis configuration")
choice := w.read() choice := w.read()
switch { switch {
case choice == "1": case choice == "1":
// Fork rule updating requested, iterate over each fork // Fork rule updating requested, iterate over each fork
fmt.Println() fmt.Println()
fmt.Printf("Which block should Homestead come into effect? (default = %v)\n", w.conf.genesis.Config.HomesteadBlock) fmt.Printf("Which block should Homestead come into effect? (default = %v)\n", w.conf.Genesis.Config.HomesteadBlock)
w.conf.genesis.Config.HomesteadBlock = w.readDefaultBigInt(w.conf.genesis.Config.HomesteadBlock) w.conf.Genesis.Config.HomesteadBlock = w.readDefaultBigInt(w.conf.Genesis.Config.HomesteadBlock)
fmt.Println() fmt.Println()
fmt.Printf("Which block should EIP150 come into effect? (default = %v)\n", w.conf.genesis.Config.EIP150Block) fmt.Printf("Which block should EIP150 come into effect? (default = %v)\n", w.conf.Genesis.Config.EIP150Block)
w.conf.genesis.Config.EIP150Block = w.readDefaultBigInt(w.conf.genesis.Config.EIP150Block) w.conf.Genesis.Config.EIP150Block = w.readDefaultBigInt(w.conf.Genesis.Config.EIP150Block)
fmt.Println() fmt.Println()
fmt.Printf("Which block should EIP155 come into effect? (default = %v)\n", w.conf.genesis.Config.EIP155Block) fmt.Printf("Which block should EIP155 come into effect? (default = %v)\n", w.conf.Genesis.Config.EIP155Block)
w.conf.genesis.Config.EIP155Block = w.readDefaultBigInt(w.conf.genesis.Config.EIP155Block) w.conf.Genesis.Config.EIP155Block = w.readDefaultBigInt(w.conf.Genesis.Config.EIP155Block)
fmt.Println() fmt.Println()
fmt.Printf("Which block should EIP158 come into effect? (default = %v)\n", w.conf.genesis.Config.EIP158Block) fmt.Printf("Which block should EIP158 come into effect? (default = %v)\n", w.conf.Genesis.Config.EIP158Block)
w.conf.genesis.Config.EIP158Block = w.readDefaultBigInt(w.conf.genesis.Config.EIP158Block) w.conf.Genesis.Config.EIP158Block = w.readDefaultBigInt(w.conf.Genesis.Config.EIP158Block)
fmt.Println() fmt.Println()
fmt.Printf("Which block should Byzantium come into effect? (default = %v)\n", w.conf.genesis.Config.ByzantiumBlock) fmt.Printf("Which block should Byzantium come into effect? (default = %v)\n", w.conf.Genesis.Config.ByzantiumBlock)
w.conf.genesis.Config.ByzantiumBlock = w.readDefaultBigInt(w.conf.genesis.Config.ByzantiumBlock) w.conf.Genesis.Config.ByzantiumBlock = w.readDefaultBigInt(w.conf.Genesis.Config.ByzantiumBlock)
out, _ := json.MarshalIndent(w.conf.genesis.Config, "", " ") out, _ := json.MarshalIndent(w.conf.Genesis.Config, "", " ")
fmt.Printf("Chain configuration updated:\n\n%s\n", out) fmt.Printf("Chain configuration updated:\n\n%s\n", out)
case choice == "2": case choice == "2":
// Save whatever genesis configuration we currently have // Save whatever genesis configuration we currently have
fmt.Println() fmt.Println()
fmt.Printf("Which file to save the genesis into? (default = %s.json)\n", w.network) fmt.Printf("Which file to save the genesis into? (default = %s.json)\n", w.network)
out, _ := json.MarshalIndent(w.conf.genesis, "", " ") out, _ := json.MarshalIndent(w.conf.Genesis, "", " ")
if err := ioutil.WriteFile(w.readDefaultString(fmt.Sprintf("%s.json", w.network)), out, 0644); err != nil { if err := ioutil.WriteFile(w.readDefaultString(fmt.Sprintf("%s.json", w.network)), out, 0644); err != nil {
log.Error("Failed to save genesis file", "err", err) log.Error("Failed to save genesis file", "err", err)
} }
log.Info("Exported existing genesis block") log.Info("Exported existing genesis block")
case choice == "3":
// Make sure we don't have any services running
if len(w.conf.servers()) > 0 {
log.Error("Genesis reset requires all services and servers torn down")
return
}
log.Info("Genesis block destroyed")
w.conf.Genesis = nil
w.conf.flush()
default: default:
log.Error("That's not something I can do") log.Error("That's not something I can do")
} }

@ -24,6 +24,7 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"sync"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
) )
@ -63,7 +64,7 @@ func (w *wizard) run() {
for { for {
w.network = w.readString() w.network = w.readString()
if !strings.Contains(w.network, " ") { if !strings.Contains(w.network, " ") {
fmt.Printf("Sweet, you can set this via --network=%s next time!\n\n", w.network) fmt.Printf("\nSweet, you can set this via --network=%s next time!\n\n", w.network)
break break
} }
log.Error("I also like to live dangerously, still no spaces") log.Error("I also like to live dangerously, still no spaces")
@ -80,22 +81,33 @@ func (w *wizard) run() {
} else if err := json.Unmarshal(blob, &w.conf); err != nil { } else if err := json.Unmarshal(blob, &w.conf); err != nil {
log.Crit("Previous configuration corrupted", "path", w.conf.path, "err", err) log.Crit("Previous configuration corrupted", "path", w.conf.path, "err", err)
} else { } else {
// Dial all previously known servers concurrently
var pend sync.WaitGroup
for server, pubkey := range w.conf.Servers { for server, pubkey := range w.conf.Servers {
pend.Add(1)
go func(server string, pubkey []byte) {
defer pend.Done()
log.Info("Dialing previously configured server", "server", server) log.Info("Dialing previously configured server", "server", server)
client, err := dial(server, pubkey) client, err := dial(server, pubkey)
if err != nil { if err != nil {
log.Error("Previous server unreachable", "server", server, "err", err) log.Error("Previous server unreachable", "server", server, "err", err)
} }
w.lock.Lock()
w.servers[server] = client w.servers[server] = client
w.lock.Unlock()
}(server, pubkey)
} }
w.networkStats(false) pend.Wait()
w.networkStats()
} }
// Basics done, loop ad infinitum about what to do // Basics done, loop ad infinitum about what to do
for { for {
fmt.Println() fmt.Println()
fmt.Println("What would you like to do? (default = stats)") fmt.Println("What would you like to do? (default = stats)")
fmt.Println(" 1. Show network stats") fmt.Println(" 1. Show network stats")
if w.conf.genesis == nil { if w.conf.Genesis == nil {
fmt.Println(" 2. Configure new genesis") fmt.Println(" 2. Configure new genesis")
} else { } else {
fmt.Println(" 2. Manage existing genesis") fmt.Println(" 2. Manage existing genesis")
@ -110,15 +122,14 @@ func (w *wizard) run() {
} else { } else {
fmt.Println(" 4. Manage network components") fmt.Println(" 4. Manage network components")
} }
//fmt.Println(" 5. ProTips for common usecases")
choice := w.read() choice := w.read()
switch { switch {
case choice == "" || choice == "1": case choice == "" || choice == "1":
w.networkStats(false) w.networkStats()
case choice == "2": case choice == "2":
if w.conf.genesis == nil { if w.conf.Genesis == nil {
w.makeGenesis() w.makeGenesis()
} else { } else {
w.manageGenesis() w.manageGenesis()
@ -126,7 +137,7 @@ func (w *wizard) run() {
case choice == "3": case choice == "3":
if len(w.servers) == 0 { if len(w.servers) == 0 {
if w.makeServer() != "" { if w.makeServer() != "" {
w.networkStats(false) w.networkStats()
} }
} else { } else {
w.manageServers() w.manageServers()
@ -138,9 +149,6 @@ func (w *wizard) run() {
w.manageComponents() w.manageComponents()
} }
case choice == "5":
w.networkStats(true)
default: default:
log.Error("That's not something I can do") log.Error("That's not something I can do")
} }

@ -18,9 +18,10 @@ package main
import ( import (
"encoding/json" "encoding/json"
"fmt"
"os" "os"
"sort"
"strings" "strings"
"sync"
"github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
@ -29,127 +30,257 @@ import (
// networkStats verifies the status of network components and generates a protip // networkStats verifies the status of network components and generates a protip
// configuration set to give users hints on how to do various tasks. // configuration set to give users hints on how to do various tasks.
func (w *wizard) networkStats(tips bool) { func (w *wizard) networkStats() {
if len(w.servers) == 0 { if len(w.servers) == 0 {
log.Error("No remote machines to gather stats from") log.Info("No remote machines to gather stats from")
return return
} }
protips := new(protips) // Clear out some previous configs to refill from current scan
w.conf.ethstats = ""
w.conf.bootFull = w.conf.bootFull[:0]
w.conf.bootLight = w.conf.bootLight[:0]
// Iterate over all the specified hosts and check their status // Iterate over all the specified hosts and check their status
stats := tablewriter.NewWriter(os.Stdout) var pend sync.WaitGroup
stats.SetHeader([]string{"Server", "IP", "Status", "Service", "Details"})
stats.SetColWidth(100)
stats := make(serverStats)
for server, pubkey := range w.conf.Servers { for server, pubkey := range w.conf.Servers {
client := w.servers[server] pend.Add(1)
// Gather the service stats for each server concurrently
go func(server string, pubkey []byte) {
defer pend.Done()
stat := w.gatherStats(server, pubkey, w.servers[server])
// All status checks complete, report and check next server
w.lock.Lock()
defer w.lock.Unlock()
delete(w.services, server)
for service := range stat.services {
w.services[server] = append(w.services[server], service)
}
stats[server] = stat
}(server, pubkey)
}
pend.Wait()
// Print any collected stats and return
stats.render()
}
// gatherStats gathers service statistics for a particular remote server.
func (w *wizard) gatherStats(server string, pubkey []byte, client *sshClient) *serverStat {
// Gather some global stats to feed into the wizard
var (
genesis string
ethstats string
bootFull []string
bootLight []string
)
// Ensure a valid SSH connection to the remote server
logger := log.New("server", server) logger := log.New("server", server)
logger.Info("Starting remote server health-check") logger.Info("Starting remote server health-check")
// If the server is not connected, try to connect again stat := &serverStat{
address: client.address,
services: make(map[string]map[string]string),
}
if client == nil { if client == nil {
conn, err := dial(server, pubkey) conn, err := dial(server, pubkey)
if err != nil { if err != nil {
logger.Error("Failed to establish remote connection", "err", err) logger.Error("Failed to establish remote connection", "err", err)
stats.Append([]string{server, "", err.Error(), "", ""}) stat.failure = err.Error()
continue return stat
} }
client = conn client = conn
} }
// Client connected one way or another, run health-checks // Client connected one way or another, run health-checks
services := make(map[string]string)
logger.Debug("Checking for nginx availability") logger.Debug("Checking for nginx availability")
if infos, err := checkNginx(client, w.network); err != nil { if infos, err := checkNginx(client, w.network); err != nil {
if err != ErrServiceUnknown { if err != ErrServiceUnknown {
services["nginx"] = err.Error() stat.services["nginx"] = map[string]string{"offline": err.Error()}
} }
} else { } else {
services["nginx"] = infos.String() stat.services["nginx"] = infos.Report()
} }
logger.Debug("Checking for ethstats availability") logger.Debug("Checking for ethstats availability")
if infos, err := checkEthstats(client, w.network); err != nil { if infos, err := checkEthstats(client, w.network); err != nil {
if err != ErrServiceUnknown { if err != ErrServiceUnknown {
services["ethstats"] = err.Error() stat.services["ethstats"] = map[string]string{"offline": err.Error()}
} }
} else { } else {
services["ethstats"] = infos.String() stat.services["ethstats"] = infos.Report()
protips.ethstats = infos.config ethstats = infos.config
} }
logger.Debug("Checking for bootnode availability") logger.Debug("Checking for bootnode availability")
if infos, err := checkNode(client, w.network, true); err != nil { if infos, err := checkNode(client, w.network, true); err != nil {
if err != ErrServiceUnknown { if err != ErrServiceUnknown {
services["bootnode"] = err.Error() stat.services["bootnode"] = map[string]string{"offline": err.Error()}
} }
} else { } else {
services["bootnode"] = infos.String() stat.services["bootnode"] = infos.Report()
protips.genesis = string(infos.genesis) genesis = string(infos.genesis)
protips.bootFull = append(protips.bootFull, infos.enodeFull) bootFull = append(bootFull, infos.enodeFull)
if infos.enodeLight != "" { if infos.enodeLight != "" {
protips.bootLight = append(protips.bootLight, infos.enodeLight) bootLight = append(bootLight, infos.enodeLight)
} }
} }
logger.Debug("Checking for sealnode availability") logger.Debug("Checking for sealnode availability")
if infos, err := checkNode(client, w.network, false); err != nil { if infos, err := checkNode(client, w.network, false); err != nil {
if err != ErrServiceUnknown { if err != ErrServiceUnknown {
services["sealnode"] = err.Error() stat.services["sealnode"] = map[string]string{"offline": err.Error()}
} }
} else { } else {
services["sealnode"] = infos.String() stat.services["sealnode"] = infos.Report()
protips.genesis = string(infos.genesis) 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 wallet availability")
if infos, err := checkWallet(client, w.network); err != nil {
if err != ErrServiceUnknown {
stat.services["wallet"] = map[string]string{"offline": err.Error()}
}
} else {
stat.services["wallet"] = infos.Report()
} }
logger.Debug("Checking for faucet availability") logger.Debug("Checking for faucet availability")
if infos, err := checkFaucet(client, w.network); err != nil { if infos, err := checkFaucet(client, w.network); err != nil {
if err != ErrServiceUnknown { if err != ErrServiceUnknown {
services["faucet"] = err.Error() stat.services["faucet"] = map[string]string{"offline": err.Error()}
} }
} else { } else {
services["faucet"] = infos.String() stat.services["faucet"] = infos.Report()
} }
logger.Debug("Checking for dashboard availability") logger.Debug("Checking for dashboard availability")
if infos, err := checkDashboard(client, w.network); err != nil { if infos, err := checkDashboard(client, w.network); err != nil {
if err != ErrServiceUnknown { if err != ErrServiceUnknown {
services["dashboard"] = err.Error() stat.services["dashboard"] = map[string]string{"offline": err.Error()}
} }
} else { } else {
services["dashboard"] = infos.String() stat.services["dashboard"] = infos.Report()
} }
// All status checks complete, report and check next server // Feed and newly discovered information into the wizard
delete(w.services, server) w.lock.Lock()
for service := range services { defer w.lock.Unlock()
w.services[server] = append(w.services[server], service)
} if genesis != "" && w.conf.Genesis == nil {
server, address := client.server, client.address g := new(core.Genesis)
for service, status := range services { if err := json.Unmarshal([]byte(genesis), g); err != nil {
stats.Append([]string{server, address, "online", service, status})
server, address = "", ""
}
if len(services) == 0 {
stats.Append([]string{server, address, "online", "", ""})
}
}
// If a genesis block was found, load it into our configs
if protips.genesis != "" && w.conf.genesis == nil {
genesis := new(core.Genesis)
if err := json.Unmarshal([]byte(protips.genesis), genesis); err != nil {
log.Error("Failed to parse remote genesis", "err", err) log.Error("Failed to parse remote genesis", "err", err)
} else { } else {
w.conf.genesis = genesis w.conf.Genesis = g
protips.network = genesis.Config.ChainId.Int64()
} }
} }
if protips.ethstats != "" { if ethstats != "" {
w.conf.ethstats = protips.ethstats w.conf.ethstats = ethstats
} }
w.conf.bootFull = protips.bootFull w.conf.bootFull = append(w.conf.bootFull, bootFull...)
w.conf.bootLight = protips.bootLight w.conf.bootLight = append(w.conf.bootLight, bootLight...)
// Print any collected stats and return return stat
if !tips { }
stats.Render()
} else { // serverStat is a collection of service configuration parameters and health
protips.print(w.network) // check reports to print to the user.
type serverStat struct {
address string
failure string
services map[string]map[string]string
}
// serverStats is a collection of server stats for multiple hosts.
type serverStats map[string]*serverStat
// render converts the gathered statistics into a user friendly tabular report
// and prints it to the standard output.
func (stats serverStats) render() {
// Start gathering service statistics and config parameters
table := tablewriter.NewWriter(os.Stdout)
table.SetHeader([]string{"Server", "Address", "Service", "Config", "Value"})
table.SetAlignment(tablewriter.ALIGN_LEFT)
table.SetColWidth(100)
// Find the longest lines for all columns for the hacked separator
separator := make([]string, 5)
for server, stat := range stats {
if len(server) > len(separator[0]) {
separator[0] = strings.Repeat("-", len(server))
} }
if len(stat.address) > len(separator[1]) {
separator[1] = strings.Repeat("-", len(stat.address))
}
for service, configs := range stat.services {
if len(service) > len(separator[2]) {
separator[2] = strings.Repeat("-", len(service))
}
for config, value := range configs {
if len(config) > len(separator[3]) {
separator[3] = strings.Repeat("-", len(config))
}
if len(value) > len(separator[4]) {
separator[4] = strings.Repeat("-", len(value))
}
}
}
}
// Fill up the server report in alphabetical order
servers := make([]string, 0, len(stats))
for server := range stats {
servers = append(servers, server)
}
sort.Strings(servers)
for i, server := range servers {
// Add a separator between all servers
if i > 0 {
table.Append(separator)
}
// Fill up the service report in alphabetical order
services := make([]string, 0, len(stats[server].services))
for service := range stats[server].services {
services = append(services, service)
}
sort.Strings(services)
if len(services) == 0 {
table.Append([]string{server, stats[server].address, "", "", ""})
}
for j, service := range services {
// Add an empty line between all services
if j > 0 {
table.Append([]string{"", "", "", separator[3], separator[4]})
}
// Fill up the config report in alphabetical order
configs := make([]string, 0, len(stats[server].services[service]))
for service := range stats[server].services[service] {
configs = append(configs, service)
}
sort.Strings(configs)
for k, config := range configs {
switch {
case j == 0 && k == 0:
table.Append([]string{server, stats[server].address, service, config, stats[server].services[service][config]})
case k == 0:
table.Append([]string{"", "", service, config, stats[server].services[service][config]})
default:
table.Append([]string{"", "", "", config, stats[server].services[service][config]})
}
}
}
}
table.Render()
} }
// protips contains a collection of network infos to report pro-tips // protips contains a collection of network infos to report pro-tips
@ -161,75 +292,3 @@ type protips struct {
bootLight []string bootLight []string
ethstats string ethstats string
} }
// print analyzes the network information available and prints a collection of
// pro tips for the user's consideration.
func (p *protips) print(network string) {
// If a known genesis block is available, display it and prepend an init command
fullinit, lightinit := "", ""
if p.genesis != "" {
fullinit = fmt.Sprintf("geth --datadir=$HOME/.%s init %s.json && ", network, network)
lightinit = fmt.Sprintf("geth --datadir=$HOME/.%s --light init %s.json && ", network, network)
}
// If an ethstats server is available, add the ethstats flag
statsflag := ""
if p.ethstats != "" {
if strings.Contains(p.ethstats, " ") {
statsflag = fmt.Sprintf(` --ethstats="yournode:%s"`, p.ethstats)
} else {
statsflag = fmt.Sprintf(` --ethstats=yournode:%s`, p.ethstats)
}
}
// If bootnodes have been specified, add the bootnode flag
bootflagFull := ""
if len(p.bootFull) > 0 {
bootflagFull = fmt.Sprintf(` --bootnodes %s`, strings.Join(p.bootFull, ","))
}
bootflagLight := ""
if len(p.bootLight) > 0 {
bootflagLight = fmt.Sprintf(` --bootnodes %s`, strings.Join(p.bootLight, ","))
}
// Assemble all the known pro-tips
var tasks, tips []string
tasks = append(tasks, "Run an archive node with historical data")
tips = append(tips, fmt.Sprintf("%sgeth --networkid=%d --datadir=$HOME/.%s --cache=1024%s%s", fullinit, p.network, network, statsflag, bootflagFull))
tasks = append(tasks, "Run a full node with recent data only")
tips = append(tips, fmt.Sprintf("%sgeth --networkid=%d --datadir=$HOME/.%s --cache=512 --fast%s%s", fullinit, p.network, network, statsflag, bootflagFull))
tasks = append(tasks, "Run a light node with on demand retrievals")
tips = append(tips, fmt.Sprintf("%sgeth --networkid=%d --datadir=$HOME/.%s --light%s%s", lightinit, p.network, network, statsflag, bootflagLight))
tasks = append(tasks, "Run an embedded node with constrained memory")
tips = append(tips, fmt.Sprintf("%sgeth --networkid=%d --datadir=$HOME/.%s --cache=32 --light%s%s", lightinit, p.network, network, statsflag, bootflagLight))
// If the tips are short, display in a table
short := true
for _, tip := range tips {
if len(tip) > 100 {
short = false
break
}
}
fmt.Println()
if short {
howto := tablewriter.NewWriter(os.Stdout)
howto.SetHeader([]string{"Fun tasks for you", "Tips on how to"})
howto.SetColWidth(100)
for i := 0; i < len(tasks); i++ {
howto.Append([]string{tasks[i], tips[i]})
}
howto.Render()
return
}
// Meh, tips got ugly, split into many lines
for i := 0; i < len(tasks); i++ {
fmt.Println(tasks[i])
fmt.Println(strings.Repeat("-", len(tasks[i])))
fmt.Println(tips[i])
fmt.Println()
fmt.Println()
}
}

@ -53,12 +53,12 @@ func (w *wizard) manageServers() {
w.conf.flush() w.conf.flush()
log.Info("Disconnected existing server", "server", server) log.Info("Disconnected existing server", "server", server)
w.networkStats(false) w.networkStats()
return return
} }
// If the user requested connecting a new server, do it // If the user requested connecting a new server, do it
if w.makeServer() != "" { if w.makeServer() != "" {
w.networkStats(false) w.networkStats()
} }
} }
@ -174,9 +174,10 @@ func (w *wizard) deployComponent() {
fmt.Println(" 1. Ethstats - Network monitoring tool") fmt.Println(" 1. Ethstats - Network monitoring tool")
fmt.Println(" 2. Bootnode - Entry point of the network") fmt.Println(" 2. Bootnode - Entry point of the network")
fmt.Println(" 3. Sealer - Full node minting new blocks") fmt.Println(" 3. Sealer - Full node minting new blocks")
fmt.Println(" 4. Wallet - Browser wallet for quick sends (todo)") fmt.Println(" 4. Explorer - Chain analysis webservice (ethash only)")
fmt.Println(" 5. Faucet - Crypto faucet to give away funds") fmt.Println(" 5. Wallet - Browser wallet for quick sends")
fmt.Println(" 6. Dashboard - Website listing above web-services") fmt.Println(" 6. Faucet - Crypto faucet to give away funds")
fmt.Println(" 7. Dashboard - Website listing above web-services")
switch w.read() { switch w.read() {
case "1": case "1":
@ -186,9 +187,12 @@ func (w *wizard) deployComponent() {
case "3": case "3":
w.deployNode(false) w.deployNode(false)
case "4": case "4":
w.deployExplorer()
case "5": case "5":
w.deployFaucet() w.deployWallet()
case "6": case "6":
w.deployFaucet()
case "7":
w.deployDashboard() w.deployDashboard()
default: default:
log.Error("That's not something I can do") log.Error("That's not something I can do")

@ -29,7 +29,8 @@ import (
// //
// If the user elects not to use a reverse proxy, an empty hostname is returned! // If the user elects not to use a reverse proxy, an empty hostname is returned!
func (w *wizard) ensureVirtualHost(client *sshClient, port int, def string) (string, error) { func (w *wizard) ensureVirtualHost(client *sshClient, port int, def string) (string, error) {
if proxy, _ := checkNginx(client, w.network); proxy != nil { proxy, _ := checkNginx(client, w.network)
if proxy != nil {
// Reverse proxy is running, if ports match, we need a virtual host // Reverse proxy is running, if ports match, we need a virtual host
if proxy.port == port { if proxy.port == port {
fmt.Println() fmt.Println()
@ -41,7 +42,13 @@ func (w *wizard) ensureVirtualHost(client *sshClient, port int, def string) (str
fmt.Println() fmt.Println()
fmt.Println("Allow sharing the port with other services (y/n)? (default = yes)") fmt.Println("Allow sharing the port with other services (y/n)? (default = yes)")
if w.readDefaultString("y") == "y" { if w.readDefaultString("y") == "y" {
if out, err := deployNginx(client, w.network, port); err != nil { nocache := false
if proxy != nil {
fmt.Println()
fmt.Printf("Should the reverse-proxy be rebuilt from scratch (y/n)? (default = no)\n")
nocache = w.readDefaultString("n") != "n"
}
if out, err := deployNginx(client, w.network, port, nocache); err != nil {
log.Error("Failed to deploy reverse-proxy", "err", err) log.Error("Failed to deploy reverse-proxy", "err", err)
if len(out) > 0 { if len(out) > 0 {
fmt.Printf("%s\n", out) fmt.Printf("%s\n", out)

@ -29,7 +29,7 @@ import (
// deployNode creates a new node configuration based on some user input. // deployNode creates a new node configuration based on some user input.
func (w *wizard) deployNode(boot bool) { func (w *wizard) deployNode(boot bool) {
// Do some sanity check before the user wastes time on input // Do some sanity check before the user wastes time on input
if w.conf.genesis == nil { if w.conf.Genesis == nil {
log.Error("No genesis block configured") log.Error("No genesis block configured")
return return
} }
@ -44,7 +44,7 @@ func (w *wizard) deployNode(boot bool) {
} }
client := w.servers[server] client := w.servers[server]
// Retrieve any active ethstats configurations from the server // Retrieve any active node configurations from the server
infos, err := checkNode(client, w.network, boot) infos, err := checkNode(client, w.network, boot)
if err != nil { if err != nil {
if boot { if boot {
@ -53,8 +53,10 @@ func (w *wizard) deployNode(boot bool) {
infos = &nodeInfos{portFull: 30303, peersTotal: 50, peersLight: 0, gasTarget: 4.7, gasPrice: 18} infos = &nodeInfos{portFull: 30303, peersTotal: 50, peersLight: 0, gasTarget: 4.7, gasPrice: 18}
} }
} }
infos.genesis, _ = json.MarshalIndent(w.conf.genesis, "", " ") existed := err == nil
infos.network = w.conf.genesis.Config.ChainId.Int64()
infos.genesis, _ = json.MarshalIndent(w.conf.Genesis, "", " ")
infos.network = w.conf.Genesis.Config.ChainId.Int64()
// Figure out where the user wants to store the persistent data // Figure out where the user wants to store the persistent data
fmt.Println() fmt.Println()
@ -65,6 +67,16 @@ func (w *wizard) deployNode(boot bool) {
fmt.Printf("Where should data be stored on the remote machine? (default = %s)\n", infos.datadir) fmt.Printf("Where should data be stored on the remote machine? (default = %s)\n", infos.datadir)
infos.datadir = w.readDefaultString(infos.datadir) infos.datadir = w.readDefaultString(infos.datadir)
} }
if w.conf.Genesis.Config.Ethash != nil && !boot {
fmt.Println()
if infos.ethashdir == "" {
fmt.Printf("Where should the ethash mining DAGs be stored on the remote machine?\n")
infos.ethashdir = w.readString()
} else {
fmt.Printf("Where should the ethash mining DAGs be stored on the remote machine? (default = %s)\n", infos.ethashdir)
infos.ethashdir = w.readDefaultString(infos.ethashdir)
}
}
// Figure out which port to listen on // Figure out which port to listen on
fmt.Println() fmt.Println()
fmt.Printf("Which TCP/UDP port to listen on? (default = %d)\n", infos.portFull) fmt.Printf("Which TCP/UDP port to listen on? (default = %d)\n", infos.portFull)
@ -91,7 +103,7 @@ func (w *wizard) deployNode(boot bool) {
} }
// If the node is a miner/signer, load up needed credentials // If the node is a miner/signer, load up needed credentials
if !boot { if !boot {
if w.conf.genesis.Config.Ethash != nil { if w.conf.Genesis.Config.Ethash != nil {
// Ethash based miners only need an etherbase to mine against // Ethash based miners only need an etherbase to mine against
fmt.Println() fmt.Println()
if infos.etherbase == "" { if infos.etherbase == "" {
@ -106,7 +118,7 @@ func (w *wizard) deployNode(boot bool) {
fmt.Printf("What address should the miner user? (default = %s)\n", infos.etherbase) fmt.Printf("What address should the miner user? (default = %s)\n", infos.etherbase)
infos.etherbase = w.readDefaultAddress(common.HexToAddress(infos.etherbase)).Hex() infos.etherbase = w.readDefaultAddress(common.HexToAddress(infos.etherbase)).Hex()
} }
} else if w.conf.genesis.Config.Clique != nil { } else if w.conf.Genesis.Config.Clique != nil {
// If a previous signer was already set, offer to reuse it // If a previous signer was already set, offer to reuse it
if infos.keyJSON != "" { if infos.keyJSON != "" {
if key, err := keystore.DecryptKey([]byte(infos.keyJSON), infos.keyPass); err != nil { if key, err := keystore.DecryptKey([]byte(infos.keyJSON), infos.keyPass); err != nil {
@ -145,7 +157,13 @@ func (w *wizard) deployNode(boot bool) {
infos.gasPrice = w.readDefaultFloat(infos.gasPrice) infos.gasPrice = w.readDefaultFloat(infos.gasPrice)
} }
// Try to deploy the full node on the host // Try to deploy the full node on the host
if out, err := deployNode(client, w.network, w.conf.bootFull, w.conf.bootLight, infos); err != nil { nocache := false
if existed {
fmt.Println()
fmt.Printf("Should the node be built from scratch (y/n)? (default = no)\n")
nocache = w.readDefaultString("n") != "n"
}
if out, err := deployNode(client, w.network, w.conf.bootFull, w.conf.bootLight, infos, nocache); err != nil {
log.Error("Failed to deploy Ethereum node container", "err", err) log.Error("Failed to deploy Ethereum node container", "err", err)
if len(out) > 0 { if len(out) > 0 {
fmt.Printf("%s\n", out) fmt.Printf("%s\n", out)
@ -156,5 +174,5 @@ func (w *wizard) deployNode(boot bool) {
log.Info("Waiting for node to finish booting") log.Info("Waiting for node to finish booting")
time.Sleep(3 * time.Second) time.Sleep(3 * time.Second)
w.networkStats(false) w.networkStats()
} }

@ -0,0 +1,113 @@
// 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"
)
// deployWallet creates a new web wallet based on some user input.
func (w *wizard) deployWallet() {
// 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
}
// 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 := checkWallet(client, w.network)
if err != nil {
infos = &walletInfos{
nodePort: 30303, rpcPort: 8545, webPort: 80, webHost: client.server,
}
}
existed := err == nil
infos.genesis, _ = json.MarshalIndent(w.conf.Genesis, "", " ")
infos.network = w.conf.Genesis.Config.ChainId.Int64()
// Figure out which port to listen on
fmt.Println()
fmt.Printf("Which port should the wallet 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 wallet 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 backing node listen on? (default = %d)\n", infos.nodePort)
infos.nodePort = w.readDefaultInt(infos.nodePort)
fmt.Println()
fmt.Printf("Which port should the backing RPC API listen on? (default = %d)\n", infos.rpcPort)
infos.rpcPort = w.readDefaultInt(infos.rpcPort)
// Set a proper name to report on the stats page
fmt.Println()
if infos.ethstats == "" {
fmt.Printf("What should the wallet be called on the stats page?\n")
infos.ethstats = w.readString() + ":" + w.conf.ethstats
} else {
fmt.Printf("What should the wallet be called on the stats page? (default = %s)\n", infos.ethstats)
infos.ethstats = w.readDefaultString(infos.ethstats) + ":" + w.conf.ethstats
}
// Try to deploy the wallet on the host
nocache := false
if existed {
fmt.Println()
fmt.Printf("Should the wallet be built from scratch (y/n)? (default = no)\n")
nocache = w.readDefaultString("n") != "n"
}
if out, err := deployWallet(client, w.network, w.conf.bootFull, infos, nocache); err != nil {
log.Error("Failed to deploy wallet 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()
}

@ -36,8 +36,8 @@ import (
// Ethash proof-of-work protocol constants. // Ethash proof-of-work protocol constants.
var ( var (
frontierBlockReward *big.Int = big.NewInt(5e+18) // Block reward in wei for successfully mining a block FrontierBlockReward *big.Int = big.NewInt(5e+18) // Block reward in wei for successfully mining a block
byzantiumBlockReward *big.Int = big.NewInt(3e+18) // Block reward in wei for successfully mining a block upward from Byzantium ByzantiumBlockReward *big.Int = big.NewInt(3e+18) // Block reward in wei for successfully mining a block upward from Byzantium
maxUncles = 2 // Maximum number of uncles allowed in a single block maxUncles = 2 // Maximum number of uncles allowed in a single block
) )
@ -529,9 +529,9 @@ var (
// TODO (karalabe): Move the chain maker into this package and make this private! // TODO (karalabe): Move the chain maker into this package and make this private!
func AccumulateRewards(config *params.ChainConfig, state *state.StateDB, header *types.Header, uncles []*types.Header) { func AccumulateRewards(config *params.ChainConfig, state *state.StateDB, header *types.Header, uncles []*types.Header) {
// Select the correct block reward based on chain progression // Select the correct block reward based on chain progression
blockReward := frontierBlockReward blockReward := FrontierBlockReward
if config.IsByzantium(header.Number) { if config.IsByzantium(header.Number) {
blockReward = byzantiumBlockReward blockReward = ByzantiumBlockReward
} }
// Accumulate the rewards for the miner and any included uncles // Accumulate the rewards for the miner and any included uncles
reward := new(big.Int).Set(blockReward) reward := new(big.Int).Set(blockReward)