github.com/SmartMeshFoundation/Spectrum@v0.0.0-20220621030607-452a266fee1e/cmd/puppeth/module_node.go (about) 1 // Copyright 2017 The Spectrum Authors 2 // This file is part of Spectrum. 3 // 4 // Spectrum 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 // Spectrum 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 Spectrum. 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/SmartMeshFoundation/Spectrum/common" 30 "github.com/SmartMeshFoundation/Spectrum/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 $'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 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 - "{{.FullPort}}:{{.FullPort}}" 60 - "{{.FullPort}}:{{.FullPort}}/udp"{{if .Light}} 61 - "{{.LightPort}}:{{.LightPort}}/udp"{{end}} 62 volumes: 63 - {{.Datadir}}:/root/.ethereum{{if .Ethashdir}} 64 - {{.Ethashdir}}:/root/.ethash{{end}} 65 environment: 66 - FULL_PORT={{.FullPort}}/tcp 67 - LIGHT_PORT={{.LightPort}}/udp 68 - TOTAL_PEERS={{.TotalPeers}} 69 - LIGHT_PEERS={{.LightPeers}} 70 - STATS_NAME={{.Ethstats}} 71 - MINER_NAME={{.Etherbase}} 72 - GAS_TARGET={{.GasTarget}} 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, bootv4, bootv5 []string, config *nodeInfos, nocache bool) ([]byte, error) { 86 kind := "sealnode" 87 if config.keyJSON == "" && config.etherbase == "" { 88 kind = "bootnode" 89 bootv4 = make([]string, 0) 90 bootv5 = make([]string, 0) 91 } 92 // Generate the content to upload to the server 93 workdir := fmt.Sprintf("%d", rand.Int63()) 94 files := make(map[string][]byte) 95 96 lightFlag := "" 97 if config.peersLight > 0 { 98 lightFlag = fmt.Sprintf("--lightpeers=%d --lightserv=50", config.peersLight) 99 } 100 dockerfile := new(bytes.Buffer) 101 template.Must(template.New("").Parse(nodeDockerfile)).Execute(dockerfile, map[string]interface{}{ 102 "NetworkID": config.network, 103 "Port": config.portFull, 104 "Peers": config.peersTotal, 105 "LightFlag": lightFlag, 106 "BootV4": strings.Join(bootv4, ","), 107 "BootV5": strings.Join(bootv5, ","), 108 "Ethstats": config.ethstats, 109 "Etherbase": config.etherbase, 110 "GasTarget": uint64(1000000 * config.gasTarget), 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 "FullPort": config.portFull, 123 "TotalPeers": config.peersTotal, 124 "Light": config.peersLight > 0, 125 "LightPort": config.portFull + 1, 126 "LightPeers": config.peersLight, 127 "Ethstats": config.ethstats[:strings.Index(config.ethstats, ":")], 128 "Etherbase": config.etherbase, 129 "GasTarget": config.gasTarget, 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", workdir, network, network)) 148 } 149 return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s up -d --build --force-recreate", 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 portFull int 161 portLight int 162 enodeFull string 163 enodeLight string 164 peersTotal int 165 peersLight int 166 etherbase string 167 keyJSON string 168 keyPass string 169 gasTarget float64 170 gasPrice float64 171 } 172 173 // Report converts the typed struct into a plain string->string map, containing 174 // most - but not all - fields for reporting to the user. 175 func (info *nodeInfos) Report() map[string]string { 176 report := map[string]string{ 177 "Data directory": info.datadir, 178 "Listener port (full nodes)": strconv.Itoa(info.portFull), 179 "Peer count (all total)": strconv.Itoa(info.peersTotal), 180 "Peer count (light nodes)": strconv.Itoa(info.peersLight), 181 "Ethstats username": info.ethstats, 182 } 183 if info.peersLight > 0 { 184 // Light server enabled 185 report["Listener port (light nodes)"] = strconv.Itoa(info.portLight) 186 } 187 if info.gasTarget > 0 { 188 // Miner or signer node 189 report["Gas limit (baseline target)"] = fmt.Sprintf("%0.3f MGas", info.gasTarget) 190 report["Gas price (minimum accepted)"] = fmt.Sprintf("%0.3f GWei", info.gasPrice) 191 192 if info.etherbase != "" { 193 // Ethash proof-of-work miner 194 report["Ethash directory"] = info.ethashdir 195 report["Miner account"] = info.etherbase 196 } 197 if info.keyJSON != "" { 198 // Clique proof-of-authority signer 199 var key struct { 200 Address string `json:"address"` 201 } 202 if err := json.Unmarshal([]byte(info.keyJSON), &key); err == nil { 203 report["Signer account"] = common.HexToAddress(key.Address).Hex() 204 } else { 205 log.Error("Failed to retrieve signer address", "err", err) 206 } 207 } 208 } 209 return report 210 } 211 212 // checkNode does a health-check against an boot or seal node server to verify 213 // whether it's running, and if yes, whether it's responsive. 214 func checkNode(client *sshClient, network string, boot bool) (*nodeInfos, error) { 215 kind := "bootnode" 216 if !boot { 217 kind = "sealnode" 218 } 219 // Inspect a possible bootnode container on the host 220 infos, err := inspectContainer(client, fmt.Sprintf("%s_%s_1", network, kind)) 221 if err != nil { 222 return nil, err 223 } 224 if !infos.running { 225 return nil, ErrServiceOffline 226 } 227 // Resolve a few types from the environmental variables 228 totalPeers, _ := strconv.Atoi(infos.envvars["TOTAL_PEERS"]) 229 lightPeers, _ := strconv.Atoi(infos.envvars["LIGHT_PEERS"]) 230 gasTarget, _ := strconv.ParseFloat(infos.envvars["GAS_TARGET"], 64) 231 gasPrice, _ := strconv.ParseFloat(infos.envvars["GAS_PRICE"], 64) 232 233 // Container available, retrieve its node ID and its genesis json 234 var out []byte 235 if out, err = client.Run(fmt.Sprintf("docker exec %s_%s_1 geth --exec admin.nodeInfo.id attach", network, kind)); err != nil { 236 return nil, ErrServiceUnreachable 237 } 238 id := bytes.Trim(bytes.TrimSpace(out), "\"") 239 240 if out, err = client.Run(fmt.Sprintf("docker exec %s_%s_1 cat /genesis.json", network, kind)); err != nil { 241 return nil, ErrServiceUnreachable 242 } 243 genesis := bytes.TrimSpace(out) 244 245 keyJSON, keyPass := "", "" 246 if out, err = client.Run(fmt.Sprintf("docker exec %s_%s_1 cat /signer.json", network, kind)); err == nil { 247 keyJSON = string(bytes.TrimSpace(out)) 248 } 249 if out, err = client.Run(fmt.Sprintf("docker exec %s_%s_1 cat /signer.pass", network, kind)); err == nil { 250 keyPass = string(bytes.TrimSpace(out)) 251 } 252 // Run a sanity check to see if the devp2p is reachable 253 port := infos.portmap[infos.envvars["FULL_PORT"]] 254 if err = checkPort(client.server, port); err != nil { 255 log.Warn(fmt.Sprintf("%s devp2p port seems unreachable", strings.Title(kind)), "server", client.server, "port", port, "err", err) 256 } 257 // Assemble and return the useful infos 258 stats := &nodeInfos{ 259 genesis: genesis, 260 datadir: infos.volumes["/root/.ethereum"], 261 ethashdir: infos.volumes["/root/.ethash"], 262 portFull: infos.portmap[infos.envvars["FULL_PORT"]], 263 portLight: infos.portmap[infos.envvars["LIGHT_PORT"]], 264 peersTotal: totalPeers, 265 peersLight: lightPeers, 266 ethstats: infos.envvars["STATS_NAME"], 267 etherbase: infos.envvars["MINER_NAME"], 268 keyJSON: keyJSON, 269 keyPass: keyPass, 270 gasTarget: gasTarget, 271 gasPrice: gasPrice, 272 } 273 stats.enodeFull = fmt.Sprintf("enode://%s@%s:%d", id, client.address, stats.portFull) 274 if stats.portLight != 0 { 275 stats.enodeLight = fmt.Sprintf("enode://%s@%s:%d?discport=%d", id, client.address, stats.portFull, stats.portLight) 276 } 277 return stats, nil 278 }