github.com/reapchain/go-reapchain@v0.2.15-0.20210609012950-9735c110c705/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  	"fmt"
    22  	"math/rand"
    23  	"path/filepath"
    24  	"strconv"
    25  	"strings"
    26  	"text/template"
    27  
    28  	"github.com/ethereum/go-ethereum/log"
    29  )
    30  
    31  // nodeDockerfile is the Dockerfile required to run an Ethereum node.
    32  var nodeDockerfile = `
    33  FROM ethereum/client-go:alpine-develop
    34  
    35  ADD genesis.json /genesis.json
    36  {{if .Unlock}}
    37  	ADD signer.json /signer.json
    38  	ADD signer.pass /signer.pass
    39  {{end}}
    40  RUN \
    41    echo '/geth init /genesis.json' > geth.sh && \{{if .Unlock}}
    42  	echo 'mkdir -p /root/.ethereum/keystore/ && cp /signer.json /root/.ethereum/keystore/' >> geth.sh && \{{end}}
    43  	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
    44  
    45  ENTRYPOINT ["/bin/sh", "geth.sh"]
    46  `
    47  
    48  // nodeComposefile is the docker-compose.yml file required to deploy and maintain
    49  // an Ethereum node (bootnode or miner for now).
    50  var nodeComposefile = `
    51  version: '2'
    52  services:
    53    {{.Type}}:
    54      build: .
    55      image: {{.Network}}/{{.Type}}
    56      ports:
    57        - "{{.FullPort}}:{{.FullPort}}"
    58        - "{{.FullPort}}:{{.FullPort}}/udp"{{if .Light}}
    59        - "{{.LightPort}}:{{.LightPort}}/udp"{{end}}
    60      volumes:
    61        - {{.Datadir}}:/root/.ethereum
    62      environment:
    63        - FULL_PORT={{.FullPort}}/tcp
    64        - LIGHT_PORT={{.LightPort}}/udp
    65        - TOTAL_PEERS={{.TotalPeers}}
    66        - LIGHT_PEERS={{.LightPeers}}
    67        - STATS_NAME={{.Ethstats}}
    68        - MINER_NAME={{.Etherbase}}
    69        - GAS_TARGET={{.GasTarget}}
    70        - GAS_PRICE={{.GasPrice}}
    71      restart: always
    72  `
    73  
    74  // deployNode deploys a new Ethereum node container to a remote machine via SSH,
    75  // docker and docker-compose. If an instance with the specified network name
    76  // already exists there, it will be overwritten!
    77  func deployNode(client *sshClient, network string, bootv4, bootv5 []string, config *nodeInfos) ([]byte, error) {
    78  	kind := "sealnode"
    79  	if config.keyJSON == "" && config.etherbase == "" {
    80  		kind = "bootnode"
    81  		bootv4 = make([]string, 0)
    82  		bootv5 = make([]string, 0)
    83  	}
    84  	// Generate the content to upload to the server
    85  	workdir := fmt.Sprintf("%d", rand.Int63())
    86  	files := make(map[string][]byte)
    87  
    88  	lightFlag := ""
    89  	if config.peersLight > 0 {
    90  		lightFlag = fmt.Sprintf("--lightpeers=%d --lightserv=50", config.peersLight)
    91  	}
    92  	dockerfile := new(bytes.Buffer)
    93  	template.Must(template.New("").Parse(nodeDockerfile)).Execute(dockerfile, map[string]interface{}{
    94  		"NetworkID": config.network,
    95  		"Port":      config.portFull,
    96  		"Peers":     config.peersTotal,
    97  		"LightFlag": lightFlag,
    98  		"BootV4":    strings.Join(bootv4, ","),
    99  		"BootV5":    strings.Join(bootv5, ","),
   100  		"Ethstats":  config.ethstats,
   101  		"Etherbase": config.etherbase,
   102  		"GasTarget": uint64(1000000 * config.gasTarget),
   103  		"GasPrice":  uint64(1000000000 * config.gasPrice),
   104  		"Unlock":    config.keyJSON != "",
   105  	})
   106  	files[filepath.Join(workdir, "Dockerfile")] = dockerfile.Bytes()
   107  
   108  	composefile := new(bytes.Buffer)
   109  	template.Must(template.New("").Parse(nodeComposefile)).Execute(composefile, map[string]interface{}{
   110  		"Type":       kind,
   111  		"Datadir":    config.datadir,
   112  		"Network":    network,
   113  		"FullPort":   config.portFull,
   114  		"TotalPeers": config.peersTotal,
   115  		"Light":      config.peersLight > 0,
   116  		"LightPort":  config.portFull + 1,
   117  		"LightPeers": config.peersLight,
   118  		"Ethstats":   config.ethstats[:strings.Index(config.ethstats, ":")],
   119  		"Etherbase":  config.etherbase,
   120  		"GasTarget":  config.gasTarget,
   121  		"GasPrice":   config.gasPrice,
   122  	})
   123  	files[filepath.Join(workdir, "docker-compose.yaml")] = composefile.Bytes()
   124  
   125  	//genesisfile, _ := json.MarshalIndent(config.genesis, "", "  ")
   126  	files[filepath.Join(workdir, "genesis.json")] = []byte(config.genesis)
   127  
   128  	if config.keyJSON != "" {
   129  		files[filepath.Join(workdir, "signer.json")] = []byte(config.keyJSON)
   130  		files[filepath.Join(workdir, "signer.pass")] = []byte(config.keyPass)
   131  	}
   132  	// Upload the deployment files to the remote server (and clean up afterwards)
   133  	if out, err := client.Upload(files); err != nil {
   134  		return out, err
   135  	}
   136  	defer client.Run("rm -rf " + workdir)
   137  
   138  	// Build and deploy the boot or seal node service
   139  	return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s up -d --build", workdir, network))
   140  }
   141  
   142  // nodeInfos is returned from a boot or seal node status check to allow reporting
   143  // various configuration parameters.
   144  type nodeInfos struct {
   145  	genesis    []byte
   146  	network    int64
   147  	datadir    string
   148  	ethstats   string
   149  	portFull   int
   150  	portLight  int
   151  	enodeFull  string
   152  	enodeLight string
   153  	peersTotal int
   154  	peersLight int
   155  	etherbase  string
   156  	keyJSON    string
   157  	keyPass    string
   158  	gasTarget  float64
   159  	gasPrice   float64
   160  }
   161  
   162  // String implements the stringer interface.
   163  func (info *nodeInfos) String() string {
   164  	discv5 := ""
   165  	if info.peersLight > 0 {
   166  		discv5 = fmt.Sprintf(", portv5=%d", info.portLight)
   167  	}
   168  	return fmt.Sprintf("port=%d%s, datadir=%s, peers=%d, lights=%d, ethstats=%s, gastarget=%0.3f MGas, gasprice=%0.3f GWei",
   169  		info.portFull, discv5, info.datadir, info.peersTotal, info.peersLight, info.ethstats, info.gasTarget, info.gasPrice)
   170  }
   171  
   172  // checkNode does a health-check against an boot or seal node server to verify
   173  // whether it's running, and if yes, whether it's responsive.
   174  func checkNode(client *sshClient, network string, boot bool) (*nodeInfos, error) {
   175  	kind := "bootnode"
   176  	if !boot {
   177  		kind = "sealnode"
   178  	}
   179  	// Inspect a possible bootnode container on the host
   180  	infos, err := inspectContainer(client, fmt.Sprintf("%s_%s_1", network, kind))
   181  	if err != nil {
   182  		return nil, err
   183  	}
   184  	if !infos.running {
   185  		return nil, ErrServiceOffline
   186  	}
   187  	// Resolve a few types from the environmental variables
   188  	totalPeers, _ := strconv.Atoi(infos.envvars["TOTAL_PEERS"])
   189  	lightPeers, _ := strconv.Atoi(infos.envvars["LIGHT_PEERS"])
   190  	gasTarget, _ := strconv.ParseFloat(infos.envvars["GAS_TARGET"], 64)
   191  	gasPrice, _ := strconv.ParseFloat(infos.envvars["GAS_PRICE"], 64)
   192  
   193  	// Container available, retrieve its node ID and its genesis json
   194  	var out []byte
   195  	if out, err = client.Run(fmt.Sprintf("docker exec %s_%s_1 /geth --exec admin.nodeInfo.id attach", network, kind)); err != nil {
   196  		return nil, ErrServiceUnreachable
   197  	}
   198  	id := bytes.Trim(bytes.TrimSpace(out), "\"")
   199  
   200  	if out, err = client.Run(fmt.Sprintf("docker exec %s_%s_1 cat /genesis.json", network, kind)); err != nil {
   201  		return nil, ErrServiceUnreachable
   202  	}
   203  	genesis := bytes.TrimSpace(out)
   204  
   205  	keyJSON, keyPass := "", ""
   206  	if out, err = client.Run(fmt.Sprintf("docker exec %s_%s_1 cat /signer.json", network, kind)); err == nil {
   207  		keyJSON = string(bytes.TrimSpace(out))
   208  	}
   209  	if out, err = client.Run(fmt.Sprintf("docker exec %s_%s_1 cat /signer.pass", network, kind)); err == nil {
   210  		keyPass = string(bytes.TrimSpace(out))
   211  	}
   212  	// Run a sanity check to see if the devp2p is reachable
   213  	port := infos.portmap[infos.envvars["FULL_PORT"]]
   214  	if err = checkPort(client.server, port); err != nil {
   215  		log.Warn(fmt.Sprintf("%s devp2p port seems unreachable", strings.Title(kind)), "server", client.server, "port", port, "err", err)
   216  	}
   217  	// Assemble and return the useful infos
   218  	stats := &nodeInfos{
   219  		genesis:    genesis,
   220  		datadir:    infos.volumes["/root/.ethereum"],
   221  		portFull:   infos.portmap[infos.envvars["FULL_PORT"]],
   222  		portLight:  infos.portmap[infos.envvars["LIGHT_PORT"]],
   223  		peersTotal: totalPeers,
   224  		peersLight: lightPeers,
   225  		ethstats:   infos.envvars["STATS_NAME"],
   226  		etherbase:  infos.envvars["MINER_NAME"],
   227  		keyJSON:    keyJSON,
   228  		keyPass:    keyPass,
   229  		gasTarget:  gasTarget,
   230  		gasPrice:   gasPrice,
   231  	}
   232  	stats.enodeFull = fmt.Sprintf("enode://%s@%s:%d", id, client.address, stats.portFull)
   233  	if stats.portLight != 0 {
   234  		stats.enodeLight = fmt.Sprintf("enode://%s@%s:%d?discport=%d", id, client.address, stats.portFull, stats.portLight)
   235  	}
   236  	return stats, nil
   237  }