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