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 }