diff options
author | Janos Guljas <janos@resenje.org> | 2017-12-13 17:23:11 +0800 |
---|---|---|
committer | Janos Guljas <janos@resenje.org> | 2017-12-13 17:40:39 +0800 |
commit | 19982f946735948478b6b7e7706f1b615f171d0d (patch) | |
tree | cbacbdb6f9e6e731c2ebc17bad74e875f4d8ea8b /cmd | |
parent | 1dc19de5da64962a98a37bbc7b93a3895d2eb6e6 (diff) | |
parent | 32516c768ec09e2a71cab5983d2c8b8ae5d92fc7 (diff) | |
download | dexon-19982f946735948478b6b7e7706f1b615f171d0d.tar.gz dexon-19982f946735948478b6b7e7706f1b615f171d0d.tar.zst dexon-19982f946735948478b6b7e7706f1b615f171d0d.zip |
swarm, cmd/swarm: Merge branch 'master' into multiple-ens-endpoints
Merge with changes that implement config file PR #15548.
Field *EnsApi string* in swarm/api.Config is replaced with
*EnsAPIs []string*.
A new field *EnsDisabled bool* is added to swarm/api.Config for
easy way to disable ENS resolving with config file.
Signature of function swarm.NewSwarm is changed and simplified.
Diffstat (limited to 'cmd')
34 files changed, 2855 insertions, 788 deletions
diff --git a/cmd/faucet/faucet.go b/cmd/faucet/faucet.go index 72098e68d..328029fdf 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) @@ -435,13 +437,19 @@ func (f *faucet) apiHandler(conn *websocket.Conn) { ) switch { case strings.HasPrefix(msg.URL, "https://gist.github.com/"): - username, avatar, address, err = authGitHub(msg.URL) + if err = sendError(conn, errors.New("GitHub authentication discontinued at the official request of GitHub")); err != nil { + log.Warn("Failed to send GitHub deprecation to client", "err", err) + return + } + continue case strings.HasPrefix(msg.URL, "https://twitter.com/"): username, avatar, address, err = authTwitter(msg.URL) case strings.HasPrefix(msg.URL, "https://plus.google.com/"): 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 +784,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 5d3b8741b..ab41b2c87 100644 --- a/cmd/faucet/faucet.html +++ b/cmd/faucet/faucet.html @@ -80,11 +80,8 @@ <div class="row" style="margin-top: 32px;"> <div class="col-lg-12"> <h3>How does this work?</h3> - <p>This Ether faucet is running on the {{.Network}} network. To prevent malicious actors from exhausting all available funds or accumulating enough Ether to mount long running spam attacks, requests are tied to certain common 3rd party accounts. Anyone having a GitHub, Twitter, Google+ or Facebook account may request funds within the permitted limits.</p> + <p>This Ether faucet is running on the {{.Network}} network. To prevent malicious actors from exhausting all available funds or accumulating enough Ether to mount long running spam attacks, requests are tied to common 3rd party social network accounts. Anyone having a Twitter, Google+ or Facebook account may request funds within the permitted limits.</p> <dl class="dl-horizontal"> - <dt style="width: auto; margin-left: 40px;"><i class="fa fa-github-alt" aria-hidden="true" style="font-size: 36px;"></i></dt> - <dd style="margin-left: 88px; margin-bottom: 10px;"></i> To request funds via GitHub, create a <a href="https://gist.github.com/" target="_about:blank">gist</a> with your Ethereum address embedded into the content (the file name doesn't matter).<br/>Copy-paste the gists URL into the above input box and fire away!</dd> - <dt style="width: auto; margin-left: 40px;"><i class="fa fa-twitter" aria-hidden="true" style="font-size: 36px;"></i></dt> <dd style="margin-left: 88px; margin-bottom: 10px;"></i> To request funds via Twitter, make a <a href="https://twitter.com/intent/tweet?text=Requesting%20faucet%20funds%20into%200x0000000000000000000000000000000000000000%20on%20the%20%23{{.Network}}%20%23Ethereum%20test%20network." target="_about:blank">tweet</a> with your Ethereum address pasted into the contents (surrounding text doesn't matter).<br/>Copy-paste the <a href="https://support.twitter.com/articles/80586" target="_about:blank">tweets URL</a> into the above input box and fire away!</dd> @@ -93,6 +90,11 @@ <dt style="width: auto; margin-left: 40px;"><i class="fa fa-facebook" aria-hidden="true" style="font-size: 36px;"></i></dt> <dd style="margin-left: 88px; margin-bottom: 10px;"></i> To request funds via Facebook, publish a new <strong>public</strong> post with your Ethereum address embedded into the content (surrounding text doesn't matter).<br/>Copy-paste the <a href="https://www.facebook.com/help/community/question/?id=282662498552845" target="_about:blank">posts URL</a> into the above input box and fire away!</dd> + + {{if .NoAuth}} + <dt class="text-danger" style="width: auto; margin-left: 40px;"><i class="fa fa-unlock-alt" aria-hidden="true" style="font-size: 36px;"></i></dt> + <dd class="text-danger" style="margin-left: 88px; margin-bottom: 10px;"></i> To request funds <strong>without authentication</strong>, simply copy-paste your Ethereum address into the above input box (surrounding text doesn't matter) and fire away.<br/>This mode is susceptible to Byzantine attacks. Only use for debugging or private networks!</dd> + {{end}} </dl> <p>You can track the current pending requests below the input field to see how much you have to wait until your turn comes.</p> {{if .Recaptcha}}<em>The faucet is running invisible reCaptcha protection against bots.</em>{{end}} @@ -126,12 +128,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 6a99f8c6f..7936b158e 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\x5a\x71\x73\xdb\xb6\x92\xff\xdb\xf9\x14\x5b\x5e\xfc\x24\x9d\x4d\x52\xb6\x93\x3c\x9f\x44\xaa\x93\x97\xd7\xd7\xcb\xcd\x5d\xdb\x69\xd3\xb9\x7b\xd3\xd7\xb9\x01\x89\x95\x88\x18\x04\x58\x00\x94\xac\x7a\xf4\xdd\x6f\x00\x90\x14\x29\xc9\x6e\xd2\xe4\xde\x34\x7f\x38\x24\xb0\xd8\x5d\xec\xfe\x16\xbb\x58\x2a\xf9\xe2\xaf\xdf\xbe\x79\xf7\xf7\xef\xbe\x82\xc2\x94\x7c\xf1\x2c\xb1\xff\x01\x27\x62\x95\x06\x28\x82\xc5\xb3\xb3\xa4\x40\x42\x17\xcf\xce\xce\x92\x12\x0d\x81\xbc\x20\x4a\xa3\x49\x83\xda\x2c\xc3\xdb\x60\x3f\x51\x18\x53\x85\xf8\x4b\xcd\xd6\x69\xf0\x3f\xe1\x8f\xaf\xc3\x37\xb2\xac\x88\x61\x19\xc7\x00\x72\x29\x0c\x0a\x93\x06\x6f\xbf\x4a\x91\xae\xb0\xb7\x4e\x90\x12\xd3\x60\xcd\x70\x53\x49\x65\x7a\xa4\x1b\x46\x4d\x91\x52\x5c\xb3\x1c\x43\xf7\x72\x09\x4c\x30\xc3\x08\x0f\x75\x4e\x38\xa6\x57\xc1\xe2\x99\xe5\x63\x98\xe1\xb8\x78\x78\x88\xbe\x41\xb3\x91\xea\x6e\xb7\x9b\xc1\xeb\xda\x14\x28\x0c\xcb\x89\x41\x0a\x7f\x23\x75\x8e\x26\x89\x3d\xa5\x5b\xc4\x99\xb8\x83\x42\xe1\x32\x0d\xac\xea\x7a\x16\xc7\x39\x15\xef\x75\x94\x73\x59\xd3\x25\x27\x0a\xa3\x5c\x96\x31\x79\x4f\xee\x63\xce\x32\x1d\x9b\x0d\x33\x06\x55\x98\x49\x69\xb4\x51\xa4\x8a\x6f\xa2\x9b\xe8\xcf\x71\xae\x75\xdc\x8d\x45\x25\x13\x51\xae\x75\x00\x0a\x79\x1a\x68\xb3\xe5\xa8\x0b\x44\x13\x40\xbc\xf8\x7d\x72\x97\x52\x98\x90\x6c\x50\xcb\x12\xe3\x17\xd1\x9f\xa3\xa9\x13\xd9\x1f\x7e\x5a\xaa\x15\xab\x73\xc5\x2a\x03\x5a\xe5\x1f\x2c\xf7\xfd\x2f\x35\xaa\x6d\x7c\x13\x5d\x45\x57\xcd\x8b\x93\xf3\x5e\x07\x8b\x24\xf6\x0c\x17\x9f\xc4\x3b\x14\xd2\x6c\xe3\xeb\xe8\x45\x74\x15\x57\x24\xbf\x23\x2b\xa4\xad\x24\x3b\x15\xb5\x83\x9f\x4d\xee\x63\x3e\x7c\x7f\xe8\xc2\xcf\x21\xac\x94\x25\x0a\x13\xbd\xd7\xf1\x75\x74\x75\x1b\x4d\xdb\x81\x63\xfe\x4e\x80\x75\x9a\x15\x75\x16\xad\x51\x59\xe4\xf2\x30\x47\x61\x50\xc1\x83\x1d\x3d\x2b\x99\x08\x0b\x64\xab\xc2\xcc\xe0\x6a\x3a\x3d\x9f\x9f\x1a\x5d\x17\x7e\x98\x32\x5d\x71\xb2\x9d\xc1\x92\xe3\xbd\x1f\x22\x9c\xad\x44\xc8\x0c\x96\x7a\x06\x9e\xb3\x9b\xd8\x39\x99\x95\x92\x2b\x85\x5a\x37\xc2\x2a\xa9\x99\x61\x52\xcc\x2c\xa2\x88\x61\x6b\x3c\x45\xab\x2b\x22\x8e\x16\x90\x4c\x4b\x5e\x1b\x3c\x50\x24\xe3\x32\xbf\xf3\x63\x2e\x9a\xfb\x9b\xc8\x25\x97\x6a\x06\x9b\x82\x35\xcb\xc0\x09\x82\x4a\x61\xc3\x1e\x2a\x42\x29\x13\xab\x19\xbc\xaa\x9a\xfd\x40\x49\xd4\x8a\x89\x19\x4c\xf7\x4b\x92\xb8\x35\x63\x12\xfb\x83\xeb\xd9\x59\x92\x49\xba\x75\x3e\xa4\x6c\x0d\x39\x27\x5a\xa7\xc1\x81\x89\xdd\x81\x34\x20\xb0\xe7\x10\x61\xa2\x9d\x1a\xcc\x29\xb9\x09\xc0\x09\x4a\x03\xaf\x44\x98\x49\x63\x64\x39\x83\x2b\xab\x5e\xb3\xe4\x80\x1f\x0f\xf9\x2a\xbc\xba\x6e\x27\xcf\x92\xe2\xaa\x65\x62\xf0\xde\x84\xce\x3f\x9d\x67\x82\x45\xc2\xda\xb5\x4b\x02\x4b\x12\x66\xc4\x14\x01\x10\xc5\x48\x58\x30\x4a\x51\xa4\x81\x51\x35\x5a\x1c\xb1\x05\xf4\x8f\xbf\x47\x4e\xbf\xe2\xaa\xd5\x2b\xa6\x6c\xdd\x6c\xab\xf7\x78\xb0\xc3\xc7\x37\x71\x0b\xcd\x83\x5c\x2e\x35\x9a\xb0\xb7\xa7\x1e\x31\x13\x55\x6d\xc2\x95\x92\x75\xd5\xcd\x9f\x25\x6e\x14\x18\x4d\x83\x5a\xf1\xa0\x39\xfe\xdd\xa3\xd9\x56\x8d\x29\x82\x6e\xe3\x52\x95\xa1\xf5\x84\x92\x3c\x80\x8a\x93\x1c\x0b\xc9\x29\xaa\x34\xf8\x41\xe6\x8c\x70\x10\x7e\xcf\xf0\xe3\xf7\xff\x09\x8d\xcb\x98\x58\xc1\x56\xd6\x0a\xbe\x32\x05\x2a\xac\x4b\x20\x94\x5a\xb8\x46\x51\xd4\x53\xc4\x61\xf7\x58\xd5\x30\x33\x62\x4f\x75\x96\x64\xb5\x31\xb2\x23\xcc\x8c\x80\xcc\x88\x90\xe2\x92\xd4\xdc\x00\x55\xb2\xa2\x72\x23\x42\x23\x57\x2b\x9b\xe9\xfc\x26\xfc\xa2\x00\x28\x31\xa4\x99\x4a\x83\x96\xb6\xf5\x21\xd1\x95\xac\xea\xaa\xf1\xa2\x1f\xc4\xfb\x8a\x08\x8a\xd4\xfa\x9c\x6b\x0c\x16\x5f\xb3\x35\x42\x89\x7e\x2f\x67\x87\x90\xc8\x89\x42\x13\xf6\x99\x1e\x01\x23\x89\xbd\x32\x7e\x4b\xd0\xfc\x4b\x6a\xde\x72\xea\xb6\x50\xa2\xa8\x61\xf0\x16\x2a\x7b\xae\x04\x8b\x87\x07\x45\xc4\x0a\xe1\x39\xa3\xf7\x97\xf0\x9c\x94\xb2\x16\x06\x66\x29\x44\xaf\xdd\xa3\xde\xed\x06\xdc\x01\x12\xce\x16\x09\x79\x0a\xde\x20\x45\xce\x59\x7e\x97\x06\x86\xa1\x4a\x1f\x1e\x2c\xf3\xdd\x6e\x0e\x0f\x0f\x6c\x09\xcf\xa3\xef\x31\x27\x95\xc9\x0b\xb2\xdb\xad\x54\xfb\x1c\xe1\x3d\xe6\xb5\xc1\xf1\xe4\xe1\x01\xb9\xc6\xdd\x4e\xd7\x59\xc9\xcc\xb8\x5d\x6e\xc7\x05\xdd\xed\xac\xce\x8d\x9e\xbb\x1d\xc4\x96\xa9\xa0\x78\x0f\xcf\xa3\xef\x50\x31\x49\x35\x78\xfa\x24\x26\x8b\x24\xe6\x6c\xd1\xac\x1b\x1a\x29\xae\xf9\x1e\x2f\xb1\x05\x4c\x87\x73\x17\x36\x4e\xd5\xbe\xa6\x27\xa2\x60\x15\x76\xda\x37\x78\xd0\xcc\xe0\x1d\x6e\xd3\xe0\xe1\xa1\xbf\xb6\x99\xcd\x09\xe7\x19\xb1\x76\xf1\x5b\xeb\x16\xfd\x8a\x16\xa7\x6b\xa6\x5d\x49\xb5\x68\x35\xd8\xab\xfd\x81\x61\x7d\x70\x70\x19\x59\xcd\xe0\xe6\xba\x77\x6a\x9d\x8a\xf8\x57\x07\x11\x7f\x73\x92\xb8\x22\x02\x39\xb8\xbf\xa1\x2e\x09\x6f\x9f\x9b\x68\xe9\x05\xdf\xe1\xa2\xd0\x9e\xd1\x9d\x6a\xdd\x59\x3f\x9d\x83\x5c\xa3\x5a\x72\xb9\x99\x01\xa9\x8d\x9c\x43\x49\xee\xbb\x7c\x77\x33\x9d\xf6\xf5\xb6\xa5\x20\xc9\x38\xba\xd3\x45\xe1\x2f\x35\x6a\xa3\xbb\xb3\xc4\x4f\xb9\xbf\xf6\x48\xa1\x28\x34\xd2\x03\x6b\x58\x89\xd6\xb4\x8e\xaa\xe7\xfa\xce\x98\x27\x75\x5f\x4a\xd9\xa5\x90\xbe\x1a\x0d\xeb\x5e\xb6\x0b\x16\x89\x51\x7b\xba\xb3\xc4\xd0\x8f\x4a\x01\xca\x96\x78\x8f\x65\x00\x7f\xa2\xd9\xbd\x57\x88\xca\xd7\x17\x16\xb2\xe0\x5e\x93\xd8\xd0\x4f\x90\x6c\x41\x98\x11\x8d\x1f\x22\xde\x65\xfa\xbd\x78\xf7\xfa\xa9\xf2\x0b\x24\xca\x64\x48\xcc\x87\x28\xb0\xac\x05\xed\xed\xdf\x9d\x9d\x9f\xaa\x40\x2d\xd8\x1a\x95\x66\x66\xfb\xa1\x1a\x20\xdd\xab\xe0\xdf\x87\x2a\x24\xb1\x51\x4f\x63\xad\xff\xf2\x99\x82\xfb\xb7\x4a\x92\x9b\xc5\xbf\xcb\x0d\x50\x89\x1a\x4c\xc1\x34\xd8\xe4\xfa\x65\x12\x17\x37\x1d\x49\xb5\x78\x67\x27\x9c\x51\x61\xe9\x4a\x0b\x60\x1a\x54\x2d\x5c\xe6\x95\x02\x4c\x81\xc3\x72\xa4\x49\xd2\x11\xbc\x93\xb6\xa4\x5b\xa3\x30\x50\x12\xce\x72\x26\x6b\x0d\x24\x37\x52\x69\x58\x2a\x59\x02\xde\x17\xa4\xd6\xc6\x32\xb2\xc7\x07\x59\x13\xc6\x5d\x2c\x39\x97\x82\x54\x40\xf2\xbc\x2e\x6b\x5b\x92\x8a\x15\xa0\x90\xf5\xaa\x68\x74\x31\x12\x7c\x62\xe2\x52\xac\x3a\x7d\x74\x45\x4a\x20\xc6\x90\xfc\x4e\x5f\x42\x7b\x2a\x00\x51\x08\x86\x21\xb5\xab\x72\x59\x96\x52\xc0\x8d\xa2\x50\x11\x65\xb6\xa0\x87\xb5\x05\xc9\x73\x97\xe5\x22\x78\x2d\xb6\x52\x20\x14\x64\xed\x34\x84\x77\xfe\x3a\x71\x09\x5f\x4b\xb9\xe2\x78\x61\x15\xfc\x1b\xc9\x31\x93\xb2\x5b\x06\x25\xd9\xb6\x72\x9b\x6d\x6c\x98\x29\x98\xb7\x53\x85\xaa\xb4\x3c\x28\x70\x56\x32\xa3\xa3\x24\xae\xf6\x47\xeb\x3e\x49\xf3\xb0\x90\x8a\xfd\x6a\x2b\x1c\xde\x3f\x47\xcd\xc1\x29\xd3\x1e\x92\xce\xfd\x1c\x97\x66\x06\x2f\xfc\x21\x79\x08\xe8\xe6\x2a\x74\x0a\xcd\x2d\x4f\x77\xc5\xb4\x99\x67\x06\x37\xbe\xae\xf5\x15\x05\x35\x3d\x0d\xe8\x01\xe6\xbc\xd0\xdb\xdb\xea\xbe\xd3\xa3\x2b\x8e\xa7\x1d\x13\x0b\x85\xa1\x51\xd6\xac\x67\xcf\x92\xdc\x21\x10\x48\xc8\xc1\x55\xb9\x51\xda\x5d\xb4\x98\x6b\x14\xc4\x66\x83\x68\xbe\xb4\x31\x9c\x7e\xef\x19\x32\xb1\x3a\xbf\x9e\x7a\x68\xda\x07\xcb\xfe\xfc\x7a\xca\x84\x91\xe7\xd7\xd3\xe9\xfd\xf4\x03\xff\x9d\x5f\x4f\xa5\x38\xbf\x9e\x9a\x02\xcf\xaf\xa7\xe7\xd7\x37\x7d\x50\xfb\x91\xb6\xc4\xb4\x54\xa8\xad\xb4\x16\xeb\x01\x18\xa2\x56\x68\xd2\xe0\x7f\x49\x26\x6b\x33\xcb\x38\x11\x77\xc1\xc2\xa9\x6b\xcb\x0e\x87\x82\xd3\x85\x2a\x54\x44\x5b\x48\x58\x8d\x1d\x4a\x9a\xa6\x88\x86\xb1\xae\x95\x92\xb5\xb0\xe9\x11\xec\x9e\x5d\xa8\x8a\x91\x45\x99\x35\xcc\x24\x4a\x32\x15\x2f\xde\xc8\x6a\x1b\x3a\x26\x6e\xf9\x91\x19\x75\x5d\x55\x52\x99\xa8\x6f\x4e\x62\x2f\x44\x1c\x75\x7c\x3b\x7d\x79\xfb\xea\x49\xf5\xb5\x2d\xb7\xdd\x1e\x3a\x0d\x49\x26\xd7\x08\xbe\xb8\xcf\xe4\x3d\x10\x41\x61\xc9\x14\x02\xd9\x90\xed\x17\x49\x4c\xdd\x55\xec\xd3\x51\xbb\x72\x81\x16\x56\xbc\xd6\xb6\x16\x61\x36\x50\xff\x50\x10\xf6\x27\x01\x7c\xc7\x6b\x7d\x09\x55\x9d\x71\xa6\x0b\x20\x20\x70\x03\x89\x36\x4a\x8a\xd5\xc2\x8d\xe6\xf6\xaa\xea\x5e\xa1\x92\xda\x3c\x85\x06\x2c\x33\xa4\xf4\x04\x1e\x7e\x27\x1c\xac\x3c\xe7\xc2\x7f\xbe\xfb\x96\xcd\xe1\xf8\x87\x72\x59\x7b\x62\xff\x51\xfd\x75\x14\xbe\x9b\xcd\x26\x6a\x2d\xe9\x62\xb7\x40\x5e\xc5\x36\x8d\xd5\x82\x99\x6d\xec\x4f\x41\x29\xe2\x2f\x19\x4d\xaf\x6f\xaf\x5f\xbd\xba\x7e\xf1\x6f\xb7\x2f\x5f\x5e\xdf\xbe\x78\xf9\x58\x60\x77\xa0\xf8\xfd\x71\xed\xaf\x43\xdf\xc8\xd7\xb5\x29\xba\xbb\x90\xc7\x4b\x5b\x83\xdb\x4a\x8b\xda\xbb\xa4\x0a\x7e\x37\x86\x6a\x61\x0b\xca\x90\xf0\x93\xb5\xe0\x47\xa0\xc8\xc1\xe8\x09\xcd\x3e\x11\x5a\x2d\x7c\x2c\x52\x64\x6d\xec\x0e\xdb\xa6\x0c\x93\xa2\x83\xd3\x25\x68\x56\x56\x7c\x0b\xf9\xde\xeb\xa7\x71\xf5\xa8\x53\x7e\x13\x56\x43\xb7\x79\x90\xb9\x2a\xae\x94\x14\x6d\xf5\xa6\x6b\x9d\x63\xe5\xba\xf5\xb6\x22\xfa\xcb\xf6\x57\x22\x0c\x13\xd8\x56\x4e\x11\x7c\x2b\xf8\x16\x6a\x8d\xb0\x94\x0a\x28\x66\xf5\x6a\xe5\xca\x3d\x05\x95\x62\x6b\x62\xb0\x2d\x97\x74\x83\x8a\x0e\x14\xbd\x1b\xaa\x2d\x5d\x79\xaf\x92\xfc\xbb\xac\x21\x27\x02\x8c\x22\xf9\x9d\x8f\x94\x5a\x29\x1b\x29\x15\xfa\xdd\x74\x05\x5b\x86\x5c\x6e\x1c\x89\xdf\xf7\x92\x21\x77\xd5\x9b\x46\x84\x42\x6e\xa0\xac\x73\x17\x90\xb6\x3a\x73\x9b\xd8\x10\x66\xa0\x16\x86\x71\x6f\x4f\x53\x2b\x61\x6b\x3d\x1c\x14\x59\x47\x77\xf8\x04\xcb\xc5\xbb\x02\x4f\x94\xb6\xdd\xed\x1b\x14\xbe\xf1\xe4\x50\x29\x69\x30\xb7\x0e\x05\xb2\x22\x4c\x68\xeb\x11\x57\xc6\x61\xf9\x01\xb7\xf3\xee\xa9\x79\xd8\x77\x9a\xdd\x74\x1c\xc3\xd7\x5c\x66\x84\xc3\xda\x22\x3d\xe3\xb6\x2c\x97\x50\x48\xbb\xf5\x9e\xb5\xb4\x21\xa6\xd6\x20\x97\x6e\xd4\x6b\x6e\xd7\xaf\x89\xb2\x1e\xc4\xb2\x32\x90\x36\x7d\x52\x3b\xa6\x51\xad\x9b\xee\xaf\x7d\x35\x0c\xd5\x60\xbe\xb3\x7a\x0a\x3f\xfd\x3c\x7f\xd6\xa8\xf2\x57\x5c\x3a\x48\x58\x7c\xfb\x2d\x9b\x82\x18\xc8\x15\x12\x83\x1a\x72\x2e\x75\xad\xbc\x86\x54\xc9\x0a\xac\x96\x2d\xa7\x96\xb3\x9d\xa8\x9c\xb4\x96\xc9\xb8\x20\xba\x98\x34\x6d\x5e\x85\xce\x4b\xdd\x5c\x3b\x7e\x66\x51\x37\xb6\x0c\x58\x3a\x9d\x03\x4b\x5a\xbe\x11\x47\xb1\x32\xc5\x1c\xd8\xc5\x45\x47\x7c\xc6\x96\x30\x6e\x29\x7e\x62\x3f\x47\xe6\x3e\xb2\x52\x20\x4d\xa1\x2f\xcd\x09\x6c\xf8\xe8\x8a\xb3\x1c\xc7\xec\x12\xae\x26\xf3\x76\x36\x53\x48\xee\xda\xb7\xc6\x8f\xfe\x3f\xf7\x77\x37\x1f\x5a\xc6\x19\x7f\x60\x1b\xdf\xc3\xd1\x40\x60\xc5\xb4\x81\x5a\x71\x68\x62\xd8\xbb\xa0\x73\x88\xa3\xeb\x5b\xe5\x08\x97\xcd\x43\x83\xa9\x76\x0b\x9e\x4d\xa4\x51\xd0\xf1\x7f\xfc\xf0\xed\x37\x91\x36\x8a\x89\x15\x5b\x6e\xc7\x0f\xb5\xe2\x33\x78\x3e\x0e\xfe\xa5\x56\x3c\x98\xfc\x34\xfd\x39\x5a\x13\x5e\xe3\xa5\xf3\xf7\xcc\xfd\x3d\x92\x72\x09\xcd\xe3\x0c\x86\x02\x77\x93\xc9\xfc\x74\xbf\xab\xd7\x9e\x53\xa8\xd1\x8c\x2d\x61\x07\xfc\x43\x1b\x11\x28\xd1\x14\xd2\x85\xae\xc2\x5c\x0a\x81\xb9\x81\xba\x92\xa2\x31\x09\x70\xa9\xf5\x1e\x88\x2d\x45\x7a\x0c\x8a\x86\x3e\x75\xc9\xfa\xbf\x31\xfb\x41\xe6\x77\x68\xc6\xe3\xf1\x86\x09\x2a\x37\x11\x97\xfe\xa8\x8d\x6c\x90\xca\x5c\x72\x48\xd3\x14\x9a\x2c\x1a\x4c\xe0\x4b\x08\x36\xda\xe6\xd3\x00\x66\xf6\xd1\x3e\x4d\xe0\x02\x0e\x97\x17\x36\xdf\x5f\x40\x10\x93\x8a\x05\x13\x1f\x0e\xad\xe1\xa5\x28\x51\x6b\xb2\xc2\xbe\x82\xee\x86\xdb\x81\xcc\xee\xa3\xd4\x2b\x48\xc1\x39\xa8\x22\x4a\xa3\x27\x89\x28\x31\xa4\x45\x9b\xc5\xac\x23\x4b\x53\x10\x35\xe7\x7b\x90\xfa\xa0\x98\xb7\xf0\x1b\x90\x47\x3e\xd7\x7c\x91\xa6\x50\x0b\xea\x4c\x4c\xf7\x2b\xad\xf3\x7d\x33\x64\x12\xd9\xbc\xb0\x5f\x31\x99\xf7\xd1\x3c\xe0\x86\xf4\xb7\xd8\x21\x3d\xe4\x87\xf4\x11\x86\xae\xf7\xf4\x14\x3f\xdf\xab\xea\xb1\x73\x03\x8f\x70\x13\x75\x99\xa1\x7a\x8a\x9d\xef\x3d\x35\xec\x9c\xa9\xdf\x0a\xd3\x5b\x7b\x09\x57\xaf\x26\x8f\x70\x47\xa5\xe4\xa3\xcc\x85\x34\xdb\xf1\x03\x27\x5b\x5b\x33\xc1\xc8\xc8\xea\x8d\x6b\x15\x8d\x2e\x5d\xc6\x9d\x41\xc7\xe1\xd2\x7d\x04\x98\xc1\xc8\xbd\xd9\x79\x56\xa2\x5b\xf5\x72\x3a\x9d\x5e\x42\xfb\xf5\xec\x2f\xc4\x06\xa1\xaa\x71\xf7\x88\x3e\xba\xce\x73\x9b\xf7\x3f\x45\xa3\x86\x47\xa7\x53\xf3\xfe\x09\x5a\x75\xb9\x61\xa0\x16\xfc\xe9\x4f\x70\x34\x3b\x84\x71\x1c\xc3\x7f\x11\x75\xe7\x1a\x3b\x95\xc2\xb5\x6b\xfe\x74\xf4\x25\xd3\xda\x35\x55\x34\x50\x29\xb0\x59\xf3\x71\xc7\xfe\x91\x8e\x0d\x19\x2c\x60\x7a\xa8\xa0\x3d\x0e\x7b\x69\xe1\x44\xb6\xe8\xf1\x1d\x26\x82\xb3\x5d\x5f\xde\x60\x25\x2b\x11\xbe\x48\x21\x08\xfa\x8b\x8f\x28\x2c\x41\xc7\xec\x4c\xa3\x79\xe7\x7d\x31\x6e\xb2\xe3\xa9\xdc\x35\xb9\x84\x9b\xe9\x74\x3a\x39\x52\x62\xb7\x37\xef\xeb\xca\x96\x4d\x40\xc4\xd6\x1d\x89\x9d\x6d\x5d\xe1\x68\x4b\x20\x7b\xa4\x71\xc8\x25\xe7\xbe\x66\x69\x96\x5a\x03\x37\x4d\xb0\x14\xc2\xab\xf9\x89\x2c\xda\xb3\x64\x6f\x6b\x87\xee\x39\x61\xfb\x43\x17\x0d\x6d\x76\x40\x1c\x5e\x0d\x9c\x32\xf0\xd7\x69\xc7\x9c\x75\x7a\xb3\xbd\x45\x0f\xdc\xb5\xf7\xd7\xa1\xcd\x7a\xfa\x7b\x3e\x17\x57\x1f\xb8\x8d\x6e\xba\xaa\x75\x31\x3e\x50\x74\x32\x3f\xf6\xcd\x5b\x83\xca\x56\xc9\xd2\xa6\x2c\xeb\x0b\x7b\x15\x50\x78\xe4\x12\x57\xaa\x2b\x0c\x15\x0a\x8a\xaa\x2d\x29\x7c\x65\x6f\x0b\xc0\x81\xcb\xfc\xad\xb2\x0f\xa7\x8f\x0c\x18\x57\x92\x49\x81\x00\x00\x07\x41\xe0\x80\x3a\x40\xaa\x25\x46\x4e\x2a\x8d\x14\x52\xf0\x3f\x66\x18\x4f\xa2\x5a\xb0\xfb\xf1\x24\x6c\xde\x0f\x79\xb4\xf3\xf3\xee\x9a\xd8\xaa\x7d\x91\x42\x90\x18\x05\x8c\xa6\xa3\x00\x2e\x4e\x85\xa0\xcd\xba\xa3\xc5\x5e\x83\xfe\x52\x80\xc4\xd0\x85\xeb\x67\xfb\xfb\xda\x3f\x82\x8c\xe4\x77\x2b\x77\x11\x9a\xd9\x52\x6b\x7c\xc4\x96\xac\x89\x21\xca\x71\x9d\xcc\x61\x4f\xde\x5c\x14\x73\xeb\x9c\x39\xf8\x1b\xa9\x6b\x9b\x43\xf7\xa9\xc9\xbd\x65\x52\x51\x54\xa1\x22\x94\xd5\x7a\x06\x2f\xaa\xfb\xf9\x3f\xda\x4f\x71\xae\xb9\xff\xa4\xaa\x95\xc2\xc5\x91\x46\x4d\x93\xf8\x02\x82\x24\xb6\x04\xbf\xc5\xa6\xdb\x6c\xff\x47\x14\x70\xe2\x13\x06\x74\x3f\x71\x68\xc6\x4b\x46\x29\x47\xab\xf0\x9e\xbd\x0d\x46\xeb\xff\x7e\x48\x0d\x45\x42\xf3\xed\x62\xbf\x66\x07\xc8\x35\x3e\xb1\xa0\xfb\x0c\x32\xb2\x00\x08\xed\x96\x99\xb3\x79\x73\xd9\x76\xc3\x6a\xe4\x6c\xd1\xfc\x24\x86\xd6\xca\xd5\x5a\xe3\xb0\x01\xd8\x25\x8c\xb4\xad\xfd\xa8\x1e\x4d\xa2\xa2\x2e\x89\x60\xbf\xe2\xd8\xe6\xa5\x89\xb7\x95\xfb\xae\x12\x1c\x1f\xc9\x47\xca\xec\x3f\x78\x8c\xda\x1c\x37\x6a\x8c\x38\x6a\xbd\xfb\x62\x7f\xb7\x9f\xc1\x74\x3e\xfa\x48\x0b\x9d\x96\x12\x66\x44\x41\xff\x25\x6c\x93\x2f\x28\x69\xa5\xb7\x73\x19\x51\x23\xdf\xc9\x70\xf5\xb9\x90\x9b\x74\x74\x33\xed\x94\xf4\x8e\x76\x7e\x1e\x35\x58\x3b\x72\x86\xd5\xb2\x0d\xcd\x05\xdc\x4c\x3f\x87\xb6\xbe\x1b\x72\xb0\x03\xa3\x58\x85\x14\x48\x6e\xd8\x1a\xff\x1f\x36\xf2\x19\x8c\xfc\xd1\x2a\x5a\x1c\xb6\xc6\x73\x30\x1d\xe8\x6b\x67\x3b\xdb\xfe\xab\x8d\x37\x88\x9d\x85\x2f\x20\x38\xb9\x91\x47\x91\x78\x40\x78\x10\xda\x8f\xc7\xbd\xfb\x50\x18\x1c\xe6\x14\x5b\xed\x76\x1f\xb9\x27\x51\x61\x4a\x3e\x0e\x12\xe3\x7e\xec\x64\x75\xee\x38\x38\x06\x7e\x78\x58\xd2\xed\x86\x17\x19\x7b\x7f\xc7\x83\x7b\x16\xf4\x8a\x93\xee\x2e\xd6\x56\x22\xb0\xdb\xff\x26\x2c\x8e\xe1\x07\x43\x94\x01\x02\x3f\xbe\x85\xba\xa2\xc4\xf8\x4f\x72\x36\x3f\xfa\xae\x73\xfb\xa3\xb1\x8c\x28\x0d\x4b\xa9\x36\x44\xd1\xa6\x3f\x63\x0a\xdc\xba\x4f\x72\x6d\xe9\xa7\xd1\xbc\xb5\xa7\xd8\x9a\xf0\xf1\xd1\xbd\xef\xf9\x78\x14\xf5\x5d\x3e\x9a\x44\x48\xf2\xe2\x98\xd0\x65\xac\x4e\x6e\x0a\xdf\xb8\x2b\xc0\xf8\xf9\xd8\x14\x4c\x4f\x22\x62\x8c\x1a\x8f\x06\x60\x18\x4d\xac\x5f\xaf\x7a\x57\xb2\x6e\x79\x32\x08\xab\xa7\x78\xec\x8b\xe9\xae\x10\x68\xc9\x73\xad\xc7\x1e\x57\xa3\xcb\x1e\xef\x21\xac\x46\xe7\xa3\xce\x51\xfb\xf0\xde\xef\x23\x3d\xa9\xc9\x80\xf5\xc8\x46\xd9\xe8\x48\x3c\xa1\xf4\x8d\x8d\x9f\x71\x70\x22\xd2\x0f\xd1\x31\xe9\x8c\xed\xcf\xeb\x27\xad\xec\x7f\x5e\xf3\x88\x89\x19\x1d\x4d\x22\x5d\x67\xbe\x37\x31\x7e\xd9\x5d\xc0\x5a\x32\x07\xde\xc3\x54\x70\x54\x50\x58\x11\xc3\xa2\x22\x3c\x28\x42\x9e\xc8\x1a\x8d\x48\xbf\xab\xdd\xa5\x35\xf8\x74\xd2\xb5\xb6\xbe\xd2\xb6\xb8\xf2\xad\xff\x0d\x66\xda\x75\x12\xa0\xc1\xbb\xeb\xe6\xf8\xae\xcd\xeb\xef\xde\xf6\x3a\x37\x5d\x44\x8c\x1d\xf7\xee\xf7\x9c\xa7\xfa\x24\x27\x7f\x40\xba\xd9\x6c\x22\xff\x45\xcb\xb5\xf1\xbb\x46\x4a\x4c\x2a\x16\xbd\xd7\x01\x10\xbd\x15\x39\x50\x5c\xa2\x5a\xf4\xd8\x37\xdd\x95\x24\xf6\x3f\x6d\x4c\x62\xff\xeb\xed\xff\x0b\x00\x00\xff\xff\x56\xf8\xb5\xef\xce\x2d\x00\x00") func faucetHtmlBytes() ([]byte, error) { return bindataRead( diff --git a/cmd/geth/accountcmd.go b/cmd/geth/accountcmd.go index 0f53c92b0..0db5c4ce0 100644 --- a/cmd/geth/accountcmd.go +++ b/cmd/geth/accountcmd.go @@ -291,15 +291,28 @@ func ambiguousAddrRecovery(ks *keystore.KeyStore, err *keystore.AmbiguousAddrErr // accountCreate creates a new account into the keystore defined by the CLI flags. func accountCreate(ctx *cli.Context) error { - stack, _ := makeConfigNode(ctx) + cfg := gethConfig{Node: defaultNodeConfig()} + // Load config file. + if file := ctx.GlobalString(configFileFlag.Name); file != "" { + if err := loadConfig(file, &cfg); err != nil { + utils.Fatalf("%v", err) + } + } + utils.SetNodeConfig(ctx, &cfg.Node) + scryptN, scryptP, keydir, err := cfg.Node.AccountConfig() + + if err != nil { + utils.Fatalf("Failed to read configuration: %v", err) + } + password := getPassPhrase("Your new account is locked with a password. Please give a password. Do not forget this password.", true, 0, utils.MakePasswordList(ctx)) - ks := stack.AccountManager().Backends(keystore.KeyStoreType)[0].(*keystore.KeyStore) - account, err := ks.NewAccount(password) + address, err := keystore.StoreKey(keydir, password, scryptN, scryptP) + if err != nil { utils.Fatalf("Failed to create account: %v", err) } - fmt.Printf("Address: {%x}\n", account.Address) + fmt.Printf("Address: {%x}\n", address) return nil } diff --git a/cmd/geth/consolecmd.go b/cmd/geth/consolecmd.go index 2bb452d73..2c6b16687 100644 --- a/cmd/geth/consolecmd.go +++ b/cmd/geth/consolecmd.go @@ -17,8 +17,10 @@ package main import ( + "fmt" "os" "os/signal" + "path/filepath" "strings" "github.com/ethereum/go-ethereum/cmd/utils" @@ -112,7 +114,18 @@ func localConsole(ctx *cli.Context) error { // console to it. func remoteConsole(ctx *cli.Context) error { // Attach to a remotely running geth instance and start the JavaScript console - client, err := dialRPC(ctx.Args().First()) + endpoint := ctx.Args().First() + if endpoint == "" { + path := node.DefaultDataDir() + if ctx.GlobalIsSet(utils.DataDirFlag.Name) { + path = ctx.GlobalString(utils.DataDirFlag.Name) + } + if path != "" && ctx.GlobalBool(utils.TestnetFlag.Name) { + path = filepath.Join(path, "testnet") + } + endpoint = fmt.Sprintf("%s/geth.ipc", path) + } + client, err := dialRPC(endpoint) if err != nil { utils.Fatalf("Unable to attach to remote geth: %v", err) } diff --git a/cmd/puppeth/genesis.go b/cmd/puppeth/genesis.go new file mode 100644 index 000000000..5e36f7fce --- /dev/null +++ b/cmd/puppeth/genesis.go @@ -0,0 +1,379 @@ +// Copyright 2017 The go-ethereum Authors +// This file is part of go-ethereum. +// +// go-ethereum is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// go-ethereum is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>. + +package main + +import ( + "encoding/binary" + "errors" + "math" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/consensus/ethash" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/params" +) + +// cppEthereumGenesisSpec represents the genesis specification format used by the +// C++ Ethereum implementation. +type cppEthereumGenesisSpec struct { + SealEngine string `json:"sealEngine"` + Params struct { + AccountStartNonce hexutil.Uint64 `json:"accountStartNonce"` + HomesteadForkBlock hexutil.Uint64 `json:"homesteadForkBlock"` + EIP150ForkBlock hexutil.Uint64 `json:"EIP150ForkBlock"` + EIP158ForkBlock hexutil.Uint64 `json:"EIP158ForkBlock"` + ByzantiumForkBlock hexutil.Uint64 `json:"byzantiumForkBlock"` + ConstantinopleForkBlock hexutil.Uint64 `json:"constantinopleForkBlock"` + NetworkID hexutil.Uint64 `json:"networkID"` + ChainID hexutil.Uint64 `json:"chainID"` + MaximumExtraDataSize hexutil.Uint64 `json:"maximumExtraDataSize"` + MinGasLimit hexutil.Uint64 `json:"minGasLimit"` + MaxGasLimit hexutil.Uint64 `json:"maxGasLimit"` + GasLimitBoundDivisor *hexutil.Big `json:"gasLimitBoundDivisor"` + MinimumDifficulty *hexutil.Big `json:"minimumDifficulty"` + DifficultyBoundDivisor *hexutil.Big `json:"difficultyBoundDivisor"` + DurationLimit *hexutil.Big `json:"durationLimit"` + BlockReward *hexutil.Big `json:"blockReward"` + } `json:"params"` + + Genesis struct { + Nonce hexutil.Bytes `json:"nonce"` + Difficulty *hexutil.Big `json:"difficulty"` + MixHash common.Hash `json:"mixHash"` + Author common.Address `json:"author"` + Timestamp hexutil.Uint64 `json:"timestamp"` + ParentHash common.Hash `json:"parentHash"` + ExtraData hexutil.Bytes `json:"extraData"` + GasLimit hexutil.Uint64 `json:"gasLimit"` + } `json:"genesis"` + + Accounts map[common.Address]*cppEthereumGenesisSpecAccount `json:"accounts"` +} + +// cppEthereumGenesisSpecAccount is the prefunded genesis account and/or precompiled +// contract definition. +type cppEthereumGenesisSpecAccount struct { + Balance *hexutil.Big `json:"balance"` + Nonce uint64 `json:"nonce,omitempty"` + Precompiled *cppEthereumGenesisSpecBuiltin `json:"precompiled,omitempty"` +} + +// cppEthereumGenesisSpecBuiltin is the precompiled contract definition. +type cppEthereumGenesisSpecBuiltin struct { + Name string `json:"name,omitempty"` + StartingBlock hexutil.Uint64 `json:"startingBlock,omitempty"` + Linear *cppEthereumGenesisSpecLinearPricing `json:"linear,omitempty"` +} + +type cppEthereumGenesisSpecLinearPricing struct { + Base uint64 `json:"base"` + Word uint64 `json:"word"` +} + +// newCppEthereumGenesisSpec converts a go-ethereum genesis block into a Parity specific +// chain specification format. +func newCppEthereumGenesisSpec(network string, genesis *core.Genesis) (*cppEthereumGenesisSpec, error) { + // Only ethash is currently supported between go-ethereum and cpp-ethereum + if genesis.Config.Ethash == nil { + return nil, errors.New("unsupported consensus engine") + } + // Reconstruct the chain spec in Parity's format + spec := &cppEthereumGenesisSpec{ + SealEngine: "Ethash", + } + spec.Params.AccountStartNonce = 0 + spec.Params.HomesteadForkBlock = (hexutil.Uint64)(genesis.Config.HomesteadBlock.Uint64()) + spec.Params.EIP150ForkBlock = (hexutil.Uint64)(genesis.Config.EIP150Block.Uint64()) + spec.Params.EIP158ForkBlock = (hexutil.Uint64)(genesis.Config.EIP158Block.Uint64()) + spec.Params.ByzantiumForkBlock = (hexutil.Uint64)(genesis.Config.ByzantiumBlock.Uint64()) + spec.Params.ConstantinopleForkBlock = (hexutil.Uint64)(math.MaxUint64) + + spec.Params.NetworkID = (hexutil.Uint64)(genesis.Config.ChainId.Uint64()) + spec.Params.ChainID = (hexutil.Uint64)(genesis.Config.ChainId.Uint64()) + + spec.Params.MaximumExtraDataSize = (hexutil.Uint64)(params.MaximumExtraDataSize) + spec.Params.MinGasLimit = (hexutil.Uint64)(params.MinGasLimit.Uint64()) + spec.Params.MaxGasLimit = (hexutil.Uint64)(math.MaxUint64) + spec.Params.MinimumDifficulty = (*hexutil.Big)(params.MinimumDifficulty) + spec.Params.DifficultyBoundDivisor = (*hexutil.Big)(params.DifficultyBoundDivisor) + spec.Params.GasLimitBoundDivisor = (*hexutil.Big)(params.GasLimitBoundDivisor) + spec.Params.DurationLimit = (*hexutil.Big)(params.DurationLimit) + spec.Params.BlockReward = (*hexutil.Big)(ethash.FrontierBlockReward) + + spec.Genesis.Nonce = (hexutil.Bytes)(make([]byte, 8)) + binary.LittleEndian.PutUint64(spec.Genesis.Nonce[:], genesis.Nonce) + + spec.Genesis.MixHash = genesis.Mixhash + spec.Genesis.Difficulty = (*hexutil.Big)(genesis.Difficulty) + spec.Genesis.Author = genesis.Coinbase + spec.Genesis.Timestamp = (hexutil.Uint64)(genesis.Timestamp) + spec.Genesis.ParentHash = genesis.ParentHash + spec.Genesis.ExtraData = (hexutil.Bytes)(genesis.ExtraData) + spec.Genesis.GasLimit = (hexutil.Uint64)(genesis.GasLimit) + + spec.Accounts = make(map[common.Address]*cppEthereumGenesisSpecAccount) + for address, account := range genesis.Alloc { + spec.Accounts[address] = &cppEthereumGenesisSpecAccount{ + Balance: (*hexutil.Big)(account.Balance), + Nonce: account.Nonce, + } + } + spec.Accounts[common.BytesToAddress([]byte{1})].Precompiled = &cppEthereumGenesisSpecBuiltin{ + Name: "ecrecover", Linear: &cppEthereumGenesisSpecLinearPricing{Base: 3000}, + } + spec.Accounts[common.BytesToAddress([]byte{2})].Precompiled = &cppEthereumGenesisSpecBuiltin{ + Name: "sha256", Linear: &cppEthereumGenesisSpecLinearPricing{Base: 60, Word: 12}, + } + spec.Accounts[common.BytesToAddress([]byte{3})].Precompiled = &cppEthereumGenesisSpecBuiltin{ + Name: "ripemd160", Linear: &cppEthereumGenesisSpecLinearPricing{Base: 600, Word: 120}, + } + spec.Accounts[common.BytesToAddress([]byte{4})].Precompiled = &cppEthereumGenesisSpecBuiltin{ + Name: "identity", Linear: &cppEthereumGenesisSpecLinearPricing{Base: 15, Word: 3}, + } + if genesis.Config.ByzantiumBlock != nil { + spec.Accounts[common.BytesToAddress([]byte{5})].Precompiled = &cppEthereumGenesisSpecBuiltin{ + Name: "modexp", StartingBlock: (hexutil.Uint64)(genesis.Config.ByzantiumBlock.Uint64()), + } + spec.Accounts[common.BytesToAddress([]byte{6})].Precompiled = &cppEthereumGenesisSpecBuiltin{ + Name: "alt_bn128_G1_add", StartingBlock: (hexutil.Uint64)(genesis.Config.ByzantiumBlock.Uint64()), Linear: &cppEthereumGenesisSpecLinearPricing{Base: 500}, + } + spec.Accounts[common.BytesToAddress([]byte{7})].Precompiled = &cppEthereumGenesisSpecBuiltin{ + Name: "alt_bn128_G1_mul", StartingBlock: (hexutil.Uint64)(genesis.Config.ByzantiumBlock.Uint64()), Linear: &cppEthereumGenesisSpecLinearPricing{Base: 40000}, + } + spec.Accounts[common.BytesToAddress([]byte{8})].Precompiled = &cppEthereumGenesisSpecBuiltin{ + Name: "alt_bn128_pairing_product", StartingBlock: (hexutil.Uint64)(genesis.Config.ByzantiumBlock.Uint64()), + } + } + return spec, nil +} + +// parityChainSpec is the chain specification format used by Parity. +type parityChainSpec struct { + Name string `json:"name"` + Engine struct { + Ethash struct { + Params struct { + MinimumDifficulty *hexutil.Big `json:"minimumDifficulty"` + DifficultyBoundDivisor *hexutil.Big `json:"difficultyBoundDivisor"` + GasLimitBoundDivisor *hexutil.Big `json:"gasLimitBoundDivisor"` + DurationLimit *hexutil.Big `json:"durationLimit"` + BlockReward *hexutil.Big `json:"blockReward"` + HomesteadTransition uint64 `json:"homesteadTransition"` + EIP150Transition uint64 `json:"eip150Transition"` + EIP160Transition uint64 `json:"eip160Transition"` + EIP161abcTransition uint64 `json:"eip161abcTransition"` + EIP161dTransition uint64 `json:"eip161dTransition"` + EIP649Reward *hexutil.Big `json:"eip649Reward"` + EIP100bTransition uint64 `json:"eip100bTransition"` + EIP649Transition uint64 `json:"eip649Transition"` + } `json:"params"` + } `json:"Ethash"` + } `json:"engine"` + + Params struct { + MaximumExtraDataSize hexutil.Uint64 `json:"maximumExtraDataSize"` + MinGasLimit *hexutil.Big `json:"minGasLimit"` + NetworkID hexutil.Uint64 `json:"networkID"` + MaxCodeSize uint64 `json:"maxCodeSize"` + EIP155Transition uint64 `json:"eip155Transition"` + EIP98Transition uint64 `json:"eip98Transition"` + EIP86Transition uint64 `json:"eip86Transition"` + EIP140Transition uint64 `json:"eip140Transition"` + EIP211Transition uint64 `json:"eip211Transition"` + EIP214Transition uint64 `json:"eip214Transition"` + EIP658Transition uint64 `json:"eip658Transition"` + } `json:"params"` + + Genesis struct { + Seal struct { + Ethereum struct { + Nonce hexutil.Bytes `json:"nonce"` + MixHash hexutil.Bytes `json:"mixHash"` + } `json:"ethereum"` + } `json:"seal"` + + Difficulty *hexutil.Big `json:"difficulty"` + Author common.Address `json:"author"` + Timestamp hexutil.Uint64 `json:"timestamp"` + ParentHash common.Hash `json:"parentHash"` + ExtraData hexutil.Bytes `json:"extraData"` + GasLimit hexutil.Uint64 `json:"gasLimit"` + } `json:"genesis"` + + Nodes []string `json:"nodes"` + Accounts map[common.Address]*parityChainSpecAccount `json:"accounts"` +} + +// parityChainSpecAccount is the prefunded genesis account and/or precompiled +// contract definition. +type parityChainSpecAccount struct { + Balance *hexutil.Big `json:"balance"` + Nonce uint64 `json:"nonce,omitempty"` + Builtin *parityChainSpecBuiltin `json:"builtin,omitempty"` +} + +// parityChainSpecBuiltin is the precompiled contract definition. +type parityChainSpecBuiltin struct { + Name string `json:"name,omitempty"` + ActivateAt uint64 `json:"activate_at,omitempty"` + Pricing *parityChainSpecPricing `json:"pricing,omitempty"` +} + +// parityChainSpecPricing represents the different pricing models that builtin +// contracts might advertise using. +type parityChainSpecPricing struct { + Linear *parityChainSpecLinearPricing `json:"linear,omitempty"` + ModExp *parityChainSpecModExpPricing `json:"modexp,omitempty"` + AltBnPairing *parityChainSpecAltBnPairingPricing `json:"alt_bn128_pairing,omitempty"` +} + +type parityChainSpecLinearPricing struct { + Base uint64 `json:"base"` + Word uint64 `json:"word"` +} + +type parityChainSpecModExpPricing struct { + Divisor uint64 `json:"divisor"` +} + +type parityChainSpecAltBnPairingPricing struct { + Base uint64 `json:"base"` + Pair uint64 `json:"pair"` +} + +// newParityChainSpec converts a go-ethereum genesis block into a Parity specific +// chain specification format. +func newParityChainSpec(network string, genesis *core.Genesis, bootnodes []string) (*parityChainSpec, error) { + // Only ethash is currently supported between go-ethereum and Parity + if genesis.Config.Ethash == nil { + return nil, errors.New("unsupported consensus engine") + } + // Reconstruct the chain spec in Parity's format + spec := &parityChainSpec{ + Name: network, + Nodes: bootnodes, + } + spec.Engine.Ethash.Params.MinimumDifficulty = (*hexutil.Big)(params.MinimumDifficulty) + spec.Engine.Ethash.Params.DifficultyBoundDivisor = (*hexutil.Big)(params.DifficultyBoundDivisor) + spec.Engine.Ethash.Params.GasLimitBoundDivisor = (*hexutil.Big)(params.GasLimitBoundDivisor) + spec.Engine.Ethash.Params.DurationLimit = (*hexutil.Big)(params.DurationLimit) + spec.Engine.Ethash.Params.BlockReward = (*hexutil.Big)(ethash.FrontierBlockReward) + spec.Engine.Ethash.Params.HomesteadTransition = genesis.Config.HomesteadBlock.Uint64() + spec.Engine.Ethash.Params.EIP150Transition = genesis.Config.EIP150Block.Uint64() + spec.Engine.Ethash.Params.EIP160Transition = genesis.Config.EIP155Block.Uint64() + spec.Engine.Ethash.Params.EIP161abcTransition = genesis.Config.EIP158Block.Uint64() + spec.Engine.Ethash.Params.EIP161dTransition = genesis.Config.EIP158Block.Uint64() + spec.Engine.Ethash.Params.EIP649Reward = (*hexutil.Big)(ethash.ByzantiumBlockReward) + spec.Engine.Ethash.Params.EIP100bTransition = genesis.Config.ByzantiumBlock.Uint64() + spec.Engine.Ethash.Params.EIP649Transition = genesis.Config.ByzantiumBlock.Uint64() + + spec.Params.MaximumExtraDataSize = (hexutil.Uint64)(params.MaximumExtraDataSize) + spec.Params.MinGasLimit = (*hexutil.Big)(params.MinGasLimit) + spec.Params.NetworkID = (hexutil.Uint64)(genesis.Config.ChainId.Uint64()) + spec.Params.MaxCodeSize = params.MaxCodeSize + spec.Params.EIP155Transition = genesis.Config.EIP155Block.Uint64() + spec.Params.EIP98Transition = math.MaxUint64 + spec.Params.EIP86Transition = math.MaxUint64 + spec.Params.EIP140Transition = genesis.Config.ByzantiumBlock.Uint64() + spec.Params.EIP211Transition = genesis.Config.ByzantiumBlock.Uint64() + spec.Params.EIP214Transition = genesis.Config.ByzantiumBlock.Uint64() + spec.Params.EIP658Transition = genesis.Config.ByzantiumBlock.Uint64() + + spec.Genesis.Seal.Ethereum.Nonce = (hexutil.Bytes)(make([]byte, 8)) + binary.LittleEndian.PutUint64(spec.Genesis.Seal.Ethereum.Nonce[:], genesis.Nonce) + + spec.Genesis.Seal.Ethereum.MixHash = (hexutil.Bytes)(genesis.Mixhash[:]) + spec.Genesis.Difficulty = (*hexutil.Big)(genesis.Difficulty) + spec.Genesis.Author = genesis.Coinbase + spec.Genesis.Timestamp = (hexutil.Uint64)(genesis.Timestamp) + spec.Genesis.ParentHash = genesis.ParentHash + spec.Genesis.ExtraData = (hexutil.Bytes)(genesis.ExtraData) + spec.Genesis.GasLimit = (hexutil.Uint64)(genesis.GasLimit) + + spec.Accounts = make(map[common.Address]*parityChainSpecAccount) + for address, account := range genesis.Alloc { + spec.Accounts[address] = &parityChainSpecAccount{ + Balance: (*hexutil.Big)(account.Balance), + Nonce: account.Nonce, + } + } + spec.Accounts[common.BytesToAddress([]byte{1})].Builtin = &parityChainSpecBuiltin{ + Name: "ecrecover", Pricing: &parityChainSpecPricing{Linear: &parityChainSpecLinearPricing{Base: 3000}}, + } + spec.Accounts[common.BytesToAddress([]byte{2})].Builtin = &parityChainSpecBuiltin{ + Name: "sha256", Pricing: &parityChainSpecPricing{Linear: &parityChainSpecLinearPricing{Base: 60, Word: 12}}, + } + spec.Accounts[common.BytesToAddress([]byte{3})].Builtin = &parityChainSpecBuiltin{ + Name: "ripemd160", Pricing: &parityChainSpecPricing{Linear: &parityChainSpecLinearPricing{Base: 600, Word: 120}}, + } + spec.Accounts[common.BytesToAddress([]byte{4})].Builtin = &parityChainSpecBuiltin{ + Name: "identity", Pricing: &parityChainSpecPricing{Linear: &parityChainSpecLinearPricing{Base: 15, Word: 3}}, + } + if genesis.Config.ByzantiumBlock != nil { + spec.Accounts[common.BytesToAddress([]byte{5})].Builtin = &parityChainSpecBuiltin{ + Name: "modexp", ActivateAt: genesis.Config.ByzantiumBlock.Uint64(), Pricing: &parityChainSpecPricing{ModExp: &parityChainSpecModExpPricing{Divisor: 20}}, + } + spec.Accounts[common.BytesToAddress([]byte{6})].Builtin = &parityChainSpecBuiltin{ + Name: "alt_bn128_add", ActivateAt: genesis.Config.ByzantiumBlock.Uint64(), Pricing: &parityChainSpecPricing{Linear: &parityChainSpecLinearPricing{Base: 500}}, + } + spec.Accounts[common.BytesToAddress([]byte{7})].Builtin = &parityChainSpecBuiltin{ + Name: "alt_bn128_mul", ActivateAt: genesis.Config.ByzantiumBlock.Uint64(), Pricing: &parityChainSpecPricing{Linear: &parityChainSpecLinearPricing{Base: 40000}}, + } + spec.Accounts[common.BytesToAddress([]byte{8})].Builtin = &parityChainSpecBuiltin{ + Name: "alt_bn128_pairing", ActivateAt: genesis.Config.ByzantiumBlock.Uint64(), Pricing: &parityChainSpecPricing{AltBnPairing: &parityChainSpecAltBnPairingPricing{Base: 100000, Pair: 80000}}, + } + } + return spec, nil +} + +// pyEthereumGenesisSpec represents the genesis specification format used by the +// Python Ethereum implementation. +type pyEthereumGenesisSpec struct { + Nonce hexutil.Bytes `json:"nonce"` + Timestamp hexutil.Uint64 `json:"timestamp"` + ExtraData hexutil.Bytes `json:"extraData"` + GasLimit hexutil.Uint64 `json:"gasLimit"` + Difficulty *hexutil.Big `json:"difficulty"` + Mixhash common.Hash `json:"mixhash"` + Coinbase common.Address `json:"coinbase"` + Alloc core.GenesisAlloc `json:"alloc"` + ParentHash common.Hash `json:"parentHash"` +} + +// newPyEthereumGenesisSpec converts a go-ethereum genesis block into a Parity specific +// chain specification format. +func newPyEthereumGenesisSpec(network string, genesis *core.Genesis) (*pyEthereumGenesisSpec, error) { + // Only ethash is currently supported between go-ethereum and pyethereum + if genesis.Config.Ethash == nil { + return nil, errors.New("unsupported consensus engine") + } + spec := &pyEthereumGenesisSpec{ + Timestamp: (hexutil.Uint64)(genesis.Timestamp), + ExtraData: genesis.ExtraData, + GasLimit: (hexutil.Uint64)(genesis.GasLimit), + Difficulty: (*hexutil.Big)(genesis.Difficulty), + Mixhash: genesis.Mixhash, + Coinbase: genesis.Coinbase, + Alloc: genesis.Alloc, + ParentHash: genesis.ParentHash, + } + spec.Nonce = (hexutil.Bytes)(make([]byte, 8)) + binary.LittleEndian.PutUint64(spec.Nonce[:], genesis.Nonce) + + return spec, nil +} diff --git a/cmd/puppeth/module_dashboard.go b/cmd/puppeth/module_dashboard.go index 1cf6cab79..b6d964696 100644 --- a/cmd/puppeth/module_dashboard.go +++ b/cmd/puppeth/module_dashboard.go @@ -18,10 +18,12 @@ package main import ( "bytes" + "encoding/json" "fmt" "html/template" "math/rand" "path/filepath" + "strconv" "strings" "github.com/ethereum/go-ethereum/log" @@ -76,25 +78,26 @@ var dashboardContent = ` <div id="sidebar-menu" class="main_menu_side hidden-print main_menu"> <div class="menu_section"> <ul class="nav side-menu"> - {{if .EthstatsPage}}<li><a onclick="load('//{{.EthstatsPage}}')"><i class="fa fa-tachometer"></i> Network Stats</a></li>{{end}} - {{if .ExplorerPage}}<li><a onclick="load('//{{.ExplorerPage}}')"><i class="fa fa-database"></i> Block Explorer</a></li>{{end}} - {{if .WalletPage}}<li><a onclick="load('//{{.WalletPage}}')"><i class="fa fa-address-book-o"></i> Browser Wallet</a></li>{{end}} - {{if .FaucetPage}}<li><a onclick="load('//{{.FaucetPage}}')"><i class="fa fa-bath"></i> Crypto Faucet</a></li>{{end}} - <li id="connect"><a><i class="fa fa-plug"></i> Connect Yourself</a> + {{if .EthstatsPage}}<li id="stats_menu"><a onclick="load('#stats')"><i class="fa fa-tachometer"></i> Network Stats</a></li>{{end}} + {{if .ExplorerPage}}<li id="explorer_menu"><a onclick="load('#explorer')"><i class="fa fa-database"></i> Block Explorer</a></li>{{end}} + {{if .WalletPage}}<li id="wallet_menu"><a onclick="load('#wallet')"><i class="fa fa-address-book-o"></i> Browser Wallet</a></li>{{end}} + {{if .FaucetPage}}<li id="faucet_menu"><a onclick="load('#faucet')"><i class="fa fa-bath"></i> Crypto Faucet</a></li>{{end}} + <li id="connect_menu"><a><i class="fa fa-plug"></i> Connect Yourself</a> <ul id="connect_list" class="nav child_menu"> - <li><a onclick="$('#connect').removeClass('active'); $('#connect_list').toggle(); load('#connect-go-ethereum-geth')">Go Ethereum: Geth</a></li> - <li><a onclick="$('#connect').removeClass('active'); $('#connect_list').toggle(); load('#connect-go-ethereum-mist')">Go Ethereum: Wallet & Mist</a></li> - <li><a onclick="$('#connect').removeClass('active'); $('#connect_list').toggle(); load('#connect-go-ethereum-mobile')">Go Ethereum: Android & iOS</a></li> + <li><a onclick="$('#connect_menu').removeClass('active'); $('#connect_list').toggle(); load('#geth')">Go Ethereum: Geth</a></li> + <li><a onclick="$('#connect_menu').removeClass('active'); $('#connect_list').toggle(); load('#mist')">Go Ethereum: Wallet & Mist</a></li> + <li><a onclick="$('#connect_menu').removeClass('active'); $('#connect_list').toggle(); load('#mobile')">Go Ethereum: Android & iOS</a></li>{{if .Ethash}} + <li><a onclick="$('#connect_menu').removeClass('active'); $('#connect_list').toggle(); load('#other')">Other Ethereum Clients</a></li>{{end}} </ul> </li> - <li><a onclick="load('#about')"><i class="fa fa-heartbeat"></i> About Puppeth</a></li> + <li id="about_menu"><a onclick="load('#about')"><i class="fa fa-heartbeat"></i> About Puppeth</a></li> </ul> </div> </div> </div> </div> - <div class="right_col" role="main" style="padding: 0"> - <div id="connect-go-ethereum-geth" hidden style="padding: 16px;"> + <div class="right_col" role="main" style="padding: 0 !important"> + <div id="geth" hidden style="padding: 16px;"> <div class="page-title"> <div class="title_left"> <h3>Connect Yourself – Go Ethereum: Geth</h3> @@ -154,7 +157,7 @@ var dashboardContent = ` <p>Initial processing required to synchronize is light, as it only verifies the validity of the headers; similarly required disk capacity is small, tallying around 500 bytes per header. Low end machines with arbitrary storage, weak CPUs and 512MB+ RAM should cope well.</p> <br/> <p>To run a light node, download <a href="/{{.GethGenesis}}"><code>{{.GethGenesis}}</code></a> and start Geth with: - <pre>geth --datadir=$HOME/.{{.Network}} --light init {{.GethGenesis}}</pre> + <pre>geth --datadir=$HOME/.{{.Network}} init {{.GethGenesis}}</pre> <pre>geth --networkid={{.NetworkID}} --datadir=$HOME/.{{.Network}} --syncmode=light{{if .Ethstats}} --ethstats='{{.Ethstats}}'{{end}} --bootnodes={{.BootnodesLightFlat}}</pre> </p> <br/> @@ -173,8 +176,8 @@ var dashboardContent = ` <p>Initial processing required to synchronize is light, as it only verifies the validity of the headers; similarly required disk capacity is small, tallying around 500 bytes per header. Embedded machines with arbitrary storage, low power CPUs and 128MB+ RAM may work.</p> <br/> <p>To run an embedded node, download <a href="/{{.GethGenesis}}"><code>{{.GethGenesis}}</code></a> and start Geth with: - <pre>geth --datadir=$HOME/.{{.Network}} --light init {{.GethGenesis}}</pre> - <pre>geth --networkid={{.NetworkID}} --datadir=$HOME/.{{.Network}} --cache=32 --syncmode=light{{if .Ethstats}} --ethstats='{{.Ethstats}}'{{end}} --bootnodes={{.BootnodesLightFlat}}</pre> + <pre>geth --datadir=$HOME/.{{.Network}} init {{.GethGenesis}}</pre> + <pre>geth --networkid={{.NetworkID}} --datadir=$HOME/.{{.Network}} --cache=16 --ethash.cachesinmem=1 --syncmode=light{{if .Ethstats}} --ethstats='{{.Ethstats}}'{{end}} --bootnodes={{.BootnodesLightFlat}}</pre> </p> <br/> <p>You can download Geth from <a href="https://geth.ethereum.org/downloads/" target="about:blank">https://geth.ethereum.org/downloads/</a>.</p> @@ -183,7 +186,7 @@ var dashboardContent = ` </div> </div> </div> - <div id="connect-go-ethereum-mist" hidden style="padding: 16px;"> + <div id="mist" hidden style="padding: 16px;"> <div class="page-title"> <div class="title_left"> <h3>Connect Yourself – Go Ethereum: Wallet & Mist</h3> @@ -235,7 +238,7 @@ var dashboardContent = ` </div> </div> </div> - <div id="connect-go-ethereum-mobile" hidden style="padding: 16px;"> + <div id="mobile" hidden style="padding: 16px;"> <div class="page-title"> <div class="title_left"> <h3>Connect Yourself – Go Ethereum: Android & iOS</h3> @@ -309,7 +312,101 @@ try! node?.start(); </div> </div> </div> - </div> + </div>{{if .Ethash}} + <div id="other" hidden style="padding: 16px;"> + <div class="page-title"> + <div class="title_left"> + <h3>Connect Yourself – Other Ethereum Clients</h3> + </div> + </div> + <div class="clearfix"></div> + <div class="row"> + <div class="col-md-6"> + <div class="x_panel"> + <div class="x_title"> + <h2> + <svg height="14px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 115 115"><path fill="#5C8DBC" d="M9.7 83.3V35.5s0-3.4 3.3-5.2c3.3-1.8 39.6-23.5 39.6-23.5s4.6-3.1 9.4 0c0 0 43.1 23.9 42.4 25.3L85.3 43.3s-3.6-8.4-13.1-13c-11.3-5.5-29.7-6.2-42.9 13.3 0 0-8.6 13.5.3 31.6l-19 10.7s-.9-.6-.9-2.6z"/><path fill="#5C8DBC" d="M71 51.3c-2.8-4.7-7.9-7.9-13.8-7.9-8.8 0-16 7.2-16 16 0 2.8.7 5.4 2 7.7L71 51.3z"/><path fill="#194674" d="M43.1 67c2.8 4.7 7.9 7.9 13.8 7.9 8.8 0 16-7.2 16-16 0-2.8-.7-5.4-2-7.7L43.1 67z"/><path fill="#1B598E" d="M104.4 32.1s1.3 52.6-.3 53.6L58 58.6l46.4-26.5z"/><path fill="#FFF" d="M90 57h-3.9v-4.1h-4.2V57h-4v4.1h4V65h4.2v-3.9H90zm13.6 0h-3.9v-4.1h-4.2V57h-4v4.1h4V65h4.2v-3.9h3.9z"/><path fill="#194674" d="M29.5 75.1s9.2 17 28.5 16.1 27.3-16.6 27.3-16.6L104 85.4s4.1.8-41.6 25.7c0 0-4.9 3.3-10.2 0 0 0-41.3-23.1-41.6-25.3l18.9-10.7z"/></svg> + C++ Ethereum <small>Official C++ client from the Ethereum Foundation</small> + </h2> + <div class="clearfix"></div> + </div> + <div class="x_content"> + <p>C++ Ethereum is the third most popular of the Ethereum clients, focusing on code portability to a broad range of operating systems and hardware. The client is currently a full node with transaction processing based synchronization.</p> + <br/> + <p>To run a cpp-ethereum node, download <a href="/{{.CppGenesis}}"><code>{{.CppGenesis}}</code></a> and start the node with: + <pre>eth --config {{.CppGenesis}} --datadir $HOME/.{{.Network}} --peerset "{{.CppBootnodes}}"</pre> + </p> + <br/> + <p>You can find cpp-ethereum at <a href="https://github.com/ethereum/cpp-ethereum/" target="about:blank">https://github.com/ethereum/cpp-ethereum/</a>.</p> + </div> + </div> + </div> + <div class="col-md-6"> + <div class="x_panel"> + <div class="x_title"> + <h2> + <svg height="14px" version="1.1" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><path d="M46.42,13.07S24.51,18.54,35,30.6c3.09,3.55-.81,6.75-0.81,6.75s7.84-4,4.24-9.11C35,23.51,32.46,21.17,46.42,13.07ZM32.1,16.88C45.05,6.65,38.4,0,38.4,0c2.68,10.57-9.46,13.76-13.84,20.34-3,4.48,1.46,9.3,7.53,14.77C29.73,29.77,21.71,25.09,32.1,16.88Z" transform="translate(-8.4)" fill="#e57125"/><path d="M23.6,49.49c-9.84,2.75,6,8.43,18.51,3.06a23.06,23.06,0,0,1-3.52-1.72,36.62,36.62,0,0,1-13.25.56C21.16,50.92,23.6,49.49,23.6,49.49Zm17-5.36a51.7,51.7,0,0,1-17.1.82c-4.19-.43-1.45-2.46-1.45-2.46-10.84,3.6,6,7.68,21.18,3.25A7.59,7.59,0,0,1,40.62,44.13ZM51.55,54.68s1.81,1.49-2,2.64c-7.23,2.19-30.1,2.85-36.45.09-2.28-1,2-2.37,3.35-2.66a8.69,8.69,0,0,1,2.21-.25c-2.54-1.79-16.41,3.51-7,5C37.15,63.67,58.17,57.67,51.55,54.68ZM42.77,39.12a20.42,20.42,0,0,1,2.93-1.57s-4.83.86-9.65,1.27A87.37,87.37,0,0,1,20.66,39c-7.51-1,4.12-3.77,4.12-3.77A22,22,0,0,0,14.7,37.61C8.14,40.79,31,42.23,42.77,39.12Zm2.88,7.77a1,1,0,0,1-.24.31C61.44,43,55.54,32.35,47.88,35a2.19,2.19,0,0,0-1,.79,9,9,0,0,1,1.37-.37C52.1,34.66,57.65,40.65,45.64,46.89Zm0.43,14.75a94.76,94.76,0,0,1-29.17.45s1.47,1.22,9,1.7c11.53,0.74,29.22-.41,29.64-5.86C55.6,57.94,54.79,60,46.08,61.65Z" transform="translate(-8.4)" fill="#5482a2"/></svg> + Ethereum Harmony<small>Third party Java client from EtherCamp</small> + </h2> + <div class="clearfix"></div> + </div> + <div class="x_content"> + <p>Ethereum Harmony is a web user-interface based graphical Ethereum client built on top of the EthereumJ Java implementation of the Ethereum protocol. The client currently is a full node with state download based synchronization.</p> + <br/> + <p>To run an Ethereum Harmony node, download <a href="/{{.HarmonyGenesis}}"><code>{{.HarmonyGenesis}}</code></a> and start the node with: + <pre>./gradlew runCustom -DgenesisFile={{.HarmonyGenesis}} -Dpeer.networkId={{.NetworkID}} -Ddatabase.dir=$HOME/.harmony/{{.Network}} {{.HarmonyBootnodes}} </pre> + </p> + <br/> + <p>You can find Ethereum Harmony at <a href="https://github.com/ether-camp/ethereum-harmony/" target="about:blank">https://github.com/ether-camp/ethereum-harmony/</a>.</p> + </div> + </div> + </div> + </div> + <div class="clearfix"></div> + <div class="row"> + <div class="col-md-6"> + <div class="x_panel"> + <div class="x_title"> + <h2> + <svg height="14px" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 104.56749 104.56675" version="1.1" viewbox="0 0 144 144" y="0px" x="0px"><metadata id="metadata10"><rdf:RDF><cc:Work rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs id="defs8" /><path style="fill:#676767;" id="path2" d="m 49.0125,12.3195 a 3.108,3.108 0 0 1 6.216,0 3.108,3.108 0 0 1 -6.216,0 m -37.077,28.14 a 3.108,3.108 0 0 1 6.216,0 3.108,3.108 0 0 1 -6.216,0 m 74.153,0.145 a 3.108,3.108 0 0 1 6.216,0 3.108,3.108 0 0 1 -6.216,0 m -65.156,4.258 c 1.43,-0.635 2.076,-2.311 1.441,-3.744 l -1.379,-3.118 h 5.423 v 24.444 h -10.941 a 38.265,38.265 0 0 1 -1.239,-14.607 z m 22.685,0.601 v -7.205 h 12.914 c 0.667,0 4.71,0.771 4.71,3.794 0,2.51 -3.101,3.41 -5.651,3.41 z m -17.631,38.793 a 3.108,3.108 0 0 1 6.216,0 3.108,3.108 0 0 1 -6.216,0 m 46.051,0.145 a 3.108,3.108 0 0 1 6.216,0 3.108,3.108 0 0 1 -6.216,0 m 0.961,-7.048 c -1.531,-0.328 -3.037,0.646 -3.365,2.18 l -1.56,7.28 a 38.265,38.265 0 0 1 -31.911,-0.153 l -1.559,-7.28 c -0.328,-1.532 -1.834,-2.508 -3.364,-2.179 l -6.427,1.38 a 38.265,38.265 0 0 1 -3.323,-3.917 h 31.272 c 0.354,0 0.59,-0.064 0.59,-0.386 v -11.062 c 0,-0.322 -0.236,-0.386 -0.59,-0.386 h -9.146 v -7.012 h 9.892 c 0.903,0 4.828,0.258 6.083,5.275 0.393,1.543 1.256,6.562 1.846,8.169 0.588,1.802 2.982,5.402 5.533,5.402 h 16.146 a 38.265,38.265 0 0 1 -3.544,4.102 z m 17.365,-29.207 a 38.265,38.265 0 0 1 0.081,6.643 h -3.926 c -0.393,0 -0.551,0.258 -0.551,0.643 v 1.803 c 0,4.244 -2.393,5.167 -4.49,5.402 -1.997,0.225 -4.211,-0.836 -4.484,-2.058 -1.178,-6.626 -3.141,-8.041 -6.241,-10.486 3.847,-2.443 7.85,-6.047 7.85,-10.871 0,-5.209 -3.571,-8.49 -6.005,-10.099 -3.415,-2.251 -7.196,-2.702 -8.216,-2.702 h -40.603 a 38.265,38.265 0 0 1 21.408,-12.082 l 4.786,5.021 c 1.082,1.133 2.874,1.175 4.006,0.092 l 5.355,-5.122 a 38.265,38.265 0 0 1 26.196,18.657 l -3.666,8.28 c -0.633,1.433 0.013,3.109 1.442,3.744 z m 9.143,0.134 -0.125,-1.28 3.776,-3.522 c 0.768,-0.716 0.481,-2.157 -0.501,-2.523 l -4.827,-1.805 -0.378,-1.246 3.011,-4.182 c 0.614,-0.85 0.05,-2.207 -0.984,-2.377 l -5.09,-0.828 -0.612,-1.143 2.139,-4.695 c 0.438,-0.956 -0.376,-2.179 -1.428,-2.139 l -5.166,0.18 -0.816,-0.99 1.187,-5.032 c 0.24,-1.022 -0.797,-2.06 -1.819,-1.82 l -5.031,1.186 -0.992,-0.816 0.181,-5.166 c 0.04,-1.046 -1.184,-1.863 -2.138,-1.429 l -4.694,2.14 -1.143,-0.613 -0.83,-5.091 c -0.168,-1.032 -1.526,-1.596 -2.376,-0.984 l -4.185,3.011 -1.244,-0.377 -1.805,-4.828 c -0.366,-0.984 -1.808,-1.267 -2.522,-0.503 l -3.522,3.779 -1.28,-0.125 -2.72,-4.395 c -0.55,-0.89 -2.023,-0.89 -2.571,0 l -2.72,4.395 -1.281,0.125 -3.523,-3.779 c -0.714,-0.764 -2.156,-0.481 -2.522,0.503 l -1.805,4.828 -1.245,0.377 -4.184,-3.011 c -0.85,-0.614 -2.209,-0.048 -2.377,0.984 l -0.83,5.091 -1.143,0.613 -4.694,-2.14 c -0.954,-0.436 -2.178,0.383 -2.138,1.429 l 0.18,5.166 -0.992,0.816 -5.031,-1.186 c -1.022,-0.238 -2.06,0.798 -1.82,1.82 l 1.185,5.032 -0.814,0.99 -5.166,-0.18 c -1.042,-0.03 -1.863,1.183 -1.429,2.139 l 2.14,4.695 -0.613,1.143 -5.09,0.828 c -1.034,0.168 -1.594,1.527 -0.984,2.377 l 3.011,4.182 -0.378,1.246 -4.828,1.805 c -0.98,0.366 -1.267,1.807 -0.501,2.523 l 3.777,3.522 -0.125,1.28 -4.394,2.72 c -0.89,0.55 -0.89,2.023 0,2.571 l 4.394,2.72 0.125,1.28 -3.777,3.523 c -0.766,0.714 -0.479,2.154 0.501,2.522 l 4.828,1.805 0.378,1.246 -3.011,4.183 c -0.612,0.852 -0.049,2.21 0.985,2.376 l 5.089,0.828 0.613,1.145 -2.14,4.693 c -0.436,0.954 0.387,2.181 1.429,2.139 l 5.164,-0.181 0.816,0.992 -1.185,5.033 c -0.24,1.02 0.798,2.056 1.82,1.816 l 5.031,-1.185 0.992,0.814 -0.18,5.167 c -0.04,1.046 1.184,1.864 2.138,1.428 l 4.694,-2.139 1.143,0.613 0.83,5.088 c 0.168,1.036 1.527,1.596 2.377,0.986 l 4.182,-3.013 1.246,0.379 1.805,4.826 c 0.366,0.98 1.808,1.269 2.522,0.501 l 3.523,-3.777 1.281,0.128 2.72,4.394 c 0.548,0.886 2.021,0.888 2.571,0 l 2.72,-4.394 1.28,-0.128 3.522,3.777 c 0.714,0.768 2.156,0.479 2.522,-0.501 l 1.805,-4.826 1.246,-0.379 4.183,3.013 c 0.85,0.61 2.208,0.048 2.376,-0.986 l 0.83,-5.088 1.143,-0.613 4.694,2.139 c 0.954,0.436 2.176,-0.38 2.138,-1.428 l -0.18,-5.167 0.991,-0.814 5.031,1.185 c 1.022,0.24 2.059,-0.796 1.819,-1.816 l -1.185,-5.033 0.814,-0.992 5.166,0.181 c 1.042,0.042 1.866,-1.185 1.428,-2.139 l -2.139,-4.693 0.612,-1.145 5.09,-0.828 c 1.036,-0.166 1.598,-1.524 0.984,-2.376 l -3.011,-4.183 0.378,-1.246 4.827,-1.805 c 0.982,-0.368 1.269,-1.808 0.501,-2.522 l -3.776,-3.523 0.125,-1.28 4.394,-2.72 c 0.89,-0.548 0.891,-2.021 10e-4,-2.571 z" /></svg> + Parity<small>Third party Rust client from Parity Technologies</small> + </h2> + <div class="clearfix"></div> + </div> + <div class="x_content"> + <p>Parity is a fast, light and secure Ethereum client, supporting both headless mode of operation as well as a web user interface for direct manual interaction. The client is currently a full node with transaction processing based synchronization and state pruning enabled.</p> + <br/> + <p>To run a Parity node, download <a href="/{{.ParityGenesis}}"><code>{{.ParityGenesis}}</code></a> and start the node with: + <pre>parity --chain={{.ParityGenesis}}</pre> + </p> + <br/> + <p>You can find Parity at <a href="https://parity.io/" target="about:blank">https://parity.io/</a>.</p> + </div> + </div> + </div> + <div class="col-md-6"> + <div class="x_panel"> + <div class="x_title"> + <h2> + <svg height="14px" version="1.1" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><defs><linearGradient id="a" x1="13.79" y1="38.21" x2="75.87" y2="-15.2" gradientTransform="matrix(0.56, 0, 0, -0.57, -8.96, 23.53)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#5c9fd3"/><stop offset="1" stop-color="#316a99"/></linearGradient><linearGradient id="b" x1="99.87" y1="-47.53" x2="77.7" y2="-16.16" gradientTransform="matrix(0.56, 0, 0, -0.57, -8.96, 23.53)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ffd43d"/><stop offset="1" stop-color="#fee875"/></linearGradient></defs><g><path d="M31.62,0a43.6,43.6,0,0,0-7.3.62c-6.46,1.14-7.63,3.53-7.63,7.94v5.82H32v1.94H11a9.53,9.53,0,0,0-9.54,7.74,28.54,28.54,0,0,0,0,15.52c1.09,4.52,3.68,7.74,8.11,7.74h5.25v-7a9.7,9.7,0,0,1,9.54-9.48H39.58a7.69,7.69,0,0,0,7.63-7.76V8.56c0-4.14-3.49-7.25-7.63-7.94A47.62,47.62,0,0,0,31.62,0ZM23.37,4.68A2.91,2.91,0,1,1,20.5,7.6,2.9,2.9,0,0,1,23.37,4.68Z" transform="translate(-0.35)" fill="url(#a)"/><path d="M49.12,16.32V23.1a9.79,9.79,0,0,1-9.54,9.68H24.33a7.79,7.79,0,0,0-7.63,7.76V55.08c0,4.14,3.6,6.57,7.63,7.76a25.55,25.55,0,0,0,15.25,0c3.84-1.11,7.63-3.35,7.63-7.76V49.26H32V47.32H54.85c4.44,0,6.09-3.1,7.63-7.74s1.53-9.38,0-15.52c-1.1-4.42-3.19-7.74-7.63-7.74H49.12ZM40.54,53.14A2.91,2.91,0,1,1,37.67,56,2.88,2.88,0,0,1,40.54,53.14Z" transform="translate(-0.35)" fill="url(#b)"/></g></svg> + PyEthApp<small>Official Python client from the Ethereum Foundation</small> + </h2> + <div class="clearfix"></div> + </div> + <div class="x_content"> + <p>Pyethapp is the Ethereum Foundation's research client, aiming to provide an easily hackable and extendable codebase. The client is currently a full node with transaction processing based synchronization and state pruning enabled.</p> + <br/> + <p>To run a pyethapp node, download <a href="/{{.PythonGenesis}}"><code>{{.PythonGenesis}}</code></a> and start the node with: + <pre>mkdir -p $HOME/.config/pyethapp/{{.Network}}</pre> + <pre>pyethapp -c eth.genesis="$(cat {{.PythonGenesis}})" -c eth.network_id={{.NetworkID}} -c data_dir=$HOME/.config/pyethapp/{{.Network}} -c discovery.bootstrap_nodes="[{{.PythonBootnodes}}]" -c eth.block.HOMESTEAD_FORK_BLKNUM={{.Homestead}} -c eth.block.ANTI_DOS_FORK_BLKNUM={{.Tangerine}} -c eth.block.SPURIOUS_DRAGON_FORK_BLKNUM={{.Spurious}} -c eth.block.METROPOLIS_FORK_BLKNUM={{.Byzantium}} -c eth.block.DAO_FORK_BLKNUM=18446744073709551615 run --console</pre> + </p> + <br/> + <p>You can find pyethapp at <a href="https://github.com/ethereum/pyethapp/" target="about:blank">https://github.com/ethereum/pyethapp/</a>.</p> + </div> + </div> + </div> + </div> + </div>{{end}} <div id="about" hidden> <div class="row vertical-center"> <div style="margin: 0 auto;"> @@ -344,13 +441,33 @@ try! node?.start(); <script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/gentelella/1.3.0/js/custom.min.js"></script> <script> - var load = function(url) { - $("#connect-go-ethereum-geth").fadeOut(300) - $("#connect-go-ethereum-mist").fadeOut(300) - $("#connect-go-ethereum-mobile").fadeOut(300) + var load = function(hash) { + window.location.hash = hash; + + // Fade out all possible pages (yes, ugly, no, don't care) + $("#geth").fadeOut(300) + $("#mist").fadeOut(300) + $("#mobile").fadeOut(300) + $("#other").fadeOut(300) $("#about").fadeOut(300) $("#frame-wrapper").fadeOut(300); + // Depending on the hash, resolve it into a local or remote URL + var url = hash; + switch (hash) { + case "#stats": + url = "//{{.EthstatsPage}}"; + break; + case "#explorer": + url = "//{{.ExplorerPage}}"; + break; + case "#wallet": + url = "//{{.WalletPage}}"; + break; + case "#faucet": + url = "//{{.FaucetPage}}"; + break; + } setTimeout(function() { if (url.substring(0, 1) == "#") { $('.body').css({overflowY: 'auto'}); @@ -364,13 +481,10 @@ try! node?.start(); } var resize = function() { var sidebar = $($(".navbar")[0]).width(); - var content = 1920; var limit = document.body.clientWidth - sidebar; - var scale = limit / content; + var scale = limit / 1920; - console.log(document.body.clientHeight); - - $("#frame-wrapper").width(content / scale); + $("#frame-wrapper").width(limit); $("#frame-wrapper").height(document.body.clientHeight / scale); $("#frame-wrapper").css({ transform: 'scale(' + (scale) + ')', @@ -379,9 +493,17 @@ try! node?.start(); }; $(window).resize(resize); - var item = $(".side-menu").children()[0]; - $(item).children()[0].click(); - $(item).addClass("active"); + if (window.location.hash == "") { + var item = $(".side-menu").children()[0]; + $(item).children()[0].click(); + $(item).addClass("active"); + } else { + load(window.location.hash); + var menu = $(window.location.hash + "_menu"); + if (menu !== undefined) { + $(menu).addClass("active"); + } + } </script> </body> </html> @@ -405,6 +527,10 @@ RUN \ echo '});' >> server.js ADD {{.Network}}.json /dashboard/{{.Network}}.json +ADD {{.Network}}-cpp.json /dashboard/{{.Network}}-cpp.json +ADD {{.Network}}-harmony.json /dashboard/{{.Network}}-harmony.json +ADD {{.Network}}-parity.json /dashboard/{{.Network}}-parity.json +ADD {{.Network}}-python.json /dashboard/{{.Network}}-python.json ADD index.html /dashboard/index.html ADD puppeth.png /dashboard/puppeth.png @@ -422,8 +548,12 @@ services: build: . image: {{.Network}}/dashboard{{if not .VHost}} ports: - - "{{.Port}}:80"{{else}} + - "{{.Port}}:80"{{end}} environment: + - ETHSTATS_PAGE={{.EthstatsPage}} + - EXPLORER_PAGE={{.ExplorerPage}} + - WALLET_PAGE={{.WalletPage}} + - FAUCET_PAGE={{.FaucetPage}}{{if .VHost}} - VIRTUAL_HOST={{.VHost}}{{end}} logging: driver: "json-file" @@ -436,7 +566,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, conf *config, config *dashboardInfos, nocache bool) ([]byte, error) { // Generate the content to upload to the server workdir := fmt.Sprintf("%d", rand.Int63()) files := make(map[string][]byte) @@ -449,37 +579,95 @@ func deployDashboard(client *sshClient, network string, port int, vhost string, composefile := new(bytes.Buffer) template.Must(template.New("").Parse(dashboardComposefile)).Execute(composefile, map[string]interface{}{ - "Network": network, - "Port": port, - "VHost": vhost, + "Network": network, + "Port": config.port, + "VHost": config.host, + "EthstatsPage": config.ethstats, + "ExplorerPage": config.explorer, + "WalletPage": config.wallet, + "FaucetPage": config.faucet, }) files[filepath.Join(workdir, "docker-compose.yaml")] = composefile.Bytes() statsLogin := fmt.Sprintf("yournode:%s", conf.ethstats) - if !ethstats { + if !config.trusted { statsLogin = "" } indexfile := new(bytes.Buffer) + bootCpp := make([]string, len(conf.bootFull)) + for i, boot := range conf.bootFull { + bootCpp[i] = "required:" + strings.TrimPrefix(boot, "enode://") + } + bootHarmony := make([]string, len(conf.bootFull)) + for i, boot := range conf.bootFull { + bootHarmony[i] = fmt.Sprintf("-Dpeer.active.%d.url=%s", i, boot) + } + bootPython := make([]string, len(conf.bootFull)) + for i, boot := range conf.bootFull { + bootPython[i] = "'" + boot + "'" + } template.Must(template.New("").Parse(dashboardContent)).Execute(indexfile, map[string]interface{}{ "Network": network, - "NetworkID": conf.genesis.Config.ChainId, + "NetworkID": conf.Genesis.Config.ChainId, "NetworkTitle": strings.Title(network), - "EthstatsPage": services["ethstats"], - "ExplorerPage": services["explorer"], - "WalletPage": services["wallet"], - "FaucetPage": services["faucet"], + "EthstatsPage": config.ethstats, + "ExplorerPage": config.explorer, + "WalletPage": config.wallet, + "FaucetPage": config.faucet, "GethGenesis": network + ".json", "BootnodesFull": conf.bootFull, "BootnodesLight": conf.bootLight, "BootnodesFullFlat": strings.Join(conf.bootFull, ","), "BootnodesLightFlat": strings.Join(conf.bootLight, ","), "Ethstats": statsLogin, + "Ethash": conf.Genesis.Config.Ethash != nil, + "CppGenesis": network + "-cpp.json", + "CppBootnodes": strings.Join(bootCpp, " "), + "HarmonyGenesis": network + "-harmony.json", + "HarmonyBootnodes": strings.Join(bootHarmony, " "), + "ParityGenesis": network + "-parity.json", + "PythonGenesis": network + "-python.json", + "PythonBootnodes": strings.Join(bootPython, ","), + "Homestead": conf.Genesis.Config.HomesteadBlock, + "Tangerine": conf.Genesis.Config.EIP150Block, + "Spurious": conf.Genesis.Config.EIP155Block, + "Byzantium": conf.Genesis.Config.ByzantiumBlock, }) files[filepath.Join(workdir, "index.html")] = indexfile.Bytes() - genesis, _ := conf.genesis.MarshalJSON() + // Marshal the genesis spec files for go-ethereum and all the other clients + genesis, _ := conf.Genesis.MarshalJSON() files[filepath.Join(workdir, network+".json")] = genesis + if conf.Genesis.Config.Ethash != nil { + cppSpec, err := newCppEthereumGenesisSpec(network, conf.Genesis) + if err != nil { + return nil, err + } + cppSpecJSON, _ := json.Marshal(cppSpec) + files[filepath.Join(workdir, network+"-cpp.json")] = cppSpecJSON + + harmonySpecJSON, _ := conf.Genesis.MarshalJSON() + files[filepath.Join(workdir, network+"-harmony.json")] = harmonySpecJSON + + paritySpec, err := newParityChainSpec(network, conf.Genesis, conf.bootFull) + if err != nil { + return nil, err + } + paritySpecJSON, _ := json.Marshal(paritySpec) + files[filepath.Join(workdir, network+"-parity.json")] = paritySpecJSON + + pyethSpec, err := newPyEthereumGenesisSpec(network, conf.Genesis) + if err != nil { + return nil, err + } + pyethSpecJSON, _ := json.Marshal(pyethSpec) + files[filepath.Join(workdir, network+"-python.json")] = pyethSpecJSON + } else { + for _, client := range []string{"cpp", "harmony", "parity", "python"} { + files[filepath.Join(workdir, network+"-"+client+".json")] = []byte{} + } + } files[filepath.Join(workdir, "puppeth.png")] = dashboardMascot // Upload the deployment files to the remote server (and clean up afterwards) @@ -489,19 +677,36 @@ 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 // various configuration parameters. type dashboardInfos struct { - host string - port int + host string + port int + trusted bool + + ethstats string + explorer string + wallet string + faucet string } -// 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, containing +// 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), + "Ethstats service": info.ethstats, + "Explorer service": info.explorer, + "Wallet service": info.wallet, + "Faucet service": info.faucet, + } } // checkDashboard does a health-check against a dashboard container to verify if @@ -536,7 +741,11 @@ func checkDashboard(client *sshClient, network string) (*dashboardInfos, error) } // Container available, assemble and return the useful infos return &dashboardInfos{ - host: host, - port: port, + host: host, + port: port, + ethstats: infos.envvars["ETHSTATS_PAGE"], + explorer: infos.envvars["EXPLORER_PAGE"], + wallet: infos.envvars["WALLET_PAGE"], + faucet: infos.envvars["FAUCET_PAGE"], }, nil } diff --git a/cmd/puppeth/module_ethstats.go b/cmd/puppeth/module_ethstats.go index 6ce662f65..20b7afe23 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" @@ -30,21 +31,9 @@ import ( // ethstatsDockerfile is the Dockerfile required to build an ethstats backend // and associated monitoring site. 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/* && \ - \ - cd /eth-netstats && npm install && npm install -g grunt-cli && grunt - -WORKDIR /eth-netstats -EXPOSE 3000 +FROM puppeth/ethstats:latest RUN echo 'module.exports = {trusted: [{{.Trusted}}], banned: [{{.Banned}}], reserved: ["yournode"]};' > lib/utils/config.js - -CMD ["npm", "start"] ` // ethstatsComposefile is the docker-compose.yml file required to deploy and @@ -72,7 +61,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) @@ -110,7 +99,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 @@ -123,9 +115,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, containing +// 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_explorer.go b/cmd/puppeth/module_explorer.go new file mode 100644 index 000000000..427134153 --- /dev/null +++ b/cmd/puppeth/module_explorer.go @@ -0,0 +1,211 @@ +// Copyright 2017 The go-ethereum Authors +// This file is part of go-ethereum. +// +// go-ethereum is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// go-ethereum is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>. + +package main + +import ( + "bytes" + "fmt" + "html/template" + "math/rand" + "path/filepath" + "strconv" + "strings" + + "github.com/ethereum/go-ethereum/log" +) + +// explorerDockerfile is the Dockerfile required to run a block explorer. +var explorerDockerfile = ` +FROM puppeth/explorer:latest + +ADD ethstats.json /ethstats.json +ADD chain.json /chain.json + +RUN \ + echo '(cd ../eth-net-intelligence-api && pm2 start /ethstats.json)' > explorer.sh && \ + echo '(cd ../etherchain-light && npm start &)' >> explorer.sh && \ + echo '/parity/parity --chain=/chain.json --port={{.NodePort}} --tracing=on --fat-db=on --pruning=archive' >> explorer.sh + +ENTRYPOINT ["/bin/sh", "explorer.sh"] +` + +// explorerEthstats is the configuration file for the ethstats javascript client. +var explorerEthstats = `[ + { + "name" : "node-app", + "script" : "app.js", + "log_date_format" : "YYYY-MM-DD HH:mm Z", + "merge_logs" : false, + "watch" : false, + "max_restarts" : 10, + "exec_interpreter" : "node", + "exec_mode" : "fork_mode", + "env": + { + "NODE_ENV" : "production", + "RPC_HOST" : "localhost", + "RPC_PORT" : "8545", + "LISTENING_PORT" : "{{.Port}}", + "INSTANCE_NAME" : "{{.Name}}", + "CONTACT_DETAILS" : "", + "WS_SERVER" : "{{.Host}}", + "WS_SECRET" : "{{.Secret}}", + "VERBOSITY" : 2 + } + } +]` + +// explorerComposefile is the docker-compose.yml file required to deploy and +// maintain a block explorer. +var explorerComposefile = ` +version: '2' +services: + explorer: + build: . + image: {{.Network}}/explorer + ports: + - "{{.NodePort}}:{{.NodePort}}" + - "{{.NodePort}}:{{.NodePort}}/udp"{{if not .VHost}} + - "{{.WebPort}}:3000"{{end}} + volumes: + - {{.Datadir}}:/root/.local/share/io.parity.ethereum + environment: + - NODE_PORT={{.NodePort}}/tcp + - STATS={{.Ethstats}}{{if .VHost}} + - VIRTUAL_HOST={{.VHost}} + - VIRTUAL_PORT=3000{{end}} + logging: + driver: "json-file" + options: + max-size: "1m" + max-file: "10" + restart: always +` + +// deployExplorer deploys a new block explorer container to a remote machine via +// SSH, docker and docker-compose. If an instance with the specified network name +// already exists there, it will be overwritten! +func deployExplorer(client *sshClient, network string, chainspec []byte, config *explorerInfos, nocache bool) ([]byte, error) { + // Generate the content to upload to the server + workdir := fmt.Sprintf("%d", rand.Int63()) + files := make(map[string][]byte) + + dockerfile := new(bytes.Buffer) + template.Must(template.New("").Parse(explorerDockerfile)).Execute(dockerfile, map[string]interface{}{ + "NodePort": config.nodePort, + }) + files[filepath.Join(workdir, "Dockerfile")] = dockerfile.Bytes() + + ethstats := new(bytes.Buffer) + template.Must(template.New("").Parse(explorerEthstats)).Execute(ethstats, map[string]interface{}{ + "Port": config.nodePort, + "Name": config.ethstats[:strings.Index(config.ethstats, ":")], + "Secret": config.ethstats[strings.Index(config.ethstats, ":")+1 : strings.Index(config.ethstats, "@")], + "Host": config.ethstats[strings.Index(config.ethstats, "@")+1:], + }) + files[filepath.Join(workdir, "ethstats.json")] = ethstats.Bytes() + + composefile := new(bytes.Buffer) + template.Must(template.New("").Parse(explorerComposefile)).Execute(composefile, map[string]interface{}{ + "Datadir": config.datadir, + "Network": network, + "NodePort": config.nodePort, + "VHost": config.webHost, + "WebPort": config.webPort, + "Ethstats": config.ethstats[:strings.Index(config.ethstats, ":")], + }) + files[filepath.Join(workdir, "docker-compose.yaml")] = composefile.Bytes() + + files[filepath.Join(workdir, "chain.json")] = chainspec + + // Upload the deployment files to the remote server (and clean up afterwards) + if out, err := client.Upload(files); err != nil { + return out, err + } + defer client.Run("rm -rf " + workdir) + + // Build and deploy the boot or seal node service + if nocache { + return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s build --pull --no-cache && docker-compose -p %s up -d --force-recreate", workdir, network, network)) + } + return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s up -d --build --force-recreate", workdir, network)) +} + +// explorerInfos is returned from a block explorer status check to allow reporting +// various configuration parameters. +type explorerInfos struct { + datadir string + ethstats string + nodePort int + webHost string + webPort int +} + +// Report converts the typed struct into a plain string->string map, containing +// most - but not all - fields for reporting to the user. +func (info *explorerInfos) Report() map[string]string { + report := map[string]string{ + "Data directory": info.datadir, + "Node listener port ": strconv.Itoa(info.nodePort), + "Ethstats username": info.ethstats, + "Website address ": info.webHost, + "Website listener port ": strconv.Itoa(info.webPort), + } + return report +} + +// checkExplorer does a health-check against an block explorer server to verify +// whether it's running, and if yes, whether it's responsive. +func checkExplorer(client *sshClient, network string) (*explorerInfos, error) { + // Inspect a possible block explorer container on the host + infos, err := inspectContainer(client, fmt.Sprintf("%s_explorer_1", network)) + if err != nil { + return nil, err + } + if !infos.running { + return nil, ErrServiceOffline + } + // Resolve the port from the host, or the reverse proxy + webPort := infos.portmap["3000/tcp"] + if webPort == 0 { + if proxy, _ := checkNginx(client, network); proxy != nil { + webPort = proxy.port + } + } + if webPort == 0 { + return nil, ErrNotExposed + } + // Resolve the host from the reverse-proxy and the config values + host := infos.envvars["VIRTUAL_HOST"] + if host == "" { + host = client.server + } + // Run a sanity check to see if the devp2p is reachable + nodePort := infos.portmap[infos.envvars["NODE_PORT"]] + if err = checkPort(client.server, nodePort); err != nil { + log.Warn(fmt.Sprintf("Explorer devp2p port seems unreachable"), "server", client.server, "port", nodePort, "err", err) + } + // Assemble and return the useful infos + stats := &explorerInfos{ + datadir: infos.volumes["/root/.local/share/io.parity.ethereum"], + nodePort: nodePort, + webHost: host, + webPort: webPort, + ethstats: infos.envvars["STATS"], + } + return stats, nil +} diff --git a/cmd/puppeth/module_faucet.go b/cmd/puppeth/module_faucet.go index 3c1296bdd..57b7a2dc0 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,36 +26,24 @@ import ( "strconv" "strings" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/log" ) // faucetDockerfile is the Dockerfile required to build an faucet container to // grant crypto tokens based on GitHub authentications. var faucetDockerfile = ` -FROM alpine:latest - -RUN mkdir /go -ENV GOPATH /go - -RUN \ - apk add --update git go make gcc musl-dev ca-certificates linux-headers && \ - mkdir -p $GOPATH/src/github.com/ethereum && \ - (cd $GOPATH/src/github.com/ethereum && git clone --depth=1 https://github.com/ethereum/go-ethereum) && \ - go build -v github.com/ethereum/go-ethereum/cmd/faucet && \ - apk del git go make gcc musl-dev linux-headers && \ - rm -rf $GOPATH && rm -rf /var/cache/apk/* +FROM ethereum/client-go:alltools-latest ADD genesis.json /genesis.json ADD account.json /account.json 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}} \ +ENTRYPOINT [ \ + "faucet", "--genesis", "/genesis.json", "--network", "{{.NetworkID}}", "--bootnodes", "{{.Bootnodes}}", "--ethstats", "{{.Ethstats}}", "--ethport", "{{.EthPort}}", \ + "--faucet.name", "{{.FaucetName}}", "--faucet.amount", "{{.FaucetAmount}}", "--faucet.minutes", "{{.FaucetMinutes}}", "--faucet.tiers", "{{.FaucetTiers}}", \ + "--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 @@ -76,10 +65,9 @@ services: - FAUCET_AMOUNT={{.FaucetAmount}} - FAUCET_MINUTES={{.FaucetMinutes}} - FAUCET_TIERS={{.FaucetTiers}} - - 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: @@ -93,7 +81,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) @@ -104,14 +92,13 @@ func deployFaucet(client *sshClient, network string, bootnodes []string, config "Bootnodes": strings.Join(bootnodes, ","), "Ethstats": config.node.ethstats, "EthPort": config.node.portFull, - "GitHubUser": config.githubUser, - "GitHubToken": config.githubToken, "CaptchaToken": config.captchaToken, "CaptchaSecret": config.captchaSecret, "FaucetName": strings.Title(network), "FaucetAmount": config.amount, "FaucetMinutes": config.minutes, "FaucetTiers": config.tiers, + "NoAuth": config.noauth, }) files[filepath.Join(workdir, "Dockerfile")] = dockerfile.Bytes() @@ -123,13 +110,12 @@ func deployFaucet(client *sshClient, network string, bootnodes []string, config "ApiPort": config.port, "EthPort": config.node.portFull, "EthName": config.node.ethstats[:strings.Index(config.node.ethstats, ":")], - "GitHubUser": config.githubUser, - "GitHubToken": config.githubToken, "CaptchaToken": config.captchaToken, "CaptchaSecret": config.captchaSecret, "FaucetAmount": config.amount, "FaucetMinutes": config.minutes, "FaucetTiers": config.tiers, + "NoAuth": config.noauth, }) files[filepath.Join(workdir, "docker-compose.yaml")] = composefile.Bytes() @@ -144,7 +130,10 @@ func deployFaucet(client *sshClient, network string, bootnodes []string, config defer client.Run("rm -rf " + workdir) // 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 @@ -156,15 +145,38 @@ type faucetInfos struct { amount int minutes int tiers int - githubUser string - githubToken string + noauth bool captchaToken string 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, containing +// 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, + } + if info.noauth { + report["Debug mode (no auth)"] = "enabled" + } + if info.node.keyJSON != "" { + var key struct { + Address string `json:"address"` + } + if err := json.Unmarshal([]byte(info.node.keyJSON), &key); err == nil { + report["Funding account"] = common.HexToAddress(key.Address).Hex() + } else { + log.Error("Failed to retrieve signer address", "err", err) + } + } + return report } // checkFaucet does a health-check against an faucet server to verify whether @@ -224,9 +236,8 @@ func checkFaucet(client *sshClient, network string) (*faucetInfos, error) { amount: amount, minutes: minutes, tiers: tiers, - githubUser: infos.envvars["GITHUB_USER"], - 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/module_nginx.go b/cmd/puppeth/module_nginx.go index fd6d1d74e..35c0efc8a 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" ) @@ -54,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 @@ -78,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 @@ -88,9 +92,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, containing +// 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 375e3e646..69cb19c34 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" ) @@ -38,9 +40,9 @@ 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{{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"] ` @@ -58,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 @@ -79,7 +82,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" @@ -114,6 +117,7 @@ func deployNode(client *sshClient, network string, bootv4, bootv5 []string, conf template.Must(template.New("").Parse(nodeComposefile)).Execute(composefile, map[string]interface{}{ "Type": kind, "Datadir": config.datadir, + "Ethashdir": config.ethashdir, "Network": network, "FullPort": config.portFull, "TotalPeers": config.peersTotal, @@ -127,9 +131,7 @@ func deployNode(client *sshClient, network string, bootv4, bootv5 []string, conf }) files[filepath.Join(workdir, "docker-compose.yaml")] = composefile.Bytes() - //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) @@ -141,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 @@ -150,6 +155,7 @@ type nodeInfos struct { genesis []byte network int64 datadir string + ethashdir string ethstats string portFull int portLight int @@ -164,14 +170,43 @@ 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, containing +// 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) + // Light server enabled + report["Listener port (light nodes)"] = strconv.Itoa(info.portLight) + } + if info.gasTarget > 0 { + // Miner or signer node + report["Gas limit (baseline target)"] = fmt.Sprintf("%0.3f MGas", info.gasTarget) + report["Gas price (minimum accepted)"] = fmt.Sprintf("%0.3f GWei", info.gasPrice) + + if info.etherbase != "" { + // Ethash proof-of-work miner + report["Ethash directory"] = info.ethashdir + report["Miner account"] = info.etherbase + } + if info.keyJSON != "" { + // Clique proof-of-authority signer + var key struct { + Address string `json:"address"` + } + if err := json.Unmarshal([]byte(info.keyJSON), &key); err == nil { + report["Signer account"] = common.HexToAddress(key.Address).Hex() + } else { + log.Error("Failed to retrieve signer address", "err", err) + } + } } - return 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 @@ -223,6 +258,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/module_wallet.go b/cmd/puppeth/module_wallet.go new file mode 100644 index 000000000..67f47c70e --- /dev/null +++ b/cmd/puppeth/module_wallet.go @@ -0,0 +1,200 @@ +// Copyright 2017 The go-ethereum Authors +// This file is part of go-ethereum. +// +// go-ethereum is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// go-ethereum is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>. + +package main + +import ( + "bytes" + "fmt" + "html/template" + "math/rand" + "path/filepath" + "strconv" + "strings" + + "github.com/ethereum/go-ethereum/log" +) + +// walletDockerfile is the Dockerfile required to run a web wallet. +var walletDockerfile = ` +FROM puppeth/wallet:latest + +ADD genesis.json /genesis.json + +RUN \ + echo 'node server.js &' > wallet.sh && \ + echo 'geth --cache 512 init /genesis.json' >> wallet.sh && \ + echo $'geth --networkid {{.NetworkID}} --port {{.NodePort}} --bootnodes {{.Bootnodes}} --ethstats \'{{.Ethstats}}\' --cache=512 --rpc --rpcaddr=0.0.0.0 --rpccorsdomain "*"' >> wallet.sh + +RUN \ + sed -i 's/PuppethNetworkID/{{.NetworkID}}/g' dist/js/etherwallet-master.js && \ + sed -i 's/PuppethNetwork/{{.Network}}/g' dist/js/etherwallet-master.js && \ + sed -i 's/PuppethDenom/{{.Denom}}/g' dist/js/etherwallet-master.js && \ + sed -i 's/PuppethHost/{{.Host}}/g' dist/js/etherwallet-master.js && \ + sed -i 's/PuppethRPCPort/{{.RPCPort}}/g' dist/js/etherwallet-master.js + +ENTRYPOINT ["/bin/sh", "wallet.sh"] +` + +// walletComposefile is the docker-compose.yml file required to deploy and +// maintain a web wallet. +var walletComposefile = ` +version: '2' +services: + wallet: + build: . + image: {{.Network}}/wallet + ports: + - "{{.NodePort}}:{{.NodePort}}" + - "{{.NodePort}}:{{.NodePort}}/udp" + - "{{.RPCPort}}:8545"{{if not .VHost}} + - "{{.WebPort}}:80"{{end}} + volumes: + - {{.Datadir}}:/root/.ethereum + environment: + - NODE_PORT={{.NodePort}}/tcp + - STATS={{.Ethstats}}{{if .VHost}} + - VIRTUAL_HOST={{.VHost}} + - VIRTUAL_PORT=80{{end}} + logging: + driver: "json-file" + options: + max-size: "1m" + max-file: "10" + restart: always +` + +// deployWallet deploys a new web wallet container to a remote machine via SSH, +// docker and docker-compose. If an instance with the specified network name +// already exists there, it will be overwritten! +func deployWallet(client *sshClient, network string, bootnodes []string, config *walletInfos, nocache bool) ([]byte, error) { + // Generate the content to upload to the server + workdir := fmt.Sprintf("%d", rand.Int63()) + files := make(map[string][]byte) + + dockerfile := new(bytes.Buffer) + template.Must(template.New("").Parse(walletDockerfile)).Execute(dockerfile, map[string]interface{}{ + "Network": strings.ToTitle(network), + "Denom": strings.ToUpper(network), + "NetworkID": config.network, + "NodePort": config.nodePort, + "RPCPort": config.rpcPort, + "Bootnodes": strings.Join(bootnodes, ","), + "Ethstats": config.ethstats, + "Host": client.address, + }) + files[filepath.Join(workdir, "Dockerfile")] = dockerfile.Bytes() + + composefile := new(bytes.Buffer) + template.Must(template.New("").Parse(walletComposefile)).Execute(composefile, map[string]interface{}{ + "Datadir": config.datadir, + "Network": network, + "NodePort": config.nodePort, + "RPCPort": config.rpcPort, + "VHost": config.webHost, + "WebPort": config.webPort, + "Ethstats": config.ethstats[:strings.Index(config.ethstats, ":")], + }) + files[filepath.Join(workdir, "docker-compose.yaml")] = composefile.Bytes() + + files[filepath.Join(workdir, "genesis.json")] = config.genesis + + // Upload the deployment files to the remote server (and clean up afterwards) + if out, err := client.Upload(files); err != nil { + return out, err + } + defer client.Run("rm -rf " + workdir) + + // Build and deploy the boot or seal node service + if nocache { + return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s build --pull --no-cache && docker-compose -p %s up -d --force-recreate", workdir, network, network)) + } + return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s up -d --build --force-recreate", workdir, network)) +} + +// walletInfos is returned from a web wallet status check to allow reporting +// various configuration parameters. +type walletInfos struct { + genesis []byte + network int64 + datadir string + ethstats string + nodePort int + rpcPort int + webHost string + webPort int +} + +// Report converts the typed struct into a plain string->string map, containing +// most - but not all - fields for reporting to the user. +func (info *walletInfos) Report() map[string]string { + report := map[string]string{ + "Data directory": info.datadir, + "Ethstats username": info.ethstats, + "Node listener port ": strconv.Itoa(info.nodePort), + "RPC listener port ": strconv.Itoa(info.rpcPort), + "Website address ": info.webHost, + "Website listener port ": strconv.Itoa(info.webPort), + } + return report +} + +// checkWallet does a health-check against web wallet server to verify whether +// it's running, and if yes, whether it's responsive. +func checkWallet(client *sshClient, network string) (*walletInfos, error) { + // Inspect a possible web wallet container on the host + infos, err := inspectContainer(client, fmt.Sprintf("%s_wallet_1", network)) + if err != nil { + return nil, err + } + if !infos.running { + return nil, ErrServiceOffline + } + // Resolve the port from the host, or the reverse proxy + webPort := infos.portmap["80/tcp"] + if webPort == 0 { + if proxy, _ := checkNginx(client, network); proxy != nil { + webPort = proxy.port + } + } + if webPort == 0 { + return nil, ErrNotExposed + } + // Resolve the host from the reverse-proxy and the config values + host := infos.envvars["VIRTUAL_HOST"] + if host == "" { + host = client.server + } + // Run a sanity check to see if the devp2p and RPC ports are reachable + nodePort := infos.portmap[infos.envvars["NODE_PORT"]] + if err = checkPort(client.server, nodePort); err != nil { + log.Warn(fmt.Sprintf("Wallet devp2p port seems unreachable"), "server", client.server, "port", nodePort, "err", err) + } + rpcPort := infos.portmap["8545/tcp"] + if err = checkPort(client.server, rpcPort); err != nil { + log.Warn(fmt.Sprintf("Wallet RPC port seems unreachable"), "server", client.server, "port", rpcPort, "err", err) + } + // Assemble and return the useful infos + stats := &walletInfos{ + datadir: infos.volumes["/root/.ethereum"], + nodePort: nodePort, + rpcPort: rpcPort, + webHost: host, + webPort: webPort, + ethstats: infos.envvars["STATS"], + } + return stats, nil +} diff --git a/cmd/puppeth/puppeth.go b/cmd/puppeth/puppeth.go index f783a7981..26382dac1 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/ssh.go b/cmd/puppeth/ssh.go index ec6a1b669..158261ce0 100644 --- a/cmd/puppeth/ssh.go +++ b/cmd/puppeth/ssh.go @@ -116,6 +116,7 @@ func dial(server string, pubkey []byte) (*sshClient, error) { keycheck := func(hostname string, remote net.Addr, key ssh.PublicKey) error { // If no public key is known for SSH, ask the user to confirm if pubkey == nil { + fmt.Println() fmt.Printf("The authenticity of host '%s (%s)' can't be established.\n", hostname, remote) fmt.Printf("SSH key fingerprint is %s [MD5]\n", ssh.FingerprintLegacyMD5(key)) fmt.Printf("Are you sure you want to continue connecting (yes/no)? ") @@ -215,8 +216,8 @@ func (client *sshClient) Stream(cmd string) error { return session.Run(cmd) } -// Upload copied the set of files to a remote server via SCP, creating any non- -// existing folder in te mean time. +// Upload copies the set of files to a remote server via SCP, creating any non- +// existing folders in the mean time. func (client *sshClient) Upload(files map[string][]byte) ([]byte, error) { // Establish a single command session session, err := client.client.NewSession() diff --git a/cmd/puppeth/wizard.go b/cmd/puppeth/wizard.go index eb6d9e5aa..2e2b4644c 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" @@ -38,12 +39,12 @@ import ( // config contains all the configurations needed by puppeth that should be saved // between sessions. type config struct { - path string // File containing the configuration values - genesis *core.Genesis // Genesis block to cache for node deploys - bootFull []string // Bootnodes to always connect to by full nodes - bootLight []string // Bootnodes to always connect to by light nodes - ethstats string // Ethstats settings to cache for node deploys + path string // File containing the configuration values + bootFull []string // Bootnodes to always connect to by full nodes + bootLight []string // Bootnodes to always connect to by light nodes + ethstats string // Ethstats settings to cache for node deploys + Genesis *core.Genesis `json:"genesis,omitempty"` // Genesis block to cache for node deploys Servers map[string][]byte `json:"servers,omitempty"` } @@ -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_dashboard.go b/cmd/puppeth/wizard_dashboard.go index 53a28a535..5f781c415 100644 --- a/cmd/puppeth/wizard_dashboard.go +++ b/cmd/puppeth/wizard_dashboard.go @@ -40,6 +40,8 @@ func (w *wizard) deployDashboard() { host: client.server, } } + existed := err == nil + // Figure out which port to listen on fmt.Println() fmt.Printf("Which port should the dashboard listen on? (default = %d)\n", infos.port) @@ -58,7 +60,6 @@ func (w *wizard) deployDashboard() { available[service] = append(available[service], server) } } - listing := make(map[string]string) for _, service := range []string{"ethstats", "explorer", "wallet", "faucet"} { // Gather all the locally hosted pages of this type var pages []string @@ -74,6 +75,14 @@ func (w *wizard) deployDashboard() { if infos, err := checkEthstats(client, w.network); err == nil { port = infos.port } + case "explorer": + if infos, err := checkExplorer(client, w.network); err == nil { + port = infos.webPort + } + case "wallet": + if infos, err := checkWallet(client, w.network); err == nil { + port = infos.webPort + } case "faucet": if infos, err := checkFaucet(client, w.network); err == nil { port = infos.port @@ -101,26 +110,43 @@ func (w *wizard) deployDashboard() { log.Error("Invalid listing choice, aborting") return } + var page string switch { case choice <= len(pages): - listing[service] = pages[choice-1] + page = pages[choice-1] case choice == len(pages)+1: fmt.Println() fmt.Printf("Which address is the external %s service at?\n", service) - listing[service] = w.readString() + page = w.readString() default: // No service hosting for this } + // Save the users choice + switch service { + case "ethstats": + infos.ethstats = page + case "explorer": + infos.explorer = page + case "wallet": + infos.wallet = page + case "faucet": + infos.faucet = page + } } // If we have ethstats running, ask whether to make the secret public or not - var ethstats bool if w.conf.ethstats != "" { fmt.Println() fmt.Println("Include ethstats secret on dashboard (y/n)? (default = yes)") - ethstats = w.readDefaultString("y") == "y" + infos.trusted = w.readDefaultString("y") == "y" } // Try to deploy the dashboard container on the host - if out, err := deployDashboard(client, w.network, infos.port, infos.host, listing, &w.conf, ethstats); err != nil { + nocache := false + if existed { + fmt.Println() + fmt.Printf("Should the dashboard be built from scratch (y/n)? (default = no)\n") + nocache = w.readDefaultString("n") != "n" + } + if out, err := deployDashboard(client, w.network, &w.conf, infos, nocache); err != nil { log.Error("Failed to deploy dashboard container", "err", err) if len(out) > 0 { fmt.Printf("%s\n", out) @@ -128,5 +154,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 8bfa1d6e5..fb2529c26 100644 --- a/cmd/puppeth/wizard_ethstats.go +++ b/cmd/puppeth/wizard_ethstats.go @@ -42,6 +42,8 @@ func (w *wizard) deployEthstats() { secret: "", } } + existed := err == nil + // Figure out which port to listen on fmt.Println() fmt.Printf("Which port should ethstats listen on? (default = %d)\n", infos.port) @@ -62,49 +64,57 @@ func (w *wizard) deployEthstats() { infos.secret = w.readDefaultString(infos.secret) } // Gather any blacklists to ban from reporting - fmt.Println() - fmt.Printf("Keep existing IP %v blacklist (y/n)? (default = yes)\n", infos.banned) - if w.readDefaultString("y") != "y" { - // The user might want to clear the entire list, although generally probably not + if existed { fmt.Println() - fmt.Printf("Clear out blacklist and start over (y/n)? (default = no)\n") - if w.readDefaultString("n") != "n" { - infos.banned = nil - } - // Offer the user to explicitly add/remove certain IP addresses - fmt.Println() - fmt.Println("Which additional IP addresses should be blacklisted?") - for { - if ip := w.readIPAddress(); ip != "" { - infos.banned = append(infos.banned, ip) - continue + fmt.Printf("Keep existing IP %v blacklist (y/n)? (default = yes)\n", infos.banned) + if w.readDefaultString("y") != "y" { + // The user might want to clear the entire list, although generally probably not + fmt.Println() + fmt.Printf("Clear out blacklist and start over (y/n)? (default = no)\n") + if w.readDefaultString("n") != "n" { + infos.banned = nil } - break - } - fmt.Println() - fmt.Println("Which IP addresses should not be blacklisted?") - for { - if ip := w.readIPAddress(); ip != "" { - for i, addr := range infos.banned { - if ip == addr { - infos.banned = append(infos.banned[:i], infos.banned[i+1:]...) - break + // Offer the user to explicitly add/remove certain IP addresses + fmt.Println() + fmt.Println("Which additional IP addresses should be blacklisted?") + for { + if ip := w.readIPAddress(); ip != "" { + infos.banned = append(infos.banned, ip) + continue + } + break + } + fmt.Println() + fmt.Println("Which IP addresses should not be blacklisted?") + for { + if ip := w.readIPAddress(); ip != "" { + for i, addr := range infos.banned { + if ip == addr { + infos.banned = append(infos.banned[:i], infos.banned[i+1:]...) + break + } } + continue } - continue + break } - break + sort.Strings(infos.banned) } - sort.Strings(infos.banned) } // Try to deploy the ethstats server on the host + nocache := false + if existed { + fmt.Println() + fmt.Printf("Should the ethstats be built from scratch (y/n)? (default = no)\n") + nocache = w.readDefaultString("n") != "n" + } trusted := make([]string, 0, len(w.servers)) 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) @@ -112,5 +122,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_explorer.go b/cmd/puppeth/wizard_explorer.go new file mode 100644 index 000000000..10ef72f78 --- /dev/null +++ b/cmd/puppeth/wizard_explorer.go @@ -0,0 +1,117 @@ +// Copyright 2017 The go-ethereum Authors +// This file is part of go-ethereum. +// +// go-ethereum is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// go-ethereum is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>. + +package main + +import ( + "encoding/json" + "fmt" + "time" + + "github.com/ethereum/go-ethereum/log" +) + +// deployExplorer creates a new block explorer based on some user input. +func (w *wizard) deployExplorer() { + // Do some sanity check before the user wastes time on input + if w.conf.Genesis == nil { + log.Error("No genesis block configured") + return + } + if w.conf.ethstats == "" { + log.Error("No ethstats server configured") + return + } + if w.conf.Genesis.Config.Ethash == nil { + log.Error("Only ethash network supported") + return + } + // Select the server to interact with + server := w.selectServer() + if server == "" { + return + } + client := w.servers[server] + + // Retrieve any active node configurations from the server + infos, err := checkExplorer(client, w.network) + if err != nil { + infos = &explorerInfos{ + nodePort: 30303, webPort: 80, webHost: client.server, + } + } + existed := err == nil + + chainspec, err := newParityChainSpec(w.network, w.conf.Genesis, w.conf.bootFull) + if err != nil { + log.Error("Failed to create chain spec for explorer", "err", err) + return + } + chain, _ := json.MarshalIndent(chainspec, "", " ") + + // Figure out which port to listen on + fmt.Println() + fmt.Printf("Which port should the explorer listen on? (default = %d)\n", infos.webPort) + infos.webPort = w.readDefaultInt(infos.webPort) + + // Figure which virtual-host to deploy ethstats on + if infos.webHost, err = w.ensureVirtualHost(client, infos.webPort, infos.webHost); err != nil { + log.Error("Failed to decide on explorer host", "err", err) + return + } + // Figure out where the user wants to store the persistent data + fmt.Println() + if infos.datadir == "" { + fmt.Printf("Where should data be stored on the remote machine?\n") + infos.datadir = w.readString() + } else { + fmt.Printf("Where should data be stored on the remote machine? (default = %s)\n", infos.datadir) + infos.datadir = w.readDefaultString(infos.datadir) + } + // Figure out which port to listen on + fmt.Println() + fmt.Printf("Which TCP/UDP port should the archive node listen on? (default = %d)\n", infos.nodePort) + infos.nodePort = w.readDefaultInt(infos.nodePort) + + // Set a proper name to report on the stats page + fmt.Println() + if infos.ethstats == "" { + fmt.Printf("What should the explorer be called on the stats page?\n") + infos.ethstats = w.readString() + ":" + w.conf.ethstats + } else { + fmt.Printf("What should the explorer be called on the stats page? (default = %s)\n", infos.ethstats) + infos.ethstats = w.readDefaultString(infos.ethstats) + ":" + w.conf.ethstats + } + // Try to deploy the explorer on the host + nocache := false + if existed { + fmt.Println() + fmt.Printf("Should the explorer be built from scratch (y/n)? (default = no)\n") + nocache = w.readDefaultString("n") != "n" + } + if out, err := deployExplorer(client, w.network, chain, infos, nocache); err != nil { + log.Error("Failed to deploy explorer container", "err", err) + if len(out) > 0 { + fmt.Printf("%s\n", out) + } + return + } + // All ok, run a network scan to pick any changes up + log.Info("Waiting for node to finish booting") + time.Sleep(3 * time.Second) + + w.networkStats() +} diff --git a/cmd/puppeth/wizard_faucet.go b/cmd/puppeth/wizard_faucet.go index 51c4e2f7f..191575b16 100644 --- a/cmd/puppeth/wizard_faucet.go +++ b/cmd/puppeth/wizard_faucet.go @@ -19,7 +19,6 @@ package main import ( "encoding/json" "fmt" - "net/http" "github.com/ethereum/go-ethereum/accounts/keystore" "github.com/ethereum/go-ethereum/log" @@ -47,8 +46,10 @@ func (w *wizard) deployFaucet() { tiers: 3, } } - infos.node.genesis, _ = json.MarshalIndent(w.conf.genesis, "", " ") - infos.node.network = w.conf.genesis.Config.ChainId.Int64() + existed := err == nil + + infos.node.genesis, _ = json.MarshalIndent(w.conf.Genesis, "", " ") + infos.node.network = w.conf.Genesis.Config.ChainId.Int64() // Figure out which port to listen on fmt.Println() @@ -60,7 +61,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) @@ -76,47 +77,6 @@ func (w *wizard) deployFaucet() { log.Error("At least one funding tier must be set") return } - // Accessing GitHub gists requires API authorization, retrieve it - if infos.githubUser != "" { - fmt.Println() - fmt.Printf("Reuse previous (%s) GitHub API authorization (y/n)? (default = yes)\n", infos.githubUser) - if w.readDefaultString("y") != "y" { - infos.githubUser, infos.githubToken = "", "" - } - } - if infos.githubUser == "" { - // No previous authorization (or new one requested) - fmt.Println() - fmt.Println("Which GitHub user to verify Gists through?") - infos.githubUser = w.readString() - - fmt.Println() - fmt.Println("What is the GitHub personal access token of the user? (won't be echoed)") - infos.githubToken = w.readPassword() - - // Do a sanity check query against github to ensure it's valid - req, _ := http.NewRequest("GET", "https://api.github.com/user", nil) - req.SetBasicAuth(infos.githubUser, infos.githubToken) - res, err := http.DefaultClient.Do(req) - if err != nil { - log.Error("Failed to verify GitHub authentication", "err", err) - return - } - defer res.Body.Close() - - var msg struct { - Login string `json:"login"` - Message string `json:"message"` - } - if err = json.NewDecoder(res.Body).Decode(&msg); err != nil { - log.Error("Failed to decode authorization response", "err", err) - return - } - if msg.Login != infos.githubUser { - log.Error("GitHub authorization failed", "user", infos.githubUser, "message", msg.Message) - return - } - } // Accessing the reCaptcha service requires API authorizations, request it if infos.captchaToken != "" { fmt.Println() @@ -129,7 +89,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 +137,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,11 +148,27 @@ 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 - if out, err := deployFaucet(client, w.network, w.conf.bootLight, infos); err != nil { + nocache := false + if existed { + fmt.Println() + fmt.Printf("Should the faucet be built from scratch (y/n)? (default = no)\n") + nocache = w.readDefaultString("n") != "n" + } + if out, err := deployFaucet(client, w.network, w.conf.bootLight, infos, nocache); err != nil { log.Error("Failed to deploy faucet container", "err", err) if len(out) > 0 { fmt.Printf("%s\n", out) @@ -198,5 +176,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_genesis.go b/cmd/puppeth/wizard_genesis.go index 222fc2a7c..f255ef6e6 100644 --- a/cmd/puppeth/wizard_genesis.go +++ b/cmd/puppeth/wizard_genesis.go @@ -37,7 +37,7 @@ func (w *wizard) makeGenesis() { genesis := &core.Genesis{ Timestamp: uint64(time.Now().Unix()), GasLimit: 4700000, - Difficulty: big.NewInt(1048576), + Difficulty: big.NewInt(524288), Alloc: make(core.GenesisAlloc), Config: ¶ms.ChainConfig{ HomesteadBlock: big.NewInt(1), @@ -118,24 +118,16 @@ func (w *wizard) makeGenesis() { for i := int64(0); i < 256; i++ { genesis.Alloc[common.BigToAddress(big.NewInt(i))] = core.GenesisAccount{Balance: big.NewInt(1)} } - fmt.Println() - // Query the user for some custom extras fmt.Println() fmt.Println("Specify your chain/network ID if you want an explicit one (default = random)") genesis.Config.ChainId = new(big.Int).SetUint64(uint64(w.readDefaultInt(rand.Intn(65536)))) - fmt.Println() - fmt.Println("Anything fun to embed into the genesis block? (max 32 bytes)") - - extra := w.read() - if len(extra) > 32 { - extra = extra[:32] - } - genesis.ExtraData = append([]byte(extra), genesis.ExtraData[len(extra):]...) - // All done, store the genesis and flush to disk - w.conf.genesis = genesis + log.Info("Configured new genesis block") + + w.conf.Genesis = genesis + w.conf.flush() } // manageGenesis permits the modification of chain configuration parameters in @@ -145,44 +137,56 @@ func (w *wizard) manageGenesis() { fmt.Println() fmt.Println(" 1. Modify existing fork rules") fmt.Println(" 2. Export genesis configuration") + fmt.Println(" 3. Remove genesis configuration") choice := w.read() switch { case choice == "1": // Fork rule updating requested, iterate over each fork fmt.Println() - fmt.Printf("Which block should Homestead come into effect? (default = %v)\n", w.conf.genesis.Config.HomesteadBlock) - w.conf.genesis.Config.HomesteadBlock = w.readDefaultBigInt(w.conf.genesis.Config.HomesteadBlock) + fmt.Printf("Which block should Homestead come into effect? (default = %v)\n", w.conf.Genesis.Config.HomesteadBlock) + w.conf.Genesis.Config.HomesteadBlock = w.readDefaultBigInt(w.conf.Genesis.Config.HomesteadBlock) fmt.Println() - fmt.Printf("Which block should EIP150 come into effect? (default = %v)\n", w.conf.genesis.Config.EIP150Block) - w.conf.genesis.Config.EIP150Block = w.readDefaultBigInt(w.conf.genesis.Config.EIP150Block) + fmt.Printf("Which block should EIP150 come into effect? (default = %v)\n", w.conf.Genesis.Config.EIP150Block) + w.conf.Genesis.Config.EIP150Block = w.readDefaultBigInt(w.conf.Genesis.Config.EIP150Block) fmt.Println() - fmt.Printf("Which block should EIP155 come into effect? (default = %v)\n", w.conf.genesis.Config.EIP155Block) - w.conf.genesis.Config.EIP155Block = w.readDefaultBigInt(w.conf.genesis.Config.EIP155Block) + fmt.Printf("Which block should EIP155 come into effect? (default = %v)\n", w.conf.Genesis.Config.EIP155Block) + w.conf.Genesis.Config.EIP155Block = w.readDefaultBigInt(w.conf.Genesis.Config.EIP155Block) fmt.Println() - fmt.Printf("Which block should EIP158 come into effect? (default = %v)\n", w.conf.genesis.Config.EIP158Block) - w.conf.genesis.Config.EIP158Block = w.readDefaultBigInt(w.conf.genesis.Config.EIP158Block) + fmt.Printf("Which block should EIP158 come into effect? (default = %v)\n", w.conf.Genesis.Config.EIP158Block) + w.conf.Genesis.Config.EIP158Block = w.readDefaultBigInt(w.conf.Genesis.Config.EIP158Block) fmt.Println() - fmt.Printf("Which block should Byzantium come into effect? (default = %v)\n", w.conf.genesis.Config.ByzantiumBlock) - w.conf.genesis.Config.ByzantiumBlock = w.readDefaultBigInt(w.conf.genesis.Config.ByzantiumBlock) + fmt.Printf("Which block should Byzantium come into effect? (default = %v)\n", w.conf.Genesis.Config.ByzantiumBlock) + w.conf.Genesis.Config.ByzantiumBlock = w.readDefaultBigInt(w.conf.Genesis.Config.ByzantiumBlock) - out, _ := json.MarshalIndent(w.conf.genesis.Config, "", " ") + out, _ := json.MarshalIndent(w.conf.Genesis.Config, "", " ") fmt.Printf("Chain configuration updated:\n\n%s\n", out) case choice == "2": // Save whatever genesis configuration we currently have fmt.Println() fmt.Printf("Which file to save the genesis into? (default = %s.json)\n", w.network) - out, _ := json.MarshalIndent(w.conf.genesis, "", " ") + out, _ := json.MarshalIndent(w.conf.Genesis, "", " ") if err := ioutil.WriteFile(w.readDefaultString(fmt.Sprintf("%s.json", w.network)), out, 0644); err != nil { log.Error("Failed to save genesis file", "err", err) } log.Info("Exported existing genesis block") + case choice == "3": + // Make sure we don't have any services running + if len(w.conf.servers()) > 0 { + log.Error("Genesis reset requires all services and servers torn down") + return + } + log.Info("Genesis block destroyed") + + w.conf.Genesis = nil + w.conf.flush() + default: log.Error("That's not something I can do") } diff --git a/cmd/puppeth/wizard_intro.go b/cmd/puppeth/wizard_intro.go index 2d9a097ee..84998afc9 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" ) @@ -63,7 +64,7 @@ func (w *wizard) run() { for { w.network = w.readString() if !strings.Contains(w.network, " ") { - fmt.Printf("Sweet, you can set this via --network=%s next time!\n\n", w.network) + fmt.Printf("\nSweet, you can set this via --network=%s next time!\n\n", w.network) break } log.Error("I also like to live dangerously, still no spaces") @@ -80,22 +81,33 @@ func (w *wizard) run() { } else if err := json.Unmarshal(blob, &w.conf); err != nil { 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) } - w.networkStats(false) + pend.Wait() + w.networkStats() } // Basics done, loop ad infinitum about what to do for { fmt.Println() fmt.Println("What would you like to do? (default = stats)") fmt.Println(" 1. Show network stats") - if w.conf.genesis == nil { + if w.conf.Genesis == nil { fmt.Println(" 2. Configure new genesis") } else { fmt.Println(" 2. Manage existing genesis") @@ -110,15 +122,14 @@ 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 { + if w.conf.Genesis == nil { w.makeGenesis() } else { w.manageGenesis() @@ -126,7 +137,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 +149,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 c06972198..e19180bb1 100644 --- a/cmd/puppeth/wizard_netstats.go +++ b/cmd/puppeth/wizard_netstats.go @@ -18,9 +18,10 @@ package main import ( "encoding/json" - "fmt" "os" + "sort" "strings" + "sync" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/log" @@ -29,207 +30,265 @@ 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") + log.Info("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 := tablewriter.NewWriter(os.Stdout) - stats.SetHeader([]string{"Server", "IP", "Status", "Service", "Details"}) - stats.SetColWidth(100) + 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") - - // If the server is not connected, try to connect again - 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(), "", ""}) - 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() - } - } else { - services["nginx"] = infos.String() - } - logger.Debug("Checking for ethstats availability") - if infos, err := checkEthstats(client, w.network); err != nil { - if err != ErrServiceUnknown { - services["ethstats"] = err.Error() - } - } else { - services["ethstats"] = infos.String() - 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() - } - } else { - services["bootnode"] = infos.String() + 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]) - protips.genesis = string(infos.genesis) - protips.bootFull = append(protips.bootFull, infos.enodeFull) - if infos.enodeLight != "" { - protips.bootLight = append(protips.bootLight, infos.enodeLight) + // All status checks complete, report and check next server + w.lock.Lock() + defer w.lock.Unlock() + + delete(w.services, server) + for service := range stat.services { + w.services[server] = append(w.services[server], service) } + stats[server] = stat + }(server, pubkey) + } + pend.Wait() + + // Print any collected stats and return + stats.render() +} + +// gatherStats gathers service statistics for a particular remote server. +func (w *wizard) gatherStats(server string, pubkey []byte, client *sshClient) *serverStat { + // Gather some global stats to feed into the wizard + var ( + genesis string + ethstats string + bootFull []string + bootLight []string + ) + // Ensure a valid SSH connection to the remote server + logger := log.New("server", server) + logger.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 } - logger.Debug("Checking for sealnode availability") - if infos, err := checkNode(client, w.network, false); err != nil { - if err != ErrServiceUnknown { - services["sealnode"] = err.Error() - } - } else { - services["sealnode"] = infos.String() - protips.genesis = string(infos.genesis) + 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 faucet availability") - if infos, err := checkFaucet(client, w.network); err != nil { - if err != ErrServiceUnknown { - services["faucet"] = err.Error() - } - } else { - services["faucet"] = infos.String() + } 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 dashboard availability") - if infos, err := checkDashboard(client, w.network); err != nil { - if err != ErrServiceUnknown { - services["dashboard"] = err.Error() - } - } else { - services["dashboard"] = infos.String() + } 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()} } - // All status checks complete, report and check next server - delete(w.services, server) - for service := range services { - w.services[server] = append(w.services[server], service) + } else { + stat.services["bootnode"] = infos.Report() + + genesis = string(infos.genesis) + bootFull = append(bootFull, infos.enodeFull) + if infos.enodeLight != "" { + bootLight = append(bootLight, infos.enodeLight) } - server, address := client.server, client.address - for service, status := range services { - stats.Append([]string{server, address, "online", service, status}) - server, address = "", "" + } + 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()} } - if len(services) == 0 { - stats.Append([]string{server, address, "online", "", ""}) + } else { + 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() } - // If a genesis block was found, load it into our configs - if protips.genesis != "" && w.conf.genesis == nil { - genesis := new(core.Genesis) - if err := json.Unmarshal([]byte(protips.genesis), genesis); err != nil { - log.Error("Failed to parse remote genesis", "err", err) - } else { - w.conf.genesis = genesis - protips.network = genesis.Config.ChainId.Int64() + 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() } - if protips.ethstats != "" { - w.conf.ethstats = protips.ethstats + 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() } - w.conf.bootFull = protips.bootFull - w.conf.bootLight = protips.bootLight - - // Print any collected stats and return - if !tips { - stats.Render() + 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 { - protips.print(w.network) + stat.services["dashboard"] = infos.Report() } -} - -// 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 -} + // Feed and newly discovered information into the wizard + w.lock.Lock() + defer w.lock.Unlock() -// 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) + 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 { - statsflag = fmt.Sprintf(` --ethstats=yournode:%s`, p.ethstats) + w.conf.Genesis = g } } - // 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, ",")) + if ethstats != "" { + w.conf.ethstats = ethstats } - // Assemble all the known pro-tips - var tasks, tips []string + w.conf.bootFull = append(w.conf.bootFull, bootFull...) + w.conf.bootLight = append(w.conf.bootLight, bootLight...) - 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)) + return stat +} + +// 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 +} - 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)) +// serverStats is a collection of server stats for multiple hosts. +type serverStats map[string]*serverStat - 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)) +// 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 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)) + table.SetHeader([]string{"Server", "Address", "Service", "Config", "Value"}) + table.SetAlignment(tablewriter.ALIGN_LEFT) + table.SetColWidth(100) - // 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)) + } + } + } + } + // Fill up the server report in alphabetical order + servers := make([]string, 0, len(stats)) + for server := range stats { + servers = append(servers, server) } - fmt.Println() - if short { - howto := tablewriter.NewWriter(os.Stdout) - howto.SetHeader([]string{"Fun tasks for you", "Tips on how to"}) - howto.SetColWidth(100) + 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) + + if len(services) == 0 { + table.Append([]string{server, stats[server].address, "", "", ""}) + } + for j, service := range services { + // Add an empty line between all services + if j > 0 { + table.Append([]string{"", "", "", separator[3], separator[4]}) + } + // Fill up the config report in alphabetical order + configs := make([]string, 0, len(stats[server].services[service])) + for service := range stats[server].services[service] { + configs = append(configs, service) + } + sort.Strings(configs) + + for k, config := range configs { + switch { + case j == 0 && k == 0: + table.Append([]string{server, stats[server].address, service, config, stats[server].services[service][config]}) + case k == 0: + table.Append([]string{"", "", service, config, stats[server].services[service][config]}) + default: + table.Append([]string{"", "", "", config, stats[server].services[service][config]}) + } + } } - 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 c20e31fab..d780c550b 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() } } @@ -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") + 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,12 @@ func (w *wizard) deployComponent() { case "3": w.deployNode(false) case "4": + w.deployExplorer() case "5": - w.deployFaucet() + w.deployWallet() case "6": + w.deployFaucet() + case "7": w.deployDashboard() default: log.Error("That's not something I can do") diff --git a/cmd/puppeth/wizard_nginx.go b/cmd/puppeth/wizard_nginx.go index 86fba29f5..4eeae93a0 100644 --- a/cmd/puppeth/wizard_nginx.go +++ b/cmd/puppeth/wizard_nginx.go @@ -29,7 +29,8 @@ import ( // // If the user elects not to use a reverse proxy, an empty hostname is returned! func (w *wizard) ensureVirtualHost(client *sshClient, port int, def string) (string, error) { - if proxy, _ := checkNginx(client, w.network); proxy != nil { + proxy, _ := checkNginx(client, w.network) + if proxy != nil { // Reverse proxy is running, if ports match, we need a virtual host if proxy.port == port { fmt.Println() @@ -41,7 +42,13 @@ 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 { + nocache := false + if proxy != nil { + fmt.Println() + fmt.Printf("Should the reverse-proxy be rebuilt from scratch (y/n)? (default = no)\n") + nocache = w.readDefaultString("n") != "n" + } + if out, err := deployNginx(client, w.network, port, nocache); err != nil { log.Error("Failed to deploy reverse-proxy", "err", err) if len(out) > 0 { fmt.Printf("%s\n", out) diff --git a/cmd/puppeth/wizard_node.go b/cmd/puppeth/wizard_node.go index 05232486b..097e2e41a 100644 --- a/cmd/puppeth/wizard_node.go +++ b/cmd/puppeth/wizard_node.go @@ -29,7 +29,7 @@ import ( // deployNode creates a new node configuration based on some user input. func (w *wizard) deployNode(boot bool) { // Do some sanity check before the user wastes time on input - if w.conf.genesis == nil { + if w.conf.Genesis == nil { log.Error("No genesis block configured") return } @@ -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 { @@ -53,8 +53,10 @@ func (w *wizard) deployNode(boot bool) { infos = &nodeInfos{portFull: 30303, peersTotal: 50, peersLight: 0, gasTarget: 4.7, gasPrice: 18} } } - infos.genesis, _ = json.MarshalIndent(w.conf.genesis, "", " ") - infos.network = w.conf.genesis.Config.ChainId.Int64() + existed := err == nil + + infos.genesis, _ = json.MarshalIndent(w.conf.Genesis, "", " ") + infos.network = w.conf.Genesis.Config.ChainId.Int64() // Figure out where the user wants to store the persistent data fmt.Println() @@ -65,6 +67,16 @@ func (w *wizard) deployNode(boot bool) { fmt.Printf("Where should data be stored on the remote machine? (default = %s)\n", infos.datadir) infos.datadir = w.readDefaultString(infos.datadir) } + if w.conf.Genesis.Config.Ethash != nil && !boot { + fmt.Println() + if infos.ethashdir == "" { + fmt.Printf("Where should the ethash mining DAGs be stored on the remote machine?\n") + infos.ethashdir = w.readString() + } else { + fmt.Printf("Where should the ethash mining DAGs be stored on the remote machine? (default = %s)\n", infos.ethashdir) + infos.ethashdir = w.readDefaultString(infos.ethashdir) + } + } // Figure out which port to listen on fmt.Println() fmt.Printf("Which TCP/UDP port to listen on? (default = %d)\n", infos.portFull) @@ -91,7 +103,7 @@ func (w *wizard) deployNode(boot bool) { } // If the node is a miner/signer, load up needed credentials if !boot { - if w.conf.genesis.Config.Ethash != nil { + if w.conf.Genesis.Config.Ethash != nil { // Ethash based miners only need an etherbase to mine against fmt.Println() if infos.etherbase == "" { @@ -106,7 +118,7 @@ func (w *wizard) deployNode(boot bool) { fmt.Printf("What address should the miner user? (default = %s)\n", infos.etherbase) infos.etherbase = w.readDefaultAddress(common.HexToAddress(infos.etherbase)).Hex() } - } else if w.conf.genesis.Config.Clique != nil { + } else if w.conf.Genesis.Config.Clique != nil { // If a previous signer was already set, offer to reuse it if infos.keyJSON != "" { if key, err := keystore.DecryptKey([]byte(infos.keyJSON), infos.keyPass); err != nil { @@ -145,7 +157,13 @@ func (w *wizard) deployNode(boot bool) { infos.gasPrice = w.readDefaultFloat(infos.gasPrice) } // Try to deploy the full node on the host - if out, err := deployNode(client, w.network, w.conf.bootFull, w.conf.bootLight, infos); err != nil { + nocache := false + if existed { + fmt.Println() + fmt.Printf("Should the node be built from scratch (y/n)? (default = no)\n") + nocache = w.readDefaultString("n") != "n" + } + if out, err := deployNode(client, w.network, w.conf.bootFull, w.conf.bootLight, infos, nocache); err != nil { log.Error("Failed to deploy Ethereum node container", "err", err) if len(out) > 0 { fmt.Printf("%s\n", out) @@ -156,5 +174,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() } diff --git a/cmd/puppeth/wizard_wallet.go b/cmd/puppeth/wizard_wallet.go new file mode 100644 index 000000000..7c3896a17 --- /dev/null +++ b/cmd/puppeth/wizard_wallet.go @@ -0,0 +1,113 @@ +// Copyright 2017 The go-ethereum Authors +// This file is part of go-ethereum. +// +// go-ethereum is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// go-ethereum is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>. + +package main + +import ( + "encoding/json" + "fmt" + "time" + + "github.com/ethereum/go-ethereum/log" +) + +// deployWallet creates a new web wallet based on some user input. +func (w *wizard) deployWallet() { + // Do some sanity check before the user wastes time on input + if w.conf.Genesis == nil { + log.Error("No genesis block configured") + return + } + if w.conf.ethstats == "" { + log.Error("No ethstats server configured") + return + } + // Select the server to interact with + server := w.selectServer() + if server == "" { + return + } + client := w.servers[server] + + // Retrieve any active node configurations from the server + infos, err := checkWallet(client, w.network) + if err != nil { + infos = &walletInfos{ + nodePort: 30303, rpcPort: 8545, webPort: 80, webHost: client.server, + } + } + existed := err == nil + + infos.genesis, _ = json.MarshalIndent(w.conf.Genesis, "", " ") + infos.network = w.conf.Genesis.Config.ChainId.Int64() + + // Figure out which port to listen on + fmt.Println() + fmt.Printf("Which port should the wallet listen on? (default = %d)\n", infos.webPort) + infos.webPort = w.readDefaultInt(infos.webPort) + + // Figure which virtual-host to deploy ethstats on + if infos.webHost, err = w.ensureVirtualHost(client, infos.webPort, infos.webHost); err != nil { + log.Error("Failed to decide on wallet host", "err", err) + return + } + // Figure out where the user wants to store the persistent data + fmt.Println() + if infos.datadir == "" { + fmt.Printf("Where should data be stored on the remote machine?\n") + infos.datadir = w.readString() + } else { + fmt.Printf("Where should data be stored on the remote machine? (default = %s)\n", infos.datadir) + infos.datadir = w.readDefaultString(infos.datadir) + } + // Figure out which port to listen on + fmt.Println() + fmt.Printf("Which TCP/UDP port should the backing node listen on? (default = %d)\n", infos.nodePort) + infos.nodePort = w.readDefaultInt(infos.nodePort) + + fmt.Println() + fmt.Printf("Which port should the backing RPC API listen on? (default = %d)\n", infos.rpcPort) + infos.rpcPort = w.readDefaultInt(infos.rpcPort) + + // Set a proper name to report on the stats page + fmt.Println() + if infos.ethstats == "" { + fmt.Printf("What should the wallet be called on the stats page?\n") + infos.ethstats = w.readString() + ":" + w.conf.ethstats + } else { + fmt.Printf("What should the wallet be called on the stats page? (default = %s)\n", infos.ethstats) + infos.ethstats = w.readDefaultString(infos.ethstats) + ":" + w.conf.ethstats + } + // Try to deploy the wallet on the host + nocache := false + if existed { + fmt.Println() + fmt.Printf("Should the wallet be built from scratch (y/n)? (default = no)\n") + nocache = w.readDefaultString("n") != "n" + } + if out, err := deployWallet(client, w.network, w.conf.bootFull, infos, nocache); err != nil { + log.Error("Failed to deploy wallet container", "err", err) + if len(out) > 0 { + fmt.Printf("%s\n", out) + } + return + } + // All ok, run a network scan to pick any changes up + log.Info("Waiting for node to finish booting") + time.Sleep(3 * time.Second) + + w.networkStats() +} diff --git a/cmd/swarm/config.go b/cmd/swarm/config.go new file mode 100644 index 000000000..05a020dc5 --- /dev/null +++ b/cmd/swarm/config.go @@ -0,0 +1,342 @@ +// Copyright 2017 The go-ethereum Authors +// This file is part of go-ethereum. +// +// go-ethereum is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// go-ethereum is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>. + +package main + +import ( + "errors" + "fmt" + "io" + "os" + "reflect" + "strconv" + "strings" + "unicode" + + cli "gopkg.in/urfave/cli.v1" + + "github.com/ethereum/go-ethereum/cmd/utils" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/node" + "github.com/naoina/toml" + + bzzapi "github.com/ethereum/go-ethereum/swarm/api" +) + +var ( + //flag definition for the dumpconfig command + DumpConfigCommand = cli.Command{ + Action: utils.MigrateFlags(dumpConfig), + Name: "dumpconfig", + Usage: "Show configuration values", + ArgsUsage: "", + Flags: app.Flags, + Category: "MISCELLANEOUS COMMANDS", + Description: `The dumpconfig command shows configuration values.`, + } + + //flag definition for the config file command + SwarmTomlConfigPathFlag = cli.StringFlag{ + Name: "config", + Usage: "TOML configuration file", + } +) + +//constants for environment variables +const ( + SWARM_ENV_CHEQUEBOOK_ADDR = "SWARM_CHEQUEBOOK_ADDR" + SWARM_ENV_ACCOUNT = "SWARM_ACCOUNT" + SWARM_ENV_LISTEN_ADDR = "SWARM_LISTEN_ADDR" + SWARM_ENV_PORT = "SWARM_PORT" + SWARM_ENV_NETWORK_ID = "SWARM_NETWORK_ID" + SWARM_ENV_SWAP_ENABLE = "SWARM_SWAP_ENABLE" + SWARM_ENV_SWAP_API = "SWARM_SWAP_API" + SWARM_ENV_SYNC_ENABLE = "SWARM_SYNC_ENABLE" + SWARM_ENV_ENS_API = "SWARM_ENS_API" + SWARM_ENV_ENS_ADDR = "SWARM_ENS_ADDR" + SWARM_ENV_CORS = "SWARM_CORS" + SWARM_ENV_BOOTNODES = "SWARM_BOOTNODES" + GETH_ENV_DATADIR = "GETH_DATADIR" +) + +// These settings ensure that TOML keys use the same names as Go struct fields. +var tomlSettings = toml.Config{ + NormFieldName: func(rt reflect.Type, key string) string { + return key + }, + FieldToKey: func(rt reflect.Type, field string) string { + return field + }, + MissingField: func(rt reflect.Type, field string) error { + link := "" + if unicode.IsUpper(rune(rt.Name()[0])) && rt.PkgPath() != "main" { + link = fmt.Sprintf(", check github.com/ethereum/go-ethereum/swarm/api/config.go for available fields") + } + return fmt.Errorf("field '%s' is not defined in %s%s", field, rt.String(), link) + }, +} + +//before booting the swarm node, build the configuration +func buildConfig(ctx *cli.Context) (config *bzzapi.Config, err error) { + //check for deprecated flags + checkDeprecated(ctx) + //start by creating a default config + config = bzzapi.NewDefaultConfig() + //first load settings from config file (if provided) + config, err = configFileOverride(config, ctx) + //override settings provided by environment variables + config = envVarsOverride(config) + //override settings provided by command line + config = cmdLineOverride(config, ctx) + + return +} + +//finally, after the configuration build phase is finished, initialize +func initSwarmNode(config *bzzapi.Config, stack *node.Node, ctx *cli.Context) { + //at this point, all vars should be set in the Config + //get the account for the provided swarm account + prvkey := getAccount(config.BzzAccount, ctx, stack) + //set the resolved config path (geth --datadir) + config.Path = stack.InstanceDir() + //finally, initialize the configuration + config.Init(prvkey) + //configuration phase completed here + log.Debug("Starting Swarm with the following parameters:") + //after having created the config, print it to screen + log.Debug(printConfig(config)) +} + +//override the current config with whatever is in the config file, if a config file has been provided +func configFileOverride(config *bzzapi.Config, ctx *cli.Context) (*bzzapi.Config, error) { + var err error + + //only do something if the -config flag has been set + if ctx.GlobalIsSet(SwarmTomlConfigPathFlag.Name) { + var filepath string + if filepath = ctx.GlobalString(SwarmTomlConfigPathFlag.Name); filepath == "" { + utils.Fatalf("Config file flag provided with invalid file path") + } + f, err := os.Open(filepath) + if err != nil { + return nil, err + } + defer f.Close() + + //decode the TOML file into a Config struct + //note that we are decoding into the existing defaultConfig; + //if an entry is not present in the file, the default entry is kept + err = tomlSettings.NewDecoder(f).Decode(&config) + // Add file name to errors that have a line number. + if _, ok := err.(*toml.LineError); ok { + err = errors.New(filepath + ", " + err.Error()) + } + } + return config, err +} + +//override the current config with whatever is provided through the command line +//most values are not allowed a zero value (empty string), if not otherwise noted +func cmdLineOverride(currentConfig *bzzapi.Config, ctx *cli.Context) *bzzapi.Config { + + if keyid := ctx.GlobalString(SwarmAccountFlag.Name); keyid != "" { + currentConfig.BzzAccount = keyid + } + + if chbookaddr := ctx.GlobalString(ChequebookAddrFlag.Name); chbookaddr != "" { + currentConfig.Contract = common.HexToAddress(chbookaddr) + } + + if networkid := ctx.GlobalString(SwarmNetworkIdFlag.Name); networkid != "" { + if id, _ := strconv.Atoi(networkid); id != 0 { + currentConfig.NetworkId = uint64(id) + } + } + + if ctx.GlobalIsSet(utils.DataDirFlag.Name) { + if datadir := ctx.GlobalString(utils.DataDirFlag.Name); datadir != "" { + currentConfig.Path = datadir + } + } + + bzzport := ctx.GlobalString(SwarmPortFlag.Name) + if len(bzzport) > 0 { + currentConfig.Port = bzzport + } + + if bzzaddr := ctx.GlobalString(SwarmListenAddrFlag.Name); bzzaddr != "" { + currentConfig.ListenAddr = bzzaddr + } + + if ctx.GlobalIsSet(SwarmSwapEnabledFlag.Name) { + currentConfig.SwapEnabled = true + } + + if ctx.GlobalIsSet(SwarmSyncEnabledFlag.Name) { + currentConfig.SyncEnabled = true + } + + currentConfig.SwapApi = ctx.GlobalString(SwarmSwapAPIFlag.Name) + if currentConfig.SwapEnabled && currentConfig.SwapApi == "" { + utils.Fatalf(SWARM_ERR_SWAP_SET_NO_API) + } + + //EnsAPIs can be set to "", so can't check for empty string, as it is allowed! + if ctx.GlobalIsSet(EnsAPIFlag.Name) { + ensAPIs := ctx.GlobalStringSlice(EnsAPIFlag.Name) + // Disable ENS resolver if --ens-api="" is specified + if len(ensAPIs) == 1 && ensAPIs[0] == "" { + currentConfig.EnsDisabled = true + currentConfig.EnsAPIs = nil + } else { + currentConfig.EnsDisabled = false + currentConfig.EnsAPIs = ensAPIs + } + } + + if ensaddr := ctx.GlobalString(DeprecatedEnsAddrFlag.Name); ensaddr != "" { + currentConfig.EnsRoot = common.HexToAddress(ensaddr) + } + + if cors := ctx.GlobalString(CorsStringFlag.Name); cors != "" { + currentConfig.Cors = cors + } + + if ctx.GlobalIsSet(utils.BootnodesFlag.Name) { + currentConfig.BootNodes = ctx.GlobalString(utils.BootnodesFlag.Name) + } + + return currentConfig + +} + +//override the current config with whatver is provided in environment variables +//most values are not allowed a zero value (empty string), if not otherwise noted +func envVarsOverride(currentConfig *bzzapi.Config) (config *bzzapi.Config) { + + if keyid := os.Getenv(SWARM_ENV_ACCOUNT); keyid != "" { + currentConfig.BzzAccount = keyid + } + + if chbookaddr := os.Getenv(SWARM_ENV_CHEQUEBOOK_ADDR); chbookaddr != "" { + currentConfig.Contract = common.HexToAddress(chbookaddr) + } + + if networkid := os.Getenv(SWARM_ENV_NETWORK_ID); networkid != "" { + if id, _ := strconv.Atoi(networkid); id != 0 { + currentConfig.NetworkId = uint64(id) + } + } + + if datadir := os.Getenv(GETH_ENV_DATADIR); datadir != "" { + currentConfig.Path = datadir + } + + bzzport := os.Getenv(SWARM_ENV_PORT) + if len(bzzport) > 0 { + currentConfig.Port = bzzport + } + + if bzzaddr := os.Getenv(SWARM_ENV_LISTEN_ADDR); bzzaddr != "" { + currentConfig.ListenAddr = bzzaddr + } + + if swapenable := os.Getenv(SWARM_ENV_SWAP_ENABLE); swapenable != "" { + if swap, err := strconv.ParseBool(swapenable); err != nil { + currentConfig.SwapEnabled = swap + } + } + + if syncenable := os.Getenv(SWARM_ENV_SYNC_ENABLE); syncenable != "" { + if sync, err := strconv.ParseBool(syncenable); err != nil { + currentConfig.SyncEnabled = sync + } + } + + if swapapi := os.Getenv(SWARM_ENV_SWAP_API); swapapi != "" { + currentConfig.SwapApi = swapapi + } + + if currentConfig.SwapEnabled && currentConfig.SwapApi == "" { + utils.Fatalf(SWARM_ERR_SWAP_SET_NO_API) + } + + //EnsAPIs can be set to "", so can't check for empty string, as it is allowed + if ensapi, exists := os.LookupEnv(SWARM_ENV_ENS_API); exists == true { + ensAPIs := strings.Split(ensapi, ",") + // Disable ENS resolver if SWARM_ENS_API="" is specified + if len(ensAPIs) == 0 { + currentConfig.EnsDisabled = true + currentConfig.EnsAPIs = nil + } else { + currentConfig.EnsDisabled = false + currentConfig.EnsAPIs = ensAPIs + } + } + + if ensaddr := os.Getenv(SWARM_ENV_ENS_ADDR); ensaddr != "" { + currentConfig.EnsRoot = common.HexToAddress(ensaddr) + } + + if cors := os.Getenv(SWARM_ENV_CORS); cors != "" { + currentConfig.Cors = cors + } + + if bootnodes := os.Getenv(SWARM_ENV_BOOTNODES); bootnodes != "" { + currentConfig.BootNodes = bootnodes + } + + return currentConfig +} + +// dumpConfig is the dumpconfig command. +// writes a default config to STDOUT +func dumpConfig(ctx *cli.Context) error { + cfg, err := buildConfig(ctx) + if err != nil { + utils.Fatalf(fmt.Sprintf("Uh oh - dumpconfig triggered an error %v", err)) + } + comment := "" + out, err := tomlSettings.Marshal(&cfg) + if err != nil { + return err + } + io.WriteString(os.Stdout, comment) + os.Stdout.Write(out) + return nil +} + +//deprecated flags checked here +func checkDeprecated(ctx *cli.Context) { + // exit if the deprecated --ethapi flag is set + if ctx.GlobalString(DeprecatedEthAPIFlag.Name) != "" { + utils.Fatalf("--ethapi is no longer a valid command line flag, please use --ens-api and/or --swap-api.") + } + // warn if --ens-api flag is set + if ctx.GlobalString(DeprecatedEnsAddrFlag.Name) != "" { + log.Warn("--ens-addr is no longer a valid command line flag, please use --ens-api to specify contract address.") + } +} + +//print a Config as string +func printConfig(config *bzzapi.Config) string { + out, err := tomlSettings.Marshal(&config) + if err != nil { + return (fmt.Sprintf("Something is not right with the configuration: %v", err)) + } + return string(out) +} diff --git a/cmd/swarm/config_test.go b/cmd/swarm/config_test.go new file mode 100644 index 000000000..7ffe2cfb9 --- /dev/null +++ b/cmd/swarm/config_test.go @@ -0,0 +1,459 @@ +// Copyright 2017 The go-ethereum Authors +// This file is part of go-ethereum. +// +// go-ethereum is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// go-ethereum is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>. + +package main + +import ( + "fmt" + "io" + "io/ioutil" + "os" + "os/exec" + "testing" + "time" + + "github.com/ethereum/go-ethereum/rpc" + "github.com/ethereum/go-ethereum/swarm" + "github.com/ethereum/go-ethereum/swarm/api" + + "github.com/docker/docker/pkg/reexec" +) + +func TestDumpConfig(t *testing.T) { + swarm := runSwarm(t, "dumpconfig") + defaultConf := api.NewDefaultConfig() + out, err := tomlSettings.Marshal(&defaultConf) + if err != nil { + t.Fatal(err) + } + swarm.Expect(string(out)) + swarm.ExpectExit() +} + +func TestFailsSwapEnabledNoSwapApi(t *testing.T) { + flags := []string{ + fmt.Sprintf("--%s", SwarmNetworkIdFlag.Name), "42", + fmt.Sprintf("--%s", SwarmPortFlag.Name), "54545", + fmt.Sprintf("--%s", SwarmSwapEnabledFlag.Name), + } + + swarm := runSwarm(t, flags...) + swarm.Expect("Fatal: " + SWARM_ERR_SWAP_SET_NO_API + "\n") + swarm.ExpectExit() +} + +func TestFailsNoBzzAccount(t *testing.T) { + flags := []string{ + fmt.Sprintf("--%s", SwarmNetworkIdFlag.Name), "42", + fmt.Sprintf("--%s", SwarmPortFlag.Name), "54545", + } + + swarm := runSwarm(t, flags...) + swarm.Expect("Fatal: " + SWARM_ERR_NO_BZZACCOUNT + "\n") + swarm.ExpectExit() +} + +func TestCmdLineOverrides(t *testing.T) { + dir, err := ioutil.TempDir("", "bzztest") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) + + conf, account := getTestAccount(t, dir) + node := &testNode{Dir: dir} + + // assign ports + httpPort, err := assignTCPPort() + if err != nil { + t.Fatal(err) + } + + flags := []string{ + fmt.Sprintf("--%s", SwarmNetworkIdFlag.Name), "42", + fmt.Sprintf("--%s", SwarmPortFlag.Name), httpPort, + fmt.Sprintf("--%s", SwarmSyncEnabledFlag.Name), + fmt.Sprintf("--%s", CorsStringFlag.Name), "*", + fmt.Sprintf("--%s", SwarmAccountFlag.Name), account.Address.String(), + fmt.Sprintf("--%s", EnsAPIFlag.Name), "", + "--datadir", dir, + "--ipcpath", conf.IPCPath, + } + node.Cmd = runSwarm(t, flags...) + node.Cmd.InputLine(testPassphrase) + defer func() { + if t.Failed() { + node.Shutdown() + } + }() + // wait for the node to start + for start := time.Now(); time.Since(start) < 10*time.Second; time.Sleep(50 * time.Millisecond) { + node.Client, err = rpc.Dial(conf.IPCEndpoint()) + if err == nil { + break + } + } + if node.Client == nil { + t.Fatal(err) + } + + // load info + var info swarm.Info + if err := node.Client.Call(&info, "bzz_info"); err != nil { + t.Fatal(err) + } + + if info.Port != httpPort { + t.Fatalf("Expected port to be %s, got %s", httpPort, info.Port) + } + + if info.NetworkId != 42 { + t.Fatalf("Expected network ID to be %d, got %d", 42, info.NetworkId) + } + + if info.SyncEnabled != true { + t.Fatal("Expected Sync to be enabled, but is false") + } + + if info.Cors != "*" { + t.Fatalf("Expected Cors flag to be set to %s, got %s", "*", info.Cors) + } + + node.Shutdown() +} + +func TestFileOverrides(t *testing.T) { + + // assign ports + httpPort, err := assignTCPPort() + if err != nil { + t.Fatal(err) + } + + //create a config file + //first, create a default conf + defaultConf := api.NewDefaultConfig() + //change some values in order to test if they have been loaded + defaultConf.SyncEnabled = true + defaultConf.NetworkId = 54 + defaultConf.Port = httpPort + defaultConf.StoreParams.DbCapacity = 9000000 + defaultConf.ChunkerParams.Branches = 64 + defaultConf.HiveParams.CallInterval = 6000000000 + defaultConf.Swap.Params.Strategy.AutoCashInterval = 600 * time.Second + defaultConf.SyncParams.KeyBufferSize = 512 + //create a TOML string + out, err := tomlSettings.Marshal(&defaultConf) + if err != nil { + t.Fatalf("Error creating TOML file in TestFileOverride: %v", err) + } + //create file + f, err := ioutil.TempFile("", "testconfig.toml") + if err != nil { + t.Fatalf("Error writing TOML file in TestFileOverride: %v", err) + } + //write file + _, err = f.WriteString(string(out)) + if err != nil { + t.Fatalf("Error writing TOML file in TestFileOverride: %v", err) + } + f.Sync() + + dir, err := ioutil.TempDir("", "bzztest") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) + conf, account := getTestAccount(t, dir) + node := &testNode{Dir: dir} + + flags := []string{ + fmt.Sprintf("--%s", SwarmTomlConfigPathFlag.Name), f.Name(), + fmt.Sprintf("--%s", SwarmAccountFlag.Name), account.Address.String(), + "--ens-api", "", + "--ipcpath", conf.IPCPath, + "--datadir", dir, + } + node.Cmd = runSwarm(t, flags...) + node.Cmd.InputLine(testPassphrase) + defer func() { + if t.Failed() { + node.Shutdown() + } + }() + // wait for the node to start + for start := time.Now(); time.Since(start) < 10*time.Second; time.Sleep(50 * time.Millisecond) { + node.Client, err = rpc.Dial(conf.IPCEndpoint()) + if err == nil { + break + } + } + if node.Client == nil { + t.Fatal(err) + } + + // load info + var info swarm.Info + if err := node.Client.Call(&info, "bzz_info"); err != nil { + t.Fatal(err) + } + + if info.Port != httpPort { + t.Fatalf("Expected port to be %s, got %s", httpPort, info.Port) + } + + if info.NetworkId != 54 { + t.Fatalf("Expected network ID to be %d, got %d", 54, info.NetworkId) + } + + if info.SyncEnabled != true { + t.Fatal("Expected Sync to be enabled, but is false") + } + + if info.StoreParams.DbCapacity != 9000000 { + t.Fatalf("Expected network ID to be %d, got %d", 54, info.NetworkId) + } + + if info.ChunkerParams.Branches != 64 { + t.Fatalf("Expected chunker params branches to be %d, got %d", 64, info.ChunkerParams.Branches) + } + + if info.HiveParams.CallInterval != 6000000000 { + t.Fatalf("Expected HiveParams CallInterval to be %d, got %d", uint64(6000000000), uint64(info.HiveParams.CallInterval)) + } + + if info.Swap.Params.Strategy.AutoCashInterval != 600*time.Second { + t.Fatalf("Expected SwapParams AutoCashInterval to be %ds, got %d", 600, info.Swap.Params.Strategy.AutoCashInterval) + } + + if info.SyncParams.KeyBufferSize != 512 { + t.Fatalf("Expected info.SyncParams.KeyBufferSize to be %d, got %d", 512, info.SyncParams.KeyBufferSize) + } + + node.Shutdown() +} + +func TestEnvVars(t *testing.T) { + // assign ports + httpPort, err := assignTCPPort() + if err != nil { + t.Fatal(err) + } + + envVars := os.Environ() + envVars = append(envVars, fmt.Sprintf("%s=%s", SwarmPortFlag.EnvVar, httpPort)) + envVars = append(envVars, fmt.Sprintf("%s=%s", SwarmNetworkIdFlag.EnvVar, "999")) + envVars = append(envVars, fmt.Sprintf("%s=%s", CorsStringFlag.EnvVar, "*")) + envVars = append(envVars, fmt.Sprintf("%s=%s", SwarmSyncEnabledFlag.EnvVar, "true")) + + dir, err := ioutil.TempDir("", "bzztest") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) + conf, account := getTestAccount(t, dir) + node := &testNode{Dir: dir} + flags := []string{ + fmt.Sprintf("--%s", SwarmAccountFlag.Name), account.Address.String(), + "--ens-api", "", + "--datadir", dir, + "--ipcpath", conf.IPCPath, + } + + //node.Cmd = runSwarm(t,flags...) + //node.Cmd.cmd.Env = envVars + //the above assignment does not work, so we need a custom Cmd here in order to pass envVars: + cmd := &exec.Cmd{ + Path: reexec.Self(), + Args: append([]string{"swarm-test"}, flags...), + Stderr: os.Stderr, + Stdout: os.Stdout, + } + cmd.Env = envVars + //stdout, err := cmd.StdoutPipe() + //if err != nil { + // t.Fatal(err) + //} + //stdout = bufio.NewReader(stdout) + var stdin io.WriteCloser + if stdin, err = cmd.StdinPipe(); err != nil { + t.Fatal(err) + } + if err := cmd.Start(); err != nil { + t.Fatal(err) + } + + //cmd.InputLine(testPassphrase) + io.WriteString(stdin, testPassphrase+"\n") + defer func() { + if t.Failed() { + node.Shutdown() + cmd.Process.Kill() + } + }() + // wait for the node to start + for start := time.Now(); time.Since(start) < 10*time.Second; time.Sleep(50 * time.Millisecond) { + node.Client, err = rpc.Dial(conf.IPCEndpoint()) + if err == nil { + break + } + } + + if node.Client == nil { + t.Fatal(err) + } + + // load info + var info swarm.Info + if err := node.Client.Call(&info, "bzz_info"); err != nil { + t.Fatal(err) + } + + if info.Port != httpPort { + t.Fatalf("Expected port to be %s, got %s", httpPort, info.Port) + } + + if info.NetworkId != 999 { + t.Fatalf("Expected network ID to be %d, got %d", 999, info.NetworkId) + } + + if info.Cors != "*" { + t.Fatalf("Expected Cors flag to be set to %s, got %s", "*", info.Cors) + } + + if info.SyncEnabled != true { + t.Fatal("Expected Sync to be enabled, but is false") + } + + node.Shutdown() + cmd.Process.Kill() +} + +func TestCmdLineOverridesFile(t *testing.T) { + + // assign ports + httpPort, err := assignTCPPort() + if err != nil { + t.Fatal(err) + } + + //create a config file + //first, create a default conf + defaultConf := api.NewDefaultConfig() + //change some values in order to test if they have been loaded + defaultConf.SyncEnabled = false + defaultConf.NetworkId = 54 + defaultConf.Port = "8588" + defaultConf.StoreParams.DbCapacity = 9000000 + defaultConf.ChunkerParams.Branches = 64 + defaultConf.HiveParams.CallInterval = 6000000000 + defaultConf.Swap.Params.Strategy.AutoCashInterval = 600 * time.Second + defaultConf.SyncParams.KeyBufferSize = 512 + //create a TOML file + out, err := tomlSettings.Marshal(&defaultConf) + if err != nil { + t.Fatalf("Error creating TOML file in TestFileOverride: %v", err) + } + //write file + f, err := ioutil.TempFile("", "testconfig.toml") + if err != nil { + t.Fatalf("Error writing TOML file in TestFileOverride: %v", err) + } + //write file + _, err = f.WriteString(string(out)) + if err != nil { + t.Fatalf("Error writing TOML file in TestFileOverride: %v", err) + } + f.Sync() + + dir, err := ioutil.TempDir("", "bzztest") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) + conf, account := getTestAccount(t, dir) + node := &testNode{Dir: dir} + + expectNetworkId := uint64(77) + + flags := []string{ + fmt.Sprintf("--%s", SwarmNetworkIdFlag.Name), "77", + fmt.Sprintf("--%s", SwarmPortFlag.Name), httpPort, + fmt.Sprintf("--%s", SwarmSyncEnabledFlag.Name), + fmt.Sprintf("--%s", SwarmTomlConfigPathFlag.Name), f.Name(), + fmt.Sprintf("--%s", SwarmAccountFlag.Name), account.Address.String(), + "--ens-api", "", + "--datadir", dir, + "--ipcpath", conf.IPCPath, + } + node.Cmd = runSwarm(t, flags...) + node.Cmd.InputLine(testPassphrase) + defer func() { + if t.Failed() { + node.Shutdown() + } + }() + // wait for the node to start + for start := time.Now(); time.Since(start) < 10*time.Second; time.Sleep(50 * time.Millisecond) { + node.Client, err = rpc.Dial(conf.IPCEndpoint()) + if err == nil { + break + } + } + if node.Client == nil { + t.Fatal(err) + } + + // load info + var info swarm.Info + if err := node.Client.Call(&info, "bzz_info"); err != nil { + t.Fatal(err) + } + + if info.Port != httpPort { + t.Fatalf("Expected port to be %s, got %s", httpPort, info.Port) + } + + if info.NetworkId != expectNetworkId { + t.Fatalf("Expected network ID to be %d, got %d", expectNetworkId, info.NetworkId) + } + + if info.SyncEnabled != true { + t.Fatal("Expected Sync to be enabled, but is false") + } + + if info.StoreParams.DbCapacity != 9000000 { + t.Fatalf("Expected network ID to be %d, got %d", 54, info.NetworkId) + } + + if info.ChunkerParams.Branches != 64 { + t.Fatalf("Expected chunker params branches to be %d, got %d", 64, info.ChunkerParams.Branches) + } + + if info.HiveParams.CallInterval != 6000000000 { + t.Fatalf("Expected HiveParams CallInterval to be %d, got %d", uint64(6000000000), uint64(info.HiveParams.CallInterval)) + } + + if info.Swap.Params.Strategy.AutoCashInterval != 600*time.Second { + t.Fatalf("Expected SwapParams AutoCashInterval to be %ds, got %d", 600, info.Swap.Params.Strategy.AutoCashInterval) + } + + if info.SyncParams.KeyBufferSize != 512 { + t.Fatalf("Expected info.SyncParams.KeyBufferSize to be %d, got %d", 512, info.SyncParams.KeyBufferSize) + } + + node.Shutdown() +} diff --git a/cmd/swarm/main.go b/cmd/swarm/main.go index 48cf032a2..4b0823796 100644 --- a/cmd/swarm/main.go +++ b/cmd/swarm/main.go @@ -27,7 +27,6 @@ import ( "strconv" "strings" "syscall" - "unicode" "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/accounts/keystore" @@ -44,6 +43,7 @@ import ( "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/swarm" bzzapi "github.com/ethereum/go-ethereum/swarm/api" + "gopkg.in/urfave/cli.v1" ) @@ -62,44 +62,53 @@ var ( var ( ChequebookAddrFlag = cli.StringFlag{ - Name: "chequebook", - Usage: "chequebook contract address", + Name: "chequebook", + Usage: "chequebook contract address", + EnvVar: SWARM_ENV_CHEQUEBOOK_ADDR, } SwarmAccountFlag = cli.StringFlag{ - Name: "bzzaccount", - Usage: "Swarm account key file", + Name: "bzzaccount", + Usage: "Swarm account key file", + EnvVar: SWARM_ENV_ACCOUNT, } SwarmListenAddrFlag = cli.StringFlag{ - Name: "httpaddr", - Usage: "Swarm HTTP API listening interface", + Name: "httpaddr", + Usage: "Swarm HTTP API listening interface", + EnvVar: SWARM_ENV_LISTEN_ADDR, } SwarmPortFlag = cli.StringFlag{ - Name: "bzzport", - Usage: "Swarm local http api port", + Name: "bzzport", + Usage: "Swarm local http api port", + EnvVar: SWARM_ENV_PORT, } SwarmNetworkIdFlag = cli.IntFlag{ - Name: "bzznetworkid", - Usage: "Network identifier (integer, default 3=swarm testnet)", + Name: "bzznetworkid", + Usage: "Network identifier (integer, default 3=swarm testnet)", + EnvVar: SWARM_ENV_NETWORK_ID, } SwarmConfigPathFlag = cli.StringFlag{ Name: "bzzconfig", - Usage: "Swarm config file path (datadir/bzz)", + Usage: "DEPRECATED: please use --config path/to/TOML-file", } SwarmSwapEnabledFlag = cli.BoolFlag{ - Name: "swap", - Usage: "Swarm SWAP enabled (default false)", + Name: "swap", + Usage: "Swarm SWAP enabled (default false)", + EnvVar: SWARM_ENV_SWAP_ENABLE, } SwarmSwapAPIFlag = cli.StringFlag{ - Name: "swap-api", - Usage: "URL of the Ethereum API provider to use to settle SWAP payments", + Name: "swap-api", + Usage: "URL of the Ethereum API provider to use to settle SWAP payments", + EnvVar: SWARM_ENV_SWAP_API, } SwarmSyncEnabledFlag = cli.BoolTFlag{ - Name: "sync", - Usage: "Swarm Syncing enabled (default true)", + Name: "sync", + Usage: "Swarm Syncing enabled (default true)", + EnvVar: SWARM_ENV_SYNC_ENABLE, } EnsAPIFlag = cli.StringSliceFlag{ - Name: "ens-api", - Usage: "ENS API endpoint for a TLD and with contract address, can be repeated, format [tld:][contract-addr@]url", + Name: "ens-api", + Usage: "ENS API endpoint for a TLD and with contract address, can be repeated, format [tld:][contract-addr@]url", + EnvVar: SWARM_ENV_ENS_API, } SwarmApiFlag = cli.StringFlag{ Name: "bzzapi", @@ -127,8 +136,9 @@ var ( Usage: "force mime type", } CorsStringFlag = cli.StringFlag{ - Name: "corsdomain", - Usage: "Domain on which to send Access-Control-Allow-Origin header (multiple domains can be supplied separated by a ',')", + Name: "corsdomain", + Usage: "Domain on which to send Access-Control-Allow-Origin header (multiple domains can be supplied separated by a ',')", + EnvVar: SWARM_ENV_CORS, } // the following flags are deprecated and should be removed in the future @@ -142,6 +152,12 @@ var ( } ) +//declare a few constant error messages, useful for later error check comparisons in test +var ( + SWARM_ERR_NO_BZZACCOUNT = "bzzaccount option is required but not set; check your config file, command line or environment variables" + SWARM_ERR_SWAP_SET_NO_API = "SWAP is enabled but --swap-api is not set" +) + var defaultNodeConfig = node.DefaultConfig // This init function sets defaults so cmd/swarm can run alongside geth. @@ -297,6 +313,8 @@ Remove corrupt entries from a local chunk database. DEPRECATED: use 'swarm db clean'. `, }, + // See config.go + DumpConfigCommand, } sort.Sort(cli.CommandsByName(app.Commands)) @@ -319,6 +337,7 @@ DEPRECATED: use 'swarm db clean'. // bzzd-specific flags CorsStringFlag, EnsAPIFlag, + SwarmTomlConfigPathFlag, SwarmConfigPathFlag, SwarmSwapEnabledFlag, SwarmSwapAPIFlag, @@ -372,19 +391,32 @@ func version(ctx *cli.Context) error { } func bzzd(ctx *cli.Context) error { - // exit if the deprecated --ethapi flag is set - if ctx.GlobalString(DeprecatedEthAPIFlag.Name) != "" { - utils.Fatalf("--ethapi is no longer a valid command line flag, please use --ens-api and/or --swap-api.") + //build a valid bzzapi.Config from all available sources: + //default config, file config, command line and env vars + bzzconfig, err := buildConfig(ctx) + if err != nil { + utils.Fatalf("unable to configure swarm: %v", err) } cfg := defaultNodeConfig + //geth only supports --datadir via command line + //in order to be consistent within swarm, if we pass --datadir via environment variable + //or via config file, we get the same directory for geth and swarm + if _, err := os.Stat(bzzconfig.Path); err == nil { + cfg.DataDir = bzzconfig.Path + } + //setup the ethereum node utils.SetNodeConfig(ctx, &cfg) stack, err := node.New(&cfg) if err != nil { utils.Fatalf("can't create node: %v", err) } - - registerBzzService(ctx, stack) + //a few steps need to be done after the config phase is completed, + //due to overriding behavior + initSwarmNode(bzzconfig, stack, ctx) + //register BZZ as node.Service in the ethereum node + registerBzzService(bzzconfig, ctx, stack) + //start the node utils.StartNode(stack) go func() { @@ -396,13 +428,12 @@ func bzzd(ctx *cli.Context) error { stack.Stop() }() - networkId := ctx.GlobalUint64(SwarmNetworkIdFlag.Name) // Add bootnodes as initial peers. - if ctx.GlobalIsSet(utils.BootnodesFlag.Name) { - bootnodes := strings.Split(ctx.GlobalString(utils.BootnodesFlag.Name), ",") + if bzzconfig.BootNodes != "" { + bootnodes := strings.Split(bzzconfig.BootNodes, ",") injectBootnodes(stack.Server(), bootnodes) } else { - if networkId == 3 { + if bzzconfig.NetworkId == 3 { injectBootnodes(stack.Server(), testbetBootNodes) } } @@ -411,139 +442,35 @@ func bzzd(ctx *cli.Context) error { return nil } -func registerBzzService(ctx *cli.Context, stack *node.Node) { - prvkey := getAccount(ctx, stack) - - chbookaddr := common.HexToAddress(ctx.GlobalString(ChequebookAddrFlag.Name)) - bzzdir := ctx.GlobalString(SwarmConfigPathFlag.Name) - if bzzdir == "" { - bzzdir = stack.InstanceDir() - } - - bzzconfig, err := bzzapi.NewConfig(bzzdir, chbookaddr, prvkey, ctx.GlobalUint64(SwarmNetworkIdFlag.Name)) - if err != nil { - utils.Fatalf("unable to configure swarm: %v", err) - } - bzzport := ctx.GlobalString(SwarmPortFlag.Name) - if len(bzzport) > 0 { - bzzconfig.Port = bzzport - } - if bzzaddr := ctx.GlobalString(SwarmListenAddrFlag.Name); bzzaddr != "" { - bzzconfig.ListenAddr = bzzaddr - } - swapEnabled := ctx.GlobalBool(SwarmSwapEnabledFlag.Name) - syncEnabled := ctx.GlobalBoolT(SwarmSyncEnabledFlag.Name) - - swapapi := ctx.GlobalString(SwarmSwapAPIFlag.Name) - if swapEnabled && swapapi == "" { - utils.Fatalf("SWAP is enabled but --swap-api is not set") - } - - ensAPIs := ctx.GlobalStringSlice(EnsAPIFlag.Name) - ensAddr := ctx.GlobalString(DeprecatedEnsAddrFlag.Name) - - if ensAddr != "" { - log.Warn("--ens-addr is no longer a valid command line flag, please use --ens-api to specify contract address.") - } - - cors := ctx.GlobalString(CorsStringFlag.Name) +func registerBzzService(bzzconfig *bzzapi.Config, ctx *cli.Context, stack *node.Node) { + //define the swarm service boot function boot := func(ctx *node.ServiceContext) (node.Service, error) { var swapClient *ethclient.Client - if swapapi != "" { - log.Info("connecting to SWAP API", "url", swapapi) - swapClient, err = ethclient.Dial(swapapi) + var err error + if bzzconfig.SwapApi != "" { + log.Info("connecting to SWAP API", "url", bzzconfig.SwapApi) + swapClient, err = ethclient.Dial(bzzconfig.SwapApi) if err != nil { - return nil, fmt.Errorf("error connecting to SWAP API %s: %s", swapapi, err) - } - } - - ensClientConfigs := []swarm.ENSClientConfig{} - switch len(ensAPIs) { - case 0: - ensClientConfigs = append(ensClientConfigs, swarm.ENSClientConfig{ - Endpoint: node.DefaultIPCEndpoint("geth"), - ContractAddress: ensAddr, - }) - case 1: - // Check if "--ens-api ''" is specified in order to disable ENS. - if ensAPIs[0] == "" { - break - } - // Check if only one --ens-api is specified in order to use --ens-addr value - // to preserve the backward compatibility with single --ens-api flag. - c := parseFlagEnsAPI(ensAPIs[0]) - if ensAddr != "" { - // If contract address is specified in both cases, check for conflict. - if c.ContractAddress != "" && ensAddr != c.ContractAddress { - utils.Fatalf("--ens-addr flag in conflict with --ens-api flag contract address") - } - c.ContractAddress = ensAddr - } - ensClientConfigs = append(ensClientConfigs, c) - default: - // Backward compatibility with single --ens-api flag and --ens-addr is preserved. - // Check for case where multiple --ens-api flags are set with --ens-addr where - // the specified contract address is not clear to which api belongs. - if ensAddr != "" { - utils.Fatalf("--ens-addr flag can not be used with multiple --ens-api flags") + return nil, fmt.Errorf("error connecting to SWAP API %s: %s", bzzconfig.SwapApi, err) } - for _, s := range ensAPIs { - if s != "" { - ensClientConfigs = append(ensClientConfigs, parseFlagEnsAPI(s)) - } - } - } - if len(ensClientConfigs) == 0 { - log.Warn("No ENS, please specify non-empty --ens-api to use domain name resolution") } - return swarm.NewSwarm(ctx, swapClient, ensClientConfigs, bzzconfig, swapEnabled, syncEnabled, cors) + return swarm.NewSwarm(ctx, swapClient, bzzconfig) } + //register within the ethereum node if err := stack.Register(boot); err != nil { utils.Fatalf("Failed to register the Swarm service: %v", err) } } -// parseFlagEnsAPI parses EnsAPIFlag according to format -// [tld:][contract-addr@]url and returns ENSClientConfig structure -// with endpoint, contract address and TLD. -func parseFlagEnsAPI(s string) swarm.ENSClientConfig { - isAllLetterString := func(s string) bool { - for _, r := range s { - if !unicode.IsLetter(r) { - return false - } - } - return true - } - endpoint := s - var addr, tld string - if i := strings.Index(endpoint, ":"); i > 0 { - if isAllLetterString(endpoint[:i]) && len(endpoint) > i+2 && endpoint[i+1:i+3] != "//" { - tld = endpoint[:i] - endpoint = endpoint[i+1:] - } - } - if i := strings.Index(endpoint, "@"); i > 0 { - addr = endpoint[:i] - endpoint = endpoint[i+1:] - } - return swarm.ENSClientConfig{ - Endpoint: endpoint, - ContractAddress: addr, - TLD: tld, - } -} - -func getAccount(ctx *cli.Context, stack *node.Node) *ecdsa.PrivateKey { - keyid := ctx.GlobalString(SwarmAccountFlag.Name) - - if keyid == "" { - utils.Fatalf("Option %q is required", SwarmAccountFlag.Name) +func getAccount(bzzaccount string, ctx *cli.Context, stack *node.Node) *ecdsa.PrivateKey { + //an account is mandatory + if bzzaccount == "" { + utils.Fatalf(SWARM_ERR_NO_BZZACCOUNT) } // Try to load the arg as a hex key file. - if key, err := crypto.LoadECDSA(keyid); err == nil { + if key, err := crypto.LoadECDSA(bzzaccount); err == nil { log.Info("Swarm account key loaded", "address", crypto.PubkeyToAddress(key.PublicKey)) return key } @@ -551,7 +478,7 @@ func getAccount(ctx *cli.Context, stack *node.Node) *ecdsa.PrivateKey { am := stack.AccountManager() ks := am.Backends(keystore.KeyStoreType)[0].(*keystore.KeyStore) - return decryptStoreAccount(ks, keyid, utils.MakePasswordList(ctx)) + return decryptStoreAccount(ks, bzzaccount, utils.MakePasswordList(ctx)) } func decryptStoreAccount(ks *keystore.KeyStore, account string, passwords []string) *ecdsa.PrivateKey { @@ -569,7 +496,7 @@ func decryptStoreAccount(ks *keystore.KeyStore, account string, passwords []stri utils.Fatalf("Can't find swarm account key %s", account) } if err != nil { - utils.Fatalf("Can't find swarm account key: %v", err) + utils.Fatalf("Can't find swarm account key: %v - Is the provided bzzaccount(%s) from the right datadir/Path?", err, account) } keyjson, err := ioutil.ReadFile(a.URL.Path) if err != nil { diff --git a/cmd/swarm/main_test.go b/cmd/swarm/main_test.go deleted file mode 100644 index f815f3387..000000000 --- a/cmd/swarm/main_test.go +++ /dev/null @@ -1,141 +0,0 @@ -// Copyright 2017 The go-ethereum Authors -// This file is part of go-ethereum. -// -// go-ethereum is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// go-ethereum is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>. - -package main - -import ( - "testing" - - "github.com/ethereum/go-ethereum/swarm" -) - -func TestParseFlagEnsAPI(t *testing.T) { - for _, x := range []struct { - description string - value string - config swarm.ENSClientConfig - }{ - { - description: "IPC endpoint", - value: "/data/testnet/geth.ipc", - config: swarm.ENSClientConfig{ - Endpoint: "/data/testnet/geth.ipc", - }, - }, - { - description: "HTTP endpoint", - value: "http://127.0.0.1:1234", - config: swarm.ENSClientConfig{ - Endpoint: "http://127.0.0.1:1234", - }, - }, - { - description: "WS endpoint", - value: "ws://127.0.0.1:1234", - config: swarm.ENSClientConfig{ - Endpoint: "ws://127.0.0.1:1234", - }, - }, - { - description: "IPC Endpoint and TLD", - value: "test:/data/testnet/geth.ipc", - config: swarm.ENSClientConfig{ - Endpoint: "/data/testnet/geth.ipc", - TLD: "test", - }, - }, - { - description: "HTTP endpoint and TLD", - value: "test:http://127.0.0.1:1234", - config: swarm.ENSClientConfig{ - Endpoint: "http://127.0.0.1:1234", - TLD: "test", - }, - }, - { - description: "WS endpoint and TLD", - value: "test:ws://127.0.0.1:1234", - config: swarm.ENSClientConfig{ - Endpoint: "ws://127.0.0.1:1234", - TLD: "test", - }, - }, - { - description: "IPC Endpoint and contract address", - value: "314159265dD8dbb310642f98f50C066173C1259b@/data/testnet/geth.ipc", - config: swarm.ENSClientConfig{ - Endpoint: "/data/testnet/geth.ipc", - ContractAddress: "314159265dD8dbb310642f98f50C066173C1259b", - }, - }, - { - description: "HTTP endpoint and contract address", - value: "314159265dD8dbb310642f98f50C066173C1259b@http://127.0.0.1:1234", - config: swarm.ENSClientConfig{ - Endpoint: "http://127.0.0.1:1234", - ContractAddress: "314159265dD8dbb310642f98f50C066173C1259b", - }, - }, - { - description: "WS endpoint and contract address", - value: "314159265dD8dbb310642f98f50C066173C1259b@ws://127.0.0.1:1234", - config: swarm.ENSClientConfig{ - Endpoint: "ws://127.0.0.1:1234", - ContractAddress: "314159265dD8dbb310642f98f50C066173C1259b", - }, - }, - { - description: "IPC Endpoint, TLD and contract address", - value: "test:314159265dD8dbb310642f98f50C066173C1259b@/data/testnet/geth.ipc", - config: swarm.ENSClientConfig{ - Endpoint: "/data/testnet/geth.ipc", - ContractAddress: "314159265dD8dbb310642f98f50C066173C1259b", - TLD: "test", - }, - }, - { - description: "HTTP endpoint, TLD and contract address", - value: "eth:314159265dD8dbb310642f98f50C066173C1259b@http://127.0.0.1:1234", - config: swarm.ENSClientConfig{ - Endpoint: "http://127.0.0.1:1234", - ContractAddress: "314159265dD8dbb310642f98f50C066173C1259b", - TLD: "eth", - }, - }, - { - description: "WS endpoint, TLD and contract address", - value: "eth:314159265dD8dbb310642f98f50C066173C1259b@ws://127.0.0.1:1234", - config: swarm.ENSClientConfig{ - Endpoint: "ws://127.0.0.1:1234", - ContractAddress: "314159265dD8dbb310642f98f50C066173C1259b", - TLD: "eth", - }, - }, - } { - t.Run(x.description, func(t *testing.T) { - config := parseFlagEnsAPI(x.value) - if config.Endpoint != x.config.Endpoint { - t.Errorf("expected Endpoint %q, got %q", x.config.Endpoint, config.Endpoint) - } - if config.ContractAddress != x.config.ContractAddress { - t.Errorf("expected ContractAddress %q, got %q", x.config.ContractAddress, config.ContractAddress) - } - if config.TLD != x.config.TLD { - t.Errorf("expected TLD %q, got %q", x.config.TLD, config.TLD) - } - }) - } -} diff --git a/cmd/swarm/manifest.go b/cmd/swarm/manifest.go index 7c4d6052c..aa276e0f9 100644 --- a/cmd/swarm/manifest.go +++ b/cmd/swarm/manifest.go @@ -30,6 +30,8 @@ import ( "gopkg.in/urfave/cli.v1" ) +const bzzManifestJSON = "application/bzz-manifest+json" + func add(ctx *cli.Context) { args := ctx.Args() if len(args) < 3 { @@ -145,7 +147,7 @@ func addEntryToManifest(ctx *cli.Context, mhash, path, hash, ctype string) strin if path == entry.Path { utils.Fatalf("Path %s already present, not adding anything", path) } else { - if entry.ContentType == "application/bzz-manifest+json" { + if entry.ContentType == bzzManifestJSON { prfxlen := strings.HasPrefix(path, entry.Path) if prfxlen && len(path) > len(longestPathEntry.Path) { longestPathEntry = entry @@ -207,7 +209,7 @@ func updateEntryInManifest(ctx *cli.Context, mhash, path, hash, ctype string) st if path == entry.Path { newEntry = entry } else { - if entry.ContentType == "application/bzz-manifest+json" { + if entry.ContentType == bzzManifestJSON { prfxlen := strings.HasPrefix(path, entry.Path) if prfxlen && len(path) > len(longestPathEntry.Path) { longestPathEntry = entry @@ -281,7 +283,7 @@ func removeEntryFromManifest(ctx *cli.Context, mhash, path string) string { if path == entry.Path { entryToRemove = entry } else { - if entry.ContentType == "application/bzz-manifest+json" { + if entry.ContentType == bzzManifestJSON { prfxlen := strings.HasPrefix(path, entry.Path) if prfxlen && len(path) > len(longestPathEntry.Path) { longestPathEntry = entry diff --git a/cmd/swarm/run_test.go b/cmd/swarm/run_test.go index aaaf9e1e5..ed1502868 100644 --- a/cmd/swarm/run_test.go +++ b/cmd/swarm/run_test.go @@ -27,6 +27,7 @@ import ( "time" "github.com/docker/docker/pkg/reexec" + "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/accounts/keystore" "github.com/ethereum/go-ethereum/internal/cmdtest" "github.com/ethereum/go-ethereum/node" @@ -156,9 +157,9 @@ type testNode struct { const testPassphrase = "swarm-test-passphrase" -func newTestNode(t *testing.T, dir string) *testNode { +func getTestAccount(t *testing.T, dir string) (conf *node.Config, account accounts.Account) { // create key - conf := &node.Config{ + conf = &node.Config{ DataDir: dir, IPCPath: "bzzd.ipc", NoUSB: true, @@ -167,18 +168,24 @@ func newTestNode(t *testing.T, dir string) *testNode { if err != nil { t.Fatal(err) } - account, err := n.AccountManager().Backends(keystore.KeyStoreType)[0].(*keystore.KeyStore).NewAccount(testPassphrase) + account, err = n.AccountManager().Backends(keystore.KeyStoreType)[0].(*keystore.KeyStore).NewAccount(testPassphrase) if err != nil { t.Fatal(err) } - node := &testNode{Dir: dir} - // use a unique IPCPath when running tests on Windows if runtime.GOOS == "windows" { conf.IPCPath = fmt.Sprintf("bzzd-%s.ipc", account.Address.String()) } + return conf, account +} + +func newTestNode(t *testing.T, dir string) *testNode { + + conf, account := getTestAccount(t, dir) + node := &testNode{Dir: dir} + // assign ports httpPort, err := assignTCPPort() if err != nil { diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index 5c2929268..30edf199c 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -217,27 +217,27 @@ var ( EthashCachesInMemoryFlag = cli.IntFlag{ Name: "ethash.cachesinmem", Usage: "Number of recent ethash caches to keep in memory (16MB each)", - Value: eth.DefaultConfig.EthashCachesInMem, + Value: eth.DefaultConfig.Ethash.CachesInMem, } EthashCachesOnDiskFlag = cli.IntFlag{ Name: "ethash.cachesondisk", Usage: "Number of recent ethash caches to keep on disk (16MB each)", - Value: eth.DefaultConfig.EthashCachesOnDisk, + Value: eth.DefaultConfig.Ethash.CachesOnDisk, } EthashDatasetDirFlag = DirectoryFlag{ Name: "ethash.dagdir", Usage: "Directory to store the ethash mining DAGs (default = inside home folder)", - Value: DirectoryString{eth.DefaultConfig.EthashDatasetDir}, + Value: DirectoryString{eth.DefaultConfig.Ethash.DatasetDir}, } EthashDatasetsInMemoryFlag = cli.IntFlag{ Name: "ethash.dagsinmem", Usage: "Number of recent ethash mining DAGs to keep in memory (1+GB each)", - Value: eth.DefaultConfig.EthashDatasetsInMem, + Value: eth.DefaultConfig.Ethash.DatasetsInMem, } EthashDatasetsOnDiskFlag = cli.IntFlag{ Name: "ethash.dagsondisk", Usage: "Number of recent ethash mining DAGs to keep on disk (1+GB each)", - Value: eth.DefaultConfig.EthashDatasetsOnDisk, + Value: eth.DefaultConfig.Ethash.DatasetsOnDisk, } // Transaction pool settings TxPoolNoLocalsFlag = cli.BoolFlag{ @@ -584,6 +584,8 @@ func setBootstrapNodes(ctx *cli.Context, cfg *p2p.Config) { urls = params.TestnetBootnodes case ctx.GlobalBool(RinkebyFlag.Name): urls = params.RinkebyBootnodes + case cfg.BootstrapNodes != nil: + return // already set, don't apply defaults. } cfg.BootstrapNodes = make([]*discover.Node, 0, len(urls)) @@ -744,6 +746,12 @@ func MakeAddress(ks *keystore.KeyStore, account string) (accounts.Account, error if err != nil || index < 0 { return accounts.Account{}, fmt.Errorf("invalid account address or index %q", account) } + log.Warn("-------------------------------------------------------------------") + log.Warn("Referring to accounts by order in the keystore folder is dangerous!") + log.Warn("This functionality is deprecated and will be removed in the future!") + log.Warn("Please use explicit addresses! (can search via `geth account list`)") + log.Warn("-------------------------------------------------------------------") + accs := ks.Accounts() if len(accs) <= index { return accounts.Account{}, fmt.Errorf("index %d higher than number of accounts %d", index, len(accs)) @@ -760,15 +768,6 @@ func setEtherbase(ctx *cli.Context, ks *keystore.KeyStore, cfg *eth.Config) { Fatalf("Option %q: %v", EtherbaseFlag.Name, err) } cfg.Etherbase = account.Address - return - } - accounts := ks.Accounts() - if (cfg.Etherbase == common.Address{}) { - if len(accounts) > 0 { - cfg.Etherbase = accounts[0].Address - } else { - log.Warn("No etherbase set and no accounts found as default") - } } } @@ -910,34 +909,60 @@ func setTxPool(ctx *cli.Context, cfg *core.TxPoolConfig) { func setEthash(ctx *cli.Context, cfg *eth.Config) { if ctx.GlobalIsSet(EthashCacheDirFlag.Name) { - cfg.EthashCacheDir = ctx.GlobalString(EthashCacheDirFlag.Name) + cfg.Ethash.CacheDir = ctx.GlobalString(EthashCacheDirFlag.Name) } if ctx.GlobalIsSet(EthashDatasetDirFlag.Name) { - cfg.EthashDatasetDir = ctx.GlobalString(EthashDatasetDirFlag.Name) + cfg.Ethash.DatasetDir = ctx.GlobalString(EthashDatasetDirFlag.Name) } if ctx.GlobalIsSet(EthashCachesInMemoryFlag.Name) { - cfg.EthashCachesInMem = ctx.GlobalInt(EthashCachesInMemoryFlag.Name) + cfg.Ethash.CachesInMem = ctx.GlobalInt(EthashCachesInMemoryFlag.Name) } if ctx.GlobalIsSet(EthashCachesOnDiskFlag.Name) { - cfg.EthashCachesOnDisk = ctx.GlobalInt(EthashCachesOnDiskFlag.Name) + cfg.Ethash.CachesOnDisk = ctx.GlobalInt(EthashCachesOnDiskFlag.Name) } if ctx.GlobalIsSet(EthashDatasetsInMemoryFlag.Name) { - cfg.EthashDatasetsInMem = ctx.GlobalInt(EthashDatasetsInMemoryFlag.Name) + cfg.Ethash.DatasetsInMem = ctx.GlobalInt(EthashDatasetsInMemoryFlag.Name) } if ctx.GlobalIsSet(EthashDatasetsOnDiskFlag.Name) { - cfg.EthashDatasetsOnDisk = ctx.GlobalInt(EthashDatasetsOnDiskFlag.Name) + cfg.Ethash.DatasetsOnDisk = ctx.GlobalInt(EthashDatasetsOnDiskFlag.Name) } } -func checkExclusive(ctx *cli.Context, flags ...cli.Flag) { +// checkExclusive verifies that only a single isntance of the provided flags was +// set by the user. Each flag might optionally be followed by a string type to +// specialize it further. +func checkExclusive(ctx *cli.Context, args ...interface{}) { set := make([]string, 0, 1) - for _, flag := range flags { + for i := 0; i < len(args); i++ { + // Make sure the next argument is a flag and skip if not set + flag, ok := args[i].(cli.Flag) + if !ok { + panic(fmt.Sprintf("invalid argument, not cli.Flag type: %T", args[i])) + } + // Check if next arg extends current and expand its name if so + name := flag.GetName() + + if i+1 < len(args) { + switch option := args[i+1].(type) { + case string: + // Extended flag, expand the name and shift the arguments + if ctx.GlobalString(flag.GetName()) == option { + name += "=" + option + } + i++ + + case cli.Flag: + default: + panic(fmt.Sprintf("invalid argument, not cli.Flag or string extension: %T", args[i+1])) + } + } + // Mark the flag if it's set if ctx.GlobalIsSet(flag.GetName()) { - set = append(set, "--"+flag.GetName()) + set = append(set, "--"+name) } } if len(set) > 1 { - Fatalf("flags %v can't be used at the same time", strings.Join(set, ", ")) + Fatalf("Flags %v can't be used at the same time", strings.Join(set, ", ")) } } @@ -956,6 +981,8 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *eth.Config) { // Avoid conflicting network flags checkExclusive(ctx, DeveloperFlag, TestnetFlag, RinkebyFlag) checkExclusive(ctx, FastSyncFlag, LightModeFlag, SyncModeFlag) + checkExclusive(ctx, LightServFlag, LightModeFlag) + checkExclusive(ctx, LightServFlag, SyncModeFlag, "light") ks := stack.AccountManager().Backends(keystore.KeyStoreType)[0].(*keystore.KeyStore) setEtherbase(ctx, ks, cfg) @@ -1159,10 +1186,14 @@ func MakeChain(ctx *cli.Context, stack *node.Node) (chain *core.BlockChain, chai } else { engine = ethash.NewFaker() if !ctx.GlobalBool(FakePoWFlag.Name) { - engine = ethash.New( - stack.ResolvePath(eth.DefaultConfig.EthashCacheDir), eth.DefaultConfig.EthashCachesInMem, eth.DefaultConfig.EthashCachesOnDisk, - stack.ResolvePath(eth.DefaultConfig.EthashDatasetDir), eth.DefaultConfig.EthashDatasetsInMem, eth.DefaultConfig.EthashDatasetsOnDisk, - ) + engine = ethash.New(ethash.Config{ + CacheDir: stack.ResolvePath(eth.DefaultConfig.Ethash.CacheDir), + CachesInMem: eth.DefaultConfig.Ethash.CachesInMem, + CachesOnDisk: eth.DefaultConfig.Ethash.CachesOnDisk, + DatasetDir: stack.ResolvePath(eth.DefaultConfig.Ethash.DatasetDir), + DatasetsInMem: eth.DefaultConfig.Ethash.DatasetsInMem, + DatasetsOnDisk: eth.DefaultConfig.Ethash.DatasetsOnDisk, + }) } } vmcfg := vm.Config{EnablePreimageRecording: ctx.GlobalBool(VMEnableDebugFlag.Name)} |