aboutsummaryrefslogblamecommitdiffstats
path: root/cmd/puppeth/module_node.go
blob: 5d9ef46523e0aac00d50930a19da03f1c0e40179 (plain) (tree)



















                                                                       
                       






                       
                                                




                                                                     
                              






                                    

                                                                                                                     
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     
 
                                 









                                                                                 
                                            
          

                                 
            

                                                     
                
                          



                                   
                                 
                               
                               




                         





                                                                                
                                                                                                                         


                                                           
                                             











                                                                                                         
                                         
                                            

                                               
                                                          

                                              
                                                                
                                                               
                                                                  







                                                                                                           
                                               
                                      
                                          

                                                    


                                                                                    
                                               
                                              
                                              


                                                                                  
                                                                      









                                                                                     
                                                         
                    
                                                                                                                                                                                                      
         
                                                                                                                                             







                                                                                 
                         
                         

                         




                         
                          
                          
                          

 
                                                                               


                                                         




                                                                          

                               
                                       
                                                                                                 

                                                                                                 




                                                                   
                 









                                                                                                 
                 
         
                     

 
                                                                             
















                                                                                     
                                                                           
                                                                         
                                                                         


                                                                         
                                                                                                                                                    

                                                 
                                                       













                                                                                                                   
                                                    






                                                                                                                                                 
                                                           
                                 





                                                        
                                      
                                     
                                     
         
                                   
 

                         
// Copyright 2017 The go-ethereum Authors
// This file is part of go-ethereum.
//
// go-ethereum is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// go-ethereum is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "math/rand"
    "path/filepath"
    "strconv"
    "strings"
    "text/template"

    "github.com/ethereum/go-ethereum/common"
    "github.com/ethereum/go-ethereum/log"
)

// nodeDockerfile is the Dockerfile required to run an Ethereum node.
var nodeDockerfile = `
FROM ethereum/client-go:latest

ADD genesis.json /genesis.json
{{if .Unlock}}
    ADD signer.json /signer.json
    ADD signer.pass /signer.pass
{{end}}
RUN \
  echo 'geth --cache 512 init /genesis.json' > geth.sh && \{{if .Unlock}}
    echo 'mkdir -p /root/.ethereum/keystore/ && cp /signer.json /root/.ethereum/keystore/' >> geth.sh && \{{end}}
    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

ENTRYPOINT ["/bin/sh", "geth.sh"]
`

// nodeComposefile is the docker-compose.yml file required to deploy and maintain
// an Ethereum node (bootnode or miner for now).
var nodeComposefile = `
version: '2'
services:
  {{.Type}}:
    build: .
    image: {{.Network}}/{{.Type}}
    container_name: {{.Network}}_{{.Type}}_1
    ports:
      - "{{.Port}}:{{.Port}}"
      - "{{.Port}}:{{.Port}}/udp"
    volumes:
      - {{.Datadir}}:/root/.ethereum{{if .Ethashdir}}
      - {{.Ethashdir}}:/root/.ethash{{end}}
    environment:
      - PORT={{.Port}}/tcp
      - TOTAL_PEERS={{.TotalPeers}}
      - LIGHT_PEERS={{.LightPeers}}
      - STATS_NAME={{.Ethstats}}
      - MINER_NAME={{.Etherbase}}
      - GAS_TARGET={{.GasTarget}}
      - GAS_LIMIT={{.GasLimit}}
      - GAS_PRICE={{.GasPrice}}
    logging:
      driver: "json-file"
      options:
        max-size: "1m"
        max-file: "10"
    restart: always
`

// deployNode deploys a new Ethereum node container to a remote machine via SSH,
// docker and docker-compose. If an instance with the specified network name
// already exists there, it will be overwritten!
func deployNode(client *sshClient, network string, bootnodes []string, config *nodeInfos, nocache bool) ([]byte, error) {
    kind := "sealnode"
    if config.keyJSON == "" && config.etherbase == "" {
        kind = "bootnode"
        bootnodes = make([]string, 0)
    }
    // Generate the content to upload to the server
    workdir := fmt.Sprintf("%d", rand.Int63())
    files := make(map[string][]byte)

    lightFlag := ""
    if config.peersLight > 0 {
        lightFlag = fmt.Sprintf("--lightpeers=%d --lightserv=50", config.peersLight)
    }
    dockerfile := new(bytes.Buffer)
    template.Must(template.New("").Parse(nodeDockerfile)).Execute(dockerfile, map[string]interface{}{
        "NetworkID": config.network,
        "Port":      config.port,
        "IP":        client.address,
        "Peers":     config.peersTotal,
        "LightFlag": lightFlag,
        "Bootnodes": strings.Join(bootnodes, ","),
        "Ethstats":  config.ethstats,
        "Etherbase": config.etherbase,
        "GasTarget": uint64(1000000 * config.gasTarget),
        "GasLimit":  uint64(1000000 * config.gasLimit),
        "GasPrice":  uint64(1000000000 * config.gasPrice),
        "Unlock":    config.keyJSON != "",
    })
    files[filepath.Join(workdir, "Dockerfile")] = dockerfile.Bytes()

    composefile := new(bytes.Buffer)
    template.Must(template.New("").Parse(nodeComposefile)).Execute(composefile, map[string]interface{}{
        "Type":       kind,
        "Datadir":    config.datadir,
        "Ethashdir":  config.ethashdir,
        "Network":    network,
        "Port":       config.port,
        "TotalPeers": config.peersTotal,
        "Light":      config.peersLight > 0,
        "LightPeers": config.peersLight,
        "Ethstats":   config.ethstats[:strings.Index(config.ethstats, ":")],
        "Etherbase":  config.etherbase,
        "GasTarget":  config.gasTarget,
        "GasLimit":   config.gasLimit,
        "GasPrice":   config.gasPrice,
    })
    files[filepath.Join(workdir, "docker-compose.yaml")] = composefile.Bytes()

    files[filepath.Join(workdir, "genesis.json")] = config.genesis
    if config.keyJSON != "" {
        files[filepath.Join(workdir, "signer.json")] = []byte(config.keyJSON)
        files[filepath.Join(workdir, "signer.pass")] = []byte(config.keyPass)
    }
    // Upload the deployment files to the remote server (and clean up afterwards)
    if out, err := client.Upload(files); err != nil {
        return out, err
    }
    defer client.Run("rm -rf " + workdir)

    // Build and deploy the boot or seal node service
    if nocache {
        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))
    }
    return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s up -d --build --force-recreate --timeout 60", workdir, network))
}

// nodeInfos is returned from a boot or seal node status check to allow reporting
// various configuration parameters.
type nodeInfos struct {
    genesis    []byte
    network    int64
    datadir    string
    ethashdir  string
    ethstats   string
    port       int
    enode      string
    peersTotal int
    peersLight int
    etherbase  string
    keyJSON    string
    keyPass    string
    gasTarget  float64
    gasLimit   float64
    gasPrice   float64
}

// Report converts the typed struct into a plain string->string map, containing
// most - but not all - fields for reporting to the user.
func (info *nodeInfos) Report() map[string]string {
    report := map[string]string{
        "Data directory":           info.datadir,
        "Listener port":            strconv.Itoa(info.port),
        "Peer count (all total)":   strconv.Itoa(info.peersTotal),
        "Peer count (light nodes)": strconv.Itoa(info.peersLight),
        "Ethstats username":        info.ethstats,
    }
    if info.gasTarget > 0 {
        // Miner or signer node
        report["Gas price (minimum accepted)"] = fmt.Sprintf("%0.3f GWei", info.gasPrice)
        report["Gas floor (baseline target)"] = fmt.Sprintf("%0.3f MGas", info.gasTarget)
        report["Gas ceil  (target maximum)"] = fmt.Sprintf("%0.3f MGas", info.gasLimit)

        if info.etherbase != "" {
            // Ethash proof-of-work miner
            report["Ethash directory"] = info.ethashdir
            report["Miner account"] = info.etherbase
        }
        if info.keyJSON != "" {
            // Clique proof-of-authority signer
            var key struct {
                Address string `json:"address"`
            }
            if err := json.Unmarshal([]byte(info.keyJSON), &key); err == nil {
                report["Signer account"] = common.HexToAddress(key.Address).Hex()
            } else {
                log.Error("Failed to retrieve signer address", "err", err)
            }
        }
    }
    return report
}

// checkNode does a health-check against a boot or seal node server to verify
// whether it's running, and if yes, whether it's responsive.
func checkNode(client *sshClient, network string, boot bool) (*nodeInfos, error) {
    kind := "bootnode"
    if !boot {
        kind = "sealnode"
    }
    // Inspect a possible bootnode container on the host
    infos, err := inspectContainer(client, fmt.Sprintf("%s_%s_1", network, kind))
    if err != nil {
        return nil, err
    }
    if !infos.running {
        return nil, ErrServiceOffline
    }
    // Resolve a few types from the environmental variables
    totalPeers, _ := strconv.Atoi(infos.envvars["TOTAL_PEERS"])
    lightPeers, _ := strconv.Atoi(infos.envvars["LIGHT_PEERS"])
    gasTarget, _ := strconv.ParseFloat(infos.envvars["GAS_TARGET"], 64)
    gasLimit, _ := strconv.ParseFloat(infos.envvars["GAS_LIMIT"], 64)
    gasPrice, _ := strconv.ParseFloat(infos.envvars["GAS_PRICE"], 64)

    // Container available, retrieve its node ID and its genesis json
    var out []byte
    if out, err = client.Run(fmt.Sprintf("docker exec %s_%s_1 geth --exec admin.nodeInfo.enode --cache=16 attach", network, kind)); err != nil {
        return nil, ErrServiceUnreachable
    }
    enode := bytes.Trim(bytes.TrimSpace(out), "\"")

    if out, err = client.Run(fmt.Sprintf("docker exec %s_%s_1 cat /genesis.json", network, kind)); err != nil {
        return nil, ErrServiceUnreachable
    }
    genesis := bytes.TrimSpace(out)

    keyJSON, keyPass := "", ""
    if out, err = client.Run(fmt.Sprintf("docker exec %s_%s_1 cat /signer.json", network, kind)); err == nil {
        keyJSON = string(bytes.TrimSpace(out))
    }
    if out, err = client.Run(fmt.Sprintf("docker exec %s_%s_1 cat /signer.pass", network, kind)); err == nil {
        keyPass = string(bytes.TrimSpace(out))
    }
    // Run a sanity check to see if the devp2p is reachable
    port := infos.portmap[infos.envvars["PORT"]]
    if err = checkPort(client.server, port); err != nil {
        log.Warn(fmt.Sprintf("%s devp2p port seems unreachable", strings.Title(kind)), "server", client.server, "port", port, "err", err)
    }
    // Assemble and return the useful infos
    stats := &nodeInfos{
        genesis:    genesis,
        datadir:    infos.volumes["/root/.ethereum"],
        ethashdir:  infos.volumes["/root/.ethash"],
        port:       port,
        peersTotal: totalPeers,
        peersLight: lightPeers,
        ethstats:   infos.envvars["STATS_NAME"],
        etherbase:  infos.envvars["MINER_NAME"],
        keyJSON:    keyJSON,
        keyPass:    keyPass,
        gasTarget:  gasTarget,
        gasLimit:   gasLimit,
        gasPrice:   gasPrice,
    }
    stats.enode = string(enode)

    return stats, nil
}