github.com/zhiqiangxu/go-ethereum@v1.9.16-0.20210824055606-be91cfdebc48/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/zhiqiangxu/go-ethereum/common" 30 "github.com/zhiqiangxu/go-ethereum/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}} --nat extip:{{.IP}} --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.gaslimit {{.GasLimit}} --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 container_name: {{.Network}}_{{.Type}}_1 59 ports: 60 - "{{.Port}}:{{.Port}}" 61 - "{{.Port}}:{{.Port}}/udp" 62 volumes: 63 - {{.Datadir}}:/root/.ethereum{{if .Ethashdir}} 64 - {{.Ethashdir}}:/root/.ethash{{end}} 65 environment: 66 - PORT={{.Port}}/tcp 67 - TOTAL_PEERS={{.TotalPeers}} 68 - LIGHT_PEERS={{.LightPeers}} 69 - STATS_NAME={{.Ethstats}} 70 - MINER_NAME={{.Etherbase}} 71 - GAS_TARGET={{.GasTarget}} 72 - GAS_LIMIT={{.GasLimit}} 73 - GAS_PRICE={{.GasPrice}} 74 logging: 75 driver: "json-file" 76 options: 77 max-size: "1m" 78 max-file: "10" 79 restart: always 80 ` 81 82 // deployNode deploys a new Ethereum node container to a remote machine via SSH, 83 // docker and docker-compose. If an instance with the specified network name 84 // already exists there, it will be overwritten! 85 func deployNode(client *sshClient, network string, bootnodes []string, config *nodeInfos, nocache bool) ([]byte, error) { 86 kind := "sealnode" 87 if config.keyJSON == "" && config.etherbase == "" { 88 kind = "bootnode" 89 bootnodes = make([]string, 0) 90 } 91 // Generate the content to upload to the server 92 workdir := fmt.Sprintf("%d", rand.Int63()) 93 files := make(map[string][]byte) 94 95 lightFlag := "" 96 if config.peersLight > 0 { 97 lightFlag = fmt.Sprintf("--lightpeers=%d --lightserv=50", config.peersLight) 98 } 99 dockerfile := new(bytes.Buffer) 100 template.Must(template.New("").Parse(nodeDockerfile)).Execute(dockerfile, map[string]interface{}{ 101 "NetworkID": config.network, 102 "Port": config.port, 103 "IP": client.address, 104 "Peers": config.peersTotal, 105 "LightFlag": lightFlag, 106 "Bootnodes": strings.Join(bootnodes, ","), 107 "Ethstats": config.ethstats, 108 "Etherbase": config.etherbase, 109 "GasTarget": uint64(1000000 * config.gasTarget), 110 "GasLimit": uint64(1000000 * config.gasLimit), 111 "GasPrice": uint64(1000000000 * config.gasPrice), 112 "Unlock": config.keyJSON != "", 113 }) 114 files[filepath.Join(workdir, "Dockerfile")] = dockerfile.Bytes() 115 116 composefile := new(bytes.Buffer) 117 template.Must(template.New("").Parse(nodeComposefile)).Execute(composefile, map[string]interface{}{ 118 "Type": kind, 119 "Datadir": config.datadir, 120 "Ethashdir": config.ethashdir, 121 "Network": network, 122 "Port": config.port, 123 "TotalPeers": config.peersTotal, 124 "Light": config.peersLight > 0, 125 "LightPeers": config.peersLight, 126 "Ethstats": config.ethstats[:strings.Index(config.ethstats, ":")], 127 "Etherbase": config.etherbase, 128 "GasTarget": config.gasTarget, 129 "GasLimit": config.gasLimit, 130 "GasPrice": config.gasPrice, 131 }) 132 files[filepath.Join(workdir, "docker-compose.yaml")] = composefile.Bytes() 133 134 files[filepath.Join(workdir, "genesis.json")] = config.genesis 135 if config.keyJSON != "" { 136 files[filepath.Join(workdir, "signer.json")] = []byte(config.keyJSON) 137 files[filepath.Join(workdir, "signer.pass")] = []byte(config.keyPass) 138 } 139 // Upload the deployment files to the remote server (and clean up afterwards) 140 if out, err := client.Upload(files); err != nil { 141 return out, err 142 } 143 defer client.Run("rm -rf " + workdir) 144 145 // Build and deploy the boot or seal node service 146 if nocache { 147 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)) 148 } 149 return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s up -d --build --force-recreate --timeout 60", workdir, network)) 150 } 151 152 // nodeInfos is returned from a boot or seal node status check to allow reporting 153 // various configuration parameters. 154 type nodeInfos struct { 155 genesis []byte 156 network int64 157 datadir string 158 ethashdir string 159 ethstats string 160 port int 161 enode string 162 peersTotal int 163 peersLight int 164 etherbase string 165 keyJSON string 166 keyPass string 167 gasTarget float64 168 gasLimit float64 169 gasPrice float64 170 } 171 172 // Report converts the typed struct into a plain string->string map, containing 173 // most - but not all - fields for reporting to the user. 174 func (info *nodeInfos) Report() map[string]string { 175 report := map[string]string{ 176 "Data directory": info.datadir, 177 "Listener port": strconv.Itoa(info.port), 178 "Peer count (all total)": strconv.Itoa(info.peersTotal), 179 "Peer count (light nodes)": strconv.Itoa(info.peersLight), 180 "Ethstats username": info.ethstats, 181 } 182 if info.gasTarget > 0 { 183 // Miner or signer node 184 report["Gas price (minimum accepted)"] = fmt.Sprintf("%0.3f GWei", info.gasPrice) 185 report["Gas floor (baseline target)"] = fmt.Sprintf("%0.3f MGas", info.gasTarget) 186 report["Gas ceil (target maximum)"] = fmt.Sprintf("%0.3f MGas", info.gasLimit) 187 188 if info.etherbase != "" { 189 // Ethash proof-of-work miner 190 report["Ethash directory"] = info.ethashdir 191 report["Miner account"] = info.etherbase 192 } 193 if info.keyJSON != "" { 194 // Clique proof-of-authority signer 195 var key struct { 196 Address string `json:"address"` 197 } 198 if err := json.Unmarshal([]byte(info.keyJSON), &key); err == nil { 199 report["Signer account"] = common.HexToAddress(key.Address).Hex() 200 } else { 201 log.Error("Failed to retrieve signer address", "err", err) 202 } 203 } 204 } 205 return report 206 } 207 208 // checkNode does a health-check against a boot or seal node server to verify 209 // whether it's running, and if yes, whether it's responsive. 210 func checkNode(client *sshClient, network string, boot bool) (*nodeInfos, error) { 211 kind := "bootnode" 212 if !boot { 213 kind = "sealnode" 214 } 215 // Inspect a possible bootnode container on the host 216 infos, err := inspectContainer(client, fmt.Sprintf("%s_%s_1", network, kind)) 217 if err != nil { 218 return nil, err 219 } 220 if !infos.running { 221 return nil, ErrServiceOffline 222 } 223 // Resolve a few types from the environmental variables 224 totalPeers, _ := strconv.Atoi(infos.envvars["TOTAL_PEERS"]) 225 lightPeers, _ := strconv.Atoi(infos.envvars["LIGHT_PEERS"]) 226 gasTarget, _ := strconv.ParseFloat(infos.envvars["GAS_TARGET"], 64) 227 gasLimit, _ := strconv.ParseFloat(infos.envvars["GAS_LIMIT"], 64) 228 gasPrice, _ := strconv.ParseFloat(infos.envvars["GAS_PRICE"], 64) 229 230 // Container available, retrieve its node ID and its genesis json 231 var out []byte 232 if out, err = client.Run(fmt.Sprintf("docker exec %s_%s_1 geth --exec admin.nodeInfo.enode --cache=16 attach", network, kind)); err != nil { 233 return nil, ErrServiceUnreachable 234 } 235 enode := bytes.Trim(bytes.TrimSpace(out), "\"") 236 237 if out, err = client.Run(fmt.Sprintf("docker exec %s_%s_1 cat /genesis.json", network, kind)); err != nil { 238 return nil, ErrServiceUnreachable 239 } 240 genesis := bytes.TrimSpace(out) 241 242 keyJSON, keyPass := "", "" 243 if out, err = client.Run(fmt.Sprintf("docker exec %s_%s_1 cat /signer.json", network, kind)); err == nil { 244 keyJSON = string(bytes.TrimSpace(out)) 245 } 246 if out, err = client.Run(fmt.Sprintf("docker exec %s_%s_1 cat /signer.pass", network, kind)); err == nil { 247 keyPass = string(bytes.TrimSpace(out)) 248 } 249 // Run a sanity check to see if the devp2p is reachable 250 port := infos.portmap[infos.envvars["PORT"]] 251 if err = checkPort(client.server, port); err != nil { 252 log.Warn(fmt.Sprintf("%s devp2p port seems unreachable", strings.Title(kind)), "server", client.server, "port", port, "err", err) 253 } 254 // Assemble and return the useful infos 255 stats := &nodeInfos{ 256 genesis: genesis, 257 datadir: infos.volumes["/root/.ethereum"], 258 ethashdir: infos.volumes["/root/.ethash"], 259 port: port, 260 peersTotal: totalPeers, 261 peersLight: lightPeers, 262 ethstats: infos.envvars["STATS_NAME"], 263 etherbase: infos.envvars["MINER_NAME"], 264 keyJSON: keyJSON, 265 keyPass: keyPass, 266 gasTarget: gasTarget, 267 gasLimit: gasLimit, 268 gasPrice: gasPrice, 269 } 270 stats.enode = string(enode) 271 272 return stats, nil 273 }