From 8c78449a9ef8f2a77cc1ff94f9a0a3178af21408 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9ter=20Szil=C3=A1gyi?= Date: Thu, 19 Oct 2017 13:59:02 +0300 Subject: [PATCH 01/15] cmd/puppeth: reorganize stats reports to make it readable --- cmd/puppeth/module_dashboard.go | 11 +- cmd/puppeth/module_ethstats.go | 13 +- cmd/puppeth/module_faucet.go | 30 ++++- cmd/puppeth/module_nginx.go | 10 +- cmd/puppeth/module_node.go | 37 +++++- cmd/puppeth/puppeth.go | 2 +- cmd/puppeth/wizard_dashboard.go | 2 +- cmd/puppeth/wizard_ethstats.go | 2 +- cmd/puppeth/wizard_faucet.go | 2 +- cmd/puppeth/wizard_intro.go | 10 +- cmd/puppeth/wizard_netstats.go | 216 +++++++++++++++++--------------- cmd/puppeth/wizard_network.go | 4 +- cmd/puppeth/wizard_node.go | 2 +- 13 files changed, 205 insertions(+), 136 deletions(-) diff --git a/cmd/puppeth/module_dashboard.go b/cmd/puppeth/module_dashboard.go index 1cf6cab799..7d01f6f0a5 100644 --- a/cmd/puppeth/module_dashboard.go +++ b/cmd/puppeth/module_dashboard.go @@ -22,6 +22,7 @@ import ( "html/template" "math/rand" "path/filepath" + "strconv" "strings" "github.com/ethereum/go-ethereum/log" @@ -499,9 +500,13 @@ type dashboardInfos struct { port int } -// String implements the stringer interface. -func (info *dashboardInfos) String() string { - return fmt.Sprintf("host=%s, port=%d", info.host, info.port) +// Report converts the typed struct into a plain string->string map, cotnaining +// most - but not all - fields for reporting to the user. +func (info *dashboardInfos) Report() map[string]string { + return map[string]string{ + "Website address": info.host, + "Website listener port": strconv.Itoa(info.port), + } } // checkDashboard does a health-check against a dashboard container to verify if diff --git a/cmd/puppeth/module_ethstats.go b/cmd/puppeth/module_ethstats.go index 6ce662f65f..2e83e366eb 100644 --- a/cmd/puppeth/module_ethstats.go +++ b/cmd/puppeth/module_ethstats.go @@ -21,6 +21,7 @@ import ( "fmt" "math/rand" "path/filepath" + "strconv" "strings" "text/template" @@ -123,9 +124,15 @@ type ethstatsInfos struct { banned []string } -// String implements the stringer interface. -func (info *ethstatsInfos) String() string { - return fmt.Sprintf("host=%s, port=%d, secret=%s, banned=%v", info.host, info.port, info.secret, info.banned) +// Report converts the typed struct into a plain string->string map, cotnaining +// most - but not all - fields for reporting to the user. +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 diff --git a/cmd/puppeth/module_faucet.go b/cmd/puppeth/module_faucet.go index 3c1296bddb..238aa115fd 100644 --- a/cmd/puppeth/module_faucet.go +++ b/cmd/puppeth/module_faucet.go @@ -18,6 +18,7 @@ package main import ( "bytes" + "encoding/json" "fmt" "html/template" "math/rand" @@ -25,6 +26,7 @@ import ( "strconv" "strings" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/log" ) @@ -162,9 +164,31 @@ type faucetInfos struct { captchaSecret string } -// String implements the stringer interface. -func (info *faucetInfos) String() string { - 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) +// Report converts the typed struct into a plain string->string map, cotnaining +// most - but not all - fields for reporting to the user. +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, + "GitHub authentication": info.githubUser, + } + 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 diff --git a/cmd/puppeth/module_nginx.go b/cmd/puppeth/module_nginx.go index fd6d1d74ed..67084c80ae 100644 --- a/cmd/puppeth/module_nginx.go +++ b/cmd/puppeth/module_nginx.go @@ -22,6 +22,7 @@ import ( "html/template" "math/rand" "path/filepath" + "strconv" "github.com/ethereum/go-ethereum/log" ) @@ -88,9 +89,12 @@ type nginxInfos struct { port int } -// String implements the stringer interface. -func (info *nginxInfos) String() string { - return fmt.Sprintf("port=%d", info.port) +// Report converts the typed struct into a plain string->string map, cotnaining +// most - but not all - fields for reporting to the user. +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 diff --git a/cmd/puppeth/module_node.go b/cmd/puppeth/module_node.go index 375e3e6463..ad50cd80a5 100644 --- a/cmd/puppeth/module_node.go +++ b/cmd/puppeth/module_node.go @@ -18,6 +18,7 @@ package main import ( "bytes" + "encoding/json" "fmt" "math/rand" "path/filepath" @@ -25,6 +26,7 @@ import ( "strings" "text/template" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/log" ) @@ -164,14 +166,37 @@ type nodeInfos struct { gasPrice float64 } -// String implements the stringer interface. -func (info *nodeInfos) String() string { - discv5 := "" +// Report converts the typed struct into a plain string->string map, cotnaining +// most - but not all - fields for reporting to the user. +func (info *nodeInfos) Report() map[string]string { + report := map[string]string{ + "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, + } if info.peersLight > 0 { - discv5 = fmt.Sprintf(", portv5=%d", info.portLight) + report["Listener port (light nodes)"] = strconv.Itoa(info.portLight) + } + if info.gasTarget > 0 { + 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 != "" { + report["Miner account"] = info.etherbase + } + if info.keyJSON != "" { + 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 fmt.Sprintf("port=%d%s, datadir=%s, peers=%d, lights=%d, ethstats=%s, gastarget=%0.3f MGas, gasprice=%0.3f GWei", - info.portFull, discv5, info.datadir, info.peersTotal, info.peersLight, info.ethstats, info.gasTarget, info.gasPrice) + return report } // checkNode does a health-check against an boot or seal node server to verify diff --git a/cmd/puppeth/puppeth.go b/cmd/puppeth/puppeth.go index f783a7981a..26382dac13 100644 --- a/cmd/puppeth/puppeth.go +++ b/cmd/puppeth/puppeth.go @@ -38,7 +38,7 @@ func main() { }, cli.IntFlag{ Name: "loglevel", - Value: 4, + Value: 3, Usage: "log level to emit to the screen", }, } diff --git a/cmd/puppeth/wizard_dashboard.go b/cmd/puppeth/wizard_dashboard.go index 53a28a5350..3f68c93a4b 100644 --- a/cmd/puppeth/wizard_dashboard.go +++ b/cmd/puppeth/wizard_dashboard.go @@ -128,5 +128,5 @@ func (w *wizard) deployDashboard() { return } // All ok, run a network scan to pick any changes up - w.networkStats(false) + w.networkStats() } diff --git a/cmd/puppeth/wizard_ethstats.go b/cmd/puppeth/wizard_ethstats.go index 8bfa1d6e52..ff75a9d5da 100644 --- a/cmd/puppeth/wizard_ethstats.go +++ b/cmd/puppeth/wizard_ethstats.go @@ -112,5 +112,5 @@ func (w *wizard) deployEthstats() { return } // All ok, run a network scan to pick any changes up - w.networkStats(false) + w.networkStats() } diff --git a/cmd/puppeth/wizard_faucet.go b/cmd/puppeth/wizard_faucet.go index 51c4e2f7f4..08e471ef86 100644 --- a/cmd/puppeth/wizard_faucet.go +++ b/cmd/puppeth/wizard_faucet.go @@ -198,5 +198,5 @@ func (w *wizard) deployFaucet() { return } // All ok, run a network scan to pick any changes up - w.networkStats(false) + w.networkStats() } diff --git a/cmd/puppeth/wizard_intro.go b/cmd/puppeth/wizard_intro.go index 2d9a097ee7..a5fea6f85b 100644 --- a/cmd/puppeth/wizard_intro.go +++ b/cmd/puppeth/wizard_intro.go @@ -88,7 +88,7 @@ func (w *wizard) run() { } w.servers[server] = client } - w.networkStats(false) + w.networkStats() } // Basics done, loop ad infinitum about what to do for { @@ -110,12 +110,11 @@ func (w *wizard) run() { } else { fmt.Println(" 4. Manage network components") } - //fmt.Println(" 5. ProTips for common usecases") choice := w.read() switch { case choice == "" || choice == "1": - w.networkStats(false) + w.networkStats() case choice == "2": if w.conf.genesis == nil { @@ -126,7 +125,7 @@ func (w *wizard) run() { case choice == "3": if len(w.servers) == 0 { if w.makeServer() != "" { - w.networkStats(false) + w.networkStats() } } else { w.manageServers() @@ -138,9 +137,6 @@ func (w *wizard) run() { w.manageComponents() } - case choice == "5": - w.networkStats(true) - default: log.Error("That's not something I can do") } diff --git a/cmd/puppeth/wizard_netstats.go b/cmd/puppeth/wizard_netstats.go index c069721982..7d8e842423 100644 --- a/cmd/puppeth/wizard_netstats.go +++ b/cmd/puppeth/wizard_netstats.go @@ -18,8 +18,8 @@ package main import ( "encoding/json" - "fmt" "os" + "sort" "strings" "github.com/ethereum/go-ethereum/core" @@ -29,7 +29,7 @@ import ( // networkStats verifies the status of network components and generates a protip // 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 { log.Error("No remote machines to gather stats from") return @@ -37,51 +37,53 @@ func (w *wizard) networkStats(tips bool) { protips := new(protips) // Iterate over all the specified hosts and check their status - stats := tablewriter.NewWriter(os.Stdout) - stats.SetHeader([]string{"Server", "IP", "Status", "Service", "Details"}) - stats.SetColWidth(100) + stats := make(serverStats) for server, pubkey := range w.conf.Servers { client := w.servers[server] logger := log.New("server", server) 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), + } + stats[client.server] = stat + if client == nil { conn, err := dial(server, pubkey) if err != nil { logger.Error("Failed to establish remote connection", "err", err) - stats.Append([]string{server, "", err.Error(), "", ""}) + stat.failure = err.Error() continue } client = conn } // Client connected one way or another, run health-checks - services := make(map[string]string) logger.Debug("Checking for nginx availability") if infos, err := checkNginx(client, w.network); err != nil { if err != ErrServiceUnknown { - services["nginx"] = err.Error() + stat.services["nginx"] = map[string]string{"offline": err.Error()} } } else { - services["nginx"] = infos.String() + stat.services["nginx"] = infos.Report() } logger.Debug("Checking for ethstats availability") if infos, err := checkEthstats(client, w.network); err != nil { if err != ErrServiceUnknown { - services["ethstats"] = err.Error() + stat.services["ethstats"] = map[string]string{"offline": err.Error()} } } else { - services["ethstats"] = infos.String() + stat.services["ethstats"] = infos.Report() protips.ethstats = infos.config } logger.Debug("Checking for bootnode availability") if infos, err := checkNode(client, w.network, true); err != nil { if err != ErrServiceUnknown { - services["bootnode"] = err.Error() + stat.services["bootnode"] = map[string]string{"offline": err.Error()} } } else { - services["bootnode"] = infos.String() + stat.services["bootnode"] = infos.Report() protips.genesis = string(infos.genesis) protips.bootFull = append(protips.bootFull, infos.enodeFull) @@ -92,41 +94,33 @@ func (w *wizard) networkStats(tips bool) { logger.Debug("Checking for sealnode availability") if infos, err := checkNode(client, w.network, false); err != nil { if err != ErrServiceUnknown { - services["sealnode"] = err.Error() + stat.services["sealnode"] = map[string]string{"offline": err.Error()} } } else { - services["sealnode"] = infos.String() + stat.services["sealnode"] = infos.Report() protips.genesis = string(infos.genesis) } logger.Debug("Checking for faucet availability") if infos, err := checkFaucet(client, w.network); err != nil { if err != ErrServiceUnknown { - services["faucet"] = err.Error() + stat.services["faucet"] = map[string]string{"offline": err.Error()} } } else { - services["faucet"] = infos.String() + stat.services["faucet"] = infos.Report() } logger.Debug("Checking for dashboard availability") if infos, err := checkDashboard(client, w.network); err != nil { if err != ErrServiceUnknown { - services["dashboard"] = err.Error() + stat.services["dashboard"] = map[string]string{"offline": err.Error()} } } else { - services["dashboard"] = infos.String() + stat.services["dashboard"] = infos.Report() } // All status checks complete, report and check next server delete(w.services, server) - for service := range services { + for service := range stat.services { w.services[server] = append(w.services[server], service) } - server, address := client.server, client.address - for service, status := range services { - 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 { @@ -145,91 +139,105 @@ func (w *wizard) networkStats(tips bool) { w.conf.bootLight = protips.bootLight // Print any collected stats and return - if !tips { - stats.Render() - } else { - protips.print(w.network) - } + stats.render() } -// protips contains a collection of network infos to report pro-tips -// based on. -type protips struct { - genesis string - network int64 - bootFull []string - bootLight []string - ethstats string +// serverStat is a collection of service configuration parameters and health +// check reports to print to the user. +type serverStat struct { + address string + failure string + services map[string]map[string]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)) +// serverStats is a collection of server stats for multiple hosts. +type serverStats map[string]*serverStat - 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)) +// 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) - 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)) + table.SetHeader([]string{"Server", "Address", "Service", "Config", "Value"}) + table.SetAlignment(tablewriter.ALIGN_LEFT) + table.SetColWidth(100) - 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 + // 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)) + } + } } } - fmt.Println() - if short { - howto := tablewriter.NewWriter(os.Stdout) - howto.SetHeader([]string{"Fun tasks for you", "Tips on how to"}) - howto.SetColWidth(100) + // 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 := 0; i < len(tasks); i++ { - howto.Append([]string{tasks[i], tips[i]}) + 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) + + 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]}) + } + } } - 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() } + table.Render() +} + +// protips contains a collection of network infos to report pro-tips +// based on. +type protips struct { + genesis string + network int64 + bootFull []string + bootLight []string + ethstats string } diff --git a/cmd/puppeth/wizard_network.go b/cmd/puppeth/wizard_network.go index c20e31fab3..bf8248e4b1 100644 --- a/cmd/puppeth/wizard_network.go +++ b/cmd/puppeth/wizard_network.go @@ -53,12 +53,12 @@ func (w *wizard) manageServers() { w.conf.flush() log.Info("Disconnected existing server", "server", server) - w.networkStats(false) + w.networkStats() return } // If the user requested connecting a new server, do it if w.makeServer() != "" { - w.networkStats(false) + w.networkStats() } } diff --git a/cmd/puppeth/wizard_node.go b/cmd/puppeth/wizard_node.go index 05232486b6..69d1715a4c 100644 --- a/cmd/puppeth/wizard_node.go +++ b/cmd/puppeth/wizard_node.go @@ -156,5 +156,5 @@ func (w *wizard) deployNode(boot bool) { log.Info("Waiting for node to finish booting") time.Sleep(3 * time.Second) - w.networkStats(false) + w.networkStats() } From 7b258c96816df56e642df7e314e8052213af70fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9ter=20Szil=C3=A1gyi?= Date: Thu, 19 Oct 2017 14:40:43 +0300 Subject: [PATCH 02/15] cmd/puppeth: concurrent server dials and health checks --- cmd/puppeth/wizard.go | 4 +- cmd/puppeth/wizard_intro.go | 24 +++- cmd/puppeth/wizard_netstats.go | 207 +++++++++++++++++++-------------- 3 files changed, 142 insertions(+), 93 deletions(-) diff --git a/cmd/puppeth/wizard.go b/cmd/puppeth/wizard.go index eb6d9e5aae..e554dbd135 100644 --- a/cmd/puppeth/wizard.go +++ b/cmd/puppeth/wizard.go @@ -28,6 +28,7 @@ import ( "sort" "strconv" "strings" + "sync" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core" @@ -75,7 +76,8 @@ type wizard struct { servers map[string]*sshClient // SSH connections to servers to administer 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. diff --git a/cmd/puppeth/wizard_intro.go b/cmd/puppeth/wizard_intro.go index a5fea6f85b..005ee47a5f 100644 --- a/cmd/puppeth/wizard_intro.go +++ b/cmd/puppeth/wizard_intro.go @@ -24,6 +24,7 @@ import ( "os" "path/filepath" "strings" + "sync" "github.com/ethereum/go-ethereum/log" ) @@ -80,14 +81,25 @@ func (w *wizard) run() { } else if err := json.Unmarshal(blob, &w.conf); err != nil { log.Crit("Previous configuration corrupted", "path", w.conf.path, "err", err) } else { + // Dial all previously known servers concurrently + var pend sync.WaitGroup for server, pubkey := range w.conf.Servers { - log.Info("Dialing previously configured server", "server", server) - client, err := dial(server, pubkey) - if err != nil { - log.Error("Previous server unreachable", "server", server, "err", err) - } - w.servers[server] = client + pend.Add(1) + + go func(server string, pubkey []byte) { + defer pend.Done() + + log.Info("Dialing previously configured server", "server", server) + client, err := dial(server, pubkey) + if err != nil { + log.Error("Previous server unreachable", "server", server, "err", err) + } + w.lock.Lock() + w.servers[server] = client + w.lock.Unlock() + }(server, pubkey) } + pend.Wait() w.networkStats() } // Basics done, loop ad infinitum about what to do diff --git a/cmd/puppeth/wizard_netstats.go b/cmd/puppeth/wizard_netstats.go index 7d8e842423..906dfeda75 100644 --- a/cmd/puppeth/wizard_netstats.go +++ b/cmd/puppeth/wizard_netstats.go @@ -21,6 +21,7 @@ import ( "os" "sort" "strings" + "sync" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/log" @@ -34,112 +35,143 @@ func (w *wizard) networkStats() { log.Error("No remote machines to gather stats from") 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 - stats := make(serverStats) + var pend sync.WaitGroup + stats := make(serverStats) for server, pubkey := range w.conf.Servers { - client := w.servers[server] - logger := log.New("server", server) - logger.Info("Starting remote server health-check") - - stat := &serverStat{ - address: client.address, - services: make(map[string]map[string]string), - } - stats[client.server] = stat - - if client == nil { - conn, err := dial(server, pubkey) - if err != nil { - logger.Error("Failed to establish remote connection", "err", err) - stat.failure = err.Error() - continue + 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) } - client = conn + 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.Info("Starting remote server health-check") + + stat := &serverStat{ + address: client.address, + services: make(map[string]map[string]string), + } + if client == nil { + conn, err := dial(server, pubkey) + if err != nil { + logger.Error("Failed to establish remote connection", "err", err) + stat.failure = err.Error() + return stat } - // Client connected one way or another, run health-checks - logger.Debug("Checking for nginx availability") - if infos, err := checkNginx(client, w.network); err != nil { - if err != ErrServiceUnknown { - stat.services["nginx"] = map[string]string{"offline": err.Error()} - } - } else { - stat.services["nginx"] = infos.Report() + client = conn + } + // Client connected one way or another, run health-checks + logger.Debug("Checking for nginx availability") + if infos, err := checkNginx(client, w.network); err != nil { + if err != ErrServiceUnknown { + stat.services["nginx"] = map[string]string{"offline": err.Error()} } - logger.Debug("Checking for ethstats availability") - if infos, err := checkEthstats(client, w.network); err != nil { - if err != ErrServiceUnknown { - stat.services["ethstats"] = map[string]string{"offline": err.Error()} - } - } else { - stat.services["ethstats"] = infos.Report() - protips.ethstats = infos.config + } else { + stat.services["nginx"] = infos.Report() + } + logger.Debug("Checking for ethstats availability") + if infos, err := checkEthstats(client, w.network); err != nil { + if err != ErrServiceUnknown { + stat.services["ethstats"] = map[string]string{"offline": err.Error()} } - logger.Debug("Checking for bootnode availability") - if infos, err := checkNode(client, w.network, true); err != nil { - if err != ErrServiceUnknown { - stat.services["bootnode"] = map[string]string{"offline": err.Error()} - } - } else { - stat.services["bootnode"] = infos.Report() - - protips.genesis = string(infos.genesis) - protips.bootFull = append(protips.bootFull, infos.enodeFull) - if infos.enodeLight != "" { - protips.bootLight = append(protips.bootLight, infos.enodeLight) - } + } else { + stat.services["ethstats"] = infos.Report() + ethstats = infos.config + } + logger.Debug("Checking for bootnode availability") + if infos, err := checkNode(client, w.network, true); err != nil { + if err != ErrServiceUnknown { + stat.services["bootnode"] = map[string]string{"offline": err.Error()} } - logger.Debug("Checking for sealnode availability") - if infos, err := checkNode(client, w.network, false); err != nil { - if err != ErrServiceUnknown { - stat.services["sealnode"] = map[string]string{"offline": err.Error()} - } - } else { - stat.services["sealnode"] = infos.Report() - protips.genesis = string(infos.genesis) + } else { + stat.services["bootnode"] = infos.Report() + + genesis = string(infos.genesis) + bootFull = append(bootFull, infos.enodeFull) + if infos.enodeLight != "" { + bootLight = append(bootLight, infos.enodeLight) } - logger.Debug("Checking for faucet availability") - if infos, err := checkFaucet(client, w.network); err != nil { - if err != ErrServiceUnknown { - stat.services["faucet"] = map[string]string{"offline": err.Error()} - } - } else { - stat.services["faucet"] = infos.Report() + } + logger.Debug("Checking for sealnode availability") + if infos, err := checkNode(client, w.network, false); err != nil { + if err != ErrServiceUnknown { + stat.services["sealnode"] = map[string]string{"offline": err.Error()} } - logger.Debug("Checking for dashboard availability") - if infos, err := checkDashboard(client, w.network); err != nil { - if err != ErrServiceUnknown { - stat.services["dashboard"] = map[string]string{"offline": err.Error()} - } - } else { - stat.services["dashboard"] = infos.Report() + } else { + stat.services["sealnode"] = infos.Report() + genesis = string(infos.genesis) + } + logger.Debug("Checking for faucet availability") + if infos, err := checkFaucet(client, w.network); err != nil { + if err != ErrServiceUnknown { + stat.services["faucet"] = map[string]string{"offline": err.Error()} } - // All status checks complete, report and check next server - delete(w.services, server) - for service := range stat.services { - w.services[server] = append(w.services[server], service) + } else { + stat.services["faucet"] = infos.Report() + } + logger.Debug("Checking for dashboard availability") + if infos, err := checkDashboard(client, w.network); err != nil { + if err != ErrServiceUnknown { + stat.services["dashboard"] = map[string]string{"offline": err.Error()} } + } else { + stat.services["dashboard"] = infos.Report() } - // 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 { + // Feed and newly discovered information into the wizard + w.lock.Lock() + defer w.lock.Unlock() + + if genesis != "" && w.conf.genesis == nil { + g := new(core.Genesis) + if err := json.Unmarshal([]byte(genesis), g); err != nil { log.Error("Failed to parse remote genesis", "err", err) } else { - w.conf.genesis = genesis - protips.network = genesis.Config.ChainId.Int64() + w.conf.genesis = g } } - if protips.ethstats != "" { - w.conf.ethstats = protips.ethstats + if ethstats != "" { + w.conf.ethstats = ethstats } - w.conf.bootFull = protips.bootFull - w.conf.bootLight = protips.bootLight + w.conf.bootFull = append(w.conf.bootFull, bootFull...) + w.conf.bootLight = append(w.conf.bootLight, bootLight...) - // Print any collected stats and return - stats.render() + return stat } // serverStat is a collection of service configuration parameters and health @@ -205,6 +237,9 @@ func (stats serverStats) render() { } 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 { From da3b9f831e6bb8f8a3c589e5cd8426fd9da72eea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9ter=20Szil=C3=A1gyi?= Date: Thu, 19 Oct 2017 16:00:55 +0300 Subject: [PATCH 03/15] cmd/puppeth: support deploying services with forced rebuilds --- cmd/puppeth/module_dashboard.go | 7 +++++-- cmd/puppeth/module_ethstats.go | 7 +++++-- cmd/puppeth/module_faucet.go | 7 +++++-- cmd/puppeth/module_nginx.go | 9 ++++++--- cmd/puppeth/module_node.go | 7 +++++-- cmd/puppeth/wizard_dashboard.go | 6 +++++- cmd/puppeth/wizard_ethstats.go | 6 +++++- cmd/puppeth/wizard_faucet.go | 6 +++++- cmd/puppeth/wizard_nginx.go | 6 +++++- cmd/puppeth/wizard_node.go | 8 ++++++-- 10 files changed, 52 insertions(+), 17 deletions(-) diff --git a/cmd/puppeth/module_dashboard.go b/cmd/puppeth/module_dashboard.go index 7d01f6f0a5..b08dbbff12 100644 --- a/cmd/puppeth/module_dashboard.go +++ b/cmd/puppeth/module_dashboard.go @@ -437,7 +437,7 @@ services: // deployDashboard deploys a new dashboard 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 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, port int, vhost string, services map[string]string, conf *config, ethstats bool, nocache bool) ([]byte, error) { // Generate the content to upload to the server workdir := fmt.Sprintf("%d", rand.Int63()) files := make(map[string][]byte) @@ -490,7 +490,10 @@ func deployDashboard(client *sshClient, network string, port int, vhost string, defer client.Run("rm -rf " + workdir) // 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 diff --git a/cmd/puppeth/module_ethstats.go b/cmd/puppeth/module_ethstats.go index 2e83e366eb..7ce3ca3cdf 100644 --- a/cmd/puppeth/module_ethstats.go +++ b/cmd/puppeth/module_ethstats.go @@ -73,7 +73,7 @@ services: // deployEthstats deploys a new ethstats 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 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 workdir := fmt.Sprintf("%d", rand.Int63()) files := make(map[string][]byte) @@ -111,7 +111,10 @@ func deployEthstats(client *sshClient, network string, port int, secret string, defer client.Run("rm -rf " + workdir) // 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 diff --git a/cmd/puppeth/module_faucet.go b/cmd/puppeth/module_faucet.go index 238aa115fd..a53e6f61e8 100644 --- a/cmd/puppeth/module_faucet.go +++ b/cmd/puppeth/module_faucet.go @@ -95,7 +95,7 @@ services: // deployFaucet deploys a new faucet 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 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 workdir := fmt.Sprintf("%d", rand.Int63()) files := make(map[string][]byte) @@ -146,7 +146,10 @@ func deployFaucet(client *sshClient, network string, bootnodes []string, config defer client.Run("rm -rf " + workdir) // 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 diff --git a/cmd/puppeth/module_nginx.go b/cmd/puppeth/module_nginx.go index 67084c80ae..ade0e4963c 100644 --- a/cmd/puppeth/module_nginx.go +++ b/cmd/puppeth/module_nginx.go @@ -55,7 +55,7 @@ services: // 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 // 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) // Generate the content to upload to the server @@ -79,8 +79,11 @@ func deployNginx(client *sshClient, network string, port int) ([]byte, error) { } defer client.Run("rm -rf " + workdir) - // Build and deploy the ethstats service - return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s up -d --build", workdir, network)) + // Build and deploy the reverse-proxy 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)) } // nginxInfos is returned from an nginx reverse-proxy status check to allow diff --git a/cmd/puppeth/module_node.go b/cmd/puppeth/module_node.go index ad50cd80a5..17e8a1a995 100644 --- a/cmd/puppeth/module_node.go +++ b/cmd/puppeth/module_node.go @@ -81,7 +81,7 @@ services: // 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 // 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" if config.keyJSON == "" && config.etherbase == "" { kind = "bootnode" @@ -143,7 +143,10 @@ func deployNode(client *sshClient, network string, bootv4, bootv5 []string, conf defer client.Run("rm -rf " + workdir) // 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 diff --git a/cmd/puppeth/wizard_dashboard.go b/cmd/puppeth/wizard_dashboard.go index 3f68c93a4b..b59489b038 100644 --- a/cmd/puppeth/wizard_dashboard.go +++ b/cmd/puppeth/wizard_dashboard.go @@ -120,7 +120,11 @@ func (w *wizard) deployDashboard() { ethstats = w.readDefaultString("y") == "y" } // 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 { + 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, infos.port, infos.host, listing, &w.conf, ethstats, nocache); err != nil { log.Error("Failed to deploy dashboard container", "err", err) if len(out) > 0 { fmt.Printf("%s\n", out) diff --git a/cmd/puppeth/wizard_ethstats.go b/cmd/puppeth/wizard_ethstats.go index ff75a9d5da..1bde5a3fdb 100644 --- a/cmd/puppeth/wizard_ethstats.go +++ b/cmd/puppeth/wizard_ethstats.go @@ -98,13 +98,17 @@ func (w *wizard) deployEthstats() { sort.Strings(infos.banned) } // Try to deploy the ethstats server on the host + 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)) for _, client := range w.servers { if client != nil { 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) if len(out) > 0 { fmt.Printf("%s\n", out) diff --git a/cmd/puppeth/wizard_faucet.go b/cmd/puppeth/wizard_faucet.go index 08e471ef86..e9d5c6016b 100644 --- a/cmd/puppeth/wizard_faucet.go +++ b/cmd/puppeth/wizard_faucet.go @@ -190,7 +190,11 @@ func (w *wizard) deployFaucet() { } } // Try to deploy the faucet server on the host - if out, err := deployFaucet(client, w.network, w.conf.bootLight, infos); err != nil { + 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) if len(out) > 0 { fmt.Printf("%s\n", out) diff --git a/cmd/puppeth/wizard_nginx.go b/cmd/puppeth/wizard_nginx.go index 86fba29f59..919ab270ba 100644 --- a/cmd/puppeth/wizard_nginx.go +++ b/cmd/puppeth/wizard_nginx.go @@ -41,7 +41,11 @@ func (w *wizard) ensureVirtualHost(client *sshClient, port int, def string) (str fmt.Println() fmt.Println("Allow sharing the port with other services (y/n)? (default = yes)") if w.readDefaultString("y") == "y" { - if out, err := deployNginx(client, w.network, port); err != 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) if len(out) > 0 { fmt.Printf("%s\n", out) diff --git a/cmd/puppeth/wizard_node.go b/cmd/puppeth/wizard_node.go index 69d1715a4c..023da8e1e6 100644 --- a/cmd/puppeth/wizard_node.go +++ b/cmd/puppeth/wizard_node.go @@ -44,7 +44,7 @@ func (w *wizard) deployNode(boot bool) { } 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) if err != nil { if boot { @@ -145,7 +145,11 @@ func (w *wizard) deployNode(boot bool) { infos.gasPrice = w.readDefaultFloat(infos.gasPrice) } // 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 { + 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) if len(out) > 0 { fmt.Printf("%s\n", out) From 9e095251b71255ff346ee9300df8754eb6b64903 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9ter=20Szil=C3=A1gyi?= Date: Thu, 19 Oct 2017 17:50:34 +0300 Subject: [PATCH 04/15] cmd/puppeth: mount ethash dir from the host to cache DAGs --- cmd/puppeth/module_node.go | 36 +++++++++++++++++++++++------------- cmd/puppeth/wizard_node.go | 10 ++++++++++ 2 files changed, 33 insertions(+), 13 deletions(-) diff --git a/cmd/puppeth/module_node.go b/cmd/puppeth/module_node.go index 17e8a1a995..37da770aa8 100644 --- a/cmd/puppeth/module_node.go +++ b/cmd/puppeth/module_node.go @@ -42,7 +42,7 @@ ADD genesis.json /genesis.json RUN \ echo 'geth init /genesis.json' > geth.sh && \{{if .Unlock}} echo 'mkdir -p /root/.ethereum/keystore/ && cp /signer.json /root/.ethereum/keystore/' >> geth.sh && \{{end}} - echo $'geth --networkid {{.NetworkID}} --cache 512 --port {{.Port}} --maxpeers {{.Peers}} {{.LightFlag}} --ethstats \'{{.Ethstats}}\' {{if .BootV4}}--bootnodesv4 {{.BootV4}}{{end}} {{if .BootV5}}--bootnodesv5 {{.BootV5}}{{end}} {{if .Etherbase}}--etherbase {{.Etherbase}} --mine{{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"] ` @@ -60,7 +60,8 @@ services: - "{{.FullPort}}:{{.FullPort}}/udp"{{if .Light}} - "{{.LightPort}}:{{.LightPort}}/udp"{{end}} volumes: - - {{.Datadir}}:/root/.ethereum + - {{.Datadir}}:/root/.ethereum{{if .Ethashdir}} + - {{.Ethashdir}}:/root/.ethash{{end}} environment: - FULL_PORT={{.FullPort}}/tcp - LIGHT_PORT={{.LightPort}}/udp @@ -116,6 +117,7 @@ func deployNode(client *sshClient, network string, bootv4, bootv5 []string, conf template.Must(template.New("").Parse(nodeComposefile)).Execute(composefile, map[string]interface{}{ "Type": kind, "Datadir": config.datadir, + "Ethashdir": config.ethashdir, "Network": network, "FullPort": config.portFull, "TotalPeers": config.peersTotal, @@ -155,6 +157,7 @@ type nodeInfos struct { genesis []byte network int64 datadir string + ethashdir string ethstats string portFull int portLight int @@ -180,23 +183,29 @@ func (info *nodeInfos) Report() map[string]string { "Ethstats username": info.ethstats, } if info.peersLight > 0 { + // 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 != "" { - report["Miner account"] = info.etherbase - } - if info.keyJSON != "" { - var key struct { - Address string `json:"address"` + + if info.etherbase != "" { + // Ethash proof-of-work miner + report["Ethash directory"] = info.ethashdir + report["Miner account"] = info.etherbase } - 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) + 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 @@ -251,6 +260,7 @@ func checkNode(client *sshClient, network string, boot bool) (*nodeInfos, error) stats := &nodeInfos{ genesis: genesis, datadir: infos.volumes["/root/.ethereum"], + ethashdir: infos.volumes["/root/.ethash"], portFull: infos.portmap[infos.envvars["FULL_PORT"]], portLight: infos.portmap[infos.envvars["LIGHT_PORT"]], peersTotal: totalPeers, diff --git a/cmd/puppeth/wizard_node.go b/cmd/puppeth/wizard_node.go index 023da8e1e6..f1b4619b5c 100644 --- a/cmd/puppeth/wizard_node.go +++ b/cmd/puppeth/wizard_node.go @@ -65,6 +65,16 @@ func (w *wizard) deployNode(boot bool) { fmt.Printf("Where should data be stored on the remote machine? (default = %s)\n", infos.datadir) infos.datadir = w.readDefaultString(infos.datadir) } + if w.conf.genesis.Config.Ethash != nil { + 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 fmt.Println() fmt.Printf("Which TCP/UDP port to listen on? (default = %d)\n", infos.portFull) From 1e0c336d293367bb75df494a685cabb2029f318e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9ter=20Szil=C3=A1gyi?= Date: Fri, 20 Oct 2017 11:14:10 +0300 Subject: [PATCH 05/15] cmd/puppeth: etherchain light block explorer for PoW nets --- cmd/puppeth/genesis.go | 208 ++++++++++++++++++++++++++++++ cmd/puppeth/module_ethstats.go | 6 +- cmd/puppeth/module_explorer.go | 226 +++++++++++++++++++++++++++++++++ cmd/puppeth/module_node.go | 4 +- cmd/puppeth/wizard_explorer.go | 111 ++++++++++++++++ cmd/puppeth/wizard_faucet.go | 2 +- cmd/puppeth/wizard_netstats.go | 8 ++ cmd/puppeth/wizard_network.go | 11 +- consensus/ethash/consensus.go | 8 +- 9 files changed, 569 insertions(+), 15 deletions(-) create mode 100644 cmd/puppeth/genesis.go create mode 100644 cmd/puppeth/module_explorer.go create mode 100644 cmd/puppeth/wizard_explorer.go diff --git a/cmd/puppeth/genesis.go b/cmd/puppeth/genesis.go new file mode 100644 index 0000000000..2b66df43c1 --- /dev/null +++ b/cmd/puppeth/genesis.go @@ -0,0 +1,208 @@ +// Copyright 2017 The go-ethereum Authors +// This file is part of go-ethereum. +// +// go-ethereum is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// go-ethereum is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with go-ethereum. If not, see . + +package main + +import ( + "encoding/binary" + "errors" + "math" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/consensus/ethash" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/params" +) + +// parityChainSpec is the chain specification format used by Parity. +type parityChainSpec struct { + Name string `json:"name"` + Engine struct { + Ethash struct { + Params struct { + MinimumDifficulty *hexutil.Big `json:"minimumDifficulty"` + DifficultyBoundDivisor *hexutil.Big `json:"difficultyBoundDivisor"` + GasLimitBoundDivisor *hexutil.Big `json:"gasLimitBoundDivisor"` + DurationLimit *hexutil.Big `json:"durationLimit"` + BlockReward *hexutil.Big `json:"blockReward"` + HomesteadTransition uint64 `json:"homesteadTransition"` + EIP150Transition uint64 `json:"eip150Transition"` + EIP160Transition uint64 `json:"eip160Transition"` + EIP161abcTransition uint64 `json:"eip161abcTransition"` + EIP161dTransition uint64 `json:"eip161dTransition"` + EIP649Reward *hexutil.Big `json:"eip649Reward"` + EIP100bTransition uint64 `json:"eip100bTransition"` + EIP649Transition uint64 `json:"eip649Transition"` + } `json:"params"` + } `json:"Ethash"` + } `json:"engine"` + + Params struct { + MaximumExtraDataSize hexutil.Uint64 `json:"maximumExtraDataSize"` + MinGasLimit *hexutil.Big `json:"minGasLimit"` + NetworkID hexutil.Uint64 `json:"networkID"` + MaxCodeSize uint64 `json:"maxCodeSize"` + EIP155Transition uint64 `json:"eip155Transition"` + EIP98Transition uint64 `json:"eip98Transition"` + EIP86Transition uint64 `json:"eip86Transition"` + EIP140Transition uint64 `json:"eip140Transition"` + EIP211Transition uint64 `json:"eip211Transition"` + EIP214Transition uint64 `json:"eip214Transition"` + EIP658Transition uint64 `json:"eip658Transition"` + } `json:"params"` + + Genesis struct { + Seal struct { + Ethereum struct { + Nonce hexutil.Bytes `json:"nonce"` + MixHash hexutil.Bytes `json:"mixHash"` + } `json:"ethereum"` + } `json:"seal"` + + Difficulty *hexutil.Big `json:"difficulty"` + Author common.Address `json:"author"` + Timestamp hexutil.Uint64 `json:"timestamp"` + ParentHash common.Hash `json:"parentHash"` + ExtraData hexutil.Bytes `json:"extraData"` + GasLimit hexutil.Uint64 `json:"gasLimit"` + } `json:"genesis"` + + Nodes []string `json:"nodes"` + Accounts map[common.Address]*parityChainSpecAccount `json:"accounts"` +} + +// parityChainSpecAccount is the prefunded genesis account and/or precompiled +// contract definition. +type parityChainSpecAccount struct { + Balance *hexutil.Big `json:"balance"` + Nonce uint64 `json:"nonce,omitempty"` + Builtin *parityChainSpecBuiltin `json:"builtin,omitempty"` +} + +// parityChainSpecBuiltin is the precompiled contract definition. +type parityChainSpecBuiltin struct { + Name string `json:"name,omitempty"` + ActivateAt uint64 `json:"activate_at,omitempty"` + Pricing *parityChainSpecPricing `json:"pricing,omitempty"` +} + +// parityChainSpecPricing represents the different pricing models that builtin +// contracts might advertise using. +type parityChainSpecPricing struct { + Linear *parityChainSpecLinearPricing `json:"linear,omitempty"` + ModExp *parityChainSpecModExpPricing `json:"modexp,omitempty"` + AltBnPairing *parityChainSpecAltBnPairingPricing `json:"alt_bn128_pairing,omitempty"` +} + +type parityChainSpecLinearPricing struct { + Base uint64 `json:"base"` + Word uint64 `json:"word"` +} + +type parityChainSpecModExpPricing struct { + Divisor uint64 `json:"divisor"` +} + +type parityChainSpecAltBnPairingPricing struct { + Base uint64 `json:"base"` + Pair uint64 `json:"pair"` +} + +// newParityChainSpec converts a go-ethereum genesis block into a Parity specific +// chain specification format. +func newParityChainSpec(network string, genesis *core.Genesis, bootnodes []string) (*parityChainSpec, error) { + // Only ethash is currently supported between go-ethereum and Parity + if genesis.Config.Ethash == nil { + return nil, errors.New("unsupported consensus engine") + } + // Reconstruct the chain spec in Parity's format + spec := &parityChainSpec{ + Name: network, + Nodes: bootnodes, + } + spec.Engine.Ethash.Params.MinimumDifficulty = (*hexutil.Big)(params.MinimumDifficulty) + spec.Engine.Ethash.Params.DifficultyBoundDivisor = (*hexutil.Big)(params.DifficultyBoundDivisor) + spec.Engine.Ethash.Params.GasLimitBoundDivisor = (*hexutil.Big)(params.GasLimitBoundDivisor) + spec.Engine.Ethash.Params.DurationLimit = (*hexutil.Big)(params.DurationLimit) + spec.Engine.Ethash.Params.BlockReward = (*hexutil.Big)(ethash.FrontierBlockReward) + spec.Engine.Ethash.Params.HomesteadTransition = genesis.Config.HomesteadBlock.Uint64() + spec.Engine.Ethash.Params.EIP150Transition = genesis.Config.EIP150Block.Uint64() + spec.Engine.Ethash.Params.EIP160Transition = genesis.Config.EIP155Block.Uint64() + spec.Engine.Ethash.Params.EIP161abcTransition = genesis.Config.EIP158Block.Uint64() + spec.Engine.Ethash.Params.EIP161dTransition = genesis.Config.EIP158Block.Uint64() + spec.Engine.Ethash.Params.EIP649Reward = (*hexutil.Big)(ethash.ByzantiumBlockReward) + spec.Engine.Ethash.Params.EIP100bTransition = genesis.Config.ByzantiumBlock.Uint64() + spec.Engine.Ethash.Params.EIP649Transition = genesis.Config.ByzantiumBlock.Uint64() + + spec.Params.MaximumExtraDataSize = (hexutil.Uint64)(params.MaximumExtraDataSize) + spec.Params.MinGasLimit = (*hexutil.Big)(params.MinGasLimit) + spec.Params.NetworkID = (hexutil.Uint64)(genesis.Config.ChainId.Uint64()) + spec.Params.MaxCodeSize = params.MaxCodeSize + spec.Params.EIP155Transition = genesis.Config.EIP155Block.Uint64() + spec.Params.EIP98Transition = math.MaxUint64 + spec.Params.EIP86Transition = math.MaxUint64 + spec.Params.EIP140Transition = genesis.Config.ByzantiumBlock.Uint64() + spec.Params.EIP211Transition = genesis.Config.ByzantiumBlock.Uint64() + spec.Params.EIP214Transition = genesis.Config.ByzantiumBlock.Uint64() + spec.Params.EIP658Transition = genesis.Config.ByzantiumBlock.Uint64() + + spec.Genesis.Seal.Ethereum.Nonce = (hexutil.Bytes)(make([]byte, 8)) + binary.LittleEndian.PutUint64(spec.Genesis.Seal.Ethereum.Nonce[:], genesis.Nonce) + + spec.Genesis.Seal.Ethereum.MixHash = (hexutil.Bytes)(genesis.Mixhash[:]) + spec.Genesis.Difficulty = (*hexutil.Big)(genesis.Difficulty) + spec.Genesis.Author = genesis.Coinbase + spec.Genesis.Timestamp = (hexutil.Uint64)(genesis.Timestamp) + spec.Genesis.ParentHash = genesis.ParentHash + spec.Genesis.ExtraData = (hexutil.Bytes)(genesis.ExtraData) + spec.Genesis.GasLimit = (hexutil.Uint64)(genesis.GasLimit) + + spec.Accounts = make(map[common.Address]*parityChainSpecAccount) + for address, account := range genesis.Alloc { + spec.Accounts[address] = &parityChainSpecAccount{ + Balance: (*hexutil.Big)(account.Balance), + Nonce: account.Nonce, + } + } + spec.Accounts[common.BytesToAddress([]byte{1})].Builtin = &parityChainSpecBuiltin{ + Name: "ecrecover", Pricing: &parityChainSpecPricing{Linear: &parityChainSpecLinearPricing{Base: 3000}}, + } + spec.Accounts[common.BytesToAddress([]byte{2})].Builtin = &parityChainSpecBuiltin{ + Name: "sha256", Pricing: &parityChainSpecPricing{Linear: &parityChainSpecLinearPricing{Base: 60, Word: 12}}, + } + spec.Accounts[common.BytesToAddress([]byte{3})].Builtin = &parityChainSpecBuiltin{ + Name: "ripemd160", Pricing: &parityChainSpecPricing{Linear: &parityChainSpecLinearPricing{Base: 600, Word: 120}}, + } + spec.Accounts[common.BytesToAddress([]byte{4})].Builtin = &parityChainSpecBuiltin{ + Name: "identity", Pricing: &parityChainSpecPricing{Linear: &parityChainSpecLinearPricing{Base: 15, Word: 3}}, + } + if genesis.Config.ByzantiumBlock != nil { + spec.Accounts[common.BytesToAddress([]byte{5})].Builtin = &parityChainSpecBuiltin{ + Name: "modexp", ActivateAt: genesis.Config.ByzantiumBlock.Uint64(), Pricing: &parityChainSpecPricing{ModExp: &parityChainSpecModExpPricing{Divisor: 20}}, + } + spec.Accounts[common.BytesToAddress([]byte{6})].Builtin = &parityChainSpecBuiltin{ + Name: "alt_bn128_add", ActivateAt: genesis.Config.ByzantiumBlock.Uint64(), Pricing: &parityChainSpecPricing{Linear: &parityChainSpecLinearPricing{Base: 500}}, + } + spec.Accounts[common.BytesToAddress([]byte{7})].Builtin = &parityChainSpecBuiltin{ + Name: "alt_bn128_mul", ActivateAt: genesis.Config.ByzantiumBlock.Uint64(), Pricing: &parityChainSpecPricing{Linear: &parityChainSpecLinearPricing{Base: 40000}}, + } + spec.Accounts[common.BytesToAddress([]byte{8})].Builtin = &parityChainSpecBuiltin{ + Name: "alt_bn128_pairing", ActivateAt: genesis.Config.ByzantiumBlock.Uint64(), Pricing: &parityChainSpecPricing{AltBnPairing: &parityChainSpecAltBnPairingPricing{Base: 100000, Pair: 80000}}, + } + } + return spec, nil +} diff --git a/cmd/puppeth/module_ethstats.go b/cmd/puppeth/module_ethstats.go index 7ce3ca3cdf..b9874cf58c 100644 --- a/cmd/puppeth/module_ethstats.go +++ b/cmd/puppeth/module_ethstats.go @@ -34,9 +34,9 @@ var ethstatsDockerfile = ` FROM mhart/alpine-node:latest RUN \ - apk add --update git && \ - git clone --depth=1 https://github.com/karalabe/eth-netstats && \ - apk del git && rm -rf /var/cache/apk/* && \ + apk add --update git && \ + git clone --depth=1 https://github.com/puppeth/eth-netstats && \ + apk del git && rm -rf /var/cache/apk/* && \ \ cd /eth-netstats && npm install && npm install -g grunt-cli && grunt diff --git a/cmd/puppeth/module_explorer.go b/cmd/puppeth/module_explorer.go new file mode 100644 index 0000000000..cb7fc54301 --- /dev/null +++ b/cmd/puppeth/module_explorer.go @@ -0,0 +1,226 @@ +// Copyright 2017 The go-ethereum Authors +// This file is part of go-ethereum. +// +// go-ethereum is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// go-ethereum is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with go-ethereum. If not, see . + +package main + +import ( + "bytes" + "fmt" + "html/template" + "math/rand" + "path/filepath" + "strconv" + "strings" + + "github.com/ethereum/go-ethereum/log" +) + +// explorerDockerfile is the Dockerfile required to run a block explorer. +var explorerDockerfile = ` +FROM parity/parity:stable + +RUN \ + apt-get update && apt-get install -y curl git npm make g++ --no-install-recommends && \ + npm install -g n pm2 && n stable + +RUN \ + git clone --depth=1 https://github.com/puppeth/eth-net-intelligence-api && \ + cd eth-net-intelligence-api && npm install + +RUN \ + git clone --depth=1 https://github.com/puppeth/etherchain-light --recursive && \ + cd etherchain-light && npm install && mv config.js.example config.js && \ + sed -i '/this.bootstrapUrl/c\ this.bootstrapUrl = "https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css";' config.js + +ADD ethstats.json /ethstats.json +ADD chain.json /chain.json + +RUN \ + echo '(cd eth-net-intelligence-api && pm2 start /ethstats.json)' > explorer.sh && \ + echo '(cd etherchain-light && npm start &)' >> explorer.sh && \ + echo '/parity/parity --chain=/chain.json --port={{.NodePort}} --tracing=on --fat-db=on --pruning=archive' >> explorer.sh + +EXPOSE 3000 + +ENTRYPOINT ["/bin/sh", "explorer.sh"] +` + +// explorerEthstats is the configuration file for the ethstats javascript client. +var explorerEthstats = `[ + { + "name" : "node-app", + "script" : "app.js", + "log_date_format" : "YYYY-MM-DD HH:mm Z", + "merge_logs" : false, + "watch" : false, + "max_restarts" : 10, + "exec_interpreter" : "node", + "exec_mode" : "fork_mode", + "env": + { + "NODE_ENV" : "production", + "RPC_HOST" : "localhost", + "RPC_PORT" : "8545", + "LISTENING_PORT" : "{{.Port}}", + "INSTANCE_NAME" : "{{.Name}}", + "CONTACT_DETAILS" : "", + "WS_SERVER" : "{{.Host}}", + "WS_SECRET" : "{{.Secret}}", + "VERBOSITY" : 2 + } + } +]` + +// explorerComposefile is the docker-compose.yml file required to deploy and +// maintain a block explorer. +var explorerComposefile = ` +version: '2' +services: + explorer: + build: . + image: {{.Network}}/explorer + ports: + - "{{.NodePort}}:{{.NodePort}}" + - "{{.NodePort}}:{{.NodePort}}/udp"{{if not .VHost}} + - "{{.WebPort}}:3000"{{end}} + volumes: + - {{.Datadir}}:/root/.local/share/io.parity.ethereum + environment: + - NODE_PORT={{.NodePort}}/tcp + - STATS={{.Ethstats}}{{if .VHost}} + - VIRTUAL_HOST={{.VHost}} + - VIRTUAL_PORT=3000{{end}} + logging: + driver: "json-file" + options: + max-size: "1m" + max-file: "10" + restart: always +` + +// deployExplorer deploys a new block explorer container to a remote machine via +// SSH, docker and docker-compose. If an instance with the specified network name +// already exists there, it will be overwritten! +func deployExplorer(client *sshClient, network string, chainspec []byte, config *explorerInfos, nocache bool) ([]byte, error) { + // Generate the content to upload to the server + workdir := fmt.Sprintf("%d", rand.Int63()) + files := make(map[string][]byte) + + dockerfile := new(bytes.Buffer) + template.Must(template.New("").Parse(explorerDockerfile)).Execute(dockerfile, map[string]interface{}{ + "NodePort": config.nodePort, + }) + files[filepath.Join(workdir, "Dockerfile")] = dockerfile.Bytes() + + ethstats := new(bytes.Buffer) + template.Must(template.New("").Parse(explorerEthstats)).Execute(ethstats, map[string]interface{}{ + "Port": config.nodePort, + "Name": config.ethstats[:strings.Index(config.ethstats, ":")], + "Secret": config.ethstats[strings.Index(config.ethstats, ":")+1 : strings.Index(config.ethstats, "@")], + "Host": config.ethstats[strings.Index(config.ethstats, "@")+1:], + }) + files[filepath.Join(workdir, "ethstats.json")] = ethstats.Bytes() + + composefile := new(bytes.Buffer) + template.Must(template.New("").Parse(explorerComposefile)).Execute(composefile, map[string]interface{}{ + "Datadir": config.datadir, + "Network": network, + "NodePort": config.nodePort, + "VHost": config.webHost, + "WebPort": config.webPort, + "Ethstats": config.ethstats[:strings.Index(config.ethstats, ":")], + }) + files[filepath.Join(workdir, "docker-compose.yaml")] = composefile.Bytes() + + files[filepath.Join(workdir, "chain.json")] = []byte(chainspec) + + // Upload the deployment files to the remote server (and clean up afterwards) + if out, err := client.Upload(files); err != nil { + return out, err + } + defer client.Run("rm -rf " + workdir) + + // Build and deploy the boot or seal node service + if nocache { + return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s build --pull --no-cache && docker-compose -p %s up -d --force-recreate", workdir, network, network)) + } + return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s up -d --build --force-recreate", workdir, network)) +} + +// explorerInfos is returned from a block explorer status check to allow reporting +// various configuration parameters. +type explorerInfos struct { + datadir string + ethstats string + nodePort int + webHost string + webPort int +} + +// Report converts the typed struct into a plain string->string map, cotnaining +// most - but not all - fields for reporting to the user. +func (info *explorerInfos) Report() map[string]string { + report := map[string]string{ + "Data directory": info.datadir, + "Node listener port ": strconv.Itoa(info.nodePort), + "Ethstats username": info.ethstats, + "Website address ": info.webHost, + "Website listener port ": strconv.Itoa(info.webPort), + } + return report +} + +// checkExplorer does a health-check against an boot or seal node server to verify +// whether it's running, and if yes, whether it's responsive. +func checkExplorer(client *sshClient, network string) (*explorerInfos, error) { + // Inspect a possible block explorer container on the host + infos, err := inspectContainer(client, fmt.Sprintf("%s_explorer_1", network)) + if err != nil { + return nil, err + } + if !infos.running { + return nil, ErrServiceOffline + } + // Resolve the port from the host, or the reverse proxy + webPort := infos.portmap["3000/tcp"] + if webPort == 0 { + if proxy, _ := checkNginx(client, network); proxy != nil { + webPort = proxy.port + } + } + if webPort == 0 { + return nil, ErrNotExposed + } + // Resolve the host from the reverse-proxy and the config values + host := infos.envvars["VIRTUAL_HOST"] + if host == "" { + host = client.server + } + // Run a sanity check to see if the devp2p is reachable + nodePort := infos.portmap[infos.envvars["NODE_PORT"]] + if err = checkPort(client.server, nodePort); err != nil { + log.Warn(fmt.Sprintf("Explorer devp2p port seems unreachable"), "server", client.server, "port", nodePort, "err", err) + } + // Assemble and return the useful infos + stats := &explorerInfos{ + datadir: infos.volumes["/root/.local/share/io.parity.ethereum"], + nodePort: nodePort, + webHost: host, + webPort: webPort, + ethstats: infos.envvars["STATS"], + } + return stats, nil +} diff --git a/cmd/puppeth/module_node.go b/cmd/puppeth/module_node.go index 37da770aa8..9b8d5f0f77 100644 --- a/cmd/puppeth/module_node.go +++ b/cmd/puppeth/module_node.go @@ -40,7 +40,7 @@ ADD genesis.json /genesis.json ADD signer.pass /signer.pass {{end}} RUN \ - echo 'geth init /genesis.json' > geth.sh && \{{if .Unlock}} + echo 'geth --cache 512 init /genesis.json' > geth.sh && \{{if .Unlock}} echo 'mkdir -p /root/.ethereum/keystore/ && cp /signer.json /root/.ethereum/keystore/' >> geth.sh && \{{end}} echo $'geth --networkid {{.NetworkID}} --cache 512 --port {{.Port}} --maxpeers {{.Peers}} {{.LightFlag}} --ethstats \'{{.Ethstats}}\' {{if .BootV4}}--bootnodesv4 {{.BootV4}}{{end}} {{if .BootV5}}--bootnodesv5 {{.BootV5}}{{end}} {{if .Etherbase}}--etherbase {{.Etherbase}} --mine --minerthreads 1{{end}} {{if .Unlock}}--unlock 0 --password /signer.pass --mine{{end}} --targetgaslimit {{.GasTarget}} --gasprice {{.GasPrice}}' >> geth.sh @@ -131,9 +131,7 @@ func deployNode(client *sshClient, network string, bootv4, bootv5 []string, conf }) files[filepath.Join(workdir, "docker-compose.yaml")] = composefile.Bytes() - //genesisfile, _ := json.MarshalIndent(config.genesis, "", " ") files[filepath.Join(workdir, "genesis.json")] = config.genesis - if config.keyJSON != "" { files[filepath.Join(workdir, "signer.json")] = []byte(config.keyJSON) files[filepath.Join(workdir, "signer.pass")] = []byte(config.keyPass) diff --git a/cmd/puppeth/wizard_explorer.go b/cmd/puppeth/wizard_explorer.go new file mode 100644 index 0000000000..2df77fa5cc --- /dev/null +++ b/cmd/puppeth/wizard_explorer.go @@ -0,0 +1,111 @@ +// Copyright 2017 The go-ethereum Authors +// This file is part of go-ethereum. +// +// go-ethereum is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// go-ethereum is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with go-ethereum. If not, see . + +package main + +import ( + "encoding/json" + "fmt" + "time" + + "github.com/ethereum/go-ethereum/log" +) + +// deployExplorer creates a new block explorer based on some user input. +func (w *wizard) deployExplorer() { + // Do some sanity check before the user wastes time on input + if w.conf.genesis == nil { + log.Error("No genesis block configured") + return + } + if w.conf.ethstats == "" { + log.Error("No ethstats server configured") + return + } + if w.conf.genesis.Config.Ethash == nil { + log.Error("Only ethash network supported") + return + } + // Select the server to interact with + server := w.selectServer() + if server == "" { + return + } + client := w.servers[server] + + // Retrieve any active node configurations from the server + infos, err := checkExplorer(client, w.network) + if err != nil { + infos = &explorerInfos{nodePort: 30303, webPort: 80, webHost: client.server} + } + chainspec, err := newParityChainSpec(w.network, w.conf.genesis, w.conf.bootFull) + if err != nil { + log.Error("Failed to create chain spec for explorer", "err", err) + return + } + chain, _ := json.MarshalIndent(chainspec, "", " ") + + // Figure out which port to listen on + fmt.Println() + fmt.Printf("Which port should the explorer listen on? (default = %d)\n", infos.webPort) + infos.webPort = w.readDefaultInt(infos.webPort) + + // Figure which virtual-host to deploy ethstats on + if infos.webHost, err = w.ensureVirtualHost(client, infos.webPort, infos.webHost); err != nil { + log.Error("Failed to decide on explorer host", "err", err) + return + } + // Figure out where the user wants to store the persistent data + fmt.Println() + if infos.datadir == "" { + fmt.Printf("Where should data be stored on the remote machine?\n") + infos.datadir = w.readString() + } else { + fmt.Printf("Where should data be stored on the remote machine? (default = %s)\n", infos.datadir) + infos.datadir = w.readDefaultString(infos.datadir) + } + // Figure out which port to listen on + fmt.Println() + fmt.Printf("Which TCP/UDP port should the archive node listen on? (default = %d)\n", infos.nodePort) + infos.nodePort = w.readDefaultInt(infos.nodePort) + + // Set a proper name to report on the stats page + fmt.Println() + if infos.ethstats == "" { + fmt.Printf("What should the explorer be called on the stats page?\n") + infos.ethstats = w.readString() + ":" + w.conf.ethstats + } else { + fmt.Printf("What should the explorer be called on the stats page? (default = %s)\n", infos.ethstats) + infos.ethstats = w.readDefaultString(infos.ethstats) + ":" + w.conf.ethstats + } + // Try to deploy the explorer on the host + fmt.Println() + fmt.Printf("Should the explorer be built from scratch (y/n)? (default = no)\n") + nocache := w.readDefaultString("n") != "n" + + if out, err := deployExplorer(client, w.network, chain, infos, nocache); err != nil { + log.Error("Failed to deploy explorer container", "err", err) + if len(out) > 0 { + fmt.Printf("%s\n", out) + } + return + } + // All ok, run a network scan to pick any changes up + log.Info("Waiting for node to finish booting") + time.Sleep(3 * time.Second) + + w.networkStats() +} diff --git a/cmd/puppeth/wizard_faucet.go b/cmd/puppeth/wizard_faucet.go index e9d5c6016b..dbb0965eb1 100644 --- a/cmd/puppeth/wizard_faucet.go +++ b/cmd/puppeth/wizard_faucet.go @@ -60,7 +60,7 @@ func (w *wizard) deployFaucet() { log.Error("Failed to decide on faucet host", "err", err) return } - // Port and proxy settings retrieved, figure out the funcing amount per perdion configurations + // Port and proxy settings retrieved, figure out the funding amount per period configurations fmt.Println() fmt.Printf("How many Ethers to release per request? (default = %d)\n", infos.amount) infos.amount = w.readDefaultInt(infos.amount) diff --git a/cmd/puppeth/wizard_netstats.go b/cmd/puppeth/wizard_netstats.go index 906dfeda75..42df501b98 100644 --- a/cmd/puppeth/wizard_netstats.go +++ b/cmd/puppeth/wizard_netstats.go @@ -137,6 +137,14 @@ func (w *wizard) gatherStats(server string, pubkey []byte, client *sshClient) *s stat.services["sealnode"] = infos.Report() genesis = string(infos.genesis) } + logger.Debug("Checking for explorer availability") + if infos, err := checkExplorer(client, w.network); err != nil { + if err != ErrServiceUnknown { + stat.services["explorer"] = map[string]string{"offline": err.Error()} + } + } else { + stat.services["explorer"] = infos.Report() + } logger.Debug("Checking for faucet availability") if infos, err := checkFaucet(client, w.network); err != nil { if err != ErrServiceUnknown { diff --git a/cmd/puppeth/wizard_network.go b/cmd/puppeth/wizard_network.go index bf8248e4b1..46b52bfcb8 100644 --- a/cmd/puppeth/wizard_network.go +++ b/cmd/puppeth/wizard_network.go @@ -174,9 +174,10 @@ func (w *wizard) deployComponent() { fmt.Println(" 1. Ethstats - Network monitoring tool") fmt.Println(" 2. Bootnode - Entry point of the network") fmt.Println(" 3. Sealer - Full node minting new blocks") - fmt.Println(" 4. Wallet - Browser wallet for quick sends (todo)") - fmt.Println(" 5. Faucet - Crypto faucet to give away funds") - fmt.Println(" 6. Dashboard - Website listing above web-services") + fmt.Println(" 4. Explorer - Chain analysis webservice (ethash only)") + fmt.Println(" 5. Wallet - Browser wallet for quick sends (todo)") + fmt.Println(" 6. Faucet - Crypto faucet to give away funds") + fmt.Println(" 7. Dashboard - Website listing above web-services") switch w.read() { case "1": @@ -186,9 +187,11 @@ func (w *wizard) deployComponent() { case "3": w.deployNode(false) case "4": + w.deployExplorer() case "5": - w.deployFaucet() case "6": + w.deployFaucet() + case "7": w.deployDashboard() default: log.Error("That's not something I can do") diff --git a/consensus/ethash/consensus.go b/consensus/ethash/consensus.go index 6a19d449f3..e330b7ce59 100644 --- a/consensus/ethash/consensus.go +++ b/consensus/ethash/consensus.go @@ -36,8 +36,8 @@ import ( // Ethash proof-of-work protocol constants. var ( - 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 + 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 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! func AccumulateRewards(config *params.ChainConfig, state *state.StateDB, header *types.Header, uncles []*types.Header) { // Select the correct block reward based on chain progression - blockReward := frontierBlockReward + blockReward := FrontierBlockReward if config.IsByzantium(header.Number) { - blockReward = byzantiumBlockReward + blockReward = ByzantiumBlockReward } // Accumulate the rewards for the miner and any included uncles reward := new(big.Int).Set(blockReward) From b5cf60389510cdfbd38b2f79936323f89388724c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9ter=20Szil=C3=A1gyi?= Date: Mon, 23 Oct 2017 09:58:33 +0300 Subject: [PATCH 06/15] cmd/puppeth: add support for deploying web wallets --- cmd/puppeth/module_explorer.go | 2 +- cmd/puppeth/module_wallet.go | 249 +++++++++++++++++++++++++++++++++ cmd/puppeth/wizard_netstats.go | 8 ++ cmd/puppeth/wizard_network.go | 1 + cmd/puppeth/wizard_wallet.go | 107 ++++++++++++++ 5 files changed, 366 insertions(+), 1 deletion(-) create mode 100644 cmd/puppeth/module_wallet.go create mode 100644 cmd/puppeth/wizard_wallet.go diff --git a/cmd/puppeth/module_explorer.go b/cmd/puppeth/module_explorer.go index cb7fc54301..589b071e7d 100644 --- a/cmd/puppeth/module_explorer.go +++ b/cmd/puppeth/module_explorer.go @@ -183,7 +183,7 @@ func (info *explorerInfos) Report() map[string]string { return report } -// checkExplorer does a health-check against an boot or seal node server to verify +// 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 diff --git a/cmd/puppeth/module_wallet.go b/cmd/puppeth/module_wallet.go new file mode 100644 index 0000000000..3ba17dece1 --- /dev/null +++ b/cmd/puppeth/module_wallet.go @@ -0,0 +1,249 @@ +// 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 . + +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 ethereum/client-go:latest + +RUN \ + apk add --update git python make g++ libnotify nodejs-npm && \ + npm install -g gulp-cli + +RUN \ + git clone --depth=1 https://github.com/kvhnuke/etherwallet.git && \ + (cd etherwallet && npm install) +WORKDIR etherwallet + +RUN \ + echo '"use strict";' > app/scripts/nodes.js && \ + echo 'var nodes = function() {}' >> app/scripts/nodes.js && \ + echo 'nodes.customNode = require("./nodeHelpers/customNode");' >> app/scripts/nodes.js && \ + echo 'nodes.nodeTypes = {' >> app/scripts/nodes.js && \ + echo ' {{.Network}}: "{{.Denom}} ETH",' >> app/scripts/nodes.js && \ + echo ' Custom: "CUSTOM ETH"' >> app/scripts/nodes.js && \ + echo '};' >> app/scripts/nodes.js && \ + echo 'nodes.ensNodeTypes = [];' >> app/scripts/nodes.js && \ + echo 'nodes.customNodeObj = {' >> app/scripts/nodes.js && \ + echo ' "name": "CUS",' >> app/scripts/nodes.js && \ + echo ' "type": nodes.nodeTypes.Custom,' >> app/scripts/nodes.js && \ + echo ' "eip155": false,' >> app/scripts/nodes.js && \ + echo ' "chainId": "",' >> app/scripts/nodes.js && \ + echo ' "tokenList": [],' >> app/scripts/nodes.js && \ + echo ' "abiList": [],' >> app/scripts/nodes.js && \ + echo ' "service": "Custom",' >> app/scripts/nodes.js && \ + echo ' "lib": null' >> app/scripts/nodes.js && \ + echo '}' >> app/scripts/nodes.js && \ + echo 'nodes.nodeList = {' >> app/scripts/nodes.js && \ + echo ' "eth_mew": {' >> app/scripts/nodes.js && \ + echo ' "name": "{{.Network}}",' >> app/scripts/nodes.js && \ + echo ' "type": nodes.nodeTypes.{{.Network}},' >> app/scripts/nodes.js && \ + echo ' "eip155": true,' >> app/scripts/nodes.js && \ + echo ' "chainId": {{.NetworkID}},' >> app/scripts/nodes.js && \ + echo ' "tokenList": [],' >> app/scripts/nodes.js && \ + echo ' "abiList": [],' >> app/scripts/nodes.js && \ + echo ' "service": "Go Ethereum",' >> app/scripts/nodes.js && \ + echo ' "lib": new nodes.customNode("http://{{.Host}}:{{.RPCPort}}", "")' >> app/scripts/nodes.js && \ + echo ' }' >> app/scripts/nodes.js && \ + echo '};' >> app/scripts/nodes.js && \ + echo 'nodes.ethPrice = require("./nodeHelpers/ethPrice");' >> app/scripts/nodes.js && \ + echo 'module.exports = nodes;' >> app/scripts/nodes.js + +RUN rm -rf dist && gulp prep && npm run dist + +RUN \ + npm install connect serve-static && \ + \ + echo 'var connect = require("connect");' > server.js && \ + echo 'var serveStatic = require("serve-static");' >> server.js && \ + echo 'connect().use(serveStatic("/etherwallet/dist")).listen(80, function(){' >> server.js && \ + echo ' console.log("Server running on 80...");' >> server.js && \ + echo '});' >> server.js + +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 + +EXPOSE 80 8545 + +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")] = []byte(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, cotnaining +// 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 +} diff --git a/cmd/puppeth/wizard_netstats.go b/cmd/puppeth/wizard_netstats.go index 42df501b98..469b7f2bfc 100644 --- a/cmd/puppeth/wizard_netstats.go +++ b/cmd/puppeth/wizard_netstats.go @@ -145,6 +145,14 @@ func (w *wizard) gatherStats(server string, pubkey []byte, client *sshClient) *s } 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") if infos, err := checkFaucet(client, w.network); err != nil { if err != ErrServiceUnknown { diff --git a/cmd/puppeth/wizard_network.go b/cmd/puppeth/wizard_network.go index 46b52bfcb8..afb0b34ef2 100644 --- a/cmd/puppeth/wizard_network.go +++ b/cmd/puppeth/wizard_network.go @@ -189,6 +189,7 @@ func (w *wizard) deployComponent() { case "4": w.deployExplorer() case "5": + w.deployWallet() case "6": w.deployFaucet() case "7": diff --git a/cmd/puppeth/wizard_wallet.go b/cmd/puppeth/wizard_wallet.go new file mode 100644 index 0000000000..dfbf11804d --- /dev/null +++ b/cmd/puppeth/wizard_wallet.go @@ -0,0 +1,107 @@ +// 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 . + +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} + } + 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 TCP/UDP 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 + 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() +} From 51a86f61be52fdd16a409fc93cf89a2226129697 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9ter=20Szil=C3=A1gyi?= Date: Mon, 23 Oct 2017 10:22:23 +0300 Subject: [PATCH 07/15] cmd/faucet: protocol relative websockets, noauth mode --- cmd/faucet/faucet.go | 19 ++++++++- cmd/faucet/faucet.html | 12 +++--- cmd/faucet/website.go | 2 +- cmd/puppeth/module_faucet.go | 24 +++++++++--- cmd/puppeth/wizard_faucet.go | 74 ++++++++++++++++++++++-------------- 5 files changed, 87 insertions(+), 44 deletions(-) diff --git a/cmd/faucet/faucet.go b/cmd/faucet/faucet.go index 72098e68d2..94d690e532 100644 --- a/cmd/faucet/faucet.go +++ b/cmd/faucet/faucet.go @@ -83,7 +83,8 @@ var ( captchaToken = flag.String("captcha.token", "", "Recaptcha site key to authenticate client side") captchaSecret = flag.String("captcha.secret", "", "Recaptcha secret key to authenticate server side") - logFlag = flag.Int("loglevel", 3, "Log level to use for Ethereum and the faucet") + noauthFlag = flag.Bool("noauth", false, "Enables funding requests without authentication") + logFlag = flag.Int("loglevel", 3, "Log level to use for Ethereum and the faucet") ) var ( @@ -132,6 +133,7 @@ func main() { "Amounts": amounts, "Periods": periods, "Recaptcha": *captchaToken, + "NoAuth": *noauthFlag, }) if err != nil { 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 { 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/") { 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) @@ -442,6 +444,8 @@ func (f *faucet) apiHandler(conn *websocket.Conn) { username, avatar, address, err = authGooglePlus(msg.URL) case strings.HasPrefix(msg.URL, "https://www.facebook.com/"): username, avatar, address, err = authFacebook(msg.URL) + case *noauthFlag: + username, avatar, address, err = authNoAuth(msg.URL) default: err = errors.New("Something funky happened, please open an issue at https://github.com/ethereum/go-ethereum/issues") } @@ -776,3 +780,14 @@ func authFacebook(url string) (string, string, common.Address, error) { } 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 +} diff --git a/cmd/faucet/faucet.html b/cmd/faucet/faucet.html index 5d3b8741ba..ff9bef5731 100644 --- a/cmd/faucet/faucet.html +++ b/cmd/faucet/faucet.html @@ -93,6 +93,11 @@
To request funds via Facebook, publish a new public post with your Ethereum address embedded into the content (surrounding text doesn't matter).
Copy-paste the posts URL into the above input box and fire away!
+ + {{if .NoAuth}} +
+
To request funds without authentication, simply copy-paste your Ethereum address into the above input box (surrounding text doesn't matter) and fire away.
This mode is susceptible to Byzantine attacks. Only use for debugging or private networks!
+ {{end}}

You can track the current pending requests below the input field to see how much you have to wait until your turn comes.

{{if .Recaptcha}}The faucet is running invisible reCaptcha protection against bots.{{end}} @@ -126,12 +131,7 @@ }; // Define a method to reconnect upon server loss var reconnect = function() { - if (attempt % 2 == 0) { - server = new WebSocket("wss://" + location.host + "/api"); - } else { - server = new WebSocket("ws://" + location.host + "/api"); - } - attempt++; + server = new WebSocket(((window.location.protocol === "https:") ? "wss://" : "ws://") + window.location.host + "/api"); server.onmessage = function(event) { var msg = JSON.parse(event.data); diff --git a/cmd/faucet/website.go b/cmd/faucet/website.go index 6a99f8c6f8..ca49b047a7 100644 --- a/cmd/faucet/website.go +++ b/cmd/faucet/website.go @@ -68,7 +68,7 @@ func (fi bindataFileInfo) Sys() interface{} { return nil } -var _faucetHtml = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xcc\x3a\x7f\x73\xdb\xb6\x92\x7f\x3b\x9f\x62\xcb\x8b\x9f\xa4\xb3\x48\xca\x76\x92\xe7\x93\x48\x75\x72\x79\x7d\x7d\xb9\xb9\xeb\xeb\xb4\xe9\xdc\xbd\x69\x3b\x37\x20\xb1\x12\x11\x83\x00\x0b\x80\x92\x55\x8f\xbe\xfb\x0d\x00\x92\xa2\x7e\xd8\x71\x9a\xdc\x5d\xfc\x87\x4c\x00\x8b\xdd\xc5\xfe\xc6\x92\xc9\x57\x7f\xf9\xfb\x9b\x77\xff\xf8\xfe\x1b\x28\x4c\xc9\xe7\xcf\x12\xfb\x0f\x38\x11\xcb\x34\x40\x11\xcc\x9f\x9d\x25\x05\x12\x3a\x7f\x76\x76\x96\x94\x68\x08\xe4\x05\x51\x1a\x4d\x1a\xd4\x66\x11\xde\x04\xbb\x85\xc2\x98\x2a\xc4\xdf\x6a\xb6\x4a\x83\xff\x0a\x7f\x7a\x1d\xbe\x91\x65\x45\x0c\xcb\x38\x06\x90\x4b\x61\x50\x98\x34\x78\xfb\x4d\x8a\x74\x89\xbd\x7d\x82\x94\x98\x06\x2b\x86\xeb\x4a\x2a\xd3\x03\x5d\x33\x6a\x8a\x94\xe2\x8a\xe5\x18\xba\xc1\x18\x98\x60\x86\x11\x1e\xea\x9c\x70\x4c\x2f\x83\xf9\x33\x8b\xc7\x30\xc3\x71\x7e\x7f\x1f\x7d\x87\x66\x2d\xd5\xed\x76\x3b\x85\xd7\xb5\x29\x50\x18\x96\x13\x83\x14\xfe\x4a\xea\x1c\x4d\x12\x7b\x48\xb7\x89\x33\x71\x0b\x85\xc2\x45\x1a\x58\xd6\xf5\x34\x8e\x73\x2a\xde\xeb\x28\xe7\xb2\xa6\x0b\x4e\x14\x46\xb9\x2c\x63\xf2\x9e\xdc\xc5\x9c\x65\x3a\x36\x6b\x66\x0c\xaa\x30\x93\xd2\x68\xa3\x48\x15\x5f\x47\xd7\xd1\x9f\xe3\x5c\xeb\xb8\x9b\x8b\x4a\x26\xa2\x5c\xeb\x00\x14\xf2\x34\xd0\x66\xc3\x51\x17\x88\x26\x80\x78\xfe\xc7\xe8\x2e\xa4\x30\x21\x59\xa3\x96\x25\xc6\x2f\xa2\x3f\x47\x13\x47\xb2\x3f\xfd\x38\x55\x4b\x56\xe7\x8a\x55\x06\xb4\xca\x9f\x4c\xf7\xfd\x6f\x35\xaa\x4d\x7c\x1d\x5d\x46\x97\xcd\xc0\xd1\x79\xaf\x83\x79\x12\x7b\x84\xf3\x4f\xc2\x1d\x0a\x69\x36\xf1\x55\xf4\x22\xba\x8c\x2b\x92\xdf\x92\x25\xd2\x96\x92\x5d\x8a\xda\xc9\xcf\x46\xf7\x21\x1d\xbe\x3f\x54\xe1\xe7\x20\x56\xca\x12\x85\x89\xde\xeb\xf8\x2a\xba\xbc\x89\x26\xed\xc4\x31\x7e\x47\xc0\x2a\xcd\x92\x3a\x8b\x56\xa8\xac\xe5\xf2\x30\x47\x61\x50\xc1\xbd\x9d\x3d\x2b\x99\x08\x0b\x64\xcb\xc2\x4c\xe1\x72\x32\x39\x9f\x9d\x9a\x5d\x15\x7e\x9a\x32\x5d\x71\xb2\x99\xc2\x82\xe3\x9d\x9f\x22\x9c\x2d\x45\xc8\x0c\x96\x7a\x0a\x1e\xb3\x5b\xd8\x3a\x9a\x95\x92\x4b\x85\x5a\x37\xc4\x2a\xa9\x99\x61\x52\x4c\xad\x45\x11\xc3\x56\x78\x0a\x56\x57\x44\x1c\x6d\x20\x99\x96\xbc\x36\x78\xc0\x48\xc6\x65\x7e\xeb\xe7\x9c\x37\xf7\x0f\x91\x4b\x2e\xd5\x14\xd6\x05\x6b\xb6\x81\x23\x04\x95\xc2\x06\x3d\x54\x84\x52\x26\x96\x53\x78\x55\x35\xe7\x81\x92\xa8\x25\x13\x53\x98\xec\xb6\x24\x71\x2b\xc6\x24\xf6\x81\xeb\xd9\x59\x92\x49\xba\x71\x3a\xa4\x6c\x05\x39\x27\x5a\xa7\xc1\x81\x88\x5d\x40\xda\x03\xb0\x71\x88\x30\xd1\x2e\xed\xad\x29\xb9\x0e\xc0\x11\x4a\x03\xcf\x44\x98\x49\x63\x64\x39\x85\x4b\xcb\x5e\xb3\xe5\x00\x1f\x0f\xf9\x32\xbc\xbc\x6a\x17\xcf\x92\xe2\xb2\x45\x62\xf0\xce\x84\x4e\x3f\x9d\x66\x82\x79\xc2\xda\xbd\x0b\x02\x0b\x12\x66\xc4\x14\x01\x10\xc5\x48\x58\x30\x4a\x51\xa4\x81\x51\x35\x5a\x3b\x62\x73\xe8\x87\xbf\x07\xa2\x5f\x71\xd9\xf2\x15\x53\xb6\x6a\x8e\xd5\x7b\x3c\x38\xe1\xc3\x87\xb8\x81\xe6\x41\x2e\x16\x1a\x4d\xd8\x3b\x53\x0f\x98\x89\xaa\x36\xe1\x52\xc9\xba\xea\xd6\xcf\x12\x37\x0b\x8c\xa6\x41\xad\x78\xd0\x84\x7f\xf7\x68\x36\x55\x23\x8a\xa0\x3b\xb8\x54\x65\x68\x35\xa1\x24\x0f\xa0\xe2\x24\xc7\x42\x72\x8a\x2a\x0d\x7e\x94\x39\x23\x1c\x84\x3f\x33\xfc\xf4\xc3\xbf\x43\xa3\x32\x26\x96\xb0\x91\xb5\x82\x6f\x4c\x81\x0a\xeb\x12\x08\xa5\xd6\x5c\xa3\x28\xea\x31\xe2\x6c\xf7\x98\xd5\x30\x33\x62\x07\x75\x96\x64\xb5\x31\xb2\x03\xcc\x8c\x80\xcc\x88\x90\xe2\x82\xd4\xdc\x00\x55\xb2\xa2\x72\x2d\x42\x23\x97\x4b\x9b\xe9\xfc\x21\xfc\xa6\x00\x28\x31\xa4\x59\x4a\x83\x16\xb6\xd5\x21\xd1\x95\xac\xea\xaa\xd1\xa2\x9f\xc4\xbb\x8a\x08\x8a\xd4\xea\x9c\x6b\x0c\xe6\xdf\xb2\x15\x42\x89\xfe\x2c\x67\x87\x26\x91\x13\x85\x26\xec\x23\x3d\x32\x8c\x24\xf6\xcc\xf8\x23\x41\xf3\x97\xd4\xbc\xc5\xd4\x1d\xa1\x44\x51\xc3\xde\x28\x54\x36\xae\x04\xf3\xfb\x7b\x45\xc4\x12\xe1\x39\xa3\x77\x63\x78\x4e\x4a\x59\x0b\x03\xd3\x14\xa2\xd7\xee\x51\x6f\xb7\x7b\xd8\x01\x12\xce\xe6\x09\x79\xcc\xbc\x41\x8a\x9c\xb3\xfc\x36\x0d\x0c\x43\x95\xde\xdf\x5b\xe4\xdb\xed\x0c\xee\xef\xd9\x02\x9e\x47\x3f\x60\x4e\x2a\x93\x17\x64\xbb\x5d\xaa\xf6\x39\xc2\x3b\xcc\x6b\x83\xc3\xd1\xfd\x3d\x72\x8d\xdb\xad\xae\xb3\x92\x99\x61\xbb\xdd\xce\x0b\xba\xdd\x5a\x9e\x1b\x3e\xb7\x5b\x88\x2d\x52\x41\xf1\x0e\x9e\x47\xdf\xa3\x62\x92\x6a\xf0\xf0\x49\x4c\xe6\x49\xcc\xd9\xbc\xd9\xb7\x2f\xa4\xb8\xe6\x3b\x7b\x89\xad\xc1\x74\x76\xee\xdc\xc6\xb1\xda\xe7\xf4\x84\x17\x2c\xc3\x8e\xfb\xc6\x1e\x34\x33\x78\x8b\x9b\x34\xb8\xbf\xef\xef\x6d\x56\x73\xc2\x79\x46\xac\x5c\xfc\xd1\xba\x4d\xbf\xa3\xb5\xd3\x15\xd3\xae\xa4\x9a\xb7\x1c\xec\xd8\x7e\xa2\x5b\x1f\x04\x2e\x23\xab\x29\x5c\x5f\xf5\xa2\xd6\x29\x8f\x7f\x75\xe0\xf1\xd7\x27\x81\x2b\x22\x90\x83\xfb\x0d\x75\x49\x78\xfb\xdc\x78\x4b\xcf\xf9\x0e\x37\x85\x36\x46\x77\xac\x75\xb1\x7e\x32\x03\xb9\x42\xb5\xe0\x72\x3d\x05\x52\x1b\x39\x83\x92\xdc\x75\xf9\xee\x7a\x32\xe9\xf3\x6d\x4b\x41\x92\x71\x74\xd1\x45\xe1\x6f\x35\x6a\xa3\xbb\x58\xe2\x97\xdc\xaf\x0d\x29\x14\x85\x46\x7a\x20\x0d\x4b\xd1\x8a\xd6\x41\xf5\x54\xdf\x09\xf3\x24\xef\x0b\x29\xbb\x14\xd2\x67\xa3\x41\xdd\xcb\x76\xc1\x3c\x31\x6a\x07\x77\x96\x18\xfa\x51\x29\x40\xd9\x12\xef\xa1\x0c\xe0\x23\x9a\x3d\x7b\x85\xa8\x7c\x7d\x61\x4d\x16\xdc\x30\x89\x0d\xfd\x04\xca\xd6\x08\x33\xa2\xf1\x29\xe4\x5d\xa6\xdf\x91\x77\xc3\x4f\xa5\x5f\x20\x51\x26\x43\x62\x9e\xc2\xc0\xa2\x16\xb4\x77\x7e\x17\x3b\x3f\x95\x81\x5a\xb0\x15\x2a\xcd\xcc\xe6\xa9\x1c\x20\xdd\xb1\xe0\xc7\xfb\x2c\x24\xb1\x51\x8f\xdb\x5a\x7f\xf0\x99\x9c\xfb\x43\x25\xc9\xf5\xfc\x6f\x72\x0d\x54\xa2\x06\x53\x30\x0d\x36\xb9\x7e\x9d\xc4\xc5\x75\x07\x52\xcd\xdf\xd9\x05\x27\x54\x58\xb8\xd2\x02\x98\x06\x55\x0b\x97\x79\xa5\x00\x53\xe0\x7e\x39\xd2\x24\xe9\x08\xde\x49\x5b\xd2\xad\x50\x18\x28\x09\x67\x39\x93\xb5\x06\x92\x1b\xa9\x34\x2c\x94\x2c\x01\xef\x0a\x52\x6b\x63\x11\xd9\xf0\x41\x56\x84\x71\xe7\x4b\x4e\xa5\x20\x15\x90\x3c\xaf\xcb\xda\x96\xa4\x62\x09\x28\x64\xbd\x2c\x1a\x5e\x8c\x04\x9f\x98\xb8\x14\xcb\x8e\x1f\x5d\x91\x12\x88\x31\x24\xbf\xd5\x63\x68\xa3\x02\x10\x85\x60\x18\x52\xbb\x2b\x47\x65\xeb\x06\xc8\x65\x59\x4a\x01\xd7\x8a\x42\x45\x94\xd9\x58\x5a\x2e\xbd\x45\xf0\x5a\x6c\xa4\x40\x28\xc8\xca\xb1\x06\xdf\x32\xf3\xb7\x3a\x1b\xc3\x3b\x7f\x9f\x18\xc3\xb7\x52\x2e\x39\x5e\x58\x0e\xff\x4a\x72\xcc\xa4\xbc\x6d\xb7\x43\x49\x36\x2d\xe1\xe6\x1c\x6b\x66\x0a\xe6\x05\x55\xa1\x2a\x2d\x0e\x0a\x9c\x95\xcc\xe8\x28\x89\xab\x5d\x6c\xdd\x65\x69\x1e\x16\x52\xb1\xdf\x6d\x89\xc3\x3b\x7d\x01\x24\xd4\x1c\xc4\x99\x36\x4c\x3a\x03\xe0\xb8\x30\x53\x78\xe1\xc3\xe4\xa1\x49\x2f\x99\x29\xea\x2c\x24\xfc\xa4\x53\xb5\x68\xdd\x3d\xd3\xa6\x9f\x29\x5c\xfb\xe2\xd6\x97\x15\xd4\xf4\x42\x22\x3d\x30\x3c\x4f\xf7\xe6\xa6\xba\xeb\x58\xe9\x2a\xe4\x49\x87\xc4\xda\xc3\xbe\x60\x56\x6c\x27\xdb\x5c\x21\x31\x08\x04\x12\x72\x70\x61\x5e\x32\x6d\x22\xcf\xbd\xbb\x72\x05\x60\x88\x5a\xa2\x49\x83\xff\x26\x99\xac\xcd\x34\xe3\x44\xdc\x06\x73\x0b\x67\x33\xbc\x93\xf7\xe9\x9a\x10\xb0\xcc\x90\x52\xa4\xc0\x84\x91\x4e\x23\x4d\x07\x02\x86\x76\xb0\x60\x1c\x5d\x91\xea\x7c\x42\x0c\xac\x36\xad\xc6\x47\x51\x92\xa9\x78\xfe\x46\x56\x9b\xb0\x22\xda\xa0\xdb\x6a\x09\x6a\x57\x8b\x76\xd8\x48\x26\x57\x08\xbe\xea\xcd\xe4\x1d\x10\x41\x61\xc1\x14\x02\x59\x93\xcd\x57\x49\x4c\xdd\x1d\xa5\x95\xe3\x1f\x57\x66\x73\xb3\xfd\xa2\x34\xd9\x79\x47\x49\x6e\x4f\x2a\xb2\x61\xda\x29\x91\x39\xa9\xc7\x66\x8d\x68\xbe\xb6\x21\x39\xfd\xc1\x23\x64\x62\x79\x7e\x35\xf1\x91\xc6\x3e\x58\xf4\xe7\x57\x13\x2b\xe1\xf3\xab\xc9\xe4\x6e\xf2\xc4\xbf\xf3\xab\x89\x14\xe7\x57\x13\x53\xe0\xf9\xd5\xe4\xfc\xea\xba\x1f\xa3\xfc\x4c\x6b\x1d\x16\x0a\xb5\xa5\xd6\x86\xae\x87\x4c\xcc\xb1\xfb\x21\x1b\x73\x06\x72\x6c\x61\x1a\x86\xba\x56\x4a\xd6\xc2\x56\x3b\x60\xcf\xfc\x24\x2b\x3b\x12\xa3\xae\xab\x4a\x2a\x13\xf5\xc5\x49\xec\xfd\x96\xa3\x8e\x6f\x26\x2f\x6f\x5e\x3d\xca\xbe\xb3\x58\x77\x86\xff\x73\xab\x5d\xba\xb0\x19\x56\xbc\xd6\xb6\xb4\x64\xf6\x4e\xf7\x45\x99\xb0\x8f\xeb\xf0\x3d\xaf\xf5\x18\xaa\x3a\xe3\x4c\x17\x40\x40\xe0\x1a\x12\x6d\x94\x14\xcb\xb9\x9b\xcd\x93\xb8\x19\x42\x25\xb5\xf9\x83\x11\xe7\x0f\x99\x83\xa5\xf7\xff\x14\x74\x16\x4d\xaa\xfb\xa2\x54\xd6\xe6\xdf\x2f\x55\x5f\x47\xee\xbb\x5e\xaf\xa3\x56\x92\xce\x77\x0b\xe4\x55\x6c\xab\x91\x5a\x30\xb3\x89\x7d\x14\x94\x22\xfe\x9a\xd1\xf4\xea\xe6\xea\xd5\xab\xab\x17\xff\x72\xf3\xf2\xe5\xd5\xcd\x8b\x97\x0f\x39\x76\x67\x14\x1f\xef\xd7\x5d\xed\xc9\x7b\x35\xdf\x3f\x64\x0d\x39\x11\x60\x14\xc9\x6f\xbd\x10\x6a\xa5\xac\x10\x2a\xf4\xe7\xef\x4a\xab\x0c\xb9\x5c\x3b\x10\x4f\x67\xc1\x90\xbb\x3a\x4b\x23\x42\x21\xd7\x50\xd6\xb9\x93\xb5\x2d\xa7\xd0\x2e\xac\x09\x33\x50\x0b\xc3\xb8\x57\x81\xa9\x95\xab\xc6\x70\xaf\x1a\x3a\xba\x6d\x27\x58\xce\xdf\xd9\x1c\x7d\x54\x84\x76\xf7\x64\x50\xf8\xc6\x83\x43\xa5\xa4\xc1\xdc\xca\x11\xc8\x92\x30\xa1\xad\x04\x5c\xbd\x85\xe5\x13\xee\xd1\xdd\x53\xf3\xb0\xeb\x09\xbb\xe5\x38\x86\x6f\xb9\xcc\x08\x87\x95\x75\x85\x8c\xdb\x02\x5a\x42\x21\xed\xd1\x7b\xd2\xd2\x86\x98\x5a\x83\x5c\xb8\x59\xcf\xb9\xdd\xbf\x22\xca\x56\xa9\x58\x56\x06\xd2\xa6\xa3\x69\xe7\x34\xaa\x55\xd3\xa7\xb5\x43\xc3\x50\xed\xad\x77\x52\x4f\xe1\xe7\x5f\x67\xcf\x1a\x56\xfe\x82\x0b\x26\x6c\xc6\x5d\xd4\xc2\x1f\xd9\x14\xc4\x34\x15\x95\x86\x9c\x4b\x5d\x2b\xcf\x21\x55\xb2\x02\xcb\x65\x8b\xa9\xc5\x6c\x17\x2a\x47\xad\x45\x32\x2c\x88\x2e\x46\x4d\x43\x56\xa1\xd3\x52\xb7\xd6\xce\x9f\x2d\xa4\x82\xa1\x45\xc0\xd2\xc9\x0c\x58\xd2\xe2\x8d\x38\x8a\xa5\x29\x66\xc0\x2e\x2e\x3a\xe0\x33\xb6\x80\x61\x0b\xf1\x33\xfb\x35\x32\x77\x91\xa5\x02\x69\x0a\x7d\x6a\x8e\x60\x83\x47\x57\x9c\xe5\x38\x64\x63\xb8\x1c\xcd\xda\xd5\x4c\x21\xb9\x6d\x47\x8d\x1e\xfd\x3f\xf7\xbb\x9d\xed\x4b\xc6\x09\x7f\x4f\x36\xbe\xdb\xa2\x81\xb8\x22\x0e\x6a\xc5\xa1\xf1\x19\xaf\x82\x4e\x21\x0e\xae\x2f\x95\x23\xbb\x6c\x1e\x1a\x9b\x6a\x8f\xe0\xd1\x44\x1a\x05\x1d\xfe\xdb\x8f\x7f\xff\x2e\xd2\x46\x31\xb1\x64\x8b\xcd\xf0\xbe\x56\x7c\x0a\xcf\x87\xc1\x3f\xd5\x8a\x07\xa3\x9f\x27\xbf\x46\x2b\xc2\x6b\x1c\x3b\x7d\x4f\xdd\xef\x11\x95\x31\x34\x8f\x53\xd8\x27\xb8\x1d\x8d\x66\xa7\x3b\x53\xbd\x46\x9a\x42\x8d\x66\x68\x01\x3b\xc3\x3f\x94\x11\x81\x12\x4d\x21\x9d\xeb\x2a\xcc\xa5\x10\x98\x1b\xa8\x2b\x29\x1a\x91\x00\x97\x5a\xef\x0c\xb1\x85\x48\x8f\x8d\xc2\x6a\xb9\xb5\xee\x73\xb8\xb2\xda\x9d\x74\xaa\x6d\x90\xa5\x2e\x48\xff\x27\x66\x3f\xca\xfc\x16\xcd\x30\x58\x6b\x1b\x1c\x03\xb8\x00\x2e\x73\x62\xf1\x45\x85\x0d\xd5\x17\x10\xc4\xa4\x62\x41\xa3\xfc\x2d\x20\xd7\xf8\x61\x64\x4f\xc2\xe5\x5f\x94\x78\x4e\x2f\x2e\xbc\x3f\xb5\x9a\x93\xa2\x44\xad\xc9\x12\xfb\x27\x74\x97\xd9\xee\x28\x56\x10\xa5\x5e\x42\x0a\x4e\xc3\x15\x51\x1a\x3d\x48\x44\x89\x21\xad\xb9\x5a\x71\x38\xb0\x34\x05\x51\x73\xbe\xb3\x72\xef\x55\xb3\xd6\x7e\xf7\xc0\x23\x9f\xe2\xbe\x4a\x53\xa8\x05\x75\x3a\xa2\xbb\x9d\xd6\x7a\x7c\xdf\x63\x14\xd9\x54\xb4\xdb\x31\x9a\xf5\xdd\x61\x0f\x1b\xd2\x0f\xa1\x43\x7a\x88\x0f\xe9\x03\x08\x5d\x9b\xe9\x31\x7c\xbe\x2d\xd5\x43\xe7\x26\x1e\xc0\x26\xea\x32\x43\xf5\x18\x3a\xdf\x66\x6a\xd0\x39\x51\xbf\x15\xa6\xb7\x77\x0c\x97\xaf\x46\x0f\x60\x47\xa5\xe4\x83\xc8\x85\x34\x9b\xe1\x3d\x27\x1b\x9b\x4f\x61\x60\x64\xf5\xc6\x75\x85\x06\x63\x97\xe4\xa7\xd0\x61\x18\xbb\x7e\xff\x14\x06\x6e\x64\xd7\x59\x89\x6e\xd7\xcb\xc9\x64\x32\x86\xf6\x45\xd9\xbf\x12\xeb\xc5\xaa\xc6\xed\x03\xfc\xe8\x3a\xcf\x6d\xad\xf1\x29\x1c\x35\x38\x3a\x9e\x9a\xf1\x27\x70\xd5\x25\x97\x3d\xb6\xe0\x4f\x7f\x82\xa3\xd5\x7d\x33\x8e\x63\xf8\x0f\xa2\x6e\x5d\x0f\xa7\x52\xb8\x72\x7d\x9e\x0e\xbe\x64\x5a\xbb\x36\x8a\x06\x2a\x05\x36\x7b\x3e\x2e\x6f\x1c\xf1\xd8\x80\xc1\x1c\x26\x87\x0c\xda\x78\xda\xcb\x2b\x27\xd2\x4d\x0f\xef\x7e\x26\x69\x25\x72\x22\x51\xb1\x12\xe1\xab\x14\x82\xa0\xbf\xf9\x08\xc2\x02\x74\xc8\xce\x34\x9a\x77\x5e\x17\xc3\x26\xbd\x9e\x4a\x7e\xa3\x31\x5c\x4f\x26\x93\xd1\x11\x13\xdb\x9d\x78\x5f\x57\xb6\xee\x02\x22\x36\x2e\xd2\x75\xb2\x75\x95\x9e\xad\xa1\x6c\x9c\xe3\x90\x4b\xce\x7d\xd1\xd3\x6c\xb5\x02\x6e\xfa\x5c\x29\x84\x97\xb3\x13\x69\xb8\x27\xc9\xde\xd1\x0e\xd5\x73\x42\xf6\x87\x2a\xda\x97\xd9\x01\x70\x78\xb9\xa7\x94\x3d\x7d\x9d\x56\xcc\x59\xc7\x37\xdb\x49\xf4\x40\x5d\x3b\x7d\x1d\xca\xac\xc7\xbf\xc7\x73\x71\xf9\xc4\x63\x74\xcb\x55\xad\x8b\xe1\x01\xa3\xa3\xd9\xb1\x6e\xde\x1a\x54\xc4\xa0\x7b\x75\xe1\x74\x81\xc2\xd8\x1a\xfb\x50\x25\xae\xfa\x56\x18\x2a\x14\x14\x55\x5b\x93\xf8\xcb\x84\xad\x20\xf7\x54\xe6\x6f\x1c\x7d\x73\xfa\x48\x87\x71\x35\x9d\x14\x08\x00\x70\xe0\x04\xce\x50\xf7\x2c\xd5\x02\x23\x27\x95\x46\x0a\x29\xf8\xef\x16\x86\xa3\xa8\x16\xec\x6e\x38\x0a\x9b\xf1\x21\x8e\x76\x7d\xd6\xdd\x2d\x5b\xb6\x2f\x52\x08\x12\xa3\x80\xd1\x74\x60\x93\xf0\xa9\x8a\xef\x02\x82\xc1\x7c\xc7\x41\x7f\x2b\x40\x62\xe8\xdc\xb5\xae\xfd\x35\xf1\x97\x20\x23\xf9\xed\xd2\xdd\xbd\xa6\xb6\x56\x1b\x1e\xa1\x25\x2b\x62\x88\x72\x58\x47\x33\xd8\x81\x37\x57\xd1\xdc\x2a\x67\x06\xfe\xc6\xeb\x3a\xe4\xd0\xbd\x55\x72\xa3\x4c\x2a\x8a\x2a\x54\x84\xb2\x5a\x4f\xe1\x45\x75\x37\xfb\xa5\x7d\xeb\xe6\xfa\xf8\x8f\xb2\x5a\x29\x9c\x1f\x71\xd4\xb4\x83\x2f\x20\x48\x62\x0b\xf0\x21\x34\xdd\x61\xfb\xdf\x4b\xc0\x89\xb7\x15\xd0\x7d\xcd\xd0\xcc\x97\x8c\x52\x8e\x96\xe1\x1d\x7a\xeb\x8c\x56\xff\x7d\x97\xda\x27\x09\xcd\x6b\x8a\xdd\x9e\xfd\xda\xea\xc4\x86\xee\x8d\xc7\xc0\x1a\x40\x68\x8f\xcc\x9c\xcc\x9b\x3e\x81\x9b\x56\x03\x27\x8b\xe6\xeb\x17\x5a\x2b\x57\x80\x0d\xc3\xc6\xc0\xc6\x30\xd0\xb6\x78\xa4\x7a\x30\x8a\x8a\xba\x24\x82\xfd\x8e\x43\x9b\x97\x46\x5e\x56\xee\x15\x4a\x70\x1c\x92\x8f\x98\xd9\xbd\xdb\x18\xb4\x39\x6e\xd0\x08\x71\xd0\x6a\xf7\xc5\xae\xa5\x30\x85\xc9\x6c\xf0\x91\x12\x3a\x4d\x25\xcc\x88\x82\xfe\x20\x6c\x93\x2f\x28\x69\xa9\xb7\x6b\x19\x51\x03\xdf\x2b\x71\x05\xbe\x90\xeb\x74\x70\x3d\xe9\x98\xf4\x8a\x76\x7a\x1e\x34\xb6\x76\xa4\x0c\xcb\x65\xeb\x9a\x73\xb8\x9e\x7c\x0e\x6e\x29\x11\x4b\x3c\x3c\x81\x51\xac\x42\x0a\x24\x37\x6c\x85\xff\x0b\x07\xf9\x0c\x42\xfe\x68\x16\xad\x1d\xb6\xc2\x73\x66\xba\xc7\xaf\x5d\xed\x64\xfb\xcf\xd6\xdf\x20\x76\x12\xbe\x80\xe0\xe4\x41\x1e\xb4\xc4\x03\xc0\x03\xd7\x7e\xd8\xef\xdd\x3b\xc1\xe0\x30\xa7\xd8\x6a\xb7\x7b\x9f\x3d\x8a\x0a\x53\xf2\x61\x90\x18\xf7\x5d\x93\xe5\xb9\xc3\xe0\x10\xf8\xe9\xfd\x92\x6e\xbb\x7f\x91\xc9\xb9\xd4\x78\x70\x51\x83\x5e\x71\xd2\x5d\xe6\xda\x4a\x04\xb6\xbb\xcf\xbf\xe2\x18\x7e\x34\x44\x19\x20\xf0\xd3\x5b\xa8\x2b\x4a\x8c\x7f\xfb\x66\xf3\xa3\xef\x48\xb6\xdf\x87\x65\x44\x69\x58\x48\xb5\x26\x8a\x36\x0d\x1e\x53\xe0\xc6\xbd\x7d\x6b\x4b\x3f\x8d\xe6\xad\x8d\x62\x2b\xc2\x87\x47\x17\xc7\xe7\xc3\x41\xd4\x57\xf9\x60\x14\x21\xc9\x8b\x63\x40\x97\xb1\x3a\xba\x29\x7c\xe7\xae\x00\xc3\xe7\x43\x53\x30\x3d\x8a\x88\x31\x6a\x38\xd8\x33\x86\xc1\xc8\xea\xf5\xb2\x77\x25\xeb\xb6\x27\x7b\x6e\xf5\x18\x8e\x5d\x31\xdd\x15\x02\x2d\x78\xae\xf5\xd0\xdb\xd5\x60\xdc\xc3\xbd\x6f\x56\x83\xf3\x41\xa7\xa8\x9d\x7b\xef\xce\x91\x9e\xe4\x64\x0f\xf5\xc0\x7a\xd9\xe0\x88\x3c\xa1\xf4\x8d\xf5\x9f\x61\x70\xc2\xd3\x0f\xad\x63\xd4\x09\xdb\xc7\xeb\x47\xa5\xec\xbf\xa4\x79\x40\xc4\x8c\x0e\x46\x91\xae\x33\xdf\xdc\x18\xbe\xec\x2e\x60\x2d\x98\x33\xde\xc3\x54\x70\x54\x50\x58\x12\xfb\x45\x45\x78\x50\x84\x3c\x92\x35\xda\xcb\xbc\x3b\xd5\x76\x6c\x05\x3e\x19\x75\xbd\xb1\x6f\xb4\x2d\xae\x7c\x5b\x78\x8d\x99\x76\x0d\x02\x68\xec\xdd\xb5\x83\x7c\xdb\xe7\xf5\xf7\x6f\x7b\xad\x9f\xce\x23\x86\x0e\x7b\xf7\xe9\xe6\xa9\x46\xcb\xc9\x6f\x45\xd7\xeb\x75\xe4\xdf\x76\xb8\x16\x6f\xd7\x89\x89\x49\xc5\xa2\xf7\x3a\x00\xa2\x37\x22\x07\x8a\x0b\x54\xf3\x1e\xfa\xa6\x3d\x93\xc4\xfe\x2b\xc6\x24\xf6\x1f\x6a\xff\x4f\x00\x00\x00\xff\xff\xf1\xa6\xb6\xb8\xb9\x2d\x00\x00") +var _faucetHtml = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xcc\x7a\x6d\x73\xe3\x36\x92\xf0\x67\xcf\xaf\xe8\xf0\x19\xaf\xa4\xc7\x22\x29\xdb\x33\xb3\x3e\x89\x54\x6a\x76\x36\x9b\x9d\xab\xbb\x24\x95\x4c\xea\x6e\x2b\x9b\xba\x02\x89\x96\x88\x31\x08\x30\x00\x28\x59\x71\xe9\xbf\x5f\x01\x20\x29\xea\xc5\x1e\xcf\xcb\xdd\xc5\x1f\x64\x12\x68\x74\x37\xfa\x1d\x0d\x26\x5f\xfd\xf5\xfb\x37\xef\xfe\xf1\xc3\x37\x50\x98\x92\xcf\x9f\x25\xf6\x1f\x70\x22\x96\x69\x80\x22\x98\x3f\x3b\x4b\x0a\x24\x74\xfe\xec\xec\x2c\x29\xd1\x10\xc8\x0b\xa2\x34\x9a\x34\xa8\xcd\x22\xbc\x09\x76\x13\x85\x31\x55\x88\xbf\xd5\x6c\x95\x06\xff\x19\xfe\xfc\x3a\x7c\x23\xcb\x8a\x18\x96\x71\x0c\x20\x97\xc2\xa0\x30\x69\xf0\xf6\x9b\x14\xe9\x12\x7b\xeb\x04\x29\x31\x0d\x56\x0c\xd7\x95\x54\xa6\x07\xba\x66\xd4\x14\x29\xc5\x15\xcb\x31\x74\x2f\x63\x60\x82\x19\x46\x78\xa8\x73\xc2\x31\xbd\x0c\xe6\xcf\x2c\x1e\xc3\x0c\xc7\xf9\xfd\x7d\xf4\x1d\x9a\xb5\x54\xb7\xdb\xed\x14\x5e\xd7\xa6\x40\x61\x58\x4e\x0c\x52\xf8\x1b\xa9\x73\x34\x49\xec\x21\xdd\x22\xce\xc4\x2d\x14\x0a\x17\x69\x60\x59\xd7\xd3\x38\xce\xa9\x78\xaf\xa3\x9c\xcb\x9a\x2e\x38\x51\x18\xe5\xb2\x8c\xc9\x7b\x72\x17\x73\x96\xe9\xd8\xac\x99\x31\xa8\xc2\x4c\x4a\xa3\x8d\x22\x55\x7c\x1d\x5d\x47\x7f\x8e\x73\xad\xe3\x6e\x2c\x2a\x99\x88\x72\xad\x03\x50\xc8\xd3\x40\x9b\x0d\x47\x5d\x20\x9a\x00\xe2\xf9\xa7\xd1\x5d\x48\x61\x42\xb2\x46\x2d\x4b\x8c\x5f\x44\x7f\x8e\x26\x8e\x64\x7f\xf8\x71\xaa\x96\xac\xce\x15\xab\x0c\x68\x95\x3f\x99\xee\xfb\xdf\x6a\x54\x9b\xf8\x3a\xba\x8c\x2e\x9b\x17\x47\xe7\xbd\x0e\xe6\x49\xec\x11\xce\x3f\x0b\x77\x28\xa4\xd9\xc4\x57\xd1\x8b\xe8\x32\xae\x48\x7e\x4b\x96\x48\x5b\x4a\x76\x2a\x6a\x07\xbf\x18\xdd\x87\x74\xf8\xfe\x50\x85\x5f\x82\x58\x29\x4b\x14\x26\x7a\xaf\xe3\xab\xe8\xf2\x26\x9a\xb4\x03\xc7\xf8\x1d\x01\xab\x34\x4b\xea\x2c\x5a\xa1\xb2\x96\xcb\xc3\x1c\x85\x41\x05\xf7\x76\xf4\xac\x64\x22\x2c\x90\x2d\x0b\x33\x85\xcb\xc9\xe4\x7c\x76\x6a\x74\x55\xf8\x61\xca\x74\xc5\xc9\x66\x0a\x0b\x8e\x77\x7e\x88\x70\xb6\x14\x21\x33\x58\xea\x29\x78\xcc\x6e\x62\xeb\x68\x56\x4a\x2e\x15\x6a\xdd\x10\xab\xa4\x66\x86\x49\x31\xb5\x16\x45\x0c\x5b\xe1\x29\x58\x5d\x11\x71\xb4\x80\x64\x5a\xf2\xda\xe0\x01\x23\x19\x97\xf9\xad\x1f\x73\xde\xdc\xdf\x44\x2e\xb9\x54\x53\x58\x17\xac\x59\x06\x8e\x10\x54\x0a\x1b\xf4\x50\x11\x4a\x99\x58\x4e\xe1\x55\xd5\xec\x07\x4a\xa2\x96\x4c\x4c\x61\xb2\x5b\x92\xc4\xad\x18\x93\xd8\x07\xae\x67\x67\x49\x26\xe9\xc6\xe9\x90\xb2\x15\xe4\x9c\x68\x9d\x06\x07\x22\x76\x01\x69\x0f\xc0\xc6\x21\xc2\x44\x3b\xb5\x37\xa7\xe4\x3a\x00\x47\x28\x0d\x3c\x13\x61\x26\x8d\x91\xe5\x14\x2e\x2d\x7b\xcd\x92\x03\x7c\x3c\xe4\xcb\xf0\xf2\xaa\x9d\x3c\x4b\x8a\xcb\x16\x89\xc1\x3b\x13\x3a\xfd\x74\x9a\x09\xe6\x09\x6b\xd7\x2e\x08\x2c\x48\x98\x11\x53\x04\x40\x14\x23\x61\xc1\x28\x45\x91\x06\x46\xd5\x68\xed\x88\xcd\xa1\x1f\xfe\x1e\x88\x7e\xc5\x65\xcb\x57\x4c\xd9\xaa\xd9\x56\xef\xf1\x60\x87\x0f\x6f\xe2\x06\x9a\x07\xb9\x58\x68\x34\x61\x6f\x4f\x3d\x60\x26\xaa\xda\x84\x4b\x25\xeb\xaa\x9b\x3f\x4b\xdc\x28\x30\x9a\x06\xb5\xe2\x41\x13\xfe\xdd\xa3\xd9\x54\x8d\x28\x82\x6e\xe3\x52\x95\xa1\xd5\x84\x92\x3c\x80\x8a\x93\x1c\x0b\xc9\x29\xaa\x34\xf8\x49\xe6\x8c\x70\x10\x7e\xcf\xf0\xf3\x8f\xff\x06\x8d\xca\x98\x58\xc2\x46\xd6\x0a\xbe\x31\x05\x2a\xac\x4b\x20\x94\x5a\x73\x8d\xa2\xa8\xc7\x88\xb3\xdd\x63\x56\xc3\xcc\x88\x1d\xd4\x59\x92\xd5\xc6\xc8\x0e\x30\x33\x02\x32\x23\x42\x8a\x0b\x52\x73\x03\x54\xc9\x8a\xca\xb5\x08\x8d\x5c\x2e\x6d\xa6\xf3\x9b\xf0\x8b\x02\xa0\xc4\x90\x66\x2a\x0d\x5a\xd8\x56\x87\x44\x57\xb2\xaa\xab\x46\x8b\x7e\x10\xef\x2a\x22\x28\x52\xab\x73\xae\x31\x98\x7f\xcb\x56\x08\x25\xfa\xbd\x9c\x1d\x9a\x44\x4e\x14\x9a\xb0\x8f\xf4\xc8\x30\x92\xd8\x33\xe3\xb7\x04\xcd\x5f\x52\xf3\x16\x53\xb7\x85\x12\x45\x0d\x7b\x6f\xa1\xb2\x71\x25\x98\xdf\xdf\x2b\x22\x96\x08\xcf\x19\xbd\x1b\xc3\x73\x52\xca\x5a\x18\x98\xa6\x10\xbd\x76\x8f\x7a\xbb\xdd\xc3\x0e\x90\x70\x36\x4f\xc8\x63\xe6\x0d\x52\xe4\x9c\xe5\xb7\x69\x60\x18\xaa\xf4\xfe\xde\x22\xdf\x6e\x67\x70\x7f\xcf\x16\xf0\x3c\xfa\x11\x73\x52\x99\xbc\x20\xdb\xed\x52\xb5\xcf\x11\xde\x61\x5e\x1b\x1c\x8e\xee\xef\x91\x6b\xdc\x6e\x75\x9d\x95\xcc\x0c\xdb\xe5\x76\x5c\xd0\xed\xd6\xf2\xdc\xf0\xb9\xdd\x42\x6c\x91\x0a\x8a\x77\xf0\x3c\xfa\x01\x15\x93\x54\x83\x87\x4f\x62\x32\x4f\x62\xce\xe6\xcd\xba\x7d\x21\xc5\x35\xdf\xd9\x4b\x6c\x0d\xa6\xb3\x73\xe7\x36\x8e\xd5\x3e\xa7\x27\xbc\x60\x19\x76\xdc\x37\xf6\xa0\x99\xc1\x5b\xdc\xa4\xc1\xfd\x7d\x7f\x6d\x33\x9b\x13\xce\x33\x62\xe5\xe2\xb7\xd6\x2d\xfa\x1d\xad\x9d\xae\x98\x76\x25\xd5\xbc\xe5\x60\xc7\xf6\x13\xdd\xfa\x20\x70\x19\x59\x4d\xe1\xfa\xaa\x17\xb5\x4e\x79\xfc\xab\x03\x8f\xbf\x3e\x09\x5c\x11\x81\x1c\xdc\x6f\xa8\x4b\xc2\xdb\xe7\xc6\x5b\x7a\xce\x77\xb8\x28\xb4\x31\xba\x63\xad\x8b\xf5\x93\x19\xc8\x15\xaa\x05\x97\xeb\x29\x90\xda\xc8\x19\x94\xe4\xae\xcb\x77\xd7\x93\x49\x9f\x6f\x5b\x0a\x92\x8c\xa3\x8b\x2e\x0a\x7f\xab\x51\x1b\xdd\xc5\x12\x3f\xe5\x7e\x6d\x48\xa1\x28\x34\xd2\x03\x69\x58\x8a\x56\xb4\x0e\xaa\xa7\xfa\x4e\x98\x27\x79\x5f\x48\xd9\xa5\x90\x3e\x1b\x0d\xea\x5e\xb6\x0b\xe6\x89\x51\x3b\xb8\xb3\xc4\xd0\x8f\x4a\x01\xca\x96\x78\x0f\x65\x00\x1f\xd1\xec\xde\x2b\x44\xe5\xeb\x0b\x6b\xb2\xe0\x5e\x93\xd8\xd0\xcf\xa0\x6c\x8d\x30\x23\x1a\x9f\x42\xde\x65\xfa\x1d\x79\xf7\xfa\xb9\xf4\x0b\x24\xca\x64\x48\xcc\x53\x18\x58\xd4\x82\xf6\xf6\xef\x62\xe7\xe7\x32\x50\x0b\xb6\x42\xa5\x99\xd9\x3c\x95\x03\xa4\x3b\x16\xfc\xfb\x3e\x0b\x49\x6c\xd4\xe3\xb6\xd6\x7f\xf9\x42\xce\xfd\xa1\x92\xe4\x7a\xfe\x77\xb9\x06\x2a\x51\x83\x29\x98\x06\x9b\x5c\xbf\x4e\xe2\xe2\xba\x03\xa9\xe6\xef\xec\x84\x13\x2a\x2c\x5c\x69\x01\x4c\x83\xaa\x85\xcb\xbc\x52\x80\x29\x70\xbf\x1c\x69\x92\x74\x04\xef\xa4\x2d\xe9\x56\x28\x0c\x94\x84\xb3\x9c\xc9\x5a\x03\xc9\x8d\x54\x1a\x16\x4a\x96\x80\x77\x05\xa9\xb5\xb1\x88\x6c\xf8\x20\x2b\xc2\xb8\xf3\x25\xa7\x52\x90\x0a\x48\x9e\xd7\x65\x6d\x4b\x52\xb1\x04\x14\xb2\x5e\x16\x0d\x2f\x46\x82\x4f\x4c\x5c\x8a\x65\xc7\x8f\xae\x48\x09\xc4\x18\x92\xdf\xea\x31\xb4\x51\x01\x88\x42\x30\x0c\xa9\x5d\x95\xa3\xb2\x75\x03\xe4\xb2\x2c\xa5\x80\x6b\x45\xa1\x22\xca\x6c\x2c\x2d\x97\xde\x22\x78\x2d\x36\x52\x20\x14\x64\xe5\x58\x83\x6f\x99\xf9\x7b\x9d\x8d\xe1\x9d\x3f\x4f\x8c\xe1\x5b\x29\x97\x1c\x2f\x2c\x87\x7f\x23\x39\x66\x52\xde\xb6\xcb\xa1\x24\x9b\x96\x70\xb3\x8f\x35\x33\x05\xf3\x82\xaa\x50\x95\x16\x07\x05\xce\x4a\x66\x74\x94\xc4\xd5\x2e\xb6\xee\xb2\x34\x0f\x0b\xa9\xd8\xef\xb6\xc4\xe1\x9d\xbe\x00\x12\x6a\x0e\xe2\x4c\x1b\x26\x9d\x01\x70\x5c\x98\x29\xbc\xf0\x61\xf2\xd0\xa4\x97\xcc\x14\x75\x16\x12\x7e\xd2\xa9\x5a\xb4\xee\x9c\x69\xd3\xcf\x14\xae\x7d\x71\xeb\xcb\x0a\x6a\x7a\x21\x91\x1e\x18\x9e\xa7\x7b\x73\x53\xdd\x75\xac\x74\x15\xf2\xa4\x43\x62\xed\x61\x5f\x30\x2b\xb6\x93\x6d\xae\x90\x18\x04\x02\x09\x39\x38\x30\x2f\x99\x36\x91\xe7\xde\x1d\xb9\x02\x30\x44\x2d\xd1\xa4\xc1\x7f\x91\x4c\xd6\x66\x9a\x71\x22\x6e\x83\xb9\x85\xb3\x19\xde\xc9\xfb\x74\x4d\x08\x58\x66\x48\x29\x52\x60\xc2\x48\xa7\x91\xa6\x03\x01\x43\xfb\xb2\x60\x1c\x5d\x91\xea\x7c\x42\x0c\xac\x36\xad\xc6\x47\x51\x92\xa9\x78\xfe\x46\x56\x9b\xb0\x22\xda\xa0\x5b\x6a\x09\x6a\x57\x8b\x76\xd8\x48\x26\x57\x08\xbe\xea\xcd\xe4\x1d\x10\x41\x61\xc1\x14\x02\x59\x93\xcd\x57\x49\x4c\xdd\x19\xa5\x95\xe3\xa7\x2b\xb3\x39\xd9\xfe\xa1\x34\xd9\x79\x47\x49\x6e\x4f\x2a\xb2\x61\xda\x29\x91\x39\xa9\xc7\x66\x8d\x68\xbe\xb6\x21\x39\xfd\xd1\x23\x64\x62\x79\x7e\x35\xf1\x91\xc6\x3e\x58\xf4\xe7\x57\x13\x2b\xe1\xf3\xab\xc9\xe4\x6e\xf2\xc4\xbf\xf3\xab\x89\x14\xe7\x57\x13\x53\xe0\xf9\xd5\xe4\xfc\xea\xba\x1f\xa3\xfc\x48\x6b\x1d\x16\x0a\xb5\xa5\xd6\x86\xae\x87\x4c\xcc\xb1\xfb\x21\x1b\x73\x06\x72\x6c\x61\x1a\x86\xba\x56\x4a\xd6\xc2\x56\x3b\x60\xf7\xfc\x24\x2b\x3b\x12\xa3\xae\xab\x4a\x2a\x13\xf5\xc5\x49\xec\xf9\x96\xa3\x8e\x6f\x26\x2f\x6f\x5e\x3d\xca\xbe\xb3\x58\xb7\x87\xff\x75\xab\x5d\xba\xb0\x19\x56\xbc\xd6\xb6\xb4\x64\xf6\x4c\xf7\x87\x32\x61\x1f\xd7\xe1\x07\x5e\xeb\x31\x54\x75\xc6\x99\x2e\x80\x80\xc0\x35\x24\xda\x28\x29\x96\x73\x37\x9a\x27\x71\xf3\x0a\x95\xd4\xe6\x13\x23\xce\x27\x99\x83\xa5\xf7\x7f\x14\x74\x16\x4d\xaa\xfb\x43\xa9\xac\xcd\xbf\x7f\x54\x7d\x1d\xb9\xef\x7a\xbd\x8e\x5a\x49\x3a\xdf\x2d\x90\x57\xb1\xad\x46\x6a\xc1\xcc\x26\xf6\x51\x50\x8a\xf8\x6b\x46\xd3\xab\x9b\xab\x57\xaf\xae\x5e\xfc\xcb\xcd\xcb\x97\x57\x37\x2f\x5e\x3e\xe4\xd8\x9d\x51\x7c\xba\x5f\xfb\xd3\xed\x77\xf2\x75\x6d\x8a\xee\x68\xeb\xed\xa5\x3d\x52\xd9\xc2\x99\x12\xb1\xb4\x79\xe7\x53\x6d\xa8\x16\xf6\x7c\xf0\x05\xaa\x10\x67\x46\x8f\x70\xf6\x99\xa6\xd5\x9a\x8f\xb5\x14\x59\x1b\xbb\xc3\xb6\xc7\xc6\xa4\xe8\xcc\x69\x0c\x9a\x95\x15\xdf\x40\xbe\xd3\xfa\x69\xbb\x7a\x50\x29\x1f\x34\xab\x7d\xb5\x79\x23\x73\x45\x79\x29\x29\xda\x62\x5c\xd7\x3a\xc7\xca\x5d\xbe\xd8\x02\xf7\x2f\x9b\xdf\x89\x30\x4c\x60\x5b\x08\x47\xf0\xbd\xe0\x1b\xa8\x35\xc2\x42\x2a\xa0\x98\xd5\xcb\xa5\xab\xde\x15\x54\x8a\xad\x6c\xdd\xd5\x64\x3e\xdd\x58\x45\x67\x14\xbd\x86\x83\x3d\x89\xf0\xde\xc1\xe0\x1f\xb2\x86\x9c\x08\x30\x8a\xe4\xb7\xde\x53\x6a\xa5\xac\xa7\x54\xe8\x77\xd3\xd5\xdf\x19\x72\xb9\x76\x20\x7e\xdf\x0b\x86\xdc\x15\xe3\x1a\x11\x0a\xb9\x86\xb2\xce\x9d\x43\xda\x9a\xdb\x6d\x62\x4d\x98\x81\x5a\x18\xc6\xbd\x3c\x4d\xad\x5c\xc9\x8e\x7b\x25\xf3\x51\x4b\x26\xc1\x72\xfe\xce\x16\x72\x47\x27\x95\xae\x99\x02\x0a\xdf\x78\x70\xa8\x94\x34\x98\x5b\x85\x02\x59\x12\x26\xb4\xd5\x88\x2b\xca\xb1\x7c\x42\xb3\xa5\x7b\x6a\x1e\x76\x17\x07\x6e\x3a\x8e\xe1\x5b\x2e\x33\xc2\x61\x65\x2d\x3d\xe3\xf6\x94\x25\xa1\x90\x76\xeb\x3d\x69\x69\x43\x4c\xad\x41\x2e\xdc\xa8\xe7\xdc\xae\x5f\x11\x65\x35\x88\x65\x65\x20\x6d\xda\xde\x76\x4c\xa3\x5a\x35\xcd\x7c\xfb\x6a\x18\xaa\xbd\xf9\x4e\xea\x29\xfc\xf2\xeb\xec\x59\xc3\xca\x5f\x71\xe1\x4c\xc2\xda\xb7\xdf\xb2\x29\x88\x69\xca\x6e\x0d\x39\x97\xba\x56\x9e\x43\xaa\x64\x05\x96\xcb\x16\x53\x8b\xd9\x4e\x54\x8e\x5a\x8b\x64\x58\x10\x5d\x8c\x9a\xae\xbd\x42\xa7\xa5\x6e\xae\x1d\x3f\xb3\x56\x37\xb4\x08\x58\x3a\x99\x01\x4b\x5a\xbc\x11\x47\xb1\x34\xc5\x0c\xd8\xc5\x45\x07\x7c\xc6\x16\x30\x6c\x21\x7e\x61\xbf\x46\xe6\x2e\xb2\x54\x20\x4d\xa1\x4f\xcd\x11\x6c\xf0\xe8\x8a\xb3\x1c\x87\x6c\x0c\x97\xa3\x59\x3b\x9b\x29\x24\xb7\xed\x5b\xa3\x47\xff\xcf\xfd\x6e\x67\xfb\x92\x71\xc2\xdf\x93\x8d\x6f\xc9\x69\x20\xae\xd2\x87\x5a\x71\x68\x7c\xd8\xab\xa0\x53\x88\x83\xeb\x4b\xe5\xc8\x2e\x9b\x87\xc6\xa6\xda\x2d\x78\x34\x91\x46\x41\x87\xff\xfa\xd3\xf7\xdf\x45\xda\x28\x26\x96\x6c\xb1\x19\xde\xd7\x8a\x4f\xe1\xf9\x30\xf8\x7f\xb5\xe2\xc1\xe8\x97\xc9\xaf\xd1\x8a\xf0\x1a\xc7\x4e\xdf\x53\xf7\x7b\x44\x65\x0c\xcd\xe3\x14\xf6\x09\x6e\x47\xa3\xd9\xe9\xf6\x65\xaf\xdb\xaa\x50\xa3\x19\x5a\xc0\xce\xf0\x0f\x65\x44\xa0\x44\x53\x48\xe7\xba\x0a\x73\x29\x04\xe6\x06\xea\x4a\x8a\x46\x24\xc0\xa5\xd6\x3b\x43\x6c\x21\xd2\x63\xa3\x68\xe0\x53\x97\xac\xff\x03\xb3\x9f\x64\x7e\x8b\x66\x38\x1c\xae\x99\xa0\x72\x1d\x71\xe9\x43\x6d\x64\x9d\x54\xe6\x92\x43\x9a\xa6\xd0\x64\xd1\x60\x04\x5f\x43\xb0\xd6\x36\x9f\x06\x30\xb5\x8f\xf6\x69\x04\x17\x70\xb8\xbc\xb0\xf9\xfe\x02\x82\x98\x54\x2c\x18\x79\x77\x68\x05\x2f\x45\x89\x5a\x93\x25\xf6\x19\x74\x0d\x8b\xce\xc8\xec\x3e\x4a\xbd\x84\x14\x9c\x82\x2a\xa2\x34\x7a\x90\x88\x12\x43\x5a\x6b\xb3\x36\xeb\xc0\xd2\x14\x44\xcd\xf9\xce\x48\xbd\x53\xcc\x5a\xf3\xdb\x03\x8f\x7c\xae\xf9\x2a\x4d\xa1\x16\xd4\x89\x98\xee\x56\x5a\xe5\xfb\xde\xd6\x28\xb2\x79\x61\xb7\x62\x34\xeb\x5b\xf3\x1e\x36\xa4\x1f\x42\x87\xf4\x10\x1f\xd2\x07\x10\xba\x56\xe2\x63\xf8\x7c\xeb\xb1\x87\xce\x0d\x3c\x80\x4d\xd4\x65\x86\xea\x31\x74\xbe\x95\xd8\xa0\x73\xa2\x7e\x2b\x4c\x6f\xed\x18\x2e\x5f\x8d\x1e\xc0\x8e\x4a\xc9\x07\x91\x0b\x69\x36\xc3\x7b\x4e\x36\xb6\x66\x82\x81\x91\xd5\x1b\xd7\xf9\x1b\x8c\x5d\xc6\x9d\x42\x87\x61\xec\xee\x74\xa6\x30\x70\x6f\x76\x9e\x95\xe8\x56\xbd\x9c\x4c\x26\x63\x68\x2f\x43\xff\x42\xac\x13\xaa\x1a\xb7\x0f\xf0\xa3\xeb\x3c\xb7\x79\xff\x73\x38\x6a\x70\x74\x3c\x35\xef\x9f\xc1\x55\x97\x1b\xf6\xd8\x82\x3f\xfd\x09\x8e\x66\xf7\xcd\x38\x8e\xe1\xdf\x89\xba\x75\x7d\xba\x4a\xe1\xca\xf5\xf2\x3a\xf8\x92\x69\xed\x5a\x65\x1a\xa8\x14\xd8\xac\xf9\xb8\xb0\x7f\xc4\x63\x03\x06\x73\x98\x1c\x32\x68\xc3\x61\x2f\x2d\x9c\xc8\x16\x3d\xbc\xfb\x89\xe0\x6c\xdb\xa7\xb7\xb7\x92\x95\x08\x5f\xa5\x10\x04\xfd\xc5\x47\x10\x16\xa0\x43\x76\xa6\xd1\xbc\xf3\xba\x18\x36\xd9\xf1\x54\xee\x1a\x8d\xe1\x7a\x32\x99\x8c\x8e\x98\xd8\xee\xc4\xfb\xba\xb2\x65\x13\x10\xb1\x71\x21\xb1\x93\xad\x2b\x1c\x6d\x09\x64\x43\x1a\x87\x5c\x72\xee\x6b\x96\x66\xa9\x15\x70\xd3\xcb\x4c\x21\xbc\x9c\x9d\xc8\xa2\x3d\x49\xf6\xb6\x76\xa8\x9e\x13\xb2\x3f\x54\xd1\xbe\xcc\x0e\x80\xc3\xcb\x3d\xa5\xec\xe9\xeb\xb4\x62\xce\x3a\xbe\xd9\x4e\xa2\x07\xea\xda\xe9\xeb\x50\x66\x3d\xfe\x3d\x9e\x8b\xcb\x27\x6e\xa3\x9b\xae\x6a\x5d\x0c\x0f\x18\x1d\xcd\x8e\x75\xf3\xd6\xa0\xb2\x55\xb2\xb4\x29\xcb\xea\xc2\x1e\x05\x14\x1e\xa9\xc4\x95\xea\x0a\x43\x85\x82\xa2\x6a\x4b\x0a\x5f\xd9\xdb\x02\x70\x4f\x65\xfe\x54\xd9\x37\xa7\x8f\x74\x18\x57\x92\x49\x81\x00\x00\x07\x4e\xe0\x0c\x75\xcf\x52\x2d\x30\x72\x52\x69\xa4\x90\x82\xff\x36\x65\x38\x8a\x6a\xc1\xee\x86\xa3\xb0\x79\x3f\xc4\xd1\xce\xcf\xba\x63\x62\xcb\xf6\x45\x0a\x41\x62\x14\x30\x9a\x0e\x02\xb8\x38\xe5\x82\x36\xeb\x0e\xe6\x3b\x0e\xfa\x4b\x01\x12\x43\xe7\xee\x7a\xc2\x9f\xd7\xfe\x19\x64\x24\xbf\x5d\xba\x83\xd0\xd4\x96\x5a\xc3\x23\xb4\x64\x45\x0c\x51\x0e\xeb\x68\x06\x3b\xf0\xe6\xa0\x98\x5b\xe5\xcc\xc0\x9f\x48\xdd\x2d\x08\x74\x37\x87\xee\x2d\x93\x8a\xa2\x0a\x15\xa1\xac\xd6\x53\x78\x51\xdd\xcd\xfe\xd9\xde\xac\xba\xbb\x9a\x47\x59\xad\x14\xce\x8f\x38\x6a\x5a\xfe\x17\x10\x24\xb1\x05\xf8\x10\x9a\x6e\xb3\xfd\x6f\x62\xe0\xc4\x8d\x14\x74\x5f\xac\x34\xe3\x25\xa3\x94\xa3\x65\x78\x87\xde\x3a\xa3\xd5\x7f\xdf\xa5\xf6\x49\x42\x73\x15\xb5\x5b\xb3\x05\xe4\x1a\x1f\x59\xd0\xdd\x6a\x0d\xac\x01\x84\x76\xcb\xcc\xc9\xbc\x39\x6c\xbb\x61\x35\x70\xb2\x68\xbe\x70\xa2\xb5\x72\xb5\xd6\x30\x6c\x0c\x6c\x0c\x03\x6d\x6b\x3f\xaa\x07\xa3\xa8\xa8\x4b\x22\xd8\xef\x38\xb4\x79\x69\xe4\x65\xe5\xae\xc9\x82\xe3\x90\x7c\xc4\xcc\xee\xfe\x6a\xd0\xe6\xb8\x41\x23\xc4\x41\xab\xdd\x17\xbb\xb3\xfd\x14\x26\xb3\xc1\x47\x4a\xe8\x34\x95\x30\x23\x0a\xfa\x2f\x61\x9b\x7c\x41\x49\x4b\xbd\x9d\xcb\x88\x1a\xf8\x4e\x86\xab\xcf\x85\x5c\xa7\x83\xeb\x49\xc7\xa4\x57\xb4\xd3\xf3\xa0\xb1\xb5\x23\x65\x58\x2e\x5b\xd7\x9c\xc3\xf5\xe4\x4b\x70\xeb\xbb\x21\x07\x3b\x30\x8a\x55\x48\x81\xe4\x86\xad\xf0\x7f\x60\x23\x5f\x40\xc8\x1f\xcd\xa2\xb5\xc3\x56\x78\xce\x4c\xf7\xf8\xb5\xb3\x9d\x6c\xff\xbf\xf5\x37\x88\x9d\x84\x2f\x20\x38\xb9\x91\x07\x2d\xf1\x00\xf0\xc0\xb5\x1f\xf6\x7b\x77\xef\x1b\x1c\xe6\x14\x5b\xed\x76\xdf\x2c\x8c\xa2\xc2\x94\x7c\x18\x24\xc6\x7d\xbb\x66\x79\xee\x30\x38\x04\x7e\x78\xbf\xa4\xdb\xee\x1f\x64\xec\xf9\x1d\x0f\xce\x59\xd0\x2b\x4e\xba\xb3\x58\x5b\x89\xc0\x76\xf7\x89\x5f\x1c\xc3\x4f\x86\x28\x03\x04\x7e\x7e\x0b\x75\x45\x89\xf1\x37\xac\x36\x3f\xfa\xae\x73\xfb\x0d\x60\x46\x94\x86\x85\x54\x6b\xa2\x68\xd3\x9f\x31\x05\x6e\xdc\x0d\x6b\x5b\xfa\x69\x34\x6f\x6d\x14\x5b\x11\x3e\x3c\x3a\xf7\x3d\x1f\x0e\xa2\xbe\xca\x07\xa3\x08\x49\x5e\x1c\x03\xba\x8c\xd5\xd1\x4d\xe1\x3b\x77\x04\x18\x3e\x1f\x9a\x82\xe9\x51\x44\x8c\x51\xc3\xc1\x9e\x31\x0c\x46\x56\xaf\x97\xbd\x23\x59\xb7\x3c\xd9\x73\xab\xc7\x70\xec\x8a\xe9\xae\x10\x68\xc1\x73\xad\x87\xde\xae\x06\xe3\x1e\xee\x7d\xb3\x1a\x9c\x0f\x3a\x45\xed\xdc\x7b\xb7\x8f\xf4\x24\x27\x7b\xa8\x07\xd6\xcb\x06\x47\xe4\x09\xa5\x6f\xac\xff\x0c\x83\x13\x9e\x7e\x68\x1d\xa3\x4e\xd8\x3e\x5e\x3f\x2a\x65\xff\xb5\xd4\x03\x22\x66\x74\x30\x8a\x74\x9d\xf9\xde\xc4\xf0\x65\x77\x00\x6b\xc1\x9c\xf1\x1e\xa6\x82\xa3\x82\xc2\x92\xd8\x2f\x2a\xc2\x83\x22\xe4\x91\xac\xd1\x90\xf4\xbb\xda\x8e\xad\xc0\x27\xa3\xae\xb5\xf5\x8d\xb6\xc5\x95\x6f\xfd\xaf\x31\xd3\xae\x93\x00\x8d\xbd\xbb\x6e\x8e\xef\xda\xbc\xfe\xe1\x6d\xaf\x73\xd3\x79\xc4\xd0\x61\xef\x3e\xcf\x3d\xd5\x27\x39\xf9\x3d\xf0\x7a\xbd\x8e\xfc\x8d\x96\x6b\xe3\x77\x8d\x94\x98\x54\x2c\x7a\xaf\x03\x20\x7a\x23\x72\xa0\xb8\x40\x35\xef\xa1\x6f\xba\x2b\x49\xec\xbf\x54\x4d\x62\xff\x31\xfe\x7f\x07\x00\x00\xff\xff\x70\x2d\x96\x9f\x9d\x2f\x00\x00") func faucetHtmlBytes() ([]byte, error) { return bindataRead( diff --git a/cmd/puppeth/module_faucet.go b/cmd/puppeth/module_faucet.go index a53e6f61e8..4e78058244 100644 --- a/cmd/puppeth/module_faucet.go +++ b/cmd/puppeth/module_faucet.go @@ -53,10 +53,10 @@ ADD account.pass /account.pass EXPOSE 8080 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}}", \ - "--github.user", "{{.GitHubUser}}", "--github.token", "{{.GitHubToken}}", "--account.json", "/account.json", "--account.pass", "/account.pass" \ - {{if .CaptchaToken}}, "--captcha.token", "{{.CaptchaToken}}", "--captcha.secret", "{{.CaptchaSecret}}"{{end}} \ + "/faucet", "--genesis", "/genesis.json", "--network", "{{.NetworkID}}", "--bootnodes", "{{.Bootnodes}}", "--ethstats", "{{.Ethstats}}", "--ethport", "{{.EthPort}}", \ + "--faucet.name", "{{.FaucetName}}", "--faucet.amount", "{{.FaucetAmount}}", "--faucet.minutes", "{{.FaucetMinutes}}", "--faucet.tiers", "{{.FaucetTiers}}", \ + {{if .GitHubUser}}"--github.user", "{{.GitHubUser}}", "--github.token", "{{.GitHubToken}}", {{end}}"--account.json", "/account.json", "--account.pass", "/account.pass" \ + {{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 @@ -81,7 +81,8 @@ services: - GITHUB_USER={{.GitHubUser}} - GITHUB_TOKEN={{.GitHubToken}} - CAPTCHA_TOKEN={{.CaptchaToken}} - - CAPTCHA_SECRET={{.CaptchaSecret}}{{if .VHost}} + - CAPTCHA_SECRET={{.CaptchaSecret}} + - NO_AUTH={{.NoAuth}}{{if .VHost}} - VIRTUAL_HOST={{.VHost}} - VIRTUAL_PORT=8080{{end}} logging: @@ -114,6 +115,7 @@ func deployFaucet(client *sshClient, network string, bootnodes []string, config "FaucetAmount": config.amount, "FaucetMinutes": config.minutes, "FaucetTiers": config.tiers, + "NoAuth": config.noauth, }) files[filepath.Join(workdir, "Dockerfile")] = dockerfile.Bytes() @@ -132,6 +134,7 @@ func deployFaucet(client *sshClient, network string, bootnodes []string, config "FaucetAmount": config.amount, "FaucetMinutes": config.minutes, "FaucetTiers": config.tiers, + "NoAuth": config.noauth, }) files[filepath.Join(workdir, "docker-compose.yaml")] = composefile.Bytes() @@ -161,6 +164,7 @@ type faucetInfos struct { amount int minutes int tiers int + noauth bool githubUser string githubToken string captchaToken string @@ -179,7 +183,14 @@ func (info *faucetInfos) Report() map[string]string { "Funding tiers": strconv.Itoa(info.tiers), "Captha protection": fmt.Sprintf("%v", info.captchaToken != ""), "Ethstats username": info.node.ethstats, - "GitHub authentication": info.githubUser, + } + if info.githubUser != "" { + report["GitHub authentication"] = info.githubUser + } else { + report["GitHub authentication"] = "disabled, rate-limited" + } + if info.noauth { + report["Debug mode (no auth)"] = "enabled" } if info.node.keyJSON != "" { var key struct { @@ -255,5 +266,6 @@ func checkFaucet(client *sshClient, network string) (*faucetInfos, error) { githubToken: infos.envvars["GITHUB_TOKEN"], captchaToken: infos.envvars["CAPTCHA_TOKEN"], captchaSecret: infos.envvars["CAPTCHA_SECRET"], + noauth: infos.envvars["NO_AUTH"] == "true", }, nil } diff --git a/cmd/puppeth/wizard_faucet.go b/cmd/puppeth/wizard_faucet.go index dbb0965eb1..d5a084f15b 100644 --- a/cmd/puppeth/wizard_faucet.go +++ b/cmd/puppeth/wizard_faucet.go @@ -87,34 +87,38 @@ func (w *wizard) deployFaucet() { 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("Which GitHub user to verify Gists through? (default = none = rate-limited API)") + infos.githubUser = w.readDefaultString("") - 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() + if infos.githubUser == "" { + log.Warn("Funding requests via GitHub will be heavily rate-limited") + } else { + 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 + 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 @@ -129,7 +133,9 @@ func (w *wizard) deployFaucet() { // No previous authorization (or old one discarded) fmt.Println() 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 fmt.Println() fmt.Printf("What is the reCaptcha site key to authenticate human users?\n") @@ -175,7 +181,7 @@ func (w *wizard) deployFaucet() { } } } - if infos.node.keyJSON == "" { + for i := 0; i < 3 && infos.node.keyJSON == ""; i++ { fmt.Println() fmt.Println("Please paste the faucet's funding account key JSON:") infos.node.keyJSON = w.readJSON() @@ -186,9 +192,19 @@ func (w *wizard) deployFaucet() { if _, err := keystore.DecryptKey([]byte(infos.node.keyJSON), infos.node.keyPass); err != nil { 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 fmt.Println() fmt.Printf("Should the faucet be built from scratch (y/n)? (default = no)\n") From 6eb38e02a8e3bd39ba155df0b40560e384e2c6f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9ter=20Szil=C3=A1gyi?= Date: Mon, 23 Oct 2017 12:24:25 +0300 Subject: [PATCH 08/15] cmd/puppeth: fix dashboard iframes, extend with new services --- cmd/puppeth/genesis.go | 171 +++++++++++++++++++ cmd/puppeth/module_dashboard.go | 287 +++++++++++++++++++++++++++----- cmd/puppeth/wizard_dashboard.go | 30 +++- cmd/puppeth/wizard_genesis.go | 2 +- cmd/puppeth/wizard_node.go | 2 +- 5 files changed, 441 insertions(+), 51 deletions(-) diff --git a/cmd/puppeth/genesis.go b/cmd/puppeth/genesis.go index 2b66df43c1..5e36f7fce5 100644 --- a/cmd/puppeth/genesis.go +++ b/cmd/puppeth/genesis.go @@ -28,6 +28,140 @@ import ( "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"` @@ -206,3 +340,40 @@ func newParityChainSpec(network string, genesis *core.Genesis, bootnodes []strin } 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 +} diff --git a/cmd/puppeth/module_dashboard.go b/cmd/puppeth/module_dashboard.go index b08dbbff12..776f2c2191 100644 --- a/cmd/puppeth/module_dashboard.go +++ b/cmd/puppeth/module_dashboard.go @@ -18,6 +18,7 @@ package main import ( "bytes" + "encoding/json" "fmt" "html/template" "math/rand" @@ -77,25 +78,26 @@ var dashboardContent = ` -
-