github.com/luckypickle/go-ethereum-vet@v1.14.2/cmd/puppeth/module_node.go (about)

     1  // Copyright 2017 The go-ethereum Authors
     2  // This file is part of go-ethereum.
     3  //
     4  // go-ethereum is free software: you can redistribute it and/or modify
     5  // it under the terms of the GNU General Public License as published by
     6  // the Free Software Foundation, either version 3 of the License, or
     7  // (at your option) any later version.
     8  //
     9  // go-ethereum is distributed in the hope that it will be useful,
    10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    12  // GNU General Public License for more details.
    13  //
    14  // You should have received a copy of the GNU General Public License
    15  // along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
    16  
    17  package main
    18  
    19  import (
    20  	"bytes"
    21  	"encoding/json"
    22  	"fmt"
    23  	"math/rand"
    24  	"path/filepath"
    25  	"strconv"
    26  	"strings"
    27  	"text/template"
    28  
    29  	"github.com/luckypickle/go-ethereum-vet/common"
    30  	"github.com/luckypickle/go-ethereum-vet/log"
    31  )
    32  
    33  // nodeDockerfile is the Dockerfile required to run an Ethereum node.
    34  var nodeDockerfile = `
    35  FROM ethereum/client-go:latest
    36  
    37  ADD genesis.json /genesis.json
    38  {{if .Unlock}}
    39  	ADD signer.json /signer.json
    40  	ADD signer.pass /signer.pass
    41  {{end}}
    42  RUN \
    43    echo 'geth --cache 512 init /genesis.json' > geth.sh && \{{if .Unlock}}
    44  	echo 'mkdir -p /root/.ethereum/keystore/ && cp /signer.json /root/.ethereum/keystore/' >> geth.sh && \{{end}}
    45  	echo $'exec geth --networkid {{.NetworkID}} --cache 512 --port {{.Port}} --maxpeers {{.Peers}} {{.LightFlag}} --ethstats \'{{.Ethstats}}\' {{if .Bootnodes}}--bootnodes {{.Bootnodes}}{{end}} {{if .Etherbase}}--miner.etherbase {{.Etherbase}} --mine --miner.threads 1{{end}} {{if .Unlock}}--unlock 0 --password /signer.pass --mine{{end}} --miner.gastarget {{.GasTarget}} --miner.gasprice {{.GasPrice}}' >> geth.sh
    46  
    47  ENTRYPOINT ["/bin/sh", "geth.sh"]
    48  `
    49  
    50  // nodeComposefile is the docker-compose.yml file required to deploy and maintain
    51  // an Ethereum node (bootnode or miner for now).
    52  var nodeComposefile = `
    53  version: '2'
    54  services:
    55    {{.Type}}:
    56      build: .
    57      image: {{.Network}}/{{.Type}}
    58      ports:
    59        - "{{.Port}}:{{.Port}}"
    60        - "{{.Port}}:{{.Port}}/udp"
    61      volumes:
    62        - {{.Datadir}}:/root/.ethereum{{if .Ethashdir}}
    63        - {{.Ethashdir}}:/root/.ethash{{end}}
    64      environment:
    65        - PORT={{.Port}}/tcp
    66        - TOTAL_PEERS={{.TotalPeers}}
    67        - LIGHT_PEERS={{.LightPeers}}
    68        - STATS_NAME={{.Ethstats}}
    69        - MINER_NAME={{.Etherbase}}
    70        - GAS_TARGET={{.GasTarget}}
    71        - GAS_PRICE={{.GasPrice}}
    72      logging:
    73        driver: "json-file"
    74        options:
    75          max-size: "1m"
    76          max-file: "10"
    77      restart: always
    78  `
    79  
    80  // deployNode deploys a new Ethereum node container to a remote machine via SSH,
    81  // docker and docker-compose. If an instance with the specified network name
    82  // already exists there, it will be overwritten!
    83  func deployNode(client *sshClient, network string, bootnodes []string, config *nodeInfos, nocache bool) ([]byte, error) {
    84  	kind := "sealnode"
    85  	if config.keyJSON == "" && config.etherbase == "" {
    86  		kind = "bootnode"
    87  		bootnodes = make([]string, 0)
    88  	}
    89  	// Generate the content to upload to the server
    90  	workdir := fmt.Sprintf("%d", rand.Int63())
    91  	files := make(map[string][]byte)
    92  
    93  	lightFlag := ""
    94  	if config.peersLight > 0 {
    95  		lightFlag = fmt.Sprintf("--lightpeers=%d --lightserv=50", config.peersLight)
    96  	}
    97  	dockerfile := new(bytes.Buffer)
    98  	template.Must(template.New("").Parse(nodeDockerfile)).Execute(dockerfile, map[string]interface{}{
    99  		"NetworkID": config.network,
   100  		"Port":      config.port,
   101  		"Peers":     config.peersTotal,
   102  		"LightFlag": lightFlag,
   103  		"Bootnodes": strings.Join(bootnodes, ","),
   104  		"Ethstats":  config.ethstats,
   105  		"Etherbase": config.etherbase,
   106  		"GasTarget": uint64(1000000 * config.gasTarget),
   107  		"GasPrice":  uint64(1000000000 * config.gasPrice),
   108  		"Unlock":    config.keyJSON != "",
   109  	})
   110  	files[filepath.Join(workdir, "Dockerfile")] = dockerfile.Bytes()
   111  
   112  	composefile := new(bytes.Buffer)
   113  	template.Must(template.New("").Parse(nodeComposefile)).Execute(composefile, map[string]interface{}{
   114  		"Type":       kind,
   115  		"Datadir":    config.datadir,
   116  		"Ethashdir":  config.ethashdir,
   117  		"Network":    network,
   118  		"Port":       config.port,
   119  		"TotalPeers": config.peersTotal,
   120  		"Light":      config.peersLight > 0,
   121  		"LightPeers": config.peersLight,
   122  		"Ethstats":   config.ethstats[:strings.Index(config.ethstats, ":")],
   123  		"Etherbase":  config.etherbase,
   124  		"GasTarget":  config.gasTarget,
   125  		"GasPrice":   config.gasPrice,
   126  	})
   127  	files[filepath.Join(workdir, "docker-compose.yaml")] = composefile.Bytes()
   128  
   129  	files[filepath.Join(workdir, "genesis.json")] = config.genesis
   130  	if config.keyJSON != "" {
   131  		files[filepath.Join(workdir, "signer.json")] = []byte(config.keyJSON)
   132  		files[filepath.Join(workdir, "signer.pass")] = []byte(config.keyPass)
   133  	}
   134  	// Upload the deployment files to the remote server (and clean up afterwards)
   135  	if out, err := client.Upload(files); err != nil {
   136  		return out, err
   137  	}
   138  	defer client.Run("rm -rf " + workdir)
   139  
   140  	// Build and deploy the boot or seal node service
   141  	if nocache {
   142  		return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s build --pull --no-cache && docker-compose -p %s up -d --force-recreate --timeout 60", workdir, network, network))
   143  	}
   144  	return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s up -d --build --force-recreate --timeout 60", workdir, network))
   145  }
   146  
   147  // nodeInfos is returned from a boot or seal node status check to allow reporting
   148  // various configuration parameters.
   149  type nodeInfos struct {
   150  	genesis    []byte
   151  	network    int64
   152  	datadir    string
   153  	ethashdir  string
   154  	ethstats   string
   155  	port       int
   156  	enode      string
   157  	peersTotal int
   158  	peersLight int
   159  	etherbase  string
   160  	keyJSON    string
   161  	keyPass    string
   162  	gasTarget  float64
   163  	gasPrice   float64
   164  }
   165  
   166  // Report converts the typed struct into a plain string->string map, containing
   167  // most - but not all - fields for reporting to the user.
   168  func (info *nodeInfos) Report() map[string]string {
   169  	report := map[string]string{
   170  		"Data directory":           info.datadir,
   171  		"Listener port":            strconv.Itoa(info.port),
   172  		"Peer count (all total)":   strconv.Itoa(info.peersTotal),
   173  		"Peer count (light nodes)": strconv.Itoa(info.peersLight),
   174  		"Ethstats username":        info.ethstats,
   175  	}
   176  	if info.gasTarget > 0 {
   177  		// Miner or signer node
   178  		report["Gas limit (baseline target)"] = fmt.Sprintf("%0.3f MGas", info.gasTarget)
   179  		report["Gas price (minimum accepted)"] = fmt.Sprintf("%0.3f GWei", info.gasPrice)
   180  
   181  		if info.etherbase != "" {
   182  			// Ethash proof-of-work miner
   183  			report["Ethash directory"] = info.ethashdir
   184  			report["Miner account"] = info.etherbase
   185  		}
   186  		if info.keyJSON != "" {
   187  			// Clique proof-of-authority signer
   188  			var key struct {
   189  				Address string `json:"address"`
   190  			}
   191  			if err := json.Unmarshal([]byte(info.keyJSON), &key); err == nil {
   192  				report["Signer account"] = common.HexToAddress(key.Address).Hex()
   193  			} else {
   194  				log.Error("Failed to retrieve signer address", "err", err)
   195  			}
   196  		}
   197  	}
   198  	return report
   199  }
   200  
   201  // checkNode does a health-check against a boot or seal node server to verify
   202  // whether it's running, and if yes, whether it's responsive.
   203  func checkNode(client *sshClient, network string, boot bool) (*nodeInfos, error) {
   204  	kind := "bootnode"
   205  	if !boot {
   206  		kind = "sealnode"
   207  	}
   208  	// Inspect a possible bootnode container on the host
   209  	infos, err := inspectContainer(client, fmt.Sprintf("%s_%s_1", network, kind))
   210  	if err != nil {
   211  		return nil, err
   212  	}
   213  	if !infos.running {
   214  		return nil, ErrServiceOffline
   215  	}
   216  	// Resolve a few types from the environmental variables
   217  	totalPeers, _ := strconv.Atoi(infos.envvars["TOTAL_PEERS"])
   218  	lightPeers, _ := strconv.Atoi(infos.envvars["LIGHT_PEERS"])
   219  	gasTarget, _ := strconv.ParseFloat(infos.envvars["GAS_TARGET"], 64)
   220  	gasPrice, _ := strconv.ParseFloat(infos.envvars["GAS_PRICE"], 64)
   221  
   222  	// Container available, retrieve its node ID and its genesis json
   223  	var out []byte
   224  	if out, err = client.Run(fmt.Sprintf("docker exec %s_%s_1 geth --exec admin.nodeInfo.id --cache=16 attach", network, kind)); err != nil {
   225  		return nil, ErrServiceUnreachable
   226  	}
   227  	id := bytes.Trim(bytes.TrimSpace(out), "\"")
   228  
   229  	if out, err = client.Run(fmt.Sprintf("docker exec %s_%s_1 cat /genesis.json", network, kind)); err != nil {
   230  		return nil, ErrServiceUnreachable
   231  	}
   232  	genesis := bytes.TrimSpace(out)
   233  
   234  	keyJSON, keyPass := "", ""
   235  	if out, err = client.Run(fmt.Sprintf("docker exec %s_%s_1 cat /signer.json", network, kind)); err == nil {
   236  		keyJSON = string(bytes.TrimSpace(out))
   237  	}
   238  	if out, err = client.Run(fmt.Sprintf("docker exec %s_%s_1 cat /signer.pass", network, kind)); err == nil {
   239  		keyPass = string(bytes.TrimSpace(out))
   240  	}
   241  	// Run a sanity check to see if the devp2p is reachable
   242  	port := infos.portmap[infos.envvars["PORT"]]
   243  	if err = checkPort(client.server, port); err != nil {
   244  		log.Warn(fmt.Sprintf("%s devp2p port seems unreachable", strings.Title(kind)), "server", client.server, "port", port, "err", err)
   245  	}
   246  	// Assemble and return the useful infos
   247  	stats := &nodeInfos{
   248  		genesis:    genesis,
   249  		datadir:    infos.volumes["/root/.ethereum"],
   250  		ethashdir:  infos.volumes["/root/.ethash"],
   251  		port:       port,
   252  		peersTotal: totalPeers,
   253  		peersLight: lightPeers,
   254  		ethstats:   infos.envvars["STATS_NAME"],
   255  		etherbase:  infos.envvars["MINER_NAME"],
   256  		keyJSON:    keyJSON,
   257  		keyPass:    keyPass,
   258  		gasTarget:  gasTarget,
   259  		gasPrice:   gasPrice,
   260  	}
   261  	stats.enode = fmt.Sprintf("enode://%s@%s:%d", id, client.address, stats.port)
   262  
   263  	return stats, nil
   264  }