mirror of
https://github.com/OffchainLabs/prysm.git
synced 2026-01-09 15:37:56 -05:00
Merge branch 'master' into gitter-badge-1
Former-commit-id: 23f542f43b4b493e38f5aa4c29788ed93a63b43b [formerly 71b23a6a28eb045fcfeab6329de69f1e5455486b] Former-commit-id: d12b3a6decc876f010a71f98e11df7387c1aaf2a
This commit is contained in:
140
cmd/abigen/main.go
Normal file
140
cmd/abigen/main.go
Normal file
@@ -0,0 +1,140 @@
|
||||
// Copyright 2016 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 (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts/abi/bind"
|
||||
"github.com/ethereum/go-ethereum/common/compiler"
|
||||
)
|
||||
|
||||
var (
|
||||
abiFlag = flag.String("abi", "", "Path to the Ethereum contract ABI json to bind")
|
||||
binFlag = flag.String("bin", "", "Path to the Ethereum contract bytecode (generate deploy method)")
|
||||
typFlag = flag.String("type", "", "Struct name for the binding (default = package name)")
|
||||
|
||||
solFlag = flag.String("sol", "", "Path to the Ethereum contract Solidity source to build and bind")
|
||||
solcFlag = flag.String("solc", "solc", "Solidity compiler to use if source builds are requested")
|
||||
excFlag = flag.String("exc", "", "Comma separated types to exclude from binding")
|
||||
|
||||
pkgFlag = flag.String("pkg", "", "Package name to generate the binding into")
|
||||
outFlag = flag.String("out", "", "Output file for the generated binding (default = stdout)")
|
||||
langFlag = flag.String("lang", "go", "Destination language for the bindings (go, java, objc)")
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Parse and ensure all needed inputs are specified
|
||||
flag.Parse()
|
||||
|
||||
if *abiFlag == "" && *solFlag == "" {
|
||||
fmt.Printf("No contract ABI (--abi) or Solidity source (--sol) specified\n")
|
||||
os.Exit(-1)
|
||||
} else if (*abiFlag != "" || *binFlag != "" || *typFlag != "") && *solFlag != "" {
|
||||
fmt.Printf("Contract ABI (--abi), bytecode (--bin) and type (--type) flags are mutually exclusive with the Solidity source (--sol) flag\n")
|
||||
os.Exit(-1)
|
||||
}
|
||||
if *pkgFlag == "" {
|
||||
fmt.Printf("No destination package specified (--pkg)\n")
|
||||
os.Exit(-1)
|
||||
}
|
||||
var lang bind.Lang
|
||||
switch *langFlag {
|
||||
case "go":
|
||||
lang = bind.LangGo
|
||||
case "java":
|
||||
lang = bind.LangJava
|
||||
case "objc":
|
||||
lang = bind.LangObjC
|
||||
default:
|
||||
fmt.Printf("Unsupported destination language \"%s\" (--lang)\n", *langFlag)
|
||||
os.Exit(-1)
|
||||
}
|
||||
// If the entire solidity code was specified, build and bind based on that
|
||||
var (
|
||||
abis []string
|
||||
bins []string
|
||||
types []string
|
||||
)
|
||||
if *solFlag != "" {
|
||||
// Generate the list of types to exclude from binding
|
||||
exclude := make(map[string]bool)
|
||||
for _, kind := range strings.Split(*excFlag, ",") {
|
||||
exclude[strings.ToLower(kind)] = true
|
||||
}
|
||||
contracts, err := compiler.CompileSolidity(*solcFlag, *solFlag)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to build Solidity contract: %v\n", err)
|
||||
os.Exit(-1)
|
||||
}
|
||||
// Gather all non-excluded contract for binding
|
||||
for name, contract := range contracts {
|
||||
if exclude[strings.ToLower(name)] {
|
||||
continue
|
||||
}
|
||||
abi, _ := json.Marshal(contract.Info.AbiDefinition) // Flatten the compiler parse
|
||||
abis = append(abis, string(abi))
|
||||
bins = append(bins, contract.Code)
|
||||
|
||||
nameParts := strings.Split(name, ":")
|
||||
types = append(types, nameParts[len(nameParts)-1])
|
||||
}
|
||||
} else {
|
||||
// Otherwise load up the ABI, optional bytecode and type name from the parameters
|
||||
abi, err := ioutil.ReadFile(*abiFlag)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to read input ABI: %v\n", err)
|
||||
os.Exit(-1)
|
||||
}
|
||||
abis = append(abis, string(abi))
|
||||
|
||||
bin := []byte{}
|
||||
if *binFlag != "" {
|
||||
if bin, err = ioutil.ReadFile(*binFlag); err != nil {
|
||||
fmt.Printf("Failed to read input bytecode: %v\n", err)
|
||||
os.Exit(-1)
|
||||
}
|
||||
}
|
||||
bins = append(bins, string(bin))
|
||||
|
||||
kind := *typFlag
|
||||
if kind == "" {
|
||||
kind = *pkgFlag
|
||||
}
|
||||
types = append(types, kind)
|
||||
}
|
||||
// Generate the contract binding
|
||||
code, err := bind.Bind(types, abis, bins, *pkgFlag, lang)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to generate ABI binding: %v\n", err)
|
||||
os.Exit(-1)
|
||||
}
|
||||
// Either flush it out to a file or display on the standard output
|
||||
if *outFlag == "" {
|
||||
fmt.Printf("%s\n", code)
|
||||
return
|
||||
}
|
||||
if err := ioutil.WriteFile(*outFlag, []byte(code), 0600); err != nil {
|
||||
fmt.Printf("Failed to write ABI binding: %v\n", err)
|
||||
os.Exit(-1)
|
||||
}
|
||||
}
|
||||
@@ -19,74 +19,92 @@ package main
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"encoding/hex"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/ethereum/go-ethereum/cmd/utils"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/ethereum/go-ethereum/logger"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/p2p/discover"
|
||||
"github.com/ethereum/go-ethereum/p2p/discv5"
|
||||
"github.com/ethereum/go-ethereum/p2p/nat"
|
||||
"github.com/ethereum/go-ethereum/p2p/netutil"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var (
|
||||
listenAddr = flag.String("addr", ":30301", "listen address")
|
||||
genKey = flag.String("genkey", "", "generate a node key and quit")
|
||||
genKey = flag.String("genkey", "", "generate a node key")
|
||||
writeAddr = flag.Bool("writeaddress", false, "write out the node's pubkey hash and quit")
|
||||
nodeKeyFile = flag.String("nodekey", "", "private key filename")
|
||||
nodeKeyHex = flag.String("nodekeyhex", "", "private key as hex (for testing)")
|
||||
natdesc = flag.String("nat", "none", "port mapping mechanism (any|none|upnp|pmp|extip:<IP>)")
|
||||
netrestrict = flag.String("netrestrict", "", "restrict network communication to the given IP networks (CIDR masks)")
|
||||
runv5 = flag.Bool("v5", false, "run a v5 topic discovery bootnode")
|
||||
verbosity = flag.Int("verbosity", int(log.LvlInfo), "log verbosity (0-9)")
|
||||
vmodule = flag.String("vmodule", "", "log verbosity pattern")
|
||||
|
||||
nodeKey *ecdsa.PrivateKey
|
||||
err error
|
||||
)
|
||||
flag.Parse()
|
||||
logger.AddLogSystem(logger.NewStdLogSystem(os.Stdout, log.LstdFlags, logger.DebugLevel))
|
||||
|
||||
if *genKey != "" {
|
||||
writeKey(*genKey)
|
||||
os.Exit(0)
|
||||
}
|
||||
glogger := log.NewGlogHandler(log.StreamHandler(os.Stderr, log.TerminalFormat(false)))
|
||||
glogger.Verbosity(log.Lvl(*verbosity))
|
||||
glogger.Vmodule(*vmodule)
|
||||
log.Root().SetHandler(glogger)
|
||||
|
||||
natm, err := nat.Parse(*natdesc)
|
||||
if err != nil {
|
||||
log.Fatalf("-nat: %v", err)
|
||||
utils.Fatalf("-nat: %v", err)
|
||||
}
|
||||
switch {
|
||||
case *genKey != "":
|
||||
nodeKey, err = crypto.GenerateKey()
|
||||
if err != nil {
|
||||
utils.Fatalf("could not generate key: %v", err)
|
||||
}
|
||||
if err = crypto.SaveECDSA(*genKey, nodeKey); err != nil {
|
||||
utils.Fatalf("%v", err)
|
||||
}
|
||||
return
|
||||
case *nodeKeyFile == "" && *nodeKeyHex == "":
|
||||
log.Fatal("Use -nodekey or -nodekeyhex to specify a private key")
|
||||
utils.Fatalf("Use -nodekey or -nodekeyhex to specify a private key")
|
||||
case *nodeKeyFile != "" && *nodeKeyHex != "":
|
||||
log.Fatal("Options -nodekey and -nodekeyhex are mutually exclusive")
|
||||
utils.Fatalf("Options -nodekey and -nodekeyhex are mutually exclusive")
|
||||
case *nodeKeyFile != "":
|
||||
if nodeKey, err = crypto.LoadECDSA(*nodeKeyFile); err != nil {
|
||||
log.Fatalf("-nodekey: %v", err)
|
||||
utils.Fatalf("-nodekey: %v", err)
|
||||
}
|
||||
case *nodeKeyHex != "":
|
||||
if nodeKey, err = crypto.HexToECDSA(*nodeKeyHex); err != nil {
|
||||
log.Fatalf("-nodekeyhex: %v", err)
|
||||
utils.Fatalf("-nodekeyhex: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := discover.ListenUDP(nodeKey, *listenAddr, natm, ""); err != nil {
|
||||
log.Fatal(err)
|
||||
if *writeAddr {
|
||||
fmt.Printf("%v\n", discover.PubkeyID(&nodeKey.PublicKey))
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
var restrictList *netutil.Netlist
|
||||
if *netrestrict != "" {
|
||||
restrictList, err = netutil.ParseNetlist(*netrestrict)
|
||||
if err != nil {
|
||||
utils.Fatalf("-netrestrict: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if *runv5 {
|
||||
if _, err := discv5.ListenUDP(nodeKey, *listenAddr, natm, "", restrictList); err != nil {
|
||||
utils.Fatalf("%v", err)
|
||||
}
|
||||
} else {
|
||||
if _, err := discover.ListenUDP(nodeKey, *listenAddr, natm, "", restrictList); err != nil {
|
||||
utils.Fatalf("%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
select {}
|
||||
}
|
||||
|
||||
func writeKey(target string) {
|
||||
key, err := crypto.GenerateKey()
|
||||
if err != nil {
|
||||
log.Fatal("could not generate key: %v", err)
|
||||
}
|
||||
b := crypto.FromECDSA(key)
|
||||
if target == "-" {
|
||||
fmt.Println(hex.EncodeToString(b))
|
||||
} else {
|
||||
if err := ioutil.WriteFile(target, b, 0600); err != nil {
|
||||
log.Fatal("write error: ", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
// Copyright 2015 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/>.
|
||||
|
||||
// disasm is a pretty-printer for EVM bytecode.
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/vm"
|
||||
)
|
||||
|
||||
func main() {
|
||||
code, err := ioutil.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
code = common.Hex2Bytes(string(code[:len(code)-1]))
|
||||
fmt.Printf("%x\n", code)
|
||||
|
||||
for pc := uint64(0); pc < uint64(len(code)); pc++ {
|
||||
op := vm.OpCode(code[pc])
|
||||
fmt.Printf("%-5d %v", pc, op)
|
||||
|
||||
switch op {
|
||||
case vm.PUSH1, vm.PUSH2, vm.PUSH3, vm.PUSH4, vm.PUSH5, vm.PUSH6, vm.PUSH7, vm.PUSH8, vm.PUSH9, vm.PUSH10, vm.PUSH11, vm.PUSH12, vm.PUSH13, vm.PUSH14, vm.PUSH15, vm.PUSH16, vm.PUSH17, vm.PUSH18, vm.PUSH19, vm.PUSH20, vm.PUSH21, vm.PUSH22, vm.PUSH23, vm.PUSH24, vm.PUSH25, vm.PUSH26, vm.PUSH27, vm.PUSH28, vm.PUSH29, vm.PUSH30, vm.PUSH31, vm.PUSH32:
|
||||
a := uint64(op) - uint64(vm.PUSH1) + 1
|
||||
fmt.Printf(" => %x", code[pc+1:pc+1+a])
|
||||
|
||||
pc += a
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
}
|
||||
41
cmd/ethkey/README.md
Normal file
41
cmd/ethkey/README.md
Normal file
@@ -0,0 +1,41 @@
|
||||
ethkey
|
||||
======
|
||||
|
||||
ethkey is a simple command-line tool for working with Ethereum keyfiles.
|
||||
|
||||
|
||||
# Usage
|
||||
|
||||
### `ethkey generate`
|
||||
|
||||
Generate a new keyfile.
|
||||
If you want to use an existing private key to use in the keyfile, it can be
|
||||
specified by setting `--privatekey` with the location of the file containing the
|
||||
private key.
|
||||
|
||||
|
||||
### `ethkey inspect <keyfile>`
|
||||
|
||||
Print various information about the keyfile.
|
||||
Private key information can be printed by using the `--private` flag;
|
||||
make sure to use this feature with great caution!
|
||||
|
||||
|
||||
### `ethkey sign <keyfile> <message/file>`
|
||||
|
||||
Sign the message with a keyfile.
|
||||
It is possible to refer to a file containing the message.
|
||||
|
||||
|
||||
### `ethkey verify <address> <signature> <message/file>`
|
||||
|
||||
Verify the signature of the message.
|
||||
It is possible to refer to a file containing the message.
|
||||
|
||||
|
||||
## Passphrases
|
||||
|
||||
For every command that uses a keyfile, you will be prompted to provide the
|
||||
passphrase for decrypting the keyfile. To avoid this message, it is possible
|
||||
to pass the passphrase by using the `--passphrase` flag pointing to a file that
|
||||
contains the passphrase.
|
||||
117
cmd/ethkey/generate.go
Normal file
117
cmd/ethkey/generate.go
Normal file
@@ -0,0 +1,117 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts/keystore"
|
||||
"github.com/ethereum/go-ethereum/cmd/utils"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/pborman/uuid"
|
||||
"gopkg.in/urfave/cli.v1"
|
||||
)
|
||||
|
||||
type outputGenerate struct {
|
||||
Address string
|
||||
AddressEIP55 string
|
||||
}
|
||||
|
||||
var commandGenerate = cli.Command{
|
||||
Name: "generate",
|
||||
Usage: "generate new keyfile",
|
||||
ArgsUsage: "[ <keyfile> ]",
|
||||
Description: `
|
||||
Generate a new keyfile.
|
||||
If you want to use an existing private key to use in the keyfile, it can be
|
||||
specified by setting --privatekey with the location of the file containing the
|
||||
private key.`,
|
||||
Flags: []cli.Flag{
|
||||
passphraseFlag,
|
||||
jsonFlag,
|
||||
cli.StringFlag{
|
||||
Name: "privatekey",
|
||||
Usage: "the file from where to read the private key to " +
|
||||
"generate a keyfile for",
|
||||
},
|
||||
},
|
||||
Action: func(ctx *cli.Context) error {
|
||||
// Check if keyfile path given and make sure it doesn't already exist.
|
||||
keyfilepath := ctx.Args().First()
|
||||
if keyfilepath == "" {
|
||||
keyfilepath = defaultKeyfileName
|
||||
}
|
||||
if _, err := os.Stat(keyfilepath); err == nil {
|
||||
utils.Fatalf("Keyfile already exists at %s.", keyfilepath)
|
||||
} else if !os.IsNotExist(err) {
|
||||
utils.Fatalf("Error checking if keyfile exists: %v", err)
|
||||
}
|
||||
|
||||
var privateKey *ecdsa.PrivateKey
|
||||
|
||||
// First check if a private key file is provided.
|
||||
privateKeyFile := ctx.String("privatekey")
|
||||
if privateKeyFile != "" {
|
||||
privateKeyBytes, err := ioutil.ReadFile(privateKeyFile)
|
||||
if err != nil {
|
||||
utils.Fatalf("Failed to read the private key file '%s': %v",
|
||||
privateKeyFile, err)
|
||||
}
|
||||
|
||||
pk, err := crypto.HexToECDSA(string(privateKeyBytes))
|
||||
if err != nil {
|
||||
utils.Fatalf(
|
||||
"Could not construct ECDSA private key from file content: %v",
|
||||
err)
|
||||
}
|
||||
privateKey = pk
|
||||
}
|
||||
|
||||
// If not loaded, generate random.
|
||||
if privateKey == nil {
|
||||
pk, err := ecdsa.GenerateKey(crypto.S256(), rand.Reader)
|
||||
if err != nil {
|
||||
utils.Fatalf("Failed to generate random private key: %v", err)
|
||||
}
|
||||
privateKey = pk
|
||||
}
|
||||
|
||||
// Create the keyfile object with a random UUID.
|
||||
id := uuid.NewRandom()
|
||||
key := &keystore.Key{
|
||||
Id: id,
|
||||
Address: crypto.PubkeyToAddress(privateKey.PublicKey),
|
||||
PrivateKey: privateKey,
|
||||
}
|
||||
|
||||
// Encrypt key with passphrase.
|
||||
passphrase := getPassPhrase(ctx, true)
|
||||
keyjson, err := keystore.EncryptKey(key, passphrase,
|
||||
keystore.StandardScryptN, keystore.StandardScryptP)
|
||||
if err != nil {
|
||||
utils.Fatalf("Error encrypting key: %v", err)
|
||||
}
|
||||
|
||||
// Store the file to disk.
|
||||
if err := os.MkdirAll(filepath.Dir(keyfilepath), 0700); err != nil {
|
||||
utils.Fatalf("Could not create directory %s", filepath.Dir(keyfilepath))
|
||||
}
|
||||
if err := ioutil.WriteFile(keyfilepath, keyjson, 0600); err != nil {
|
||||
utils.Fatalf("Failed to write keyfile to %s: %v", keyfilepath, err)
|
||||
}
|
||||
|
||||
// Output some information.
|
||||
out := outputGenerate{
|
||||
Address: key.Address.Hex(),
|
||||
}
|
||||
if ctx.Bool(jsonFlag.Name) {
|
||||
mustPrintJSON(out)
|
||||
} else {
|
||||
fmt.Println("Address: ", out.Address)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
74
cmd/ethkey/inspect.go
Normal file
74
cmd/ethkey/inspect.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts/keystore"
|
||||
"github.com/ethereum/go-ethereum/cmd/utils"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"gopkg.in/urfave/cli.v1"
|
||||
)
|
||||
|
||||
type outputInspect struct {
|
||||
Address string
|
||||
PublicKey string
|
||||
PrivateKey string
|
||||
}
|
||||
|
||||
var commandInspect = cli.Command{
|
||||
Name: "inspect",
|
||||
Usage: "inspect a keyfile",
|
||||
ArgsUsage: "<keyfile>",
|
||||
Description: `
|
||||
Print various information about the keyfile.
|
||||
Private key information can be printed by using the --private flag;
|
||||
make sure to use this feature with great caution!`,
|
||||
Flags: []cli.Flag{
|
||||
passphraseFlag,
|
||||
jsonFlag,
|
||||
cli.BoolFlag{
|
||||
Name: "private",
|
||||
Usage: "include the private key in the output",
|
||||
},
|
||||
},
|
||||
Action: func(ctx *cli.Context) error {
|
||||
keyfilepath := ctx.Args().First()
|
||||
|
||||
// Read key from file.
|
||||
keyjson, err := ioutil.ReadFile(keyfilepath)
|
||||
if err != nil {
|
||||
utils.Fatalf("Failed to read the keyfile at '%s': %v", keyfilepath, err)
|
||||
}
|
||||
|
||||
// Decrypt key with passphrase.
|
||||
passphrase := getPassPhrase(ctx, false)
|
||||
key, err := keystore.DecryptKey(keyjson, passphrase)
|
||||
if err != nil {
|
||||
utils.Fatalf("Error decrypting key: %v", err)
|
||||
}
|
||||
|
||||
// Output all relevant information we can retrieve.
|
||||
showPrivate := ctx.Bool("private")
|
||||
out := outputInspect{
|
||||
Address: key.Address.Hex(),
|
||||
PublicKey: hex.EncodeToString(
|
||||
crypto.FromECDSAPub(&key.PrivateKey.PublicKey)),
|
||||
}
|
||||
if showPrivate {
|
||||
out.PrivateKey = hex.EncodeToString(crypto.FromECDSA(key.PrivateKey))
|
||||
}
|
||||
|
||||
if ctx.Bool(jsonFlag.Name) {
|
||||
mustPrintJSON(out)
|
||||
} else {
|
||||
fmt.Println("Address: ", out.Address)
|
||||
fmt.Println("Public key: ", out.PublicKey)
|
||||
if showPrivate {
|
||||
fmt.Println("Private key: ", out.PrivateKey)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
70
cmd/ethkey/main.go
Normal file
70
cmd/ethkey/main.go
Normal file
@@ -0,0 +1,70 @@
|
||||
// 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 (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/ethereum/go-ethereum/cmd/utils"
|
||||
"gopkg.in/urfave/cli.v1"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultKeyfileName = "keyfile.json"
|
||||
)
|
||||
|
||||
var (
|
||||
gitCommit = "" // Git SHA1 commit hash of the release (set via linker flags)
|
||||
|
||||
app *cli.App // the main app instance
|
||||
)
|
||||
|
||||
var ( // Commonly used command line flags.
|
||||
passphraseFlag = cli.StringFlag{
|
||||
Name: "passwordfile",
|
||||
Usage: "the file that contains the passphrase for the keyfile",
|
||||
}
|
||||
|
||||
jsonFlag = cli.BoolFlag{
|
||||
Name: "json",
|
||||
Usage: "output JSON instead of human-readable format",
|
||||
}
|
||||
|
||||
messageFlag = cli.StringFlag{
|
||||
Name: "message",
|
||||
Usage: "the file that contains the message to sign/verify",
|
||||
}
|
||||
)
|
||||
|
||||
// Configure the app instance.
|
||||
func init() {
|
||||
app = utils.NewApp(gitCommit, "an Ethereum key manager")
|
||||
app.Commands = []cli.Command{
|
||||
commandGenerate,
|
||||
commandInspect,
|
||||
commandSignMessage,
|
||||
commandVerifyMessage,
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
if err := app.Run(os.Args); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
148
cmd/ethkey/message.go
Normal file
148
cmd/ethkey/message.go
Normal file
@@ -0,0 +1,148 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts/keystore"
|
||||
"github.com/ethereum/go-ethereum/cmd/utils"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"gopkg.in/urfave/cli.v1"
|
||||
)
|
||||
|
||||
type outputSign struct {
|
||||
Signature string
|
||||
}
|
||||
|
||||
var commandSignMessage = cli.Command{
|
||||
Name: "signmessage",
|
||||
Usage: "sign a message",
|
||||
ArgsUsage: "<keyfile> <message/file>",
|
||||
Description: `
|
||||
Sign the message with a keyfile.
|
||||
It is possible to refer to a file containing the message.`,
|
||||
Flags: []cli.Flag{
|
||||
passphraseFlag,
|
||||
jsonFlag,
|
||||
},
|
||||
Action: func(ctx *cli.Context) error {
|
||||
keyfilepath := ctx.Args().First()
|
||||
message := []byte(ctx.Args().Get(1))
|
||||
|
||||
// Load the keyfile.
|
||||
keyjson, err := ioutil.ReadFile(keyfilepath)
|
||||
if err != nil {
|
||||
utils.Fatalf("Failed to read the keyfile at '%s': %v",
|
||||
keyfilepath, err)
|
||||
}
|
||||
|
||||
// Decrypt key with passphrase.
|
||||
passphrase := getPassPhrase(ctx, false)
|
||||
key, err := keystore.DecryptKey(keyjson, passphrase)
|
||||
if err != nil {
|
||||
utils.Fatalf("Error decrypting key: %v", err)
|
||||
}
|
||||
|
||||
if len(message) == 0 {
|
||||
utils.Fatalf("A message must be provided")
|
||||
}
|
||||
// Read message if file.
|
||||
if _, err := os.Stat(string(message)); err == nil {
|
||||
message, err = ioutil.ReadFile(string(message))
|
||||
if err != nil {
|
||||
utils.Fatalf("Failed to read the message file: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
signature, err := crypto.Sign(signHash(message), key.PrivateKey)
|
||||
if err != nil {
|
||||
utils.Fatalf("Failed to sign message: %v", err)
|
||||
}
|
||||
|
||||
out := outputSign{
|
||||
Signature: hex.EncodeToString(signature),
|
||||
}
|
||||
if ctx.Bool(jsonFlag.Name) {
|
||||
mustPrintJSON(out)
|
||||
} else {
|
||||
fmt.Println("Signature: ", out.Signature)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
type outputVerify struct {
|
||||
Success bool
|
||||
RecoveredAddress string
|
||||
RecoveredPublicKey string
|
||||
}
|
||||
|
||||
var commandVerifyMessage = cli.Command{
|
||||
Name: "verifymessage",
|
||||
Usage: "verify the signature of a signed message",
|
||||
ArgsUsage: "<address> <signature> <message/file>",
|
||||
Description: `
|
||||
Verify the signature of the message.
|
||||
It is possible to refer to a file containing the message.`,
|
||||
Flags: []cli.Flag{
|
||||
jsonFlag,
|
||||
},
|
||||
Action: func(ctx *cli.Context) error {
|
||||
addressStr := ctx.Args().First()
|
||||
signatureHex := ctx.Args().Get(1)
|
||||
message := []byte(ctx.Args().Get(2))
|
||||
|
||||
// Determine whether it is a keyfile, public key or address.
|
||||
if !common.IsHexAddress(addressStr) {
|
||||
utils.Fatalf("Invalid address: %s", addressStr)
|
||||
}
|
||||
address := common.HexToAddress(addressStr)
|
||||
|
||||
signature, err := hex.DecodeString(signatureHex)
|
||||
if err != nil {
|
||||
utils.Fatalf("Signature encoding is not hexadecimal: %v", err)
|
||||
}
|
||||
|
||||
if len(message) == 0 {
|
||||
utils.Fatalf("A message must be provided")
|
||||
}
|
||||
// Read message if file.
|
||||
if _, err := os.Stat(string(message)); err == nil {
|
||||
message, err = ioutil.ReadFile(string(message))
|
||||
if err != nil {
|
||||
utils.Fatalf("Failed to read the message file: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
recoveredPubkey, err := crypto.SigToPub(signHash(message), signature)
|
||||
if err != nil || recoveredPubkey == nil {
|
||||
utils.Fatalf("Signature verification failed: %v", err)
|
||||
}
|
||||
recoveredPubkeyBytes := crypto.FromECDSAPub(recoveredPubkey)
|
||||
recoveredAddress := crypto.PubkeyToAddress(*recoveredPubkey)
|
||||
|
||||
success := address == recoveredAddress
|
||||
|
||||
out := outputVerify{
|
||||
Success: success,
|
||||
RecoveredPublicKey: hex.EncodeToString(recoveredPubkeyBytes),
|
||||
RecoveredAddress: strings.ToLower(recoveredAddress.Hex()),
|
||||
}
|
||||
if ctx.Bool(jsonFlag.Name) {
|
||||
mustPrintJSON(out)
|
||||
} else {
|
||||
if out.Success {
|
||||
fmt.Println("Signature verification successful!")
|
||||
} else {
|
||||
fmt.Println("Signature verification failed!")
|
||||
}
|
||||
fmt.Println("Recovered public key: ", out.RecoveredPublicKey)
|
||||
fmt.Println("Recovered address: ", out.RecoveredAddress)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
83
cmd/ethkey/utils.go
Normal file
83
cmd/ethkey/utils.go
Normal file
@@ -0,0 +1,83 @@
|
||||
// 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 (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
|
||||
"github.com/ethereum/go-ethereum/cmd/utils"
|
||||
"github.com/ethereum/go-ethereum/console"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"gopkg.in/urfave/cli.v1"
|
||||
)
|
||||
|
||||
// getPassPhrase obtains a passphrase given by the user. It first checks the
|
||||
// --passphrase command line flag and ultimately prompts the user for a
|
||||
// passphrase.
|
||||
func getPassPhrase(ctx *cli.Context, confirmation bool) string {
|
||||
// Look for the --passphrase flag.
|
||||
passphraseFile := ctx.String(passphraseFlag.Name)
|
||||
if passphraseFile != "" {
|
||||
content, err := ioutil.ReadFile(passphraseFile)
|
||||
if err != nil {
|
||||
utils.Fatalf("Failed to read passphrase file '%s': %v",
|
||||
passphraseFile, err)
|
||||
}
|
||||
return strings.TrimRight(string(content), "\r\n")
|
||||
}
|
||||
|
||||
// Otherwise prompt the user for the passphrase.
|
||||
passphrase, err := console.Stdin.PromptPassword("Passphrase: ")
|
||||
if err != nil {
|
||||
utils.Fatalf("Failed to read passphrase: %v", err)
|
||||
}
|
||||
if confirmation {
|
||||
confirm, err := console.Stdin.PromptPassword("Repeat passphrase: ")
|
||||
if err != nil {
|
||||
utils.Fatalf("Failed to read passphrase confirmation: %v", err)
|
||||
}
|
||||
if passphrase != confirm {
|
||||
utils.Fatalf("Passphrases do not match")
|
||||
}
|
||||
}
|
||||
return passphrase
|
||||
}
|
||||
|
||||
// signHash is a helper function that calculates a hash for the given message
|
||||
// that can be safely used to calculate a signature from.
|
||||
//
|
||||
// The hash is calulcated as
|
||||
// keccak256("\x19Ethereum Signed Message:\n"${message length}${message}).
|
||||
//
|
||||
// This gives context to the signed message and prevents signing of transactions.
|
||||
func signHash(data []byte) []byte {
|
||||
msg := fmt.Sprintf("\x19Ethereum Signed Message:\n%d%s", len(data), data)
|
||||
return crypto.Keccak256([]byte(msg))
|
||||
}
|
||||
|
||||
// mustPrintJSON prints the JSON encoding of the given object and
|
||||
// exits the program with an error message when the marshaling fails.
|
||||
func mustPrintJSON(jsonObject interface{}) {
|
||||
str, err := json.MarshalIndent(jsonObject, "", " ")
|
||||
if err != nil {
|
||||
utils.Fatalf("Failed to marshal JSON object: %v", err)
|
||||
}
|
||||
fmt.Println(string(str))
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"directory": "example/js/",
|
||||
"cwd": "./",
|
||||
"analytics": false
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
18
cmd/ethtest/.gitignore
vendored
18
cmd/ethtest/.gitignore
vendored
@@ -1,18 +0,0 @@
|
||||
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
||||
#
|
||||
# If you find yourself ignoring temporary files generated by your text editor
|
||||
# or operating system, you probably want to add a global ignore instead:
|
||||
# git config --global core.excludesfile ~/.gitignore_global
|
||||
|
||||
*.swp
|
||||
/tmp
|
||||
*/**/*un~
|
||||
*un~
|
||||
.DS_Store
|
||||
*/**/.DS_Store
|
||||
ethereum/ethereum
|
||||
ethereal/ethereal
|
||||
example/js
|
||||
node_modules
|
||||
bower_components
|
||||
npm-debug.log
|
||||
@@ -1,50 +0,0 @@
|
||||
{
|
||||
"predef": [
|
||||
"console",
|
||||
"require",
|
||||
"equal",
|
||||
"test",
|
||||
"testBoth",
|
||||
"testWithDefault",
|
||||
"raises",
|
||||
"deepEqual",
|
||||
"start",
|
||||
"stop",
|
||||
"ok",
|
||||
"strictEqual",
|
||||
"module",
|
||||
"expect",
|
||||
"reject",
|
||||
"impl"
|
||||
],
|
||||
|
||||
"esnext": true,
|
||||
"proto": true,
|
||||
"node" : true,
|
||||
"browser" : true,
|
||||
"browserify" : true,
|
||||
|
||||
"boss" : true,
|
||||
"curly": false,
|
||||
"debug": true,
|
||||
"devel": true,
|
||||
"eqeqeq": true,
|
||||
"evil": true,
|
||||
"forin": false,
|
||||
"immed": false,
|
||||
"laxbreak": false,
|
||||
"newcap": true,
|
||||
"noarg": true,
|
||||
"noempty": false,
|
||||
"nonew": false,
|
||||
"nomen": false,
|
||||
"onevar": false,
|
||||
"plusplus": false,
|
||||
"regexp": false,
|
||||
"undef": true,
|
||||
"sub": true,
|
||||
"strict": false,
|
||||
"white": false,
|
||||
"shadow": true,
|
||||
"eqnull": true
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
example/js
|
||||
node_modules
|
||||
test
|
||||
.gitignore
|
||||
.editorconfig
|
||||
.travis.yml
|
||||
.npmignore
|
||||
component.json
|
||||
testling.html
|
||||
@@ -1,11 +0,0 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- "0.11"
|
||||
- "0.10"
|
||||
before_script:
|
||||
- npm install
|
||||
- npm install jshint
|
||||
script:
|
||||
- "jshint *.js lib"
|
||||
after_script:
|
||||
- npm run-script gulp
|
||||
@@ -1,220 +0,0 @@
|
||||
// Copyright 2014 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/>.
|
||||
|
||||
// ethtest executes Ethereum JSON tests.
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/codegangsta/cli"
|
||||
"github.com/ethereum/go-ethereum/logger/glog"
|
||||
"github.com/ethereum/go-ethereum/tests"
|
||||
)
|
||||
|
||||
var (
|
||||
continueOnError = false
|
||||
testExtension = ".json"
|
||||
defaultTest = "all"
|
||||
defaultDir = "."
|
||||
allTests = []string{"BlockTests", "StateTests", "TransactionTests", "VMTests", "RLPTests"}
|
||||
testDirMapping = map[string]string{"BlockTests": "BlockchainTests"}
|
||||
skipTests = []string{}
|
||||
|
||||
TestFlag = cli.StringFlag{
|
||||
Name: "test",
|
||||
Usage: "Test type (string): VMTests, TransactionTests, StateTests, BlockTests",
|
||||
Value: defaultTest,
|
||||
}
|
||||
FileFlag = cli.StringFlag{
|
||||
Name: "file",
|
||||
Usage: "Test file or directory. Directories are searched for .json files 1 level deep",
|
||||
Value: defaultDir,
|
||||
EnvVar: "ETHEREUM_TEST_PATH",
|
||||
}
|
||||
ContinueOnErrorFlag = cli.BoolFlag{
|
||||
Name: "continue",
|
||||
Usage: "Continue running tests on error (true) or [default] exit immediately (false)",
|
||||
}
|
||||
ReadStdInFlag = cli.BoolFlag{
|
||||
Name: "stdin",
|
||||
Usage: "Accept input from stdin instead of reading from file",
|
||||
}
|
||||
SkipTestsFlag = cli.StringFlag{
|
||||
Name: "skip",
|
||||
Usage: "Tests names to skip",
|
||||
}
|
||||
)
|
||||
|
||||
func runTestWithReader(test string, r io.Reader) error {
|
||||
glog.Infoln("runTest", test)
|
||||
var err error
|
||||
switch strings.ToLower(test) {
|
||||
case "bk", "block", "blocktest", "blockchaintest", "blocktests", "blockchaintests":
|
||||
err = tests.RunBlockTestWithReader(r, skipTests)
|
||||
case "st", "state", "statetest", "statetests":
|
||||
err = tests.RunStateTestWithReader(r, skipTests)
|
||||
case "tx", "transactiontest", "transactiontests":
|
||||
err = tests.RunTransactionTestsWithReader(r, skipTests)
|
||||
case "vm", "vmtest", "vmtests":
|
||||
err = tests.RunVmTestWithReader(r, skipTests)
|
||||
case "rlp", "rlptest", "rlptests":
|
||||
err = tests.RunRLPTestWithReader(r, skipTests)
|
||||
default:
|
||||
err = fmt.Errorf("Invalid test type specified: %v", test)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getFiles(path string) ([]string, error) {
|
||||
glog.Infoln("getFiles", path)
|
||||
var files []string
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
fi, err := f.Stat()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch mode := fi.Mode(); {
|
||||
case mode.IsDir():
|
||||
fi, _ := ioutil.ReadDir(path)
|
||||
files = make([]string, len(fi))
|
||||
for i, v := range fi {
|
||||
// only go 1 depth and leave directory entires blank
|
||||
if !v.IsDir() && v.Name()[len(v.Name())-len(testExtension):len(v.Name())] == testExtension {
|
||||
files[i] = filepath.Join(path, v.Name())
|
||||
glog.Infoln("Found file", files[i])
|
||||
}
|
||||
}
|
||||
case mode.IsRegular():
|
||||
files = make([]string, 1)
|
||||
files[0] = path
|
||||
}
|
||||
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func runSuite(test, file string) {
|
||||
var tests []string
|
||||
|
||||
if test == defaultTest {
|
||||
tests = allTests
|
||||
} else {
|
||||
tests = []string{test}
|
||||
}
|
||||
|
||||
for _, curTest := range tests {
|
||||
glog.Infoln("runSuite", curTest, file)
|
||||
var err error
|
||||
var files []string
|
||||
if test == defaultTest {
|
||||
// check if we have an explicit directory mapping for the test
|
||||
if _, ok := testDirMapping[curTest]; ok {
|
||||
files, err = getFiles(filepath.Join(file, testDirMapping[curTest]))
|
||||
} else {
|
||||
// otherwise assume test name
|
||||
files, err = getFiles(filepath.Join(file, curTest))
|
||||
}
|
||||
} else {
|
||||
files, err = getFiles(file)
|
||||
}
|
||||
if err != nil {
|
||||
glog.Fatalln(err)
|
||||
}
|
||||
|
||||
if len(files) == 0 {
|
||||
glog.Warningln("No files matched path")
|
||||
}
|
||||
for _, curFile := range files {
|
||||
// Skip blank entries
|
||||
if len(curFile) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
r, err := os.Open(curFile)
|
||||
if err != nil {
|
||||
glog.Fatalln(err)
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
err = runTestWithReader(curTest, r)
|
||||
if err != nil {
|
||||
if continueOnError {
|
||||
glog.Errorln(err)
|
||||
} else {
|
||||
glog.Fatalln(err)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func setupApp(c *cli.Context) {
|
||||
flagTest := c.GlobalString(TestFlag.Name)
|
||||
flagFile := c.GlobalString(FileFlag.Name)
|
||||
continueOnError = c.GlobalBool(ContinueOnErrorFlag.Name)
|
||||
useStdIn := c.GlobalBool(ReadStdInFlag.Name)
|
||||
skipTests = strings.Split(c.GlobalString(SkipTestsFlag.Name), " ")
|
||||
|
||||
if !useStdIn {
|
||||
runSuite(flagTest, flagFile)
|
||||
} else {
|
||||
if err := runTestWithReader(flagTest, os.Stdin); err != nil {
|
||||
glog.Fatalln(err)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
glog.SetToStderr(true)
|
||||
|
||||
app := cli.NewApp()
|
||||
app.Name = "ethtest"
|
||||
app.Usage = "go-ethereum test interface"
|
||||
app.Action = setupApp
|
||||
app.Version = "0.2.0"
|
||||
app.Author = "go-ethereum team"
|
||||
|
||||
app.Flags = []cli.Flag{
|
||||
TestFlag,
|
||||
FileFlag,
|
||||
ContinueOnErrorFlag,
|
||||
ReadStdInFlag,
|
||||
SkipTestsFlag,
|
||||
}
|
||||
|
||||
if err := app.Run(os.Args); err != nil {
|
||||
glog.Fatalln(err)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
60006102ff5360003560001a60008114156103395760013560405260216040516020025990590160009052606052604051602002816060513760405160200281019050506002604051121561005957604051602002606051f35b604051602002599059016000905260a052600060c052604051602002599059016000905260e0526000610100526001610120525b604051610120511215610109576060515161012051602002606051015112156100d8576101205160200260605101516101005160200260e051015260016101005101610100526100f9565b61012051602002606051015160c05160200260a0510152600160c0510160c0525b600161012051016101205261008d565b60216020599059016000905260c051808252806020028301925050602082015990590160009052600081538151600182015260218101825160200260a0518260005b8381101561016657808301518186015260208101905061014b565b50505050825160200281019050604059905901600090526102405281610240515283602061024051015261024051905090509050905060c05160200280599059016000905281816020850151855160003060195a03f1508090509050905060a05260216020599059016000905261010051808252806020028301925050602082015990590160009052600081538151600182015260218101825160200260e0518260005b8381101561022557808301518186015260208101905061020a565b50505050825160200281019050604059905901600090526102c052816102c051528360206102c05101526102c05190509050905090506101005160200280599059016000905281816020850151855160003060195a03f1508090509050905060e05260405160200259905901600090526102e0526000610120525b610100516101205112156102d7576101205160200260e0510151610120516020026102e051015260016101205101610120526102a0565b60605151610100516020026102e05101526000610120525b60c05161012051121561032d576101205160200260a05101516101205160016101005101016020026102e051015260016101205101610120526102ef565b6040516020026102e051f35b50
|
||||
55
cmd/evm/compiler.go
Normal file
55
cmd/evm/compiler.go
Normal file
@@ -0,0 +1,55 @@
|
||||
// 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 (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/ethereum/go-ethereum/cmd/evm/internal/compiler"
|
||||
|
||||
cli "gopkg.in/urfave/cli.v1"
|
||||
)
|
||||
|
||||
var compileCommand = cli.Command{
|
||||
Action: compileCmd,
|
||||
Name: "compile",
|
||||
Usage: "compiles easm source to evm binary",
|
||||
ArgsUsage: "<file>",
|
||||
}
|
||||
|
||||
func compileCmd(ctx *cli.Context) error {
|
||||
debug := ctx.GlobalBool(DebugFlag.Name)
|
||||
|
||||
if len(ctx.Args().First()) == 0 {
|
||||
return errors.New("filename required")
|
||||
}
|
||||
|
||||
fn := ctx.Args().First()
|
||||
src, err := ioutil.ReadFile(fn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bin, err := compiler.Compile(fn, src, debug)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(bin)
|
||||
return nil
|
||||
}
|
||||
50
cmd/evm/disasm.go
Normal file
50
cmd/evm/disasm.go
Normal file
@@ -0,0 +1,50 @@
|
||||
// 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 (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
|
||||
"github.com/ethereum/go-ethereum/core/asm"
|
||||
cli "gopkg.in/urfave/cli.v1"
|
||||
)
|
||||
|
||||
var disasmCommand = cli.Command{
|
||||
Action: disasmCmd,
|
||||
Name: "disasm",
|
||||
Usage: "disassembles evm binary",
|
||||
ArgsUsage: "<file>",
|
||||
}
|
||||
|
||||
func disasmCmd(ctx *cli.Context) error {
|
||||
if len(ctx.Args().First()) == 0 {
|
||||
return errors.New("filename required")
|
||||
}
|
||||
|
||||
fn := ctx.Args().First()
|
||||
in, err := ioutil.ReadFile(fn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
code := strings.TrimSpace(string(in[:]))
|
||||
fmt.Printf("%v\n", code)
|
||||
return asm.PrintDisassembled(code)
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
39
cmd/evm/internal/compiler/compiler.go
Normal file
39
cmd/evm/internal/compiler/compiler.go
Normal file
@@ -0,0 +1,39 @@
|
||||
// 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 compiler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/ethereum/go-ethereum/core/asm"
|
||||
)
|
||||
|
||||
func Compile(fn string, src []byte, debug bool) (string, error) {
|
||||
compiler := asm.NewCompiler(debug)
|
||||
compiler.Feed(asm.Lex(fn, src, debug))
|
||||
|
||||
bin, compileErrors := compiler.Compile()
|
||||
if len(compileErrors) > 0 {
|
||||
// report errors
|
||||
for _, err := range compileErrors {
|
||||
fmt.Printf("%s:%v\n", fn, err)
|
||||
}
|
||||
return "", errors.New("compiling failed")
|
||||
}
|
||||
return bin, nil
|
||||
}
|
||||
81
cmd/evm/json_logger.go
Normal file
81
cmd/evm/json_logger.go
Normal file
@@ -0,0 +1,81 @@
|
||||
// Copyright 2017 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library 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 Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"math/big"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/common/math"
|
||||
"github.com/ethereum/go-ethereum/core/vm"
|
||||
)
|
||||
|
||||
type JSONLogger struct {
|
||||
encoder *json.Encoder
|
||||
cfg *vm.LogConfig
|
||||
}
|
||||
|
||||
func NewJSONLogger(cfg *vm.LogConfig, writer io.Writer) *JSONLogger {
|
||||
return &JSONLogger{json.NewEncoder(writer), cfg}
|
||||
}
|
||||
|
||||
func (l *JSONLogger) CaptureStart(from common.Address, to common.Address, create bool, input []byte, gas uint64, value *big.Int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// CaptureState outputs state information on the logger.
|
||||
func (l *JSONLogger) CaptureState(env *vm.EVM, pc uint64, op vm.OpCode, gas, cost uint64, memory *vm.Memory, stack *vm.Stack, contract *vm.Contract, depth int, err error) error {
|
||||
log := vm.StructLog{
|
||||
Pc: pc,
|
||||
Op: op,
|
||||
Gas: gas,
|
||||
GasCost: cost,
|
||||
MemorySize: memory.Len(),
|
||||
Storage: nil,
|
||||
Depth: depth,
|
||||
Err: err,
|
||||
}
|
||||
if !l.cfg.DisableMemory {
|
||||
log.Memory = memory.Data()
|
||||
}
|
||||
if !l.cfg.DisableStack {
|
||||
log.Stack = stack.Data()
|
||||
}
|
||||
return l.encoder.Encode(log)
|
||||
}
|
||||
|
||||
// CaptureFault outputs state information on the logger.
|
||||
func (l *JSONLogger) CaptureFault(env *vm.EVM, pc uint64, op vm.OpCode, gas, cost uint64, memory *vm.Memory, stack *vm.Stack, contract *vm.Contract, depth int, err error) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// CaptureEnd is triggered at end of execution.
|
||||
func (l *JSONLogger) CaptureEnd(output []byte, gasUsed uint64, t time.Duration, err error) error {
|
||||
type endLog struct {
|
||||
Output string `json:"output"`
|
||||
GasUsed math.HexOrDecimal64 `json:"gasUsed"`
|
||||
Time time.Duration `json:"time"`
|
||||
Err string `json:"error,omitempty"`
|
||||
}
|
||||
if err != nil {
|
||||
return l.encoder.Encode(endLog{common.Bytes2Hex(output), math.HexOrDecimal64(gasUsed), t, err.Error()})
|
||||
}
|
||||
return l.encoder.Encode(endLog{common.Bytes2Hex(output), math.HexOrDecimal64(gasUsed), t, ""})
|
||||
}
|
||||
224
cmd/evm/main.go
224
cmd/evm/main.go
@@ -21,43 +21,54 @@ import (
|
||||
"fmt"
|
||||
"math/big"
|
||||
"os"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/codegangsta/cli"
|
||||
"github.com/ethereum/go-ethereum/cmd/utils"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core"
|
||||
"github.com/ethereum/go-ethereum/core/state"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/core/vm"
|
||||
"github.com/ethereum/go-ethereum/ethdb"
|
||||
"gopkg.in/urfave/cli.v1"
|
||||
)
|
||||
|
||||
var gitCommit = "" // Git SHA1 commit hash of the release (set via linker flags)
|
||||
|
||||
var (
|
||||
app *cli.App
|
||||
app = utils.NewApp(gitCommit, "the evm command line interface")
|
||||
|
||||
DebugFlag = cli.BoolFlag{
|
||||
Name: "debug",
|
||||
Usage: "output full trace logs",
|
||||
}
|
||||
MemProfileFlag = cli.StringFlag{
|
||||
Name: "memprofile",
|
||||
Usage: "creates a memory profile at the given path",
|
||||
}
|
||||
CPUProfileFlag = cli.StringFlag{
|
||||
Name: "cpuprofile",
|
||||
Usage: "creates a CPU profile at the given path",
|
||||
}
|
||||
StatDumpFlag = cli.BoolFlag{
|
||||
Name: "statdump",
|
||||
Usage: "displays stack and heap memory information",
|
||||
}
|
||||
CodeFlag = cli.StringFlag{
|
||||
Name: "code",
|
||||
Usage: "EVM code",
|
||||
}
|
||||
GasFlag = cli.StringFlag{
|
||||
CodeFileFlag = cli.StringFlag{
|
||||
Name: "codefile",
|
||||
Usage: "File containing EVM code. If '-' is specified, code is read from stdin ",
|
||||
}
|
||||
GasFlag = cli.Uint64Flag{
|
||||
Name: "gas",
|
||||
Usage: "gas limit for the evm",
|
||||
Value: "10000000000",
|
||||
Value: 10000000000,
|
||||
}
|
||||
PriceFlag = cli.StringFlag{
|
||||
PriceFlag = utils.BigFlag{
|
||||
Name: "price",
|
||||
Usage: "price set for the evm",
|
||||
Value: "0",
|
||||
Value: new(big.Int),
|
||||
}
|
||||
ValueFlag = cli.StringFlag{
|
||||
ValueFlag = utils.BigFlag{
|
||||
Name: "value",
|
||||
Usage: "value set for the evm",
|
||||
Value: "0",
|
||||
Value: new(big.Int),
|
||||
}
|
||||
DumpFlag = cli.BoolFlag{
|
||||
Name: "dump",
|
||||
@@ -67,73 +78,73 @@ var (
|
||||
Name: "input",
|
||||
Usage: "input for the EVM",
|
||||
}
|
||||
SysStatFlag = cli.BoolFlag{
|
||||
Name: "sysstat",
|
||||
Usage: "display system stats",
|
||||
VerbosityFlag = cli.IntFlag{
|
||||
Name: "verbosity",
|
||||
Usage: "sets the verbosity level",
|
||||
}
|
||||
CreateFlag = cli.BoolFlag{
|
||||
Name: "create",
|
||||
Usage: "indicates the action should be create rather than call",
|
||||
}
|
||||
DisableGasMeteringFlag = cli.BoolFlag{
|
||||
Name: "nogasmetering",
|
||||
Usage: "disable gas metering",
|
||||
}
|
||||
GenesisFlag = cli.StringFlag{
|
||||
Name: "prestate",
|
||||
Usage: "JSON file with prestate (genesis) config",
|
||||
}
|
||||
MachineFlag = cli.BoolFlag{
|
||||
Name: "json",
|
||||
Usage: "output trace logs in machine readable format (json)",
|
||||
}
|
||||
SenderFlag = cli.StringFlag{
|
||||
Name: "sender",
|
||||
Usage: "The transaction origin",
|
||||
}
|
||||
ReceiverFlag = cli.StringFlag{
|
||||
Name: "receiver",
|
||||
Usage: "The transaction receiver (execution context)",
|
||||
}
|
||||
DisableMemoryFlag = cli.BoolFlag{
|
||||
Name: "nomemory",
|
||||
Usage: "disable memory output",
|
||||
}
|
||||
DisableStackFlag = cli.BoolFlag{
|
||||
Name: "nostack",
|
||||
Usage: "disable stack output",
|
||||
}
|
||||
)
|
||||
|
||||
func init() {
|
||||
app = utils.NewApp("0.2", "the evm command line interface")
|
||||
app.Flags = []cli.Flag{
|
||||
CreateFlag,
|
||||
DebugFlag,
|
||||
SysStatFlag,
|
||||
VerbosityFlag,
|
||||
CodeFlag,
|
||||
CodeFileFlag,
|
||||
GasFlag,
|
||||
PriceFlag,
|
||||
ValueFlag,
|
||||
DumpFlag,
|
||||
InputFlag,
|
||||
DisableGasMeteringFlag,
|
||||
MemProfileFlag,
|
||||
CPUProfileFlag,
|
||||
StatDumpFlag,
|
||||
GenesisFlag,
|
||||
MachineFlag,
|
||||
SenderFlag,
|
||||
ReceiverFlag,
|
||||
DisableMemoryFlag,
|
||||
DisableStackFlag,
|
||||
}
|
||||
app.Action = run
|
||||
}
|
||||
|
||||
func run(ctx *cli.Context) {
|
||||
vm.Debug = ctx.GlobalBool(DebugFlag.Name)
|
||||
|
||||
db, _ := ethdb.NewMemDatabase()
|
||||
statedb := state.New(common.Hash{}, db)
|
||||
sender := statedb.CreateAccount(common.StringToAddress("sender"))
|
||||
receiver := statedb.CreateAccount(common.StringToAddress("receiver"))
|
||||
receiver.SetCode(common.Hex2Bytes(ctx.GlobalString(CodeFlag.Name)))
|
||||
|
||||
vmenv := NewEnv(statedb, common.StringToAddress("evmuser"), common.Big(ctx.GlobalString(ValueFlag.Name)))
|
||||
|
||||
tstart := time.Now()
|
||||
ret, e := vmenv.Call(
|
||||
sender,
|
||||
receiver.Address(),
|
||||
common.Hex2Bytes(ctx.GlobalString(InputFlag.Name)),
|
||||
common.Big(ctx.GlobalString(GasFlag.Name)),
|
||||
common.Big(ctx.GlobalString(PriceFlag.Name)),
|
||||
common.Big(ctx.GlobalString(ValueFlag.Name)),
|
||||
)
|
||||
vmdone := time.Since(tstart)
|
||||
|
||||
if e != nil {
|
||||
fmt.Println(e)
|
||||
os.Exit(1)
|
||||
app.Commands = []cli.Command{
|
||||
compileCommand,
|
||||
disasmCommand,
|
||||
runCommand,
|
||||
stateTestCommand,
|
||||
}
|
||||
|
||||
if ctx.GlobalBool(DumpFlag.Name) {
|
||||
fmt.Println(string(statedb.Dump()))
|
||||
}
|
||||
vm.StdErrFormat(vmenv.StructLogs())
|
||||
|
||||
if ctx.GlobalBool(SysStatFlag.Name) {
|
||||
var mem runtime.MemStats
|
||||
runtime.ReadMemStats(&mem)
|
||||
fmt.Printf("vm took %v\n", vmdone)
|
||||
fmt.Printf(`alloc: %d
|
||||
tot alloc: %d
|
||||
no. malloc: %d
|
||||
heap alloc: %d
|
||||
heap objs: %d
|
||||
num gc: %d
|
||||
`, mem.Alloc, mem.TotalAlloc, mem.Mallocs, mem.HeapAlloc, mem.HeapObjects, mem.NumGC)
|
||||
}
|
||||
|
||||
fmt.Printf("OUT: 0x%x\n", ret)
|
||||
}
|
||||
|
||||
func main() {
|
||||
@@ -142,78 +153,3 @@ func main() {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
type VMEnv struct {
|
||||
state *state.StateDB
|
||||
block *types.Block
|
||||
|
||||
transactor *common.Address
|
||||
value *big.Int
|
||||
|
||||
depth int
|
||||
Gas *big.Int
|
||||
time uint64
|
||||
logs []vm.StructLog
|
||||
}
|
||||
|
||||
func NewEnv(state *state.StateDB, transactor common.Address, value *big.Int) *VMEnv {
|
||||
return &VMEnv{
|
||||
state: state,
|
||||
transactor: &transactor,
|
||||
value: value,
|
||||
time: uint64(time.Now().Unix()),
|
||||
}
|
||||
}
|
||||
|
||||
func (self *VMEnv) State() *state.StateDB { return self.state }
|
||||
func (self *VMEnv) Origin() common.Address { return *self.transactor }
|
||||
func (self *VMEnv) BlockNumber() *big.Int { return common.Big0 }
|
||||
func (self *VMEnv) Coinbase() common.Address { return *self.transactor }
|
||||
func (self *VMEnv) Time() uint64 { return self.time }
|
||||
func (self *VMEnv) Difficulty() *big.Int { return common.Big1 }
|
||||
func (self *VMEnv) BlockHash() []byte { return make([]byte, 32) }
|
||||
func (self *VMEnv) Value() *big.Int { return self.value }
|
||||
func (self *VMEnv) GasLimit() *big.Int { return big.NewInt(1000000000) }
|
||||
func (self *VMEnv) VmType() vm.Type { return vm.StdVmTy }
|
||||
func (self *VMEnv) Depth() int { return 0 }
|
||||
func (self *VMEnv) SetDepth(i int) { self.depth = i }
|
||||
func (self *VMEnv) GetHash(n uint64) common.Hash {
|
||||
if self.block.Number().Cmp(big.NewInt(int64(n))) == 0 {
|
||||
return self.block.Hash()
|
||||
}
|
||||
return common.Hash{}
|
||||
}
|
||||
func (self *VMEnv) AddStructLog(log vm.StructLog) {
|
||||
self.logs = append(self.logs, log)
|
||||
}
|
||||
func (self *VMEnv) StructLogs() []vm.StructLog {
|
||||
return self.logs
|
||||
}
|
||||
func (self *VMEnv) AddLog(log *state.Log) {
|
||||
self.state.AddLog(log)
|
||||
}
|
||||
func (self *VMEnv) Transfer(from, to vm.Account, amount *big.Int) error {
|
||||
return vm.Transfer(from, to, amount)
|
||||
}
|
||||
|
||||
func (self *VMEnv) vm(addr *common.Address, data []byte, gas, price, value *big.Int) *core.Execution {
|
||||
return core.NewExecution(self, addr, data, gas, price, value)
|
||||
}
|
||||
|
||||
func (self *VMEnv) Call(caller vm.ContextRef, addr common.Address, data []byte, gas, price, value *big.Int) ([]byte, error) {
|
||||
exe := self.vm(&addr, data, gas, price, value)
|
||||
ret, err := exe.Call(addr, caller)
|
||||
self.Gas = exe.Gas
|
||||
|
||||
return ret, err
|
||||
}
|
||||
func (self *VMEnv) CallCode(caller vm.ContextRef, addr common.Address, data []byte, gas, price, value *big.Int) ([]byte, error) {
|
||||
a := caller.Address()
|
||||
exe := self.vm(&a, data, gas, price, value)
|
||||
return exe.Call(addr, caller)
|
||||
}
|
||||
|
||||
func (self *VMEnv) Create(caller vm.ContextRef, data []byte, gas, price, value *big.Int) ([]byte, error, vm.ContextRef) {
|
||||
exe := self.vm(nil, data, gas, price, value)
|
||||
return exe.Create(caller)
|
||||
}
|
||||
|
||||
246
cmd/evm/runner.go
Normal file
246
cmd/evm/runner.go
Normal file
@@ -0,0 +1,246 @@
|
||||
// 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"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"runtime/pprof"
|
||||
"time"
|
||||
|
||||
goruntime "runtime"
|
||||
|
||||
"github.com/ethereum/go-ethereum/cmd/evm/internal/compiler"
|
||||
"github.com/ethereum/go-ethereum/cmd/utils"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core"
|
||||
"github.com/ethereum/go-ethereum/core/state"
|
||||
"github.com/ethereum/go-ethereum/core/vm"
|
||||
"github.com/ethereum/go-ethereum/core/vm/runtime"
|
||||
"github.com/ethereum/go-ethereum/ethdb"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/params"
|
||||
cli "gopkg.in/urfave/cli.v1"
|
||||
)
|
||||
|
||||
var runCommand = cli.Command{
|
||||
Action: runCmd,
|
||||
Name: "run",
|
||||
Usage: "run arbitrary evm binary",
|
||||
ArgsUsage: "<code>",
|
||||
Description: `The run command runs arbitrary EVM code.`,
|
||||
}
|
||||
|
||||
// readGenesis will read the given JSON format genesis file and return
|
||||
// the initialized Genesis structure
|
||||
func readGenesis(genesisPath string) *core.Genesis {
|
||||
// Make sure we have a valid genesis JSON
|
||||
//genesisPath := ctx.Args().First()
|
||||
if len(genesisPath) == 0 {
|
||||
utils.Fatalf("Must supply path to genesis JSON file")
|
||||
}
|
||||
file, err := os.Open(genesisPath)
|
||||
if err != nil {
|
||||
utils.Fatalf("Failed to read genesis file: %v", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
genesis := new(core.Genesis)
|
||||
if err := json.NewDecoder(file).Decode(genesis); err != nil {
|
||||
utils.Fatalf("invalid genesis file: %v", err)
|
||||
}
|
||||
return genesis
|
||||
}
|
||||
|
||||
func runCmd(ctx *cli.Context) error {
|
||||
glogger := log.NewGlogHandler(log.StreamHandler(os.Stderr, log.TerminalFormat(false)))
|
||||
glogger.Verbosity(log.Lvl(ctx.GlobalInt(VerbosityFlag.Name)))
|
||||
log.Root().SetHandler(glogger)
|
||||
logconfig := &vm.LogConfig{
|
||||
DisableMemory: ctx.GlobalBool(DisableMemoryFlag.Name),
|
||||
DisableStack: ctx.GlobalBool(DisableStackFlag.Name),
|
||||
}
|
||||
|
||||
var (
|
||||
tracer vm.Tracer
|
||||
debugLogger *vm.StructLogger
|
||||
statedb *state.StateDB
|
||||
chainConfig *params.ChainConfig
|
||||
sender = common.StringToAddress("sender")
|
||||
receiver = common.StringToAddress("receiver")
|
||||
)
|
||||
if ctx.GlobalBool(MachineFlag.Name) {
|
||||
tracer = NewJSONLogger(logconfig, os.Stdout)
|
||||
} else if ctx.GlobalBool(DebugFlag.Name) {
|
||||
debugLogger = vm.NewStructLogger(logconfig)
|
||||
tracer = debugLogger
|
||||
} else {
|
||||
debugLogger = vm.NewStructLogger(logconfig)
|
||||
}
|
||||
if ctx.GlobalString(GenesisFlag.Name) != "" {
|
||||
gen := readGenesis(ctx.GlobalString(GenesisFlag.Name))
|
||||
_, statedb = gen.ToBlock()
|
||||
chainConfig = gen.Config
|
||||
} else {
|
||||
db, _ := ethdb.NewMemDatabase()
|
||||
statedb, _ = state.New(common.Hash{}, state.NewDatabase(db))
|
||||
}
|
||||
if ctx.GlobalString(SenderFlag.Name) != "" {
|
||||
sender = common.HexToAddress(ctx.GlobalString(SenderFlag.Name))
|
||||
}
|
||||
statedb.CreateAccount(sender)
|
||||
|
||||
if ctx.GlobalString(ReceiverFlag.Name) != "" {
|
||||
receiver = common.HexToAddress(ctx.GlobalString(ReceiverFlag.Name))
|
||||
}
|
||||
|
||||
var (
|
||||
code []byte
|
||||
ret []byte
|
||||
err error
|
||||
)
|
||||
// The '--code' or '--codefile' flag overrides code in state
|
||||
if ctx.GlobalString(CodeFileFlag.Name) != "" {
|
||||
var hexcode []byte
|
||||
var err error
|
||||
// If - is specified, it means that code comes from stdin
|
||||
if ctx.GlobalString(CodeFileFlag.Name) == "-" {
|
||||
//Try reading from stdin
|
||||
if hexcode, err = ioutil.ReadAll(os.Stdin); err != nil {
|
||||
fmt.Printf("Could not load code from stdin: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
} else {
|
||||
// Codefile with hex assembly
|
||||
if hexcode, err = ioutil.ReadFile(ctx.GlobalString(CodeFileFlag.Name)); err != nil {
|
||||
fmt.Printf("Could not load code from file: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
code = common.Hex2Bytes(string(bytes.TrimRight(hexcode, "\n")))
|
||||
|
||||
} else if ctx.GlobalString(CodeFlag.Name) != "" {
|
||||
code = common.Hex2Bytes(ctx.GlobalString(CodeFlag.Name))
|
||||
} else if fn := ctx.Args().First(); len(fn) > 0 {
|
||||
// EASM-file to compile
|
||||
src, err := ioutil.ReadFile(fn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
bin, err := compiler.Compile(fn, src, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
code = common.Hex2Bytes(bin)
|
||||
}
|
||||
|
||||
initialGas := ctx.GlobalUint64(GasFlag.Name)
|
||||
runtimeConfig := runtime.Config{
|
||||
Origin: sender,
|
||||
State: statedb,
|
||||
GasLimit: initialGas,
|
||||
GasPrice: utils.GlobalBig(ctx, PriceFlag.Name),
|
||||
Value: utils.GlobalBig(ctx, ValueFlag.Name),
|
||||
EVMConfig: vm.Config{
|
||||
Tracer: tracer,
|
||||
Debug: ctx.GlobalBool(DebugFlag.Name) || ctx.GlobalBool(MachineFlag.Name),
|
||||
DisableGasMetering: ctx.GlobalBool(DisableGasMeteringFlag.Name),
|
||||
},
|
||||
}
|
||||
|
||||
if cpuProfilePath := ctx.GlobalString(CPUProfileFlag.Name); cpuProfilePath != "" {
|
||||
f, err := os.Create(cpuProfilePath)
|
||||
if err != nil {
|
||||
fmt.Println("could not create CPU profile: ", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := pprof.StartCPUProfile(f); err != nil {
|
||||
fmt.Println("could not start CPU profile: ", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer pprof.StopCPUProfile()
|
||||
}
|
||||
|
||||
if chainConfig != nil {
|
||||
runtimeConfig.ChainConfig = chainConfig
|
||||
}
|
||||
tstart := time.Now()
|
||||
var leftOverGas uint64
|
||||
if ctx.GlobalBool(CreateFlag.Name) {
|
||||
input := append(code, common.Hex2Bytes(ctx.GlobalString(InputFlag.Name))...)
|
||||
ret, _, leftOverGas, err = runtime.Create(input, &runtimeConfig)
|
||||
} else {
|
||||
if len(code) > 0 {
|
||||
statedb.SetCode(receiver, code)
|
||||
}
|
||||
ret, leftOverGas, err = runtime.Call(receiver, common.Hex2Bytes(ctx.GlobalString(InputFlag.Name)), &runtimeConfig)
|
||||
}
|
||||
execTime := time.Since(tstart)
|
||||
|
||||
if ctx.GlobalBool(DumpFlag.Name) {
|
||||
statedb.IntermediateRoot(true)
|
||||
fmt.Println(string(statedb.Dump()))
|
||||
}
|
||||
|
||||
if memProfilePath := ctx.GlobalString(MemProfileFlag.Name); memProfilePath != "" {
|
||||
f, err := os.Create(memProfilePath)
|
||||
if err != nil {
|
||||
fmt.Println("could not create memory profile: ", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := pprof.WriteHeapProfile(f); err != nil {
|
||||
fmt.Println("could not write memory profile: ", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
f.Close()
|
||||
}
|
||||
|
||||
if ctx.GlobalBool(DebugFlag.Name) {
|
||||
if debugLogger != nil {
|
||||
fmt.Fprintln(os.Stderr, "#### TRACE ####")
|
||||
vm.WriteTrace(os.Stderr, debugLogger.StructLogs())
|
||||
}
|
||||
fmt.Fprintln(os.Stderr, "#### LOGS ####")
|
||||
vm.WriteLogs(os.Stderr, statedb.Logs())
|
||||
}
|
||||
|
||||
if ctx.GlobalBool(StatDumpFlag.Name) {
|
||||
var mem goruntime.MemStats
|
||||
goruntime.ReadMemStats(&mem)
|
||||
fmt.Fprintf(os.Stderr, `evm execution time: %v
|
||||
heap objects: %d
|
||||
allocations: %d
|
||||
total allocations: %d
|
||||
GC calls: %d
|
||||
Gas used: %d
|
||||
|
||||
`, execTime, mem.HeapObjects, mem.Alloc, mem.TotalAlloc, mem.NumGC, initialGas-leftOverGas)
|
||||
}
|
||||
if tracer != nil {
|
||||
tracer.CaptureEnd(ret, initialGas-leftOverGas, execTime, err)
|
||||
} else {
|
||||
fmt.Printf("0x%x\n", ret)
|
||||
if err != nil {
|
||||
fmt.Printf(" error: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
125
cmd/evm/staterunner.go
Normal file
125
cmd/evm/staterunner.go
Normal file
@@ -0,0 +1,125 @@
|
||||
// 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 (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
"github.com/ethereum/go-ethereum/core/state"
|
||||
"github.com/ethereum/go-ethereum/core/vm"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/tests"
|
||||
|
||||
cli "gopkg.in/urfave/cli.v1"
|
||||
)
|
||||
|
||||
var stateTestCommand = cli.Command{
|
||||
Action: stateTestCmd,
|
||||
Name: "statetest",
|
||||
Usage: "executes the given state tests",
|
||||
ArgsUsage: "<file>",
|
||||
}
|
||||
|
||||
type StatetestResult struct {
|
||||
Name string `json:"name"`
|
||||
Pass bool `json:"pass"`
|
||||
Fork string `json:"fork"`
|
||||
Error string `json:"error,omitempty"`
|
||||
State *state.Dump `json:"state,omitempty"`
|
||||
}
|
||||
|
||||
func stateTestCmd(ctx *cli.Context) error {
|
||||
if len(ctx.Args().First()) == 0 {
|
||||
return errors.New("path-to-test argument required")
|
||||
}
|
||||
// Configure the go-ethereum logger
|
||||
glogger := log.NewGlogHandler(log.StreamHandler(os.Stderr, log.TerminalFormat(false)))
|
||||
glogger.Verbosity(log.Lvl(ctx.GlobalInt(VerbosityFlag.Name)))
|
||||
log.Root().SetHandler(glogger)
|
||||
|
||||
// Configure the EVM logger
|
||||
config := &vm.LogConfig{
|
||||
DisableMemory: ctx.GlobalBool(DisableMemoryFlag.Name),
|
||||
DisableStack: ctx.GlobalBool(DisableStackFlag.Name),
|
||||
}
|
||||
var (
|
||||
tracer vm.Tracer
|
||||
debugger *vm.StructLogger
|
||||
)
|
||||
switch {
|
||||
case ctx.GlobalBool(MachineFlag.Name):
|
||||
tracer = NewJSONLogger(config, os.Stderr)
|
||||
|
||||
case ctx.GlobalBool(DebugFlag.Name):
|
||||
debugger = vm.NewStructLogger(config)
|
||||
tracer = debugger
|
||||
|
||||
default:
|
||||
debugger = vm.NewStructLogger(config)
|
||||
}
|
||||
// Load the test content from the input file
|
||||
src, err := ioutil.ReadFile(ctx.Args().First())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var tests map[string]tests.StateTest
|
||||
if err = json.Unmarshal(src, &tests); err != nil {
|
||||
return err
|
||||
}
|
||||
// Iterate over all the tests, run them and aggregate the results
|
||||
cfg := vm.Config{
|
||||
Tracer: tracer,
|
||||
Debug: ctx.GlobalBool(DebugFlag.Name) || ctx.GlobalBool(MachineFlag.Name),
|
||||
}
|
||||
results := make([]StatetestResult, 0, len(tests))
|
||||
for key, test := range tests {
|
||||
for _, st := range test.Subtests() {
|
||||
// Run the test and aggregate the result
|
||||
result := &StatetestResult{Name: key, Fork: st.Fork, Pass: true}
|
||||
state, err := test.Run(st, cfg)
|
||||
if err != nil {
|
||||
// Test failed, mark as so and dump any state to aid debugging
|
||||
result.Pass, result.Error = false, err.Error()
|
||||
if ctx.GlobalBool(DumpFlag.Name) && state != nil {
|
||||
dump := state.RawDump()
|
||||
result.State = &dump
|
||||
}
|
||||
}
|
||||
// print state root for evmlab tracing (already committed above, so no need to delete objects again
|
||||
if ctx.GlobalBool(MachineFlag.Name) && state != nil {
|
||||
fmt.Fprintf(os.Stderr, "{\"stateRoot\": \"%x\"}\n", state.IntermediateRoot(false))
|
||||
}
|
||||
|
||||
results = append(results, *result)
|
||||
|
||||
// Print any structured logs collected
|
||||
if ctx.GlobalBool(DebugFlag.Name) {
|
||||
if debugger != nil {
|
||||
fmt.Fprintln(os.Stderr, "#### TRACE ####")
|
||||
vm.WriteTrace(os.Stderr, debugger.StructLogs())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
out, _ := json.MarshalIndent(results, "", " ")
|
||||
fmt.Println(string(out))
|
||||
return nil
|
||||
}
|
||||
793
cmd/faucet/faucet.go
Normal file
793
cmd/faucet/faucet.go
Normal file
@@ -0,0 +1,793 @@
|
||||
// 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/>.
|
||||
|
||||
// faucet is a Ether faucet backed by a light client.
|
||||
package main
|
||||
|
||||
//go:generate go-bindata -nometadata -o website.go faucet.html
|
||||
//go:generate gofmt -w -s website.go
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io/ioutil"
|
||||
"math"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts"
|
||||
"github.com/ethereum/go-ethereum/accounts/keystore"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/eth"
|
||||
"github.com/ethereum/go-ethereum/eth/downloader"
|
||||
"github.com/ethereum/go-ethereum/ethclient"
|
||||
"github.com/ethereum/go-ethereum/ethstats"
|
||||
"github.com/ethereum/go-ethereum/les"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/node"
|
||||
"github.com/ethereum/go-ethereum/p2p"
|
||||
"github.com/ethereum/go-ethereum/p2p/discover"
|
||||
"github.com/ethereum/go-ethereum/p2p/discv5"
|
||||
"github.com/ethereum/go-ethereum/p2p/nat"
|
||||
"github.com/ethereum/go-ethereum/params"
|
||||
"golang.org/x/net/websocket"
|
||||
)
|
||||
|
||||
var (
|
||||
genesisFlag = flag.String("genesis", "", "Genesis json file to seed the chain with")
|
||||
apiPortFlag = flag.Int("apiport", 8080, "Listener port for the HTTP API connection")
|
||||
ethPortFlag = flag.Int("ethport", 30303, "Listener port for the devp2p connection")
|
||||
bootFlag = flag.String("bootnodes", "", "Comma separated bootnode enode URLs to seed with")
|
||||
netFlag = flag.Uint64("network", 0, "Network ID to use for the Ethereum protocol")
|
||||
statsFlag = flag.String("ethstats", "", "Ethstats network monitoring auth string")
|
||||
|
||||
netnameFlag = flag.String("faucet.name", "", "Network name to assign to the faucet")
|
||||
payoutFlag = flag.Int("faucet.amount", 1, "Number of Ethers to pay out per user request")
|
||||
minutesFlag = flag.Int("faucet.minutes", 1440, "Number of minutes to wait between funding rounds")
|
||||
tiersFlag = flag.Int("faucet.tiers", 3, "Number of funding tiers to enable (x3 time, x2.5 funds)")
|
||||
|
||||
accJSONFlag = flag.String("account.json", "", "Key json file to fund user requests with")
|
||||
accPassFlag = flag.String("account.pass", "", "Decryption password to access faucet funds")
|
||||
|
||||
githubUser = flag.String("github.user", "", "GitHub user to authenticate with for Gist access")
|
||||
githubToken = flag.String("github.token", "", "GitHub personal token to access Gists with")
|
||||
|
||||
captchaToken = flag.String("captcha.token", "", "Recaptcha site key to authenticate client side")
|
||||
captchaSecret = flag.String("captcha.secret", "", "Recaptcha secret key to authenticate server side")
|
||||
|
||||
noauthFlag = flag.Bool("noauth", false, "Enables funding requests without authentication")
|
||||
logFlag = flag.Int("loglevel", 3, "Log level to use for Ethereum and the faucet")
|
||||
)
|
||||
|
||||
var (
|
||||
ether = new(big.Int).Exp(big.NewInt(10), big.NewInt(18), nil)
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Parse the flags and set up the logger to print everything requested
|
||||
flag.Parse()
|
||||
log.Root().SetHandler(log.LvlFilterHandler(log.Lvl(*logFlag), log.StreamHandler(os.Stderr, log.TerminalFormat(true))))
|
||||
|
||||
// Construct the payout tiers
|
||||
amounts := make([]string, *tiersFlag)
|
||||
periods := make([]string, *tiersFlag)
|
||||
for i := 0; i < *tiersFlag; i++ {
|
||||
// Calculate the amount for the next tier and format it
|
||||
amount := float64(*payoutFlag) * math.Pow(2.5, float64(i))
|
||||
amounts[i] = fmt.Sprintf("%s Ethers", strconv.FormatFloat(amount, 'f', -1, 64))
|
||||
if amount == 1 {
|
||||
amounts[i] = strings.TrimSuffix(amounts[i], "s")
|
||||
}
|
||||
// Calculate the period for the next tier and format it
|
||||
period := *minutesFlag * int(math.Pow(3, float64(i)))
|
||||
periods[i] = fmt.Sprintf("%d mins", period)
|
||||
if period%60 == 0 {
|
||||
period /= 60
|
||||
periods[i] = fmt.Sprintf("%d hours", period)
|
||||
|
||||
if period%24 == 0 {
|
||||
period /= 24
|
||||
periods[i] = fmt.Sprintf("%d days", period)
|
||||
}
|
||||
}
|
||||
if period == 1 {
|
||||
periods[i] = strings.TrimSuffix(periods[i], "s")
|
||||
}
|
||||
}
|
||||
// Load up and render the faucet website
|
||||
tmpl, err := Asset("faucet.html")
|
||||
if err != nil {
|
||||
log.Crit("Failed to load the faucet template", "err", err)
|
||||
}
|
||||
website := new(bytes.Buffer)
|
||||
err = template.Must(template.New("").Parse(string(tmpl))).Execute(website, map[string]interface{}{
|
||||
"Network": *netnameFlag,
|
||||
"Amounts": amounts,
|
||||
"Periods": periods,
|
||||
"Recaptcha": *captchaToken,
|
||||
"NoAuth": *noauthFlag,
|
||||
})
|
||||
if err != nil {
|
||||
log.Crit("Failed to render the faucet template", "err", err)
|
||||
}
|
||||
// Load and parse the genesis block requested by the user
|
||||
blob, err := ioutil.ReadFile(*genesisFlag)
|
||||
if err != nil {
|
||||
log.Crit("Failed to read genesis block contents", "genesis", *genesisFlag, "err", err)
|
||||
}
|
||||
genesis := new(core.Genesis)
|
||||
if err = json.Unmarshal(blob, genesis); err != nil {
|
||||
log.Crit("Failed to parse genesis block json", "err", err)
|
||||
}
|
||||
// Convert the bootnodes to internal enode representations
|
||||
var enodes []*discv5.Node
|
||||
for _, boot := range strings.Split(*bootFlag, ",") {
|
||||
if url, err := discv5.ParseNode(boot); err == nil {
|
||||
enodes = append(enodes, url)
|
||||
} else {
|
||||
log.Error("Failed to parse bootnode URL", "url", boot, "err", err)
|
||||
}
|
||||
}
|
||||
// Load up the account key and decrypt its password
|
||||
if blob, err = ioutil.ReadFile(*accPassFlag); err != nil {
|
||||
log.Crit("Failed to read account password contents", "file", *accPassFlag, "err", err)
|
||||
}
|
||||
pass := string(blob)
|
||||
|
||||
ks := keystore.NewKeyStore(filepath.Join(os.Getenv("HOME"), ".faucet", "keys"), keystore.StandardScryptN, keystore.StandardScryptP)
|
||||
if blob, err = ioutil.ReadFile(*accJSONFlag); err != nil {
|
||||
log.Crit("Failed to read account key contents", "file", *accJSONFlag, "err", err)
|
||||
}
|
||||
acc, err := ks.Import(blob, pass, pass)
|
||||
if err != nil {
|
||||
log.Crit("Failed to import faucet signer account", "err", err)
|
||||
}
|
||||
ks.Unlock(acc, pass)
|
||||
|
||||
// Assemble and start the faucet light service
|
||||
faucet, err := newFaucet(genesis, *ethPortFlag, enodes, *netFlag, *statsFlag, ks, website.Bytes())
|
||||
if err != nil {
|
||||
log.Crit("Failed to start faucet", "err", err)
|
||||
}
|
||||
defer faucet.close()
|
||||
|
||||
if err := faucet.listenAndServe(*apiPortFlag); err != nil {
|
||||
log.Crit("Failed to launch faucet API", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
// request represents an accepted funding request.
|
||||
type request struct {
|
||||
Avatar string `json:"avatar"` // Avatar URL to make the UI nicer
|
||||
Account common.Address `json:"account"` // Ethereum address being funded
|
||||
Time time.Time `json:"time"` // Timestamp when the request was accepted
|
||||
Tx *types.Transaction `json:"tx"` // Transaction funding the account
|
||||
}
|
||||
|
||||
// faucet represents a crypto faucet backed by an Ethereum light client.
|
||||
type faucet struct {
|
||||
config *params.ChainConfig // Chain configurations for signing
|
||||
stack *node.Node // Ethereum protocol stack
|
||||
client *ethclient.Client // Client connection to the Ethereum chain
|
||||
index []byte // Index page to serve up on the web
|
||||
|
||||
keystore *keystore.KeyStore // Keystore containing the single signer
|
||||
account accounts.Account // Account funding user faucet requests
|
||||
nonce uint64 // Current pending nonce of the faucet
|
||||
price *big.Int // Current gas price to issue funds with
|
||||
|
||||
conns []*websocket.Conn // Currently live websocket connections
|
||||
timeouts map[string]time.Time // History of users and their funding timeouts
|
||||
reqs []*request // Currently pending funding requests
|
||||
update chan struct{} // Channel to signal request updates
|
||||
|
||||
lock sync.RWMutex // Lock protecting the faucet's internals
|
||||
}
|
||||
|
||||
func newFaucet(genesis *core.Genesis, port int, enodes []*discv5.Node, network uint64, stats string, ks *keystore.KeyStore, index []byte) (*faucet, error) {
|
||||
// Assemble the raw devp2p protocol stack
|
||||
stack, err := node.New(&node.Config{
|
||||
Name: "geth",
|
||||
Version: params.Version,
|
||||
DataDir: filepath.Join(os.Getenv("HOME"), ".faucet"),
|
||||
P2P: p2p.Config{
|
||||
NAT: nat.Any(),
|
||||
NoDiscovery: true,
|
||||
DiscoveryV5: true,
|
||||
ListenAddr: fmt.Sprintf(":%d", port),
|
||||
DiscoveryV5Addr: fmt.Sprintf(":%d", port+1),
|
||||
MaxPeers: 25,
|
||||
BootstrapNodesV5: enodes,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Assemble the Ethereum light client protocol
|
||||
if err := stack.Register(func(ctx *node.ServiceContext) (node.Service, error) {
|
||||
cfg := eth.DefaultConfig
|
||||
cfg.SyncMode = downloader.LightSync
|
||||
cfg.NetworkId = network
|
||||
cfg.Genesis = genesis
|
||||
return les.New(ctx, &cfg)
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Assemble the ethstats monitoring and reporting service'
|
||||
if stats != "" {
|
||||
if err := stack.Register(func(ctx *node.ServiceContext) (node.Service, error) {
|
||||
var serv *les.LightEthereum
|
||||
ctx.Service(&serv)
|
||||
return ethstats.New(stats, nil, serv)
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
// Boot up the client and ensure it connects to bootnodes
|
||||
if err := stack.Start(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, boot := range enodes {
|
||||
old, _ := discover.ParseNode(boot.String())
|
||||
stack.Server().AddPeer(old)
|
||||
}
|
||||
// Attach to the client and retrieve and interesting metadatas
|
||||
api, err := stack.Attach()
|
||||
if err != nil {
|
||||
stack.Stop()
|
||||
return nil, err
|
||||
}
|
||||
client := ethclient.NewClient(api)
|
||||
|
||||
return &faucet{
|
||||
config: genesis.Config,
|
||||
stack: stack,
|
||||
client: client,
|
||||
index: index,
|
||||
keystore: ks,
|
||||
account: ks.Accounts()[0],
|
||||
timeouts: make(map[string]time.Time),
|
||||
update: make(chan struct{}, 1),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// close terminates the Ethereum connection and tears down the faucet.
|
||||
func (f *faucet) close() error {
|
||||
return f.stack.Stop()
|
||||
}
|
||||
|
||||
// listenAndServe registers the HTTP handlers for the faucet and boots it up
|
||||
// for service user funding requests.
|
||||
func (f *faucet) listenAndServe(port int) error {
|
||||
go f.loop()
|
||||
|
||||
http.HandleFunc("/", f.webHandler)
|
||||
http.Handle("/api", websocket.Handler(f.apiHandler))
|
||||
|
||||
return http.ListenAndServe(fmt.Sprintf(":%d", port), nil)
|
||||
}
|
||||
|
||||
// webHandler handles all non-api requests, simply flattening and returning the
|
||||
// faucet website.
|
||||
func (f *faucet) webHandler(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write(f.index)
|
||||
}
|
||||
|
||||
// apiHandler handles requests for Ether grants and transaction statuses.
|
||||
func (f *faucet) apiHandler(conn *websocket.Conn) {
|
||||
// Start tracking the connection and drop at the end
|
||||
defer conn.Close()
|
||||
|
||||
f.lock.Lock()
|
||||
f.conns = append(f.conns, conn)
|
||||
f.lock.Unlock()
|
||||
|
||||
defer func() {
|
||||
f.lock.Lock()
|
||||
for i, c := range f.conns {
|
||||
if c == conn {
|
||||
f.conns = append(f.conns[:i], f.conns[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
f.lock.Unlock()
|
||||
}()
|
||||
// Gather the initial stats from the network to report
|
||||
var (
|
||||
head *types.Header
|
||||
balance *big.Int
|
||||
nonce uint64
|
||||
err error
|
||||
)
|
||||
for {
|
||||
// Attempt to retrieve the stats, may error on no faucet connectivity
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
head, err = f.client.HeaderByNumber(ctx, nil)
|
||||
if err == nil {
|
||||
balance, err = f.client.BalanceAt(ctx, f.account.Address, head.Number)
|
||||
if err == nil {
|
||||
nonce, err = f.client.NonceAt(ctx, f.account.Address, nil)
|
||||
}
|
||||
}
|
||||
cancel()
|
||||
|
||||
// If stats retrieval failed, wait a bit and retry
|
||||
if err != nil {
|
||||
if err = sendError(conn, errors.New("Faucet offline: "+err.Error())); err != nil {
|
||||
log.Warn("Failed to send faucet error to client", "err", err)
|
||||
return
|
||||
}
|
||||
time.Sleep(3 * time.Second)
|
||||
continue
|
||||
}
|
||||
// Initial stats reported successfully, proceed with user interaction
|
||||
break
|
||||
}
|
||||
// Send over the initial stats and the latest header
|
||||
if err = send(conn, map[string]interface{}{
|
||||
"funds": balance.Div(balance, ether),
|
||||
"funded": nonce,
|
||||
"peers": f.stack.Server().PeerCount(),
|
||||
"requests": f.reqs,
|
||||
}, 3*time.Second); err != nil {
|
||||
log.Warn("Failed to send initial stats to client", "err", err)
|
||||
return
|
||||
}
|
||||
if err = send(conn, head, 3*time.Second); err != nil {
|
||||
log.Warn("Failed to send initial header to client", "err", err)
|
||||
return
|
||||
}
|
||||
// Keep reading requests from the websocket until the connection breaks
|
||||
for {
|
||||
// Fetch the next funding request and validate against github
|
||||
var msg struct {
|
||||
URL string `json:"url"`
|
||||
Tier uint `json:"tier"`
|
||||
Captcha string `json:"captcha"`
|
||||
}
|
||||
if err = websocket.JSON.Receive(conn, &msg); err != nil {
|
||||
return
|
||||
}
|
||||
if !*noauthFlag && !strings.HasPrefix(msg.URL, "https://gist.github.com/") && !strings.HasPrefix(msg.URL, "https://twitter.com/") &&
|
||||
!strings.HasPrefix(msg.URL, "https://plus.google.com/") && !strings.HasPrefix(msg.URL, "https://www.facebook.com/") {
|
||||
if err = sendError(conn, errors.New("URL doesn't link to supported services")); err != nil {
|
||||
log.Warn("Failed to send URL error to client", "err", err)
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
if msg.Tier >= uint(*tiersFlag) {
|
||||
if err = sendError(conn, errors.New("Invalid funding tier requested")); err != nil {
|
||||
log.Warn("Failed to send tier error to client", "err", err)
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
log.Info("Faucet funds requested", "url", msg.URL, "tier", msg.Tier)
|
||||
|
||||
// If captcha verifications are enabled, make sure we're not dealing with a robot
|
||||
if *captchaToken != "" {
|
||||
form := url.Values{}
|
||||
form.Add("secret", *captchaSecret)
|
||||
form.Add("response", msg.Captcha)
|
||||
|
||||
res, err := http.PostForm("https://www.google.com/recaptcha/api/siteverify", form)
|
||||
if err != nil {
|
||||
if err = sendError(conn, err); err != nil {
|
||||
log.Warn("Failed to send captcha post error to client", "err", err)
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
var result struct {
|
||||
Success bool `json:"success"`
|
||||
Errors json.RawMessage `json:"error-codes"`
|
||||
}
|
||||
err = json.NewDecoder(res.Body).Decode(&result)
|
||||
res.Body.Close()
|
||||
if err != nil {
|
||||
if err = sendError(conn, err); err != nil {
|
||||
log.Warn("Failed to send captcha decode error to client", "err", err)
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
if !result.Success {
|
||||
log.Warn("Captcha verification failed", "err", string(result.Errors))
|
||||
if err = sendError(conn, errors.New("Beep-bop, you're a robot!")); err != nil {
|
||||
log.Warn("Failed to send captcha failure to client", "err", err)
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
// Retrieve the Ethereum address to fund, the requesting user and a profile picture
|
||||
var (
|
||||
username string
|
||||
avatar string
|
||||
address common.Address
|
||||
)
|
||||
switch {
|
||||
case strings.HasPrefix(msg.URL, "https://gist.github.com/"):
|
||||
if err = sendError(conn, errors.New("GitHub authentication discontinued at the official request of GitHub")); err != nil {
|
||||
log.Warn("Failed to send GitHub deprecation to client", "err", err)
|
||||
return
|
||||
}
|
||||
continue
|
||||
case strings.HasPrefix(msg.URL, "https://twitter.com/"):
|
||||
username, avatar, address, err = authTwitter(msg.URL)
|
||||
case strings.HasPrefix(msg.URL, "https://plus.google.com/"):
|
||||
username, avatar, address, err = authGooglePlus(msg.URL)
|
||||
case strings.HasPrefix(msg.URL, "https://www.facebook.com/"):
|
||||
username, avatar, address, err = authFacebook(msg.URL)
|
||||
case *noauthFlag:
|
||||
username, avatar, address, err = authNoAuth(msg.URL)
|
||||
default:
|
||||
err = errors.New("Something funky happened, please open an issue at https://github.com/ethereum/go-ethereum/issues")
|
||||
}
|
||||
if err != nil {
|
||||
if err = sendError(conn, err); err != nil {
|
||||
log.Warn("Failed to send prefix error to client", "err", err)
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
log.Info("Faucet request valid", "url", msg.URL, "tier", msg.Tier, "user", username, "address", address)
|
||||
|
||||
// Ensure the user didn't request funds too recently
|
||||
f.lock.Lock()
|
||||
var (
|
||||
fund bool
|
||||
timeout time.Time
|
||||
)
|
||||
if timeout = f.timeouts[username]; time.Now().After(timeout) {
|
||||
// User wasn't funded recently, create the funding transaction
|
||||
amount := new(big.Int).Mul(big.NewInt(int64(*payoutFlag)), ether)
|
||||
amount = new(big.Int).Mul(amount, new(big.Int).Exp(big.NewInt(5), big.NewInt(int64(msg.Tier)), nil))
|
||||
amount = new(big.Int).Div(amount, new(big.Int).Exp(big.NewInt(2), big.NewInt(int64(msg.Tier)), nil))
|
||||
|
||||
tx := types.NewTransaction(f.nonce+uint64(len(f.reqs)), address, amount, 21000, f.price, nil)
|
||||
signed, err := f.keystore.SignTx(f.account, tx, f.config.ChainId)
|
||||
if err != nil {
|
||||
f.lock.Unlock()
|
||||
if err = sendError(conn, err); err != nil {
|
||||
log.Warn("Failed to send transaction creation error to client", "err", err)
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
// Submit the transaction and mark as funded if successful
|
||||
if err := f.client.SendTransaction(context.Background(), signed); err != nil {
|
||||
f.lock.Unlock()
|
||||
if err = sendError(conn, err); err != nil {
|
||||
log.Warn("Failed to send transaction transmission error to client", "err", err)
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
f.reqs = append(f.reqs, &request{
|
||||
Avatar: avatar,
|
||||
Account: address,
|
||||
Time: time.Now(),
|
||||
Tx: signed,
|
||||
})
|
||||
f.timeouts[username] = time.Now().Add(time.Duration(*minutesFlag*int(math.Pow(3, float64(msg.Tier)))) * time.Minute)
|
||||
fund = true
|
||||
}
|
||||
f.lock.Unlock()
|
||||
|
||||
// Send an error if too frequent funding, othewise a success
|
||||
if !fund {
|
||||
if err = sendError(conn, fmt.Errorf("%s left until next allowance", common.PrettyDuration(timeout.Sub(time.Now())))); err != nil { // nolint: gosimple
|
||||
log.Warn("Failed to send funding error to client", "err", err)
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err = sendSuccess(conn, fmt.Sprintf("Funding request accepted for %s into %s", username, address.Hex())); err != nil {
|
||||
log.Warn("Failed to send funding success to client", "err", err)
|
||||
return
|
||||
}
|
||||
select {
|
||||
case f.update <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// loop keeps waiting for interesting events and pushes them out to connected
|
||||
// websockets.
|
||||
func (f *faucet) loop() {
|
||||
// Wait for chain events and push them to clients
|
||||
heads := make(chan *types.Header, 16)
|
||||
sub, err := f.client.SubscribeNewHead(context.Background(), heads)
|
||||
if err != nil {
|
||||
log.Crit("Failed to subscribe to head events", "err", err)
|
||||
}
|
||||
defer sub.Unsubscribe()
|
||||
|
||||
for {
|
||||
select {
|
||||
case head := <-heads:
|
||||
// New chain head arrived, query the current stats and stream to clients
|
||||
var (
|
||||
balance *big.Int
|
||||
nonce uint64
|
||||
price *big.Int
|
||||
err error
|
||||
)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
balance, err = f.client.BalanceAt(ctx, f.account.Address, head.Number)
|
||||
if err == nil {
|
||||
nonce, err = f.client.NonceAt(ctx, f.account.Address, nil)
|
||||
if err == nil {
|
||||
price, err = f.client.SuggestGasPrice(ctx)
|
||||
}
|
||||
}
|
||||
cancel()
|
||||
|
||||
// If querying the data failed, try for the next block
|
||||
if err != nil {
|
||||
log.Warn("Failed to update faucet state", "block", head.Number, "hash", head.Hash(), "err", err)
|
||||
continue
|
||||
} else {
|
||||
log.Info("Updated faucet state", "block", head.Number, "hash", head.Hash(), "balance", balance, "nonce", nonce, "price", price)
|
||||
}
|
||||
// Faucet state retrieved, update locally and send to clients
|
||||
balance = new(big.Int).Div(balance, ether)
|
||||
|
||||
f.lock.Lock()
|
||||
f.price, f.nonce = price, nonce
|
||||
for len(f.reqs) > 0 && f.reqs[0].Tx.Nonce() < f.nonce {
|
||||
f.reqs = f.reqs[1:]
|
||||
}
|
||||
f.lock.Unlock()
|
||||
|
||||
f.lock.RLock()
|
||||
for _, conn := range f.conns {
|
||||
if err := send(conn, map[string]interface{}{
|
||||
"funds": balance,
|
||||
"funded": f.nonce,
|
||||
"peers": f.stack.Server().PeerCount(),
|
||||
"requests": f.reqs,
|
||||
}, time.Second); err != nil {
|
||||
log.Warn("Failed to send stats to client", "err", err)
|
||||
conn.Close()
|
||||
continue
|
||||
}
|
||||
if err := send(conn, head, time.Second); err != nil {
|
||||
log.Warn("Failed to send header to client", "err", err)
|
||||
conn.Close()
|
||||
}
|
||||
}
|
||||
f.lock.RUnlock()
|
||||
|
||||
case <-f.update:
|
||||
// Pending requests updated, stream to clients
|
||||
f.lock.RLock()
|
||||
for _, conn := range f.conns {
|
||||
if err := send(conn, map[string]interface{}{"requests": f.reqs}, time.Second); err != nil {
|
||||
log.Warn("Failed to send requests to client", "err", err)
|
||||
conn.Close()
|
||||
}
|
||||
}
|
||||
f.lock.RUnlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sends transmits a data packet to the remote end of the websocket, but also
|
||||
// setting a write deadline to prevent waiting forever on the node.
|
||||
func send(conn *websocket.Conn, value interface{}, timeout time.Duration) error {
|
||||
if timeout == 0 {
|
||||
timeout = 60 * time.Second
|
||||
}
|
||||
conn.SetWriteDeadline(time.Now().Add(timeout))
|
||||
return websocket.JSON.Send(conn, value)
|
||||
}
|
||||
|
||||
// sendError transmits an error to the remote end of the websocket, also setting
|
||||
// the write deadline to 1 second to prevent waiting forever.
|
||||
func sendError(conn *websocket.Conn, err error) error {
|
||||
return send(conn, map[string]string{"error": err.Error()}, time.Second)
|
||||
}
|
||||
|
||||
// sendSuccess transmits a success message to the remote end of the websocket, also
|
||||
// setting the write deadline to 1 second to prevent waiting forever.
|
||||
func sendSuccess(conn *websocket.Conn, msg string) error {
|
||||
return send(conn, map[string]string{"success": msg}, time.Second)
|
||||
}
|
||||
|
||||
// authGitHub tries to authenticate a faucet request using GitHub gists, returning
|
||||
// the username, avatar URL and Ethereum address to fund on success.
|
||||
func authGitHub(url string) (string, string, common.Address, error) {
|
||||
// Retrieve the gist from the GitHub Gist APIs
|
||||
parts := strings.Split(url, "/")
|
||||
req, _ := http.NewRequest("GET", "https://api.github.com/gists/"+parts[len(parts)-1], nil)
|
||||
if *githubUser != "" {
|
||||
req.SetBasicAuth(*githubUser, *githubToken)
|
||||
}
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return "", "", common.Address{}, err
|
||||
}
|
||||
var gist struct {
|
||||
Owner struct {
|
||||
Login string `json:"login"`
|
||||
} `json:"owner"`
|
||||
Files map[string]struct {
|
||||
Content string `json:"content"`
|
||||
} `json:"files"`
|
||||
}
|
||||
err = json.NewDecoder(res.Body).Decode(&gist)
|
||||
res.Body.Close()
|
||||
if err != nil {
|
||||
return "", "", common.Address{}, err
|
||||
}
|
||||
if gist.Owner.Login == "" {
|
||||
return "", "", common.Address{}, errors.New("Anonymous Gists not allowed")
|
||||
}
|
||||
// Iterate over all the files and look for Ethereum addresses
|
||||
var address common.Address
|
||||
for _, file := range gist.Files {
|
||||
content := strings.TrimSpace(file.Content)
|
||||
if len(content) == 2+common.AddressLength*2 {
|
||||
address = common.HexToAddress(content)
|
||||
}
|
||||
}
|
||||
if address == (common.Address{}) {
|
||||
return "", "", common.Address{}, errors.New("No Ethereum address found to fund")
|
||||
}
|
||||
// Validate the user's existence since the API is unhelpful here
|
||||
if res, err = http.Head("https://github.com/" + gist.Owner.Login); err != nil {
|
||||
return "", "", common.Address{}, err
|
||||
}
|
||||
res.Body.Close()
|
||||
|
||||
if res.StatusCode != 200 {
|
||||
return "", "", common.Address{}, errors.New("Invalid user... boom!")
|
||||
}
|
||||
// Everything passed validation, return the gathered infos
|
||||
return gist.Owner.Login + "@github", fmt.Sprintf("https://github.com/%s.png?size=64", gist.Owner.Login), address, nil
|
||||
}
|
||||
|
||||
// authTwitter tries to authenticate a faucet request using Twitter posts, returning
|
||||
// the username, avatar URL and Ethereum address to fund on success.
|
||||
func authTwitter(url string) (string, string, common.Address, error) {
|
||||
// Ensure the user specified a meaningful URL, no fancy nonsense
|
||||
parts := strings.Split(url, "/")
|
||||
if len(parts) < 4 || parts[len(parts)-2] != "status" {
|
||||
return "", "", common.Address{}, errors.New("Invalid Twitter status URL")
|
||||
}
|
||||
username := parts[len(parts)-3]
|
||||
|
||||
// Twitter's API isn't really friendly with direct links. Still, we don't
|
||||
// want to do ask read permissions from users, so just load the public posts and
|
||||
// scrape it for the Ethereum address and profile URL.
|
||||
res, err := http.Get(url)
|
||||
if err != nil {
|
||||
return "", "", common.Address{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return "", "", common.Address{}, err
|
||||
}
|
||||
address := common.HexToAddress(string(regexp.MustCompile("0x[0-9a-fA-F]{40}").Find(body)))
|
||||
if address == (common.Address{}) {
|
||||
return "", "", common.Address{}, errors.New("No Ethereum address found to fund")
|
||||
}
|
||||
var avatar string
|
||||
if parts = regexp.MustCompile("src=\"([^\"]+twimg.com/profile_images[^\"]+)\"").FindStringSubmatch(string(body)); len(parts) == 2 {
|
||||
avatar = parts[1]
|
||||
}
|
||||
return username + "@twitter", avatar, address, nil
|
||||
}
|
||||
|
||||
// authGooglePlus tries to authenticate a faucet request using GooglePlus posts,
|
||||
// returning the username, avatar URL and Ethereum address to fund on success.
|
||||
func authGooglePlus(url string) (string, string, common.Address, error) {
|
||||
// Ensure the user specified a meaningful URL, no fancy nonsense
|
||||
parts := strings.Split(url, "/")
|
||||
if len(parts) < 4 || parts[len(parts)-2] != "posts" {
|
||||
return "", "", common.Address{}, errors.New("Invalid Google+ post URL")
|
||||
}
|
||||
username := parts[len(parts)-3]
|
||||
|
||||
// Google's API isn't really friendly with direct links. Still, we don't
|
||||
// want to do ask read permissions from users, so just load the public posts and
|
||||
// scrape it for the Ethereum address and profile URL.
|
||||
res, err := http.Get(url)
|
||||
if err != nil {
|
||||
return "", "", common.Address{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return "", "", common.Address{}, err
|
||||
}
|
||||
address := common.HexToAddress(string(regexp.MustCompile("0x[0-9a-fA-F]{40}").Find(body)))
|
||||
if address == (common.Address{}) {
|
||||
return "", "", common.Address{}, errors.New("No Ethereum address found to fund")
|
||||
}
|
||||
var avatar string
|
||||
if parts = regexp.MustCompile("src=\"([^\"]+googleusercontent.com[^\"]+photo.jpg)\"").FindStringSubmatch(string(body)); len(parts) == 2 {
|
||||
avatar = parts[1]
|
||||
}
|
||||
return username + "@google+", avatar, address, nil
|
||||
}
|
||||
|
||||
// authFacebook tries to authenticate a faucet request using Facebook posts,
|
||||
// returning the username, avatar URL and Ethereum address to fund on success.
|
||||
func authFacebook(url string) (string, string, common.Address, error) {
|
||||
// Ensure the user specified a meaningful URL, no fancy nonsense
|
||||
parts := strings.Split(url, "/")
|
||||
if len(parts) < 4 || parts[len(parts)-2] != "posts" {
|
||||
return "", "", common.Address{}, errors.New("Invalid Facebook post URL")
|
||||
}
|
||||
username := parts[len(parts)-3]
|
||||
|
||||
// Facebook's Graph API isn't really friendly with direct links. Still, we don't
|
||||
// want to do ask read permissions from users, so just load the public posts and
|
||||
// scrape it for the Ethereum address and profile URL.
|
||||
res, err := http.Get(url)
|
||||
if err != nil {
|
||||
return "", "", common.Address{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return "", "", common.Address{}, err
|
||||
}
|
||||
address := common.HexToAddress(string(regexp.MustCompile("0x[0-9a-fA-F]{40}").Find(body)))
|
||||
if address == (common.Address{}) {
|
||||
return "", "", common.Address{}, errors.New("No Ethereum address found to fund")
|
||||
}
|
||||
var avatar string
|
||||
if parts = regexp.MustCompile("src=\"([^\"]+fbcdn.net[^\"]+)\"").FindStringSubmatch(string(body)); len(parts) == 2 {
|
||||
avatar = parts[1]
|
||||
}
|
||||
return username + "@facebook", avatar, address, nil
|
||||
}
|
||||
|
||||
// authNoAuth tries to interpret a faucet request as a plain Ethereum address,
|
||||
// without actually performing any remote authentication. This mode is prone to
|
||||
// Byzantine attack, so only ever use for truly private networks.
|
||||
func authNoAuth(url string) (string, string, common.Address, error) {
|
||||
address := common.HexToAddress(regexp.MustCompile("0x[0-9a-fA-F]{40}").FindString(url))
|
||||
if address == (common.Address{}) {
|
||||
return "", "", common.Address{}, errors.New("No Ethereum address found to fund")
|
||||
}
|
||||
return address.Hex() + "@noauth", "", address, nil
|
||||
}
|
||||
236
cmd/faucet/faucet.html
Normal file
236
cmd/faucet/faucet.html
Normal file
@@ -0,0 +1,236 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<title>{{.Network}}: Authenticated Faucet</title>
|
||||
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet" />
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet" />
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-noty/2.4.1/packaged/jquery.noty.packaged.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.18.0/moment.min.js"></script>
|
||||
|
||||
<style>
|
||||
.vertical-center {
|
||||
min-height: 100%;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.progress {
|
||||
position: relative;
|
||||
}
|
||||
.progress span {
|
||||
position: absolute;
|
||||
display: block;
|
||||
width: 100%;
|
||||
color: white;
|
||||
}
|
||||
pre {
|
||||
padding: 6px;
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="vertical-center">
|
||||
<div class="container">
|
||||
<div class="row" style="margin-bottom: 16px;">
|
||||
<div class="col-lg-12">
|
||||
<h1 style="text-align: center;"><i class="fa fa-bath" aria-hidden="true"></i> {{.Network}} Authenticated Faucet</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-8 col-lg-offset-2">
|
||||
<div class="input-group">
|
||||
<input id="url" name="url" type="text" class="form-control" placeholder="Social network URL containing your Ethereum address...">
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-default dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">Give me Ether <i class="fa fa-caret-down" aria-hidden="true"></i></button>
|
||||
<ul class="dropdown-menu dropdown-menu-right">{{range $idx, $amount := .Amounts}}
|
||||
<li><a style="text-align: center;" onclick="tier={{$idx}}; {{if $.Recaptcha}}grecaptcha.execute(){{else}}submit({{$idx}}){{end}}">{{$amount}} / {{index $.Periods $idx}}</a></li>{{end}}
|
||||
</ul>
|
||||
</span>
|
||||
</div>{{if .Recaptcha}}
|
||||
<div class="g-recaptcha" data-sitekey="{{.Recaptcha}}" data-callback="submit" data-size="invisible"></div>{{end}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" style="margin-top: 32px;">
|
||||
<div class="col-lg-6 col-lg-offset-3">
|
||||
<div class="panel panel-small panel-default">
|
||||
<div class="panel-body" style="padding: 0; overflow: auto; max-height: 300px;">
|
||||
<table id="requests" class="table table-condensed" style="margin: 0;"></table>
|
||||
</div>
|
||||
<div class="panel-footer">
|
||||
<table style="width: 100%"><tr>
|
||||
<td style="text-align: center;"><i class="fa fa-rss" aria-hidden="true"></i> <span id="peers"></span> peers</td>
|
||||
<td style="text-align: center;"><i class="fa fa-database" aria-hidden="true"></i> <span id="block"></span> blocks</td>
|
||||
<td style="text-align: center;"><i class="fa fa-heartbeat" aria-hidden="true"></i> <span id="funds"></span> Ethers</td>
|
||||
<td style="text-align: center;"><i class="fa fa-university" aria-hidden="true"></i> <span id="funded"></span> funded</td>
|
||||
</tr></table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" style="margin-top: 32px;">
|
||||
<div class="col-lg-12">
|
||||
<h3>How does this work?</h3>
|
||||
<p>This Ether faucet is running on the {{.Network}} network. To prevent malicious actors from exhausting all available funds or accumulating enough Ether to mount long running spam attacks, requests are tied to common 3rd party social network accounts. Anyone having a Twitter, Google+ or Facebook account may request funds within the permitted limits.</p>
|
||||
<dl class="dl-horizontal">
|
||||
<dt style="width: auto; margin-left: 40px;"><i class="fa fa-twitter" aria-hidden="true" style="font-size: 36px;"></i></dt>
|
||||
<dd style="margin-left: 88px; margin-bottom: 10px;"></i> To request funds via Twitter, make a <a href="https://twitter.com/intent/tweet?text=Requesting%20faucet%20funds%20into%200x0000000000000000000000000000000000000000%20on%20the%20%23{{.Network}}%20%23Ethereum%20test%20network." target="_about:blank">tweet</a> with your Ethereum address pasted into the contents (surrounding text doesn't matter).<br/>Copy-paste the <a href="https://support.twitter.com/articles/80586" target="_about:blank">tweets URL</a> into the above input box and fire away!</dd>
|
||||
|
||||
<dt style="width: auto; margin-left: 40px;"><i class="fa fa-google-plus-official" aria-hidden="true" style="font-size: 36px;"></i></dt>
|
||||
<dd style="margin-left: 88px; margin-bottom: 10px;"></i> To request funds via Google Plus, publish a new <strong>public</strong> post with your Ethereum address embedded into the content (surrounding text doesn't matter).<br/>Copy-paste the posts URL into the above input box and fire away!</dd>
|
||||
|
||||
<dt style="width: auto; margin-left: 40px;"><i class="fa fa-facebook" aria-hidden="true" style="font-size: 36px;"></i></dt>
|
||||
<dd style="margin-left: 88px; margin-bottom: 10px;"></i> To request funds via Facebook, publish a new <strong>public</strong> post with your Ethereum address embedded into the content (surrounding text doesn't matter).<br/>Copy-paste the <a href="https://www.facebook.com/help/community/question/?id=282662498552845" target="_about:blank">posts URL</a> into the above input box and fire away!</dd>
|
||||
|
||||
{{if .NoAuth}}
|
||||
<dt class="text-danger" style="width: auto; margin-left: 40px;"><i class="fa fa-unlock-alt" aria-hidden="true" style="font-size: 36px;"></i></dt>
|
||||
<dd class="text-danger" style="margin-left: 88px; margin-bottom: 10px;"></i> To request funds <strong>without authentication</strong>, simply copy-paste your Ethereum address into the above input box (surrounding text doesn't matter) and fire away.<br/>This mode is susceptible to Byzantine attacks. Only use for debugging or private networks!</dd>
|
||||
{{end}}
|
||||
</dl>
|
||||
<p>You can track the current pending requests below the input field to see how much you have to wait until your turn comes.</p>
|
||||
{{if .Recaptcha}}<em>The faucet is running invisible reCaptcha protection against bots.</em>{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Global variables to hold the current status of the faucet
|
||||
var attempt = 0;
|
||||
var server;
|
||||
var tier = 0;
|
||||
var requests = [];
|
||||
|
||||
// Define a function that creates closures to drop old requests
|
||||
var dropper = function(hash) {
|
||||
return function() {
|
||||
for (var i=0; i<requests.length; i++) {
|
||||
if (requests[i].tx.hash == hash) {
|
||||
requests.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
// Define the function that submits a gist url to the server
|
||||
var submit = function({{if .Recaptcha}}captcha{{end}}) {
|
||||
server.send(JSON.stringify({url: $("#url")[0].value, tier: tier{{if .Recaptcha}}, captcha: captcha{{end}}}));{{if .Recaptcha}}
|
||||
grecaptcha.reset();{{end}}
|
||||
};
|
||||
// Define a method to reconnect upon server loss
|
||||
var reconnect = function() {
|
||||
server = new WebSocket(((window.location.protocol === "https:") ? "wss://" : "ws://") + window.location.host + "/api");
|
||||
|
||||
server.onmessage = function(event) {
|
||||
var msg = JSON.parse(event.data);
|
||||
if (msg === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.funds !== undefined) {
|
||||
$("#funds").text(msg.funds);
|
||||
}
|
||||
if (msg.funded !== undefined) {
|
||||
$("#funded").text(msg.funded);
|
||||
}
|
||||
if (msg.peers !== undefined) {
|
||||
$("#peers").text(msg.peers);
|
||||
}
|
||||
if (msg.number !== undefined) {
|
||||
$("#block").text(parseInt(msg.number, 16));
|
||||
}
|
||||
if (msg.error !== undefined) {
|
||||
noty({layout: 'topCenter', text: msg.error, type: 'error', timeout: 5000, progressBar: true});
|
||||
}
|
||||
if (msg.success !== undefined) {
|
||||
noty({layout: 'topCenter', text: msg.success, type: 'success', timeout: 5000, progressBar: true});
|
||||
}
|
||||
if (msg.requests !== undefined && msg.requests !== null) {
|
||||
// Mark all previous requests missing as done
|
||||
for (var i=0; i<requests.length; i++) {
|
||||
if (msg.requests.length > 0 && msg.requests[0].tx.hash == requests[i].tx.hash) {
|
||||
break;
|
||||
}
|
||||
if (requests[i].time != "") {
|
||||
requests[i].time = "";
|
||||
setTimeout(dropper(requests[i].tx.hash), 3000);
|
||||
}
|
||||
}
|
||||
// Append any new requests into our local collection
|
||||
var common = -1;
|
||||
if (requests.length > 0) {
|
||||
for (var i=0; i<msg.requests.length; i++) {
|
||||
if (requests[requests.length-1].tx.hash == msg.requests[i].tx.hash) {
|
||||
common = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (var i=common+1; i<msg.requests.length; i++) {
|
||||
requests.push(msg.requests[i]);
|
||||
}
|
||||
// Iterate over our entire local collection and re-render the funding table
|
||||
var content = "";
|
||||
for (var i=0; i<requests.length; i++) {
|
||||
var done = requests[i].time == "";
|
||||
var elapsed = moment().unix()-moment(requests[i].time).unix();
|
||||
|
||||
content += "<tr id='" + requests[i].tx.hash + "'>";
|
||||
content += " <td><div style=\"background: url('" + requests[i].avatar + "'); background-size: cover; width:32px; height: 32px; border-radius: 4px;\"></div></td>";
|
||||
content += " <td><pre>" + requests[i].account + "</pre></td>";
|
||||
content += " <td style=\"width: 100%; text-align: center; vertical-align: middle;\">";
|
||||
if (done) {
|
||||
content += " funded";
|
||||
} else {
|
||||
content += " <span id='time-" + i + "' class='timer'>" + moment.duration(-elapsed, 'seconds').humanize(true) + "</span>";
|
||||
}
|
||||
content += " <div class='progress' style='height: 4px; margin: 0;'>";
|
||||
if (done) {
|
||||
content += " <div class='progress-bar progress-bar-success' role='progressbar' aria-valuenow='30' style='width:100%;'></div>";
|
||||
} else if (elapsed > 30) {
|
||||
content += " <div class='progress-bar progress-bar-danger progress-bar-striped active' role='progressbar' aria-valuenow='30' style='width:100%;'></div>";
|
||||
} else {
|
||||
content += " <div class='progress-bar progress-bar-striped active' role='progressbar' aria-valuenow='" + elapsed + "' style='width:" + (elapsed * 100 / 30) + "%;'></div>";
|
||||
}
|
||||
content += " </div>";
|
||||
content += " </td>";
|
||||
content += "</tr>";
|
||||
}
|
||||
$("#requests").html("<tbody>" + content + "</tbody>");
|
||||
}
|
||||
}
|
||||
server.onclose = function() { setTimeout(reconnect, 3000); };
|
||||
}
|
||||
// Start a UI updater to push the progress bars forward until they are done
|
||||
setInterval(function() {
|
||||
$('.progress-bar').each(function() {
|
||||
var progress = Number($(this).attr('aria-valuenow')) + 1;
|
||||
if (progress < 30) {
|
||||
$(this).attr('aria-valuenow', progress);
|
||||
$(this).css('width', (progress * 100 / 30) + '%');
|
||||
} else if (progress == 30) {
|
||||
$(this).css('width', '100%');
|
||||
$(this).addClass("progress-bar-danger");
|
||||
}
|
||||
})
|
||||
$('.timer').each(function() {
|
||||
var index = Number($(this).attr('id').substring(5));
|
||||
$(this).html(moment.duration(moment(requests[index].time).unix()-moment().unix(), 'seconds').humanize(true));
|
||||
})
|
||||
}, 1000);
|
||||
|
||||
// Establish a websocket connection to the API server
|
||||
reconnect();
|
||||
</script>{{if .Recaptcha}}
|
||||
<script src="https://www.google.com/recaptcha/api.js" async defer></script>{{end}}
|
||||
</body>
|
||||
</html>
|
||||
230
cmd/faucet/website.go
Normal file
230
cmd/faucet/website.go
Normal file
File diff suppressed because one or more lines are too long
379
cmd/geth/accountcmd.go
Normal file
379
cmd/geth/accountcmd.go
Normal file
@@ -0,0 +1,379 @@
|
||||
// Copyright 2016 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 (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts"
|
||||
"github.com/ethereum/go-ethereum/accounts/keystore"
|
||||
"github.com/ethereum/go-ethereum/cmd/utils"
|
||||
"github.com/ethereum/go-ethereum/console"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"gopkg.in/urfave/cli.v1"
|
||||
)
|
||||
|
||||
var (
|
||||
walletCommand = cli.Command{
|
||||
Name: "wallet",
|
||||
Usage: "Manage Ethereum presale wallets",
|
||||
ArgsUsage: "",
|
||||
Category: "ACCOUNT COMMANDS",
|
||||
Description: `
|
||||
geth wallet import /path/to/my/presale.wallet
|
||||
|
||||
will prompt for your password and imports your ether presale account.
|
||||
It can be used non-interactively with the --password option taking a
|
||||
passwordfile as argument containing the wallet password in plaintext.`,
|
||||
Subcommands: []cli.Command{
|
||||
{
|
||||
|
||||
Name: "import",
|
||||
Usage: "Import Ethereum presale wallet",
|
||||
ArgsUsage: "<keyFile>",
|
||||
Action: utils.MigrateFlags(importWallet),
|
||||
Category: "ACCOUNT COMMANDS",
|
||||
Flags: []cli.Flag{
|
||||
utils.DataDirFlag,
|
||||
utils.KeyStoreDirFlag,
|
||||
utils.PasswordFileFlag,
|
||||
utils.LightKDFFlag,
|
||||
},
|
||||
Description: `
|
||||
geth wallet [options] /path/to/my/presale.wallet
|
||||
|
||||
will prompt for your password and imports your ether presale account.
|
||||
It can be used non-interactively with the --password option taking a
|
||||
passwordfile as argument containing the wallet password in plaintext.`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
accountCommand = cli.Command{
|
||||
Name: "account",
|
||||
Usage: "Manage accounts",
|
||||
Category: "ACCOUNT COMMANDS",
|
||||
Description: `
|
||||
|
||||
Manage accounts, list all existing accounts, import a private key into a new
|
||||
account, create a new account or update an existing account.
|
||||
|
||||
It supports interactive mode, when you are prompted for password as well as
|
||||
non-interactive mode where passwords are supplied via a given password file.
|
||||
Non-interactive mode is only meant for scripted use on test networks or known
|
||||
safe environments.
|
||||
|
||||
Make sure you remember the password you gave when creating a new account (with
|
||||
either new or import). Without it you are not able to unlock your account.
|
||||
|
||||
Note that exporting your key in unencrypted format is NOT supported.
|
||||
|
||||
Keys are stored under <DATADIR>/keystore.
|
||||
It is safe to transfer the entire directory or the individual keys therein
|
||||
between ethereum nodes by simply copying.
|
||||
|
||||
Make sure you backup your keys regularly.`,
|
||||
Subcommands: []cli.Command{
|
||||
{
|
||||
Name: "list",
|
||||
Usage: "Print summary of existing accounts",
|
||||
Action: utils.MigrateFlags(accountList),
|
||||
Flags: []cli.Flag{
|
||||
utils.DataDirFlag,
|
||||
utils.KeyStoreDirFlag,
|
||||
},
|
||||
Description: `
|
||||
Print a short summary of all accounts`,
|
||||
},
|
||||
{
|
||||
Name: "new",
|
||||
Usage: "Create a new account",
|
||||
Action: utils.MigrateFlags(accountCreate),
|
||||
Flags: []cli.Flag{
|
||||
utils.DataDirFlag,
|
||||
utils.KeyStoreDirFlag,
|
||||
utils.PasswordFileFlag,
|
||||
utils.LightKDFFlag,
|
||||
},
|
||||
Description: `
|
||||
geth account new
|
||||
|
||||
Creates a new account and prints the address.
|
||||
|
||||
The account is saved in encrypted format, you are prompted for a passphrase.
|
||||
|
||||
You must remember this passphrase to unlock your account in the future.
|
||||
|
||||
For non-interactive use the passphrase can be specified with the --password flag:
|
||||
|
||||
Note, this is meant to be used for testing only, it is a bad idea to save your
|
||||
password to file or expose in any other way.
|
||||
`,
|
||||
},
|
||||
{
|
||||
Name: "update",
|
||||
Usage: "Update an existing account",
|
||||
Action: utils.MigrateFlags(accountUpdate),
|
||||
ArgsUsage: "<address>",
|
||||
Flags: []cli.Flag{
|
||||
utils.DataDirFlag,
|
||||
utils.KeyStoreDirFlag,
|
||||
utils.LightKDFFlag,
|
||||
},
|
||||
Description: `
|
||||
geth account update <address>
|
||||
|
||||
Update an existing account.
|
||||
|
||||
The account is saved in the newest version in encrypted format, you are prompted
|
||||
for a passphrase to unlock the account and another to save the updated file.
|
||||
|
||||
This same command can therefore be used to migrate an account of a deprecated
|
||||
format to the newest format or change the password for an account.
|
||||
|
||||
For non-interactive use the passphrase can be specified with the --password flag:
|
||||
|
||||
geth account update [options] <address>
|
||||
|
||||
Since only one password can be given, only format update can be performed,
|
||||
changing your password is only possible interactively.
|
||||
`,
|
||||
},
|
||||
{
|
||||
Name: "import",
|
||||
Usage: "Import a private key into a new account",
|
||||
Action: utils.MigrateFlags(accountImport),
|
||||
Flags: []cli.Flag{
|
||||
utils.DataDirFlag,
|
||||
utils.KeyStoreDirFlag,
|
||||
utils.PasswordFileFlag,
|
||||
utils.LightKDFFlag,
|
||||
},
|
||||
ArgsUsage: "<keyFile>",
|
||||
Description: `
|
||||
geth account import <keyfile>
|
||||
|
||||
Imports an unencrypted private key from <keyfile> and creates a new account.
|
||||
Prints the address.
|
||||
|
||||
The keyfile is assumed to contain an unencrypted private key in hexadecimal format.
|
||||
|
||||
The account is saved in encrypted format, you are prompted for a passphrase.
|
||||
|
||||
You must remember this passphrase to unlock your account in the future.
|
||||
|
||||
For non-interactive use the passphrase can be specified with the -password flag:
|
||||
|
||||
geth account import [options] <keyfile>
|
||||
|
||||
Note:
|
||||
As you can directly copy your encrypted accounts to another ethereum instance,
|
||||
this import mechanism is not needed when you transfer an account between
|
||||
nodes.
|
||||
`,
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func accountList(ctx *cli.Context) error {
|
||||
stack, _ := makeConfigNode(ctx)
|
||||
var index int
|
||||
for _, wallet := range stack.AccountManager().Wallets() {
|
||||
for _, account := range wallet.Accounts() {
|
||||
fmt.Printf("Account #%d: {%x} %s\n", index, account.Address, &account.URL)
|
||||
index++
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// tries unlocking the specified account a few times.
|
||||
func unlockAccount(ctx *cli.Context, ks *keystore.KeyStore, address string, i int, passwords []string) (accounts.Account, string) {
|
||||
account, err := utils.MakeAddress(ks, address)
|
||||
if err != nil {
|
||||
utils.Fatalf("Could not list accounts: %v", err)
|
||||
}
|
||||
for trials := 0; trials < 3; trials++ {
|
||||
prompt := fmt.Sprintf("Unlocking account %s | Attempt %d/%d", address, trials+1, 3)
|
||||
password := getPassPhrase(prompt, false, i, passwords)
|
||||
err = ks.Unlock(account, password)
|
||||
if err == nil {
|
||||
log.Info("Unlocked account", "address", account.Address.Hex())
|
||||
return account, password
|
||||
}
|
||||
if err, ok := err.(*keystore.AmbiguousAddrError); ok {
|
||||
log.Info("Unlocked account", "address", account.Address.Hex())
|
||||
return ambiguousAddrRecovery(ks, err, password), password
|
||||
}
|
||||
if err != keystore.ErrDecrypt {
|
||||
// No need to prompt again if the error is not decryption-related.
|
||||
break
|
||||
}
|
||||
}
|
||||
// All trials expended to unlock account, bail out
|
||||
utils.Fatalf("Failed to unlock account %s (%v)", address, err)
|
||||
|
||||
return accounts.Account{}, ""
|
||||
}
|
||||
|
||||
// getPassPhrase retrieves the password associated with an account, either fetched
|
||||
// from a list of preloaded passphrases, or requested interactively from the user.
|
||||
func getPassPhrase(prompt string, confirmation bool, i int, passwords []string) string {
|
||||
// If a list of passwords was supplied, retrieve from them
|
||||
if len(passwords) > 0 {
|
||||
if i < len(passwords) {
|
||||
return passwords[i]
|
||||
}
|
||||
return passwords[len(passwords)-1]
|
||||
}
|
||||
// Otherwise prompt the user for the password
|
||||
if prompt != "" {
|
||||
fmt.Println(prompt)
|
||||
}
|
||||
password, err := console.Stdin.PromptPassword("Passphrase: ")
|
||||
if err != nil {
|
||||
utils.Fatalf("Failed to read passphrase: %v", err)
|
||||
}
|
||||
if confirmation {
|
||||
confirm, err := console.Stdin.PromptPassword("Repeat passphrase: ")
|
||||
if err != nil {
|
||||
utils.Fatalf("Failed to read passphrase confirmation: %v", err)
|
||||
}
|
||||
if password != confirm {
|
||||
utils.Fatalf("Passphrases do not match")
|
||||
}
|
||||
}
|
||||
return password
|
||||
}
|
||||
|
||||
func ambiguousAddrRecovery(ks *keystore.KeyStore, err *keystore.AmbiguousAddrError, auth string) accounts.Account {
|
||||
fmt.Printf("Multiple key files exist for address %x:\n", err.Addr)
|
||||
for _, a := range err.Matches {
|
||||
fmt.Println(" ", a.URL)
|
||||
}
|
||||
fmt.Println("Testing your passphrase against all of them...")
|
||||
var match *accounts.Account
|
||||
for _, a := range err.Matches {
|
||||
if err := ks.Unlock(a, auth); err == nil {
|
||||
match = &a
|
||||
break
|
||||
}
|
||||
}
|
||||
if match == nil {
|
||||
utils.Fatalf("None of the listed files could be unlocked.")
|
||||
}
|
||||
fmt.Printf("Your passphrase unlocked %s\n", match.URL)
|
||||
fmt.Println("In order to avoid this warning, you need to remove the following duplicate key files:")
|
||||
for _, a := range err.Matches {
|
||||
if a != *match {
|
||||
fmt.Println(" ", a.URL)
|
||||
}
|
||||
}
|
||||
return *match
|
||||
}
|
||||
|
||||
// accountCreate creates a new account into the keystore defined by the CLI flags.
|
||||
func accountCreate(ctx *cli.Context) error {
|
||||
cfg := gethConfig{Node: defaultNodeConfig()}
|
||||
// Load config file.
|
||||
if file := ctx.GlobalString(configFileFlag.Name); file != "" {
|
||||
if err := loadConfig(file, &cfg); err != nil {
|
||||
utils.Fatalf("%v", err)
|
||||
}
|
||||
}
|
||||
utils.SetNodeConfig(ctx, &cfg.Node)
|
||||
scryptN, scryptP, keydir, err := cfg.Node.AccountConfig()
|
||||
|
||||
if err != nil {
|
||||
utils.Fatalf("Failed to read configuration: %v", err)
|
||||
}
|
||||
|
||||
password := getPassPhrase("Your new account is locked with a password. Please give a password. Do not forget this password.", true, 0, utils.MakePasswordList(ctx))
|
||||
|
||||
address, err := keystore.StoreKey(keydir, password, scryptN, scryptP)
|
||||
|
||||
if err != nil {
|
||||
utils.Fatalf("Failed to create account: %v", err)
|
||||
}
|
||||
fmt.Printf("Address: {%x}\n", address)
|
||||
return nil
|
||||
}
|
||||
|
||||
// accountUpdate transitions an account from a previous format to the current
|
||||
// one, also providing the possibility to change the pass-phrase.
|
||||
func accountUpdate(ctx *cli.Context) error {
|
||||
if len(ctx.Args()) == 0 {
|
||||
utils.Fatalf("No accounts specified to update")
|
||||
}
|
||||
stack, _ := makeConfigNode(ctx)
|
||||
ks := stack.AccountManager().Backends(keystore.KeyStoreType)[0].(*keystore.KeyStore)
|
||||
|
||||
for _, addr := range ctx.Args() {
|
||||
account, oldPassword := unlockAccount(ctx, ks, addr, 0, nil)
|
||||
newPassword := getPassPhrase("Please give a new password. Do not forget this password.", true, 0, nil)
|
||||
if err := ks.Update(account, oldPassword, newPassword); err != nil {
|
||||
utils.Fatalf("Could not update the account: %v", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func importWallet(ctx *cli.Context) error {
|
||||
keyfile := ctx.Args().First()
|
||||
if len(keyfile) == 0 {
|
||||
utils.Fatalf("keyfile must be given as argument")
|
||||
}
|
||||
keyJson, err := ioutil.ReadFile(keyfile)
|
||||
if err != nil {
|
||||
utils.Fatalf("Could not read wallet file: %v", err)
|
||||
}
|
||||
|
||||
stack, _ := makeConfigNode(ctx)
|
||||
passphrase := getPassPhrase("", false, 0, utils.MakePasswordList(ctx))
|
||||
|
||||
ks := stack.AccountManager().Backends(keystore.KeyStoreType)[0].(*keystore.KeyStore)
|
||||
acct, err := ks.ImportPreSaleKey(keyJson, passphrase)
|
||||
if err != nil {
|
||||
utils.Fatalf("%v", err)
|
||||
}
|
||||
fmt.Printf("Address: {%x}\n", acct.Address)
|
||||
return nil
|
||||
}
|
||||
|
||||
func accountImport(ctx *cli.Context) error {
|
||||
keyfile := ctx.Args().First()
|
||||
if len(keyfile) == 0 {
|
||||
utils.Fatalf("keyfile must be given as argument")
|
||||
}
|
||||
key, err := crypto.LoadECDSA(keyfile)
|
||||
if err != nil {
|
||||
utils.Fatalf("Failed to load the private key: %v", err)
|
||||
}
|
||||
stack, _ := makeConfigNode(ctx)
|
||||
passphrase := getPassPhrase("Your new account is locked with a password. Please give a password. Do not forget this password.", true, 0, utils.MakePasswordList(ctx))
|
||||
|
||||
ks := stack.AccountManager().Backends(keystore.KeyStoreType)[0].(*keystore.KeyStore)
|
||||
acct, err := ks.ImportECDSA(key, passphrase)
|
||||
if err != nil {
|
||||
utils.Fatalf("Could not create the account: %v", err)
|
||||
}
|
||||
fmt.Printf("Address: {%x}\n", acct.Address)
|
||||
return nil
|
||||
}
|
||||
296
cmd/geth/accountcmd_test.go
Normal file
296
cmd/geth/accountcmd_test.go
Normal file
@@ -0,0 +1,296 @@
|
||||
// Copyright 2016 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 (
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/cespare/cp"
|
||||
)
|
||||
|
||||
// These tests are 'smoke tests' for the account related
|
||||
// subcommands and flags.
|
||||
//
|
||||
// For most tests, the test files from package accounts
|
||||
// are copied into a temporary keystore directory.
|
||||
|
||||
func tmpDatadirWithKeystore(t *testing.T) string {
|
||||
datadir := tmpdir(t)
|
||||
keystore := filepath.Join(datadir, "keystore")
|
||||
source := filepath.Join("..", "..", "accounts", "keystore", "testdata", "keystore")
|
||||
if err := cp.CopyAll(keystore, source); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return datadir
|
||||
}
|
||||
|
||||
func TestAccountListEmpty(t *testing.T) {
|
||||
geth := runGeth(t, "account", "list")
|
||||
geth.ExpectExit()
|
||||
}
|
||||
|
||||
func TestAccountList(t *testing.T) {
|
||||
datadir := tmpDatadirWithKeystore(t)
|
||||
geth := runGeth(t, "account", "list", "--datadir", datadir)
|
||||
defer geth.ExpectExit()
|
||||
if runtime.GOOS == "windows" {
|
||||
geth.Expect(`
|
||||
Account #0: {7ef5a6135f1fd6a02593eedc869c6d41d934aef8} keystore://{{.Datadir}}\keystore\UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8
|
||||
Account #1: {f466859ead1932d743d622cb74fc058882e8648a} keystore://{{.Datadir}}\keystore\aaa
|
||||
Account #2: {289d485d9771714cce91d3393d764e1311907acc} keystore://{{.Datadir}}\keystore\zzz
|
||||
`)
|
||||
} else {
|
||||
geth.Expect(`
|
||||
Account #0: {7ef5a6135f1fd6a02593eedc869c6d41d934aef8} keystore://{{.Datadir}}/keystore/UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8
|
||||
Account #1: {f466859ead1932d743d622cb74fc058882e8648a} keystore://{{.Datadir}}/keystore/aaa
|
||||
Account #2: {289d485d9771714cce91d3393d764e1311907acc} keystore://{{.Datadir}}/keystore/zzz
|
||||
`)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccountNew(t *testing.T) {
|
||||
geth := runGeth(t, "account", "new", "--lightkdf")
|
||||
defer geth.ExpectExit()
|
||||
geth.Expect(`
|
||||
Your new account is locked with a password. Please give a password. Do not forget this password.
|
||||
!! Unsupported terminal, password will be echoed.
|
||||
Passphrase: {{.InputLine "foobar"}}
|
||||
Repeat passphrase: {{.InputLine "foobar"}}
|
||||
`)
|
||||
geth.ExpectRegexp(`Address: \{[0-9a-f]{40}\}\n`)
|
||||
}
|
||||
|
||||
func TestAccountNewBadRepeat(t *testing.T) {
|
||||
geth := runGeth(t, "account", "new", "--lightkdf")
|
||||
defer geth.ExpectExit()
|
||||
geth.Expect(`
|
||||
Your new account is locked with a password. Please give a password. Do not forget this password.
|
||||
!! Unsupported terminal, password will be echoed.
|
||||
Passphrase: {{.InputLine "something"}}
|
||||
Repeat passphrase: {{.InputLine "something else"}}
|
||||
Fatal: Passphrases do not match
|
||||
`)
|
||||
}
|
||||
|
||||
func TestAccountUpdate(t *testing.T) {
|
||||
datadir := tmpDatadirWithKeystore(t)
|
||||
geth := runGeth(t, "account", "update",
|
||||
"--datadir", datadir, "--lightkdf",
|
||||
"f466859ead1932d743d622cb74fc058882e8648a")
|
||||
defer geth.ExpectExit()
|
||||
geth.Expect(`
|
||||
Unlocking account f466859ead1932d743d622cb74fc058882e8648a | Attempt 1/3
|
||||
!! Unsupported terminal, password will be echoed.
|
||||
Passphrase: {{.InputLine "foobar"}}
|
||||
Please give a new password. Do not forget this password.
|
||||
Passphrase: {{.InputLine "foobar2"}}
|
||||
Repeat passphrase: {{.InputLine "foobar2"}}
|
||||
`)
|
||||
}
|
||||
|
||||
func TestWalletImport(t *testing.T) {
|
||||
geth := runGeth(t, "wallet", "import", "--lightkdf", "testdata/guswallet.json")
|
||||
defer geth.ExpectExit()
|
||||
geth.Expect(`
|
||||
!! Unsupported terminal, password will be echoed.
|
||||
Passphrase: {{.InputLine "foo"}}
|
||||
Address: {d4584b5f6229b7be90727b0fc8c6b91bb427821f}
|
||||
`)
|
||||
|
||||
files, err := ioutil.ReadDir(filepath.Join(geth.Datadir, "keystore"))
|
||||
if len(files) != 1 {
|
||||
t.Errorf("expected one key file in keystore directory, found %d files (error: %v)", len(files), err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWalletImportBadPassword(t *testing.T) {
|
||||
geth := runGeth(t, "wallet", "import", "--lightkdf", "testdata/guswallet.json")
|
||||
defer geth.ExpectExit()
|
||||
geth.Expect(`
|
||||
!! Unsupported terminal, password will be echoed.
|
||||
Passphrase: {{.InputLine "wrong"}}
|
||||
Fatal: could not decrypt key with given passphrase
|
||||
`)
|
||||
}
|
||||
|
||||
func TestUnlockFlag(t *testing.T) {
|
||||
datadir := tmpDatadirWithKeystore(t)
|
||||
geth := runGeth(t,
|
||||
"--datadir", datadir, "--nat", "none", "--nodiscover", "--maxpeers", "0", "--port", "0",
|
||||
"--unlock", "f466859ead1932d743d622cb74fc058882e8648a",
|
||||
"js", "testdata/empty.js")
|
||||
geth.Expect(`
|
||||
Unlocking account f466859ead1932d743d622cb74fc058882e8648a | Attempt 1/3
|
||||
!! Unsupported terminal, password will be echoed.
|
||||
Passphrase: {{.InputLine "foobar"}}
|
||||
`)
|
||||
geth.ExpectExit()
|
||||
|
||||
wantMessages := []string{
|
||||
"Unlocked account",
|
||||
"=0xf466859eAD1932D743d622CB74FC058882E8648A",
|
||||
}
|
||||
for _, m := range wantMessages {
|
||||
if !strings.Contains(geth.StderrText(), m) {
|
||||
t.Errorf("stderr text does not contain %q", m)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnlockFlagWrongPassword(t *testing.T) {
|
||||
datadir := tmpDatadirWithKeystore(t)
|
||||
geth := runGeth(t,
|
||||
"--datadir", datadir, "--nat", "none", "--nodiscover", "--maxpeers", "0", "--port", "0",
|
||||
"--unlock", "f466859ead1932d743d622cb74fc058882e8648a")
|
||||
defer geth.ExpectExit()
|
||||
geth.Expect(`
|
||||
Unlocking account f466859ead1932d743d622cb74fc058882e8648a | Attempt 1/3
|
||||
!! Unsupported terminal, password will be echoed.
|
||||
Passphrase: {{.InputLine "wrong1"}}
|
||||
Unlocking account f466859ead1932d743d622cb74fc058882e8648a | Attempt 2/3
|
||||
Passphrase: {{.InputLine "wrong2"}}
|
||||
Unlocking account f466859ead1932d743d622cb74fc058882e8648a | Attempt 3/3
|
||||
Passphrase: {{.InputLine "wrong3"}}
|
||||
Fatal: Failed to unlock account f466859ead1932d743d622cb74fc058882e8648a (could not decrypt key with given passphrase)
|
||||
`)
|
||||
}
|
||||
|
||||
// https://github.com/ethereum/go-ethereum/issues/1785
|
||||
func TestUnlockFlagMultiIndex(t *testing.T) {
|
||||
datadir := tmpDatadirWithKeystore(t)
|
||||
geth := runGeth(t,
|
||||
"--datadir", datadir, "--nat", "none", "--nodiscover", "--maxpeers", "0", "--port", "0",
|
||||
"--unlock", "0,2",
|
||||
"js", "testdata/empty.js")
|
||||
geth.Expect(`
|
||||
Unlocking account 0 | Attempt 1/3
|
||||
!! Unsupported terminal, password will be echoed.
|
||||
Passphrase: {{.InputLine "foobar"}}
|
||||
Unlocking account 2 | Attempt 1/3
|
||||
Passphrase: {{.InputLine "foobar"}}
|
||||
`)
|
||||
geth.ExpectExit()
|
||||
|
||||
wantMessages := []string{
|
||||
"Unlocked account",
|
||||
"=0x7EF5A6135f1FD6a02593eEdC869c6D41D934aef8",
|
||||
"=0x289d485D9771714CCe91D3393D764E1311907ACc",
|
||||
}
|
||||
for _, m := range wantMessages {
|
||||
if !strings.Contains(geth.StderrText(), m) {
|
||||
t.Errorf("stderr text does not contain %q", m)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnlockFlagPasswordFile(t *testing.T) {
|
||||
datadir := tmpDatadirWithKeystore(t)
|
||||
geth := runGeth(t,
|
||||
"--datadir", datadir, "--nat", "none", "--nodiscover", "--maxpeers", "0", "--port", "0",
|
||||
"--password", "testdata/passwords.txt", "--unlock", "0,2",
|
||||
"js", "testdata/empty.js")
|
||||
geth.ExpectExit()
|
||||
|
||||
wantMessages := []string{
|
||||
"Unlocked account",
|
||||
"=0x7EF5A6135f1FD6a02593eEdC869c6D41D934aef8",
|
||||
"=0x289d485D9771714CCe91D3393D764E1311907ACc",
|
||||
}
|
||||
for _, m := range wantMessages {
|
||||
if !strings.Contains(geth.StderrText(), m) {
|
||||
t.Errorf("stderr text does not contain %q", m)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnlockFlagPasswordFileWrongPassword(t *testing.T) {
|
||||
datadir := tmpDatadirWithKeystore(t)
|
||||
geth := runGeth(t,
|
||||
"--datadir", datadir, "--nat", "none", "--nodiscover", "--maxpeers", "0", "--port", "0",
|
||||
"--password", "testdata/wrong-passwords.txt", "--unlock", "0,2")
|
||||
defer geth.ExpectExit()
|
||||
geth.Expect(`
|
||||
Fatal: Failed to unlock account 0 (could not decrypt key with given passphrase)
|
||||
`)
|
||||
}
|
||||
|
||||
func TestUnlockFlagAmbiguous(t *testing.T) {
|
||||
store := filepath.Join("..", "..", "accounts", "keystore", "testdata", "dupes")
|
||||
geth := runGeth(t,
|
||||
"--keystore", store, "--nat", "none", "--nodiscover", "--maxpeers", "0", "--port", "0",
|
||||
"--unlock", "f466859ead1932d743d622cb74fc058882e8648a",
|
||||
"js", "testdata/empty.js")
|
||||
defer geth.ExpectExit()
|
||||
|
||||
// Helper for the expect template, returns absolute keystore path.
|
||||
geth.SetTemplateFunc("keypath", func(file string) string {
|
||||
abs, _ := filepath.Abs(filepath.Join(store, file))
|
||||
return abs
|
||||
})
|
||||
geth.Expect(`
|
||||
Unlocking account f466859ead1932d743d622cb74fc058882e8648a | Attempt 1/3
|
||||
!! Unsupported terminal, password will be echoed.
|
||||
Passphrase: {{.InputLine "foobar"}}
|
||||
Multiple key files exist for address f466859ead1932d743d622cb74fc058882e8648a:
|
||||
keystore://{{keypath "1"}}
|
||||
keystore://{{keypath "2"}}
|
||||
Testing your passphrase against all of them...
|
||||
Your passphrase unlocked keystore://{{keypath "1"}}
|
||||
In order to avoid this warning, you need to remove the following duplicate key files:
|
||||
keystore://{{keypath "2"}}
|
||||
`)
|
||||
geth.ExpectExit()
|
||||
|
||||
wantMessages := []string{
|
||||
"Unlocked account",
|
||||
"=0xf466859eAD1932D743d622CB74FC058882E8648A",
|
||||
}
|
||||
for _, m := range wantMessages {
|
||||
if !strings.Contains(geth.StderrText(), m) {
|
||||
t.Errorf("stderr text does not contain %q", m)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnlockFlagAmbiguousWrongPassword(t *testing.T) {
|
||||
store := filepath.Join("..", "..", "accounts", "keystore", "testdata", "dupes")
|
||||
geth := runGeth(t,
|
||||
"--keystore", store, "--nat", "none", "--nodiscover", "--maxpeers", "0", "--port", "0",
|
||||
"--unlock", "f466859ead1932d743d622cb74fc058882e8648a")
|
||||
defer geth.ExpectExit()
|
||||
|
||||
// Helper for the expect template, returns absolute keystore path.
|
||||
geth.SetTemplateFunc("keypath", func(file string) string {
|
||||
abs, _ := filepath.Abs(filepath.Join(store, file))
|
||||
return abs
|
||||
})
|
||||
geth.Expect(`
|
||||
Unlocking account f466859ead1932d743d622cb74fc058882e8648a | Attempt 1/3
|
||||
!! Unsupported terminal, password will be echoed.
|
||||
Passphrase: {{.InputLine "wrong"}}
|
||||
Multiple key files exist for address f466859ead1932d743d622cb74fc058882e8648a:
|
||||
keystore://{{keypath "1"}}
|
||||
keystore://{{keypath "2"}}
|
||||
Testing your passphrase against all of them...
|
||||
Fatal: None of the listed files could be unlocked.
|
||||
`)
|
||||
geth.ExpectExit()
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
// Copyright 2015 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 (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/codegangsta/cli"
|
||||
"github.com/ethereum/go-ethereum/cmd/utils"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/eth"
|
||||
"github.com/ethereum/go-ethereum/ethdb"
|
||||
"github.com/ethereum/go-ethereum/tests"
|
||||
)
|
||||
|
||||
var blocktestCommand = cli.Command{
|
||||
Action: runBlockTest,
|
||||
Name: "blocktest",
|
||||
Usage: `loads a block test file`,
|
||||
Description: `
|
||||
The first argument should be a block test file.
|
||||
The second argument is the name of a block test from the file.
|
||||
|
||||
The block test will be loaded into an in-memory database.
|
||||
If loading succeeds, the RPC server is started. Clients will
|
||||
be able to interact with the chain defined by the test.
|
||||
`,
|
||||
}
|
||||
|
||||
func runBlockTest(ctx *cli.Context) {
|
||||
var (
|
||||
file, testname string
|
||||
rpc bool
|
||||
)
|
||||
args := ctx.Args()
|
||||
switch {
|
||||
case len(args) == 1:
|
||||
file = args[0]
|
||||
case len(args) == 2:
|
||||
file, testname = args[0], args[1]
|
||||
case len(args) == 3:
|
||||
file, testname = args[0], args[1]
|
||||
rpc = true
|
||||
default:
|
||||
utils.Fatalf(`Usage: ethereum blocktest <path-to-test-file> [ <test-name> [ "rpc" ] ]`)
|
||||
}
|
||||
bt, err := tests.LoadBlockTests(file)
|
||||
if err != nil {
|
||||
utils.Fatalf("%v", err)
|
||||
}
|
||||
|
||||
// run all tests if no test name is specified
|
||||
if testname == "" {
|
||||
ecode := 0
|
||||
for name, test := range bt {
|
||||
fmt.Printf("----------------- Running Block Test %q\n", name)
|
||||
ethereum, err := runOneBlockTest(ctx, test)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
fmt.Println("FAIL")
|
||||
ecode = 1
|
||||
}
|
||||
if ethereum != nil {
|
||||
ethereum.Stop()
|
||||
ethereum.WaitForShutdown()
|
||||
}
|
||||
}
|
||||
os.Exit(ecode)
|
||||
return
|
||||
}
|
||||
// otherwise, run the given test
|
||||
test, ok := bt[testname]
|
||||
if !ok {
|
||||
utils.Fatalf("Test file does not contain test named %q", testname)
|
||||
}
|
||||
ethereum, err := runOneBlockTest(ctx, test)
|
||||
if err != nil {
|
||||
utils.Fatalf("%v", err)
|
||||
}
|
||||
defer ethereum.Stop()
|
||||
if rpc {
|
||||
fmt.Println("Block Test post state validated, starting RPC interface.")
|
||||
startEth(ctx, ethereum)
|
||||
utils.StartRPC(ethereum, ctx)
|
||||
ethereum.WaitForShutdown()
|
||||
}
|
||||
}
|
||||
|
||||
func runOneBlockTest(ctx *cli.Context, test *tests.BlockTest) (*eth.Ethereum, error) {
|
||||
cfg := utils.MakeEthConfig(ClientIdentifier, Version, ctx)
|
||||
cfg.NewDB = func(path string) (common.Database, error) { return ethdb.NewMemDatabase() }
|
||||
cfg.MaxPeers = 0 // disable network
|
||||
cfg.Shh = false // disable whisper
|
||||
cfg.NAT = nil // disable port mapping
|
||||
|
||||
ethereum, err := eth.New(cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// if err := ethereum.Start(); err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
|
||||
// import the genesis block
|
||||
ethereum.ResetWithGenesisBlock(test.Genesis)
|
||||
|
||||
// import pre accounts
|
||||
statedb, err := test.InsertPreState(ethereum)
|
||||
if err != nil {
|
||||
return ethereum, fmt.Errorf("InsertPreState: %v", err)
|
||||
}
|
||||
|
||||
if err := test.TryBlocksInsert(ethereum.ChainManager()); err != nil {
|
||||
return ethereum, fmt.Errorf("Block Test load error: %v", err)
|
||||
}
|
||||
|
||||
if err := test.ValidatePostState(statedb); err != nil {
|
||||
return ethereum, fmt.Errorf("post state validation failed: %v", err)
|
||||
}
|
||||
return ethereum, nil
|
||||
}
|
||||
109
cmd/geth/bugcmd.go
Normal file
109
cmd/geth/bugcmd.go
Normal file
@@ -0,0 +1,109 @@
|
||||
// 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"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/ethereum/go-ethereum/cmd/internal/browser"
|
||||
"github.com/ethereum/go-ethereum/params"
|
||||
|
||||
"github.com/ethereum/go-ethereum/cmd/utils"
|
||||
cli "gopkg.in/urfave/cli.v1"
|
||||
)
|
||||
|
||||
var bugCommand = cli.Command{
|
||||
Action: utils.MigrateFlags(reportBug),
|
||||
Name: "bug",
|
||||
Usage: "opens a window to report a bug on the geth repo",
|
||||
ArgsUsage: " ",
|
||||
Category: "MISCELLANEOUS COMMANDS",
|
||||
}
|
||||
|
||||
const issueUrl = "https://github.com/ethereum/go-ethereum/issues/new"
|
||||
|
||||
// reportBug reports a bug by opening a new URL to the go-ethereum GH issue
|
||||
// tracker and setting default values as the issue body.
|
||||
func reportBug(ctx *cli.Context) error {
|
||||
// execute template and write contents to buff
|
||||
var buff bytes.Buffer
|
||||
|
||||
fmt.Fprintln(&buff, header)
|
||||
fmt.Fprintln(&buff, "Version:", params.Version)
|
||||
fmt.Fprintln(&buff, "Go Version:", runtime.Version())
|
||||
fmt.Fprintln(&buff, "OS:", runtime.GOOS)
|
||||
printOSDetails(&buff)
|
||||
|
||||
// open a new GH issue
|
||||
if !browser.Open(issueUrl + "?body=" + url.QueryEscape(buff.String())) {
|
||||
fmt.Printf("Please file a new issue at %s using this template:\n%s", issueUrl, buff.String())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// copied from the Go source. Copyright 2017 The Go Authors
|
||||
func printOSDetails(w io.Writer) {
|
||||
switch runtime.GOOS {
|
||||
case "darwin":
|
||||
printCmdOut(w, "uname -v: ", "uname", "-v")
|
||||
printCmdOut(w, "", "sw_vers")
|
||||
case "linux":
|
||||
printCmdOut(w, "uname -sr: ", "uname", "-sr")
|
||||
printCmdOut(w, "", "lsb_release", "-a")
|
||||
case "openbsd", "netbsd", "freebsd", "dragonfly":
|
||||
printCmdOut(w, "uname -v: ", "uname", "-v")
|
||||
case "solaris":
|
||||
out, err := ioutil.ReadFile("/etc/release")
|
||||
if err == nil {
|
||||
fmt.Fprintf(w, "/etc/release: %s\n", out)
|
||||
} else {
|
||||
fmt.Printf("failed to read /etc/release: %v\n", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// printCmdOut prints the output of running the given command.
|
||||
// It ignores failures; 'go bug' is best effort.
|
||||
//
|
||||
// copied from the Go source. Copyright 2017 The Go Authors
|
||||
func printCmdOut(w io.Writer, prefix, path string, args ...string) {
|
||||
cmd := exec.Command(path, args...)
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
fmt.Printf("%s %s: %v\n", path, strings.Join(args, " "), err)
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(w, "%s%s\n", prefix, bytes.TrimSpace(out))
|
||||
}
|
||||
|
||||
const header = `Please answer these questions before submitting your issue. Thanks!
|
||||
|
||||
#### What did you do?
|
||||
|
||||
#### What did you expect to see?
|
||||
|
||||
#### What did you see instead?
|
||||
|
||||
#### System details
|
||||
`
|
||||
@@ -17,78 +17,250 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/codegangsta/cli"
|
||||
"github.com/ethereum/go-ethereum/cmd/utils"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/console"
|
||||
"github.com/ethereum/go-ethereum/core"
|
||||
"github.com/ethereum/go-ethereum/core/state"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/logger/glog"
|
||||
"github.com/ethereum/go-ethereum/eth/downloader"
|
||||
"github.com/ethereum/go-ethereum/ethdb"
|
||||
"github.com/ethereum/go-ethereum/event"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/trie"
|
||||
"github.com/syndtr/goleveldb/leveldb/util"
|
||||
"gopkg.in/urfave/cli.v1"
|
||||
)
|
||||
|
||||
var (
|
||||
initCommand = cli.Command{
|
||||
Action: utils.MigrateFlags(initGenesis),
|
||||
Name: "init",
|
||||
Usage: "Bootstrap and initialize a new genesis block",
|
||||
ArgsUsage: "<genesisPath>",
|
||||
Flags: []cli.Flag{
|
||||
utils.DataDirFlag,
|
||||
utils.LightModeFlag,
|
||||
},
|
||||
Category: "BLOCKCHAIN COMMANDS",
|
||||
Description: `
|
||||
The init command initializes a new genesis block and definition for the network.
|
||||
This is a destructive action and changes the network in which you will be
|
||||
participating.
|
||||
|
||||
It expects the genesis file as argument.`,
|
||||
}
|
||||
importCommand = cli.Command{
|
||||
Action: importChain,
|
||||
Name: "import",
|
||||
Usage: `import a blockchain file`,
|
||||
Action: utils.MigrateFlags(importChain),
|
||||
Name: "import",
|
||||
Usage: "Import a blockchain file",
|
||||
ArgsUsage: "<filename> (<filename 2> ... <filename N>) ",
|
||||
Flags: []cli.Flag{
|
||||
utils.DataDirFlag,
|
||||
utils.CacheFlag,
|
||||
utils.LightModeFlag,
|
||||
},
|
||||
Category: "BLOCKCHAIN COMMANDS",
|
||||
Description: `
|
||||
The import command imports blocks from an RLP-encoded form. The form can be one file
|
||||
with several RLP-encoded blocks, or several files can be used.
|
||||
|
||||
If only one file is used, import error will result in failure. If several files are used,
|
||||
processing will proceed even if an individual RLP-file import failure occurs.`,
|
||||
}
|
||||
exportCommand = cli.Command{
|
||||
Action: exportChain,
|
||||
Name: "export",
|
||||
Usage: `export blockchain into file`,
|
||||
Action: utils.MigrateFlags(exportChain),
|
||||
Name: "export",
|
||||
Usage: "Export blockchain into file",
|
||||
ArgsUsage: "<filename> [<blockNumFirst> <blockNumLast>]",
|
||||
Flags: []cli.Flag{
|
||||
utils.DataDirFlag,
|
||||
utils.CacheFlag,
|
||||
utils.LightModeFlag,
|
||||
},
|
||||
Category: "BLOCKCHAIN COMMANDS",
|
||||
Description: `
|
||||
Requires a first argument of the file to write to.
|
||||
Optional second and third arguments control the first and
|
||||
last block to write. In this mode, the file will be appended
|
||||
if already existing.
|
||||
`,
|
||||
if already existing.`,
|
||||
}
|
||||
upgradedbCommand = cli.Command{
|
||||
Action: upgradeDB,
|
||||
Name: "upgradedb",
|
||||
Usage: "upgrade chainblock database",
|
||||
copydbCommand = cli.Command{
|
||||
Action: utils.MigrateFlags(copyDb),
|
||||
Name: "copydb",
|
||||
Usage: "Create a local chain from a target chaindata folder",
|
||||
ArgsUsage: "<sourceChaindataDir>",
|
||||
Flags: []cli.Flag{
|
||||
utils.DataDirFlag,
|
||||
utils.CacheFlag,
|
||||
utils.SyncModeFlag,
|
||||
utils.FakePoWFlag,
|
||||
utils.TestnetFlag,
|
||||
utils.RinkebyFlag,
|
||||
},
|
||||
Category: "BLOCKCHAIN COMMANDS",
|
||||
Description: `
|
||||
The first argument must be the directory containing the blockchain to download from`,
|
||||
}
|
||||
removedbCommand = cli.Command{
|
||||
Action: removeDB,
|
||||
Name: "removedb",
|
||||
Usage: "Remove blockchain and state databases",
|
||||
Action: utils.MigrateFlags(removeDB),
|
||||
Name: "removedb",
|
||||
Usage: "Remove blockchain and state databases",
|
||||
ArgsUsage: " ",
|
||||
Flags: []cli.Flag{
|
||||
utils.DataDirFlag,
|
||||
utils.LightModeFlag,
|
||||
},
|
||||
Category: "BLOCKCHAIN COMMANDS",
|
||||
Description: `
|
||||
Remove blockchain and state databases`,
|
||||
}
|
||||
dumpCommand = cli.Command{
|
||||
Action: dump,
|
||||
Name: "dump",
|
||||
Usage: `dump a specific block from storage`,
|
||||
Action: utils.MigrateFlags(dump),
|
||||
Name: "dump",
|
||||
Usage: "Dump a specific block from storage",
|
||||
ArgsUsage: "[<blockHash> | <blockNum>]...",
|
||||
Flags: []cli.Flag{
|
||||
utils.DataDirFlag,
|
||||
utils.CacheFlag,
|
||||
utils.LightModeFlag,
|
||||
},
|
||||
Category: "BLOCKCHAIN COMMANDS",
|
||||
Description: `
|
||||
The arguments are interpreted as block numbers or hashes.
|
||||
Use "ethereum dump 0" to dump the genesis block.
|
||||
`,
|
||||
Use "ethereum dump 0" to dump the genesis block.`,
|
||||
}
|
||||
)
|
||||
|
||||
func importChain(ctx *cli.Context) {
|
||||
if len(ctx.Args()) != 1 {
|
||||
utils.Fatalf("This command requires an argument.")
|
||||
// initGenesis will initialise the given JSON format genesis file and writes it as
|
||||
// the zero'd block (i.e. genesis) or will fail hard if it can't succeed.
|
||||
func initGenesis(ctx *cli.Context) error {
|
||||
// Make sure we have a valid genesis JSON
|
||||
genesisPath := ctx.Args().First()
|
||||
if len(genesisPath) == 0 {
|
||||
utils.Fatalf("Must supply path to genesis JSON file")
|
||||
}
|
||||
chain, blockDB, stateDB, extraDB := utils.MakeChain(ctx)
|
||||
start := time.Now()
|
||||
err := utils.ImportChain(chain, ctx.Args().First())
|
||||
closeAll(blockDB, stateDB, extraDB)
|
||||
file, err := os.Open(genesisPath)
|
||||
if err != nil {
|
||||
utils.Fatalf("Import error: %v", err)
|
||||
utils.Fatalf("Failed to read genesis file: %v", err)
|
||||
}
|
||||
fmt.Printf("Import done in %v", time.Since(start))
|
||||
defer file.Close()
|
||||
|
||||
genesis := new(core.Genesis)
|
||||
if err := json.NewDecoder(file).Decode(genesis); err != nil {
|
||||
utils.Fatalf("invalid genesis file: %v", err)
|
||||
}
|
||||
// Open an initialise both full and light databases
|
||||
stack := makeFullNode(ctx)
|
||||
for _, name := range []string{"chaindata", "lightchaindata"} {
|
||||
chaindb, err := stack.OpenDatabase(name, 0, 0)
|
||||
if err != nil {
|
||||
utils.Fatalf("Failed to open database: %v", err)
|
||||
}
|
||||
_, hash, err := core.SetupGenesisBlock(chaindb, genesis)
|
||||
if err != nil {
|
||||
utils.Fatalf("Failed to write genesis block: %v", err)
|
||||
}
|
||||
log.Info("Successfully wrote genesis state", "database", name, "hash", hash)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func exportChain(ctx *cli.Context) {
|
||||
func importChain(ctx *cli.Context) error {
|
||||
if len(ctx.Args()) < 1 {
|
||||
utils.Fatalf("This command requires an argument.")
|
||||
}
|
||||
chain, _, _, _ := utils.MakeChain(ctx)
|
||||
stack := makeFullNode(ctx)
|
||||
chain, chainDb := utils.MakeChain(ctx, stack)
|
||||
defer chainDb.Close()
|
||||
|
||||
// Start periodically gathering memory profiles
|
||||
var peakMemAlloc, peakMemSys uint64
|
||||
go func() {
|
||||
stats := new(runtime.MemStats)
|
||||
for {
|
||||
runtime.ReadMemStats(stats)
|
||||
if atomic.LoadUint64(&peakMemAlloc) < stats.Alloc {
|
||||
atomic.StoreUint64(&peakMemAlloc, stats.Alloc)
|
||||
}
|
||||
if atomic.LoadUint64(&peakMemSys) < stats.Sys {
|
||||
atomic.StoreUint64(&peakMemSys, stats.Sys)
|
||||
}
|
||||
time.Sleep(5 * time.Second)
|
||||
}
|
||||
}()
|
||||
// Import the chain
|
||||
start := time.Now()
|
||||
|
||||
if len(ctx.Args()) == 1 {
|
||||
if err := utils.ImportChain(chain, ctx.Args().First()); err != nil {
|
||||
utils.Fatalf("Import error: %v", err)
|
||||
}
|
||||
} else {
|
||||
for _, arg := range ctx.Args() {
|
||||
if err := utils.ImportChain(chain, arg); err != nil {
|
||||
log.Error("Import error", "file", arg, "err", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("Import done in %v.\n\n", time.Since(start))
|
||||
|
||||
// Output pre-compaction stats mostly to see the import trashing
|
||||
db := chainDb.(*ethdb.LDBDatabase)
|
||||
|
||||
stats, err := db.LDB().GetProperty("leveldb.stats")
|
||||
if err != nil {
|
||||
utils.Fatalf("Failed to read database stats: %v", err)
|
||||
}
|
||||
fmt.Println(stats)
|
||||
fmt.Printf("Trie cache misses: %d\n", trie.CacheMisses())
|
||||
fmt.Printf("Trie cache unloads: %d\n\n", trie.CacheUnloads())
|
||||
|
||||
// Print the memory statistics used by the importing
|
||||
mem := new(runtime.MemStats)
|
||||
runtime.ReadMemStats(mem)
|
||||
|
||||
fmt.Printf("Object memory: %.3f MB current, %.3f MB peak\n", float64(mem.Alloc)/1024/1024, float64(atomic.LoadUint64(&peakMemAlloc))/1024/1024)
|
||||
fmt.Printf("System memory: %.3f MB current, %.3f MB peak\n", float64(mem.Sys)/1024/1024, float64(atomic.LoadUint64(&peakMemSys))/1024/1024)
|
||||
fmt.Printf("Allocations: %.3f million\n", float64(mem.Mallocs)/1000000)
|
||||
fmt.Printf("GC pause: %v\n\n", time.Duration(mem.PauseTotalNs))
|
||||
|
||||
if ctx.GlobalIsSet(utils.NoCompactionFlag.Name) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Compact the entire database to more accurately measure disk io and print the stats
|
||||
start = time.Now()
|
||||
fmt.Println("Compacting entire database...")
|
||||
if err = db.LDB().CompactRange(util.Range{}); err != nil {
|
||||
utils.Fatalf("Compaction failed: %v", err)
|
||||
}
|
||||
fmt.Printf("Compaction done in %v.\n\n", time.Since(start))
|
||||
|
||||
stats, err = db.LDB().GetProperty("leveldb.stats")
|
||||
if err != nil {
|
||||
utils.Fatalf("Failed to read database stats: %v", err)
|
||||
}
|
||||
fmt.Println(stats)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func exportChain(ctx *cli.Context) error {
|
||||
if len(ctx.Args()) < 1 {
|
||||
utils.Fatalf("This command requires an argument.")
|
||||
}
|
||||
stack := makeFullNode(ctx)
|
||||
chain, _ := utils.MakeChain(ctx, stack)
|
||||
start := time.Now()
|
||||
|
||||
var err error
|
||||
@@ -112,66 +284,93 @@ func exportChain(ctx *cli.Context) {
|
||||
utils.Fatalf("Export error: %v\n", err)
|
||||
}
|
||||
fmt.Printf("Export done in %v", time.Since(start))
|
||||
return nil
|
||||
}
|
||||
|
||||
func removeDB(ctx *cli.Context) {
|
||||
confirm, err := utils.PromptConfirm("Remove local databases?")
|
||||
func copyDb(ctx *cli.Context) error {
|
||||
// Ensure we have a source chain directory to copy
|
||||
if len(ctx.Args()) != 1 {
|
||||
utils.Fatalf("Source chaindata directory path argument missing")
|
||||
}
|
||||
// Initialize a new chain for the running node to sync into
|
||||
stack := makeFullNode(ctx)
|
||||
chain, chainDb := utils.MakeChain(ctx, stack)
|
||||
|
||||
syncmode := *utils.GlobalTextMarshaler(ctx, utils.SyncModeFlag.Name).(*downloader.SyncMode)
|
||||
dl := downloader.New(syncmode, chainDb, new(event.TypeMux), chain, nil, nil)
|
||||
|
||||
// Create a source peer to satisfy downloader requests from
|
||||
db, err := ethdb.NewLDBDatabase(ctx.Args().First(), ctx.GlobalInt(utils.CacheFlag.Name), 256)
|
||||
if err != nil {
|
||||
utils.Fatalf("%v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if confirm {
|
||||
fmt.Println("Removing chain and state databases...")
|
||||
start := time.Now()
|
||||
|
||||
os.RemoveAll(filepath.Join(ctx.GlobalString(utils.DataDirFlag.Name), "blockchain"))
|
||||
os.RemoveAll(filepath.Join(ctx.GlobalString(utils.DataDirFlag.Name), "state"))
|
||||
|
||||
fmt.Printf("Removed in %v\n", time.Since(start))
|
||||
} else {
|
||||
fmt.Println("Operation aborted")
|
||||
}
|
||||
}
|
||||
|
||||
func upgradeDB(ctx *cli.Context) {
|
||||
glog.Infoln("Upgrading blockchain database")
|
||||
|
||||
chain, blockDB, stateDB, extraDB := utils.MakeChain(ctx)
|
||||
v, _ := blockDB.Get([]byte("BlockchainVersion"))
|
||||
bcVersion := int(common.NewValue(v).Uint())
|
||||
if bcVersion == 0 {
|
||||
bcVersion = core.BlockChainVersion
|
||||
}
|
||||
|
||||
// Export the current chain.
|
||||
filename := fmt.Sprintf("blockchain_%d_%s.chain", bcVersion, time.Now().Format("20060102_150405"))
|
||||
exportFile := filepath.Join(ctx.GlobalString(utils.DataDirFlag.Name), filename)
|
||||
if err := utils.ExportChain(chain, exportFile); err != nil {
|
||||
utils.Fatalf("Unable to export chain for reimport %s", err)
|
||||
}
|
||||
closeAll(blockDB, stateDB, extraDB)
|
||||
os.RemoveAll(filepath.Join(ctx.GlobalString(utils.DataDirFlag.Name), "blockchain"))
|
||||
os.RemoveAll(filepath.Join(ctx.GlobalString(utils.DataDirFlag.Name), "state"))
|
||||
|
||||
// Import the chain file.
|
||||
chain, blockDB, stateDB, extraDB = utils.MakeChain(ctx)
|
||||
blockDB.Put([]byte("BlockchainVersion"), common.NewValue(core.BlockChainVersion).Bytes())
|
||||
err := utils.ImportChain(chain, exportFile)
|
||||
closeAll(blockDB, stateDB, extraDB)
|
||||
hc, err := core.NewHeaderChain(db, chain.Config(), chain.Engine(), func() bool { return false })
|
||||
if err != nil {
|
||||
utils.Fatalf("Import error %v (a backup is made in %s, use the import command to import it)", err, exportFile)
|
||||
} else {
|
||||
os.Remove(exportFile)
|
||||
glog.Infoln("Import finished")
|
||||
return err
|
||||
}
|
||||
peer := downloader.NewFakePeer("local", db, hc, dl)
|
||||
if err = dl.RegisterPeer("local", 63, peer); err != nil {
|
||||
return err
|
||||
}
|
||||
// Synchronise with the simulated peer
|
||||
start := time.Now()
|
||||
|
||||
currentHeader := hc.CurrentHeader()
|
||||
if err = dl.Synchronise("local", currentHeader.Hash(), hc.GetTd(currentHeader.Hash(), currentHeader.Number.Uint64()), syncmode); err != nil {
|
||||
return err
|
||||
}
|
||||
for dl.Synchronising() {
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
fmt.Printf("Database copy done in %v\n", time.Since(start))
|
||||
|
||||
// Compact the entire database to remove any sync overhead
|
||||
start = time.Now()
|
||||
fmt.Println("Compacting entire database...")
|
||||
if err = chainDb.(*ethdb.LDBDatabase).LDB().CompactRange(util.Range{}); err != nil {
|
||||
utils.Fatalf("Compaction failed: %v", err)
|
||||
}
|
||||
fmt.Printf("Compaction done in %v.\n\n", time.Since(start))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func dump(ctx *cli.Context) {
|
||||
chain, _, stateDB, _ := utils.MakeChain(ctx)
|
||||
func removeDB(ctx *cli.Context) error {
|
||||
stack, _ := makeConfigNode(ctx)
|
||||
|
||||
for _, name := range []string{"chaindata", "lightchaindata"} {
|
||||
// Ensure the database exists in the first place
|
||||
logger := log.New("database", name)
|
||||
|
||||
dbdir := stack.ResolvePath(name)
|
||||
if !common.FileExist(dbdir) {
|
||||
logger.Info("Database doesn't exist, skipping", "path", dbdir)
|
||||
continue
|
||||
}
|
||||
// Confirm removal and execute
|
||||
fmt.Println(dbdir)
|
||||
confirm, err := console.Stdin.PromptConfirm("Remove this database?")
|
||||
switch {
|
||||
case err != nil:
|
||||
utils.Fatalf("%v", err)
|
||||
case !confirm:
|
||||
logger.Warn("Database deletion aborted")
|
||||
default:
|
||||
start := time.Now()
|
||||
os.RemoveAll(dbdir)
|
||||
logger.Info("Database successfully deleted", "elapsed", common.PrettyDuration(time.Since(start)))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func dump(ctx *cli.Context) error {
|
||||
stack := makeFullNode(ctx)
|
||||
chain, chainDb := utils.MakeChain(ctx, stack)
|
||||
for _, arg := range ctx.Args() {
|
||||
var block *types.Block
|
||||
if hashish(arg) {
|
||||
block = chain.GetBlock(common.HexToHash(arg))
|
||||
block = chain.GetBlockByHash(common.HexToHash(arg))
|
||||
} else {
|
||||
num, _ := strconv.Atoi(arg)
|
||||
block = chain.GetBlockByNumber(uint64(num))
|
||||
@@ -180,10 +379,15 @@ func dump(ctx *cli.Context) {
|
||||
fmt.Println("{}")
|
||||
utils.Fatalf("block not found")
|
||||
} else {
|
||||
state := state.New(block.Root(), stateDB)
|
||||
state, err := state.New(block.Root(), state.NewDatabase(chainDb))
|
||||
if err != nil {
|
||||
utils.Fatalf("could not create new state: %v", err)
|
||||
}
|
||||
fmt.Printf("%s\n", state.Dump())
|
||||
}
|
||||
}
|
||||
chainDb.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
// hashish returns true for strings that look like hashes.
|
||||
@@ -191,9 +395,3 @@ func hashish(x string) bool {
|
||||
_, err := strconv.Atoi(x)
|
||||
return err != nil
|
||||
}
|
||||
|
||||
func closeAll(dbs ...common.Database) {
|
||||
for _, db := range dbs {
|
||||
db.Close()
|
||||
}
|
||||
}
|
||||
|
||||
215
cmd/geth/config.go
Normal file
215
cmd/geth/config.go
Normal file
@@ -0,0 +1,215 @@
|
||||
// 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 (
|
||||
"bufio"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"reflect"
|
||||
"unicode"
|
||||
|
||||
cli "gopkg.in/urfave/cli.v1"
|
||||
|
||||
"github.com/ethereum/go-ethereum/cmd/utils"
|
||||
"github.com/ethereum/go-ethereum/contracts/release"
|
||||
"github.com/ethereum/go-ethereum/dashboard"
|
||||
"github.com/ethereum/go-ethereum/eth"
|
||||
"github.com/ethereum/go-ethereum/node"
|
||||
"github.com/ethereum/go-ethereum/params"
|
||||
whisper "github.com/ethereum/go-ethereum/whisper/whisperv5"
|
||||
"github.com/naoina/toml"
|
||||
)
|
||||
|
||||
var (
|
||||
dumpConfigCommand = cli.Command{
|
||||
Action: utils.MigrateFlags(dumpConfig),
|
||||
Name: "dumpconfig",
|
||||
Usage: "Show configuration values",
|
||||
ArgsUsage: "",
|
||||
Flags: append(append(nodeFlags, rpcFlags...), whisperFlags...),
|
||||
Category: "MISCELLANEOUS COMMANDS",
|
||||
Description: `The dumpconfig command shows configuration values.`,
|
||||
}
|
||||
|
||||
configFileFlag = cli.StringFlag{
|
||||
Name: "config",
|
||||
Usage: "TOML configuration file",
|
||||
}
|
||||
)
|
||||
|
||||
// These settings ensure that TOML keys use the same names as Go struct fields.
|
||||
var tomlSettings = toml.Config{
|
||||
NormFieldName: func(rt reflect.Type, key string) string {
|
||||
return key
|
||||
},
|
||||
FieldToKey: func(rt reflect.Type, field string) string {
|
||||
return field
|
||||
},
|
||||
MissingField: func(rt reflect.Type, field string) error {
|
||||
link := ""
|
||||
if unicode.IsUpper(rune(rt.Name()[0])) && rt.PkgPath() != "main" {
|
||||
link = fmt.Sprintf(", see https://godoc.org/%s#%s for available fields", rt.PkgPath(), rt.Name())
|
||||
}
|
||||
return fmt.Errorf("field '%s' is not defined in %s%s", field, rt.String(), link)
|
||||
},
|
||||
}
|
||||
|
||||
type ethstatsConfig struct {
|
||||
URL string `toml:",omitempty"`
|
||||
}
|
||||
|
||||
type gethConfig struct {
|
||||
Eth eth.Config
|
||||
Shh whisper.Config
|
||||
Node node.Config
|
||||
Ethstats ethstatsConfig
|
||||
Dashboard dashboard.Config
|
||||
}
|
||||
|
||||
func loadConfig(file string, cfg *gethConfig) error {
|
||||
f, err := os.Open(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
err = tomlSettings.NewDecoder(bufio.NewReader(f)).Decode(cfg)
|
||||
// Add file name to errors that have a line number.
|
||||
if _, ok := err.(*toml.LineError); ok {
|
||||
err = errors.New(file + ", " + err.Error())
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func defaultNodeConfig() node.Config {
|
||||
cfg := node.DefaultConfig
|
||||
cfg.Name = clientIdentifier
|
||||
cfg.Version = params.VersionWithCommit(gitCommit)
|
||||
cfg.HTTPModules = append(cfg.HTTPModules, "eth", "shh")
|
||||
cfg.WSModules = append(cfg.WSModules, "eth", "shh")
|
||||
cfg.IPCPath = "geth.ipc"
|
||||
return cfg
|
||||
}
|
||||
|
||||
func makeConfigNode(ctx *cli.Context) (*node.Node, gethConfig) {
|
||||
// Load defaults.
|
||||
cfg := gethConfig{
|
||||
Eth: eth.DefaultConfig,
|
||||
Shh: whisper.DefaultConfig,
|
||||
Node: defaultNodeConfig(),
|
||||
Dashboard: dashboard.DefaultConfig,
|
||||
}
|
||||
|
||||
// Load config file.
|
||||
if file := ctx.GlobalString(configFileFlag.Name); file != "" {
|
||||
if err := loadConfig(file, &cfg); err != nil {
|
||||
utils.Fatalf("%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Apply flags.
|
||||
utils.SetNodeConfig(ctx, &cfg.Node)
|
||||
stack, err := node.New(&cfg.Node)
|
||||
if err != nil {
|
||||
utils.Fatalf("Failed to create the protocol stack: %v", err)
|
||||
}
|
||||
utils.SetEthConfig(ctx, stack, &cfg.Eth)
|
||||
if ctx.GlobalIsSet(utils.EthStatsURLFlag.Name) {
|
||||
cfg.Ethstats.URL = ctx.GlobalString(utils.EthStatsURLFlag.Name)
|
||||
}
|
||||
|
||||
utils.SetShhConfig(ctx, stack, &cfg.Shh)
|
||||
utils.SetDashboardConfig(ctx, &cfg.Dashboard)
|
||||
|
||||
return stack, cfg
|
||||
}
|
||||
|
||||
// enableWhisper returns true in case one of the whisper flags is set.
|
||||
func enableWhisper(ctx *cli.Context) bool {
|
||||
for _, flag := range whisperFlags {
|
||||
if ctx.GlobalIsSet(flag.GetName()) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func makeFullNode(ctx *cli.Context) *node.Node {
|
||||
stack, cfg := makeConfigNode(ctx)
|
||||
|
||||
utils.RegisterEthService(stack, &cfg.Eth)
|
||||
|
||||
if ctx.GlobalBool(utils.DashboardEnabledFlag.Name) {
|
||||
utils.RegisterDashboardService(stack, &cfg.Dashboard)
|
||||
}
|
||||
// Whisper must be explicitly enabled by specifying at least 1 whisper flag or in dev mode
|
||||
shhEnabled := enableWhisper(ctx)
|
||||
shhAutoEnabled := !ctx.GlobalIsSet(utils.WhisperEnabledFlag.Name) && ctx.GlobalIsSet(utils.DeveloperFlag.Name)
|
||||
if shhEnabled || shhAutoEnabled {
|
||||
if ctx.GlobalIsSet(utils.WhisperMaxMessageSizeFlag.Name) {
|
||||
cfg.Shh.MaxMessageSize = uint32(ctx.Int(utils.WhisperMaxMessageSizeFlag.Name))
|
||||
}
|
||||
if ctx.GlobalIsSet(utils.WhisperMinPOWFlag.Name) {
|
||||
cfg.Shh.MinimumAcceptedPOW = ctx.Float64(utils.WhisperMinPOWFlag.Name)
|
||||
}
|
||||
utils.RegisterShhService(stack, &cfg.Shh)
|
||||
}
|
||||
|
||||
// Add the Ethereum Stats daemon if requested.
|
||||
if cfg.Ethstats.URL != "" {
|
||||
utils.RegisterEthStatsService(stack, cfg.Ethstats.URL)
|
||||
}
|
||||
|
||||
// Add the release oracle service so it boots along with node.
|
||||
if err := stack.Register(func(ctx *node.ServiceContext) (node.Service, error) {
|
||||
config := release.Config{
|
||||
Oracle: relOracle,
|
||||
Major: uint32(params.VersionMajor),
|
||||
Minor: uint32(params.VersionMinor),
|
||||
Patch: uint32(params.VersionPatch),
|
||||
}
|
||||
commit, _ := hex.DecodeString(gitCommit)
|
||||
copy(config.Commit[:], commit)
|
||||
return release.NewReleaseService(ctx, config)
|
||||
}); err != nil {
|
||||
utils.Fatalf("Failed to register the Geth release oracle service: %v", err)
|
||||
}
|
||||
return stack
|
||||
}
|
||||
|
||||
// dumpConfig is the dumpconfig command.
|
||||
func dumpConfig(ctx *cli.Context) error {
|
||||
_, cfg := makeConfigNode(ctx)
|
||||
comment := ""
|
||||
|
||||
if cfg.Eth.Genesis != nil {
|
||||
cfg.Eth.Genesis = nil
|
||||
comment += "# Note: this config doesn't contain the genesis block.\n\n"
|
||||
}
|
||||
|
||||
out, err := tomlSettings.Marshal(&cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
io.WriteString(os.Stdout, comment)
|
||||
os.Stdout.Write(out)
|
||||
return nil
|
||||
}
|
||||
219
cmd/geth/consolecmd.go
Normal file
219
cmd/geth/consolecmd.go
Normal file
@@ -0,0 +1,219 @@
|
||||
// Copyright 2016 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 (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/ethereum/go-ethereum/cmd/utils"
|
||||
"github.com/ethereum/go-ethereum/console"
|
||||
"github.com/ethereum/go-ethereum/node"
|
||||
"github.com/ethereum/go-ethereum/rpc"
|
||||
"gopkg.in/urfave/cli.v1"
|
||||
)
|
||||
|
||||
var (
|
||||
consoleFlags = []cli.Flag{utils.JSpathFlag, utils.ExecFlag, utils.PreloadJSFlag}
|
||||
|
||||
consoleCommand = cli.Command{
|
||||
Action: utils.MigrateFlags(localConsole),
|
||||
Name: "console",
|
||||
Usage: "Start an interactive JavaScript environment",
|
||||
Flags: append(append(append(nodeFlags, rpcFlags...), consoleFlags...), whisperFlags...),
|
||||
Category: "CONSOLE COMMANDS",
|
||||
Description: `
|
||||
The Geth console is an interactive shell for the JavaScript runtime environment
|
||||
which exposes a node admin interface as well as the Ðapp JavaScript API.
|
||||
See https://github.com/ethereum/go-ethereum/wiki/Javascipt-Console.`,
|
||||
}
|
||||
|
||||
attachCommand = cli.Command{
|
||||
Action: utils.MigrateFlags(remoteConsole),
|
||||
Name: "attach",
|
||||
Usage: "Start an interactive JavaScript environment (connect to node)",
|
||||
ArgsUsage: "[endpoint]",
|
||||
Flags: append(consoleFlags, utils.DataDirFlag),
|
||||
Category: "CONSOLE COMMANDS",
|
||||
Description: `
|
||||
The Geth console is an interactive shell for the JavaScript runtime environment
|
||||
which exposes a node admin interface as well as the Ðapp JavaScript API.
|
||||
See https://github.com/ethereum/go-ethereum/wiki/Javascipt-Console.
|
||||
This command allows to open a console on a running geth node.`,
|
||||
}
|
||||
|
||||
javascriptCommand = cli.Command{
|
||||
Action: utils.MigrateFlags(ephemeralConsole),
|
||||
Name: "js",
|
||||
Usage: "Execute the specified JavaScript files",
|
||||
ArgsUsage: "<jsfile> [jsfile...]",
|
||||
Flags: append(nodeFlags, consoleFlags...),
|
||||
Category: "CONSOLE COMMANDS",
|
||||
Description: `
|
||||
The JavaScript VM exposes a node admin interface as well as the Ðapp
|
||||
JavaScript API. See https://github.com/ethereum/go-ethereum/wiki/Javascipt-Console`,
|
||||
}
|
||||
)
|
||||
|
||||
// localConsole starts a new geth node, attaching a JavaScript console to it at the
|
||||
// same time.
|
||||
func localConsole(ctx *cli.Context) error {
|
||||
// Create and start the node based on the CLI flags
|
||||
node := makeFullNode(ctx)
|
||||
startNode(ctx, node)
|
||||
defer node.Stop()
|
||||
|
||||
// Attach to the newly started node and start the JavaScript console
|
||||
client, err := node.Attach()
|
||||
if err != nil {
|
||||
utils.Fatalf("Failed to attach to the inproc geth: %v", err)
|
||||
}
|
||||
config := console.Config{
|
||||
DataDir: utils.MakeDataDir(ctx),
|
||||
DocRoot: ctx.GlobalString(utils.JSpathFlag.Name),
|
||||
Client: client,
|
||||
Preload: utils.MakeConsolePreloads(ctx),
|
||||
}
|
||||
|
||||
console, err := console.New(config)
|
||||
if err != nil {
|
||||
utils.Fatalf("Failed to start the JavaScript console: %v", err)
|
||||
}
|
||||
defer console.Stop(false)
|
||||
|
||||
// If only a short execution was requested, evaluate and return
|
||||
if script := ctx.GlobalString(utils.ExecFlag.Name); script != "" {
|
||||
console.Evaluate(script)
|
||||
return nil
|
||||
}
|
||||
// Otherwise print the welcome screen and enter interactive mode
|
||||
console.Welcome()
|
||||
console.Interactive()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// remoteConsole will connect to a remote geth instance, attaching a JavaScript
|
||||
// console to it.
|
||||
func remoteConsole(ctx *cli.Context) error {
|
||||
// Attach to a remotely running geth instance and start the JavaScript console
|
||||
endpoint := ctx.Args().First()
|
||||
if endpoint == "" {
|
||||
path := node.DefaultDataDir()
|
||||
if ctx.GlobalIsSet(utils.DataDirFlag.Name) {
|
||||
path = ctx.GlobalString(utils.DataDirFlag.Name)
|
||||
}
|
||||
if path != "" {
|
||||
if ctx.GlobalBool(utils.TestnetFlag.Name) {
|
||||
path = filepath.Join(path, "testnet")
|
||||
} else if ctx.GlobalBool(utils.RinkebyFlag.Name) {
|
||||
path = filepath.Join(path, "rinkeby")
|
||||
}
|
||||
}
|
||||
endpoint = fmt.Sprintf("%s/geth.ipc", path)
|
||||
}
|
||||
client, err := dialRPC(endpoint)
|
||||
if err != nil {
|
||||
utils.Fatalf("Unable to attach to remote geth: %v", err)
|
||||
}
|
||||
config := console.Config{
|
||||
DataDir: utils.MakeDataDir(ctx),
|
||||
DocRoot: ctx.GlobalString(utils.JSpathFlag.Name),
|
||||
Client: client,
|
||||
Preload: utils.MakeConsolePreloads(ctx),
|
||||
}
|
||||
|
||||
console, err := console.New(config)
|
||||
if err != nil {
|
||||
utils.Fatalf("Failed to start the JavaScript console: %v", err)
|
||||
}
|
||||
defer console.Stop(false)
|
||||
|
||||
if script := ctx.GlobalString(utils.ExecFlag.Name); script != "" {
|
||||
console.Evaluate(script)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Otherwise print the welcome screen and enter interactive mode
|
||||
console.Welcome()
|
||||
console.Interactive()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// dialRPC returns a RPC client which connects to the given endpoint.
|
||||
// The check for empty endpoint implements the defaulting logic
|
||||
// for "geth attach" and "geth monitor" with no argument.
|
||||
func dialRPC(endpoint string) (*rpc.Client, error) {
|
||||
if endpoint == "" {
|
||||
endpoint = node.DefaultIPCEndpoint(clientIdentifier)
|
||||
} else if strings.HasPrefix(endpoint, "rpc:") || strings.HasPrefix(endpoint, "ipc:") {
|
||||
// Backwards compatibility with geth < 1.5 which required
|
||||
// these prefixes.
|
||||
endpoint = endpoint[4:]
|
||||
}
|
||||
return rpc.Dial(endpoint)
|
||||
}
|
||||
|
||||
// ephemeralConsole starts a new geth node, attaches an ephemeral JavaScript
|
||||
// console to it, executes each of the files specified as arguments and tears
|
||||
// everything down.
|
||||
func ephemeralConsole(ctx *cli.Context) error {
|
||||
// Create and start the node based on the CLI flags
|
||||
node := makeFullNode(ctx)
|
||||
startNode(ctx, node)
|
||||
defer node.Stop()
|
||||
|
||||
// Attach to the newly started node and start the JavaScript console
|
||||
client, err := node.Attach()
|
||||
if err != nil {
|
||||
utils.Fatalf("Failed to attach to the inproc geth: %v", err)
|
||||
}
|
||||
config := console.Config{
|
||||
DataDir: utils.MakeDataDir(ctx),
|
||||
DocRoot: ctx.GlobalString(utils.JSpathFlag.Name),
|
||||
Client: client,
|
||||
Preload: utils.MakeConsolePreloads(ctx),
|
||||
}
|
||||
|
||||
console, err := console.New(config)
|
||||
if err != nil {
|
||||
utils.Fatalf("Failed to start the JavaScript console: %v", err)
|
||||
}
|
||||
defer console.Stop(false)
|
||||
|
||||
// Evaluate each of the specified JavaScript files
|
||||
for _, file := range ctx.Args() {
|
||||
if err = console.Execute(file); err != nil {
|
||||
utils.Fatalf("Failed to execute %s: %v", file, err)
|
||||
}
|
||||
}
|
||||
// Wait for pending callbacks, but stop for Ctrl-C.
|
||||
abort := make(chan os.Signal, 1)
|
||||
signal.Notify(abort, os.Interrupt)
|
||||
|
||||
go func() {
|
||||
<-abort
|
||||
os.Exit(0)
|
||||
}()
|
||||
console.Stop(true)
|
||||
|
||||
return nil
|
||||
}
|
||||
163
cmd/geth/consolecmd_test.go
Normal file
163
cmd/geth/consolecmd_test.go
Normal file
@@ -0,0 +1,163 @@
|
||||
// Copyright 2016 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 (
|
||||
"crypto/rand"
|
||||
"math/big"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/params"
|
||||
)
|
||||
|
||||
const (
|
||||
ipcAPIs = "admin:1.0 debug:1.0 eth:1.0 miner:1.0 net:1.0 personal:1.0 rpc:1.0 shh:1.0 txpool:1.0 web3:1.0"
|
||||
httpAPIs = "eth:1.0 net:1.0 rpc:1.0 web3:1.0"
|
||||
)
|
||||
|
||||
// Tests that a node embedded within a console can be started up properly and
|
||||
// then terminated by closing the input stream.
|
||||
func TestConsoleWelcome(t *testing.T) {
|
||||
coinbase := "0x8605cdbbdb6d264aa742e77020dcbc58fcdce182"
|
||||
|
||||
// Start a geth console, make sure it's cleaned up and terminate the console
|
||||
geth := runGeth(t,
|
||||
"--port", "0", "--maxpeers", "0", "--nodiscover", "--nat", "none",
|
||||
"--etherbase", coinbase, "--shh",
|
||||
"console")
|
||||
|
||||
// Gather all the infos the welcome message needs to contain
|
||||
geth.SetTemplateFunc("goos", func() string { return runtime.GOOS })
|
||||
geth.SetTemplateFunc("goarch", func() string { return runtime.GOARCH })
|
||||
geth.SetTemplateFunc("gover", runtime.Version)
|
||||
geth.SetTemplateFunc("gethver", func() string { return params.Version })
|
||||
geth.SetTemplateFunc("niltime", func() string { return time.Unix(0, 0).Format(time.RFC1123) })
|
||||
geth.SetTemplateFunc("apis", func() string { return ipcAPIs })
|
||||
|
||||
// Verify the actual welcome message to the required template
|
||||
geth.Expect(`
|
||||
Welcome to the Geth JavaScript console!
|
||||
|
||||
instance: Geth/v{{gethver}}/{{goos}}-{{goarch}}/{{gover}}
|
||||
coinbase: {{.Etherbase}}
|
||||
at block: 0 ({{niltime}})
|
||||
datadir: {{.Datadir}}
|
||||
modules: {{apis}}
|
||||
|
||||
> {{.InputLine "exit"}}
|
||||
`)
|
||||
geth.ExpectExit()
|
||||
}
|
||||
|
||||
// Tests that a console can be attached to a running node via various means.
|
||||
func TestIPCAttachWelcome(t *testing.T) {
|
||||
// Configure the instance for IPC attachement
|
||||
coinbase := "0x8605cdbbdb6d264aa742e77020dcbc58fcdce182"
|
||||
var ipc string
|
||||
if runtime.GOOS == "windows" {
|
||||
ipc = `\\.\pipe\geth` + strconv.Itoa(trulyRandInt(100000, 999999))
|
||||
} else {
|
||||
ws := tmpdir(t)
|
||||
defer os.RemoveAll(ws)
|
||||
ipc = filepath.Join(ws, "geth.ipc")
|
||||
}
|
||||
// Note: we need --shh because testAttachWelcome checks for default
|
||||
// list of ipc modules and shh is included there.
|
||||
geth := runGeth(t,
|
||||
"--port", "0", "--maxpeers", "0", "--nodiscover", "--nat", "none",
|
||||
"--etherbase", coinbase, "--shh", "--ipcpath", ipc)
|
||||
|
||||
time.Sleep(2 * time.Second) // Simple way to wait for the RPC endpoint to open
|
||||
testAttachWelcome(t, geth, "ipc:"+ipc, ipcAPIs)
|
||||
|
||||
geth.Interrupt()
|
||||
geth.ExpectExit()
|
||||
}
|
||||
|
||||
func TestHTTPAttachWelcome(t *testing.T) {
|
||||
coinbase := "0x8605cdbbdb6d264aa742e77020dcbc58fcdce182"
|
||||
port := strconv.Itoa(trulyRandInt(1024, 65536)) // Yeah, sometimes this will fail, sorry :P
|
||||
geth := runGeth(t,
|
||||
"--port", "0", "--maxpeers", "0", "--nodiscover", "--nat", "none",
|
||||
"--etherbase", coinbase, "--rpc", "--rpcport", port)
|
||||
|
||||
time.Sleep(2 * time.Second) // Simple way to wait for the RPC endpoint to open
|
||||
testAttachWelcome(t, geth, "http://localhost:"+port, httpAPIs)
|
||||
|
||||
geth.Interrupt()
|
||||
geth.ExpectExit()
|
||||
}
|
||||
|
||||
func TestWSAttachWelcome(t *testing.T) {
|
||||
coinbase := "0x8605cdbbdb6d264aa742e77020dcbc58fcdce182"
|
||||
port := strconv.Itoa(trulyRandInt(1024, 65536)) // Yeah, sometimes this will fail, sorry :P
|
||||
|
||||
geth := runGeth(t,
|
||||
"--port", "0", "--maxpeers", "0", "--nodiscover", "--nat", "none",
|
||||
"--etherbase", coinbase, "--ws", "--wsport", port)
|
||||
|
||||
time.Sleep(2 * time.Second) // Simple way to wait for the RPC endpoint to open
|
||||
testAttachWelcome(t, geth, "ws://localhost:"+port, httpAPIs)
|
||||
|
||||
geth.Interrupt()
|
||||
geth.ExpectExit()
|
||||
}
|
||||
|
||||
func testAttachWelcome(t *testing.T, geth *testgeth, endpoint, apis string) {
|
||||
// Attach to a running geth note and terminate immediately
|
||||
attach := runGeth(t, "attach", endpoint)
|
||||
defer attach.ExpectExit()
|
||||
attach.CloseStdin()
|
||||
|
||||
// Gather all the infos the welcome message needs to contain
|
||||
attach.SetTemplateFunc("goos", func() string { return runtime.GOOS })
|
||||
attach.SetTemplateFunc("goarch", func() string { return runtime.GOARCH })
|
||||
attach.SetTemplateFunc("gover", runtime.Version)
|
||||
attach.SetTemplateFunc("gethver", func() string { return params.Version })
|
||||
attach.SetTemplateFunc("etherbase", func() string { return geth.Etherbase })
|
||||
attach.SetTemplateFunc("niltime", func() string { return time.Unix(0, 0).Format(time.RFC1123) })
|
||||
attach.SetTemplateFunc("ipc", func() bool { return strings.HasPrefix(endpoint, "ipc") })
|
||||
attach.SetTemplateFunc("datadir", func() string { return geth.Datadir })
|
||||
attach.SetTemplateFunc("apis", func() string { return apis })
|
||||
|
||||
// Verify the actual welcome message to the required template
|
||||
attach.Expect(`
|
||||
Welcome to the Geth JavaScript console!
|
||||
|
||||
instance: Geth/v{{gethver}}/{{goos}}-{{goarch}}/{{gover}}
|
||||
coinbase: {{etherbase}}
|
||||
at block: 0 ({{niltime}}){{if ipc}}
|
||||
datadir: {{datadir}}{{end}}
|
||||
modules: {{apis}}
|
||||
|
||||
> {{.InputLine "exit" }}
|
||||
`)
|
||||
attach.ExpectExit()
|
||||
}
|
||||
|
||||
// trulyRandInt generates a crypto random integer used by the console tests to
|
||||
// not clash network ports with other tests running cocurrently.
|
||||
func trulyRandInt(lo, hi int) int {
|
||||
num, _ := rand.Int(rand.Reader, big.NewInt(int64(hi-lo)))
|
||||
return int(num.Int64()) + lo
|
||||
}
|
||||
152
cmd/geth/dao_test.go
Normal file
152
cmd/geth/dao_test.go
Normal file
@@ -0,0 +1,152 @@
|
||||
// Copyright 2016 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 (
|
||||
"io/ioutil"
|
||||
"math/big"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core"
|
||||
"github.com/ethereum/go-ethereum/ethdb"
|
||||
"github.com/ethereum/go-ethereum/params"
|
||||
)
|
||||
|
||||
// Genesis block for nodes which don't care about the DAO fork (i.e. not configured)
|
||||
var daoOldGenesis = `{
|
||||
"alloc" : {},
|
||||
"coinbase" : "0x0000000000000000000000000000000000000000",
|
||||
"difficulty" : "0x20000",
|
||||
"extraData" : "",
|
||||
"gasLimit" : "0x2fefd8",
|
||||
"nonce" : "0x0000000000000042",
|
||||
"mixhash" : "0x0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"parentHash" : "0x0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"timestamp" : "0x00",
|
||||
"config" : {}
|
||||
}`
|
||||
|
||||
// Genesis block for nodes which actively oppose the DAO fork
|
||||
var daoNoForkGenesis = `{
|
||||
"alloc" : {},
|
||||
"coinbase" : "0x0000000000000000000000000000000000000000",
|
||||
"difficulty" : "0x20000",
|
||||
"extraData" : "",
|
||||
"gasLimit" : "0x2fefd8",
|
||||
"nonce" : "0x0000000000000042",
|
||||
"mixhash" : "0x0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"parentHash" : "0x0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"timestamp" : "0x00",
|
||||
"config" : {
|
||||
"daoForkBlock" : 314,
|
||||
"daoForkSupport" : false
|
||||
}
|
||||
}`
|
||||
|
||||
// Genesis block for nodes which actively support the DAO fork
|
||||
var daoProForkGenesis = `{
|
||||
"alloc" : {},
|
||||
"coinbase" : "0x0000000000000000000000000000000000000000",
|
||||
"difficulty" : "0x20000",
|
||||
"extraData" : "",
|
||||
"gasLimit" : "0x2fefd8",
|
||||
"nonce" : "0x0000000000000042",
|
||||
"mixhash" : "0x0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"parentHash" : "0x0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"timestamp" : "0x00",
|
||||
"config" : {
|
||||
"daoForkBlock" : 314,
|
||||
"daoForkSupport" : true
|
||||
}
|
||||
}`
|
||||
|
||||
var daoGenesisHash = common.HexToHash("5e1fc79cb4ffa4739177b5408045cd5d51c6cf766133f23f7cd72ee1f8d790e0")
|
||||
var daoGenesisForkBlock = big.NewInt(314)
|
||||
|
||||
// TestDAOForkBlockNewChain tests that the DAO hard-fork number and the nodes support/opposition is correctly
|
||||
// set in the database after various initialization procedures and invocations.
|
||||
func TestDAOForkBlockNewChain(t *testing.T) {
|
||||
for i, arg := range []struct {
|
||||
genesis string
|
||||
expectBlock *big.Int
|
||||
expectVote bool
|
||||
}{
|
||||
// Test DAO Default Mainnet
|
||||
{"", params.MainnetChainConfig.DAOForkBlock, true},
|
||||
// test DAO Init Old Privnet
|
||||
{daoOldGenesis, nil, false},
|
||||
// test DAO Default No Fork Privnet
|
||||
{daoNoForkGenesis, daoGenesisForkBlock, false},
|
||||
// test DAO Default Pro Fork Privnet
|
||||
{daoProForkGenesis, daoGenesisForkBlock, true},
|
||||
} {
|
||||
testDAOForkBlockNewChain(t, i, arg.genesis, arg.expectBlock, arg.expectVote)
|
||||
}
|
||||
}
|
||||
|
||||
func testDAOForkBlockNewChain(t *testing.T, test int, genesis string, expectBlock *big.Int, expectVote bool) {
|
||||
// Create a temporary data directory to use and inspect later
|
||||
datadir := tmpdir(t)
|
||||
defer os.RemoveAll(datadir)
|
||||
|
||||
// Start a Geth instance with the requested flags set and immediately terminate
|
||||
if genesis != "" {
|
||||
json := filepath.Join(datadir, "genesis.json")
|
||||
if err := ioutil.WriteFile(json, []byte(genesis), 0600); err != nil {
|
||||
t.Fatalf("test %d: failed to write genesis file: %v", test, err)
|
||||
}
|
||||
runGeth(t, "--datadir", datadir, "init", json).WaitExit()
|
||||
} else {
|
||||
// Force chain initialization
|
||||
args := []string{"--port", "0", "--maxpeers", "0", "--nodiscover", "--nat", "none", "--ipcdisable", "--datadir", datadir}
|
||||
geth := runGeth(t, append(args, []string{"--exec", "2+2", "console"}...)...)
|
||||
geth.WaitExit()
|
||||
}
|
||||
// Retrieve the DAO config flag from the database
|
||||
path := filepath.Join(datadir, "geth", "chaindata")
|
||||
db, err := ethdb.NewLDBDatabase(path, 0, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("test %d: failed to open test database: %v", test, err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
genesisHash := common.HexToHash("0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3")
|
||||
if genesis != "" {
|
||||
genesisHash = daoGenesisHash
|
||||
}
|
||||
config, err := core.GetChainConfig(db, genesisHash)
|
||||
if err != nil {
|
||||
t.Errorf("test %d: failed to retrieve chain config: %v", test, err)
|
||||
return // we want to return here, the other checks can't make it past this point (nil panic).
|
||||
}
|
||||
// Validate the DAO hard-fork block number against the expected value
|
||||
if config.DAOForkBlock == nil {
|
||||
if expectBlock != nil {
|
||||
t.Errorf("test %d: dao hard-fork block mismatch: have nil, want %v", test, expectBlock)
|
||||
}
|
||||
} else if expectBlock == nil {
|
||||
t.Errorf("test %d: dao hard-fork block mismatch: have %v, want nil", test, config.DAOForkBlock)
|
||||
} else if config.DAOForkBlock.Cmp(expectBlock) != 0 {
|
||||
t.Errorf("test %d: dao hard-fork block mismatch: have %v, want %v", test, config.DAOForkBlock, expectBlock)
|
||||
}
|
||||
if config.DAOForkSupport != expectVote {
|
||||
t.Errorf("test %d: dao hard-fork support mismatch: have %v, want %v", test, config.DAOForkSupport, expectVote)
|
||||
}
|
||||
}
|
||||
110
cmd/geth/genesis_test.go
Normal file
110
cmd/geth/genesis_test.go
Normal file
@@ -0,0 +1,110 @@
|
||||
// Copyright 2016 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 (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var customGenesisTests = []struct {
|
||||
genesis string
|
||||
query string
|
||||
result string
|
||||
}{
|
||||
// Plain genesis file without anything extra
|
||||
{
|
||||
genesis: `{
|
||||
"alloc" : {},
|
||||
"coinbase" : "0x0000000000000000000000000000000000000000",
|
||||
"difficulty" : "0x20000",
|
||||
"extraData" : "",
|
||||
"gasLimit" : "0x2fefd8",
|
||||
"nonce" : "0x0000000000000042",
|
||||
"mixhash" : "0x0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"parentHash" : "0x0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"timestamp" : "0x00"
|
||||
}`,
|
||||
query: "eth.getBlock(0).nonce",
|
||||
result: "0x0000000000000042",
|
||||
},
|
||||
// Genesis file with an empty chain configuration (ensure missing fields work)
|
||||
{
|
||||
genesis: `{
|
||||
"alloc" : {},
|
||||
"coinbase" : "0x0000000000000000000000000000000000000000",
|
||||
"difficulty" : "0x20000",
|
||||
"extraData" : "",
|
||||
"gasLimit" : "0x2fefd8",
|
||||
"nonce" : "0x0000000000000042",
|
||||
"mixhash" : "0x0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"parentHash" : "0x0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"timestamp" : "0x00",
|
||||
"config" : {}
|
||||
}`,
|
||||
query: "eth.getBlock(0).nonce",
|
||||
result: "0x0000000000000042",
|
||||
},
|
||||
// Genesis file with specific chain configurations
|
||||
{
|
||||
genesis: `{
|
||||
"alloc" : {},
|
||||
"coinbase" : "0x0000000000000000000000000000000000000000",
|
||||
"difficulty" : "0x20000",
|
||||
"extraData" : "",
|
||||
"gasLimit" : "0x2fefd8",
|
||||
"nonce" : "0x0000000000000042",
|
||||
"mixhash" : "0x0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"parentHash" : "0x0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"timestamp" : "0x00",
|
||||
"config" : {
|
||||
"homesteadBlock" : 314,
|
||||
"daoForkBlock" : 141,
|
||||
"daoForkSupport" : true
|
||||
},
|
||||
}`,
|
||||
query: "eth.getBlock(0).nonce",
|
||||
result: "0x0000000000000042",
|
||||
},
|
||||
}
|
||||
|
||||
// Tests that initializing Geth with a custom genesis block and chain definitions
|
||||
// work properly.
|
||||
func TestCustomGenesis(t *testing.T) {
|
||||
for i, tt := range customGenesisTests {
|
||||
// Create a temporary data directory to use and inspect later
|
||||
datadir := tmpdir(t)
|
||||
defer os.RemoveAll(datadir)
|
||||
|
||||
// Initialize the data directory with the custom genesis block
|
||||
json := filepath.Join(datadir, "genesis.json")
|
||||
if err := ioutil.WriteFile(json, []byte(tt.genesis), 0600); err != nil {
|
||||
t.Fatalf("test %d: failed to write genesis file: %v", i, err)
|
||||
}
|
||||
runGeth(t, "--datadir", datadir, "init", json).WaitExit()
|
||||
|
||||
// Query the custom genesis block
|
||||
geth := runGeth(t,
|
||||
"--datadir", datadir, "--maxpeers", "0", "--port", "0",
|
||||
"--nodiscover", "--nat", "none", "--ipcdisable",
|
||||
"--exec", tt.query, "console")
|
||||
geth.ExpectRegexp(tt.result)
|
||||
geth.ExpectExit()
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
{"code":"0x605880600c6000396000f3006000357c010000000000000000000000000000000000000000000000000000000090048063c6888fa114602e57005b603d6004803590602001506047565b8060005260206000f35b60006007820290506053565b91905056","info":{"abiDefinition":[{"constant":false,"inputs":[{"name":"a","type":"uint256"}],"name":"multiply","outputs":[{"name":"d","type":"uint256"}],"type":"function"}],"compilerVersion":"0.9.23","developerDoc":{"methods":{}},"language":"Solidity","languageVersion":"0","source":"contract test {\n /// @notice Will multiply `a` by 7.\n function multiply(uint a) returns(uint d) {\n return a * 7;\n }\n}\n","userDoc":{"methods":{"multiply(uint256)":{"notice":"Will multiply `a` by 7."}}}}}
|
||||
495
cmd/geth/js.go
495
cmd/geth/js.go
@@ -1,495 +0,0 @@
|
||||
// Copyright 2014 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 (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"sort"
|
||||
|
||||
"github.com/ethereum/go-ethereum/cmd/utils"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/common/docserver"
|
||||
"github.com/ethereum/go-ethereum/common/natspec"
|
||||
"github.com/ethereum/go-ethereum/common/registrar"
|
||||
"github.com/ethereum/go-ethereum/eth"
|
||||
re "github.com/ethereum/go-ethereum/jsre"
|
||||
"github.com/ethereum/go-ethereum/rpc"
|
||||
"github.com/ethereum/go-ethereum/rpc/api"
|
||||
"github.com/ethereum/go-ethereum/rpc/codec"
|
||||
"github.com/ethereum/go-ethereum/rpc/comms"
|
||||
"github.com/ethereum/go-ethereum/rpc/shared"
|
||||
"github.com/ethereum/go-ethereum/xeth"
|
||||
"github.com/peterh/liner"
|
||||
"github.com/robertkrimen/otto"
|
||||
)
|
||||
|
||||
var passwordRegexp = regexp.MustCompile("personal.[nu]")
|
||||
|
||||
const passwordRepl = ""
|
||||
|
||||
type prompter interface {
|
||||
AppendHistory(string)
|
||||
Prompt(p string) (string, error)
|
||||
PasswordPrompt(p string) (string, error)
|
||||
}
|
||||
|
||||
type dumbterm struct{ r *bufio.Reader }
|
||||
|
||||
func (r dumbterm) Prompt(p string) (string, error) {
|
||||
fmt.Print(p)
|
||||
line, err := r.r.ReadString('\n')
|
||||
return strings.TrimSuffix(line, "\n"), err
|
||||
}
|
||||
|
||||
func (r dumbterm) PasswordPrompt(p string) (string, error) {
|
||||
fmt.Println("!! Unsupported terminal, password will echo.")
|
||||
fmt.Print(p)
|
||||
input, err := bufio.NewReader(os.Stdin).ReadString('\n')
|
||||
fmt.Println()
|
||||
return input, err
|
||||
}
|
||||
|
||||
func (r dumbterm) AppendHistory(string) {}
|
||||
|
||||
type jsre struct {
|
||||
ds *docserver.DocServer
|
||||
re *re.JSRE
|
||||
ethereum *eth.Ethereum
|
||||
xeth *xeth.XEth
|
||||
wait chan *big.Int
|
||||
ps1 string
|
||||
atexit func()
|
||||
corsDomain string
|
||||
client comms.EthereumClient
|
||||
prompter
|
||||
}
|
||||
|
||||
var (
|
||||
loadedModulesMethods map[string][]string
|
||||
)
|
||||
|
||||
func keywordCompleter(line string) []string {
|
||||
results := make([]string, 0)
|
||||
|
||||
if strings.Contains(line, ".") {
|
||||
elements := strings.Split(line, ".")
|
||||
if len(elements) == 2 {
|
||||
module := elements[0]
|
||||
partialMethod := elements[1]
|
||||
if methods, found := loadedModulesMethods[module]; found {
|
||||
for _, method := range methods {
|
||||
if strings.HasPrefix(method, partialMethod) { // e.g. debug.se
|
||||
results = append(results, module+"."+method)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for module, methods := range loadedModulesMethods {
|
||||
if line == module { // user typed in full module name, show all methods
|
||||
for _, method := range methods {
|
||||
results = append(results, module+"."+method)
|
||||
}
|
||||
} else if strings.HasPrefix(module, line) { // partial method name, e.g. admi
|
||||
results = append(results, module)
|
||||
}
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
func apiWordCompleter(line string, pos int) (head string, completions []string, tail string) {
|
||||
if len(line) == 0 {
|
||||
return "", nil, ""
|
||||
}
|
||||
|
||||
i := 0
|
||||
for i = pos - 1; i > 0; i-- {
|
||||
if line[i] == '.' || (line[i] >= 'a' && line[i] <= 'z') || (line[i] >= 'A' && line[i] <= 'Z') {
|
||||
continue
|
||||
}
|
||||
if i >= 3 && line[i] == '3' && line[i-3] == 'w' && line[i-2] == 'e' && line[i-1] == 'b' {
|
||||
continue
|
||||
}
|
||||
i += 1
|
||||
break
|
||||
}
|
||||
|
||||
begin := line[:i]
|
||||
keyword := line[i:pos]
|
||||
end := line[pos:]
|
||||
|
||||
completionWords := keywordCompleter(keyword)
|
||||
return begin, completionWords, end
|
||||
}
|
||||
|
||||
func newLightweightJSRE(libPath string, client comms.EthereumClient, interactive bool, f xeth.Frontend) *jsre {
|
||||
js := &jsre{ps1: "> "}
|
||||
js.wait = make(chan *big.Int)
|
||||
js.client = client
|
||||
js.ds = docserver.New("/")
|
||||
|
||||
if f == nil {
|
||||
f = js
|
||||
}
|
||||
|
||||
// update state in separare forever blocks
|
||||
js.re = re.New(libPath)
|
||||
if err := js.apiBindings(f); err != nil {
|
||||
utils.Fatalf("Unable to initialize console - %v", err)
|
||||
}
|
||||
|
||||
if !liner.TerminalSupported() || !interactive {
|
||||
js.prompter = dumbterm{bufio.NewReader(os.Stdin)}
|
||||
} else {
|
||||
lr := liner.NewLiner()
|
||||
js.withHistory(func(hist *os.File) { lr.ReadHistory(hist) })
|
||||
lr.SetCtrlCAborts(true)
|
||||
js.loadAutoCompletion()
|
||||
lr.SetWordCompleter(apiWordCompleter)
|
||||
lr.SetTabCompletionStyle(liner.TabPrints)
|
||||
js.prompter = lr
|
||||
js.atexit = func() {
|
||||
js.withHistory(func(hist *os.File) { hist.Truncate(0); lr.WriteHistory(hist) })
|
||||
lr.Close()
|
||||
close(js.wait)
|
||||
}
|
||||
}
|
||||
return js
|
||||
}
|
||||
|
||||
func newJSRE(ethereum *eth.Ethereum, libPath, corsDomain string, client comms.EthereumClient, interactive bool, f xeth.Frontend) *jsre {
|
||||
js := &jsre{ethereum: ethereum, ps1: "> "}
|
||||
// set default cors domain used by startRpc from CLI flag
|
||||
js.corsDomain = corsDomain
|
||||
if f == nil {
|
||||
f = js
|
||||
}
|
||||
js.ds = docserver.New("/")
|
||||
js.xeth = xeth.New(ethereum, f)
|
||||
js.wait = js.xeth.UpdateState()
|
||||
js.client = client
|
||||
if clt, ok := js.client.(*comms.InProcClient); ok {
|
||||
if offeredApis, err := api.ParseApiString(shared.AllApis, codec.JSON, js.xeth, ethereum); err == nil {
|
||||
clt.Initialize(api.Merge(offeredApis...))
|
||||
}
|
||||
}
|
||||
|
||||
// update state in separare forever blocks
|
||||
js.re = re.New(libPath)
|
||||
if err := js.apiBindings(f); err != nil {
|
||||
utils.Fatalf("Unable to connect - %v", err)
|
||||
}
|
||||
|
||||
if !liner.TerminalSupported() || !interactive {
|
||||
js.prompter = dumbterm{bufio.NewReader(os.Stdin)}
|
||||
} else {
|
||||
lr := liner.NewLiner()
|
||||
js.withHistory(func(hist *os.File) { lr.ReadHistory(hist) })
|
||||
lr.SetCtrlCAborts(true)
|
||||
js.loadAutoCompletion()
|
||||
lr.SetWordCompleter(apiWordCompleter)
|
||||
lr.SetTabCompletionStyle(liner.TabPrints)
|
||||
js.prompter = lr
|
||||
js.atexit = func() {
|
||||
js.withHistory(func(hist *os.File) { hist.Truncate(0); lr.WriteHistory(hist) })
|
||||
lr.Close()
|
||||
close(js.wait)
|
||||
}
|
||||
}
|
||||
return js
|
||||
}
|
||||
|
||||
func (self *jsre) loadAutoCompletion() {
|
||||
if modules, err := self.supportedApis(); err == nil {
|
||||
loadedModulesMethods = make(map[string][]string)
|
||||
for module, _ := range modules {
|
||||
loadedModulesMethods[module] = api.AutoCompletion[module]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (self *jsre) batch(statement string) {
|
||||
val, err := self.re.Run(statement)
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("error: %v", err)
|
||||
} else if val.IsDefined() && val.IsObject() {
|
||||
obj, _ := self.re.Get("ret_result")
|
||||
fmt.Printf("%v", obj)
|
||||
} else if val.IsDefined() {
|
||||
fmt.Printf("%v", val)
|
||||
}
|
||||
|
||||
if self.atexit != nil {
|
||||
self.atexit()
|
||||
}
|
||||
|
||||
self.re.Stop(false)
|
||||
}
|
||||
|
||||
// show summary of current geth instance
|
||||
func (self *jsre) welcome() {
|
||||
self.re.Eval(`console.log('instance: ' + web3.version.client);`)
|
||||
self.re.Eval(`console.log(' datadir: ' + admin.datadir);`)
|
||||
self.re.Eval(`console.log("coinbase: " + eth.coinbase);`)
|
||||
self.re.Eval(`var lastBlockTimestamp = 1000 * eth.getBlock(eth.blockNumber).timestamp`)
|
||||
self.re.Eval(`console.log("at block: " + eth.blockNumber + " (" + new Date(lastBlockTimestamp).toLocaleDateString()
|
||||
+ " " + new Date(lastBlockTimestamp).toLocaleTimeString() + ")");`)
|
||||
|
||||
if modules, err := self.supportedApis(); err == nil {
|
||||
loadedModules := make([]string, 0)
|
||||
for api, version := range modules {
|
||||
loadedModules = append(loadedModules, fmt.Sprintf("%s:%s", api, version))
|
||||
}
|
||||
sort.Strings(loadedModules)
|
||||
|
||||
self.re.Eval(fmt.Sprintf("var modules = '%s';", strings.Join(loadedModules, " ")))
|
||||
self.re.Eval(`console.log(" modules: " + modules);`)
|
||||
}
|
||||
}
|
||||
|
||||
func (self *jsre) supportedApis() (map[string]string, error) {
|
||||
return self.client.SupportedModules()
|
||||
}
|
||||
|
||||
func (js *jsre) apiBindings(f xeth.Frontend) error {
|
||||
apis, err := js.supportedApis()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
apiNames := make([]string, 0, len(apis))
|
||||
for a, _ := range apis {
|
||||
apiNames = append(apiNames, a)
|
||||
}
|
||||
|
||||
apiImpl, err := api.ParseApiString(strings.Join(apiNames, ","), codec.JSON, js.xeth, js.ethereum)
|
||||
if err != nil {
|
||||
utils.Fatalf("Unable to determine supported api's: %v", err)
|
||||
}
|
||||
|
||||
jeth := rpc.NewJeth(api.Merge(apiImpl...), js.re, js.client)
|
||||
js.re.Set("jeth", struct{}{})
|
||||
t, _ := js.re.Get("jeth")
|
||||
jethObj := t.Object()
|
||||
|
||||
jethObj.Set("send", jeth.Send)
|
||||
jethObj.Set("sendAsync", jeth.Send)
|
||||
|
||||
err = js.re.Compile("bignumber.js", re.BigNumber_JS)
|
||||
if err != nil {
|
||||
utils.Fatalf("Error loading bignumber.js: %v", err)
|
||||
}
|
||||
|
||||
err = js.re.Compile("ethereum.js", re.Web3_JS)
|
||||
if err != nil {
|
||||
utils.Fatalf("Error loading web3.js: %v", err)
|
||||
}
|
||||
|
||||
_, err = js.re.Eval("var web3 = require('web3');")
|
||||
if err != nil {
|
||||
utils.Fatalf("Error requiring web3: %v", err)
|
||||
}
|
||||
|
||||
_, err = js.re.Eval("web3.setProvider(jeth)")
|
||||
if err != nil {
|
||||
utils.Fatalf("Error setting web3 provider: %v", err)
|
||||
}
|
||||
|
||||
// load only supported API's in javascript runtime
|
||||
shortcuts := "var eth = web3.eth; "
|
||||
for _, apiName := range apiNames {
|
||||
if apiName == shared.Web3ApiName {
|
||||
continue // manually mapped
|
||||
}
|
||||
|
||||
if err = js.re.Compile(fmt.Sprintf("%s.js", apiName), api.Javascript(apiName)); err == nil {
|
||||
shortcuts += fmt.Sprintf("var %s = web3.%s; ", apiName, apiName)
|
||||
} else {
|
||||
utils.Fatalf("Error loading %s.js: %v", apiName, err)
|
||||
}
|
||||
}
|
||||
|
||||
_, err = js.re.Eval(shortcuts)
|
||||
|
||||
if err != nil {
|
||||
utils.Fatalf("Error setting namespaces: %v", err)
|
||||
}
|
||||
|
||||
js.re.Eval(`var GlobalRegistrar = eth.contract(` + registrar.GlobalRegistrarAbi + `); registrar = GlobalRegistrar.at("` + registrar.GlobalRegistrarAddr + `");`)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *jsre) ConfirmTransaction(tx string) bool {
|
||||
if self.ethereum.NatSpec {
|
||||
notice := natspec.GetNotice(self.xeth, tx, self.ds)
|
||||
fmt.Println(notice)
|
||||
answer, _ := self.Prompt("Confirm Transaction [y/n]")
|
||||
return strings.HasPrefix(strings.Trim(answer, " "), "y")
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func (self *jsre) UnlockAccount(addr []byte) bool {
|
||||
fmt.Printf("Please unlock account %x.\n", addr)
|
||||
pass, err := self.PasswordPrompt("Passphrase: ")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
// TODO: allow retry
|
||||
if err := self.ethereum.AccountManager().Unlock(common.BytesToAddress(addr), pass); err != nil {
|
||||
return false
|
||||
} else {
|
||||
fmt.Println("Account is now unlocked for this session.")
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func (self *jsre) exec(filename string) error {
|
||||
if err := self.re.Exec(filename); err != nil {
|
||||
self.re.Stop(false)
|
||||
return fmt.Errorf("Javascript Error: %v", err)
|
||||
}
|
||||
self.re.Stop(true)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *jsre) interactive() {
|
||||
// Read input lines.
|
||||
prompt := make(chan string)
|
||||
inputln := make(chan string)
|
||||
go func() {
|
||||
defer close(inputln)
|
||||
for {
|
||||
line, err := self.Prompt(<-prompt)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
inputln <- line
|
||||
}
|
||||
}()
|
||||
// Wait for Ctrl-C, too.
|
||||
sig := make(chan os.Signal, 1)
|
||||
signal.Notify(sig, os.Interrupt)
|
||||
|
||||
defer func() {
|
||||
if self.atexit != nil {
|
||||
self.atexit()
|
||||
}
|
||||
self.re.Stop(false)
|
||||
}()
|
||||
for {
|
||||
prompt <- self.ps1
|
||||
select {
|
||||
case <-sig:
|
||||
fmt.Println("caught interrupt, exiting")
|
||||
return
|
||||
case input, ok := <-inputln:
|
||||
if !ok || indentCount <= 0 && input == "exit" {
|
||||
return
|
||||
}
|
||||
if input == "" {
|
||||
continue
|
||||
}
|
||||
str += input + "\n"
|
||||
self.setIndent()
|
||||
if indentCount <= 0 {
|
||||
hist := hidepassword(str[:len(str)-1])
|
||||
if len(hist) > 0 {
|
||||
self.AppendHistory(hist)
|
||||
}
|
||||
self.parseInput(str)
|
||||
str = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func hidepassword(input string) string {
|
||||
if passwordRegexp.MatchString(input) {
|
||||
return passwordRepl
|
||||
} else {
|
||||
return input
|
||||
}
|
||||
}
|
||||
|
||||
func (self *jsre) withHistory(op func(*os.File)) {
|
||||
datadir := common.DefaultDataDir()
|
||||
if self.ethereum != nil {
|
||||
datadir = self.ethereum.DataDir
|
||||
}
|
||||
|
||||
hist, err := os.OpenFile(filepath.Join(datadir, "history"), os.O_RDWR|os.O_CREATE, os.ModePerm)
|
||||
if err != nil {
|
||||
fmt.Printf("unable to open history file: %v\n", err)
|
||||
return
|
||||
}
|
||||
op(hist)
|
||||
hist.Close()
|
||||
}
|
||||
|
||||
func (self *jsre) parseInput(code string) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
fmt.Println("[native] error", r)
|
||||
}
|
||||
}()
|
||||
value, err := self.re.Run(code)
|
||||
if err != nil {
|
||||
if ottoErr, ok := err.(*otto.Error); ok {
|
||||
fmt.Println(ottoErr.String())
|
||||
} else {
|
||||
fmt.Println(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
self.printValue(value)
|
||||
}
|
||||
|
||||
var indentCount = 0
|
||||
var str = ""
|
||||
|
||||
func (self *jsre) setIndent() {
|
||||
open := strings.Count(str, "{")
|
||||
open += strings.Count(str, "(")
|
||||
closed := strings.Count(str, "}")
|
||||
closed += strings.Count(str, ")")
|
||||
indentCount = open - closed
|
||||
if indentCount <= 0 {
|
||||
self.ps1 = "> "
|
||||
} else {
|
||||
self.ps1 = strings.Join(make([]string, indentCount*2), "..")
|
||||
self.ps1 += " "
|
||||
}
|
||||
}
|
||||
|
||||
func (self *jsre) printValue(v interface{}) {
|
||||
val, err := self.re.PrettyPrint(v)
|
||||
if err == nil {
|
||||
fmt.Printf("%v", val)
|
||||
}
|
||||
}
|
||||
@@ -1,517 +0,0 @@
|
||||
// Copyright 2015 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 (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math/big"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/common/compiler"
|
||||
"github.com/ethereum/go-ethereum/common/docserver"
|
||||
"github.com/ethereum/go-ethereum/common/natspec"
|
||||
"github.com/ethereum/go-ethereum/common/registrar"
|
||||
"github.com/ethereum/go-ethereum/core"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/ethereum/go-ethereum/eth"
|
||||
"github.com/ethereum/go-ethereum/ethdb"
|
||||
"github.com/ethereum/go-ethereum/rpc/codec"
|
||||
"github.com/ethereum/go-ethereum/rpc/comms"
|
||||
)
|
||||
|
||||
const (
|
||||
testSolcPath = ""
|
||||
solcVersion = "0.9.23"
|
||||
|
||||
testKey = "e6fab74a43941f82d89cb7faa408e227cdad3153c4720e540e855c19b15e6674"
|
||||
testAddress = "0x8605cdbbdb6d264aa742e77020dcbc58fcdce182"
|
||||
testBalance = "10000000000000000000"
|
||||
// of empty string
|
||||
testHash = "0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470"
|
||||
)
|
||||
|
||||
var (
|
||||
versionRE = regexp.MustCompile(strconv.Quote(`"compilerVersion":"` + solcVersion + `"`))
|
||||
testNodeKey = crypto.ToECDSA(common.Hex2Bytes("4b50fa71f5c3eeb8fdc452224b2395af2fcc3d125e06c32c82e048c0559db03f"))
|
||||
testGenesis = `{"` + testAddress[2:] + `": {"balance": "` + testBalance + `"}}`
|
||||
)
|
||||
|
||||
type testjethre struct {
|
||||
*jsre
|
||||
lastConfirm string
|
||||
ds *docserver.DocServer
|
||||
}
|
||||
|
||||
func (self *testjethre) UnlockAccount(acc []byte) bool {
|
||||
err := self.ethereum.AccountManager().Unlock(common.BytesToAddress(acc), "")
|
||||
if err != nil {
|
||||
panic("unable to unlock")
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (self *testjethre) ConfirmTransaction(tx string) bool {
|
||||
if self.ethereum.NatSpec {
|
||||
self.lastConfirm = natspec.GetNotice(self.xeth, tx, self.ds)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func testJEthRE(t *testing.T) (string, *testjethre, *eth.Ethereum) {
|
||||
return testREPL(t, nil)
|
||||
}
|
||||
|
||||
func testREPL(t *testing.T, config func(*eth.Config)) (string, *testjethre, *eth.Ethereum) {
|
||||
tmp, err := ioutil.TempDir("", "geth-test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
db, _ := ethdb.NewMemDatabase()
|
||||
|
||||
core.WriteGenesisBlockForTesting(db, common.HexToAddress(testAddress), common.String2Big(testBalance))
|
||||
ks := crypto.NewKeyStorePlain(filepath.Join(tmp, "keystore"))
|
||||
am := accounts.NewManager(ks)
|
||||
conf := ð.Config{
|
||||
NodeKey: testNodeKey,
|
||||
DataDir: tmp,
|
||||
AccountManager: am,
|
||||
MaxPeers: 0,
|
||||
Name: "test",
|
||||
SolcPath: testSolcPath,
|
||||
PowTest: true,
|
||||
NewDB: func(path string) (common.Database, error) { return db, nil },
|
||||
}
|
||||
if config != nil {
|
||||
config(conf)
|
||||
}
|
||||
ethereum, err := eth.New(conf)
|
||||
if err != nil {
|
||||
t.Fatal("%v", err)
|
||||
}
|
||||
|
||||
keyb, err := crypto.HexToECDSA(testKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
key := crypto.NewKeyFromECDSA(keyb)
|
||||
err = ks.StoreKey(key, "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = am.Unlock(key.Address, "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assetPath := filepath.Join(os.Getenv("GOPATH"), "src", "github.com", "ethereum", "go-ethereum", "cmd", "mist", "assets", "ext")
|
||||
client := comms.NewInProcClient(codec.JSON)
|
||||
ds := docserver.New("/")
|
||||
tf := &testjethre{ds: ds}
|
||||
repl := newJSRE(ethereum, assetPath, "", client, false, tf)
|
||||
tf.jsre = repl
|
||||
return tmp, tf, ethereum
|
||||
}
|
||||
|
||||
func TestNodeInfo(t *testing.T) {
|
||||
t.Skip("broken after p2p update")
|
||||
tmp, repl, ethereum := testJEthRE(t)
|
||||
if err := ethereum.Start(); err != nil {
|
||||
t.Fatalf("error starting ethereum: %v", err)
|
||||
}
|
||||
defer ethereum.Stop()
|
||||
defer os.RemoveAll(tmp)
|
||||
|
||||
want := `{"DiscPort":0,"IP":"0.0.0.0","ListenAddr":"","Name":"test","NodeID":"4cb2fc32924e94277bf94b5e4c983beedb2eabd5a0bc941db32202735c6625d020ca14a5963d1738af43b6ac0a711d61b1a06de931a499fe2aa0b1a132a902b5","NodeUrl":"enode://4cb2fc32924e94277bf94b5e4c983beedb2eabd5a0bc941db32202735c6625d020ca14a5963d1738af43b6ac0a711d61b1a06de931a499fe2aa0b1a132a902b5@0.0.0.0:0","TCPPort":0,"Td":"131072"}`
|
||||
checkEvalJSON(t, repl, `admin.nodeInfo`, want)
|
||||
}
|
||||
|
||||
func TestAccounts(t *testing.T) {
|
||||
tmp, repl, ethereum := testJEthRE(t)
|
||||
if err := ethereum.Start(); err != nil {
|
||||
t.Fatalf("error starting ethereum: %v", err)
|
||||
}
|
||||
defer ethereum.Stop()
|
||||
defer os.RemoveAll(tmp)
|
||||
|
||||
checkEvalJSON(t, repl, `eth.accounts`, `["`+testAddress+`"]`)
|
||||
checkEvalJSON(t, repl, `eth.coinbase`, `"`+testAddress+`"`)
|
||||
val, err := repl.re.Run(`personal.newAccount("password")`)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error, got %v", err)
|
||||
}
|
||||
addr := val.String()
|
||||
if !regexp.MustCompile(`0x[0-9a-f]{40}`).MatchString(addr) {
|
||||
t.Errorf("address not hex: %q", addr)
|
||||
}
|
||||
|
||||
checkEvalJSON(t, repl, `eth.accounts`, `["`+testAddress+`","`+addr+`"]`)
|
||||
|
||||
}
|
||||
|
||||
func TestBlockChain(t *testing.T) {
|
||||
tmp, repl, ethereum := testJEthRE(t)
|
||||
if err := ethereum.Start(); err != nil {
|
||||
t.Fatalf("error starting ethereum: %v", err)
|
||||
}
|
||||
defer ethereum.Stop()
|
||||
defer os.RemoveAll(tmp)
|
||||
// get current block dump before export/import.
|
||||
val, err := repl.re.Run("JSON.stringify(debug.dumpBlock(eth.blockNumber))")
|
||||
if err != nil {
|
||||
t.Errorf("expected no error, got %v", err)
|
||||
}
|
||||
beforeExport := val.String()
|
||||
|
||||
// do the export
|
||||
extmp, err := ioutil.TempDir("", "geth-test-export")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(extmp)
|
||||
tmpfile := filepath.Join(extmp, "export.chain")
|
||||
tmpfileq := strconv.Quote(tmpfile)
|
||||
|
||||
ethereum.ChainManager().Reset()
|
||||
|
||||
checkEvalJSON(t, repl, `admin.exportChain(`+tmpfileq+`)`, `true`)
|
||||
if _, err := os.Stat(tmpfile); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// check import, verify that dumpBlock gives the same result.
|
||||
checkEvalJSON(t, repl, `admin.importChain(`+tmpfileq+`)`, `true`)
|
||||
checkEvalJSON(t, repl, `debug.dumpBlock(eth.blockNumber)`, beforeExport)
|
||||
}
|
||||
|
||||
func TestMining(t *testing.T) {
|
||||
tmp, repl, ethereum := testJEthRE(t)
|
||||
if err := ethereum.Start(); err != nil {
|
||||
t.Fatalf("error starting ethereum: %v", err)
|
||||
}
|
||||
defer ethereum.Stop()
|
||||
defer os.RemoveAll(tmp)
|
||||
checkEvalJSON(t, repl, `eth.mining`, `false`)
|
||||
}
|
||||
|
||||
func TestRPC(t *testing.T) {
|
||||
tmp, repl, ethereum := testJEthRE(t)
|
||||
if err := ethereum.Start(); err != nil {
|
||||
t.Errorf("error starting ethereum: %v", err)
|
||||
return
|
||||
}
|
||||
defer ethereum.Stop()
|
||||
defer os.RemoveAll(tmp)
|
||||
|
||||
checkEvalJSON(t, repl, `admin.startRPC("127.0.0.1", 5004, "*", "web3,eth,net")`, `true`)
|
||||
}
|
||||
|
||||
func TestCheckTestAccountBalance(t *testing.T) {
|
||||
t.Skip() // i don't think it tests the correct behaviour here. it's actually testing
|
||||
// internals which shouldn't be tested. This now fails because of a change in the core
|
||||
// and i have no means to fix this, sorry - @obscuren
|
||||
tmp, repl, ethereum := testJEthRE(t)
|
||||
if err := ethereum.Start(); err != nil {
|
||||
t.Errorf("error starting ethereum: %v", err)
|
||||
return
|
||||
}
|
||||
defer ethereum.Stop()
|
||||
defer os.RemoveAll(tmp)
|
||||
|
||||
repl.re.Run(`primary = "` + testAddress + `"`)
|
||||
checkEvalJSON(t, repl, `eth.getBalance(primary)`, `"`+testBalance+`"`)
|
||||
}
|
||||
|
||||
func TestSignature(t *testing.T) {
|
||||
tmp, repl, ethereum := testJEthRE(t)
|
||||
if err := ethereum.Start(); err != nil {
|
||||
t.Errorf("error starting ethereum: %v", err)
|
||||
return
|
||||
}
|
||||
defer ethereum.Stop()
|
||||
defer os.RemoveAll(tmp)
|
||||
|
||||
val, err := repl.re.Run(`eth.sign("` + testAddress + `", "` + testHash + `")`)
|
||||
|
||||
// This is a very preliminary test, lacking actual signature verification
|
||||
if err != nil {
|
||||
t.Errorf("Error running js: %v", err)
|
||||
return
|
||||
}
|
||||
output := val.String()
|
||||
t.Logf("Output: %v", output)
|
||||
|
||||
regex := regexp.MustCompile(`^0x[0-9a-f]{130}$`)
|
||||
if !regex.MatchString(output) {
|
||||
t.Errorf("Signature is not 65 bytes represented in hexadecimal.")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func TestContract(t *testing.T) {
|
||||
t.Skip("contract testing is implemented with mining in ethash test mode. This takes about 7seconds to run. Unskip and run on demand")
|
||||
coinbase := common.HexToAddress(testAddress)
|
||||
tmp, repl, ethereum := testREPL(t, func(conf *eth.Config) {
|
||||
conf.Etherbase = coinbase
|
||||
conf.PowTest = true
|
||||
})
|
||||
if err := ethereum.Start(); err != nil {
|
||||
t.Errorf("error starting ethereum: %v", err)
|
||||
return
|
||||
}
|
||||
defer ethereum.Stop()
|
||||
defer os.RemoveAll(tmp)
|
||||
|
||||
reg := registrar.New(repl.xeth)
|
||||
_, err := reg.SetGlobalRegistrar("", coinbase)
|
||||
if err != nil {
|
||||
t.Errorf("error setting HashReg: %v", err)
|
||||
}
|
||||
_, err = reg.SetHashReg("", coinbase)
|
||||
if err != nil {
|
||||
t.Errorf("error setting HashReg: %v", err)
|
||||
}
|
||||
_, err = reg.SetUrlHint("", coinbase)
|
||||
if err != nil {
|
||||
t.Errorf("error setting HashReg: %v", err)
|
||||
}
|
||||
/* TODO:
|
||||
* lookup receipt and contract addresses by tx hash
|
||||
* name registration for HashReg and UrlHint addresses
|
||||
* mine those transactions
|
||||
* then set once more SetHashReg SetUrlHint
|
||||
*/
|
||||
|
||||
source := `contract test {\n` +
|
||||
" /// @notice Will multiply `a` by 7." + `\n` +
|
||||
` function multiply(uint a) returns(uint d) {\n` +
|
||||
` return a * 7;\n` +
|
||||
` }\n` +
|
||||
`}\n`
|
||||
|
||||
if checkEvalJSON(t, repl, `admin.stopNatSpec()`, `true`) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
contractInfo, err := ioutil.ReadFile("info_test.json")
|
||||
if err != nil {
|
||||
t.Fatalf("%v", err)
|
||||
}
|
||||
if checkEvalJSON(t, repl, `primary = eth.accounts[0]`, `"`+testAddress+`"`) != nil {
|
||||
return
|
||||
}
|
||||
if checkEvalJSON(t, repl, `source = "`+source+`"`, `"`+source+`"`) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// if solc is found with right version, test it, otherwise read from file
|
||||
sol, err := compiler.New("")
|
||||
if err != nil {
|
||||
t.Logf("solc not found: mocking contract compilation step")
|
||||
} else if sol.Version() != solcVersion {
|
||||
t.Logf("WARNING: solc different version found (%v, test written for %v, may need to update)", sol.Version(), solcVersion)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
info, err := ioutil.ReadFile("info_test.json")
|
||||
if err != nil {
|
||||
t.Fatalf("%v", err)
|
||||
}
|
||||
_, err = repl.re.Run(`contract = JSON.parse(` + strconv.Quote(string(info)) + `)`)
|
||||
if err != nil {
|
||||
t.Errorf("%v", err)
|
||||
}
|
||||
} else {
|
||||
if checkEvalJSON(t, repl, `contract = eth.compile.solidity(source).test`, string(contractInfo)) != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if checkEvalJSON(t, repl, `contract.code`, `"0x605880600c6000396000f3006000357c010000000000000000000000000000000000000000000000000000000090048063c6888fa114602e57005b603d6004803590602001506047565b8060005260206000f35b60006007820290506053565b91905056"`) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if checkEvalJSON(
|
||||
t, repl,
|
||||
`contractaddress = eth.sendTransaction({from: primary, data: contract.code})`,
|
||||
`"0x46d69d55c3c4b86a924a92c9fc4720bb7bce1d74"`,
|
||||
) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !processTxs(repl, t, 8) {
|
||||
return
|
||||
}
|
||||
|
||||
callSetup := `abiDef = JSON.parse('[{"constant":false,"inputs":[{"name":"a","type":"uint256"}],"name":"multiply","outputs":[{"name":"d","type":"uint256"}],"type":"function"}]');
|
||||
Multiply7 = eth.contract(abiDef);
|
||||
multiply7 = Multiply7.at(contractaddress);
|
||||
`
|
||||
_, err = repl.re.Run(callSetup)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error setting up contract, got %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
expNotice := ""
|
||||
if repl.lastConfirm != expNotice {
|
||||
t.Errorf("incorrect confirmation message: expected %v, got %v", expNotice, repl.lastConfirm)
|
||||
return
|
||||
}
|
||||
|
||||
if checkEvalJSON(t, repl, `admin.startNatSpec()`, `true`) != nil {
|
||||
return
|
||||
}
|
||||
if checkEvalJSON(t, repl, `multiply7.multiply.sendTransaction(6, { from: primary })`, `"0x4ef9088431a8033e4580d00e4eb2487275e031ff4163c7529df0ef45af17857b"`) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !processTxs(repl, t, 1) {
|
||||
return
|
||||
}
|
||||
|
||||
expNotice = `About to submit transaction (no NatSpec info found for contract: content hash not found for '0x87e2802265838c7f14bb69eecd2112911af6767907a702eeaa445239fb20711b'): {"params":[{"to":"0x46d69d55c3c4b86a924a92c9fc4720bb7bce1d74","data": "0xc6888fa10000000000000000000000000000000000000000000000000000000000000006"}]}`
|
||||
if repl.lastConfirm != expNotice {
|
||||
t.Errorf("incorrect confirmation message: expected\n%v, got\n%v", expNotice, repl.lastConfirm)
|
||||
return
|
||||
}
|
||||
|
||||
var contentHash = `"0x86d2b7cf1e72e9a7a3f8d96601f0151742a2f780f1526414304fbe413dc7f9bd"`
|
||||
if sol != nil && solcVersion != sol.Version() {
|
||||
modContractInfo := versionRE.ReplaceAll(contractInfo, []byte(`"compilerVersion":"`+sol.Version()+`"`))
|
||||
fmt.Printf("modified contractinfo:\n%s\n", modContractInfo)
|
||||
contentHash = `"` + common.ToHex(crypto.Sha3([]byte(modContractInfo))) + `"`
|
||||
}
|
||||
if checkEvalJSON(t, repl, `filename = "/tmp/info.json"`, `"/tmp/info.json"`) != nil {
|
||||
return
|
||||
}
|
||||
if checkEvalJSON(t, repl, `contentHash = admin.saveInfo(contract.info, filename)`, contentHash) != nil {
|
||||
return
|
||||
}
|
||||
if checkEvalJSON(t, repl, `admin.register(primary, contractaddress, contentHash)`, `true`) != nil {
|
||||
return
|
||||
}
|
||||
if checkEvalJSON(t, repl, `admin.registerUrl(primary, contentHash, "file://"+filename)`, `true`) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if checkEvalJSON(t, repl, `admin.startNatSpec()`, `true`) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !processTxs(repl, t, 3) {
|
||||
return
|
||||
}
|
||||
|
||||
if checkEvalJSON(t, repl, `multiply7.multiply.sendTransaction(6, { from: primary })`, `"0x66d7635c12ad0b231e66da2f987ca3dfdca58ffe49c6442aa55960858103fd0c"`) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !processTxs(repl, t, 1) {
|
||||
return
|
||||
}
|
||||
|
||||
expNotice = "Will multiply 6 by 7."
|
||||
if repl.lastConfirm != expNotice {
|
||||
t.Errorf("incorrect confirmation message: expected\n%v, got\n%v", expNotice, repl.lastConfirm)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func pendingTransactions(repl *testjethre, t *testing.T) (txc int64, err error) {
|
||||
txs := repl.ethereum.TxPool().GetTransactions()
|
||||
return int64(len(txs)), nil
|
||||
}
|
||||
|
||||
func processTxs(repl *testjethre, t *testing.T, expTxc int) bool {
|
||||
var txc int64
|
||||
var err error
|
||||
for i := 0; i < 50; i++ {
|
||||
txc, err = pendingTransactions(repl, t)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error checking pending transactions: %v", err)
|
||||
return false
|
||||
}
|
||||
if expTxc < int(txc) {
|
||||
t.Errorf("too many pending transactions: expected %v, got %v", expTxc, txc)
|
||||
return false
|
||||
} else if expTxc == int(txc) {
|
||||
break
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
if int(txc) != expTxc {
|
||||
t.Errorf("incorrect number of pending transactions, expected %v, got %v", expTxc, txc)
|
||||
return false
|
||||
}
|
||||
|
||||
err = repl.ethereum.StartMining(runtime.NumCPU())
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error mining: %v", err)
|
||||
return false
|
||||
}
|
||||
defer repl.ethereum.StopMining()
|
||||
|
||||
timer := time.NewTimer(100 * time.Second)
|
||||
height := new(big.Int).Add(repl.xeth.CurrentBlock().Number(), big.NewInt(1))
|
||||
repl.wait <- height
|
||||
select {
|
||||
case <-timer.C:
|
||||
// if times out make sure the xeth loop does not block
|
||||
go func() {
|
||||
select {
|
||||
case repl.wait <- nil:
|
||||
case <-repl.wait:
|
||||
}
|
||||
}()
|
||||
case <-repl.wait:
|
||||
}
|
||||
txc, err = pendingTransactions(repl, t)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error checking pending transactions: %v", err)
|
||||
return false
|
||||
}
|
||||
if txc != 0 {
|
||||
t.Errorf("%d trasactions were not mined", txc)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func checkEvalJSON(t *testing.T, re *testjethre, expr, want string) error {
|
||||
val, err := re.re.Run("JSON.stringify(" + expr + ")")
|
||||
if err == nil && val.String() != want {
|
||||
err = fmt.Errorf("Output mismatch for `%s`:\ngot: %s\nwant: %s", expr, val.String(), want)
|
||||
}
|
||||
if err != nil {
|
||||
_, file, line, _ := runtime.Caller(1)
|
||||
file = filepath.Base(file)
|
||||
fmt.Printf("\t%s:%d: %v\n", file, line, err)
|
||||
t.Fail()
|
||||
}
|
||||
return err
|
||||
}
|
||||
850
cmd/geth/main.go
850
cmd/geth/main.go
@@ -19,271 +19,77 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
_ "net/http/pprof"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/codegangsta/cli"
|
||||
"github.com/ethereum/ethash"
|
||||
"github.com/ethereum/go-ethereum/accounts"
|
||||
"github.com/ethereum/go-ethereum/accounts/keystore"
|
||||
"github.com/ethereum/go-ethereum/cmd/utils"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/console"
|
||||
"github.com/ethereum/go-ethereum/eth"
|
||||
"github.com/ethereum/go-ethereum/ethdb"
|
||||
"github.com/ethereum/go-ethereum/fdtrack"
|
||||
"github.com/ethereum/go-ethereum/logger"
|
||||
"github.com/ethereum/go-ethereum/logger/glog"
|
||||
"github.com/ethereum/go-ethereum/ethclient"
|
||||
"github.com/ethereum/go-ethereum/internal/debug"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/metrics"
|
||||
"github.com/ethereum/go-ethereum/rpc/codec"
|
||||
"github.com/ethereum/go-ethereum/rpc/comms"
|
||||
"github.com/mattn/go-colorable"
|
||||
"github.com/mattn/go-isatty"
|
||||
"github.com/ethereum/go-ethereum/node"
|
||||
"gopkg.in/urfave/cli.v1"
|
||||
)
|
||||
|
||||
const (
|
||||
ClientIdentifier = "Geth"
|
||||
Version = "1.0.1"
|
||||
clientIdentifier = "geth" // Client identifier to advertise over the network
|
||||
)
|
||||
|
||||
var (
|
||||
gitCommit string // set via linker flagg
|
||||
nodeNameVersion string
|
||||
app *cli.App
|
||||
)
|
||||
|
||||
func init() {
|
||||
if gitCommit == "" {
|
||||
nodeNameVersion = Version
|
||||
} else {
|
||||
nodeNameVersion = Version + "-" + gitCommit[:8]
|
||||
}
|
||||
|
||||
app = utils.NewApp(Version, "the go-ethereum command line interface")
|
||||
app.Action = run
|
||||
app.HideVersion = true // we have a command to print the version
|
||||
app.Commands = []cli.Command{
|
||||
{
|
||||
Action: blockRecovery,
|
||||
Name: "recover",
|
||||
Usage: "attempts to recover a corrupted database by setting a new block by number or hash. See help recover.",
|
||||
Description: `
|
||||
The recover commands will attempt to read out the last
|
||||
block based on that.
|
||||
|
||||
recover #number recovers by number
|
||||
recover <hex> recovers by hash
|
||||
`,
|
||||
},
|
||||
blocktestCommand,
|
||||
importCommand,
|
||||
exportCommand,
|
||||
upgradedbCommand,
|
||||
removedbCommand,
|
||||
dumpCommand,
|
||||
monitorCommand,
|
||||
{
|
||||
Action: makedag,
|
||||
Name: "makedag",
|
||||
Usage: "generate ethash dag (for testing)",
|
||||
Description: `
|
||||
The makedag command generates an ethash DAG in /tmp/dag.
|
||||
|
||||
This command exists to support the system testing project.
|
||||
Regular users do not need to execute it.
|
||||
`,
|
||||
},
|
||||
{
|
||||
Action: version,
|
||||
Name: "version",
|
||||
Usage: "print ethereum version numbers",
|
||||
Description: `
|
||||
The output of this command is supposed to be machine-readable.
|
||||
`,
|
||||
},
|
||||
|
||||
{
|
||||
Name: "wallet",
|
||||
Usage: "ethereum presale wallet",
|
||||
Subcommands: []cli.Command{
|
||||
{
|
||||
Action: importWallet,
|
||||
Name: "import",
|
||||
Usage: "import ethereum presale wallet",
|
||||
},
|
||||
},
|
||||
Description: `
|
||||
|
||||
get wallet import /path/to/my/presale.wallet
|
||||
|
||||
will prompt for your password and imports your ether presale account.
|
||||
It can be used non-interactively with the --password option taking a
|
||||
passwordfile as argument containing the wallet password in plaintext.
|
||||
|
||||
`},
|
||||
{
|
||||
Action: accountList,
|
||||
Name: "account",
|
||||
Usage: "manage accounts",
|
||||
Description: `
|
||||
|
||||
Manage accounts lets you create new accounts, list all existing accounts,
|
||||
import a private key into a new account.
|
||||
|
||||
' help' shows a list of subcommands or help for one subcommand.
|
||||
|
||||
It supports interactive mode, when you are prompted for password as well as
|
||||
non-interactive mode where passwords are supplied via a given password file.
|
||||
Non-interactive mode is only meant for scripted use on test networks or known
|
||||
safe environments.
|
||||
|
||||
Make sure you remember the password you gave when creating a new account (with
|
||||
either new or import). Without it you are not able to unlock your account.
|
||||
|
||||
Note that exporting your key in unencrypted format is NOT supported.
|
||||
|
||||
Keys are stored under <DATADIR>/keys.
|
||||
It is safe to transfer the entire directory or the individual keys therein
|
||||
between ethereum nodes by simply copying.
|
||||
Make sure you backup your keys regularly.
|
||||
|
||||
In order to use your account to send transactions, you need to unlock them using the
|
||||
'--unlock' option. The argument is a comma
|
||||
|
||||
And finally. DO NOT FORGET YOUR PASSWORD.
|
||||
`,
|
||||
Subcommands: []cli.Command{
|
||||
{
|
||||
Action: accountList,
|
||||
Name: "list",
|
||||
Usage: "print account addresses",
|
||||
},
|
||||
{
|
||||
Action: accountCreate,
|
||||
Name: "new",
|
||||
Usage: "create a new account",
|
||||
Description: `
|
||||
|
||||
ethereum account new
|
||||
|
||||
Creates a new account. Prints the address.
|
||||
|
||||
The account is saved in encrypted format, you are prompted for a passphrase.
|
||||
|
||||
You must remember this passphrase to unlock your account in the future.
|
||||
|
||||
For non-interactive use the passphrase can be specified with the --password flag:
|
||||
|
||||
ethereum --password <passwordfile> account new
|
||||
|
||||
Note, this is meant to be used for testing only, it is a bad idea to save your
|
||||
password to file or expose in any other way.
|
||||
`,
|
||||
},
|
||||
{
|
||||
Action: accountUpdate,
|
||||
Name: "update",
|
||||
Usage: "update an existing account",
|
||||
Description: `
|
||||
|
||||
ethereum account update <address>
|
||||
|
||||
Update an existing account.
|
||||
|
||||
The account is saved in the newest version in encrypted format, you are prompted
|
||||
for a passphrase to unlock the account and another to save the updated file.
|
||||
|
||||
This same command can therefore be used to migrate an account of a deprecated
|
||||
format to the newest format or change the password for an account.
|
||||
|
||||
For non-interactive use the passphrase can be specified with the --password flag:
|
||||
|
||||
ethereum --password <passwordfile> account new
|
||||
|
||||
Since only one password can be given, only format update can be performed,
|
||||
changing your password is only possible interactively.
|
||||
|
||||
Note that account update has the a side effect that the order of your accounts
|
||||
changes.
|
||||
`,
|
||||
},
|
||||
{
|
||||
Action: accountImport,
|
||||
Name: "import",
|
||||
Usage: "import a private key into a new account",
|
||||
Description: `
|
||||
|
||||
ethereum account import <keyfile>
|
||||
|
||||
Imports an unencrypted private key from <keyfile> and creates a new account.
|
||||
Prints the address.
|
||||
|
||||
The keyfile is assumed to contain an unencrypted private key in hexadecimal format.
|
||||
|
||||
The account is saved in encrypted format, you are prompted for a passphrase.
|
||||
|
||||
You must remember this passphrase to unlock your account in the future.
|
||||
|
||||
For non-interactive use the passphrase can be specified with the -password flag:
|
||||
|
||||
ethereum --password <passwordfile> account import <keyfile>
|
||||
|
||||
Note:
|
||||
As you can directly copy your encrypted accounts to another ethereum instance,
|
||||
this import mechanism is not needed when you transfer an account between
|
||||
nodes.
|
||||
`,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Action: console,
|
||||
Name: "console",
|
||||
Usage: `Geth Console: interactive JavaScript environment`,
|
||||
Description: `
|
||||
The Geth console is an interactive shell for the JavaScript runtime environment
|
||||
which exposes a node admin interface as well as the Ðapp JavaScript API.
|
||||
See https://github.com/ethereum/go-ethereum/wiki/Javascipt-Console
|
||||
`},
|
||||
{
|
||||
Action: attach,
|
||||
Name: "attach",
|
||||
Usage: `Geth Console: interactive JavaScript environment (connect to node)`,
|
||||
Description: `
|
||||
The Geth console is an interactive shell for the JavaScript runtime environment
|
||||
which exposes a node admin interface as well as the Ðapp JavaScript API.
|
||||
See https://github.com/ethereum/go-ethereum/wiki/Javascipt-Console.
|
||||
This command allows to open a console on a running geth node.
|
||||
`,
|
||||
},
|
||||
{
|
||||
Action: execJSFiles,
|
||||
Name: "js",
|
||||
Usage: `executes the given JavaScript files in the Geth JavaScript VM`,
|
||||
Description: `
|
||||
The JavaScript VM exposes a node admin interface as well as the Ðapp
|
||||
JavaScript API. See https://github.com/ethereum/go-ethereum/wiki/Javascipt-Console
|
||||
`,
|
||||
},
|
||||
}
|
||||
app.Flags = []cli.Flag{
|
||||
// Git SHA1 commit hash of the release (set via linker flags)
|
||||
gitCommit = ""
|
||||
// Ethereum address of the Geth release oracle.
|
||||
relOracle = common.HexToAddress("0xfa7b9770ca4cb04296cac84f37736d4041251cdf")
|
||||
// The app that holds all commands and flags.
|
||||
app = utils.NewApp(gitCommit, "the go-ethereum command line interface")
|
||||
// flags that configure the node
|
||||
nodeFlags = []cli.Flag{
|
||||
utils.IdentityFlag,
|
||||
utils.UnlockedAccountFlag,
|
||||
utils.PasswordFileFlag,
|
||||
utils.GenesisFileFlag,
|
||||
utils.BootnodesFlag,
|
||||
utils.BootnodesV4Flag,
|
||||
utils.BootnodesV5Flag,
|
||||
utils.DataDirFlag,
|
||||
utils.BlockchainVersionFlag,
|
||||
utils.OlympicFlag,
|
||||
utils.KeyStoreDirFlag,
|
||||
utils.NoUSBFlag,
|
||||
utils.DashboardEnabledFlag,
|
||||
utils.DashboardAddrFlag,
|
||||
utils.DashboardPortFlag,
|
||||
utils.DashboardRefreshFlag,
|
||||
utils.DashboardAssetsFlag,
|
||||
utils.EthashCacheDirFlag,
|
||||
utils.EthashCachesInMemoryFlag,
|
||||
utils.EthashCachesOnDiskFlag,
|
||||
utils.EthashDatasetDirFlag,
|
||||
utils.EthashDatasetsInMemoryFlag,
|
||||
utils.EthashDatasetsOnDiskFlag,
|
||||
utils.TxPoolNoLocalsFlag,
|
||||
utils.TxPoolJournalFlag,
|
||||
utils.TxPoolRejournalFlag,
|
||||
utils.TxPoolPriceLimitFlag,
|
||||
utils.TxPoolPriceBumpFlag,
|
||||
utils.TxPoolAccountSlotsFlag,
|
||||
utils.TxPoolGlobalSlotsFlag,
|
||||
utils.TxPoolAccountQueueFlag,
|
||||
utils.TxPoolGlobalQueueFlag,
|
||||
utils.TxPoolLifetimeFlag,
|
||||
utils.FastSyncFlag,
|
||||
utils.LightModeFlag,
|
||||
utils.SyncModeFlag,
|
||||
utils.LightServFlag,
|
||||
utils.LightPeersFlag,
|
||||
utils.LightKDFFlag,
|
||||
utils.CacheFlag,
|
||||
utils.JSpathFlag,
|
||||
utils.TrieCacheGenFlag,
|
||||
utils.ListenPortFlag,
|
||||
utils.MaxPeersFlag,
|
||||
utils.MaxPendingPeersFlag,
|
||||
@@ -291,438 +97,204 @@ JavaScript API. See https://github.com/ethereum/go-ethereum/wiki/Javascipt-Conso
|
||||
utils.GasPriceFlag,
|
||||
utils.MinerThreadsFlag,
|
||||
utils.MiningEnabledFlag,
|
||||
utils.AutoDAGFlag,
|
||||
utils.TargetGasLimitFlag,
|
||||
utils.NATFlag,
|
||||
utils.NatspecEnabledFlag,
|
||||
utils.NoDiscoverFlag,
|
||||
utils.DiscoveryV5Flag,
|
||||
utils.NetrestrictFlag,
|
||||
utils.NodeKeyFileFlag,
|
||||
utils.NodeKeyHexFlag,
|
||||
utils.DeveloperFlag,
|
||||
utils.DeveloperPeriodFlag,
|
||||
utils.TestnetFlag,
|
||||
utils.RinkebyFlag,
|
||||
utils.VMEnableDebugFlag,
|
||||
utils.NetworkIdFlag,
|
||||
utils.RPCCORSDomainFlag,
|
||||
utils.EthStatsURLFlag,
|
||||
utils.MetricsEnabledFlag,
|
||||
utils.FakePoWFlag,
|
||||
utils.NoCompactionFlag,
|
||||
utils.GpoBlocksFlag,
|
||||
utils.GpoPercentileFlag,
|
||||
utils.ExtraDataFlag,
|
||||
configFileFlag,
|
||||
}
|
||||
|
||||
rpcFlags = []cli.Flag{
|
||||
utils.RPCEnabledFlag,
|
||||
utils.RPCListenAddrFlag,
|
||||
utils.RPCPortFlag,
|
||||
utils.RpcApiFlag,
|
||||
utils.RPCApiFlag,
|
||||
utils.WSEnabledFlag,
|
||||
utils.WSListenAddrFlag,
|
||||
utils.WSPortFlag,
|
||||
utils.WSApiFlag,
|
||||
utils.WSAllowedOriginsFlag,
|
||||
utils.IPCDisabledFlag,
|
||||
utils.IPCApiFlag,
|
||||
utils.IPCPathFlag,
|
||||
utils.ExecFlag,
|
||||
utils.WhisperEnabledFlag,
|
||||
utils.VMDebugFlag,
|
||||
utils.NetworkIdFlag,
|
||||
utils.RPCCORSDomainFlag,
|
||||
utils.VerbosityFlag,
|
||||
utils.BacktraceAtFlag,
|
||||
utils.LogToStdErrFlag,
|
||||
utils.LogVModuleFlag,
|
||||
utils.LogFileFlag,
|
||||
utils.LogJSONFlag,
|
||||
utils.PProfEanbledFlag,
|
||||
utils.PProfPortFlag,
|
||||
utils.MetricsEnabledFlag,
|
||||
utils.SolcPathFlag,
|
||||
utils.GpoMinGasPriceFlag,
|
||||
utils.GpoMaxGasPriceFlag,
|
||||
utils.GpoFullBlockRatioFlag,
|
||||
utils.GpobaseStepDownFlag,
|
||||
utils.GpobaseStepUpFlag,
|
||||
utils.GpobaseCorrectionFactorFlag,
|
||||
}
|
||||
|
||||
whisperFlags = []cli.Flag{
|
||||
utils.WhisperEnabledFlag,
|
||||
utils.WhisperMaxMessageSizeFlag,
|
||||
utils.WhisperMinPOWFlag,
|
||||
}
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Initialize the CLI app and start Geth
|
||||
app.Action = geth
|
||||
app.HideVersion = true // we have a command to print the version
|
||||
app.Copyright = "Copyright 2013-2017 The go-ethereum Authors"
|
||||
app.Commands = []cli.Command{
|
||||
// See chaincmd.go:
|
||||
initCommand,
|
||||
importCommand,
|
||||
exportCommand,
|
||||
copydbCommand,
|
||||
removedbCommand,
|
||||
dumpCommand,
|
||||
// See monitorcmd.go:
|
||||
monitorCommand,
|
||||
// See accountcmd.go:
|
||||
accountCommand,
|
||||
walletCommand,
|
||||
// See consolecmd.go:
|
||||
consoleCommand,
|
||||
attachCommand,
|
||||
javascriptCommand,
|
||||
// See misccmd.go:
|
||||
makecacheCommand,
|
||||
makedagCommand,
|
||||
versionCommand,
|
||||
bugCommand,
|
||||
licenseCommand,
|
||||
// See config.go
|
||||
dumpConfigCommand,
|
||||
}
|
||||
sort.Sort(cli.CommandsByName(app.Commands))
|
||||
|
||||
app.Flags = append(app.Flags, nodeFlags...)
|
||||
app.Flags = append(app.Flags, rpcFlags...)
|
||||
app.Flags = append(app.Flags, consoleFlags...)
|
||||
app.Flags = append(app.Flags, debug.Flags...)
|
||||
app.Flags = append(app.Flags, whisperFlags...)
|
||||
|
||||
app.Before = func(ctx *cli.Context) error {
|
||||
utils.SetupLogger(ctx)
|
||||
if ctx.GlobalBool(utils.PProfEanbledFlag.Name) {
|
||||
utils.StartPProf(ctx)
|
||||
runtime.GOMAXPROCS(runtime.NumCPU())
|
||||
if err := debug.Setup(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
// Start system runtime metrics collection
|
||||
go metrics.CollectProcessMetrics(3 * time.Second)
|
||||
|
||||
utils.SetupNetwork(ctx)
|
||||
return nil
|
||||
}
|
||||
|
||||
app.After = func(ctx *cli.Context) error {
|
||||
debug.Exit()
|
||||
console.Stdin.Close() // Resets terminal mode.
|
||||
return nil
|
||||
}
|
||||
// Start system runtime metrics collection
|
||||
go metrics.CollectProcessMetrics(3 * time.Second)
|
||||
}
|
||||
|
||||
func main() {
|
||||
runtime.GOMAXPROCS(runtime.NumCPU())
|
||||
defer logger.Flush()
|
||||
if err := app.Run(os.Args); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func run(ctx *cli.Context) {
|
||||
utils.CheckLegalese(ctx.GlobalString(utils.DataDirFlag.Name))
|
||||
if ctx.GlobalBool(utils.OlympicFlag.Name) {
|
||||
utils.InitOlympic()
|
||||
}
|
||||
|
||||
cfg := utils.MakeEthConfig(ClientIdentifier, nodeNameVersion, ctx)
|
||||
ethereum, err := eth.New(cfg)
|
||||
if err != nil {
|
||||
utils.Fatalf("%v", err)
|
||||
}
|
||||
|
||||
startEth(ctx, ethereum)
|
||||
// this blocks the thread
|
||||
ethereum.WaitForShutdown()
|
||||
// geth is the main entry point into the system if no special subcommand is ran.
|
||||
// It creates a default node based on the command line arguments and runs it in
|
||||
// blocking mode, waiting for it to be shut down.
|
||||
func geth(ctx *cli.Context) error {
|
||||
node := makeFullNode(ctx)
|
||||
startNode(ctx, node)
|
||||
node.Wait()
|
||||
return nil
|
||||
}
|
||||
|
||||
func attach(ctx *cli.Context) {
|
||||
utils.CheckLegalese(ctx.GlobalString(utils.DataDirFlag.Name))
|
||||
// startNode boots up the system node and all registered protocols, after which
|
||||
// it unlocks any requested accounts, and starts the RPC/IPC interfaces and the
|
||||
// miner.
|
||||
func startNode(ctx *cli.Context, stack *node.Node) {
|
||||
// Start up the node itself
|
||||
utils.StartNode(stack)
|
||||
|
||||
// Wrap the standard output with a colorified stream (windows)
|
||||
if isatty.IsTerminal(os.Stdout.Fd()) {
|
||||
if pr, pw, err := os.Pipe(); err == nil {
|
||||
go io.Copy(colorable.NewColorableStdout(), pr)
|
||||
os.Stdout = pw
|
||||
// Unlock any account specifically requested
|
||||
ks := stack.AccountManager().Backends(keystore.KeyStoreType)[0].(*keystore.KeyStore)
|
||||
|
||||
passwords := utils.MakePasswordList(ctx)
|
||||
unlocks := strings.Split(ctx.GlobalString(utils.UnlockedAccountFlag.Name), ",")
|
||||
for i, account := range unlocks {
|
||||
if trimmed := strings.TrimSpace(account); trimmed != "" {
|
||||
unlockAccount(ctx, ks, trimmed, i, passwords)
|
||||
}
|
||||
}
|
||||
// Register wallet event handlers to open and auto-derive wallets
|
||||
events := make(chan accounts.WalletEvent, 16)
|
||||
stack.AccountManager().Subscribe(events)
|
||||
|
||||
var client comms.EthereumClient
|
||||
var err error
|
||||
if ctx.Args().Present() {
|
||||
client, err = comms.ClientFromEndpoint(ctx.Args().First(), codec.JSON)
|
||||
} else {
|
||||
cfg := comms.IpcConfig{
|
||||
Endpoint: ctx.GlobalString(utils.IPCPathFlag.Name),
|
||||
}
|
||||
client, err = comms.NewIpcClient(cfg, codec.JSON)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
utils.Fatalf("Unable to attach to geth node - %v", err)
|
||||
}
|
||||
|
||||
repl := newLightweightJSRE(
|
||||
ctx.GlobalString(utils.JSpathFlag.Name),
|
||||
client,
|
||||
true,
|
||||
nil)
|
||||
|
||||
if ctx.GlobalString(utils.ExecFlag.Name) != "" {
|
||||
repl.batch(ctx.GlobalString(utils.ExecFlag.Name))
|
||||
} else {
|
||||
repl.welcome()
|
||||
repl.interactive()
|
||||
}
|
||||
}
|
||||
|
||||
func console(ctx *cli.Context) {
|
||||
utils.CheckLegalese(ctx.GlobalString(utils.DataDirFlag.Name))
|
||||
|
||||
// Wrap the standard output with a colorified stream (windows)
|
||||
if isatty.IsTerminal(os.Stdout.Fd()) {
|
||||
if pr, pw, err := os.Pipe(); err == nil {
|
||||
go io.Copy(colorable.NewColorableStdout(), pr)
|
||||
os.Stdout = pw
|
||||
}
|
||||
}
|
||||
|
||||
cfg := utils.MakeEthConfig(ClientIdentifier, nodeNameVersion, ctx)
|
||||
ethereum, err := eth.New(cfg)
|
||||
if err != nil {
|
||||
utils.Fatalf("%v", err)
|
||||
}
|
||||
|
||||
client := comms.NewInProcClient(codec.JSON)
|
||||
|
||||
startEth(ctx, ethereum)
|
||||
repl := newJSRE(
|
||||
ethereum,
|
||||
ctx.GlobalString(utils.JSpathFlag.Name),
|
||||
ctx.GlobalString(utils.RPCCORSDomainFlag.Name),
|
||||
client,
|
||||
true,
|
||||
nil,
|
||||
)
|
||||
|
||||
if ctx.GlobalString(utils.ExecFlag.Name) != "" {
|
||||
repl.batch(ctx.GlobalString(utils.ExecFlag.Name))
|
||||
} else {
|
||||
repl.welcome()
|
||||
repl.interactive()
|
||||
}
|
||||
|
||||
ethereum.Stop()
|
||||
ethereum.WaitForShutdown()
|
||||
}
|
||||
|
||||
func execJSFiles(ctx *cli.Context) {
|
||||
utils.CheckLegalese(ctx.GlobalString(utils.DataDirFlag.Name))
|
||||
|
||||
cfg := utils.MakeEthConfig(ClientIdentifier, nodeNameVersion, ctx)
|
||||
ethereum, err := eth.New(cfg)
|
||||
if err != nil {
|
||||
utils.Fatalf("%v", err)
|
||||
}
|
||||
|
||||
client := comms.NewInProcClient(codec.JSON)
|
||||
startEth(ctx, ethereum)
|
||||
repl := newJSRE(
|
||||
ethereum,
|
||||
ctx.GlobalString(utils.JSpathFlag.Name),
|
||||
ctx.GlobalString(utils.RPCCORSDomainFlag.Name),
|
||||
client,
|
||||
false,
|
||||
nil,
|
||||
)
|
||||
for _, file := range ctx.Args() {
|
||||
repl.exec(file)
|
||||
}
|
||||
|
||||
ethereum.Stop()
|
||||
ethereum.WaitForShutdown()
|
||||
}
|
||||
|
||||
func unlockAccount(ctx *cli.Context, am *accounts.Manager, addr string, i int) (addrHex, auth string) {
|
||||
utils.CheckLegalese(ctx.GlobalString(utils.DataDirFlag.Name))
|
||||
|
||||
var err error
|
||||
addrHex, err = utils.ParamToAddress(addr, am)
|
||||
if err == nil {
|
||||
// Attempt to unlock the account 3 times
|
||||
attempts := 3
|
||||
for tries := 0; tries < attempts; tries++ {
|
||||
msg := fmt.Sprintf("Unlocking account %s | Attempt %d/%d", addr, tries+1, attempts)
|
||||
auth = getPassPhrase(ctx, msg, false, i)
|
||||
err = am.Unlock(common.HexToAddress(addrHex), auth)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
utils.Fatalf("Unlock account failed '%v'", err)
|
||||
}
|
||||
fmt.Printf("Account '%s' unlocked.\n", addr)
|
||||
return
|
||||
}
|
||||
|
||||
func blockRecovery(ctx *cli.Context) {
|
||||
utils.CheckLegalese(ctx.GlobalString(utils.DataDirFlag.Name))
|
||||
|
||||
arg := ctx.Args().First()
|
||||
if len(ctx.Args()) < 1 && len(arg) > 0 {
|
||||
glog.Fatal("recover requires block number or hash")
|
||||
}
|
||||
|
||||
cfg := utils.MakeEthConfig(ClientIdentifier, nodeNameVersion, ctx)
|
||||
utils.CheckLegalese(cfg.DataDir)
|
||||
|
||||
blockDb, err := ethdb.NewLDBDatabase(filepath.Join(cfg.DataDir, "blockchain"), cfg.DatabaseCache)
|
||||
if err != nil {
|
||||
glog.Fatalln("could not open db:", err)
|
||||
}
|
||||
|
||||
var block *types.Block
|
||||
if arg[0] == '#' {
|
||||
block = core.GetBlockByNumber(blockDb, common.String2Big(arg[1:]).Uint64())
|
||||
} else {
|
||||
block = core.GetBlockByHash(blockDb, common.HexToHash(arg))
|
||||
}
|
||||
|
||||
if block == nil {
|
||||
glog.Fatalln("block not found. Recovery failed")
|
||||
}
|
||||
|
||||
err = core.WriteHead(blockDb, block)
|
||||
if err != nil {
|
||||
glog.Fatalln("block write err", err)
|
||||
}
|
||||
glog.Infof("Recovery succesful. New HEAD %x\n", block.Hash())
|
||||
}
|
||||
|
||||
func startEth(ctx *cli.Context, eth *eth.Ethereum) {
|
||||
// Start Ethereum itself
|
||||
utils.StartEthereum(eth)
|
||||
|
||||
// Start logging file descriptor stats.
|
||||
fdtrack.Start()
|
||||
|
||||
am := eth.AccountManager()
|
||||
account := ctx.GlobalString(utils.UnlockedAccountFlag.Name)
|
||||
accounts := strings.Split(account, " ")
|
||||
for i, account := range accounts {
|
||||
if len(account) > 0 {
|
||||
if account == "primary" {
|
||||
utils.Fatalf("the 'primary' keyword is deprecated. You can use integer indexes, but the indexes are not permanent, they can change if you add external keys, export your keys or copy your keystore to another node.")
|
||||
}
|
||||
unlockAccount(ctx, am, account, i)
|
||||
}
|
||||
}
|
||||
// Start auxiliary services if enabled.
|
||||
if !ctx.GlobalBool(utils.IPCDisabledFlag.Name) {
|
||||
if err := utils.StartIPC(eth, ctx); err != nil {
|
||||
utils.Fatalf("Error string IPC: %v", err)
|
||||
}
|
||||
}
|
||||
if ctx.GlobalBool(utils.RPCEnabledFlag.Name) {
|
||||
if err := utils.StartRPC(eth, ctx); err != nil {
|
||||
utils.Fatalf("Error starting RPC: %v", err)
|
||||
}
|
||||
}
|
||||
if ctx.GlobalBool(utils.MiningEnabledFlag.Name) {
|
||||
if err := eth.StartMining(ctx.GlobalInt(utils.MinerThreadsFlag.Name)); err != nil {
|
||||
utils.Fatalf("%v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func accountList(ctx *cli.Context) {
|
||||
utils.CheckLegalese(ctx.GlobalString(utils.DataDirFlag.Name))
|
||||
|
||||
am := utils.MakeAccountManager(ctx)
|
||||
accts, err := am.Accounts()
|
||||
if err != nil {
|
||||
utils.Fatalf("Could not list accounts: %v", err)
|
||||
}
|
||||
for i, acct := range accts {
|
||||
fmt.Printf("Account #%d: %x\n", i, acct)
|
||||
}
|
||||
}
|
||||
|
||||
func getPassPhrase(ctx *cli.Context, desc string, confirmation bool, i int) (passphrase string) {
|
||||
passfile := ctx.GlobalString(utils.PasswordFileFlag.Name)
|
||||
if len(passfile) == 0 {
|
||||
fmt.Println(desc)
|
||||
auth, err := utils.PromptPassword("Passphrase: ", true)
|
||||
go func() {
|
||||
// Create an chain state reader for self-derivation
|
||||
rpcClient, err := stack.Attach()
|
||||
if err != nil {
|
||||
utils.Fatalf("%v", err)
|
||||
utils.Fatalf("Failed to attach to self: %v", err)
|
||||
}
|
||||
if confirmation {
|
||||
confirm, err := utils.PromptPassword("Repeat Passphrase: ", false)
|
||||
if err != nil {
|
||||
utils.Fatalf("%v", err)
|
||||
}
|
||||
if auth != confirm {
|
||||
utils.Fatalf("Passphrases did not match.")
|
||||
stateReader := ethclient.NewClient(rpcClient)
|
||||
|
||||
// Open any wallets already attached
|
||||
for _, wallet := range stack.AccountManager().Wallets() {
|
||||
if err := wallet.Open(""); err != nil {
|
||||
log.Warn("Failed to open wallet", "url", wallet.URL(), "err", err)
|
||||
}
|
||||
}
|
||||
passphrase = auth
|
||||
// Listen for wallet event till termination
|
||||
for event := range events {
|
||||
switch event.Kind {
|
||||
case accounts.WalletArrived:
|
||||
if err := event.Wallet.Open(""); err != nil {
|
||||
log.Warn("New wallet appeared, failed to open", "url", event.Wallet.URL(), "err", err)
|
||||
}
|
||||
case accounts.WalletOpened:
|
||||
status, _ := event.Wallet.Status()
|
||||
log.Info("New wallet appeared", "url", event.Wallet.URL(), "status", status)
|
||||
|
||||
} else {
|
||||
passbytes, err := ioutil.ReadFile(passfile)
|
||||
if err != nil {
|
||||
utils.Fatalf("Unable to read password file '%s': %v", passfile, err)
|
||||
}
|
||||
// this is backwards compatible if the same password unlocks several accounts
|
||||
// it also has the consequence that trailing newlines will not count as part
|
||||
// of the password, so --password <(echo -n 'pass') will now work without -n
|
||||
passphrases := strings.Split(string(passbytes), "\n")
|
||||
if i >= len(passphrases) {
|
||||
passphrase = passphrases[len(passphrases)-1]
|
||||
} else {
|
||||
passphrase = passphrases[i]
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
if event.Wallet.URL().Scheme == "ledger" {
|
||||
event.Wallet.SelfDerive(accounts.DefaultLedgerBaseDerivationPath, stateReader)
|
||||
} else {
|
||||
event.Wallet.SelfDerive(accounts.DefaultBaseDerivationPath, stateReader)
|
||||
}
|
||||
|
||||
func accountCreate(ctx *cli.Context) {
|
||||
utils.CheckLegalese(ctx.GlobalString(utils.DataDirFlag.Name))
|
||||
|
||||
am := utils.MakeAccountManager(ctx)
|
||||
passphrase := getPassPhrase(ctx, "Your new account is locked with a password. Please give a password. Do not forget this password.", true, 0)
|
||||
acct, err := am.NewAccount(passphrase)
|
||||
if err != nil {
|
||||
utils.Fatalf("Could not create the account: %v", err)
|
||||
}
|
||||
fmt.Printf("Address: %x\n", acct)
|
||||
}
|
||||
|
||||
func accountUpdate(ctx *cli.Context) {
|
||||
utils.CheckLegalese(ctx.GlobalString(utils.DataDirFlag.Name))
|
||||
|
||||
am := utils.MakeAccountManager(ctx)
|
||||
arg := ctx.Args().First()
|
||||
if len(arg) == 0 {
|
||||
utils.Fatalf("account address or index must be given as argument")
|
||||
}
|
||||
|
||||
addr, authFrom := unlockAccount(ctx, am, arg, 0)
|
||||
authTo := getPassPhrase(ctx, "Please give a new password. Do not forget this password.", true, 0)
|
||||
err := am.Update(common.HexToAddress(addr), authFrom, authTo)
|
||||
if err != nil {
|
||||
utils.Fatalf("Could not update the account: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func importWallet(ctx *cli.Context) {
|
||||
utils.CheckLegalese(ctx.GlobalString(utils.DataDirFlag.Name))
|
||||
|
||||
keyfile := ctx.Args().First()
|
||||
if len(keyfile) == 0 {
|
||||
utils.Fatalf("keyfile must be given as argument")
|
||||
}
|
||||
keyJson, err := ioutil.ReadFile(keyfile)
|
||||
if err != nil {
|
||||
utils.Fatalf("Could not read wallet file: %v", err)
|
||||
}
|
||||
|
||||
am := utils.MakeAccountManager(ctx)
|
||||
passphrase := getPassPhrase(ctx, "", false, 0)
|
||||
|
||||
acct, err := am.ImportPreSaleKey(keyJson, passphrase)
|
||||
if err != nil {
|
||||
utils.Fatalf("Could not create the account: %v", err)
|
||||
}
|
||||
fmt.Printf("Address: %x\n", acct)
|
||||
}
|
||||
|
||||
func accountImport(ctx *cli.Context) {
|
||||
utils.CheckLegalese(ctx.GlobalString(utils.DataDirFlag.Name))
|
||||
|
||||
keyfile := ctx.Args().First()
|
||||
if len(keyfile) == 0 {
|
||||
utils.Fatalf("keyfile must be given as argument")
|
||||
}
|
||||
am := utils.MakeAccountManager(ctx)
|
||||
passphrase := getPassPhrase(ctx, "Your new account is locked with a password. Please give a password. Do not forget this password.", true, 0)
|
||||
acct, err := am.Import(keyfile, passphrase)
|
||||
if err != nil {
|
||||
utils.Fatalf("Could not create the account: %v", err)
|
||||
}
|
||||
fmt.Printf("Address: %x\n", acct)
|
||||
}
|
||||
|
||||
func makedag(ctx *cli.Context) {
|
||||
utils.CheckLegalese(ctx.GlobalString(utils.DataDirFlag.Name))
|
||||
|
||||
args := ctx.Args()
|
||||
wrongArgs := func() {
|
||||
utils.Fatalf(`Usage: geth makedag <block number> <outputdir>`)
|
||||
}
|
||||
switch {
|
||||
case len(args) == 2:
|
||||
blockNum, err := strconv.ParseUint(args[0], 0, 64)
|
||||
dir := args[1]
|
||||
if err != nil {
|
||||
wrongArgs()
|
||||
} else {
|
||||
dir = filepath.Clean(dir)
|
||||
// seems to require a trailing slash
|
||||
if !strings.HasSuffix(dir, "/") {
|
||||
dir = dir + "/"
|
||||
case accounts.WalletDropped:
|
||||
log.Info("Old wallet dropped", "url", event.Wallet.URL())
|
||||
event.Wallet.Close()
|
||||
}
|
||||
_, err = ioutil.ReadDir(dir)
|
||||
if err != nil {
|
||||
utils.Fatalf("Can't find dir")
|
||||
}
|
||||
fmt.Println("making DAG, this could take awhile...")
|
||||
ethash.MakeDAG(blockNum, dir)
|
||||
}
|
||||
default:
|
||||
wrongArgs()
|
||||
}()
|
||||
// Start auxiliary services if enabled
|
||||
if ctx.GlobalBool(utils.MiningEnabledFlag.Name) || ctx.GlobalBool(utils.DeveloperFlag.Name) {
|
||||
// Mining only makes sense if a full Ethereum node is running
|
||||
var ethereum *eth.Ethereum
|
||||
if err := stack.Service(ðereum); err != nil {
|
||||
utils.Fatalf("ethereum service not running: %v", err)
|
||||
}
|
||||
// Use a reduced number of threads if requested
|
||||
if threads := ctx.GlobalInt(utils.MinerThreadsFlag.Name); threads > 0 {
|
||||
type threaded interface {
|
||||
SetThreads(threads int)
|
||||
}
|
||||
if th, ok := ethereum.Engine().(threaded); ok {
|
||||
th.SetThreads(threads)
|
||||
}
|
||||
}
|
||||
// Set the gas price to the limits from the CLI and start mining
|
||||
ethereum.TxPool().SetGasPrice(utils.GlobalBig(ctx, utils.GasPriceFlag.Name))
|
||||
if err := ethereum.StartMining(true); err != nil {
|
||||
utils.Fatalf("Failed to start mining: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func version(c *cli.Context) {
|
||||
fmt.Println(ClientIdentifier)
|
||||
fmt.Println("Version:", Version)
|
||||
if gitCommit != "" {
|
||||
fmt.Println("Git Commit:", gitCommit)
|
||||
}
|
||||
fmt.Println("Protocol Versions:", eth.ProtocolVersions)
|
||||
fmt.Println("Network Id:", c.GlobalInt(utils.NetworkIdFlag.Name))
|
||||
fmt.Println("Go Version:", runtime.Version())
|
||||
fmt.Println("OS:", runtime.GOOS)
|
||||
fmt.Printf("GOPATH=%s\n", os.Getenv("GOPATH"))
|
||||
fmt.Printf("GOROOT=%s\n", runtime.GOROOT())
|
||||
}
|
||||
|
||||
139
cmd/geth/misccmd.go
Normal file
139
cmd/geth/misccmd.go
Normal file
@@ -0,0 +1,139 @@
|
||||
// Copyright 2016 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 (
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/ethereum/go-ethereum/cmd/utils"
|
||||
"github.com/ethereum/go-ethereum/consensus/ethash"
|
||||
"github.com/ethereum/go-ethereum/eth"
|
||||
"github.com/ethereum/go-ethereum/params"
|
||||
"gopkg.in/urfave/cli.v1"
|
||||
)
|
||||
|
||||
var (
|
||||
makecacheCommand = cli.Command{
|
||||
Action: utils.MigrateFlags(makecache),
|
||||
Name: "makecache",
|
||||
Usage: "Generate ethash verification cache (for testing)",
|
||||
ArgsUsage: "<blockNum> <outputDir>",
|
||||
Category: "MISCELLANEOUS COMMANDS",
|
||||
Description: `
|
||||
The makecache command generates an ethash cache in <outputDir>.
|
||||
|
||||
This command exists to support the system testing project.
|
||||
Regular users do not need to execute it.
|
||||
`,
|
||||
}
|
||||
makedagCommand = cli.Command{
|
||||
Action: utils.MigrateFlags(makedag),
|
||||
Name: "makedag",
|
||||
Usage: "Generate ethash mining DAG (for testing)",
|
||||
ArgsUsage: "<blockNum> <outputDir>",
|
||||
Category: "MISCELLANEOUS COMMANDS",
|
||||
Description: `
|
||||
The makedag command generates an ethash DAG in <outputDir>.
|
||||
|
||||
This command exists to support the system testing project.
|
||||
Regular users do not need to execute it.
|
||||
`,
|
||||
}
|
||||
versionCommand = cli.Command{
|
||||
Action: utils.MigrateFlags(version),
|
||||
Name: "version",
|
||||
Usage: "Print version numbers",
|
||||
ArgsUsage: " ",
|
||||
Category: "MISCELLANEOUS COMMANDS",
|
||||
Description: `
|
||||
The output of this command is supposed to be machine-readable.
|
||||
`,
|
||||
}
|
||||
licenseCommand = cli.Command{
|
||||
Action: utils.MigrateFlags(license),
|
||||
Name: "license",
|
||||
Usage: "Display license information",
|
||||
ArgsUsage: " ",
|
||||
Category: "MISCELLANEOUS COMMANDS",
|
||||
}
|
||||
)
|
||||
|
||||
// makecache generates an ethash verification cache into the provided folder.
|
||||
func makecache(ctx *cli.Context) error {
|
||||
args := ctx.Args()
|
||||
if len(args) != 2 {
|
||||
utils.Fatalf(`Usage: geth makecache <block number> <outputdir>`)
|
||||
}
|
||||
block, err := strconv.ParseUint(args[0], 0, 64)
|
||||
if err != nil {
|
||||
utils.Fatalf("Invalid block number: %v", err)
|
||||
}
|
||||
ethash.MakeCache(block, args[1])
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// makedag generates an ethash mining DAG into the provided folder.
|
||||
func makedag(ctx *cli.Context) error {
|
||||
args := ctx.Args()
|
||||
if len(args) != 2 {
|
||||
utils.Fatalf(`Usage: geth makedag <block number> <outputdir>`)
|
||||
}
|
||||
block, err := strconv.ParseUint(args[0], 0, 64)
|
||||
if err != nil {
|
||||
utils.Fatalf("Invalid block number: %v", err)
|
||||
}
|
||||
ethash.MakeDataset(block, args[1])
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func version(ctx *cli.Context) error {
|
||||
fmt.Println(strings.Title(clientIdentifier))
|
||||
fmt.Println("Version:", params.Version)
|
||||
if gitCommit != "" {
|
||||
fmt.Println("Git Commit:", gitCommit)
|
||||
}
|
||||
fmt.Println("Architecture:", runtime.GOARCH)
|
||||
fmt.Println("Protocol Versions:", eth.ProtocolVersions)
|
||||
fmt.Println("Network Id:", eth.DefaultConfig.NetworkId)
|
||||
fmt.Println("Go Version:", runtime.Version())
|
||||
fmt.Println("Operating System:", runtime.GOOS)
|
||||
fmt.Printf("GOPATH=%s\n", os.Getenv("GOPATH"))
|
||||
fmt.Printf("GOROOT=%s\n", runtime.GOROOT())
|
||||
return nil
|
||||
}
|
||||
|
||||
func license(_ *cli.Context) error {
|
||||
fmt.Println(`Geth 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.
|
||||
|
||||
Geth 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 geth. If not, see <http://www.gnu.org/licenses/>.`)
|
||||
return nil
|
||||
}
|
||||
@@ -25,19 +25,17 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/codegangsta/cli"
|
||||
"github.com/ethereum/go-ethereum/cmd/utils"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/node"
|
||||
"github.com/ethereum/go-ethereum/rpc"
|
||||
"github.com/ethereum/go-ethereum/rpc/codec"
|
||||
"github.com/ethereum/go-ethereum/rpc/comms"
|
||||
"github.com/gizak/termui"
|
||||
"gopkg.in/urfave/cli.v1"
|
||||
)
|
||||
|
||||
var (
|
||||
monitorCommandAttachFlag = cli.StringFlag{
|
||||
Name: "attach",
|
||||
Value: "ipc:" + common.DefaultIpcPath(),
|
||||
Value: node.DefaultIPCEndpoint(clientIdentifier),
|
||||
Usage: "API endpoint to attach to",
|
||||
}
|
||||
monitorCommandRowsFlag = cli.IntFlag{
|
||||
@@ -51,9 +49,11 @@ var (
|
||||
Usage: "Refresh interval in seconds",
|
||||
}
|
||||
monitorCommand = cli.Command{
|
||||
Action: monitor,
|
||||
Name: "monitor",
|
||||
Usage: `Geth Monitor: node metrics monitoring and visualization`,
|
||||
Action: utils.MigrateFlags(monitor), // keep track of migration progress
|
||||
Name: "monitor",
|
||||
Usage: "Monitor and visualize node metrics",
|
||||
ArgsUsage: " ",
|
||||
Category: "MONITOR COMMANDS",
|
||||
Description: `
|
||||
The Geth monitor is a tool to collect and visualize various internal metrics
|
||||
gathered by the node, supporting different chart types as well as the capacity
|
||||
@@ -68,22 +68,20 @@ to display multiple metrics simultaneously.
|
||||
)
|
||||
|
||||
// monitor starts a terminal UI based monitoring tool for the requested metrics.
|
||||
func monitor(ctx *cli.Context) {
|
||||
func monitor(ctx *cli.Context) error {
|
||||
var (
|
||||
client comms.EthereumClient
|
||||
client *rpc.Client
|
||||
err error
|
||||
)
|
||||
// Attach to an Ethereum node over IPC or RPC
|
||||
endpoint := ctx.String(monitorCommandAttachFlag.Name)
|
||||
if client, err = comms.ClientFromEndpoint(endpoint, codec.JSON); err != nil {
|
||||
if client, err = dialRPC(endpoint); err != nil {
|
||||
utils.Fatalf("Unable to attach to geth node: %v", err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
xeth := rpc.NewXeth(client)
|
||||
|
||||
// Retrieve all the available metrics and resolve the user pattens
|
||||
metrics, err := retrieveMetrics(xeth)
|
||||
metrics, err := retrieveMetrics(client)
|
||||
if err != nil {
|
||||
utils.Fatalf("Failed to retrieve system metrics: %v", err)
|
||||
}
|
||||
@@ -108,8 +106,6 @@ func monitor(ctx *cli.Context) {
|
||||
}
|
||||
defer termui.Close()
|
||||
|
||||
termui.UseTheme("helloworld")
|
||||
|
||||
rows := len(monitored)
|
||||
if max := ctx.Int(monitorCommandRowsFlag.Name); rows > max {
|
||||
rows = max
|
||||
@@ -120,7 +116,7 @@ func monitor(ctx *cli.Context) {
|
||||
}
|
||||
// Create each individual data chart
|
||||
footer := termui.NewPar("")
|
||||
footer.HasBorder = true
|
||||
footer.Block.Border = true
|
||||
footer.Height = 3
|
||||
|
||||
charts := make([]*termui.LineChart, len(monitored))
|
||||
@@ -133,39 +129,41 @@ func monitor(ctx *cli.Context) {
|
||||
}
|
||||
termui.Body.AddRows(termui.NewRow(termui.NewCol(12, 0, footer)))
|
||||
|
||||
refreshCharts(xeth, monitored, data, units, charts, ctx, footer)
|
||||
refreshCharts(client, monitored, data, units, charts, ctx, footer)
|
||||
termui.Body.Align()
|
||||
termui.Render(termui.Body)
|
||||
|
||||
// Watch for various system events, and periodically refresh the charts
|
||||
refresh := time.Tick(time.Duration(ctx.Int(monitorCommandRefreshFlag.Name)) * time.Second)
|
||||
for {
|
||||
select {
|
||||
case event := <-termui.EventCh():
|
||||
if event.Type == termui.EventKey && event.Key == termui.KeyCtrlC {
|
||||
return
|
||||
}
|
||||
if event.Type == termui.EventResize {
|
||||
termui.Body.Width = termui.TermWidth()
|
||||
for _, chart := range charts {
|
||||
chart.Height = (termui.TermHeight() - footer.Height) / rows
|
||||
}
|
||||
termui.Body.Align()
|
||||
termui.Render(termui.Body)
|
||||
}
|
||||
case <-refresh:
|
||||
if refreshCharts(xeth, monitored, data, units, charts, ctx, footer) {
|
||||
termui.Handle("/sys/kbd/C-c", func(termui.Event) {
|
||||
termui.StopLoop()
|
||||
})
|
||||
termui.Handle("/sys/wnd/resize", func(termui.Event) {
|
||||
termui.Body.Width = termui.TermWidth()
|
||||
for _, chart := range charts {
|
||||
chart.Height = (termui.TermHeight() - footer.Height) / rows
|
||||
}
|
||||
termui.Body.Align()
|
||||
termui.Render(termui.Body)
|
||||
})
|
||||
go func() {
|
||||
tick := time.NewTicker(time.Duration(ctx.Int(monitorCommandRefreshFlag.Name)) * time.Second)
|
||||
for range tick.C {
|
||||
if refreshCharts(client, monitored, data, units, charts, ctx, footer) {
|
||||
termui.Body.Align()
|
||||
}
|
||||
termui.Render(termui.Body)
|
||||
}
|
||||
}
|
||||
}()
|
||||
termui.Loop()
|
||||
return nil
|
||||
}
|
||||
|
||||
// retrieveMetrics contacts the attached geth node and retrieves the entire set
|
||||
// of collected system metrics.
|
||||
func retrieveMetrics(xeth *rpc.Xeth) (map[string]interface{}, error) {
|
||||
return xeth.Call("debug_metrics", []interface{}{true})
|
||||
func retrieveMetrics(client *rpc.Client) (map[string]interface{}, error) {
|
||||
var metrics map[string]interface{}
|
||||
err := client.Call(&metrics, "debug_metrics", true)
|
||||
return metrics, err
|
||||
}
|
||||
|
||||
// resolveMetrics takes a list of input metric patterns, and resolves each to one
|
||||
@@ -238,8 +236,9 @@ func expandMetrics(metrics map[string]interface{}, path string) []string {
|
||||
|
||||
// fetchMetric iterates over the metrics map and retrieves a specific one.
|
||||
func fetchMetric(metrics map[string]interface{}, metric string) float64 {
|
||||
parts, found := strings.Split(metric, "/"), true
|
||||
parts := strings.Split(metric, "/")
|
||||
for _, part := range parts[:len(parts)-1] {
|
||||
var found bool
|
||||
metrics, found = metrics[part].(map[string]interface{})
|
||||
if !found {
|
||||
return 0
|
||||
@@ -253,8 +252,8 @@ func fetchMetric(metrics map[string]interface{}, metric string) float64 {
|
||||
|
||||
// refreshCharts retrieves a next batch of metrics, and inserts all the new
|
||||
// values into the active datasets and charts
|
||||
func refreshCharts(xeth *rpc.Xeth, metrics []string, data [][]float64, units []int, charts []*termui.LineChart, ctx *cli.Context, footer *termui.Par) (realign bool) {
|
||||
values, err := retrieveMetrics(xeth)
|
||||
func refreshCharts(client *rpc.Client, metrics []string, data [][]float64, units []int, charts []*termui.LineChart, ctx *cli.Context, footer *termui.Par) (realign bool) {
|
||||
values, err := retrieveMetrics(client)
|
||||
for i, metric := range metrics {
|
||||
if len(data) < 512 {
|
||||
data[i] = append([]float64{fetchMetric(values, metric)}, data[i]...)
|
||||
@@ -289,7 +288,7 @@ func updateChart(metric string, data []float64, base *int, chart *termui.LineCha
|
||||
}
|
||||
}
|
||||
unit, scale := 0, 1.0
|
||||
for high >= 1000 {
|
||||
for high >= 1000 && unit+1 < len(dataUnits) {
|
||||
high, unit, scale = high/1000, unit+1, scale*1000
|
||||
}
|
||||
// If the unit changes, re-create the chart (hack to set max height...)
|
||||
@@ -309,9 +308,9 @@ func updateChart(metric string, data []float64, base *int, chart *termui.LineCha
|
||||
if strings.Contains(metric, "/Percentiles/") || strings.Contains(metric, "/pauses/") || strings.Contains(metric, "/time/") {
|
||||
units = timeUnits
|
||||
}
|
||||
chart.Border.Label = metric
|
||||
chart.BorderLabel = metric
|
||||
if len(units[unit]) > 0 {
|
||||
chart.Border.Label += " [" + units[unit] + "]"
|
||||
chart.BorderLabel += " [" + units[unit] + "]"
|
||||
}
|
||||
chart.LineColor = colors[unit] | termui.AttrBold
|
||||
if err != nil {
|
||||
@@ -331,8 +330,8 @@ func createChart(height int) *termui.LineChart {
|
||||
chart.AxesColor = termui.ColorWhite
|
||||
chart.PaddingBottom = -2
|
||||
|
||||
chart.Border.LabelFgColor = chart.Border.FgColor | termui.AttrBold
|
||||
chart.Border.FgColor = chart.Border.BgColor
|
||||
chart.BorderLabelFg = chart.BorderFg | termui.AttrBold
|
||||
chart.BorderFg = chart.BorderBg
|
||||
|
||||
return chart
|
||||
}
|
||||
@@ -342,7 +341,7 @@ func updateFooter(ctx *cli.Context, err error, footer *termui.Par) {
|
||||
// Generate the basic footer
|
||||
refresh := time.Duration(ctx.Int(monitorCommandRefreshFlag.Name)) * time.Second
|
||||
footer.Text = fmt.Sprintf("Press Ctrl+C to quit. Refresh interval: %v.", refresh)
|
||||
footer.TextFgColor = termui.Theme().ParTextFg | termui.AttrBold
|
||||
footer.TextFgColor = termui.ThemeAttr("par.fg") | termui.AttrBold
|
||||
|
||||
// Append any encountered errors
|
||||
if err != nil {
|
||||
|
||||
98
cmd/geth/run_test.go
Normal file
98
cmd/geth/run_test.go
Normal file
@@ -0,0 +1,98 @@
|
||||
// Copyright 2016 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 (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/pkg/reexec"
|
||||
"github.com/ethereum/go-ethereum/internal/cmdtest"
|
||||
)
|
||||
|
||||
func tmpdir(t *testing.T) string {
|
||||
dir, err := ioutil.TempDir("", "geth-test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return dir
|
||||
}
|
||||
|
||||
type testgeth struct {
|
||||
*cmdtest.TestCmd
|
||||
|
||||
// template variables for expect
|
||||
Datadir string
|
||||
Etherbase string
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Run the app if we've been exec'd as "geth-test" in runGeth.
|
||||
reexec.Register("geth-test", func() {
|
||||
if err := app.Run(os.Args); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(0)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
// check if we have been reexec'd
|
||||
if reexec.Init() {
|
||||
return
|
||||
}
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
// spawns geth with the given command line args. If the args don't set --datadir, the
|
||||
// child g gets a temporary data directory.
|
||||
func runGeth(t *testing.T, args ...string) *testgeth {
|
||||
tt := &testgeth{}
|
||||
tt.TestCmd = cmdtest.NewTestCmd(t, tt)
|
||||
for i, arg := range args {
|
||||
switch {
|
||||
case arg == "-datadir" || arg == "--datadir":
|
||||
if i < len(args)-1 {
|
||||
tt.Datadir = args[i+1]
|
||||
}
|
||||
case arg == "-etherbase" || arg == "--etherbase":
|
||||
if i < len(args)-1 {
|
||||
tt.Etherbase = args[i+1]
|
||||
}
|
||||
}
|
||||
}
|
||||
if tt.Datadir == "" {
|
||||
tt.Datadir = tmpdir(t)
|
||||
tt.Cleanup = func() { os.RemoveAll(tt.Datadir) }
|
||||
args = append([]string{"-datadir", tt.Datadir}, args...)
|
||||
// Remove the temporary datadir if something fails below.
|
||||
defer func() {
|
||||
if t.Failed() {
|
||||
tt.Cleanup()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Boot "geth". This actually runs the test binary but the TestMain
|
||||
// function will prevent any tests from running.
|
||||
tt.Run("geth-test", args...)
|
||||
|
||||
return tt
|
||||
}
|
||||
1
cmd/geth/testdata/empty.js
vendored
Normal file
1
cmd/geth/testdata/empty.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
6
cmd/geth/testdata/guswallet.json
vendored
Normal file
6
cmd/geth/testdata/guswallet.json
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"encseed": "26d87f5f2bf9835f9a47eefae571bc09f9107bb13d54ff12a4ec095d01f83897494cf34f7bed2ed34126ecba9db7b62de56c9d7cd136520a0427bfb11b8954ba7ac39b90d4650d3448e31185affcd74226a68f1e94b1108e6e0a4a91cdd83eba",
|
||||
"ethaddr": "d4584b5f6229b7be90727b0fc8c6b91bb427821f",
|
||||
"email": "gustav.simonsson@gmail.com",
|
||||
"btcaddr": "1EVknXyFC68kKNLkh6YnKzW41svSRoaAcx"
|
||||
}
|
||||
3
cmd/geth/testdata/passwords.txt
vendored
Normal file
3
cmd/geth/testdata/passwords.txt
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
foobar
|
||||
foobar
|
||||
foobar
|
||||
3
cmd/geth/testdata/wrong-passwords.txt
vendored
Normal file
3
cmd/geth/testdata/wrong-passwords.txt
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
wrong
|
||||
wrong
|
||||
wrong
|
||||
325
cmd/geth/usage.go
Normal file
325
cmd/geth/usage.go
Normal file
@@ -0,0 +1,325 @@
|
||||
// Copyright 2015 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/>.
|
||||
|
||||
// Contains the geth command usage template and generator.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
"sort"
|
||||
|
||||
"github.com/ethereum/go-ethereum/cmd/utils"
|
||||
"github.com/ethereum/go-ethereum/internal/debug"
|
||||
"gopkg.in/urfave/cli.v1"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// AppHelpTemplate is the test template for the default, global app help topic.
|
||||
var AppHelpTemplate = `NAME:
|
||||
{{.App.Name}} - {{.App.Usage}}
|
||||
|
||||
Copyright 2013-2017 The go-ethereum Authors
|
||||
|
||||
USAGE:
|
||||
{{.App.HelpName}} [options]{{if .App.Commands}} command [command options]{{end}} {{if .App.ArgsUsage}}{{.App.ArgsUsage}}{{else}}[arguments...]{{end}}
|
||||
{{if .App.Version}}
|
||||
VERSION:
|
||||
{{.App.Version}}
|
||||
{{end}}{{if len .App.Authors}}
|
||||
AUTHOR(S):
|
||||
{{range .App.Authors}}{{ . }}{{end}}
|
||||
{{end}}{{if .App.Commands}}
|
||||
COMMANDS:
|
||||
{{range .App.Commands}}{{join .Names ", "}}{{ "\t" }}{{.Usage}}
|
||||
{{end}}{{end}}{{if .FlagGroups}}
|
||||
{{range .FlagGroups}}{{.Name}} OPTIONS:
|
||||
{{range .Flags}}{{.}}
|
||||
{{end}}
|
||||
{{end}}{{end}}{{if .App.Copyright }}
|
||||
COPYRIGHT:
|
||||
{{.App.Copyright}}
|
||||
{{end}}
|
||||
`
|
||||
|
||||
// flagGroup is a collection of flags belonging to a single topic.
|
||||
type flagGroup struct {
|
||||
Name string
|
||||
Flags []cli.Flag
|
||||
}
|
||||
|
||||
// AppHelpFlagGroups is the application flags, grouped by functionality.
|
||||
var AppHelpFlagGroups = []flagGroup{
|
||||
{
|
||||
Name: "ETHEREUM",
|
||||
Flags: []cli.Flag{
|
||||
configFileFlag,
|
||||
utils.DataDirFlag,
|
||||
utils.KeyStoreDirFlag,
|
||||
utils.NoUSBFlag,
|
||||
utils.NetworkIdFlag,
|
||||
utils.TestnetFlag,
|
||||
utils.RinkebyFlag,
|
||||
utils.SyncModeFlag,
|
||||
utils.EthStatsURLFlag,
|
||||
utils.IdentityFlag,
|
||||
utils.LightServFlag,
|
||||
utils.LightPeersFlag,
|
||||
utils.LightKDFFlag,
|
||||
},
|
||||
},
|
||||
{Name: "DEVELOPER CHAIN",
|
||||
Flags: []cli.Flag{
|
||||
utils.DeveloperFlag,
|
||||
utils.DeveloperPeriodFlag,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "ETHASH",
|
||||
Flags: []cli.Flag{
|
||||
utils.EthashCacheDirFlag,
|
||||
utils.EthashCachesInMemoryFlag,
|
||||
utils.EthashCachesOnDiskFlag,
|
||||
utils.EthashDatasetDirFlag,
|
||||
utils.EthashDatasetsInMemoryFlag,
|
||||
utils.EthashDatasetsOnDiskFlag,
|
||||
},
|
||||
},
|
||||
//{
|
||||
// Name: "DASHBOARD",
|
||||
// Flags: []cli.Flag{
|
||||
// utils.DashboardEnabledFlag,
|
||||
// utils.DashboardAddrFlag,
|
||||
// utils.DashboardPortFlag,
|
||||
// utils.DashboardRefreshFlag,
|
||||
// utils.DashboardAssetsFlag,
|
||||
// },
|
||||
//},
|
||||
{
|
||||
Name: "TRANSACTION POOL",
|
||||
Flags: []cli.Flag{
|
||||
utils.TxPoolNoLocalsFlag,
|
||||
utils.TxPoolJournalFlag,
|
||||
utils.TxPoolRejournalFlag,
|
||||
utils.TxPoolPriceLimitFlag,
|
||||
utils.TxPoolPriceBumpFlag,
|
||||
utils.TxPoolAccountSlotsFlag,
|
||||
utils.TxPoolGlobalSlotsFlag,
|
||||
utils.TxPoolAccountQueueFlag,
|
||||
utils.TxPoolGlobalQueueFlag,
|
||||
utils.TxPoolLifetimeFlag,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "PERFORMANCE TUNING",
|
||||
Flags: []cli.Flag{
|
||||
utils.CacheFlag,
|
||||
utils.TrieCacheGenFlag,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "ACCOUNT",
|
||||
Flags: []cli.Flag{
|
||||
utils.UnlockedAccountFlag,
|
||||
utils.PasswordFileFlag,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "API AND CONSOLE",
|
||||
Flags: []cli.Flag{
|
||||
utils.RPCEnabledFlag,
|
||||
utils.RPCListenAddrFlag,
|
||||
utils.RPCPortFlag,
|
||||
utils.RPCApiFlag,
|
||||
utils.WSEnabledFlag,
|
||||
utils.WSListenAddrFlag,
|
||||
utils.WSPortFlag,
|
||||
utils.WSApiFlag,
|
||||
utils.WSAllowedOriginsFlag,
|
||||
utils.IPCDisabledFlag,
|
||||
utils.IPCPathFlag,
|
||||
utils.RPCCORSDomainFlag,
|
||||
utils.JSpathFlag,
|
||||
utils.ExecFlag,
|
||||
utils.PreloadJSFlag,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "NETWORKING",
|
||||
Flags: []cli.Flag{
|
||||
utils.BootnodesFlag,
|
||||
utils.BootnodesV4Flag,
|
||||
utils.BootnodesV5Flag,
|
||||
utils.ListenPortFlag,
|
||||
utils.MaxPeersFlag,
|
||||
utils.MaxPendingPeersFlag,
|
||||
utils.NATFlag,
|
||||
utils.NoDiscoverFlag,
|
||||
utils.DiscoveryV5Flag,
|
||||
utils.NetrestrictFlag,
|
||||
utils.NodeKeyFileFlag,
|
||||
utils.NodeKeyHexFlag,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "MINER",
|
||||
Flags: []cli.Flag{
|
||||
utils.MiningEnabledFlag,
|
||||
utils.MinerThreadsFlag,
|
||||
utils.EtherbaseFlag,
|
||||
utils.TargetGasLimitFlag,
|
||||
utils.GasPriceFlag,
|
||||
utils.ExtraDataFlag,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "GAS PRICE ORACLE",
|
||||
Flags: []cli.Flag{
|
||||
utils.GpoBlocksFlag,
|
||||
utils.GpoPercentileFlag,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "VIRTUAL MACHINE",
|
||||
Flags: []cli.Flag{
|
||||
utils.VMEnableDebugFlag,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "LOGGING AND DEBUGGING",
|
||||
Flags: append([]cli.Flag{
|
||||
utils.MetricsEnabledFlag,
|
||||
utils.FakePoWFlag,
|
||||
utils.NoCompactionFlag,
|
||||
}, debug.Flags...),
|
||||
},
|
||||
{
|
||||
Name: "WHISPER (EXPERIMENTAL)",
|
||||
Flags: whisperFlags,
|
||||
},
|
||||
{
|
||||
Name: "DEPRECATED",
|
||||
Flags: []cli.Flag{
|
||||
utils.FastSyncFlag,
|
||||
utils.LightModeFlag,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "MISC",
|
||||
},
|
||||
}
|
||||
|
||||
// byCategory sorts an array of flagGroup by Name in the order
|
||||
// defined in AppHelpFlagGroups.
|
||||
type byCategory []flagGroup
|
||||
|
||||
func (a byCategory) Len() int { return len(a) }
|
||||
func (a byCategory) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a byCategory) Less(i, j int) bool {
|
||||
iCat, jCat := a[i].Name, a[j].Name
|
||||
iIdx, jIdx := len(AppHelpFlagGroups), len(AppHelpFlagGroups) // ensure non categorized flags come last
|
||||
|
||||
for i, group := range AppHelpFlagGroups {
|
||||
if iCat == group.Name {
|
||||
iIdx = i
|
||||
}
|
||||
if jCat == group.Name {
|
||||
jIdx = i
|
||||
}
|
||||
}
|
||||
|
||||
return iIdx < jIdx
|
||||
}
|
||||
|
||||
func flagCategory(flag cli.Flag) string {
|
||||
for _, category := range AppHelpFlagGroups {
|
||||
for _, flg := range category.Flags {
|
||||
if flg.GetName() == flag.GetName() {
|
||||
return category.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
return "MISC"
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Override the default app help template
|
||||
cli.AppHelpTemplate = AppHelpTemplate
|
||||
|
||||
// Define a one shot struct to pass to the usage template
|
||||
type helpData struct {
|
||||
App interface{}
|
||||
FlagGroups []flagGroup
|
||||
}
|
||||
|
||||
// Override the default app help printer, but only for the global app help
|
||||
originalHelpPrinter := cli.HelpPrinter
|
||||
cli.HelpPrinter = func(w io.Writer, tmpl string, data interface{}) {
|
||||
if tmpl == AppHelpTemplate {
|
||||
// Iterate over all the flags and add any uncategorized ones
|
||||
categorized := make(map[string]struct{})
|
||||
for _, group := range AppHelpFlagGroups {
|
||||
for _, flag := range group.Flags {
|
||||
categorized[flag.String()] = struct{}{}
|
||||
}
|
||||
}
|
||||
uncategorized := []cli.Flag{}
|
||||
for _, flag := range data.(*cli.App).Flags {
|
||||
if _, ok := categorized[flag.String()]; !ok {
|
||||
if strings.HasPrefix(flag.GetName(), "dashboard") {
|
||||
continue
|
||||
}
|
||||
uncategorized = append(uncategorized, flag)
|
||||
}
|
||||
}
|
||||
if len(uncategorized) > 0 {
|
||||
// Append all ungategorized options to the misc group
|
||||
miscs := len(AppHelpFlagGroups[len(AppHelpFlagGroups)-1].Flags)
|
||||
AppHelpFlagGroups[len(AppHelpFlagGroups)-1].Flags = append(AppHelpFlagGroups[len(AppHelpFlagGroups)-1].Flags, uncategorized...)
|
||||
|
||||
// Make sure they are removed afterwards
|
||||
defer func() {
|
||||
AppHelpFlagGroups[len(AppHelpFlagGroups)-1].Flags = AppHelpFlagGroups[len(AppHelpFlagGroups)-1].Flags[:miscs]
|
||||
}()
|
||||
}
|
||||
// Render out custom usage screen
|
||||
originalHelpPrinter(w, tmpl, helpData{data, AppHelpFlagGroups})
|
||||
} else if tmpl == utils.CommandHelpTemplate {
|
||||
// Iterate over all command specific flags and categorize them
|
||||
categorized := make(map[string][]cli.Flag)
|
||||
for _, flag := range data.(cli.Command).Flags {
|
||||
if _, ok := categorized[flag.String()]; !ok {
|
||||
categorized[flagCategory(flag)] = append(categorized[flagCategory(flag)], flag)
|
||||
}
|
||||
}
|
||||
|
||||
// sort to get a stable ordering
|
||||
sorted := make([]flagGroup, 0, len(categorized))
|
||||
for cat, flgs := range categorized {
|
||||
sorted = append(sorted, flagGroup{cat, flgs})
|
||||
}
|
||||
sort.Sort(byCategory(sorted))
|
||||
|
||||
// add sorted array to data and render with default printer
|
||||
originalHelpPrinter(w, tmpl, map[string]interface{}{
|
||||
"cmd": data,
|
||||
"categorizedFlags": sorted,
|
||||
})
|
||||
} else {
|
||||
originalHelpPrinter(w, tmpl, data)
|
||||
}
|
||||
}
|
||||
}
|
||||
46
cmd/internal/browser/browser.go
Normal file
46
cmd/internal/browser/browser.go
Normal file
@@ -0,0 +1,46 @@
|
||||
// Copyright 2016 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package browser provides utilities for interacting with users' browsers.
|
||||
package browser
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
// Commands returns a list of possible commands to use to open a url.
|
||||
func Commands() [][]string {
|
||||
var cmds [][]string
|
||||
if exe := os.Getenv("BROWSER"); exe != "" {
|
||||
cmds = append(cmds, []string{exe})
|
||||
}
|
||||
switch runtime.GOOS {
|
||||
case "darwin":
|
||||
cmds = append(cmds, []string{"/usr/bin/open"})
|
||||
case "windows":
|
||||
cmds = append(cmds, []string{"cmd", "/c", "start"})
|
||||
default:
|
||||
cmds = append(cmds, []string{"xdg-open"})
|
||||
}
|
||||
cmds = append(cmds,
|
||||
[]string{"chrome"},
|
||||
[]string{"google-chrome"},
|
||||
[]string{"chromium"},
|
||||
[]string{"firefox"},
|
||||
)
|
||||
return cmds
|
||||
}
|
||||
|
||||
// Open tries to open url in a browser and reports whether it succeeded.
|
||||
func Open(url string) bool {
|
||||
for _, args := range Commands() {
|
||||
cmd := exec.Command(args[0], append(args[1:], url)...)
|
||||
if cmd.Start() == nil {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
414
cmd/p2psim/main.go
Normal file
414
cmd/p2psim/main.go
Normal file
@@ -0,0 +1,414 @@
|
||||
// p2psim provides a command-line client for a simulation HTTP API.
|
||||
//
|
||||
// Here is an example of creating a 2 node network with the first node
|
||||
// connected to the second:
|
||||
//
|
||||
// $ p2psim node create
|
||||
// Created node01
|
||||
//
|
||||
// $ p2psim node start node01
|
||||
// Started node01
|
||||
//
|
||||
// $ p2psim node create
|
||||
// Created node02
|
||||
//
|
||||
// $ p2psim node start node02
|
||||
// Started node02
|
||||
//
|
||||
// $ p2psim node connect node01 node02
|
||||
// Connected node01 to node02
|
||||
//
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/ethereum/go-ethereum/p2p"
|
||||
"github.com/ethereum/go-ethereum/p2p/discover"
|
||||
"github.com/ethereum/go-ethereum/p2p/simulations"
|
||||
"github.com/ethereum/go-ethereum/p2p/simulations/adapters"
|
||||
"github.com/ethereum/go-ethereum/rpc"
|
||||
"gopkg.in/urfave/cli.v1"
|
||||
)
|
||||
|
||||
var client *simulations.Client
|
||||
|
||||
func main() {
|
||||
app := cli.NewApp()
|
||||
app.Usage = "devp2p simulation command-line client"
|
||||
app.Flags = []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "api",
|
||||
Value: "http://localhost:8888",
|
||||
Usage: "simulation API URL",
|
||||
EnvVar: "P2PSIM_API_URL",
|
||||
},
|
||||
}
|
||||
app.Before = func(ctx *cli.Context) error {
|
||||
client = simulations.NewClient(ctx.GlobalString("api"))
|
||||
return nil
|
||||
}
|
||||
app.Commands = []cli.Command{
|
||||
{
|
||||
Name: "show",
|
||||
Usage: "show network information",
|
||||
Action: showNetwork,
|
||||
},
|
||||
{
|
||||
Name: "events",
|
||||
Usage: "stream network events",
|
||||
Action: streamNetwork,
|
||||
Flags: []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "current",
|
||||
Usage: "get existing nodes and conns first",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "filter",
|
||||
Value: "",
|
||||
Usage: "message filter",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "snapshot",
|
||||
Usage: "create a network snapshot to stdout",
|
||||
Action: createSnapshot,
|
||||
},
|
||||
{
|
||||
Name: "load",
|
||||
Usage: "load a network snapshot from stdin",
|
||||
Action: loadSnapshot,
|
||||
},
|
||||
{
|
||||
Name: "node",
|
||||
Usage: "manage simulation nodes",
|
||||
Action: listNodes,
|
||||
Subcommands: []cli.Command{
|
||||
{
|
||||
Name: "list",
|
||||
Usage: "list nodes",
|
||||
Action: listNodes,
|
||||
},
|
||||
{
|
||||
Name: "create",
|
||||
Usage: "create a node",
|
||||
Action: createNode,
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "name",
|
||||
Value: "",
|
||||
Usage: "node name",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "services",
|
||||
Value: "",
|
||||
Usage: "node services (comma separated)",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "key",
|
||||
Value: "",
|
||||
Usage: "node private key (hex encoded)",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "show",
|
||||
ArgsUsage: "<node>",
|
||||
Usage: "show node information",
|
||||
Action: showNode,
|
||||
},
|
||||
{
|
||||
Name: "start",
|
||||
ArgsUsage: "<node>",
|
||||
Usage: "start a node",
|
||||
Action: startNode,
|
||||
},
|
||||
{
|
||||
Name: "stop",
|
||||
ArgsUsage: "<node>",
|
||||
Usage: "stop a node",
|
||||
Action: stopNode,
|
||||
},
|
||||
{
|
||||
Name: "connect",
|
||||
ArgsUsage: "<node> <peer>",
|
||||
Usage: "connect a node to a peer node",
|
||||
Action: connectNode,
|
||||
},
|
||||
{
|
||||
Name: "disconnect",
|
||||
ArgsUsage: "<node> <peer>",
|
||||
Usage: "disconnect a node from a peer node",
|
||||
Action: disconnectNode,
|
||||
},
|
||||
{
|
||||
Name: "rpc",
|
||||
ArgsUsage: "<node> <method> [<args>]",
|
||||
Usage: "call a node RPC method",
|
||||
Action: rpcNode,
|
||||
Flags: []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "subscribe",
|
||||
Usage: "method is a subscription",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
app.Run(os.Args)
|
||||
}
|
||||
|
||||
func showNetwork(ctx *cli.Context) error {
|
||||
if len(ctx.Args()) != 0 {
|
||||
return cli.ShowCommandHelp(ctx, ctx.Command.Name)
|
||||
}
|
||||
network, err := client.GetNetwork()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
w := tabwriter.NewWriter(ctx.App.Writer, 1, 2, 2, ' ', 0)
|
||||
defer w.Flush()
|
||||
fmt.Fprintf(w, "NODES\t%d\n", len(network.Nodes))
|
||||
fmt.Fprintf(w, "CONNS\t%d\n", len(network.Conns))
|
||||
return nil
|
||||
}
|
||||
|
||||
func streamNetwork(ctx *cli.Context) error {
|
||||
if len(ctx.Args()) != 0 {
|
||||
return cli.ShowCommandHelp(ctx, ctx.Command.Name)
|
||||
}
|
||||
events := make(chan *simulations.Event)
|
||||
sub, err := client.SubscribeNetwork(events, simulations.SubscribeOpts{
|
||||
Current: ctx.Bool("current"),
|
||||
Filter: ctx.String("filter"),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer sub.Unsubscribe()
|
||||
enc := json.NewEncoder(ctx.App.Writer)
|
||||
for {
|
||||
select {
|
||||
case event := <-events:
|
||||
if err := enc.Encode(event); err != nil {
|
||||
return err
|
||||
}
|
||||
case err := <-sub.Err():
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func createSnapshot(ctx *cli.Context) error {
|
||||
if len(ctx.Args()) != 0 {
|
||||
return cli.ShowCommandHelp(ctx, ctx.Command.Name)
|
||||
}
|
||||
snap, err := client.CreateSnapshot()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.NewEncoder(os.Stdout).Encode(snap)
|
||||
}
|
||||
|
||||
func loadSnapshot(ctx *cli.Context) error {
|
||||
if len(ctx.Args()) != 0 {
|
||||
return cli.ShowCommandHelp(ctx, ctx.Command.Name)
|
||||
}
|
||||
snap := &simulations.Snapshot{}
|
||||
if err := json.NewDecoder(os.Stdin).Decode(snap); err != nil {
|
||||
return err
|
||||
}
|
||||
return client.LoadSnapshot(snap)
|
||||
}
|
||||
|
||||
func listNodes(ctx *cli.Context) error {
|
||||
if len(ctx.Args()) != 0 {
|
||||
return cli.ShowCommandHelp(ctx, ctx.Command.Name)
|
||||
}
|
||||
nodes, err := client.GetNodes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
w := tabwriter.NewWriter(ctx.App.Writer, 1, 2, 2, ' ', 0)
|
||||
defer w.Flush()
|
||||
fmt.Fprintf(w, "NAME\tPROTOCOLS\tID\n")
|
||||
for _, node := range nodes {
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\n", node.Name, strings.Join(protocolList(node), ","), node.ID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func protocolList(node *p2p.NodeInfo) []string {
|
||||
protos := make([]string, 0, len(node.Protocols))
|
||||
for name := range node.Protocols {
|
||||
protos = append(protos, name)
|
||||
}
|
||||
return protos
|
||||
}
|
||||
|
||||
func createNode(ctx *cli.Context) error {
|
||||
if len(ctx.Args()) != 0 {
|
||||
return cli.ShowCommandHelp(ctx, ctx.Command.Name)
|
||||
}
|
||||
config := &adapters.NodeConfig{
|
||||
Name: ctx.String("name"),
|
||||
}
|
||||
if key := ctx.String("key"); key != "" {
|
||||
privKey, err := crypto.HexToECDSA(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
config.ID = discover.PubkeyID(&privKey.PublicKey)
|
||||
config.PrivateKey = privKey
|
||||
}
|
||||
if services := ctx.String("services"); services != "" {
|
||||
config.Services = strings.Split(services, ",")
|
||||
}
|
||||
node, err := client.CreateNode(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintln(ctx.App.Writer, "Created", node.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func showNode(ctx *cli.Context) error {
|
||||
args := ctx.Args()
|
||||
if len(args) != 1 {
|
||||
return cli.ShowCommandHelp(ctx, ctx.Command.Name)
|
||||
}
|
||||
nodeName := args[0]
|
||||
node, err := client.GetNode(nodeName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
w := tabwriter.NewWriter(ctx.App.Writer, 1, 2, 2, ' ', 0)
|
||||
defer w.Flush()
|
||||
fmt.Fprintf(w, "NAME\t%s\n", node.Name)
|
||||
fmt.Fprintf(w, "PROTOCOLS\t%s\n", strings.Join(protocolList(node), ","))
|
||||
fmt.Fprintf(w, "ID\t%s\n", node.ID)
|
||||
fmt.Fprintf(w, "ENODE\t%s\n", node.Enode)
|
||||
for name, proto := range node.Protocols {
|
||||
fmt.Fprintln(w)
|
||||
fmt.Fprintf(w, "--- PROTOCOL INFO: %s\n", name)
|
||||
fmt.Fprintf(w, "%v\n", proto)
|
||||
fmt.Fprintf(w, "---\n")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func startNode(ctx *cli.Context) error {
|
||||
args := ctx.Args()
|
||||
if len(args) != 1 {
|
||||
return cli.ShowCommandHelp(ctx, ctx.Command.Name)
|
||||
}
|
||||
nodeName := args[0]
|
||||
if err := client.StartNode(nodeName); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintln(ctx.App.Writer, "Started", nodeName)
|
||||
return nil
|
||||
}
|
||||
|
||||
func stopNode(ctx *cli.Context) error {
|
||||
args := ctx.Args()
|
||||
if len(args) != 1 {
|
||||
return cli.ShowCommandHelp(ctx, ctx.Command.Name)
|
||||
}
|
||||
nodeName := args[0]
|
||||
if err := client.StopNode(nodeName); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintln(ctx.App.Writer, "Stopped", nodeName)
|
||||
return nil
|
||||
}
|
||||
|
||||
func connectNode(ctx *cli.Context) error {
|
||||
args := ctx.Args()
|
||||
if len(args) != 2 {
|
||||
return cli.ShowCommandHelp(ctx, ctx.Command.Name)
|
||||
}
|
||||
nodeName := args[0]
|
||||
peerName := args[1]
|
||||
if err := client.ConnectNode(nodeName, peerName); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintln(ctx.App.Writer, "Connected", nodeName, "to", peerName)
|
||||
return nil
|
||||
}
|
||||
|
||||
func disconnectNode(ctx *cli.Context) error {
|
||||
args := ctx.Args()
|
||||
if len(args) != 2 {
|
||||
return cli.ShowCommandHelp(ctx, ctx.Command.Name)
|
||||
}
|
||||
nodeName := args[0]
|
||||
peerName := args[1]
|
||||
if err := client.DisconnectNode(nodeName, peerName); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintln(ctx.App.Writer, "Disconnected", nodeName, "from", peerName)
|
||||
return nil
|
||||
}
|
||||
|
||||
func rpcNode(ctx *cli.Context) error {
|
||||
args := ctx.Args()
|
||||
if len(args) < 2 {
|
||||
return cli.ShowCommandHelp(ctx, ctx.Command.Name)
|
||||
}
|
||||
nodeName := args[0]
|
||||
method := args[1]
|
||||
rpcClient, err := client.RPCClient(context.Background(), nodeName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ctx.Bool("subscribe") {
|
||||
return rpcSubscribe(rpcClient, ctx.App.Writer, method, args[3:]...)
|
||||
}
|
||||
var result interface{}
|
||||
params := make([]interface{}, len(args[3:]))
|
||||
for i, v := range args[3:] {
|
||||
params[i] = v
|
||||
}
|
||||
if err := rpcClient.Call(&result, method, params...); err != nil {
|
||||
return err
|
||||
}
|
||||
return json.NewEncoder(ctx.App.Writer).Encode(result)
|
||||
}
|
||||
|
||||
func rpcSubscribe(client *rpc.Client, out io.Writer, method string, args ...string) error {
|
||||
parts := strings.SplitN(method, "_", 2)
|
||||
namespace := parts[0]
|
||||
method = parts[1]
|
||||
ch := make(chan interface{})
|
||||
subArgs := make([]interface{}, len(args)+1)
|
||||
subArgs[0] = method
|
||||
for i, v := range args {
|
||||
subArgs[i+1] = v
|
||||
}
|
||||
sub, err := client.Subscribe(context.Background(), namespace, ch, subArgs...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer sub.Unsubscribe()
|
||||
enc := json.NewEncoder(out)
|
||||
for {
|
||||
select {
|
||||
case v := <-ch:
|
||||
if err := enc.Encode(v); err != nil {
|
||||
return err
|
||||
}
|
||||
case err := <-sub.Err():
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
379
cmd/puppeth/genesis.go
Normal file
379
cmd/puppeth/genesis.go
Normal file
@@ -0,0 +1,379 @@
|
||||
// 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 (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"math"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||
"github.com/ethereum/go-ethereum/consensus/ethash"
|
||||
"github.com/ethereum/go-ethereum/core"
|
||||
"github.com/ethereum/go-ethereum/params"
|
||||
)
|
||||
|
||||
// cppEthereumGenesisSpec represents the genesis specification format used by the
|
||||
// C++ Ethereum implementation.
|
||||
type cppEthereumGenesisSpec struct {
|
||||
SealEngine string `json:"sealEngine"`
|
||||
Params struct {
|
||||
AccountStartNonce hexutil.Uint64 `json:"accountStartNonce"`
|
||||
HomesteadForkBlock hexutil.Uint64 `json:"homesteadForkBlock"`
|
||||
EIP150ForkBlock hexutil.Uint64 `json:"EIP150ForkBlock"`
|
||||
EIP158ForkBlock hexutil.Uint64 `json:"EIP158ForkBlock"`
|
||||
ByzantiumForkBlock hexutil.Uint64 `json:"byzantiumForkBlock"`
|
||||
ConstantinopleForkBlock hexutil.Uint64 `json:"constantinopleForkBlock"`
|
||||
NetworkID hexutil.Uint64 `json:"networkID"`
|
||||
ChainID hexutil.Uint64 `json:"chainID"`
|
||||
MaximumExtraDataSize hexutil.Uint64 `json:"maximumExtraDataSize"`
|
||||
MinGasLimit hexutil.Uint64 `json:"minGasLimit"`
|
||||
MaxGasLimit hexutil.Uint64 `json:"maxGasLimit"`
|
||||
GasLimitBoundDivisor hexutil.Uint64 `json:"gasLimitBoundDivisor"`
|
||||
MinimumDifficulty *hexutil.Big `json:"minimumDifficulty"`
|
||||
DifficultyBoundDivisor *hexutil.Big `json:"difficultyBoundDivisor"`
|
||||
DurationLimit *hexutil.Big `json:"durationLimit"`
|
||||
BlockReward *hexutil.Big `json:"blockReward"`
|
||||
} `json:"params"`
|
||||
|
||||
Genesis struct {
|
||||
Nonce hexutil.Bytes `json:"nonce"`
|
||||
Difficulty *hexutil.Big `json:"difficulty"`
|
||||
MixHash common.Hash `json:"mixHash"`
|
||||
Author common.Address `json:"author"`
|
||||
Timestamp hexutil.Uint64 `json:"timestamp"`
|
||||
ParentHash common.Hash `json:"parentHash"`
|
||||
ExtraData hexutil.Bytes `json:"extraData"`
|
||||
GasLimit hexutil.Uint64 `json:"gasLimit"`
|
||||
} `json:"genesis"`
|
||||
|
||||
Accounts map[common.Address]*cppEthereumGenesisSpecAccount `json:"accounts"`
|
||||
}
|
||||
|
||||
// cppEthereumGenesisSpecAccount is the prefunded genesis account and/or precompiled
|
||||
// contract definition.
|
||||
type cppEthereumGenesisSpecAccount struct {
|
||||
Balance *hexutil.Big `json:"balance"`
|
||||
Nonce uint64 `json:"nonce,omitempty"`
|
||||
Precompiled *cppEthereumGenesisSpecBuiltin `json:"precompiled,omitempty"`
|
||||
}
|
||||
|
||||
// cppEthereumGenesisSpecBuiltin is the precompiled contract definition.
|
||||
type cppEthereumGenesisSpecBuiltin struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
StartingBlock hexutil.Uint64 `json:"startingBlock,omitempty"`
|
||||
Linear *cppEthereumGenesisSpecLinearPricing `json:"linear,omitempty"`
|
||||
}
|
||||
|
||||
type cppEthereumGenesisSpecLinearPricing struct {
|
||||
Base uint64 `json:"base"`
|
||||
Word uint64 `json:"word"`
|
||||
}
|
||||
|
||||
// newCppEthereumGenesisSpec converts a go-ethereum genesis block into a Parity specific
|
||||
// chain specification format.
|
||||
func newCppEthereumGenesisSpec(network string, genesis *core.Genesis) (*cppEthereumGenesisSpec, error) {
|
||||
// Only ethash is currently supported between go-ethereum and cpp-ethereum
|
||||
if genesis.Config.Ethash == nil {
|
||||
return nil, errors.New("unsupported consensus engine")
|
||||
}
|
||||
// Reconstruct the chain spec in Parity's format
|
||||
spec := &cppEthereumGenesisSpec{
|
||||
SealEngine: "Ethash",
|
||||
}
|
||||
spec.Params.AccountStartNonce = 0
|
||||
spec.Params.HomesteadForkBlock = (hexutil.Uint64)(genesis.Config.HomesteadBlock.Uint64())
|
||||
spec.Params.EIP150ForkBlock = (hexutil.Uint64)(genesis.Config.EIP150Block.Uint64())
|
||||
spec.Params.EIP158ForkBlock = (hexutil.Uint64)(genesis.Config.EIP158Block.Uint64())
|
||||
spec.Params.ByzantiumForkBlock = (hexutil.Uint64)(genesis.Config.ByzantiumBlock.Uint64())
|
||||
spec.Params.ConstantinopleForkBlock = (hexutil.Uint64)(math.MaxUint64)
|
||||
|
||||
spec.Params.NetworkID = (hexutil.Uint64)(genesis.Config.ChainId.Uint64())
|
||||
spec.Params.ChainID = (hexutil.Uint64)(genesis.Config.ChainId.Uint64())
|
||||
|
||||
spec.Params.MaximumExtraDataSize = (hexutil.Uint64)(params.MaximumExtraDataSize)
|
||||
spec.Params.MinGasLimit = (hexutil.Uint64)(params.MinGasLimit)
|
||||
spec.Params.MaxGasLimit = (hexutil.Uint64)(math.MaxUint64)
|
||||
spec.Params.MinimumDifficulty = (*hexutil.Big)(params.MinimumDifficulty)
|
||||
spec.Params.DifficultyBoundDivisor = (*hexutil.Big)(params.DifficultyBoundDivisor)
|
||||
spec.Params.GasLimitBoundDivisor = (hexutil.Uint64)(params.GasLimitBoundDivisor)
|
||||
spec.Params.DurationLimit = (*hexutil.Big)(params.DurationLimit)
|
||||
spec.Params.BlockReward = (*hexutil.Big)(ethash.FrontierBlockReward)
|
||||
|
||||
spec.Genesis.Nonce = (hexutil.Bytes)(make([]byte, 8))
|
||||
binary.LittleEndian.PutUint64(spec.Genesis.Nonce[:], genesis.Nonce)
|
||||
|
||||
spec.Genesis.MixHash = genesis.Mixhash
|
||||
spec.Genesis.Difficulty = (*hexutil.Big)(genesis.Difficulty)
|
||||
spec.Genesis.Author = genesis.Coinbase
|
||||
spec.Genesis.Timestamp = (hexutil.Uint64)(genesis.Timestamp)
|
||||
spec.Genesis.ParentHash = genesis.ParentHash
|
||||
spec.Genesis.ExtraData = (hexutil.Bytes)(genesis.ExtraData)
|
||||
spec.Genesis.GasLimit = (hexutil.Uint64)(genesis.GasLimit)
|
||||
|
||||
spec.Accounts = make(map[common.Address]*cppEthereumGenesisSpecAccount)
|
||||
for address, account := range genesis.Alloc {
|
||||
spec.Accounts[address] = &cppEthereumGenesisSpecAccount{
|
||||
Balance: (*hexutil.Big)(account.Balance),
|
||||
Nonce: account.Nonce,
|
||||
}
|
||||
}
|
||||
spec.Accounts[common.BytesToAddress([]byte{1})].Precompiled = &cppEthereumGenesisSpecBuiltin{
|
||||
Name: "ecrecover", Linear: &cppEthereumGenesisSpecLinearPricing{Base: 3000},
|
||||
}
|
||||
spec.Accounts[common.BytesToAddress([]byte{2})].Precompiled = &cppEthereumGenesisSpecBuiltin{
|
||||
Name: "sha256", Linear: &cppEthereumGenesisSpecLinearPricing{Base: 60, Word: 12},
|
||||
}
|
||||
spec.Accounts[common.BytesToAddress([]byte{3})].Precompiled = &cppEthereumGenesisSpecBuiltin{
|
||||
Name: "ripemd160", Linear: &cppEthereumGenesisSpecLinearPricing{Base: 600, Word: 120},
|
||||
}
|
||||
spec.Accounts[common.BytesToAddress([]byte{4})].Precompiled = &cppEthereumGenesisSpecBuiltin{
|
||||
Name: "identity", Linear: &cppEthereumGenesisSpecLinearPricing{Base: 15, Word: 3},
|
||||
}
|
||||
if genesis.Config.ByzantiumBlock != nil {
|
||||
spec.Accounts[common.BytesToAddress([]byte{5})].Precompiled = &cppEthereumGenesisSpecBuiltin{
|
||||
Name: "modexp", StartingBlock: (hexutil.Uint64)(genesis.Config.ByzantiumBlock.Uint64()),
|
||||
}
|
||||
spec.Accounts[common.BytesToAddress([]byte{6})].Precompiled = &cppEthereumGenesisSpecBuiltin{
|
||||
Name: "alt_bn128_G1_add", StartingBlock: (hexutil.Uint64)(genesis.Config.ByzantiumBlock.Uint64()), Linear: &cppEthereumGenesisSpecLinearPricing{Base: 500},
|
||||
}
|
||||
spec.Accounts[common.BytesToAddress([]byte{7})].Precompiled = &cppEthereumGenesisSpecBuiltin{
|
||||
Name: "alt_bn128_G1_mul", StartingBlock: (hexutil.Uint64)(genesis.Config.ByzantiumBlock.Uint64()), Linear: &cppEthereumGenesisSpecLinearPricing{Base: 40000},
|
||||
}
|
||||
spec.Accounts[common.BytesToAddress([]byte{8})].Precompiled = &cppEthereumGenesisSpecBuiltin{
|
||||
Name: "alt_bn128_pairing_product", StartingBlock: (hexutil.Uint64)(genesis.Config.ByzantiumBlock.Uint64()),
|
||||
}
|
||||
}
|
||||
return spec, nil
|
||||
}
|
||||
|
||||
// parityChainSpec is the chain specification format used by Parity.
|
||||
type parityChainSpec struct {
|
||||
Name string `json:"name"`
|
||||
Engine struct {
|
||||
Ethash struct {
|
||||
Params struct {
|
||||
MinimumDifficulty *hexutil.Big `json:"minimumDifficulty"`
|
||||
DifficultyBoundDivisor *hexutil.Big `json:"difficultyBoundDivisor"`
|
||||
GasLimitBoundDivisor hexutil.Uint64 `json:"gasLimitBoundDivisor"`
|
||||
DurationLimit *hexutil.Big `json:"durationLimit"`
|
||||
BlockReward *hexutil.Big `json:"blockReward"`
|
||||
HomesteadTransition uint64 `json:"homesteadTransition"`
|
||||
EIP150Transition uint64 `json:"eip150Transition"`
|
||||
EIP160Transition uint64 `json:"eip160Transition"`
|
||||
EIP161abcTransition uint64 `json:"eip161abcTransition"`
|
||||
EIP161dTransition uint64 `json:"eip161dTransition"`
|
||||
EIP649Reward *hexutil.Big `json:"eip649Reward"`
|
||||
EIP100bTransition uint64 `json:"eip100bTransition"`
|
||||
EIP649Transition uint64 `json:"eip649Transition"`
|
||||
} `json:"params"`
|
||||
} `json:"Ethash"`
|
||||
} `json:"engine"`
|
||||
|
||||
Params struct {
|
||||
MaximumExtraDataSize hexutil.Uint64 `json:"maximumExtraDataSize"`
|
||||
MinGasLimit hexutil.Uint64 `json:"minGasLimit"`
|
||||
NetworkID hexutil.Uint64 `json:"networkID"`
|
||||
MaxCodeSize uint64 `json:"maxCodeSize"`
|
||||
EIP155Transition uint64 `json:"eip155Transition"`
|
||||
EIP98Transition uint64 `json:"eip98Transition"`
|
||||
EIP86Transition uint64 `json:"eip86Transition"`
|
||||
EIP140Transition uint64 `json:"eip140Transition"`
|
||||
EIP211Transition uint64 `json:"eip211Transition"`
|
||||
EIP214Transition uint64 `json:"eip214Transition"`
|
||||
EIP658Transition uint64 `json:"eip658Transition"`
|
||||
} `json:"params"`
|
||||
|
||||
Genesis struct {
|
||||
Seal struct {
|
||||
Ethereum struct {
|
||||
Nonce hexutil.Bytes `json:"nonce"`
|
||||
MixHash hexutil.Bytes `json:"mixHash"`
|
||||
} `json:"ethereum"`
|
||||
} `json:"seal"`
|
||||
|
||||
Difficulty *hexutil.Big `json:"difficulty"`
|
||||
Author common.Address `json:"author"`
|
||||
Timestamp hexutil.Uint64 `json:"timestamp"`
|
||||
ParentHash common.Hash `json:"parentHash"`
|
||||
ExtraData hexutil.Bytes `json:"extraData"`
|
||||
GasLimit hexutil.Uint64 `json:"gasLimit"`
|
||||
} `json:"genesis"`
|
||||
|
||||
Nodes []string `json:"nodes"`
|
||||
Accounts map[common.Address]*parityChainSpecAccount `json:"accounts"`
|
||||
}
|
||||
|
||||
// parityChainSpecAccount is the prefunded genesis account and/or precompiled
|
||||
// contract definition.
|
||||
type parityChainSpecAccount struct {
|
||||
Balance *hexutil.Big `json:"balance"`
|
||||
Nonce uint64 `json:"nonce,omitempty"`
|
||||
Builtin *parityChainSpecBuiltin `json:"builtin,omitempty"`
|
||||
}
|
||||
|
||||
// parityChainSpecBuiltin is the precompiled contract definition.
|
||||
type parityChainSpecBuiltin struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
ActivateAt uint64 `json:"activate_at,omitempty"`
|
||||
Pricing *parityChainSpecPricing `json:"pricing,omitempty"`
|
||||
}
|
||||
|
||||
// parityChainSpecPricing represents the different pricing models that builtin
|
||||
// contracts might advertise using.
|
||||
type parityChainSpecPricing struct {
|
||||
Linear *parityChainSpecLinearPricing `json:"linear,omitempty"`
|
||||
ModExp *parityChainSpecModExpPricing `json:"modexp,omitempty"`
|
||||
AltBnPairing *parityChainSpecAltBnPairingPricing `json:"alt_bn128_pairing,omitempty"`
|
||||
}
|
||||
|
||||
type parityChainSpecLinearPricing struct {
|
||||
Base uint64 `json:"base"`
|
||||
Word uint64 `json:"word"`
|
||||
}
|
||||
|
||||
type parityChainSpecModExpPricing struct {
|
||||
Divisor uint64 `json:"divisor"`
|
||||
}
|
||||
|
||||
type parityChainSpecAltBnPairingPricing struct {
|
||||
Base uint64 `json:"base"`
|
||||
Pair uint64 `json:"pair"`
|
||||
}
|
||||
|
||||
// newParityChainSpec converts a go-ethereum genesis block into a Parity specific
|
||||
// chain specification format.
|
||||
func newParityChainSpec(network string, genesis *core.Genesis, bootnodes []string) (*parityChainSpec, error) {
|
||||
// Only ethash is currently supported between go-ethereum and Parity
|
||||
if genesis.Config.Ethash == nil {
|
||||
return nil, errors.New("unsupported consensus engine")
|
||||
}
|
||||
// Reconstruct the chain spec in Parity's format
|
||||
spec := &parityChainSpec{
|
||||
Name: network,
|
||||
Nodes: bootnodes,
|
||||
}
|
||||
spec.Engine.Ethash.Params.MinimumDifficulty = (*hexutil.Big)(params.MinimumDifficulty)
|
||||
spec.Engine.Ethash.Params.DifficultyBoundDivisor = (*hexutil.Big)(params.DifficultyBoundDivisor)
|
||||
spec.Engine.Ethash.Params.GasLimitBoundDivisor = (hexutil.Uint64)(params.GasLimitBoundDivisor)
|
||||
spec.Engine.Ethash.Params.DurationLimit = (*hexutil.Big)(params.DurationLimit)
|
||||
spec.Engine.Ethash.Params.BlockReward = (*hexutil.Big)(ethash.FrontierBlockReward)
|
||||
spec.Engine.Ethash.Params.HomesteadTransition = genesis.Config.HomesteadBlock.Uint64()
|
||||
spec.Engine.Ethash.Params.EIP150Transition = genesis.Config.EIP150Block.Uint64()
|
||||
spec.Engine.Ethash.Params.EIP160Transition = genesis.Config.EIP155Block.Uint64()
|
||||
spec.Engine.Ethash.Params.EIP161abcTransition = genesis.Config.EIP158Block.Uint64()
|
||||
spec.Engine.Ethash.Params.EIP161dTransition = genesis.Config.EIP158Block.Uint64()
|
||||
spec.Engine.Ethash.Params.EIP649Reward = (*hexutil.Big)(ethash.ByzantiumBlockReward)
|
||||
spec.Engine.Ethash.Params.EIP100bTransition = genesis.Config.ByzantiumBlock.Uint64()
|
||||
spec.Engine.Ethash.Params.EIP649Transition = genesis.Config.ByzantiumBlock.Uint64()
|
||||
|
||||
spec.Params.MaximumExtraDataSize = (hexutil.Uint64)(params.MaximumExtraDataSize)
|
||||
spec.Params.MinGasLimit = (hexutil.Uint64)(params.MinGasLimit)
|
||||
spec.Params.NetworkID = (hexutil.Uint64)(genesis.Config.ChainId.Uint64())
|
||||
spec.Params.MaxCodeSize = params.MaxCodeSize
|
||||
spec.Params.EIP155Transition = genesis.Config.EIP155Block.Uint64()
|
||||
spec.Params.EIP98Transition = math.MaxUint64
|
||||
spec.Params.EIP86Transition = math.MaxUint64
|
||||
spec.Params.EIP140Transition = genesis.Config.ByzantiumBlock.Uint64()
|
||||
spec.Params.EIP211Transition = genesis.Config.ByzantiumBlock.Uint64()
|
||||
spec.Params.EIP214Transition = genesis.Config.ByzantiumBlock.Uint64()
|
||||
spec.Params.EIP658Transition = genesis.Config.ByzantiumBlock.Uint64()
|
||||
|
||||
spec.Genesis.Seal.Ethereum.Nonce = (hexutil.Bytes)(make([]byte, 8))
|
||||
binary.LittleEndian.PutUint64(spec.Genesis.Seal.Ethereum.Nonce[:], genesis.Nonce)
|
||||
|
||||
spec.Genesis.Seal.Ethereum.MixHash = (hexutil.Bytes)(genesis.Mixhash[:])
|
||||
spec.Genesis.Difficulty = (*hexutil.Big)(genesis.Difficulty)
|
||||
spec.Genesis.Author = genesis.Coinbase
|
||||
spec.Genesis.Timestamp = (hexutil.Uint64)(genesis.Timestamp)
|
||||
spec.Genesis.ParentHash = genesis.ParentHash
|
||||
spec.Genesis.ExtraData = (hexutil.Bytes)(genesis.ExtraData)
|
||||
spec.Genesis.GasLimit = (hexutil.Uint64)(genesis.GasLimit)
|
||||
|
||||
spec.Accounts = make(map[common.Address]*parityChainSpecAccount)
|
||||
for address, account := range genesis.Alloc {
|
||||
spec.Accounts[address] = &parityChainSpecAccount{
|
||||
Balance: (*hexutil.Big)(account.Balance),
|
||||
Nonce: account.Nonce,
|
||||
}
|
||||
}
|
||||
spec.Accounts[common.BytesToAddress([]byte{1})].Builtin = &parityChainSpecBuiltin{
|
||||
Name: "ecrecover", Pricing: &parityChainSpecPricing{Linear: &parityChainSpecLinearPricing{Base: 3000}},
|
||||
}
|
||||
spec.Accounts[common.BytesToAddress([]byte{2})].Builtin = &parityChainSpecBuiltin{
|
||||
Name: "sha256", Pricing: &parityChainSpecPricing{Linear: &parityChainSpecLinearPricing{Base: 60, Word: 12}},
|
||||
}
|
||||
spec.Accounts[common.BytesToAddress([]byte{3})].Builtin = &parityChainSpecBuiltin{
|
||||
Name: "ripemd160", Pricing: &parityChainSpecPricing{Linear: &parityChainSpecLinearPricing{Base: 600, Word: 120}},
|
||||
}
|
||||
spec.Accounts[common.BytesToAddress([]byte{4})].Builtin = &parityChainSpecBuiltin{
|
||||
Name: "identity", Pricing: &parityChainSpecPricing{Linear: &parityChainSpecLinearPricing{Base: 15, Word: 3}},
|
||||
}
|
||||
if genesis.Config.ByzantiumBlock != nil {
|
||||
spec.Accounts[common.BytesToAddress([]byte{5})].Builtin = &parityChainSpecBuiltin{
|
||||
Name: "modexp", ActivateAt: genesis.Config.ByzantiumBlock.Uint64(), Pricing: &parityChainSpecPricing{ModExp: &parityChainSpecModExpPricing{Divisor: 20}},
|
||||
}
|
||||
spec.Accounts[common.BytesToAddress([]byte{6})].Builtin = &parityChainSpecBuiltin{
|
||||
Name: "alt_bn128_add", ActivateAt: genesis.Config.ByzantiumBlock.Uint64(), Pricing: &parityChainSpecPricing{Linear: &parityChainSpecLinearPricing{Base: 500}},
|
||||
}
|
||||
spec.Accounts[common.BytesToAddress([]byte{7})].Builtin = &parityChainSpecBuiltin{
|
||||
Name: "alt_bn128_mul", ActivateAt: genesis.Config.ByzantiumBlock.Uint64(), Pricing: &parityChainSpecPricing{Linear: &parityChainSpecLinearPricing{Base: 40000}},
|
||||
}
|
||||
spec.Accounts[common.BytesToAddress([]byte{8})].Builtin = &parityChainSpecBuiltin{
|
||||
Name: "alt_bn128_pairing", ActivateAt: genesis.Config.ByzantiumBlock.Uint64(), Pricing: &parityChainSpecPricing{AltBnPairing: &parityChainSpecAltBnPairingPricing{Base: 100000, Pair: 80000}},
|
||||
}
|
||||
}
|
||||
return spec, nil
|
||||
}
|
||||
|
||||
// pyEthereumGenesisSpec represents the genesis specification format used by the
|
||||
// Python Ethereum implementation.
|
||||
type pyEthereumGenesisSpec struct {
|
||||
Nonce hexutil.Bytes `json:"nonce"`
|
||||
Timestamp hexutil.Uint64 `json:"timestamp"`
|
||||
ExtraData hexutil.Bytes `json:"extraData"`
|
||||
GasLimit hexutil.Uint64 `json:"gasLimit"`
|
||||
Difficulty *hexutil.Big `json:"difficulty"`
|
||||
Mixhash common.Hash `json:"mixhash"`
|
||||
Coinbase common.Address `json:"coinbase"`
|
||||
Alloc core.GenesisAlloc `json:"alloc"`
|
||||
ParentHash common.Hash `json:"parentHash"`
|
||||
}
|
||||
|
||||
// newPyEthereumGenesisSpec converts a go-ethereum genesis block into a Parity specific
|
||||
// chain specification format.
|
||||
func newPyEthereumGenesisSpec(network string, genesis *core.Genesis) (*pyEthereumGenesisSpec, error) {
|
||||
// Only ethash is currently supported between go-ethereum and pyethereum
|
||||
if genesis.Config.Ethash == nil {
|
||||
return nil, errors.New("unsupported consensus engine")
|
||||
}
|
||||
spec := &pyEthereumGenesisSpec{
|
||||
Timestamp: (hexutil.Uint64)(genesis.Timestamp),
|
||||
ExtraData: genesis.ExtraData,
|
||||
GasLimit: (hexutil.Uint64)(genesis.GasLimit),
|
||||
Difficulty: (*hexutil.Big)(genesis.Difficulty),
|
||||
Mixhash: genesis.Mixhash,
|
||||
Coinbase: genesis.Coinbase,
|
||||
Alloc: genesis.Alloc,
|
||||
ParentHash: genesis.ParentHash,
|
||||
}
|
||||
spec.Nonce = (hexutil.Bytes)(make([]byte, 8))
|
||||
binary.LittleEndian.PutUint64(spec.Nonce[:], genesis.Nonce)
|
||||
|
||||
return spec, nil
|
||||
}
|
||||
152
cmd/puppeth/module.go
Normal file
152
cmd/puppeth/module.go
Normal file
@@ -0,0 +1,152 @@
|
||||
// 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 (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrServiceUnknown is returned when a service container doesn't exist.
|
||||
ErrServiceUnknown = errors.New("service unknown")
|
||||
|
||||
// ErrServiceOffline is returned when a service container exists, but it is not
|
||||
// running.
|
||||
ErrServiceOffline = errors.New("service offline")
|
||||
|
||||
// ErrServiceUnreachable is returned when a service container is running, but
|
||||
// seems to not respond to communication attempts.
|
||||
ErrServiceUnreachable = errors.New("service unreachable")
|
||||
|
||||
// ErrNotExposed is returned if a web-service doesn't have an exposed port, nor
|
||||
// a reverse-proxy in front of it to forward requests.
|
||||
ErrNotExposed = errors.New("service not exposed, nor proxied")
|
||||
)
|
||||
|
||||
// containerInfos is a heavily reduced version of the huge inspection dataset
|
||||
// returned from docker inspect, parsed into a form easily usable by puppeth.
|
||||
type containerInfos struct {
|
||||
running bool // Flag whether the container is running currently
|
||||
envvars map[string]string // Collection of environmental variables set on the container
|
||||
portmap map[string]int // Port mapping from internal port/proto combos to host binds
|
||||
volumes map[string]string // Volume mount points from container to host directories
|
||||
}
|
||||
|
||||
// inspectContainer runs docker inspect against a running container
|
||||
func inspectContainer(client *sshClient, container string) (*containerInfos, error) {
|
||||
// Check whether there's a container running for the service
|
||||
out, err := client.Run(fmt.Sprintf("docker inspect %s", container))
|
||||
if err != nil {
|
||||
return nil, ErrServiceUnknown
|
||||
}
|
||||
// If yes, extract various configuration options
|
||||
type inspection struct {
|
||||
State struct {
|
||||
Running bool
|
||||
}
|
||||
Mounts []struct {
|
||||
Source string
|
||||
Destination string
|
||||
}
|
||||
Config struct {
|
||||
Env []string
|
||||
}
|
||||
HostConfig struct {
|
||||
PortBindings map[string][]map[string]string
|
||||
}
|
||||
}
|
||||
var inspects []inspection
|
||||
if err = json.Unmarshal(out, &inspects); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
inspect := inspects[0]
|
||||
|
||||
// Infos retrieved, parse the above into something meaningful
|
||||
infos := &containerInfos{
|
||||
running: inspect.State.Running,
|
||||
envvars: make(map[string]string),
|
||||
portmap: make(map[string]int),
|
||||
volumes: make(map[string]string),
|
||||
}
|
||||
for _, envvar := range inspect.Config.Env {
|
||||
if parts := strings.Split(envvar, "="); len(parts) == 2 {
|
||||
infos.envvars[parts[0]] = parts[1]
|
||||
}
|
||||
}
|
||||
for portname, details := range inspect.HostConfig.PortBindings {
|
||||
if len(details) > 0 {
|
||||
port, _ := strconv.Atoi(details[0]["HostPort"])
|
||||
infos.portmap[portname] = port
|
||||
}
|
||||
}
|
||||
for _, mount := range inspect.Mounts {
|
||||
infos.volumes[mount.Destination] = mount.Source
|
||||
}
|
||||
return infos, err
|
||||
}
|
||||
|
||||
// tearDown connects to a remote machine via SSH and terminates docker containers
|
||||
// running with the specified name in the specified network.
|
||||
func tearDown(client *sshClient, network string, service string, purge bool) ([]byte, error) {
|
||||
// Tear down the running (or paused) container
|
||||
out, err := client.Run(fmt.Sprintf("docker rm -f %s_%s_1", network, service))
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
// If requested, purge the associated docker image too
|
||||
if purge {
|
||||
return client.Run(fmt.Sprintf("docker rmi %s/%s", network, service))
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// resolve retrieves the hostname a service is running on either by returning the
|
||||
// actual server name and port, or preferably an nginx virtual host if available.
|
||||
func resolve(client *sshClient, network string, service string, port int) (string, error) {
|
||||
// Inspect the service to get various configurations from it
|
||||
infos, err := inspectContainer(client, fmt.Sprintf("%s_%s_1", network, service))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !infos.running {
|
||||
return "", ErrServiceOffline
|
||||
}
|
||||
// Container online, extract any environmental variables
|
||||
if vhost := infos.envvars["VIRTUAL_HOST"]; vhost != "" {
|
||||
return vhost, nil
|
||||
}
|
||||
return fmt.Sprintf("%s:%d", client.server, port), nil
|
||||
}
|
||||
|
||||
// checkPort tries to connect to a remote host on a given
|
||||
func checkPort(host string, port int) error {
|
||||
log.Trace("Verifying remote TCP connectivity", "server", host, "port", port)
|
||||
conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", host, port), time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
conn.Close()
|
||||
return nil
|
||||
}
|
||||
752
cmd/puppeth/module_dashboard.go
Normal file
752
cmd/puppeth/module_dashboard.go
Normal file
File diff suppressed because one or more lines are too long
175
cmd/puppeth/module_ethstats.go
Normal file
175
cmd/puppeth/module_ethstats.go
Normal file
@@ -0,0 +1,175 @@
|
||||
// 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"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
)
|
||||
|
||||
// ethstatsDockerfile is the Dockerfile required to build an ethstats backend
|
||||
// and associated monitoring site.
|
||||
var ethstatsDockerfile = `
|
||||
FROM puppeth/ethstats:latest
|
||||
|
||||
RUN echo 'module.exports = {trusted: [{{.Trusted}}], banned: [{{.Banned}}], reserved: ["yournode"]};' > lib/utils/config.js
|
||||
`
|
||||
|
||||
// ethstatsComposefile is the docker-compose.yml file required to deploy and
|
||||
// maintain an ethstats monitoring site.
|
||||
var ethstatsComposefile = `
|
||||
version: '2'
|
||||
services:
|
||||
ethstats:
|
||||
build: .
|
||||
image: {{.Network}}/ethstats{{if not .VHost}}
|
||||
ports:
|
||||
- "{{.Port}}:3000"{{end}}
|
||||
environment:
|
||||
- WS_SECRET={{.Secret}}{{if .VHost}}
|
||||
- VIRTUAL_HOST={{.VHost}}{{end}}{{if .Banned}}
|
||||
- BANNED={{.Banned}}{{end}}
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "1m"
|
||||
max-file: "10"
|
||||
restart: always
|
||||
`
|
||||
|
||||
// deployEthstats deploys a new ethstats 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 deployEthstats(client *sshClient, network string, port int, secret string, vhost string, trusted []string, banned []string, nocache bool) ([]byte, error) {
|
||||
// Generate the content to upload to the server
|
||||
workdir := fmt.Sprintf("%d", rand.Int63())
|
||||
files := make(map[string][]byte)
|
||||
|
||||
trustedLabels := make([]string, len(trusted))
|
||||
for i, address := range trusted {
|
||||
trustedLabels[i] = fmt.Sprintf("\"%s\"", address)
|
||||
}
|
||||
bannedLabels := make([]string, len(banned))
|
||||
for i, address := range banned {
|
||||
bannedLabels[i] = fmt.Sprintf("\"%s\"", address)
|
||||
}
|
||||
|
||||
dockerfile := new(bytes.Buffer)
|
||||
template.Must(template.New("").Parse(ethstatsDockerfile)).Execute(dockerfile, map[string]interface{}{
|
||||
"Trusted": strings.Join(trustedLabels, ", "),
|
||||
"Banned": strings.Join(bannedLabels, ", "),
|
||||
})
|
||||
files[filepath.Join(workdir, "Dockerfile")] = dockerfile.Bytes()
|
||||
|
||||
composefile := new(bytes.Buffer)
|
||||
template.Must(template.New("").Parse(ethstatsComposefile)).Execute(composefile, map[string]interface{}{
|
||||
"Network": network,
|
||||
"Port": port,
|
||||
"Secret": secret,
|
||||
"VHost": vhost,
|
||||
"Banned": strings.Join(banned, ","),
|
||||
})
|
||||
files[filepath.Join(workdir, "docker-compose.yaml")] = composefile.Bytes()
|
||||
|
||||
// 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 ethstats 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", workdir, network, network))
|
||||
}
|
||||
return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s up -d --build --force-recreate", workdir, network))
|
||||
}
|
||||
|
||||
// ethstatsInfos is returned from an ethstats status check to allow reporting
|
||||
// various configuration parameters.
|
||||
type ethstatsInfos struct {
|
||||
host string
|
||||
port int
|
||||
secret string
|
||||
config string
|
||||
banned []string
|
||||
}
|
||||
|
||||
// Report converts the typed struct into a plain string->string map, containing
|
||||
// most - but not all - fields for reporting to the user.
|
||||
func (info *ethstatsInfos) Report() map[string]string {
|
||||
return map[string]string{
|
||||
"Website address": info.host,
|
||||
"Website listener port": strconv.Itoa(info.port),
|
||||
"Login secret": info.secret,
|
||||
"Banned addresses": fmt.Sprintf("%v", info.banned),
|
||||
}
|
||||
}
|
||||
|
||||
// checkEthstats does a health-check against an ethstats server to verify whether
|
||||
// it's running, and if yes, gathering a collection of useful infos about it.
|
||||
func checkEthstats(client *sshClient, network string) (*ethstatsInfos, error) {
|
||||
// Inspect a possible ethstats container on the host
|
||||
infos, err := inspectContainer(client, fmt.Sprintf("%s_ethstats_1", network))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !infos.running {
|
||||
return nil, ErrServiceOffline
|
||||
}
|
||||
// Resolve the port from the host, or the reverse proxy
|
||||
port := infos.portmap["3000/tcp"]
|
||||
if port == 0 {
|
||||
if proxy, _ := checkNginx(client, network); proxy != nil {
|
||||
port = proxy.port
|
||||
}
|
||||
}
|
||||
if port == 0 {
|
||||
return nil, ErrNotExposed
|
||||
}
|
||||
// Resolve the host from the reverse-proxy and configure the connection string
|
||||
host := infos.envvars["VIRTUAL_HOST"]
|
||||
if host == "" {
|
||||
host = client.server
|
||||
}
|
||||
secret := infos.envvars["WS_SECRET"]
|
||||
config := fmt.Sprintf("%s@%s", secret, host)
|
||||
if port != 80 && port != 443 {
|
||||
config += fmt.Sprintf(":%d", port)
|
||||
}
|
||||
// Retrieve the IP blacklist
|
||||
banned := strings.Split(infos.envvars["BANNED"], ",")
|
||||
|
||||
// Run a sanity check to see if the port is reachable
|
||||
if err = checkPort(host, port); err != nil {
|
||||
log.Warn("Ethstats service seems unreachable", "server", host, "port", port, "err", err)
|
||||
}
|
||||
// Container available, assemble and return the useful infos
|
||||
return ðstatsInfos{
|
||||
host: host,
|
||||
port: port,
|
||||
secret: secret,
|
||||
config: config,
|
||||
banned: banned,
|
||||
}, nil
|
||||
}
|
||||
211
cmd/puppeth/module_explorer.go
Normal file
211
cmd/puppeth/module_explorer.go
Normal file
@@ -0,0 +1,211 @@
|
||||
// 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"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"math/rand"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
)
|
||||
|
||||
// explorerDockerfile is the Dockerfile required to run a block explorer.
|
||||
var explorerDockerfile = `
|
||||
FROM puppeth/explorer:latest
|
||||
|
||||
ADD ethstats.json /ethstats.json
|
||||
ADD chain.json /chain.json
|
||||
|
||||
RUN \
|
||||
echo '(cd ../eth-net-intelligence-api && pm2 start /ethstats.json)' > explorer.sh && \
|
||||
echo '(cd ../etherchain-light && npm start &)' >> explorer.sh && \
|
||||
echo '/parity/parity --chain=/chain.json --port={{.NodePort}} --tracing=on --fat-db=on --pruning=archive' >> explorer.sh
|
||||
|
||||
ENTRYPOINT ["/bin/sh", "explorer.sh"]
|
||||
`
|
||||
|
||||
// explorerEthstats is the configuration file for the ethstats javascript client.
|
||||
var explorerEthstats = `[
|
||||
{
|
||||
"name" : "node-app",
|
||||
"script" : "app.js",
|
||||
"log_date_format" : "YYYY-MM-DD HH:mm Z",
|
||||
"merge_logs" : false,
|
||||
"watch" : false,
|
||||
"max_restarts" : 10,
|
||||
"exec_interpreter" : "node",
|
||||
"exec_mode" : "fork_mode",
|
||||
"env":
|
||||
{
|
||||
"NODE_ENV" : "production",
|
||||
"RPC_HOST" : "localhost",
|
||||
"RPC_PORT" : "8545",
|
||||
"LISTENING_PORT" : "{{.Port}}",
|
||||
"INSTANCE_NAME" : "{{.Name}}",
|
||||
"CONTACT_DETAILS" : "",
|
||||
"WS_SERVER" : "{{.Host}}",
|
||||
"WS_SECRET" : "{{.Secret}}",
|
||||
"VERBOSITY" : 2
|
||||
}
|
||||
}
|
||||
]`
|
||||
|
||||
// explorerComposefile is the docker-compose.yml file required to deploy and
|
||||
// maintain a block explorer.
|
||||
var explorerComposefile = `
|
||||
version: '2'
|
||||
services:
|
||||
explorer:
|
||||
build: .
|
||||
image: {{.Network}}/explorer
|
||||
ports:
|
||||
- "{{.NodePort}}:{{.NodePort}}"
|
||||
- "{{.NodePort}}:{{.NodePort}}/udp"{{if not .VHost}}
|
||||
- "{{.WebPort}}:3000"{{end}}
|
||||
volumes:
|
||||
- {{.Datadir}}:/root/.local/share/io.parity.ethereum
|
||||
environment:
|
||||
- NODE_PORT={{.NodePort}}/tcp
|
||||
- STATS={{.Ethstats}}{{if .VHost}}
|
||||
- VIRTUAL_HOST={{.VHost}}
|
||||
- VIRTUAL_PORT=3000{{end}}
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "1m"
|
||||
max-file: "10"
|
||||
restart: always
|
||||
`
|
||||
|
||||
// deployExplorer deploys a new block explorer 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 deployExplorer(client *sshClient, network string, chainspec []byte, config *explorerInfos, nocache bool) ([]byte, error) {
|
||||
// Generate the content to upload to the server
|
||||
workdir := fmt.Sprintf("%d", rand.Int63())
|
||||
files := make(map[string][]byte)
|
||||
|
||||
dockerfile := new(bytes.Buffer)
|
||||
template.Must(template.New("").Parse(explorerDockerfile)).Execute(dockerfile, map[string]interface{}{
|
||||
"NodePort": config.nodePort,
|
||||
})
|
||||
files[filepath.Join(workdir, "Dockerfile")] = dockerfile.Bytes()
|
||||
|
||||
ethstats := new(bytes.Buffer)
|
||||
template.Must(template.New("").Parse(explorerEthstats)).Execute(ethstats, map[string]interface{}{
|
||||
"Port": config.nodePort,
|
||||
"Name": config.ethstats[:strings.Index(config.ethstats, ":")],
|
||||
"Secret": config.ethstats[strings.Index(config.ethstats, ":")+1 : strings.Index(config.ethstats, "@")],
|
||||
"Host": config.ethstats[strings.Index(config.ethstats, "@")+1:],
|
||||
})
|
||||
files[filepath.Join(workdir, "ethstats.json")] = ethstats.Bytes()
|
||||
|
||||
composefile := new(bytes.Buffer)
|
||||
template.Must(template.New("").Parse(explorerComposefile)).Execute(composefile, map[string]interface{}{
|
||||
"Datadir": config.datadir,
|
||||
"Network": network,
|
||||
"NodePort": config.nodePort,
|
||||
"VHost": config.webHost,
|
||||
"WebPort": config.webPort,
|
||||
"Ethstats": config.ethstats[:strings.Index(config.ethstats, ":")],
|
||||
})
|
||||
files[filepath.Join(workdir, "docker-compose.yaml")] = composefile.Bytes()
|
||||
|
||||
files[filepath.Join(workdir, "chain.json")] = chainspec
|
||||
|
||||
// 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", workdir, network, network))
|
||||
}
|
||||
return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s up -d --build --force-recreate", workdir, network))
|
||||
}
|
||||
|
||||
// explorerInfos is returned from a block explorer status check to allow reporting
|
||||
// various configuration parameters.
|
||||
type explorerInfos struct {
|
||||
datadir string
|
||||
ethstats string
|
||||
nodePort int
|
||||
webHost string
|
||||
webPort int
|
||||
}
|
||||
|
||||
// Report converts the typed struct into a plain string->string map, containing
|
||||
// most - but not all - fields for reporting to the user.
|
||||
func (info *explorerInfos) Report() map[string]string {
|
||||
report := map[string]string{
|
||||
"Data directory": info.datadir,
|
||||
"Node listener port ": strconv.Itoa(info.nodePort),
|
||||
"Ethstats username": info.ethstats,
|
||||
"Website address ": info.webHost,
|
||||
"Website listener port ": strconv.Itoa(info.webPort),
|
||||
}
|
||||
return report
|
||||
}
|
||||
|
||||
// checkExplorer does a health-check against an block explorer server to verify
|
||||
// whether it's running, and if yes, whether it's responsive.
|
||||
func checkExplorer(client *sshClient, network string) (*explorerInfos, error) {
|
||||
// Inspect a possible block explorer container on the host
|
||||
infos, err := inspectContainer(client, fmt.Sprintf("%s_explorer_1", network))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !infos.running {
|
||||
return nil, ErrServiceOffline
|
||||
}
|
||||
// Resolve the port from the host, or the reverse proxy
|
||||
webPort := infos.portmap["3000/tcp"]
|
||||
if webPort == 0 {
|
||||
if proxy, _ := checkNginx(client, network); proxy != nil {
|
||||
webPort = proxy.port
|
||||
}
|
||||
}
|
||||
if webPort == 0 {
|
||||
return nil, ErrNotExposed
|
||||
}
|
||||
// Resolve the host from the reverse-proxy and the config values
|
||||
host := infos.envvars["VIRTUAL_HOST"]
|
||||
if host == "" {
|
||||
host = client.server
|
||||
}
|
||||
// Run a sanity check to see if the devp2p is reachable
|
||||
nodePort := infos.portmap[infos.envvars["NODE_PORT"]]
|
||||
if err = checkPort(client.server, nodePort); err != nil {
|
||||
log.Warn(fmt.Sprintf("Explorer devp2p port seems unreachable"), "server", client.server, "port", nodePort, "err", err)
|
||||
}
|
||||
// Assemble and return the useful infos
|
||||
stats := &explorerInfos{
|
||||
datadir: infos.volumes["/root/.local/share/io.parity.ethereum"],
|
||||
nodePort: nodePort,
|
||||
webHost: host,
|
||||
webPort: webPort,
|
||||
ethstats: infos.envvars["STATS"],
|
||||
}
|
||||
return stats, nil
|
||||
}
|
||||
245
cmd/puppeth/module_faucet.go
Normal file
245
cmd/puppeth/module_faucet.go
Normal file
@@ -0,0 +1,245 @@
|
||||
// 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"
|
||||
"html/template"
|
||||
"math/rand"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
)
|
||||
|
||||
// faucetDockerfile is the Dockerfile required to build an faucet container to
|
||||
// grant crypto tokens based on GitHub authentications.
|
||||
var faucetDockerfile = `
|
||||
FROM ethereum/client-go:alltools-latest
|
||||
|
||||
ADD genesis.json /genesis.json
|
||||
ADD account.json /account.json
|
||||
ADD account.pass /account.pass
|
||||
|
||||
EXPOSE 8080 30303 30303/udp
|
||||
|
||||
ENTRYPOINT [ \
|
||||
"faucet", "--genesis", "/genesis.json", "--network", "{{.NetworkID}}", "--bootnodes", "{{.Bootnodes}}", "--ethstats", "{{.Ethstats}}", "--ethport", "{{.EthPort}}", \
|
||||
"--faucet.name", "{{.FaucetName}}", "--faucet.amount", "{{.FaucetAmount}}", "--faucet.minutes", "{{.FaucetMinutes}}", "--faucet.tiers", "{{.FaucetTiers}}", \
|
||||
"--account.json", "/account.json", "--account.pass", "/account.pass" \
|
||||
{{if .CaptchaToken}}, "--captcha.token", "{{.CaptchaToken}}", "--captcha.secret", "{{.CaptchaSecret}}"{{end}}{{if .NoAuth}}, "--noauth"{{end}} \
|
||||
]`
|
||||
|
||||
// faucetComposefile is the docker-compose.yml file required to deploy and maintain
|
||||
// a crypto faucet.
|
||||
var faucetComposefile = `
|
||||
version: '2'
|
||||
services:
|
||||
faucet:
|
||||
build: .
|
||||
image: {{.Network}}/faucet
|
||||
ports:
|
||||
- "{{.EthPort}}:{{.EthPort}}"{{if not .VHost}}
|
||||
- "{{.ApiPort}}:8080"{{end}}
|
||||
volumes:
|
||||
- {{.Datadir}}:/root/.faucet
|
||||
environment:
|
||||
- ETH_PORT={{.EthPort}}
|
||||
- ETH_NAME={{.EthName}}
|
||||
- FAUCET_AMOUNT={{.FaucetAmount}}
|
||||
- FAUCET_MINUTES={{.FaucetMinutes}}
|
||||
- FAUCET_TIERS={{.FaucetTiers}}
|
||||
- CAPTCHA_TOKEN={{.CaptchaToken}}
|
||||
- CAPTCHA_SECRET={{.CaptchaSecret}}
|
||||
- NO_AUTH={{.NoAuth}}{{if .VHost}}
|
||||
- VIRTUAL_HOST={{.VHost}}
|
||||
- VIRTUAL_PORT=8080{{end}}
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "1m"
|
||||
max-file: "10"
|
||||
restart: always
|
||||
`
|
||||
|
||||
// deployFaucet deploys a new faucet 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 deployFaucet(client *sshClient, network string, bootnodes []string, config *faucetInfos, nocache bool) ([]byte, error) {
|
||||
// Generate the content to upload to the server
|
||||
workdir := fmt.Sprintf("%d", rand.Int63())
|
||||
files := make(map[string][]byte)
|
||||
|
||||
dockerfile := new(bytes.Buffer)
|
||||
template.Must(template.New("").Parse(faucetDockerfile)).Execute(dockerfile, map[string]interface{}{
|
||||
"NetworkID": config.node.network,
|
||||
"Bootnodes": strings.Join(bootnodes, ","),
|
||||
"Ethstats": config.node.ethstats,
|
||||
"EthPort": config.node.portFull,
|
||||
"CaptchaToken": config.captchaToken,
|
||||
"CaptchaSecret": config.captchaSecret,
|
||||
"FaucetName": strings.Title(network),
|
||||
"FaucetAmount": config.amount,
|
||||
"FaucetMinutes": config.minutes,
|
||||
"FaucetTiers": config.tiers,
|
||||
"NoAuth": config.noauth,
|
||||
})
|
||||
files[filepath.Join(workdir, "Dockerfile")] = dockerfile.Bytes()
|
||||
|
||||
composefile := new(bytes.Buffer)
|
||||
template.Must(template.New("").Parse(faucetComposefile)).Execute(composefile, map[string]interface{}{
|
||||
"Network": network,
|
||||
"Datadir": config.node.datadir,
|
||||
"VHost": config.host,
|
||||
"ApiPort": config.port,
|
||||
"EthPort": config.node.portFull,
|
||||
"EthName": config.node.ethstats[:strings.Index(config.node.ethstats, ":")],
|
||||
"CaptchaToken": config.captchaToken,
|
||||
"CaptchaSecret": config.captchaSecret,
|
||||
"FaucetAmount": config.amount,
|
||||
"FaucetMinutes": config.minutes,
|
||||
"FaucetTiers": config.tiers,
|
||||
"NoAuth": config.noauth,
|
||||
})
|
||||
files[filepath.Join(workdir, "docker-compose.yaml")] = composefile.Bytes()
|
||||
|
||||
files[filepath.Join(workdir, "genesis.json")] = config.node.genesis
|
||||
files[filepath.Join(workdir, "account.json")] = []byte(config.node.keyJSON)
|
||||
files[filepath.Join(workdir, "account.pass")] = []byte(config.node.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 faucet 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", workdir, network, network))
|
||||
}
|
||||
return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s up -d --build --force-recreate", workdir, network))
|
||||
}
|
||||
|
||||
// faucetInfos is returned from an faucet status check to allow reporting various
|
||||
// configuration parameters.
|
||||
type faucetInfos struct {
|
||||
node *nodeInfos
|
||||
host string
|
||||
port int
|
||||
amount int
|
||||
minutes int
|
||||
tiers int
|
||||
noauth bool
|
||||
captchaToken string
|
||||
captchaSecret string
|
||||
}
|
||||
|
||||
// Report converts the typed struct into a plain string->string map, containing
|
||||
// most - but not all - fields for reporting to the user.
|
||||
func (info *faucetInfos) Report() map[string]string {
|
||||
report := map[string]string{
|
||||
"Website address": info.host,
|
||||
"Website listener port": strconv.Itoa(info.port),
|
||||
"Ethereum listener port": strconv.Itoa(info.node.portFull),
|
||||
"Funding amount (base tier)": fmt.Sprintf("%d Ethers", info.amount),
|
||||
"Funding cooldown (base tier)": fmt.Sprintf("%d mins", info.minutes),
|
||||
"Funding tiers": strconv.Itoa(info.tiers),
|
||||
"Captha protection": fmt.Sprintf("%v", info.captchaToken != ""),
|
||||
"Ethstats username": info.node.ethstats,
|
||||
}
|
||||
if info.noauth {
|
||||
report["Debug mode (no auth)"] = "enabled"
|
||||
}
|
||||
if info.node.keyJSON != "" {
|
||||
var key struct {
|
||||
Address string `json:"address"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(info.node.keyJSON), &key); err == nil {
|
||||
report["Funding account"] = common.HexToAddress(key.Address).Hex()
|
||||
} else {
|
||||
log.Error("Failed to retrieve signer address", "err", err)
|
||||
}
|
||||
}
|
||||
return report
|
||||
}
|
||||
|
||||
// checkFaucet does a health-check against an faucet server to verify whether
|
||||
// it's running, and if yes, gathering a collection of useful infos about it.
|
||||
func checkFaucet(client *sshClient, network string) (*faucetInfos, error) {
|
||||
// Inspect a possible faucet container on the host
|
||||
infos, err := inspectContainer(client, fmt.Sprintf("%s_faucet_1", network))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !infos.running {
|
||||
return nil, ErrServiceOffline
|
||||
}
|
||||
// Resolve the port from the host, or the reverse proxy
|
||||
port := infos.portmap["8080/tcp"]
|
||||
if port == 0 {
|
||||
if proxy, _ := checkNginx(client, network); proxy != nil {
|
||||
port = proxy.port
|
||||
}
|
||||
}
|
||||
if port == 0 {
|
||||
return nil, ErrNotExposed
|
||||
}
|
||||
// Resolve the host from the reverse-proxy and the config values
|
||||
host := infos.envvars["VIRTUAL_HOST"]
|
||||
if host == "" {
|
||||
host = client.server
|
||||
}
|
||||
amount, _ := strconv.Atoi(infos.envvars["FAUCET_AMOUNT"])
|
||||
minutes, _ := strconv.Atoi(infos.envvars["FAUCET_MINUTES"])
|
||||
tiers, _ := strconv.Atoi(infos.envvars["FAUCET_TIERS"])
|
||||
|
||||
// Retrieve the funding account informations
|
||||
var out []byte
|
||||
keyJSON, keyPass := "", ""
|
||||
if out, err = client.Run(fmt.Sprintf("docker exec %s_faucet_1 cat /account.json", network)); err == nil {
|
||||
keyJSON = string(bytes.TrimSpace(out))
|
||||
}
|
||||
if out, err = client.Run(fmt.Sprintf("docker exec %s_faucet_1 cat /account.pass", network)); err == nil {
|
||||
keyPass = string(bytes.TrimSpace(out))
|
||||
}
|
||||
// Run a sanity check to see if the port is reachable
|
||||
if err = checkPort(host, port); err != nil {
|
||||
log.Warn("Faucet service seems unreachable", "server", host, "port", port, "err", err)
|
||||
}
|
||||
// Container available, assemble and return the useful infos
|
||||
return &faucetInfos{
|
||||
node: &nodeInfos{
|
||||
datadir: infos.volumes["/root/.faucet"],
|
||||
portFull: infos.portmap[infos.envvars["ETH_PORT"]+"/tcp"],
|
||||
ethstats: infos.envvars["ETH_NAME"],
|
||||
keyJSON: keyJSON,
|
||||
keyPass: keyPass,
|
||||
},
|
||||
host: host,
|
||||
port: port,
|
||||
amount: amount,
|
||||
minutes: minutes,
|
||||
tiers: tiers,
|
||||
captchaToken: infos.envvars["CAPTCHA_TOKEN"],
|
||||
captchaSecret: infos.envvars["CAPTCHA_SECRET"],
|
||||
noauth: infos.envvars["NO_AUTH"] == "true",
|
||||
}, nil
|
||||
}
|
||||
118
cmd/puppeth/module_nginx.go
Normal file
118
cmd/puppeth/module_nginx.go
Normal file
@@ -0,0 +1,118 @@
|
||||
// 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"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"math/rand"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
)
|
||||
|
||||
// nginxDockerfile is theis the Dockerfile required to build an nginx reverse-
|
||||
// proxy.
|
||||
var nginxDockerfile = `FROM jwilder/nginx-proxy`
|
||||
|
||||
// nginxComposefile is the docker-compose.yml file required to deploy and maintain
|
||||
// an nginx reverse-proxy. The proxy is responsible for exposing one or more HTTP
|
||||
// services running on a single host.
|
||||
var nginxComposefile = `
|
||||
version: '2'
|
||||
services:
|
||||
nginx:
|
||||
build: .
|
||||
image: {{.Network}}/nginx
|
||||
ports:
|
||||
- "{{.Port}}:80"
|
||||
volumes:
|
||||
- /var/run/docker.sock:/tmp/docker.sock:ro
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "1m"
|
||||
max-file: "10"
|
||||
restart: always
|
||||
`
|
||||
|
||||
// deployNginx deploys a new nginx reverse-proxy container to expose one or more
|
||||
// HTTP services running on a single host. If an instance with the specified
|
||||
// network name already exists there, it will be overwritten!
|
||||
func deployNginx(client *sshClient, network string, port int, nocache bool) ([]byte, error) {
|
||||
log.Info("Deploying nginx reverse-proxy", "server", client.server, "port", port)
|
||||
|
||||
// Generate the content to upload to the server
|
||||
workdir := fmt.Sprintf("%d", rand.Int63())
|
||||
files := make(map[string][]byte)
|
||||
|
||||
dockerfile := new(bytes.Buffer)
|
||||
template.Must(template.New("").Parse(nginxDockerfile)).Execute(dockerfile, nil)
|
||||
files[filepath.Join(workdir, "Dockerfile")] = dockerfile.Bytes()
|
||||
|
||||
composefile := new(bytes.Buffer)
|
||||
template.Must(template.New("").Parse(nginxComposefile)).Execute(composefile, map[string]interface{}{
|
||||
"Network": network,
|
||||
"Port": port,
|
||||
})
|
||||
files[filepath.Join(workdir, "docker-compose.yaml")] = composefile.Bytes()
|
||||
|
||||
// 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 reverse-proxy 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", workdir, network, network))
|
||||
}
|
||||
return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s up -d --build --force-recreate", workdir, network))
|
||||
}
|
||||
|
||||
// nginxInfos is returned from an nginx reverse-proxy status check to allow
|
||||
// reporting various configuration parameters.
|
||||
type nginxInfos struct {
|
||||
port int
|
||||
}
|
||||
|
||||
// Report converts the typed struct into a plain string->string map, containing
|
||||
// most - but not all - fields for reporting to the user.
|
||||
func (info *nginxInfos) Report() map[string]string {
|
||||
return map[string]string{
|
||||
"Shared listener port": strconv.Itoa(info.port),
|
||||
}
|
||||
}
|
||||
|
||||
// checkNginx does a health-check against an nginx reverse-proxy to verify whether
|
||||
// it's running, and if yes, gathering a collection of useful infos about it.
|
||||
func checkNginx(client *sshClient, network string) (*nginxInfos, error) {
|
||||
// Inspect a possible nginx container on the host
|
||||
infos, err := inspectContainer(client, fmt.Sprintf("%s_nginx_1", network))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !infos.running {
|
||||
return nil, ErrServiceOffline
|
||||
}
|
||||
// Container available, assemble and return the useful infos
|
||||
return &nginxInfos{
|
||||
port: infos.portmap["80/tcp"],
|
||||
}, nil
|
||||
}
|
||||
278
cmd/puppeth/module_node.go
Normal file
278
cmd/puppeth/module_node.go
Normal file
@@ -0,0 +1,278 @@
|
||||
// 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 $'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
|
||||
|
||||
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}}
|
||||
ports:
|
||||
- "{{.FullPort}}:{{.FullPort}}"
|
||||
- "{{.FullPort}}:{{.FullPort}}/udp"{{if .Light}}
|
||||
- "{{.LightPort}}:{{.LightPort}}/udp"{{end}}
|
||||
volumes:
|
||||
- {{.Datadir}}:/root/.ethereum{{if .Ethashdir}}
|
||||
- {{.Ethashdir}}:/root/.ethash{{end}}
|
||||
environment:
|
||||
- FULL_PORT={{.FullPort}}/tcp
|
||||
- LIGHT_PORT={{.LightPort}}/udp
|
||||
- TOTAL_PEERS={{.TotalPeers}}
|
||||
- LIGHT_PEERS={{.LightPeers}}
|
||||
- STATS_NAME={{.Ethstats}}
|
||||
- MINER_NAME={{.Etherbase}}
|
||||
- GAS_TARGET={{.GasTarget}}
|
||||
- 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, bootv4, bootv5 []string, config *nodeInfos, nocache bool) ([]byte, error) {
|
||||
kind := "sealnode"
|
||||
if config.keyJSON == "" && config.etherbase == "" {
|
||||
kind = "bootnode"
|
||||
bootv4 = make([]string, 0)
|
||||
bootv5 = 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.portFull,
|
||||
"Peers": config.peersTotal,
|
||||
"LightFlag": lightFlag,
|
||||
"BootV4": strings.Join(bootv4, ","),
|
||||
"BootV5": strings.Join(bootv5, ","),
|
||||
"Ethstats": config.ethstats,
|
||||
"Etherbase": config.etherbase,
|
||||
"GasTarget": uint64(1000000 * config.gasTarget),
|
||||
"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,
|
||||
"FullPort": config.portFull,
|
||||
"TotalPeers": config.peersTotal,
|
||||
"Light": config.peersLight > 0,
|
||||
"LightPort": config.portFull + 1,
|
||||
"LightPeers": config.peersLight,
|
||||
"Ethstats": config.ethstats[:strings.Index(config.ethstats, ":")],
|
||||
"Etherbase": config.etherbase,
|
||||
"GasTarget": config.gasTarget,
|
||||
"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", workdir, network, network))
|
||||
}
|
||||
return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s up -d --build --force-recreate", 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
|
||||
portFull int
|
||||
portLight int
|
||||
enodeFull string
|
||||
enodeLight string
|
||||
peersTotal int
|
||||
peersLight int
|
||||
etherbase string
|
||||
keyJSON string
|
||||
keyPass string
|
||||
gasTarget 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 (full nodes)": strconv.Itoa(info.portFull),
|
||||
"Peer count (all total)": strconv.Itoa(info.peersTotal),
|
||||
"Peer count (light nodes)": strconv.Itoa(info.peersLight),
|
||||
"Ethstats username": info.ethstats,
|
||||
}
|
||||
if info.peersLight > 0 {
|
||||
// Light server enabled
|
||||
report["Listener port (light nodes)"] = strconv.Itoa(info.portLight)
|
||||
}
|
||||
if info.gasTarget > 0 {
|
||||
// Miner or signer node
|
||||
report["Gas limit (baseline target)"] = fmt.Sprintf("%0.3f MGas", info.gasTarget)
|
||||
report["Gas price (minimum accepted)"] = fmt.Sprintf("%0.3f GWei", info.gasPrice)
|
||||
|
||||
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 an 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)
|
||||
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.id attach", network, kind)); err != nil {
|
||||
return nil, ErrServiceUnreachable
|
||||
}
|
||||
id := 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["FULL_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"],
|
||||
portFull: infos.portmap[infos.envvars["FULL_PORT"]],
|
||||
portLight: infos.portmap[infos.envvars["LIGHT_PORT"]],
|
||||
peersTotal: totalPeers,
|
||||
peersLight: lightPeers,
|
||||
ethstats: infos.envvars["STATS_NAME"],
|
||||
etherbase: infos.envvars["MINER_NAME"],
|
||||
keyJSON: keyJSON,
|
||||
keyPass: keyPass,
|
||||
gasTarget: gasTarget,
|
||||
gasPrice: gasPrice,
|
||||
}
|
||||
stats.enodeFull = fmt.Sprintf("enode://%s@%s:%d", id, client.address, stats.portFull)
|
||||
if stats.portLight != 0 {
|
||||
stats.enodeLight = fmt.Sprintf("enode://%s@%s:%d?discport=%d", id, client.address, stats.portFull, stats.portLight)
|
||||
}
|
||||
return stats, nil
|
||||
}
|
||||
200
cmd/puppeth/module_wallet.go
Normal file
200
cmd/puppeth/module_wallet.go
Normal file
@@ -0,0 +1,200 @@
|
||||
// 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"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"math/rand"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
)
|
||||
|
||||
// walletDockerfile is the Dockerfile required to run a web wallet.
|
||||
var walletDockerfile = `
|
||||
FROM puppeth/wallet:latest
|
||||
|
||||
ADD genesis.json /genesis.json
|
||||
|
||||
RUN \
|
||||
echo 'node server.js &' > wallet.sh && \
|
||||
echo 'geth --cache 512 init /genesis.json' >> wallet.sh && \
|
||||
echo $'geth --networkid {{.NetworkID}} --port {{.NodePort}} --bootnodes {{.Bootnodes}} --ethstats \'{{.Ethstats}}\' --cache=512 --rpc --rpcaddr=0.0.0.0 --rpccorsdomain "*"' >> wallet.sh
|
||||
|
||||
RUN \
|
||||
sed -i 's/PuppethNetworkID/{{.NetworkID}}/g' dist/js/etherwallet-master.js && \
|
||||
sed -i 's/PuppethNetwork/{{.Network}}/g' dist/js/etherwallet-master.js && \
|
||||
sed -i 's/PuppethDenom/{{.Denom}}/g' dist/js/etherwallet-master.js && \
|
||||
sed -i 's/PuppethHost/{{.Host}}/g' dist/js/etherwallet-master.js && \
|
||||
sed -i 's/PuppethRPCPort/{{.RPCPort}}/g' dist/js/etherwallet-master.js
|
||||
|
||||
ENTRYPOINT ["/bin/sh", "wallet.sh"]
|
||||
`
|
||||
|
||||
// walletComposefile is the docker-compose.yml file required to deploy and
|
||||
// maintain a web wallet.
|
||||
var walletComposefile = `
|
||||
version: '2'
|
||||
services:
|
||||
wallet:
|
||||
build: .
|
||||
image: {{.Network}}/wallet
|
||||
ports:
|
||||
- "{{.NodePort}}:{{.NodePort}}"
|
||||
- "{{.NodePort}}:{{.NodePort}}/udp"
|
||||
- "{{.RPCPort}}:8545"{{if not .VHost}}
|
||||
- "{{.WebPort}}:80"{{end}}
|
||||
volumes:
|
||||
- {{.Datadir}}:/root/.ethereum
|
||||
environment:
|
||||
- NODE_PORT={{.NodePort}}/tcp
|
||||
- STATS={{.Ethstats}}{{if .VHost}}
|
||||
- VIRTUAL_HOST={{.VHost}}
|
||||
- VIRTUAL_PORT=80{{end}}
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "1m"
|
||||
max-file: "10"
|
||||
restart: always
|
||||
`
|
||||
|
||||
// deployWallet deploys a new web wallet 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 deployWallet(client *sshClient, network string, bootnodes []string, config *walletInfos, nocache bool) ([]byte, error) {
|
||||
// Generate the content to upload to the server
|
||||
workdir := fmt.Sprintf("%d", rand.Int63())
|
||||
files := make(map[string][]byte)
|
||||
|
||||
dockerfile := new(bytes.Buffer)
|
||||
template.Must(template.New("").Parse(walletDockerfile)).Execute(dockerfile, map[string]interface{}{
|
||||
"Network": strings.ToTitle(network),
|
||||
"Denom": strings.ToUpper(network),
|
||||
"NetworkID": config.network,
|
||||
"NodePort": config.nodePort,
|
||||
"RPCPort": config.rpcPort,
|
||||
"Bootnodes": strings.Join(bootnodes, ","),
|
||||
"Ethstats": config.ethstats,
|
||||
"Host": client.address,
|
||||
})
|
||||
files[filepath.Join(workdir, "Dockerfile")] = dockerfile.Bytes()
|
||||
|
||||
composefile := new(bytes.Buffer)
|
||||
template.Must(template.New("").Parse(walletComposefile)).Execute(composefile, map[string]interface{}{
|
||||
"Datadir": config.datadir,
|
||||
"Network": network,
|
||||
"NodePort": config.nodePort,
|
||||
"RPCPort": config.rpcPort,
|
||||
"VHost": config.webHost,
|
||||
"WebPort": config.webPort,
|
||||
"Ethstats": config.ethstats[:strings.Index(config.ethstats, ":")],
|
||||
})
|
||||
files[filepath.Join(workdir, "docker-compose.yaml")] = composefile.Bytes()
|
||||
|
||||
files[filepath.Join(workdir, "genesis.json")] = config.genesis
|
||||
|
||||
// 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", workdir, network, network))
|
||||
}
|
||||
return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s up -d --build --force-recreate", workdir, network))
|
||||
}
|
||||
|
||||
// walletInfos is returned from a web wallet status check to allow reporting
|
||||
// various configuration parameters.
|
||||
type walletInfos struct {
|
||||
genesis []byte
|
||||
network int64
|
||||
datadir string
|
||||
ethstats string
|
||||
nodePort int
|
||||
rpcPort int
|
||||
webHost string
|
||||
webPort int
|
||||
}
|
||||
|
||||
// Report converts the typed struct into a plain string->string map, containing
|
||||
// most - but not all - fields for reporting to the user.
|
||||
func (info *walletInfos) Report() map[string]string {
|
||||
report := map[string]string{
|
||||
"Data directory": info.datadir,
|
||||
"Ethstats username": info.ethstats,
|
||||
"Node listener port ": strconv.Itoa(info.nodePort),
|
||||
"RPC listener port ": strconv.Itoa(info.rpcPort),
|
||||
"Website address ": info.webHost,
|
||||
"Website listener port ": strconv.Itoa(info.webPort),
|
||||
}
|
||||
return report
|
||||
}
|
||||
|
||||
// checkWallet does a health-check against web wallet server to verify whether
|
||||
// it's running, and if yes, whether it's responsive.
|
||||
func checkWallet(client *sshClient, network string) (*walletInfos, error) {
|
||||
// Inspect a possible web wallet container on the host
|
||||
infos, err := inspectContainer(client, fmt.Sprintf("%s_wallet_1", network))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !infos.running {
|
||||
return nil, ErrServiceOffline
|
||||
}
|
||||
// Resolve the port from the host, or the reverse proxy
|
||||
webPort := infos.portmap["80/tcp"]
|
||||
if webPort == 0 {
|
||||
if proxy, _ := checkNginx(client, network); proxy != nil {
|
||||
webPort = proxy.port
|
||||
}
|
||||
}
|
||||
if webPort == 0 {
|
||||
return nil, ErrNotExposed
|
||||
}
|
||||
// Resolve the host from the reverse-proxy and the config values
|
||||
host := infos.envvars["VIRTUAL_HOST"]
|
||||
if host == "" {
|
||||
host = client.server
|
||||
}
|
||||
// Run a sanity check to see if the devp2p and RPC ports are reachable
|
||||
nodePort := infos.portmap[infos.envvars["NODE_PORT"]]
|
||||
if err = checkPort(client.server, nodePort); err != nil {
|
||||
log.Warn(fmt.Sprintf("Wallet devp2p port seems unreachable"), "server", client.server, "port", nodePort, "err", err)
|
||||
}
|
||||
rpcPort := infos.portmap["8545/tcp"]
|
||||
if err = checkPort(client.server, rpcPort); err != nil {
|
||||
log.Warn(fmt.Sprintf("Wallet RPC port seems unreachable"), "server", client.server, "port", rpcPort, "err", err)
|
||||
}
|
||||
// Assemble and return the useful infos
|
||||
stats := &walletInfos{
|
||||
datadir: infos.volumes["/root/.ethereum"],
|
||||
nodePort: nodePort,
|
||||
rpcPort: rpcPort,
|
||||
webHost: host,
|
||||
webPort: webPort,
|
||||
ethstats: infos.envvars["STATS"],
|
||||
}
|
||||
return stats, nil
|
||||
}
|
||||
55
cmd/puppeth/puppeth.go
Normal file
55
cmd/puppeth/puppeth.go
Normal file
@@ -0,0 +1,55 @@
|
||||
// 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/>.
|
||||
|
||||
// puppeth is a command to assemble and maintain private networks.
|
||||
package main
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"gopkg.in/urfave/cli.v1"
|
||||
)
|
||||
|
||||
// main is just a boring entry point to set up the CLI app.
|
||||
func main() {
|
||||
app := cli.NewApp()
|
||||
app.Name = "puppeth"
|
||||
app.Usage = "assemble and maintain private Ethereum networks"
|
||||
app.Flags = []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "network",
|
||||
Usage: "name of the network to administer",
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "loglevel",
|
||||
Value: 3,
|
||||
Usage: "log level to emit to the screen",
|
||||
},
|
||||
}
|
||||
app.Action = func(c *cli.Context) error {
|
||||
// Set up the logger to print everything and the random generator
|
||||
log.Root().SetHandler(log.LvlFilterHandler(log.Lvl(c.Int("loglevel")), log.StreamHandler(os.Stdout, log.TerminalFormat(true))))
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
|
||||
// Start the wizard and relinquish control
|
||||
makeWizard(c.String("network")).run()
|
||||
return nil
|
||||
}
|
||||
app.Run(os.Args)
|
||||
}
|
||||
245
cmd/puppeth/ssh.go
Normal file
245
cmd/puppeth/ssh.go
Normal file
@@ -0,0 +1,245 @@
|
||||
// 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 (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
)
|
||||
|
||||
// sshClient is a small wrapper around Go's SSH client with a few utility methods
|
||||
// implemented on top.
|
||||
type sshClient struct {
|
||||
server string // Server name or IP without port number
|
||||
address string // IP address of the remote server
|
||||
pubkey []byte // RSA public key to authenticate the server
|
||||
client *ssh.Client
|
||||
logger log.Logger
|
||||
}
|
||||
|
||||
// dial establishes an SSH connection to a remote node using the current user and
|
||||
// the user's configured private RSA key. If that fails, password authentication
|
||||
// is fallen back to. The caller may override the login user via user@server:port.
|
||||
func dial(server string, pubkey []byte) (*sshClient, error) {
|
||||
// Figure out a label for the server and a logger
|
||||
label := server
|
||||
if strings.Contains(label, ":") {
|
||||
label = label[:strings.Index(label, ":")]
|
||||
}
|
||||
login := ""
|
||||
if strings.Contains(server, "@") {
|
||||
login = label[:strings.Index(label, "@")]
|
||||
label = label[strings.Index(label, "@")+1:]
|
||||
server = server[strings.Index(server, "@")+1:]
|
||||
}
|
||||
logger := log.New("server", label)
|
||||
logger.Debug("Attempting to establish SSH connection")
|
||||
|
||||
user, err := user.Current()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if login == "" {
|
||||
login = user.Username
|
||||
}
|
||||
// Configure the supported authentication methods (private key and password)
|
||||
var auths []ssh.AuthMethod
|
||||
|
||||
path := filepath.Join(user.HomeDir, ".ssh", "id_rsa")
|
||||
if buf, err := ioutil.ReadFile(path); err != nil {
|
||||
log.Warn("No SSH key, falling back to passwords", "path", path, "err", err)
|
||||
} else {
|
||||
key, err := ssh.ParsePrivateKey(buf)
|
||||
if err != nil {
|
||||
fmt.Printf("What's the decryption password for %s? (won't be echoed)\n>", path)
|
||||
blob, err := terminal.ReadPassword(int(os.Stdin.Fd()))
|
||||
fmt.Println()
|
||||
if err != nil {
|
||||
log.Warn("Couldn't read password", "err", err)
|
||||
}
|
||||
key, err := ssh.ParsePrivateKeyWithPassphrase(buf, blob)
|
||||
if err != nil {
|
||||
log.Warn("Failed to decrypt SSH key, falling back to passwords", "path", path, "err", err)
|
||||
} else {
|
||||
auths = append(auths, ssh.PublicKeys(key))
|
||||
}
|
||||
} else {
|
||||
auths = append(auths, ssh.PublicKeys(key))
|
||||
}
|
||||
}
|
||||
auths = append(auths, ssh.PasswordCallback(func() (string, error) {
|
||||
fmt.Printf("What's the login password for %s at %s? (won't be echoed)\n> ", login, server)
|
||||
blob, err := terminal.ReadPassword(int(os.Stdin.Fd()))
|
||||
|
||||
fmt.Println()
|
||||
return string(blob), err
|
||||
}))
|
||||
// Resolve the IP address of the remote server
|
||||
addr, err := net.LookupHost(label)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(addr) == 0 {
|
||||
return nil, errors.New("no IPs associated with domain")
|
||||
}
|
||||
// Try to dial in to the remote server
|
||||
logger.Trace("Dialing remote SSH server", "user", login)
|
||||
if !strings.Contains(server, ":") {
|
||||
server += ":22"
|
||||
}
|
||||
keycheck := func(hostname string, remote net.Addr, key ssh.PublicKey) error {
|
||||
// If no public key is known for SSH, ask the user to confirm
|
||||
if pubkey == nil {
|
||||
fmt.Println()
|
||||
fmt.Printf("The authenticity of host '%s (%s)' can't be established.\n", hostname, remote)
|
||||
fmt.Printf("SSH key fingerprint is %s [MD5]\n", ssh.FingerprintLegacyMD5(key))
|
||||
fmt.Printf("Are you sure you want to continue connecting (yes/no)? ")
|
||||
|
||||
text, err := bufio.NewReader(os.Stdin).ReadString('\n')
|
||||
switch {
|
||||
case err != nil:
|
||||
return err
|
||||
case strings.TrimSpace(text) == "yes":
|
||||
pubkey = key.Marshal()
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("unknown auth choice: %v", text)
|
||||
}
|
||||
}
|
||||
// If a public key exists for this SSH server, check that it matches
|
||||
if bytes.Equal(pubkey, key.Marshal()) {
|
||||
return nil
|
||||
}
|
||||
// We have a mismatch, forbid connecting
|
||||
return errors.New("ssh key mismatch, readd the machine to update")
|
||||
}
|
||||
client, err := ssh.Dial("tcp", server, &ssh.ClientConfig{User: login, Auth: auths, HostKeyCallback: keycheck})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Connection established, return our utility wrapper
|
||||
c := &sshClient{
|
||||
server: label,
|
||||
address: addr[0],
|
||||
pubkey: pubkey,
|
||||
client: client,
|
||||
logger: logger,
|
||||
}
|
||||
if err := c.init(); err != nil {
|
||||
client.Close()
|
||||
return nil, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// init runs some initialization commands on the remote server to ensure it's
|
||||
// capable of acting as puppeth target.
|
||||
func (client *sshClient) init() error {
|
||||
client.logger.Debug("Verifying if docker is available")
|
||||
if out, err := client.Run("docker version"); err != nil {
|
||||
if len(out) == 0 {
|
||||
return err
|
||||
}
|
||||
return fmt.Errorf("docker configured incorrectly: %s", out)
|
||||
}
|
||||
client.logger.Debug("Verifying if docker-compose is available")
|
||||
if out, err := client.Run("docker-compose version"); err != nil {
|
||||
if len(out) == 0 {
|
||||
return err
|
||||
}
|
||||
return fmt.Errorf("docker-compose configured incorrectly: %s", out)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close terminates the connection to an SSH server.
|
||||
func (client *sshClient) Close() error {
|
||||
return client.client.Close()
|
||||
}
|
||||
|
||||
// Run executes a command on the remote server and returns the combined output
|
||||
// along with any error status.
|
||||
func (client *sshClient) Run(cmd string) ([]byte, error) {
|
||||
// Establish a single command session
|
||||
session, err := client.client.NewSession()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
// Execute the command and return any output
|
||||
client.logger.Trace("Running command on remote server", "cmd", cmd)
|
||||
return session.CombinedOutput(cmd)
|
||||
}
|
||||
|
||||
// Stream executes a command on the remote server and streams all outputs into
|
||||
// the local stdout and stderr streams.
|
||||
func (client *sshClient) Stream(cmd string) error {
|
||||
// Establish a single command session
|
||||
session, err := client.client.NewSession()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
session.Stdout = os.Stdout
|
||||
session.Stderr = os.Stderr
|
||||
|
||||
// Execute the command and return any output
|
||||
client.logger.Trace("Streaming command on remote server", "cmd", cmd)
|
||||
return session.Run(cmd)
|
||||
}
|
||||
|
||||
// Upload copies the set of files to a remote server via SCP, creating any non-
|
||||
// existing folders in the mean time.
|
||||
func (client *sshClient) Upload(files map[string][]byte) ([]byte, error) {
|
||||
// Establish a single command session
|
||||
session, err := client.client.NewSession()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
// Create a goroutine that streams the SCP content
|
||||
go func() {
|
||||
out, _ := session.StdinPipe()
|
||||
defer out.Close()
|
||||
|
||||
for file, content := range files {
|
||||
client.logger.Trace("Uploading file to server", "file", file, "bytes", len(content))
|
||||
|
||||
fmt.Fprintln(out, "D0755", 0, filepath.Dir(file)) // Ensure the folder exists
|
||||
fmt.Fprintln(out, "C0644", len(content), filepath.Base(file)) // Create the actual file
|
||||
out.Write(content) // Stream the data content
|
||||
fmt.Fprint(out, "\x00") // Transfer end with \x00
|
||||
fmt.Fprintln(out, "E") // Leave directory (simpler)
|
||||
}
|
||||
}()
|
||||
return session.CombinedOutput("/usr/bin/scp -v -tr ./")
|
||||
}
|
||||
327
cmd/puppeth/wizard.go
Normal file
327
cmd/puppeth/wizard.go
Normal file
@@ -0,0 +1,327 @@
|
||||
// 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 (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math/big"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
)
|
||||
|
||||
// config contains all the configurations needed by puppeth that should be saved
|
||||
// between sessions.
|
||||
type config struct {
|
||||
path string // File containing the configuration values
|
||||
bootFull []string // Bootnodes to always connect to by full nodes
|
||||
bootLight []string // Bootnodes to always connect to by light nodes
|
||||
ethstats string // Ethstats settings to cache for node deploys
|
||||
|
||||
Genesis *core.Genesis `json:"genesis,omitempty"` // Genesis block to cache for node deploys
|
||||
Servers map[string][]byte `json:"servers,omitempty"`
|
||||
}
|
||||
|
||||
// servers retrieves an alphabetically sorted list of servers.
|
||||
func (c config) servers() []string {
|
||||
servers := make([]string, 0, len(c.Servers))
|
||||
for server := range c.Servers {
|
||||
servers = append(servers, server)
|
||||
}
|
||||
sort.Strings(servers)
|
||||
|
||||
return servers
|
||||
}
|
||||
|
||||
// flush dumps the contents of config to disk.
|
||||
func (c config) flush() {
|
||||
os.MkdirAll(filepath.Dir(c.path), 0755)
|
||||
|
||||
out, _ := json.MarshalIndent(c, "", " ")
|
||||
if err := ioutil.WriteFile(c.path, out, 0644); err != nil {
|
||||
log.Warn("Failed to save puppeth configs", "file", c.path, "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
type wizard struct {
|
||||
network string // Network name to manage
|
||||
conf config // Configurations from previous runs
|
||||
|
||||
servers map[string]*sshClient // SSH connections to servers to administer
|
||||
services map[string][]string // Ethereum services known to be running on servers
|
||||
|
||||
in *bufio.Reader // Wrapper around stdin to allow reading user input
|
||||
lock sync.Mutex // Lock to protect configs during concurrent service discovery
|
||||
}
|
||||
|
||||
// read reads a single line from stdin, trimming if from spaces.
|
||||
func (w *wizard) read() string {
|
||||
fmt.Printf("> ")
|
||||
text, err := w.in.ReadString('\n')
|
||||
if err != nil {
|
||||
log.Crit("Failed to read user input", "err", err)
|
||||
}
|
||||
return strings.TrimSpace(text)
|
||||
}
|
||||
|
||||
// readString reads a single line from stdin, trimming if from spaces, enforcing
|
||||
// non-emptyness.
|
||||
func (w *wizard) readString() string {
|
||||
for {
|
||||
fmt.Printf("> ")
|
||||
text, err := w.in.ReadString('\n')
|
||||
if err != nil {
|
||||
log.Crit("Failed to read user input", "err", err)
|
||||
}
|
||||
if text = strings.TrimSpace(text); text != "" {
|
||||
return text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// readDefaultString reads a single line from stdin, trimming if from spaces. If
|
||||
// an empty line is entered, the default value is returned.
|
||||
func (w *wizard) readDefaultString(def string) string {
|
||||
fmt.Printf("> ")
|
||||
text, err := w.in.ReadString('\n')
|
||||
if err != nil {
|
||||
log.Crit("Failed to read user input", "err", err)
|
||||
}
|
||||
if text = strings.TrimSpace(text); text != "" {
|
||||
return text
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
// readInt reads a single line from stdin, trimming if from spaces, enforcing it
|
||||
// to parse into an integer.
|
||||
func (w *wizard) readInt() int {
|
||||
for {
|
||||
fmt.Printf("> ")
|
||||
text, err := w.in.ReadString('\n')
|
||||
if err != nil {
|
||||
log.Crit("Failed to read user input", "err", err)
|
||||
}
|
||||
if text = strings.TrimSpace(text); text == "" {
|
||||
continue
|
||||
}
|
||||
val, err := strconv.Atoi(strings.TrimSpace(text))
|
||||
if err != nil {
|
||||
log.Error("Invalid input, expected integer", "err", err)
|
||||
continue
|
||||
}
|
||||
return val
|
||||
}
|
||||
}
|
||||
|
||||
// readDefaultInt reads a single line from stdin, trimming if from spaces, enforcing
|
||||
// it to parse into an integer. If an empty line is entered, the default value is
|
||||
// returned.
|
||||
func (w *wizard) readDefaultInt(def int) int {
|
||||
for {
|
||||
fmt.Printf("> ")
|
||||
text, err := w.in.ReadString('\n')
|
||||
if err != nil {
|
||||
log.Crit("Failed to read user input", "err", err)
|
||||
}
|
||||
if text = strings.TrimSpace(text); text == "" {
|
||||
return def
|
||||
}
|
||||
val, err := strconv.Atoi(strings.TrimSpace(text))
|
||||
if err != nil {
|
||||
log.Error("Invalid input, expected integer", "err", err)
|
||||
continue
|
||||
}
|
||||
return val
|
||||
}
|
||||
}
|
||||
|
||||
// readDefaultBigInt reads a single line from stdin, trimming if from spaces,
|
||||
// enforcing it to parse into a big integer. If an empty line is entered, the
|
||||
// default value is returned.
|
||||
func (w *wizard) readDefaultBigInt(def *big.Int) *big.Int {
|
||||
for {
|
||||
fmt.Printf("> ")
|
||||
text, err := w.in.ReadString('\n')
|
||||
if err != nil {
|
||||
log.Crit("Failed to read user input", "err", err)
|
||||
}
|
||||
if text = strings.TrimSpace(text); text == "" {
|
||||
return def
|
||||
}
|
||||
val, ok := new(big.Int).SetString(text, 0)
|
||||
if !ok {
|
||||
log.Error("Invalid input, expected big integer")
|
||||
continue
|
||||
}
|
||||
return val
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
// readFloat reads a single line from stdin, trimming if from spaces, enforcing it
|
||||
// to parse into a float.
|
||||
func (w *wizard) readFloat() float64 {
|
||||
for {
|
||||
fmt.Printf("> ")
|
||||
text, err := w.in.ReadString('\n')
|
||||
if err != nil {
|
||||
log.Crit("Failed to read user input", "err", err)
|
||||
}
|
||||
if text = strings.TrimSpace(text); text == "" {
|
||||
continue
|
||||
}
|
||||
val, err := strconv.ParseFloat(strings.TrimSpace(text), 64)
|
||||
if err != nil {
|
||||
log.Error("Invalid input, expected float", "err", err)
|
||||
continue
|
||||
}
|
||||
return val
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
// readDefaultFloat reads a single line from stdin, trimming if from spaces, enforcing
|
||||
// it to parse into a float. If an empty line is entered, the default value is returned.
|
||||
func (w *wizard) readDefaultFloat(def float64) float64 {
|
||||
for {
|
||||
fmt.Printf("> ")
|
||||
text, err := w.in.ReadString('\n')
|
||||
if err != nil {
|
||||
log.Crit("Failed to read user input", "err", err)
|
||||
}
|
||||
if text = strings.TrimSpace(text); text == "" {
|
||||
return def
|
||||
}
|
||||
val, err := strconv.ParseFloat(strings.TrimSpace(text), 64)
|
||||
if err != nil {
|
||||
log.Error("Invalid input, expected float", "err", err)
|
||||
continue
|
||||
}
|
||||
return val
|
||||
}
|
||||
}
|
||||
|
||||
// readPassword reads a single line from stdin, trimming it from the trailing new
|
||||
// line and returns it. The input will not be echoed.
|
||||
func (w *wizard) readPassword() string {
|
||||
fmt.Printf("> ")
|
||||
text, err := terminal.ReadPassword(int(os.Stdin.Fd()))
|
||||
if err != nil {
|
||||
log.Crit("Failed to read password", "err", err)
|
||||
}
|
||||
fmt.Println()
|
||||
return string(text)
|
||||
}
|
||||
|
||||
// readAddress reads a single line from stdin, trimming if from spaces and converts
|
||||
// it to an Ethereum address.
|
||||
func (w *wizard) readAddress() *common.Address {
|
||||
for {
|
||||
// Read the address from the user
|
||||
fmt.Printf("> 0x")
|
||||
text, err := w.in.ReadString('\n')
|
||||
if err != nil {
|
||||
log.Crit("Failed to read user input", "err", err)
|
||||
}
|
||||
if text = strings.TrimSpace(text); text == "" {
|
||||
return nil
|
||||
}
|
||||
// Make sure it looks ok and return it if so
|
||||
if len(text) != 40 {
|
||||
log.Error("Invalid address length, please retry")
|
||||
continue
|
||||
}
|
||||
bigaddr, _ := new(big.Int).SetString(text, 16)
|
||||
address := common.BigToAddress(bigaddr)
|
||||
return &address
|
||||
}
|
||||
}
|
||||
|
||||
// readDefaultAddress reads a single line from stdin, trimming if from spaces and
|
||||
// converts it to an Ethereum address. If an empty line is entered, the default
|
||||
// value is returned.
|
||||
func (w *wizard) readDefaultAddress(def common.Address) common.Address {
|
||||
for {
|
||||
// Read the address from the user
|
||||
fmt.Printf("> 0x")
|
||||
text, err := w.in.ReadString('\n')
|
||||
if err != nil {
|
||||
log.Crit("Failed to read user input", "err", err)
|
||||
}
|
||||
if text = strings.TrimSpace(text); text == "" {
|
||||
return def
|
||||
}
|
||||
// Make sure it looks ok and return it if so
|
||||
if len(text) != 40 {
|
||||
log.Error("Invalid address length, please retry")
|
||||
continue
|
||||
}
|
||||
bigaddr, _ := new(big.Int).SetString(text, 16)
|
||||
return common.BigToAddress(bigaddr)
|
||||
}
|
||||
}
|
||||
|
||||
// readJSON reads a raw JSON message and returns it.
|
||||
func (w *wizard) readJSON() string {
|
||||
var blob json.RawMessage
|
||||
|
||||
for {
|
||||
fmt.Printf("> ")
|
||||
if err := json.NewDecoder(w.in).Decode(&blob); err != nil {
|
||||
log.Error("Invalid JSON, please try again", "err", err)
|
||||
continue
|
||||
}
|
||||
return string(blob)
|
||||
}
|
||||
}
|
||||
|
||||
// readIPAddress reads a single line from stdin, trimming if from spaces and
|
||||
// returning it if it's convertible to an IP address. The reason for keeping
|
||||
// the user input format instead of returning a Go net.IP is to match with
|
||||
// weird formats used by ethstats, which compares IPs textually, not by value.
|
||||
func (w *wizard) readIPAddress() string {
|
||||
for {
|
||||
// Read the IP address from the user
|
||||
fmt.Printf("> ")
|
||||
text, err := w.in.ReadString('\n')
|
||||
if err != nil {
|
||||
log.Crit("Failed to read user input", "err", err)
|
||||
}
|
||||
if text = strings.TrimSpace(text); text == "" {
|
||||
return ""
|
||||
}
|
||||
// Make sure it looks ok and return it if so
|
||||
if ip := net.ParseIP(text); ip == nil {
|
||||
log.Error("Invalid IP address, please retry")
|
||||
continue
|
||||
}
|
||||
return text
|
||||
}
|
||||
}
|
||||
158
cmd/puppeth/wizard_dashboard.go
Normal file
158
cmd/puppeth/wizard_dashboard.go
Normal file
@@ -0,0 +1,158 @@
|
||||
// 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 (
|
||||
"fmt"
|
||||
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
)
|
||||
|
||||
// deployDashboard queries the user for various input on deploying a web-service
|
||||
// dashboard, after which is pushes the container.
|
||||
func (w *wizard) deployDashboard() {
|
||||
// Select the server to interact with
|
||||
server := w.selectServer()
|
||||
if server == "" {
|
||||
return
|
||||
}
|
||||
client := w.servers[server]
|
||||
|
||||
// Retrieve any active dashboard configurations from the server
|
||||
infos, err := checkDashboard(client, w.network)
|
||||
if err != nil {
|
||||
infos = &dashboardInfos{
|
||||
port: 80,
|
||||
host: client.server,
|
||||
}
|
||||
}
|
||||
existed := err == nil
|
||||
|
||||
// Figure out which port to listen on
|
||||
fmt.Println()
|
||||
fmt.Printf("Which port should the dashboard listen on? (default = %d)\n", infos.port)
|
||||
infos.port = w.readDefaultInt(infos.port)
|
||||
|
||||
// Figure which virtual-host to deploy the dashboard on
|
||||
infos.host, err = w.ensureVirtualHost(client, infos.port, infos.host)
|
||||
if err != nil {
|
||||
log.Error("Failed to decide on dashboard host", "err", err)
|
||||
return
|
||||
}
|
||||
// Port and proxy settings retrieved, figure out which services are available
|
||||
available := make(map[string][]string)
|
||||
for server, services := range w.services {
|
||||
for _, service := range services {
|
||||
available[service] = append(available[service], server)
|
||||
}
|
||||
}
|
||||
for _, service := range []string{"ethstats", "explorer", "wallet", "faucet"} {
|
||||
// Gather all the locally hosted pages of this type
|
||||
var pages []string
|
||||
for _, server := range available[service] {
|
||||
client := w.servers[server]
|
||||
if client == nil {
|
||||
continue
|
||||
}
|
||||
// If there's a service running on the machine, retrieve it's port number
|
||||
var port int
|
||||
switch service {
|
||||
case "ethstats":
|
||||
if infos, err := checkEthstats(client, w.network); err == nil {
|
||||
port = infos.port
|
||||
}
|
||||
case "explorer":
|
||||
if infos, err := checkExplorer(client, w.network); err == nil {
|
||||
port = infos.webPort
|
||||
}
|
||||
case "wallet":
|
||||
if infos, err := checkWallet(client, w.network); err == nil {
|
||||
port = infos.webPort
|
||||
}
|
||||
case "faucet":
|
||||
if infos, err := checkFaucet(client, w.network); err == nil {
|
||||
port = infos.port
|
||||
}
|
||||
}
|
||||
if page, err := resolve(client, w.network, service, port); err == nil && page != "" {
|
||||
pages = append(pages, page)
|
||||
}
|
||||
}
|
||||
// Promt the user to chose one, enter manually or simply not list this service
|
||||
defLabel, defChoice := "don't list", len(pages)+2
|
||||
if len(pages) > 0 {
|
||||
defLabel, defChoice = pages[0], 1
|
||||
}
|
||||
fmt.Println()
|
||||
fmt.Printf("Which %s service to list? (default = %s)\n", service, defLabel)
|
||||
for i, page := range pages {
|
||||
fmt.Printf(" %d. %s\n", i+1, page)
|
||||
}
|
||||
fmt.Printf(" %d. List external %s service\n", len(pages)+1, service)
|
||||
fmt.Printf(" %d. Don't list any %s service\n", len(pages)+2, service)
|
||||
|
||||
choice := w.readDefaultInt(defChoice)
|
||||
if choice < 0 || choice > len(pages)+2 {
|
||||
log.Error("Invalid listing choice, aborting")
|
||||
return
|
||||
}
|
||||
var page string
|
||||
switch {
|
||||
case choice <= len(pages):
|
||||
page = pages[choice-1]
|
||||
case choice == len(pages)+1:
|
||||
fmt.Println()
|
||||
fmt.Printf("Which address is the external %s service at?\n", service)
|
||||
page = w.readString()
|
||||
default:
|
||||
// No service hosting for this
|
||||
}
|
||||
// Save the users choice
|
||||
switch service {
|
||||
case "ethstats":
|
||||
infos.ethstats = page
|
||||
case "explorer":
|
||||
infos.explorer = page
|
||||
case "wallet":
|
||||
infos.wallet = page
|
||||
case "faucet":
|
||||
infos.faucet = page
|
||||
}
|
||||
}
|
||||
// If we have ethstats running, ask whether to make the secret public or not
|
||||
if w.conf.ethstats != "" {
|
||||
fmt.Println()
|
||||
fmt.Println("Include ethstats secret on dashboard (y/n)? (default = yes)")
|
||||
infos.trusted = w.readDefaultString("y") == "y"
|
||||
}
|
||||
// Try to deploy the dashboard container on the host
|
||||
nocache := false
|
||||
if existed {
|
||||
fmt.Println()
|
||||
fmt.Printf("Should the dashboard be built from scratch (y/n)? (default = no)\n")
|
||||
nocache = w.readDefaultString("n") != "n"
|
||||
}
|
||||
if out, err := deployDashboard(client, w.network, &w.conf, infos, nocache); err != nil {
|
||||
log.Error("Failed to deploy dashboard container", "err", err)
|
||||
if len(out) > 0 {
|
||||
fmt.Printf("%s\n", out)
|
||||
}
|
||||
return
|
||||
}
|
||||
// All ok, run a network scan to pick any changes up
|
||||
w.networkStats()
|
||||
}
|
||||
126
cmd/puppeth/wizard_ethstats.go
Normal file
126
cmd/puppeth/wizard_ethstats.go
Normal file
@@ -0,0 +1,126 @@
|
||||
// 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 (
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
)
|
||||
|
||||
// deployEthstats queries the user for various input on deploying an ethstats
|
||||
// monitoring server, after which it executes it.
|
||||
func (w *wizard) deployEthstats() {
|
||||
// Select the server to interact with
|
||||
server := w.selectServer()
|
||||
if server == "" {
|
||||
return
|
||||
}
|
||||
client := w.servers[server]
|
||||
|
||||
// Retrieve any active ethstats configurations from the server
|
||||
infos, err := checkEthstats(client, w.network)
|
||||
if err != nil {
|
||||
infos = ðstatsInfos{
|
||||
port: 80,
|
||||
host: client.server,
|
||||
secret: "",
|
||||
}
|
||||
}
|
||||
existed := err == nil
|
||||
|
||||
// Figure out which port to listen on
|
||||
fmt.Println()
|
||||
fmt.Printf("Which port should ethstats listen on? (default = %d)\n", infos.port)
|
||||
infos.port = w.readDefaultInt(infos.port)
|
||||
|
||||
// Figure which virtual-host to deploy ethstats on
|
||||
if infos.host, err = w.ensureVirtualHost(client, infos.port, infos.host); err != nil {
|
||||
log.Error("Failed to decide on ethstats host", "err", err)
|
||||
return
|
||||
}
|
||||
// Port and proxy settings retrieved, figure out the secret and boot ethstats
|
||||
fmt.Println()
|
||||
if infos.secret == "" {
|
||||
fmt.Printf("What should be the secret password for the API? (must not be empty)\n")
|
||||
infos.secret = w.readString()
|
||||
} else {
|
||||
fmt.Printf("What should be the secret password for the API? (default = %s)\n", infos.secret)
|
||||
infos.secret = w.readDefaultString(infos.secret)
|
||||
}
|
||||
// Gather any blacklists to ban from reporting
|
||||
if existed {
|
||||
fmt.Println()
|
||||
fmt.Printf("Keep existing IP %v blacklist (y/n)? (default = yes)\n", infos.banned)
|
||||
if w.readDefaultString("y") != "y" {
|
||||
// The user might want to clear the entire list, although generally probably not
|
||||
fmt.Println()
|
||||
fmt.Printf("Clear out blacklist and start over (y/n)? (default = no)\n")
|
||||
if w.readDefaultString("n") != "n" {
|
||||
infos.banned = nil
|
||||
}
|
||||
// Offer the user to explicitly add/remove certain IP addresses
|
||||
fmt.Println()
|
||||
fmt.Println("Which additional IP addresses should be blacklisted?")
|
||||
for {
|
||||
if ip := w.readIPAddress(); ip != "" {
|
||||
infos.banned = append(infos.banned, ip)
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
fmt.Println()
|
||||
fmt.Println("Which IP addresses should not be blacklisted?")
|
||||
for {
|
||||
if ip := w.readIPAddress(); ip != "" {
|
||||
for i, addr := range infos.banned {
|
||||
if ip == addr {
|
||||
infos.banned = append(infos.banned[:i], infos.banned[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
sort.Strings(infos.banned)
|
||||
}
|
||||
}
|
||||
// Try to deploy the ethstats server on the host
|
||||
nocache := false
|
||||
if existed {
|
||||
fmt.Println()
|
||||
fmt.Printf("Should the ethstats be built from scratch (y/n)? (default = no)\n")
|
||||
nocache = w.readDefaultString("n") != "n"
|
||||
}
|
||||
trusted := make([]string, 0, len(w.servers))
|
||||
for _, client := range w.servers {
|
||||
if client != nil {
|
||||
trusted = append(trusted, client.address)
|
||||
}
|
||||
}
|
||||
if out, err := deployEthstats(client, w.network, infos.port, infos.secret, infos.host, trusted, infos.banned, nocache); err != nil {
|
||||
log.Error("Failed to deploy ethstats container", "err", err)
|
||||
if len(out) > 0 {
|
||||
fmt.Printf("%s\n", out)
|
||||
}
|
||||
return
|
||||
}
|
||||
// All ok, run a network scan to pick any changes up
|
||||
w.networkStats()
|
||||
}
|
||||
117
cmd/puppeth/wizard_explorer.go
Normal file
117
cmd/puppeth/wizard_explorer.go
Normal file
@@ -0,0 +1,117 @@
|
||||
// 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 (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
)
|
||||
|
||||
// deployExplorer creates a new block explorer based on some user input.
|
||||
func (w *wizard) deployExplorer() {
|
||||
// Do some sanity check before the user wastes time on input
|
||||
if w.conf.Genesis == nil {
|
||||
log.Error("No genesis block configured")
|
||||
return
|
||||
}
|
||||
if w.conf.ethstats == "" {
|
||||
log.Error("No ethstats server configured")
|
||||
return
|
||||
}
|
||||
if w.conf.Genesis.Config.Ethash == nil {
|
||||
log.Error("Only ethash network supported")
|
||||
return
|
||||
}
|
||||
// Select the server to interact with
|
||||
server := w.selectServer()
|
||||
if server == "" {
|
||||
return
|
||||
}
|
||||
client := w.servers[server]
|
||||
|
||||
// Retrieve any active node configurations from the server
|
||||
infos, err := checkExplorer(client, w.network)
|
||||
if err != nil {
|
||||
infos = &explorerInfos{
|
||||
nodePort: 30303, webPort: 80, webHost: client.server,
|
||||
}
|
||||
}
|
||||
existed := err == nil
|
||||
|
||||
chainspec, err := newParityChainSpec(w.network, w.conf.Genesis, w.conf.bootFull)
|
||||
if err != nil {
|
||||
log.Error("Failed to create chain spec for explorer", "err", err)
|
||||
return
|
||||
}
|
||||
chain, _ := json.MarshalIndent(chainspec, "", " ")
|
||||
|
||||
// Figure out which port to listen on
|
||||
fmt.Println()
|
||||
fmt.Printf("Which port should the explorer listen on? (default = %d)\n", infos.webPort)
|
||||
infos.webPort = w.readDefaultInt(infos.webPort)
|
||||
|
||||
// Figure which virtual-host to deploy ethstats on
|
||||
if infos.webHost, err = w.ensureVirtualHost(client, infos.webPort, infos.webHost); err != nil {
|
||||
log.Error("Failed to decide on explorer host", "err", err)
|
||||
return
|
||||
}
|
||||
// Figure out where the user wants to store the persistent data
|
||||
fmt.Println()
|
||||
if infos.datadir == "" {
|
||||
fmt.Printf("Where should data be stored on the remote machine?\n")
|
||||
infos.datadir = w.readString()
|
||||
} else {
|
||||
fmt.Printf("Where should data be stored on the remote machine? (default = %s)\n", infos.datadir)
|
||||
infos.datadir = w.readDefaultString(infos.datadir)
|
||||
}
|
||||
// Figure out which port to listen on
|
||||
fmt.Println()
|
||||
fmt.Printf("Which TCP/UDP port should the archive node listen on? (default = %d)\n", infos.nodePort)
|
||||
infos.nodePort = w.readDefaultInt(infos.nodePort)
|
||||
|
||||
// Set a proper name to report on the stats page
|
||||
fmt.Println()
|
||||
if infos.ethstats == "" {
|
||||
fmt.Printf("What should the explorer be called on the stats page?\n")
|
||||
infos.ethstats = w.readString() + ":" + w.conf.ethstats
|
||||
} else {
|
||||
fmt.Printf("What should the explorer be called on the stats page? (default = %s)\n", infos.ethstats)
|
||||
infos.ethstats = w.readDefaultString(infos.ethstats) + ":" + w.conf.ethstats
|
||||
}
|
||||
// Try to deploy the explorer on the host
|
||||
nocache := false
|
||||
if existed {
|
||||
fmt.Println()
|
||||
fmt.Printf("Should the explorer be built from scratch (y/n)? (default = no)\n")
|
||||
nocache = w.readDefaultString("n") != "n"
|
||||
}
|
||||
if out, err := deployExplorer(client, w.network, chain, infos, nocache); err != nil {
|
||||
log.Error("Failed to deploy explorer container", "err", err)
|
||||
if len(out) > 0 {
|
||||
fmt.Printf("%s\n", out)
|
||||
}
|
||||
return
|
||||
}
|
||||
// All ok, run a network scan to pick any changes up
|
||||
log.Info("Waiting for node to finish booting")
|
||||
time.Sleep(3 * time.Second)
|
||||
|
||||
w.networkStats()
|
||||
}
|
||||
180
cmd/puppeth/wizard_faucet.go
Normal file
180
cmd/puppeth/wizard_faucet.go
Normal file
@@ -0,0 +1,180 @@
|
||||
// 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 (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts/keystore"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
)
|
||||
|
||||
// deployFaucet queries the user for various input on deploying a faucet, after
|
||||
// which it executes it.
|
||||
func (w *wizard) deployFaucet() {
|
||||
// Select the server to interact with
|
||||
server := w.selectServer()
|
||||
if server == "" {
|
||||
return
|
||||
}
|
||||
client := w.servers[server]
|
||||
|
||||
// Retrieve any active faucet configurations from the server
|
||||
infos, err := checkFaucet(client, w.network)
|
||||
if err != nil {
|
||||
infos = &faucetInfos{
|
||||
node: &nodeInfos{portFull: 30303, peersTotal: 25},
|
||||
port: 80,
|
||||
host: client.server,
|
||||
amount: 1,
|
||||
minutes: 1440,
|
||||
tiers: 3,
|
||||
}
|
||||
}
|
||||
existed := err == nil
|
||||
|
||||
infos.node.genesis, _ = json.MarshalIndent(w.conf.Genesis, "", " ")
|
||||
infos.node.network = w.conf.Genesis.Config.ChainId.Int64()
|
||||
|
||||
// Figure out which port to listen on
|
||||
fmt.Println()
|
||||
fmt.Printf("Which port should the faucet listen on? (default = %d)\n", infos.port)
|
||||
infos.port = w.readDefaultInt(infos.port)
|
||||
|
||||
// Figure which virtual-host to deploy ethstats on
|
||||
if infos.host, err = w.ensureVirtualHost(client, infos.port, infos.host); err != nil {
|
||||
log.Error("Failed to decide on faucet host", "err", err)
|
||||
return
|
||||
}
|
||||
// Port and proxy settings retrieved, figure out the funding amount per period configurations
|
||||
fmt.Println()
|
||||
fmt.Printf("How many Ethers to release per request? (default = %d)\n", infos.amount)
|
||||
infos.amount = w.readDefaultInt(infos.amount)
|
||||
|
||||
fmt.Println()
|
||||
fmt.Printf("How many minutes to enforce between requests? (default = %d)\n", infos.minutes)
|
||||
infos.minutes = w.readDefaultInt(infos.minutes)
|
||||
|
||||
fmt.Println()
|
||||
fmt.Printf("How many funding tiers to feature (x2.5 amounts, x3 timeout)? (default = %d)\n", infos.tiers)
|
||||
infos.tiers = w.readDefaultInt(infos.tiers)
|
||||
if infos.tiers == 0 {
|
||||
log.Error("At least one funding tier must be set")
|
||||
return
|
||||
}
|
||||
// Accessing the reCaptcha service requires API authorizations, request it
|
||||
if infos.captchaToken != "" {
|
||||
fmt.Println()
|
||||
fmt.Println("Reuse previous reCaptcha API authorization (y/n)? (default = yes)")
|
||||
if w.readDefaultString("y") != "y" {
|
||||
infos.captchaToken, infos.captchaSecret = "", ""
|
||||
}
|
||||
}
|
||||
if infos.captchaToken == "" {
|
||||
// No previous authorization (or old one discarded)
|
||||
fmt.Println()
|
||||
fmt.Println("Enable reCaptcha protection against robots (y/n)? (default = no)")
|
||||
if w.readDefaultString("n") == "n" {
|
||||
log.Warn("Users will be able to requests funds via automated scripts")
|
||||
} else {
|
||||
// Captcha protection explicitly requested, read the site and secret keys
|
||||
fmt.Println()
|
||||
fmt.Printf("What is the reCaptcha site key to authenticate human users?\n")
|
||||
infos.captchaToken = w.readString()
|
||||
|
||||
fmt.Println()
|
||||
fmt.Printf("What is the reCaptcha secret key to verify authentications? (won't be echoed)\n")
|
||||
infos.captchaSecret = w.readPassword()
|
||||
}
|
||||
}
|
||||
// Figure out where the user wants to store the persistent data
|
||||
fmt.Println()
|
||||
if infos.node.datadir == "" {
|
||||
fmt.Printf("Where should data be stored on the remote machine?\n")
|
||||
infos.node.datadir = w.readString()
|
||||
} else {
|
||||
fmt.Printf("Where should data be stored on the remote machine? (default = %s)\n", infos.node.datadir)
|
||||
infos.node.datadir = w.readDefaultString(infos.node.datadir)
|
||||
}
|
||||
// Figure out which port to listen on
|
||||
fmt.Println()
|
||||
fmt.Printf("Which TCP/UDP port should the light client listen on? (default = %d)\n", infos.node.portFull)
|
||||
infos.node.portFull = w.readDefaultInt(infos.node.portFull)
|
||||
|
||||
// Set a proper name to report on the stats page
|
||||
fmt.Println()
|
||||
if infos.node.ethstats == "" {
|
||||
fmt.Printf("What should the node be called on the stats page?\n")
|
||||
infos.node.ethstats = w.readString() + ":" + w.conf.ethstats
|
||||
} else {
|
||||
fmt.Printf("What should the node be called on the stats page? (default = %s)\n", infos.node.ethstats)
|
||||
infos.node.ethstats = w.readDefaultString(infos.node.ethstats) + ":" + w.conf.ethstats
|
||||
}
|
||||
// Load up the credential needed to release funds
|
||||
if infos.node.keyJSON != "" {
|
||||
if key, err := keystore.DecryptKey([]byte(infos.node.keyJSON), infos.node.keyPass); err != nil {
|
||||
infos.node.keyJSON, infos.node.keyPass = "", ""
|
||||
} else {
|
||||
fmt.Println()
|
||||
fmt.Printf("Reuse previous (%s) funding account (y/n)? (default = yes)\n", key.Address.Hex())
|
||||
if w.readDefaultString("y") != "y" {
|
||||
infos.node.keyJSON, infos.node.keyPass = "", ""
|
||||
}
|
||||
}
|
||||
}
|
||||
for i := 0; i < 3 && infos.node.keyJSON == ""; i++ {
|
||||
fmt.Println()
|
||||
fmt.Println("Please paste the faucet's funding account key JSON:")
|
||||
infos.node.keyJSON = w.readJSON()
|
||||
|
||||
fmt.Println()
|
||||
fmt.Println("What's the unlock password for the account? (won't be echoed)")
|
||||
infos.node.keyPass = w.readPassword()
|
||||
|
||||
if _, err := keystore.DecryptKey([]byte(infos.node.keyJSON), infos.node.keyPass); err != nil {
|
||||
log.Error("Failed to decrypt key with given passphrase")
|
||||
infos.node.keyJSON = ""
|
||||
infos.node.keyPass = ""
|
||||
}
|
||||
}
|
||||
// Check if the user wants to run the faucet in debug mode (noauth)
|
||||
noauth := "n"
|
||||
if infos.noauth {
|
||||
noauth = "y"
|
||||
}
|
||||
fmt.Println()
|
||||
fmt.Printf("Permit non-authenticated funding requests (y/n)? (default = %v)\n", infos.noauth)
|
||||
infos.noauth = w.readDefaultString(noauth) != "n"
|
||||
|
||||
// Try to deploy the faucet server on the host
|
||||
nocache := false
|
||||
if existed {
|
||||
fmt.Println()
|
||||
fmt.Printf("Should the faucet be built from scratch (y/n)? (default = no)\n")
|
||||
nocache = w.readDefaultString("n") != "n"
|
||||
}
|
||||
if out, err := deployFaucet(client, w.network, w.conf.bootLight, infos, nocache); err != nil {
|
||||
log.Error("Failed to deploy faucet container", "err", err)
|
||||
if len(out) > 0 {
|
||||
fmt.Printf("%s\n", out)
|
||||
}
|
||||
return
|
||||
}
|
||||
// All ok, run a network scan to pick any changes up
|
||||
w.networkStats()
|
||||
}
|
||||
193
cmd/puppeth/wizard_genesis.go
Normal file
193
cmd/puppeth/wizard_genesis.go
Normal file
@@ -0,0 +1,193 @@
|
||||
// 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"
|
||||
"io/ioutil"
|
||||
"math/big"
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/params"
|
||||
)
|
||||
|
||||
// makeGenesis creates a new genesis struct based on some user input.
|
||||
func (w *wizard) makeGenesis() {
|
||||
// Construct a default genesis block
|
||||
genesis := &core.Genesis{
|
||||
Timestamp: uint64(time.Now().Unix()),
|
||||
GasLimit: 4700000,
|
||||
Difficulty: big.NewInt(524288),
|
||||
Alloc: make(core.GenesisAlloc),
|
||||
Config: ¶ms.ChainConfig{
|
||||
HomesteadBlock: big.NewInt(1),
|
||||
EIP150Block: big.NewInt(2),
|
||||
EIP155Block: big.NewInt(3),
|
||||
EIP158Block: big.NewInt(3),
|
||||
ByzantiumBlock: big.NewInt(4),
|
||||
},
|
||||
}
|
||||
// Figure out which consensus engine to choose
|
||||
fmt.Println()
|
||||
fmt.Println("Which consensus engine to use? (default = clique)")
|
||||
fmt.Println(" 1. Ethash - proof-of-work")
|
||||
fmt.Println(" 2. Clique - proof-of-authority")
|
||||
|
||||
choice := w.read()
|
||||
switch {
|
||||
case choice == "1":
|
||||
// In case of ethash, we're pretty much done
|
||||
genesis.Config.Ethash = new(params.EthashConfig)
|
||||
genesis.ExtraData = make([]byte, 32)
|
||||
|
||||
case choice == "" || choice == "2":
|
||||
// In the case of clique, configure the consensus parameters
|
||||
genesis.Difficulty = big.NewInt(1)
|
||||
genesis.Config.Clique = ¶ms.CliqueConfig{
|
||||
Period: 15,
|
||||
Epoch: 30000,
|
||||
}
|
||||
fmt.Println()
|
||||
fmt.Println("How many seconds should blocks take? (default = 15)")
|
||||
genesis.Config.Clique.Period = uint64(w.readDefaultInt(15))
|
||||
|
||||
// We also need the initial list of signers
|
||||
fmt.Println()
|
||||
fmt.Println("Which accounts are allowed to seal? (mandatory at least one)")
|
||||
|
||||
var signers []common.Address
|
||||
for {
|
||||
if address := w.readAddress(); address != nil {
|
||||
signers = append(signers, *address)
|
||||
continue
|
||||
}
|
||||
if len(signers) > 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
// Sort the signers and embed into the extra-data section
|
||||
for i := 0; i < len(signers); i++ {
|
||||
for j := i + 1; j < len(signers); j++ {
|
||||
if bytes.Compare(signers[i][:], signers[j][:]) > 0 {
|
||||
signers[i], signers[j] = signers[j], signers[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
genesis.ExtraData = make([]byte, 32+len(signers)*common.AddressLength+65)
|
||||
for i, signer := range signers {
|
||||
copy(genesis.ExtraData[32+i*common.AddressLength:], signer[:])
|
||||
}
|
||||
|
||||
default:
|
||||
log.Crit("Invalid consensus engine choice", "choice", choice)
|
||||
}
|
||||
// Consensus all set, just ask for initial funds and go
|
||||
fmt.Println()
|
||||
fmt.Println("Which accounts should be pre-funded? (advisable at least one)")
|
||||
for {
|
||||
// Read the address of the account to fund
|
||||
if address := w.readAddress(); address != nil {
|
||||
genesis.Alloc[*address] = core.GenesisAccount{
|
||||
Balance: new(big.Int).Lsh(big.NewInt(1), 256-7), // 2^256 / 128 (allow many pre-funds without balance overflows)
|
||||
}
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
// Add a batch of precompile balances to avoid them getting deleted
|
||||
for i := int64(0); i < 256; i++ {
|
||||
genesis.Alloc[common.BigToAddress(big.NewInt(i))] = core.GenesisAccount{Balance: big.NewInt(1)}
|
||||
}
|
||||
// Query the user for some custom extras
|
||||
fmt.Println()
|
||||
fmt.Println("Specify your chain/network ID if you want an explicit one (default = random)")
|
||||
genesis.Config.ChainId = new(big.Int).SetUint64(uint64(w.readDefaultInt(rand.Intn(65536))))
|
||||
|
||||
// All done, store the genesis and flush to disk
|
||||
log.Info("Configured new genesis block")
|
||||
|
||||
w.conf.Genesis = genesis
|
||||
w.conf.flush()
|
||||
}
|
||||
|
||||
// manageGenesis permits the modification of chain configuration parameters in
|
||||
// a genesis config and the export of the entire genesis spec.
|
||||
func (w *wizard) manageGenesis() {
|
||||
// Figure out whether to modify or export the genesis
|
||||
fmt.Println()
|
||||
fmt.Println(" 1. Modify existing fork rules")
|
||||
fmt.Println(" 2. Export genesis configuration")
|
||||
fmt.Println(" 3. Remove genesis configuration")
|
||||
|
||||
choice := w.read()
|
||||
switch {
|
||||
case choice == "1":
|
||||
// Fork rule updating requested, iterate over each fork
|
||||
fmt.Println()
|
||||
fmt.Printf("Which block should Homestead come into effect? (default = %v)\n", w.conf.Genesis.Config.HomesteadBlock)
|
||||
w.conf.Genesis.Config.HomesteadBlock = w.readDefaultBigInt(w.conf.Genesis.Config.HomesteadBlock)
|
||||
|
||||
fmt.Println()
|
||||
fmt.Printf("Which block should EIP150 come into effect? (default = %v)\n", w.conf.Genesis.Config.EIP150Block)
|
||||
w.conf.Genesis.Config.EIP150Block = w.readDefaultBigInt(w.conf.Genesis.Config.EIP150Block)
|
||||
|
||||
fmt.Println()
|
||||
fmt.Printf("Which block should EIP155 come into effect? (default = %v)\n", w.conf.Genesis.Config.EIP155Block)
|
||||
w.conf.Genesis.Config.EIP155Block = w.readDefaultBigInt(w.conf.Genesis.Config.EIP155Block)
|
||||
|
||||
fmt.Println()
|
||||
fmt.Printf("Which block should EIP158 come into effect? (default = %v)\n", w.conf.Genesis.Config.EIP158Block)
|
||||
w.conf.Genesis.Config.EIP158Block = w.readDefaultBigInt(w.conf.Genesis.Config.EIP158Block)
|
||||
|
||||
fmt.Println()
|
||||
fmt.Printf("Which block should Byzantium come into effect? (default = %v)\n", w.conf.Genesis.Config.ByzantiumBlock)
|
||||
w.conf.Genesis.Config.ByzantiumBlock = w.readDefaultBigInt(w.conf.Genesis.Config.ByzantiumBlock)
|
||||
|
||||
out, _ := json.MarshalIndent(w.conf.Genesis.Config, "", " ")
|
||||
fmt.Printf("Chain configuration updated:\n\n%s\n", out)
|
||||
|
||||
case choice == "2":
|
||||
// Save whatever genesis configuration we currently have
|
||||
fmt.Println()
|
||||
fmt.Printf("Which file to save the genesis into? (default = %s.json)\n", w.network)
|
||||
out, _ := json.MarshalIndent(w.conf.Genesis, "", " ")
|
||||
if err := ioutil.WriteFile(w.readDefaultString(fmt.Sprintf("%s.json", w.network)), out, 0644); err != nil {
|
||||
log.Error("Failed to save genesis file", "err", err)
|
||||
}
|
||||
log.Info("Exported existing genesis block")
|
||||
|
||||
case choice == "3":
|
||||
// Make sure we don't have any services running
|
||||
if len(w.conf.servers()) > 0 {
|
||||
log.Error("Genesis reset requires all services and servers torn down")
|
||||
return
|
||||
}
|
||||
log.Info("Genesis block destroyed")
|
||||
|
||||
w.conf.Genesis = nil
|
||||
w.conf.flush()
|
||||
|
||||
default:
|
||||
log.Error("That's not something I can do")
|
||||
}
|
||||
}
|
||||
156
cmd/puppeth/wizard_intro.go
Normal file
156
cmd/puppeth/wizard_intro.go
Normal file
@@ -0,0 +1,156 @@
|
||||
// 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 (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
)
|
||||
|
||||
// makeWizard creates and returns a new puppeth wizard.
|
||||
func makeWizard(network string) *wizard {
|
||||
return &wizard{
|
||||
network: network,
|
||||
conf: config{
|
||||
Servers: make(map[string][]byte),
|
||||
},
|
||||
servers: make(map[string]*sshClient),
|
||||
services: make(map[string][]string),
|
||||
in: bufio.NewReader(os.Stdin),
|
||||
}
|
||||
}
|
||||
|
||||
// run displays some useful infos to the user, starting on the journey of
|
||||
// setting up a new or managing an existing Ethereum private network.
|
||||
func (w *wizard) run() {
|
||||
fmt.Println("+-----------------------------------------------------------+")
|
||||
fmt.Println("| Welcome to puppeth, your Ethereum private network manager |")
|
||||
fmt.Println("| |")
|
||||
fmt.Println("| This tool lets you create a new Ethereum network down to |")
|
||||
fmt.Println("| the genesis block, bootnodes, miners and ethstats servers |")
|
||||
fmt.Println("| without the hassle that it would normally entail. |")
|
||||
fmt.Println("| |")
|
||||
fmt.Println("| Puppeth uses SSH to dial in to remote servers, and builds |")
|
||||
fmt.Println("| its network components out of Docker containers using the |")
|
||||
fmt.Println("| docker-compose toolset. |")
|
||||
fmt.Println("+-----------------------------------------------------------+")
|
||||
fmt.Println()
|
||||
|
||||
// Make sure we have a good network name to work with fmt.Println()
|
||||
if w.network == "" {
|
||||
fmt.Println("Please specify a network name to administer (no spaces, please)")
|
||||
for {
|
||||
w.network = w.readString()
|
||||
if !strings.Contains(w.network, " ") {
|
||||
fmt.Printf("\nSweet, you can set this via --network=%s next time!\n\n", w.network)
|
||||
break
|
||||
}
|
||||
log.Error("I also like to live dangerously, still no spaces")
|
||||
}
|
||||
}
|
||||
log.Info("Administering Ethereum network", "name", w.network)
|
||||
|
||||
// Load initial configurations and connect to all live servers
|
||||
w.conf.path = filepath.Join(os.Getenv("HOME"), ".puppeth", w.network)
|
||||
|
||||
blob, err := ioutil.ReadFile(w.conf.path)
|
||||
if err != nil {
|
||||
log.Warn("No previous configurations found", "path", w.conf.path)
|
||||
} else if err := json.Unmarshal(blob, &w.conf); err != nil {
|
||||
log.Crit("Previous configuration corrupted", "path", w.conf.path, "err", err)
|
||||
} else {
|
||||
// Dial all previously known servers concurrently
|
||||
var pend sync.WaitGroup
|
||||
for server, pubkey := range w.conf.Servers {
|
||||
pend.Add(1)
|
||||
|
||||
go func(server string, pubkey []byte) {
|
||||
defer pend.Done()
|
||||
|
||||
log.Info("Dialing previously configured server", "server", server)
|
||||
client, err := dial(server, pubkey)
|
||||
if err != nil {
|
||||
log.Error("Previous server unreachable", "server", server, "err", err)
|
||||
}
|
||||
w.lock.Lock()
|
||||
w.servers[server] = client
|
||||
w.lock.Unlock()
|
||||
}(server, pubkey)
|
||||
}
|
||||
pend.Wait()
|
||||
w.networkStats()
|
||||
}
|
||||
// Basics done, loop ad infinitum about what to do
|
||||
for {
|
||||
fmt.Println()
|
||||
fmt.Println("What would you like to do? (default = stats)")
|
||||
fmt.Println(" 1. Show network stats")
|
||||
if w.conf.Genesis == nil {
|
||||
fmt.Println(" 2. Configure new genesis")
|
||||
} else {
|
||||
fmt.Println(" 2. Manage existing genesis")
|
||||
}
|
||||
if len(w.servers) == 0 {
|
||||
fmt.Println(" 3. Track new remote server")
|
||||
} else {
|
||||
fmt.Println(" 3. Manage tracked machines")
|
||||
}
|
||||
if len(w.services) == 0 {
|
||||
fmt.Println(" 4. Deploy network components")
|
||||
} else {
|
||||
fmt.Println(" 4. Manage network components")
|
||||
}
|
||||
|
||||
choice := w.read()
|
||||
switch {
|
||||
case choice == "" || choice == "1":
|
||||
w.networkStats()
|
||||
|
||||
case choice == "2":
|
||||
if w.conf.Genesis == nil {
|
||||
w.makeGenesis()
|
||||
} else {
|
||||
w.manageGenesis()
|
||||
}
|
||||
case choice == "3":
|
||||
if len(w.servers) == 0 {
|
||||
if w.makeServer() != "" {
|
||||
w.networkStats()
|
||||
}
|
||||
} else {
|
||||
w.manageServers()
|
||||
}
|
||||
case choice == "4":
|
||||
if len(w.services) == 0 {
|
||||
w.deployComponent()
|
||||
} else {
|
||||
w.manageComponents()
|
||||
}
|
||||
|
||||
default:
|
||||
log.Error("That's not something I can do")
|
||||
}
|
||||
}
|
||||
}
|
||||
294
cmd/puppeth/wizard_netstats.go
Normal file
294
cmd/puppeth/wizard_netstats.go
Normal file
@@ -0,0 +1,294 @@
|
||||
// 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 (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/ethereum/go-ethereum/core"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
)
|
||||
|
||||
// networkStats verifies the status of network components and generates a protip
|
||||
// configuration set to give users hints on how to do various tasks.
|
||||
func (w *wizard) networkStats() {
|
||||
if len(w.servers) == 0 {
|
||||
log.Info("No remote machines to gather stats from")
|
||||
return
|
||||
}
|
||||
// Clear out some previous configs to refill from current scan
|
||||
w.conf.ethstats = ""
|
||||
w.conf.bootFull = w.conf.bootFull[:0]
|
||||
w.conf.bootLight = w.conf.bootLight[:0]
|
||||
|
||||
// Iterate over all the specified hosts and check their status
|
||||
var pend sync.WaitGroup
|
||||
|
||||
stats := make(serverStats)
|
||||
for server, pubkey := range w.conf.Servers {
|
||||
pend.Add(1)
|
||||
|
||||
// Gather the service stats for each server concurrently
|
||||
go func(server string, pubkey []byte) {
|
||||
defer pend.Done()
|
||||
|
||||
stat := w.gatherStats(server, pubkey, w.servers[server])
|
||||
|
||||
// All status checks complete, report and check next server
|
||||
w.lock.Lock()
|
||||
defer w.lock.Unlock()
|
||||
|
||||
delete(w.services, server)
|
||||
for service := range stat.services {
|
||||
w.services[server] = append(w.services[server], service)
|
||||
}
|
||||
stats[server] = stat
|
||||
}(server, pubkey)
|
||||
}
|
||||
pend.Wait()
|
||||
|
||||
// Print any collected stats and return
|
||||
stats.render()
|
||||
}
|
||||
|
||||
// gatherStats gathers service statistics for a particular remote server.
|
||||
func (w *wizard) gatherStats(server string, pubkey []byte, client *sshClient) *serverStat {
|
||||
// Gather some global stats to feed into the wizard
|
||||
var (
|
||||
genesis string
|
||||
ethstats string
|
||||
bootFull []string
|
||||
bootLight []string
|
||||
)
|
||||
// Ensure a valid SSH connection to the remote server
|
||||
logger := log.New("server", server)
|
||||
logger.Info("Starting remote server health-check")
|
||||
|
||||
stat := &serverStat{
|
||||
address: client.address,
|
||||
services: make(map[string]map[string]string),
|
||||
}
|
||||
if client == nil {
|
||||
conn, err := dial(server, pubkey)
|
||||
if err != nil {
|
||||
logger.Error("Failed to establish remote connection", "err", err)
|
||||
stat.failure = err.Error()
|
||||
return stat
|
||||
}
|
||||
client = conn
|
||||
}
|
||||
// Client connected one way or another, run health-checks
|
||||
logger.Debug("Checking for nginx availability")
|
||||
if infos, err := checkNginx(client, w.network); err != nil {
|
||||
if err != ErrServiceUnknown {
|
||||
stat.services["nginx"] = map[string]string{"offline": err.Error()}
|
||||
}
|
||||
} else {
|
||||
stat.services["nginx"] = infos.Report()
|
||||
}
|
||||
logger.Debug("Checking for ethstats availability")
|
||||
if infos, err := checkEthstats(client, w.network); err != nil {
|
||||
if err != ErrServiceUnknown {
|
||||
stat.services["ethstats"] = map[string]string{"offline": err.Error()}
|
||||
}
|
||||
} else {
|
||||
stat.services["ethstats"] = infos.Report()
|
||||
ethstats = infos.config
|
||||
}
|
||||
logger.Debug("Checking for bootnode availability")
|
||||
if infos, err := checkNode(client, w.network, true); err != nil {
|
||||
if err != ErrServiceUnknown {
|
||||
stat.services["bootnode"] = map[string]string{"offline": err.Error()}
|
||||
}
|
||||
} else {
|
||||
stat.services["bootnode"] = infos.Report()
|
||||
|
||||
genesis = string(infos.genesis)
|
||||
bootFull = append(bootFull, infos.enodeFull)
|
||||
if infos.enodeLight != "" {
|
||||
bootLight = append(bootLight, infos.enodeLight)
|
||||
}
|
||||
}
|
||||
logger.Debug("Checking for sealnode availability")
|
||||
if infos, err := checkNode(client, w.network, false); err != nil {
|
||||
if err != ErrServiceUnknown {
|
||||
stat.services["sealnode"] = map[string]string{"offline": err.Error()}
|
||||
}
|
||||
} else {
|
||||
stat.services["sealnode"] = infos.Report()
|
||||
genesis = string(infos.genesis)
|
||||
}
|
||||
logger.Debug("Checking for explorer availability")
|
||||
if infos, err := checkExplorer(client, w.network); err != nil {
|
||||
if err != ErrServiceUnknown {
|
||||
stat.services["explorer"] = map[string]string{"offline": err.Error()}
|
||||
}
|
||||
} else {
|
||||
stat.services["explorer"] = infos.Report()
|
||||
}
|
||||
logger.Debug("Checking for wallet availability")
|
||||
if infos, err := checkWallet(client, w.network); err != nil {
|
||||
if err != ErrServiceUnknown {
|
||||
stat.services["wallet"] = map[string]string{"offline": err.Error()}
|
||||
}
|
||||
} else {
|
||||
stat.services["wallet"] = infos.Report()
|
||||
}
|
||||
logger.Debug("Checking for faucet availability")
|
||||
if infos, err := checkFaucet(client, w.network); err != nil {
|
||||
if err != ErrServiceUnknown {
|
||||
stat.services["faucet"] = map[string]string{"offline": err.Error()}
|
||||
}
|
||||
} else {
|
||||
stat.services["faucet"] = infos.Report()
|
||||
}
|
||||
logger.Debug("Checking for dashboard availability")
|
||||
if infos, err := checkDashboard(client, w.network); err != nil {
|
||||
if err != ErrServiceUnknown {
|
||||
stat.services["dashboard"] = map[string]string{"offline": err.Error()}
|
||||
}
|
||||
} else {
|
||||
stat.services["dashboard"] = infos.Report()
|
||||
}
|
||||
// Feed and newly discovered information into the wizard
|
||||
w.lock.Lock()
|
||||
defer w.lock.Unlock()
|
||||
|
||||
if genesis != "" && w.conf.Genesis == nil {
|
||||
g := new(core.Genesis)
|
||||
if err := json.Unmarshal([]byte(genesis), g); err != nil {
|
||||
log.Error("Failed to parse remote genesis", "err", err)
|
||||
} else {
|
||||
w.conf.Genesis = g
|
||||
}
|
||||
}
|
||||
if ethstats != "" {
|
||||
w.conf.ethstats = ethstats
|
||||
}
|
||||
w.conf.bootFull = append(w.conf.bootFull, bootFull...)
|
||||
w.conf.bootLight = append(w.conf.bootLight, bootLight...)
|
||||
|
||||
return stat
|
||||
}
|
||||
|
||||
// serverStat is a collection of service configuration parameters and health
|
||||
// check reports to print to the user.
|
||||
type serverStat struct {
|
||||
address string
|
||||
failure string
|
||||
services map[string]map[string]string
|
||||
}
|
||||
|
||||
// serverStats is a collection of server stats for multiple hosts.
|
||||
type serverStats map[string]*serverStat
|
||||
|
||||
// render converts the gathered statistics into a user friendly tabular report
|
||||
// and prints it to the standard output.
|
||||
func (stats serverStats) render() {
|
||||
// Start gathering service statistics and config parameters
|
||||
table := tablewriter.NewWriter(os.Stdout)
|
||||
|
||||
table.SetHeader([]string{"Server", "Address", "Service", "Config", "Value"})
|
||||
table.SetAlignment(tablewriter.ALIGN_LEFT)
|
||||
table.SetColWidth(100)
|
||||
|
||||
// Find the longest lines for all columns for the hacked separator
|
||||
separator := make([]string, 5)
|
||||
for server, stat := range stats {
|
||||
if len(server) > len(separator[0]) {
|
||||
separator[0] = strings.Repeat("-", len(server))
|
||||
}
|
||||
if len(stat.address) > len(separator[1]) {
|
||||
separator[1] = strings.Repeat("-", len(stat.address))
|
||||
}
|
||||
for service, configs := range stat.services {
|
||||
if len(service) > len(separator[2]) {
|
||||
separator[2] = strings.Repeat("-", len(service))
|
||||
}
|
||||
for config, value := range configs {
|
||||
if len(config) > len(separator[3]) {
|
||||
separator[3] = strings.Repeat("-", len(config))
|
||||
}
|
||||
if len(value) > len(separator[4]) {
|
||||
separator[4] = strings.Repeat("-", len(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Fill up the server report in alphabetical order
|
||||
servers := make([]string, 0, len(stats))
|
||||
for server := range stats {
|
||||
servers = append(servers, server)
|
||||
}
|
||||
sort.Strings(servers)
|
||||
|
||||
for i, server := range servers {
|
||||
// Add a separator between all servers
|
||||
if i > 0 {
|
||||
table.Append(separator)
|
||||
}
|
||||
// Fill up the service report in alphabetical order
|
||||
services := make([]string, 0, len(stats[server].services))
|
||||
for service := range stats[server].services {
|
||||
services = append(services, service)
|
||||
}
|
||||
sort.Strings(services)
|
||||
|
||||
if len(services) == 0 {
|
||||
table.Append([]string{server, stats[server].address, "", "", ""})
|
||||
}
|
||||
for j, service := range services {
|
||||
// Add an empty line between all services
|
||||
if j > 0 {
|
||||
table.Append([]string{"", "", "", separator[3], separator[4]})
|
||||
}
|
||||
// Fill up the config report in alphabetical order
|
||||
configs := make([]string, 0, len(stats[server].services[service]))
|
||||
for service := range stats[server].services[service] {
|
||||
configs = append(configs, service)
|
||||
}
|
||||
sort.Strings(configs)
|
||||
|
||||
for k, config := range configs {
|
||||
switch {
|
||||
case j == 0 && k == 0:
|
||||
table.Append([]string{server, stats[server].address, service, config, stats[server].services[service][config]})
|
||||
case k == 0:
|
||||
table.Append([]string{"", "", service, config, stats[server].services[service][config]})
|
||||
default:
|
||||
table.Append([]string{"", "", "", config, stats[server].services[service][config]})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
table.Render()
|
||||
}
|
||||
|
||||
// protips contains a collection of network infos to report pro-tips
|
||||
// based on.
|
||||
type protips struct {
|
||||
genesis string
|
||||
network int64
|
||||
bootFull []string
|
||||
bootLight []string
|
||||
ethstats string
|
||||
}
|
||||
200
cmd/puppeth/wizard_network.go
Normal file
200
cmd/puppeth/wizard_network.go
Normal file
@@ -0,0 +1,200 @@
|
||||
// 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 (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
)
|
||||
|
||||
// manageServers displays a list of servers the user can disconnect from, and an
|
||||
// option to connect to new servers.
|
||||
func (w *wizard) manageServers() {
|
||||
// List all the servers we can disconnect, along with an entry to connect a new one
|
||||
fmt.Println()
|
||||
|
||||
servers := w.conf.servers()
|
||||
for i, server := range servers {
|
||||
fmt.Printf(" %d. Disconnect %s\n", i+1, server)
|
||||
}
|
||||
fmt.Printf(" %d. Connect another server\n", len(w.conf.Servers)+1)
|
||||
|
||||
choice := w.readInt()
|
||||
if choice < 0 || choice > len(w.conf.Servers)+1 {
|
||||
log.Error("Invalid server choice, aborting")
|
||||
return
|
||||
}
|
||||
// If the user selected an existing server, drop it
|
||||
if choice <= len(w.conf.Servers) {
|
||||
server := servers[choice-1]
|
||||
client := w.servers[server]
|
||||
|
||||
delete(w.servers, server)
|
||||
if client != nil {
|
||||
client.Close()
|
||||
}
|
||||
delete(w.conf.Servers, server)
|
||||
w.conf.flush()
|
||||
|
||||
log.Info("Disconnected existing server", "server", server)
|
||||
w.networkStats()
|
||||
return
|
||||
}
|
||||
// If the user requested connecting a new server, do it
|
||||
if w.makeServer() != "" {
|
||||
w.networkStats()
|
||||
}
|
||||
}
|
||||
|
||||
// makeServer reads a single line from stdin and interprets it as a hostname to
|
||||
// connect to. It tries to establish a new SSH session and also executing some
|
||||
// baseline validations.
|
||||
//
|
||||
// If connection succeeds, the server is added to the wizards configs!
|
||||
func (w *wizard) makeServer() string {
|
||||
fmt.Println()
|
||||
fmt.Println("Please enter remote server's address:")
|
||||
|
||||
// Read and dial the server to ensure docker is present
|
||||
input := w.readString()
|
||||
|
||||
client, err := dial(input, nil)
|
||||
if err != nil {
|
||||
log.Error("Server not ready for puppeth", "err", err)
|
||||
return ""
|
||||
}
|
||||
// All checks passed, start tracking the server
|
||||
w.servers[input] = client
|
||||
w.conf.Servers[input] = client.pubkey
|
||||
w.conf.flush()
|
||||
|
||||
return input
|
||||
}
|
||||
|
||||
// selectServer lists the user all the currnetly known servers to choose from,
|
||||
// also granting the option to add a new one.
|
||||
func (w *wizard) selectServer() string {
|
||||
// List the available server to the user and wait for a choice
|
||||
fmt.Println()
|
||||
fmt.Println("Which server do you want to interact with?")
|
||||
|
||||
servers := w.conf.servers()
|
||||
for i, server := range servers {
|
||||
fmt.Printf(" %d. %s\n", i+1, server)
|
||||
}
|
||||
fmt.Printf(" %d. Connect another server\n", len(w.conf.Servers)+1)
|
||||
|
||||
choice := w.readInt()
|
||||
if choice < 0 || choice > len(w.conf.Servers)+1 {
|
||||
log.Error("Invalid server choice, aborting")
|
||||
return ""
|
||||
}
|
||||
// If the user requested connecting to a new server, go for it
|
||||
if choice <= len(w.conf.Servers) {
|
||||
return servers[choice-1]
|
||||
}
|
||||
return w.makeServer()
|
||||
}
|
||||
|
||||
// manageComponents displays a list of network components the user can tear down
|
||||
// and an option
|
||||
func (w *wizard) manageComponents() {
|
||||
// List all the componens we can tear down, along with an entry to deploy a new one
|
||||
fmt.Println()
|
||||
|
||||
var serviceHosts, serviceNames []string
|
||||
for server, services := range w.services {
|
||||
for _, service := range services {
|
||||
serviceHosts = append(serviceHosts, server)
|
||||
serviceNames = append(serviceNames, service)
|
||||
|
||||
fmt.Printf(" %d. Tear down %s on %s\n", len(serviceHosts), strings.Title(service), server)
|
||||
}
|
||||
}
|
||||
fmt.Printf(" %d. Deploy new network component\n", len(serviceHosts)+1)
|
||||
|
||||
choice := w.readInt()
|
||||
if choice < 0 || choice > len(serviceHosts)+1 {
|
||||
log.Error("Invalid component choice, aborting")
|
||||
return
|
||||
}
|
||||
// If the user selected an existing service, destroy it
|
||||
if choice <= len(serviceHosts) {
|
||||
// Figure out the service to destroy and execute it
|
||||
service := serviceNames[choice-1]
|
||||
server := serviceHosts[choice-1]
|
||||
client := w.servers[server]
|
||||
|
||||
if out, err := tearDown(client, w.network, service, true); err != nil {
|
||||
log.Error("Failed to tear down component", "err", err)
|
||||
if len(out) > 0 {
|
||||
fmt.Printf("%s\n", out)
|
||||
}
|
||||
return
|
||||
}
|
||||
// Clean up any references to it from out state
|
||||
services := w.services[server]
|
||||
for i, name := range services {
|
||||
if name == service {
|
||||
w.services[server] = append(services[:i], services[i+1:]...)
|
||||
if len(w.services[server]) == 0 {
|
||||
delete(w.services, server)
|
||||
}
|
||||
}
|
||||
}
|
||||
log.Info("Torn down existing component", "server", server, "service", service)
|
||||
return
|
||||
}
|
||||
// If the user requested deploying a new component, do it
|
||||
w.deployComponent()
|
||||
}
|
||||
|
||||
// deployComponent displays a list of network components the user can deploy and
|
||||
// guides through the process.
|
||||
func (w *wizard) deployComponent() {
|
||||
// Print all the things we can deploy and wait or user choice
|
||||
fmt.Println()
|
||||
fmt.Println("What would you like to deploy? (recommended order)")
|
||||
fmt.Println(" 1. Ethstats - Network monitoring tool")
|
||||
fmt.Println(" 2. Bootnode - Entry point of the network")
|
||||
fmt.Println(" 3. Sealer - Full node minting new blocks")
|
||||
fmt.Println(" 4. Explorer - Chain analysis webservice (ethash only)")
|
||||
fmt.Println(" 5. Wallet - Browser wallet for quick sends")
|
||||
fmt.Println(" 6. Faucet - Crypto faucet to give away funds")
|
||||
fmt.Println(" 7. Dashboard - Website listing above web-services")
|
||||
|
||||
switch w.read() {
|
||||
case "1":
|
||||
w.deployEthstats()
|
||||
case "2":
|
||||
w.deployNode(true)
|
||||
case "3":
|
||||
w.deployNode(false)
|
||||
case "4":
|
||||
w.deployExplorer()
|
||||
case "5":
|
||||
w.deployWallet()
|
||||
case "6":
|
||||
w.deployFaucet()
|
||||
case "7":
|
||||
w.deployDashboard()
|
||||
default:
|
||||
log.Error("That's not something I can do")
|
||||
}
|
||||
}
|
||||
65
cmd/puppeth/wizard_nginx.go
Normal file
65
cmd/puppeth/wizard_nginx.go
Normal file
@@ -0,0 +1,65 @@
|
||||
// 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 (
|
||||
"fmt"
|
||||
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
)
|
||||
|
||||
// ensureVirtualHost checks whether a reverse-proxy is running on the specified
|
||||
// host machine, and if yes requests a virtual host from the user to host a
|
||||
// specific web service on. If no proxy exists, the method will offer to deploy
|
||||
// one.
|
||||
//
|
||||
// If the user elects not to use a reverse proxy, an empty hostname is returned!
|
||||
func (w *wizard) ensureVirtualHost(client *sshClient, port int, def string) (string, error) {
|
||||
proxy, _ := checkNginx(client, w.network)
|
||||
if proxy != nil {
|
||||
// Reverse proxy is running, if ports match, we need a virtual host
|
||||
if proxy.port == port {
|
||||
fmt.Println()
|
||||
fmt.Printf("Shared port, which domain to assign? (default = %s)\n", def)
|
||||
return w.readDefaultString(def), nil
|
||||
}
|
||||
}
|
||||
// Reverse proxy is not running, offer to deploy a new one
|
||||
fmt.Println()
|
||||
fmt.Println("Allow sharing the port with other services (y/n)? (default = yes)")
|
||||
if w.readDefaultString("y") == "y" {
|
||||
nocache := false
|
||||
if proxy != nil {
|
||||
fmt.Println()
|
||||
fmt.Printf("Should the reverse-proxy be rebuilt from scratch (y/n)? (default = no)\n")
|
||||
nocache = w.readDefaultString("n") != "n"
|
||||
}
|
||||
if out, err := deployNginx(client, w.network, port, nocache); err != nil {
|
||||
log.Error("Failed to deploy reverse-proxy", "err", err)
|
||||
if len(out) > 0 {
|
||||
fmt.Printf("%s\n", out)
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
// Reverse proxy deployed, ask again for the virtual-host
|
||||
fmt.Println()
|
||||
fmt.Printf("Proxy deployed, which domain to assign? (default = %s)\n", def)
|
||||
return w.readDefaultString(def), nil
|
||||
}
|
||||
// Reverse proxy not requested, deploy as a standalone service
|
||||
return "", nil
|
||||
}
|
||||
178
cmd/puppeth/wizard_node.go
Normal file
178
cmd/puppeth/wizard_node.go
Normal file
@@ -0,0 +1,178 @@
|
||||
// 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 (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts/keystore"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
)
|
||||
|
||||
// deployNode creates a new node configuration based on some user input.
|
||||
func (w *wizard) deployNode(boot bool) {
|
||||
// Do some sanity check before the user wastes time on input
|
||||
if w.conf.Genesis == nil {
|
||||
log.Error("No genesis block configured")
|
||||
return
|
||||
}
|
||||
if w.conf.ethstats == "" {
|
||||
log.Error("No ethstats server configured")
|
||||
return
|
||||
}
|
||||
// Select the server to interact with
|
||||
server := w.selectServer()
|
||||
if server == "" {
|
||||
return
|
||||
}
|
||||
client := w.servers[server]
|
||||
|
||||
// Retrieve any active node configurations from the server
|
||||
infos, err := checkNode(client, w.network, boot)
|
||||
if err != nil {
|
||||
if boot {
|
||||
infos = &nodeInfos{portFull: 30303, peersTotal: 512, peersLight: 256}
|
||||
} else {
|
||||
infos = &nodeInfos{portFull: 30303, peersTotal: 50, peersLight: 0, gasTarget: 4.7, gasPrice: 18}
|
||||
}
|
||||
}
|
||||
existed := err == nil
|
||||
|
||||
infos.genesis, _ = json.MarshalIndent(w.conf.Genesis, "", " ")
|
||||
infos.network = w.conf.Genesis.Config.ChainId.Int64()
|
||||
|
||||
// Figure out where the user wants to store the persistent data
|
||||
fmt.Println()
|
||||
if infos.datadir == "" {
|
||||
fmt.Printf("Where should data be stored on the remote machine?\n")
|
||||
infos.datadir = w.readString()
|
||||
} else {
|
||||
fmt.Printf("Where should data be stored on the remote machine? (default = %s)\n", infos.datadir)
|
||||
infos.datadir = w.readDefaultString(infos.datadir)
|
||||
}
|
||||
if w.conf.Genesis.Config.Ethash != nil && !boot {
|
||||
fmt.Println()
|
||||
if infos.ethashdir == "" {
|
||||
fmt.Printf("Where should the ethash mining DAGs be stored on the remote machine?\n")
|
||||
infos.ethashdir = w.readString()
|
||||
} else {
|
||||
fmt.Printf("Where should the ethash mining DAGs be stored on the remote machine? (default = %s)\n", infos.ethashdir)
|
||||
infos.ethashdir = w.readDefaultString(infos.ethashdir)
|
||||
}
|
||||
}
|
||||
// Figure out which port to listen on
|
||||
fmt.Println()
|
||||
fmt.Printf("Which TCP/UDP port to listen on? (default = %d)\n", infos.portFull)
|
||||
infos.portFull = w.readDefaultInt(infos.portFull)
|
||||
|
||||
// Figure out how many peers to allow (different based on node type)
|
||||
fmt.Println()
|
||||
fmt.Printf("How many peers to allow connecting? (default = %d)\n", infos.peersTotal)
|
||||
infos.peersTotal = w.readDefaultInt(infos.peersTotal)
|
||||
|
||||
// Figure out how many light peers to allow (different based on node type)
|
||||
fmt.Println()
|
||||
fmt.Printf("How many light peers to allow connecting? (default = %d)\n", infos.peersLight)
|
||||
infos.peersLight = w.readDefaultInt(infos.peersLight)
|
||||
|
||||
// Set a proper name to report on the stats page
|
||||
fmt.Println()
|
||||
if infos.ethstats == "" {
|
||||
fmt.Printf("What should the node be called on the stats page?\n")
|
||||
infos.ethstats = w.readString() + ":" + w.conf.ethstats
|
||||
} else {
|
||||
fmt.Printf("What should the node be called on the stats page? (default = %s)\n", infos.ethstats)
|
||||
infos.ethstats = w.readDefaultString(infos.ethstats) + ":" + w.conf.ethstats
|
||||
}
|
||||
// If the node is a miner/signer, load up needed credentials
|
||||
if !boot {
|
||||
if w.conf.Genesis.Config.Ethash != nil {
|
||||
// Ethash based miners only need an etherbase to mine against
|
||||
fmt.Println()
|
||||
if infos.etherbase == "" {
|
||||
fmt.Printf("What address should the miner user?\n")
|
||||
for {
|
||||
if address := w.readAddress(); address != nil {
|
||||
infos.etherbase = address.Hex()
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
fmt.Printf("What address should the miner user? (default = %s)\n", infos.etherbase)
|
||||
infos.etherbase = w.readDefaultAddress(common.HexToAddress(infos.etherbase)).Hex()
|
||||
}
|
||||
} else if w.conf.Genesis.Config.Clique != nil {
|
||||
// If a previous signer was already set, offer to reuse it
|
||||
if infos.keyJSON != "" {
|
||||
if key, err := keystore.DecryptKey([]byte(infos.keyJSON), infos.keyPass); err != nil {
|
||||
infos.keyJSON, infos.keyPass = "", ""
|
||||
} else {
|
||||
fmt.Println()
|
||||
fmt.Printf("Reuse previous (%s) signing account (y/n)? (default = yes)\n", key.Address.Hex())
|
||||
if w.readDefaultString("y") != "y" {
|
||||
infos.keyJSON, infos.keyPass = "", ""
|
||||
}
|
||||
}
|
||||
}
|
||||
// Clique based signers need a keyfile and unlock password, ask if unavailable
|
||||
if infos.keyJSON == "" {
|
||||
fmt.Println()
|
||||
fmt.Println("Please paste the signer's key JSON:")
|
||||
infos.keyJSON = w.readJSON()
|
||||
|
||||
fmt.Println()
|
||||
fmt.Println("What's the unlock password for the account? (won't be echoed)")
|
||||
infos.keyPass = w.readPassword()
|
||||
|
||||
if _, err := keystore.DecryptKey([]byte(infos.keyJSON), infos.keyPass); err != nil {
|
||||
log.Error("Failed to decrypt key with given passphrase")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
// Establish the gas dynamics to be enforced by the signer
|
||||
fmt.Println()
|
||||
fmt.Printf("What gas limit should empty blocks target (MGas)? (default = %0.3f)\n", infos.gasTarget)
|
||||
infos.gasTarget = w.readDefaultFloat(infos.gasTarget)
|
||||
|
||||
fmt.Println()
|
||||
fmt.Printf("What gas price should the signer require (GWei)? (default = %0.3f)\n", infos.gasPrice)
|
||||
infos.gasPrice = w.readDefaultFloat(infos.gasPrice)
|
||||
}
|
||||
// Try to deploy the full node on the host
|
||||
nocache := false
|
||||
if existed {
|
||||
fmt.Println()
|
||||
fmt.Printf("Should the node be built from scratch (y/n)? (default = no)\n")
|
||||
nocache = w.readDefaultString("n") != "n"
|
||||
}
|
||||
if out, err := deployNode(client, w.network, w.conf.bootFull, w.conf.bootLight, infos, nocache); err != nil {
|
||||
log.Error("Failed to deploy Ethereum node container", "err", err)
|
||||
if len(out) > 0 {
|
||||
fmt.Printf("%s\n", out)
|
||||
}
|
||||
return
|
||||
}
|
||||
// All ok, run a network scan to pick any changes up
|
||||
log.Info("Waiting for node to finish booting")
|
||||
time.Sleep(3 * time.Second)
|
||||
|
||||
w.networkStats()
|
||||
}
|
||||
113
cmd/puppeth/wizard_wallet.go
Normal file
113
cmd/puppeth/wizard_wallet.go
Normal file
@@ -0,0 +1,113 @@
|
||||
// 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 (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
)
|
||||
|
||||
// deployWallet creates a new web wallet based on some user input.
|
||||
func (w *wizard) deployWallet() {
|
||||
// Do some sanity check before the user wastes time on input
|
||||
if w.conf.Genesis == nil {
|
||||
log.Error("No genesis block configured")
|
||||
return
|
||||
}
|
||||
if w.conf.ethstats == "" {
|
||||
log.Error("No ethstats server configured")
|
||||
return
|
||||
}
|
||||
// Select the server to interact with
|
||||
server := w.selectServer()
|
||||
if server == "" {
|
||||
return
|
||||
}
|
||||
client := w.servers[server]
|
||||
|
||||
// Retrieve any active node configurations from the server
|
||||
infos, err := checkWallet(client, w.network)
|
||||
if err != nil {
|
||||
infos = &walletInfos{
|
||||
nodePort: 30303, rpcPort: 8545, webPort: 80, webHost: client.server,
|
||||
}
|
||||
}
|
||||
existed := err == nil
|
||||
|
||||
infos.genesis, _ = json.MarshalIndent(w.conf.Genesis, "", " ")
|
||||
infos.network = w.conf.Genesis.Config.ChainId.Int64()
|
||||
|
||||
// Figure out which port to listen on
|
||||
fmt.Println()
|
||||
fmt.Printf("Which port should the wallet listen on? (default = %d)\n", infos.webPort)
|
||||
infos.webPort = w.readDefaultInt(infos.webPort)
|
||||
|
||||
// Figure which virtual-host to deploy ethstats on
|
||||
if infos.webHost, err = w.ensureVirtualHost(client, infos.webPort, infos.webHost); err != nil {
|
||||
log.Error("Failed to decide on wallet host", "err", err)
|
||||
return
|
||||
}
|
||||
// Figure out where the user wants to store the persistent data
|
||||
fmt.Println()
|
||||
if infos.datadir == "" {
|
||||
fmt.Printf("Where should data be stored on the remote machine?\n")
|
||||
infos.datadir = w.readString()
|
||||
} else {
|
||||
fmt.Printf("Where should data be stored on the remote machine? (default = %s)\n", infos.datadir)
|
||||
infos.datadir = w.readDefaultString(infos.datadir)
|
||||
}
|
||||
// Figure out which port to listen on
|
||||
fmt.Println()
|
||||
fmt.Printf("Which TCP/UDP port should the backing node listen on? (default = %d)\n", infos.nodePort)
|
||||
infos.nodePort = w.readDefaultInt(infos.nodePort)
|
||||
|
||||
fmt.Println()
|
||||
fmt.Printf("Which port should the backing RPC API listen on? (default = %d)\n", infos.rpcPort)
|
||||
infos.rpcPort = w.readDefaultInt(infos.rpcPort)
|
||||
|
||||
// Set a proper name to report on the stats page
|
||||
fmt.Println()
|
||||
if infos.ethstats == "" {
|
||||
fmt.Printf("What should the wallet be called on the stats page?\n")
|
||||
infos.ethstats = w.readString() + ":" + w.conf.ethstats
|
||||
} else {
|
||||
fmt.Printf("What should the wallet be called on the stats page? (default = %s)\n", infos.ethstats)
|
||||
infos.ethstats = w.readDefaultString(infos.ethstats) + ":" + w.conf.ethstats
|
||||
}
|
||||
// Try to deploy the wallet on the host
|
||||
nocache := false
|
||||
if existed {
|
||||
fmt.Println()
|
||||
fmt.Printf("Should the wallet be built from scratch (y/n)? (default = no)\n")
|
||||
nocache = w.readDefaultString("n") != "n"
|
||||
}
|
||||
if out, err := deployWallet(client, w.network, w.conf.bootFull, infos, nocache); err != nil {
|
||||
log.Error("Failed to deploy wallet container", "err", err)
|
||||
if len(out) > 0 {
|
||||
fmt.Printf("%s\n", out)
|
||||
}
|
||||
return
|
||||
}
|
||||
// All ok, run a network scan to pick any changes up
|
||||
log.Info("Waiting for node to finish booting")
|
||||
time.Sleep(3 * time.Second)
|
||||
|
||||
w.networkStats()
|
||||
}
|
||||
@@ -32,6 +32,7 @@ import (
|
||||
var (
|
||||
hexMode = flag.String("hex", "", "dump given hex data")
|
||||
noASCII = flag.Bool("noascii", false, "don't print ASCII strings readably")
|
||||
single = flag.Bool("single", false, "print only the first element, discard the rest")
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -50,7 +51,7 @@ func main() {
|
||||
var r io.Reader
|
||||
switch {
|
||||
case *hexMode != "":
|
||||
data, err := hex.DecodeString(*hexMode)
|
||||
data, err := hex.DecodeString(strings.TrimPrefix(*hexMode, "0x"))
|
||||
if err != nil {
|
||||
die(err)
|
||||
}
|
||||
@@ -82,6 +83,9 @@ func main() {
|
||||
break
|
||||
}
|
||||
fmt.Println()
|
||||
if *single {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
321
cmd/swarm/config.go
Normal file
321
cmd/swarm/config.go
Normal file
@@ -0,0 +1,321 @@
|
||||
// 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 (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"unicode"
|
||||
|
||||
cli "gopkg.in/urfave/cli.v1"
|
||||
|
||||
"github.com/ethereum/go-ethereum/cmd/utils"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/node"
|
||||
"github.com/naoina/toml"
|
||||
|
||||
bzzapi "github.com/ethereum/go-ethereum/swarm/api"
|
||||
)
|
||||
|
||||
var (
|
||||
//flag definition for the dumpconfig command
|
||||
DumpConfigCommand = cli.Command{
|
||||
Action: utils.MigrateFlags(dumpConfig),
|
||||
Name: "dumpconfig",
|
||||
Usage: "Show configuration values",
|
||||
ArgsUsage: "",
|
||||
Flags: app.Flags,
|
||||
Category: "MISCELLANEOUS COMMANDS",
|
||||
Description: `The dumpconfig command shows configuration values.`,
|
||||
}
|
||||
|
||||
//flag definition for the config file command
|
||||
SwarmTomlConfigPathFlag = cli.StringFlag{
|
||||
Name: "config",
|
||||
Usage: "TOML configuration file",
|
||||
}
|
||||
)
|
||||
|
||||
//constants for environment variables
|
||||
const (
|
||||
SWARM_ENV_CHEQUEBOOK_ADDR = "SWARM_CHEQUEBOOK_ADDR"
|
||||
SWARM_ENV_ACCOUNT = "SWARM_ACCOUNT"
|
||||
SWARM_ENV_LISTEN_ADDR = "SWARM_LISTEN_ADDR"
|
||||
SWARM_ENV_PORT = "SWARM_PORT"
|
||||
SWARM_ENV_NETWORK_ID = "SWARM_NETWORK_ID"
|
||||
SWARM_ENV_SWAP_ENABLE = "SWARM_SWAP_ENABLE"
|
||||
SWARM_ENV_SWAP_API = "SWARM_SWAP_API"
|
||||
SWARM_ENV_SYNC_ENABLE = "SWARM_SYNC_ENABLE"
|
||||
SWARM_ENV_ENS_API = "SWARM_ENS_API"
|
||||
SWARM_ENV_ENS_ADDR = "SWARM_ENS_ADDR"
|
||||
SWARM_ENV_CORS = "SWARM_CORS"
|
||||
SWARM_ENV_BOOTNODES = "SWARM_BOOTNODES"
|
||||
GETH_ENV_DATADIR = "GETH_DATADIR"
|
||||
)
|
||||
|
||||
// These settings ensure that TOML keys use the same names as Go struct fields.
|
||||
var tomlSettings = toml.Config{
|
||||
NormFieldName: func(rt reflect.Type, key string) string {
|
||||
return key
|
||||
},
|
||||
FieldToKey: func(rt reflect.Type, field string) string {
|
||||
return field
|
||||
},
|
||||
MissingField: func(rt reflect.Type, field string) error {
|
||||
link := ""
|
||||
if unicode.IsUpper(rune(rt.Name()[0])) && rt.PkgPath() != "main" {
|
||||
link = fmt.Sprintf(", check github.com/ethereum/go-ethereum/swarm/api/config.go for available fields")
|
||||
}
|
||||
return fmt.Errorf("field '%s' is not defined in %s%s", field, rt.String(), link)
|
||||
},
|
||||
}
|
||||
|
||||
//before booting the swarm node, build the configuration
|
||||
func buildConfig(ctx *cli.Context) (config *bzzapi.Config, err error) {
|
||||
//check for deprecated flags
|
||||
checkDeprecated(ctx)
|
||||
//start by creating a default config
|
||||
config = bzzapi.NewDefaultConfig()
|
||||
//first load settings from config file (if provided)
|
||||
config, err = configFileOverride(config, ctx)
|
||||
//override settings provided by environment variables
|
||||
config = envVarsOverride(config)
|
||||
//override settings provided by command line
|
||||
config = cmdLineOverride(config, ctx)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
//finally, after the configuration build phase is finished, initialize
|
||||
func initSwarmNode(config *bzzapi.Config, stack *node.Node, ctx *cli.Context) {
|
||||
//at this point, all vars should be set in the Config
|
||||
//get the account for the provided swarm account
|
||||
prvkey := getAccount(config.BzzAccount, ctx, stack)
|
||||
//set the resolved config path (geth --datadir)
|
||||
config.Path = stack.InstanceDir()
|
||||
//finally, initialize the configuration
|
||||
config.Init(prvkey)
|
||||
//configuration phase completed here
|
||||
log.Debug("Starting Swarm with the following parameters:")
|
||||
//after having created the config, print it to screen
|
||||
log.Debug(printConfig(config))
|
||||
}
|
||||
|
||||
//override the current config with whatever is in the config file, if a config file has been provided
|
||||
func configFileOverride(config *bzzapi.Config, ctx *cli.Context) (*bzzapi.Config, error) {
|
||||
var err error
|
||||
|
||||
//only do something if the -config flag has been set
|
||||
if ctx.GlobalIsSet(SwarmTomlConfigPathFlag.Name) {
|
||||
var filepath string
|
||||
if filepath = ctx.GlobalString(SwarmTomlConfigPathFlag.Name); filepath == "" {
|
||||
utils.Fatalf("Config file flag provided with invalid file path")
|
||||
}
|
||||
f, err := os.Open(filepath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
//decode the TOML file into a Config struct
|
||||
//note that we are decoding into the existing defaultConfig;
|
||||
//if an entry is not present in the file, the default entry is kept
|
||||
err = tomlSettings.NewDecoder(f).Decode(&config)
|
||||
// Add file name to errors that have a line number.
|
||||
if _, ok := err.(*toml.LineError); ok {
|
||||
err = errors.New(filepath + ", " + err.Error())
|
||||
}
|
||||
}
|
||||
return config, err
|
||||
}
|
||||
|
||||
//override the current config with whatever is provided through the command line
|
||||
//most values are not allowed a zero value (empty string), if not otherwise noted
|
||||
func cmdLineOverride(currentConfig *bzzapi.Config, ctx *cli.Context) *bzzapi.Config {
|
||||
|
||||
if keyid := ctx.GlobalString(SwarmAccountFlag.Name); keyid != "" {
|
||||
currentConfig.BzzAccount = keyid
|
||||
}
|
||||
|
||||
if chbookaddr := ctx.GlobalString(ChequebookAddrFlag.Name); chbookaddr != "" {
|
||||
currentConfig.Contract = common.HexToAddress(chbookaddr)
|
||||
}
|
||||
|
||||
if networkid := ctx.GlobalString(SwarmNetworkIdFlag.Name); networkid != "" {
|
||||
if id, _ := strconv.Atoi(networkid); id != 0 {
|
||||
currentConfig.NetworkId = uint64(id)
|
||||
}
|
||||
}
|
||||
|
||||
if ctx.GlobalIsSet(utils.DataDirFlag.Name) {
|
||||
if datadir := ctx.GlobalString(utils.DataDirFlag.Name); datadir != "" {
|
||||
currentConfig.Path = datadir
|
||||
}
|
||||
}
|
||||
|
||||
bzzport := ctx.GlobalString(SwarmPortFlag.Name)
|
||||
if len(bzzport) > 0 {
|
||||
currentConfig.Port = bzzport
|
||||
}
|
||||
|
||||
if bzzaddr := ctx.GlobalString(SwarmListenAddrFlag.Name); bzzaddr != "" {
|
||||
currentConfig.ListenAddr = bzzaddr
|
||||
}
|
||||
|
||||
if ctx.GlobalIsSet(SwarmSwapEnabledFlag.Name) {
|
||||
currentConfig.SwapEnabled = true
|
||||
}
|
||||
|
||||
if ctx.GlobalIsSet(SwarmSyncEnabledFlag.Name) {
|
||||
currentConfig.SyncEnabled = true
|
||||
}
|
||||
|
||||
currentConfig.SwapApi = ctx.GlobalString(SwarmSwapAPIFlag.Name)
|
||||
if currentConfig.SwapEnabled && currentConfig.SwapApi == "" {
|
||||
utils.Fatalf(SWARM_ERR_SWAP_SET_NO_API)
|
||||
}
|
||||
|
||||
//EnsApi can be set to "", so can't check for empty string, as it is allowed!
|
||||
if ctx.GlobalIsSet(EnsAPIFlag.Name) {
|
||||
currentConfig.EnsApi = ctx.GlobalString(EnsAPIFlag.Name)
|
||||
}
|
||||
|
||||
if ensaddr := ctx.GlobalString(EnsAddrFlag.Name); ensaddr != "" {
|
||||
currentConfig.EnsRoot = common.HexToAddress(ensaddr)
|
||||
}
|
||||
|
||||
if cors := ctx.GlobalString(CorsStringFlag.Name); cors != "" {
|
||||
currentConfig.Cors = cors
|
||||
}
|
||||
|
||||
if ctx.GlobalIsSet(utils.BootnodesFlag.Name) {
|
||||
currentConfig.BootNodes = ctx.GlobalString(utils.BootnodesFlag.Name)
|
||||
}
|
||||
|
||||
return currentConfig
|
||||
|
||||
}
|
||||
|
||||
//override the current config with whatver is provided in environment variables
|
||||
//most values are not allowed a zero value (empty string), if not otherwise noted
|
||||
func envVarsOverride(currentConfig *bzzapi.Config) (config *bzzapi.Config) {
|
||||
|
||||
if keyid := os.Getenv(SWARM_ENV_ACCOUNT); keyid != "" {
|
||||
currentConfig.BzzAccount = keyid
|
||||
}
|
||||
|
||||
if chbookaddr := os.Getenv(SWARM_ENV_CHEQUEBOOK_ADDR); chbookaddr != "" {
|
||||
currentConfig.Contract = common.HexToAddress(chbookaddr)
|
||||
}
|
||||
|
||||
if networkid := os.Getenv(SWARM_ENV_NETWORK_ID); networkid != "" {
|
||||
if id, _ := strconv.Atoi(networkid); id != 0 {
|
||||
currentConfig.NetworkId = uint64(id)
|
||||
}
|
||||
}
|
||||
|
||||
if datadir := os.Getenv(GETH_ENV_DATADIR); datadir != "" {
|
||||
currentConfig.Path = datadir
|
||||
}
|
||||
|
||||
bzzport := os.Getenv(SWARM_ENV_PORT)
|
||||
if len(bzzport) > 0 {
|
||||
currentConfig.Port = bzzport
|
||||
}
|
||||
|
||||
if bzzaddr := os.Getenv(SWARM_ENV_LISTEN_ADDR); bzzaddr != "" {
|
||||
currentConfig.ListenAddr = bzzaddr
|
||||
}
|
||||
|
||||
if swapenable := os.Getenv(SWARM_ENV_SWAP_ENABLE); swapenable != "" {
|
||||
if swap, err := strconv.ParseBool(swapenable); err != nil {
|
||||
currentConfig.SwapEnabled = swap
|
||||
}
|
||||
}
|
||||
|
||||
if syncenable := os.Getenv(SWARM_ENV_SYNC_ENABLE); syncenable != "" {
|
||||
if sync, err := strconv.ParseBool(syncenable); err != nil {
|
||||
currentConfig.SyncEnabled = sync
|
||||
}
|
||||
}
|
||||
|
||||
if swapapi := os.Getenv(SWARM_ENV_SWAP_API); swapapi != "" {
|
||||
currentConfig.SwapApi = swapapi
|
||||
}
|
||||
|
||||
if currentConfig.SwapEnabled && currentConfig.SwapApi == "" {
|
||||
utils.Fatalf(SWARM_ERR_SWAP_SET_NO_API)
|
||||
}
|
||||
|
||||
//EnsApi can be set to "", so can't check for empty string, as it is allowed
|
||||
if ensapi, exists := os.LookupEnv(SWARM_ENV_ENS_API); exists {
|
||||
currentConfig.EnsApi = ensapi
|
||||
}
|
||||
|
||||
if ensaddr := os.Getenv(SWARM_ENV_ENS_ADDR); ensaddr != "" {
|
||||
currentConfig.EnsRoot = common.HexToAddress(ensaddr)
|
||||
}
|
||||
|
||||
if cors := os.Getenv(SWARM_ENV_CORS); cors != "" {
|
||||
currentConfig.Cors = cors
|
||||
}
|
||||
|
||||
if bootnodes := os.Getenv(SWARM_ENV_BOOTNODES); bootnodes != "" {
|
||||
currentConfig.BootNodes = bootnodes
|
||||
}
|
||||
|
||||
return currentConfig
|
||||
}
|
||||
|
||||
// dumpConfig is the dumpconfig command.
|
||||
// writes a default config to STDOUT
|
||||
func dumpConfig(ctx *cli.Context) error {
|
||||
cfg, err := buildConfig(ctx)
|
||||
if err != nil {
|
||||
utils.Fatalf(fmt.Sprintf("Uh oh - dumpconfig triggered an error %v", err))
|
||||
}
|
||||
comment := ""
|
||||
out, err := tomlSettings.Marshal(&cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
io.WriteString(os.Stdout, comment)
|
||||
os.Stdout.Write(out)
|
||||
return nil
|
||||
}
|
||||
|
||||
//deprecated flags checked here
|
||||
func checkDeprecated(ctx *cli.Context) {
|
||||
// exit if the deprecated --ethapi flag is set
|
||||
if ctx.GlobalString(DeprecatedEthAPIFlag.Name) != "" {
|
||||
utils.Fatalf("--ethapi is no longer a valid command line flag, please use --ens-api and/or --swap-api.")
|
||||
}
|
||||
}
|
||||
|
||||
//print a Config as string
|
||||
func printConfig(config *bzzapi.Config) string {
|
||||
out, err := tomlSettings.Marshal(&config)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("Something is not right with the configuration: %v", err)
|
||||
}
|
||||
return string(out)
|
||||
}
|
||||
459
cmd/swarm/config_test.go
Normal file
459
cmd/swarm/config_test.go
Normal file
@@ -0,0 +1,459 @@
|
||||
// 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 (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/rpc"
|
||||
"github.com/ethereum/go-ethereum/swarm"
|
||||
"github.com/ethereum/go-ethereum/swarm/api"
|
||||
|
||||
"github.com/docker/docker/pkg/reexec"
|
||||
)
|
||||
|
||||
func TestDumpConfig(t *testing.T) {
|
||||
swarm := runSwarm(t, "dumpconfig")
|
||||
defaultConf := api.NewDefaultConfig()
|
||||
out, err := tomlSettings.Marshal(&defaultConf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
swarm.Expect(string(out))
|
||||
swarm.ExpectExit()
|
||||
}
|
||||
|
||||
func TestFailsSwapEnabledNoSwapApi(t *testing.T) {
|
||||
flags := []string{
|
||||
fmt.Sprintf("--%s", SwarmNetworkIdFlag.Name), "42",
|
||||
fmt.Sprintf("--%s", SwarmPortFlag.Name), "54545",
|
||||
fmt.Sprintf("--%s", SwarmSwapEnabledFlag.Name),
|
||||
}
|
||||
|
||||
swarm := runSwarm(t, flags...)
|
||||
swarm.Expect("Fatal: " + SWARM_ERR_SWAP_SET_NO_API + "\n")
|
||||
swarm.ExpectExit()
|
||||
}
|
||||
|
||||
func TestFailsNoBzzAccount(t *testing.T) {
|
||||
flags := []string{
|
||||
fmt.Sprintf("--%s", SwarmNetworkIdFlag.Name), "42",
|
||||
fmt.Sprintf("--%s", SwarmPortFlag.Name), "54545",
|
||||
}
|
||||
|
||||
swarm := runSwarm(t, flags...)
|
||||
swarm.Expect("Fatal: " + SWARM_ERR_NO_BZZACCOUNT + "\n")
|
||||
swarm.ExpectExit()
|
||||
}
|
||||
|
||||
func TestCmdLineOverrides(t *testing.T) {
|
||||
dir, err := ioutil.TempDir("", "bzztest")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
conf, account := getTestAccount(t, dir)
|
||||
node := &testNode{Dir: dir}
|
||||
|
||||
// assign ports
|
||||
httpPort, err := assignTCPPort()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
flags := []string{
|
||||
fmt.Sprintf("--%s", SwarmNetworkIdFlag.Name), "42",
|
||||
fmt.Sprintf("--%s", SwarmPortFlag.Name), httpPort,
|
||||
fmt.Sprintf("--%s", SwarmSyncEnabledFlag.Name),
|
||||
fmt.Sprintf("--%s", CorsStringFlag.Name), "*",
|
||||
fmt.Sprintf("--%s", SwarmAccountFlag.Name), account.Address.String(),
|
||||
fmt.Sprintf("--%s", EnsAPIFlag.Name), "",
|
||||
"--datadir", dir,
|
||||
"--ipcpath", conf.IPCPath,
|
||||
}
|
||||
node.Cmd = runSwarm(t, flags...)
|
||||
node.Cmd.InputLine(testPassphrase)
|
||||
defer func() {
|
||||
if t.Failed() {
|
||||
node.Shutdown()
|
||||
}
|
||||
}()
|
||||
// wait for the node to start
|
||||
for start := time.Now(); time.Since(start) < 10*time.Second; time.Sleep(50 * time.Millisecond) {
|
||||
node.Client, err = rpc.Dial(conf.IPCEndpoint())
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
if node.Client == nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// load info
|
||||
var info swarm.Info
|
||||
if err := node.Client.Call(&info, "bzz_info"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if info.Port != httpPort {
|
||||
t.Fatalf("Expected port to be %s, got %s", httpPort, info.Port)
|
||||
}
|
||||
|
||||
if info.NetworkId != 42 {
|
||||
t.Fatalf("Expected network ID to be %d, got %d", 42, info.NetworkId)
|
||||
}
|
||||
|
||||
if !info.SyncEnabled {
|
||||
t.Fatal("Expected Sync to be enabled, but is false")
|
||||
}
|
||||
|
||||
if info.Cors != "*" {
|
||||
t.Fatalf("Expected Cors flag to be set to %s, got %s", "*", info.Cors)
|
||||
}
|
||||
|
||||
node.Shutdown()
|
||||
}
|
||||
|
||||
func TestFileOverrides(t *testing.T) {
|
||||
|
||||
// assign ports
|
||||
httpPort, err := assignTCPPort()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
//create a config file
|
||||
//first, create a default conf
|
||||
defaultConf := api.NewDefaultConfig()
|
||||
//change some values in order to test if they have been loaded
|
||||
defaultConf.SyncEnabled = true
|
||||
defaultConf.NetworkId = 54
|
||||
defaultConf.Port = httpPort
|
||||
defaultConf.StoreParams.DbCapacity = 9000000
|
||||
defaultConf.ChunkerParams.Branches = 64
|
||||
defaultConf.HiveParams.CallInterval = 6000000000
|
||||
defaultConf.Swap.Params.Strategy.AutoCashInterval = 600 * time.Second
|
||||
defaultConf.SyncParams.KeyBufferSize = 512
|
||||
//create a TOML string
|
||||
out, err := tomlSettings.Marshal(&defaultConf)
|
||||
if err != nil {
|
||||
t.Fatalf("Error creating TOML file in TestFileOverride: %v", err)
|
||||
}
|
||||
//create file
|
||||
f, err := ioutil.TempFile("", "testconfig.toml")
|
||||
if err != nil {
|
||||
t.Fatalf("Error writing TOML file in TestFileOverride: %v", err)
|
||||
}
|
||||
//write file
|
||||
_, err = f.WriteString(string(out))
|
||||
if err != nil {
|
||||
t.Fatalf("Error writing TOML file in TestFileOverride: %v", err)
|
||||
}
|
||||
f.Sync()
|
||||
|
||||
dir, err := ioutil.TempDir("", "bzztest")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
conf, account := getTestAccount(t, dir)
|
||||
node := &testNode{Dir: dir}
|
||||
|
||||
flags := []string{
|
||||
fmt.Sprintf("--%s", SwarmTomlConfigPathFlag.Name), f.Name(),
|
||||
fmt.Sprintf("--%s", SwarmAccountFlag.Name), account.Address.String(),
|
||||
"--ens-api", "",
|
||||
"--ipcpath", conf.IPCPath,
|
||||
"--datadir", dir,
|
||||
}
|
||||
node.Cmd = runSwarm(t, flags...)
|
||||
node.Cmd.InputLine(testPassphrase)
|
||||
defer func() {
|
||||
if t.Failed() {
|
||||
node.Shutdown()
|
||||
}
|
||||
}()
|
||||
// wait for the node to start
|
||||
for start := time.Now(); time.Since(start) < 10*time.Second; time.Sleep(50 * time.Millisecond) {
|
||||
node.Client, err = rpc.Dial(conf.IPCEndpoint())
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
if node.Client == nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// load info
|
||||
var info swarm.Info
|
||||
if err := node.Client.Call(&info, "bzz_info"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if info.Port != httpPort {
|
||||
t.Fatalf("Expected port to be %s, got %s", httpPort, info.Port)
|
||||
}
|
||||
|
||||
if info.NetworkId != 54 {
|
||||
t.Fatalf("Expected network ID to be %d, got %d", 54, info.NetworkId)
|
||||
}
|
||||
|
||||
if !info.SyncEnabled {
|
||||
t.Fatal("Expected Sync to be enabled, but is false")
|
||||
}
|
||||
|
||||
if info.StoreParams.DbCapacity != 9000000 {
|
||||
t.Fatalf("Expected network ID to be %d, got %d", 54, info.NetworkId)
|
||||
}
|
||||
|
||||
if info.ChunkerParams.Branches != 64 {
|
||||
t.Fatalf("Expected chunker params branches to be %d, got %d", 64, info.ChunkerParams.Branches)
|
||||
}
|
||||
|
||||
if info.HiveParams.CallInterval != 6000000000 {
|
||||
t.Fatalf("Expected HiveParams CallInterval to be %d, got %d", uint64(6000000000), uint64(info.HiveParams.CallInterval))
|
||||
}
|
||||
|
||||
if info.Swap.Params.Strategy.AutoCashInterval != 600*time.Second {
|
||||
t.Fatalf("Expected SwapParams AutoCashInterval to be %ds, got %d", 600, info.Swap.Params.Strategy.AutoCashInterval)
|
||||
}
|
||||
|
||||
if info.SyncParams.KeyBufferSize != 512 {
|
||||
t.Fatalf("Expected info.SyncParams.KeyBufferSize to be %d, got %d", 512, info.SyncParams.KeyBufferSize)
|
||||
}
|
||||
|
||||
node.Shutdown()
|
||||
}
|
||||
|
||||
func TestEnvVars(t *testing.T) {
|
||||
// assign ports
|
||||
httpPort, err := assignTCPPort()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
envVars := os.Environ()
|
||||
envVars = append(envVars, fmt.Sprintf("%s=%s", SwarmPortFlag.EnvVar, httpPort))
|
||||
envVars = append(envVars, fmt.Sprintf("%s=%s", SwarmNetworkIdFlag.EnvVar, "999"))
|
||||
envVars = append(envVars, fmt.Sprintf("%s=%s", CorsStringFlag.EnvVar, "*"))
|
||||
envVars = append(envVars, fmt.Sprintf("%s=%s", SwarmSyncEnabledFlag.EnvVar, "true"))
|
||||
|
||||
dir, err := ioutil.TempDir("", "bzztest")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
conf, account := getTestAccount(t, dir)
|
||||
node := &testNode{Dir: dir}
|
||||
flags := []string{
|
||||
fmt.Sprintf("--%s", SwarmAccountFlag.Name), account.Address.String(),
|
||||
"--ens-api", "",
|
||||
"--datadir", dir,
|
||||
"--ipcpath", conf.IPCPath,
|
||||
}
|
||||
|
||||
//node.Cmd = runSwarm(t,flags...)
|
||||
//node.Cmd.cmd.Env = envVars
|
||||
//the above assignment does not work, so we need a custom Cmd here in order to pass envVars:
|
||||
cmd := &exec.Cmd{
|
||||
Path: reexec.Self(),
|
||||
Args: append([]string{"swarm-test"}, flags...),
|
||||
Stderr: os.Stderr,
|
||||
Stdout: os.Stdout,
|
||||
}
|
||||
cmd.Env = envVars
|
||||
//stdout, err := cmd.StdoutPipe()
|
||||
//if err != nil {
|
||||
// t.Fatal(err)
|
||||
//}
|
||||
//stdout = bufio.NewReader(stdout)
|
||||
var stdin io.WriteCloser
|
||||
if stdin, err = cmd.StdinPipe(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := cmd.Start(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
//cmd.InputLine(testPassphrase)
|
||||
io.WriteString(stdin, testPassphrase+"\n")
|
||||
defer func() {
|
||||
if t.Failed() {
|
||||
node.Shutdown()
|
||||
cmd.Process.Kill()
|
||||
}
|
||||
}()
|
||||
// wait for the node to start
|
||||
for start := time.Now(); time.Since(start) < 10*time.Second; time.Sleep(50 * time.Millisecond) {
|
||||
node.Client, err = rpc.Dial(conf.IPCEndpoint())
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if node.Client == nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// load info
|
||||
var info swarm.Info
|
||||
if err := node.Client.Call(&info, "bzz_info"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if info.Port != httpPort {
|
||||
t.Fatalf("Expected port to be %s, got %s", httpPort, info.Port)
|
||||
}
|
||||
|
||||
if info.NetworkId != 999 {
|
||||
t.Fatalf("Expected network ID to be %d, got %d", 999, info.NetworkId)
|
||||
}
|
||||
|
||||
if info.Cors != "*" {
|
||||
t.Fatalf("Expected Cors flag to be set to %s, got %s", "*", info.Cors)
|
||||
}
|
||||
|
||||
if !info.SyncEnabled {
|
||||
t.Fatal("Expected Sync to be enabled, but is false")
|
||||
}
|
||||
|
||||
node.Shutdown()
|
||||
cmd.Process.Kill()
|
||||
}
|
||||
|
||||
func TestCmdLineOverridesFile(t *testing.T) {
|
||||
|
||||
// assign ports
|
||||
httpPort, err := assignTCPPort()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
//create a config file
|
||||
//first, create a default conf
|
||||
defaultConf := api.NewDefaultConfig()
|
||||
//change some values in order to test if they have been loaded
|
||||
defaultConf.SyncEnabled = false
|
||||
defaultConf.NetworkId = 54
|
||||
defaultConf.Port = "8588"
|
||||
defaultConf.StoreParams.DbCapacity = 9000000
|
||||
defaultConf.ChunkerParams.Branches = 64
|
||||
defaultConf.HiveParams.CallInterval = 6000000000
|
||||
defaultConf.Swap.Params.Strategy.AutoCashInterval = 600 * time.Second
|
||||
defaultConf.SyncParams.KeyBufferSize = 512
|
||||
//create a TOML file
|
||||
out, err := tomlSettings.Marshal(&defaultConf)
|
||||
if err != nil {
|
||||
t.Fatalf("Error creating TOML file in TestFileOverride: %v", err)
|
||||
}
|
||||
//write file
|
||||
f, err := ioutil.TempFile("", "testconfig.toml")
|
||||
if err != nil {
|
||||
t.Fatalf("Error writing TOML file in TestFileOverride: %v", err)
|
||||
}
|
||||
//write file
|
||||
_, err = f.WriteString(string(out))
|
||||
if err != nil {
|
||||
t.Fatalf("Error writing TOML file in TestFileOverride: %v", err)
|
||||
}
|
||||
f.Sync()
|
||||
|
||||
dir, err := ioutil.TempDir("", "bzztest")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
conf, account := getTestAccount(t, dir)
|
||||
node := &testNode{Dir: dir}
|
||||
|
||||
expectNetworkId := uint64(77)
|
||||
|
||||
flags := []string{
|
||||
fmt.Sprintf("--%s", SwarmNetworkIdFlag.Name), "77",
|
||||
fmt.Sprintf("--%s", SwarmPortFlag.Name), httpPort,
|
||||
fmt.Sprintf("--%s", SwarmSyncEnabledFlag.Name),
|
||||
fmt.Sprintf("--%s", SwarmTomlConfigPathFlag.Name), f.Name(),
|
||||
fmt.Sprintf("--%s", SwarmAccountFlag.Name), account.Address.String(),
|
||||
"--ens-api", "",
|
||||
"--datadir", dir,
|
||||
"--ipcpath", conf.IPCPath,
|
||||
}
|
||||
node.Cmd = runSwarm(t, flags...)
|
||||
node.Cmd.InputLine(testPassphrase)
|
||||
defer func() {
|
||||
if t.Failed() {
|
||||
node.Shutdown()
|
||||
}
|
||||
}()
|
||||
// wait for the node to start
|
||||
for start := time.Now(); time.Since(start) < 10*time.Second; time.Sleep(50 * time.Millisecond) {
|
||||
node.Client, err = rpc.Dial(conf.IPCEndpoint())
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
if node.Client == nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// load info
|
||||
var info swarm.Info
|
||||
if err := node.Client.Call(&info, "bzz_info"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if info.Port != httpPort {
|
||||
t.Fatalf("Expected port to be %s, got %s", httpPort, info.Port)
|
||||
}
|
||||
|
||||
if info.NetworkId != expectNetworkId {
|
||||
t.Fatalf("Expected network ID to be %d, got %d", expectNetworkId, info.NetworkId)
|
||||
}
|
||||
|
||||
if !info.SyncEnabled {
|
||||
t.Fatal("Expected Sync to be enabled, but is false")
|
||||
}
|
||||
|
||||
if info.StoreParams.DbCapacity != 9000000 {
|
||||
t.Fatalf("Expected network ID to be %d, got %d", 54, info.NetworkId)
|
||||
}
|
||||
|
||||
if info.ChunkerParams.Branches != 64 {
|
||||
t.Fatalf("Expected chunker params branches to be %d, got %d", 64, info.ChunkerParams.Branches)
|
||||
}
|
||||
|
||||
if info.HiveParams.CallInterval != 6000000000 {
|
||||
t.Fatalf("Expected HiveParams CallInterval to be %d, got %d", uint64(6000000000), uint64(info.HiveParams.CallInterval))
|
||||
}
|
||||
|
||||
if info.Swap.Params.Strategy.AutoCashInterval != 600*time.Second {
|
||||
t.Fatalf("Expected SwapParams AutoCashInterval to be %ds, got %d", 600, info.Swap.Params.Strategy.AutoCashInterval)
|
||||
}
|
||||
|
||||
if info.SyncParams.KeyBufferSize != 512 {
|
||||
t.Fatalf("Expected info.SyncParams.KeyBufferSize to be %d, got %d", 512, info.SyncParams.KeyBufferSize)
|
||||
}
|
||||
|
||||
node.Shutdown()
|
||||
}
|
||||
116
cmd/swarm/db.go
Normal file
116
cmd/swarm/db.go
Normal file
@@ -0,0 +1,116 @@
|
||||
// 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 (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/ethereum/go-ethereum/cmd/utils"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/swarm/storage"
|
||||
"gopkg.in/urfave/cli.v1"
|
||||
)
|
||||
|
||||
func dbExport(ctx *cli.Context) {
|
||||
args := ctx.Args()
|
||||
if len(args) != 2 {
|
||||
utils.Fatalf("invalid arguments, please specify both <chunkdb> (path to a local chunk database) and <file> (path to write the tar archive to, - for stdout)")
|
||||
}
|
||||
|
||||
store, err := openDbStore(args[0])
|
||||
if err != nil {
|
||||
utils.Fatalf("error opening local chunk database: %s", err)
|
||||
}
|
||||
defer store.Close()
|
||||
|
||||
var out io.Writer
|
||||
if args[1] == "-" {
|
||||
out = os.Stdout
|
||||
} else {
|
||||
f, err := os.Create(args[1])
|
||||
if err != nil {
|
||||
utils.Fatalf("error opening output file: %s", err)
|
||||
}
|
||||
defer f.Close()
|
||||
out = f
|
||||
}
|
||||
|
||||
count, err := store.Export(out)
|
||||
if err != nil {
|
||||
utils.Fatalf("error exporting local chunk database: %s", err)
|
||||
}
|
||||
|
||||
log.Info(fmt.Sprintf("successfully exported %d chunks", count))
|
||||
}
|
||||
|
||||
func dbImport(ctx *cli.Context) {
|
||||
args := ctx.Args()
|
||||
if len(args) != 2 {
|
||||
utils.Fatalf("invalid arguments, please specify both <chunkdb> (path to a local chunk database) and <file> (path to read the tar archive from, - for stdin)")
|
||||
}
|
||||
|
||||
store, err := openDbStore(args[0])
|
||||
if err != nil {
|
||||
utils.Fatalf("error opening local chunk database: %s", err)
|
||||
}
|
||||
defer store.Close()
|
||||
|
||||
var in io.Reader
|
||||
if args[1] == "-" {
|
||||
in = os.Stdin
|
||||
} else {
|
||||
f, err := os.Open(args[1])
|
||||
if err != nil {
|
||||
utils.Fatalf("error opening input file: %s", err)
|
||||
}
|
||||
defer f.Close()
|
||||
in = f
|
||||
}
|
||||
|
||||
count, err := store.Import(in)
|
||||
if err != nil {
|
||||
utils.Fatalf("error importing local chunk database: %s", err)
|
||||
}
|
||||
|
||||
log.Info(fmt.Sprintf("successfully imported %d chunks", count))
|
||||
}
|
||||
|
||||
func dbClean(ctx *cli.Context) {
|
||||
args := ctx.Args()
|
||||
if len(args) != 1 {
|
||||
utils.Fatalf("invalid arguments, please specify <chunkdb> (path to a local chunk database)")
|
||||
}
|
||||
|
||||
store, err := openDbStore(args[0])
|
||||
if err != nil {
|
||||
utils.Fatalf("error opening local chunk database: %s", err)
|
||||
}
|
||||
defer store.Close()
|
||||
|
||||
store.Cleanup()
|
||||
}
|
||||
|
||||
func openDbStore(path string) (*storage.DbStore, error) {
|
||||
if _, err := os.Stat(filepath.Join(path, "CURRENT")); err != nil {
|
||||
return nil, fmt.Errorf("invalid chunkdb path: %s", err)
|
||||
}
|
||||
hash := storage.MakeHashFunc("SHA3")
|
||||
return storage.NewDbStore(path, hash, 10000000, 0)
|
||||
}
|
||||
48
cmd/swarm/hash.go
Normal file
48
cmd/swarm/hash.go
Normal file
@@ -0,0 +1,48 @@
|
||||
// Copyright 2016 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/>.
|
||||
|
||||
// Command bzzhash computes a swarm tree hash.
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/ethereum/go-ethereum/cmd/utils"
|
||||
"github.com/ethereum/go-ethereum/swarm/storage"
|
||||
"gopkg.in/urfave/cli.v1"
|
||||
)
|
||||
|
||||
func hash(ctx *cli.Context) {
|
||||
args := ctx.Args()
|
||||
if len(args) < 1 {
|
||||
utils.Fatalf("Usage: swarm hash <file name>")
|
||||
}
|
||||
f, err := os.Open(args[0])
|
||||
if err != nil {
|
||||
utils.Fatalf("Error opening file " + args[1])
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
stat, _ := f.Stat()
|
||||
chunker := storage.NewTreeChunker(storage.NewChunkerParams())
|
||||
key, err := chunker.Split(f, stat.Size(), nil, nil, nil)
|
||||
if err != nil {
|
||||
utils.Fatalf("%v\n", err)
|
||||
} else {
|
||||
fmt.Printf("%v\n", key)
|
||||
}
|
||||
}
|
||||
61
cmd/swarm/list.go
Normal file
61
cmd/swarm/list.go
Normal file
@@ -0,0 +1,61 @@
|
||||
// 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 (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/ethereum/go-ethereum/cmd/utils"
|
||||
swarm "github.com/ethereum/go-ethereum/swarm/api/client"
|
||||
"gopkg.in/urfave/cli.v1"
|
||||
)
|
||||
|
||||
func list(ctx *cli.Context) {
|
||||
args := ctx.Args()
|
||||
|
||||
if len(args) < 1 {
|
||||
utils.Fatalf("Please supply a manifest reference as the first argument")
|
||||
} else if len(args) > 2 {
|
||||
utils.Fatalf("Too many arguments - usage 'swarm ls manifest [prefix]'")
|
||||
}
|
||||
manifest := args[0]
|
||||
|
||||
var prefix string
|
||||
if len(args) == 2 {
|
||||
prefix = args[1]
|
||||
}
|
||||
|
||||
bzzapi := strings.TrimRight(ctx.GlobalString(SwarmApiFlag.Name), "/")
|
||||
client := swarm.NewClient(bzzapi)
|
||||
list, err := client.List(manifest, prefix)
|
||||
if err != nil {
|
||||
utils.Fatalf("Failed to generate file and directory list: %s", err)
|
||||
}
|
||||
|
||||
w := tabwriter.NewWriter(os.Stdout, 1, 2, 2, ' ', 0)
|
||||
defer w.Flush()
|
||||
fmt.Fprintln(w, "HASH\tCONTENT TYPE\tPATH")
|
||||
for _, prefix := range list.CommonPrefixes {
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\n", "", "DIR", prefix)
|
||||
}
|
||||
for _, entry := range list.Entries {
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\n", entry.Hash, entry.ContentType, entry.Path)
|
||||
}
|
||||
}
|
||||
605
cmd/swarm/main.go
Normal file
605
cmd/swarm/main.go
Normal file
@@ -0,0 +1,605 @@
|
||||
// Copyright 2016 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 (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math/big"
|
||||
"os"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts"
|
||||
"github.com/ethereum/go-ethereum/accounts/keystore"
|
||||
"github.com/ethereum/go-ethereum/cmd/utils"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/console"
|
||||
"github.com/ethereum/go-ethereum/contracts/ens"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/ethereum/go-ethereum/ethclient"
|
||||
"github.com/ethereum/go-ethereum/internal/debug"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/node"
|
||||
"github.com/ethereum/go-ethereum/p2p"
|
||||
"github.com/ethereum/go-ethereum/p2p/discover"
|
||||
"github.com/ethereum/go-ethereum/params"
|
||||
"github.com/ethereum/go-ethereum/rpc"
|
||||
"github.com/ethereum/go-ethereum/swarm"
|
||||
bzzapi "github.com/ethereum/go-ethereum/swarm/api"
|
||||
|
||||
"gopkg.in/urfave/cli.v1"
|
||||
)
|
||||
|
||||
const clientIdentifier = "swarm"
|
||||
|
||||
var (
|
||||
gitCommit string // Git SHA1 commit hash of the release (set via linker flags)
|
||||
testbetBootNodes = []string{
|
||||
"enode://ec8ae764f7cb0417bdfb009b9d0f18ab3818a3a4e8e7c67dd5f18971a93510a2e6f43cd0b69a27e439a9629457ea804104f37c85e41eed057d3faabbf7744cdf@13.74.157.139:30429",
|
||||
"enode://c2e1fceb3bf3be19dff71eec6cccf19f2dbf7567ee017d130240c670be8594bc9163353ca55dd8df7a4f161dd94b36d0615c17418b5a3cdcbb4e9d99dfa4de37@13.74.157.139:30430",
|
||||
"enode://fe29b82319b734ce1ec68b84657d57145fee237387e63273989d354486731e59f78858e452ef800a020559da22dcca759536e6aa5517c53930d29ce0b1029286@13.74.157.139:30431",
|
||||
"enode://1d7187e7bde45cf0bee489ce9852dd6d1a0d9aa67a33a6b8e6db8a4fbc6fcfa6f0f1a5419343671521b863b187d1c73bad3603bae66421d157ffef357669ddb8@13.74.157.139:30432",
|
||||
"enode://0e4cba800f7b1ee73673afa6a4acead4018f0149d2e3216be3f133318fd165b324cd71b81fbe1e80deac8dbf56e57a49db7be67f8b9bc81bd2b7ee496434fb5d@13.74.157.139:30433",
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
ChequebookAddrFlag = cli.StringFlag{
|
||||
Name: "chequebook",
|
||||
Usage: "chequebook contract address",
|
||||
EnvVar: SWARM_ENV_CHEQUEBOOK_ADDR,
|
||||
}
|
||||
SwarmAccountFlag = cli.StringFlag{
|
||||
Name: "bzzaccount",
|
||||
Usage: "Swarm account key file",
|
||||
EnvVar: SWARM_ENV_ACCOUNT,
|
||||
}
|
||||
SwarmListenAddrFlag = cli.StringFlag{
|
||||
Name: "httpaddr",
|
||||
Usage: "Swarm HTTP API listening interface",
|
||||
EnvVar: SWARM_ENV_LISTEN_ADDR,
|
||||
}
|
||||
SwarmPortFlag = cli.StringFlag{
|
||||
Name: "bzzport",
|
||||
Usage: "Swarm local http api port",
|
||||
EnvVar: SWARM_ENV_PORT,
|
||||
}
|
||||
SwarmNetworkIdFlag = cli.IntFlag{
|
||||
Name: "bzznetworkid",
|
||||
Usage: "Network identifier (integer, default 3=swarm testnet)",
|
||||
EnvVar: SWARM_ENV_NETWORK_ID,
|
||||
}
|
||||
SwarmConfigPathFlag = cli.StringFlag{
|
||||
Name: "bzzconfig",
|
||||
Usage: "DEPRECATED: please use --config path/to/TOML-file",
|
||||
}
|
||||
SwarmSwapEnabledFlag = cli.BoolFlag{
|
||||
Name: "swap",
|
||||
Usage: "Swarm SWAP enabled (default false)",
|
||||
EnvVar: SWARM_ENV_SWAP_ENABLE,
|
||||
}
|
||||
SwarmSwapAPIFlag = cli.StringFlag{
|
||||
Name: "swap-api",
|
||||
Usage: "URL of the Ethereum API provider to use to settle SWAP payments",
|
||||
EnvVar: SWARM_ENV_SWAP_API,
|
||||
}
|
||||
SwarmSyncEnabledFlag = cli.BoolTFlag{
|
||||
Name: "sync",
|
||||
Usage: "Swarm Syncing enabled (default true)",
|
||||
EnvVar: SWARM_ENV_SYNC_ENABLE,
|
||||
}
|
||||
EnsAPIFlag = cli.StringFlag{
|
||||
Name: "ens-api",
|
||||
Usage: "URL of the Ethereum API provider to use for ENS record lookups",
|
||||
EnvVar: SWARM_ENV_ENS_API,
|
||||
}
|
||||
EnsAddrFlag = cli.StringFlag{
|
||||
Name: "ens-addr",
|
||||
Usage: "ENS contract address (default is detected as testnet or mainnet using --ens-api)",
|
||||
EnvVar: SWARM_ENV_ENS_ADDR,
|
||||
}
|
||||
SwarmApiFlag = cli.StringFlag{
|
||||
Name: "bzzapi",
|
||||
Usage: "Swarm HTTP endpoint",
|
||||
Value: "http://127.0.0.1:8500",
|
||||
}
|
||||
SwarmRecursiveUploadFlag = cli.BoolFlag{
|
||||
Name: "recursive",
|
||||
Usage: "Upload directories recursively",
|
||||
}
|
||||
SwarmWantManifestFlag = cli.BoolTFlag{
|
||||
Name: "manifest",
|
||||
Usage: "Automatic manifest upload",
|
||||
}
|
||||
SwarmUploadDefaultPath = cli.StringFlag{
|
||||
Name: "defaultpath",
|
||||
Usage: "path to file served for empty url path (none)",
|
||||
}
|
||||
SwarmUpFromStdinFlag = cli.BoolFlag{
|
||||
Name: "stdin",
|
||||
Usage: "reads data to be uploaded from stdin",
|
||||
}
|
||||
SwarmUploadMimeType = cli.StringFlag{
|
||||
Name: "mime",
|
||||
Usage: "force mime type",
|
||||
}
|
||||
CorsStringFlag = cli.StringFlag{
|
||||
Name: "corsdomain",
|
||||
Usage: "Domain on which to send Access-Control-Allow-Origin header (multiple domains can be supplied separated by a ',')",
|
||||
EnvVar: SWARM_ENV_CORS,
|
||||
}
|
||||
|
||||
// the following flags are deprecated and should be removed in the future
|
||||
DeprecatedEthAPIFlag = cli.StringFlag{
|
||||
Name: "ethapi",
|
||||
Usage: "DEPRECATED: please use --ens-api and --swap-api",
|
||||
}
|
||||
)
|
||||
|
||||
//declare a few constant error messages, useful for later error check comparisons in test
|
||||
var (
|
||||
SWARM_ERR_NO_BZZACCOUNT = "bzzaccount option is required but not set; check your config file, command line or environment variables"
|
||||
SWARM_ERR_SWAP_SET_NO_API = "SWAP is enabled but --swap-api is not set"
|
||||
)
|
||||
|
||||
var defaultNodeConfig = node.DefaultConfig
|
||||
|
||||
// This init function sets defaults so cmd/swarm can run alongside geth.
|
||||
func init() {
|
||||
defaultNodeConfig.Name = clientIdentifier
|
||||
defaultNodeConfig.Version = params.VersionWithCommit(gitCommit)
|
||||
defaultNodeConfig.P2P.ListenAddr = ":30399"
|
||||
defaultNodeConfig.IPCPath = "bzzd.ipc"
|
||||
// Set flag defaults for --help display.
|
||||
utils.ListenPortFlag.Value = 30399
|
||||
}
|
||||
|
||||
var app = utils.NewApp(gitCommit, "Ethereum Swarm")
|
||||
|
||||
// This init function creates the cli.App.
|
||||
func init() {
|
||||
app.Action = bzzd
|
||||
app.HideVersion = true // we have a command to print the version
|
||||
app.Copyright = "Copyright 2013-2016 The go-ethereum Authors"
|
||||
app.Commands = []cli.Command{
|
||||
{
|
||||
Action: version,
|
||||
Name: "version",
|
||||
Usage: "Print version numbers",
|
||||
ArgsUsage: " ",
|
||||
Description: `
|
||||
The output of this command is supposed to be machine-readable.
|
||||
`,
|
||||
},
|
||||
{
|
||||
Action: upload,
|
||||
Name: "up",
|
||||
Usage: "upload a file or directory to swarm using the HTTP API",
|
||||
ArgsUsage: " <file>",
|
||||
Description: `
|
||||
"upload a file or directory to swarm using the HTTP API and prints the root hash",
|
||||
`,
|
||||
},
|
||||
{
|
||||
Action: list,
|
||||
Name: "ls",
|
||||
Usage: "list files and directories contained in a manifest",
|
||||
ArgsUsage: " <manifest> [<prefix>]",
|
||||
Description: `
|
||||
Lists files and directories contained in a manifest.
|
||||
`,
|
||||
},
|
||||
{
|
||||
Action: hash,
|
||||
Name: "hash",
|
||||
Usage: "print the swarm hash of a file or directory",
|
||||
ArgsUsage: " <file>",
|
||||
Description: `
|
||||
Prints the swarm hash of file or directory.
|
||||
`,
|
||||
},
|
||||
{
|
||||
Name: "manifest",
|
||||
Usage: "update a MANIFEST",
|
||||
ArgsUsage: "manifest COMMAND",
|
||||
Description: `
|
||||
Updates a MANIFEST by adding/removing/updating the hash of a path.
|
||||
`,
|
||||
Subcommands: []cli.Command{
|
||||
{
|
||||
Action: add,
|
||||
Name: "add",
|
||||
Usage: "add a new path to the manifest",
|
||||
ArgsUsage: "<MANIFEST> <path> <hash> [<content-type>]",
|
||||
Description: `
|
||||
Adds a new path to the manifest
|
||||
`,
|
||||
},
|
||||
{
|
||||
Action: update,
|
||||
Name: "update",
|
||||
Usage: "update the hash for an already existing path in the manifest",
|
||||
ArgsUsage: "<MANIFEST> <path> <newhash> [<newcontent-type>]",
|
||||
Description: `
|
||||
Update the hash for an already existing path in the manifest
|
||||
`,
|
||||
},
|
||||
{
|
||||
Action: remove,
|
||||
Name: "remove",
|
||||
Usage: "removes a path from the manifest",
|
||||
ArgsUsage: "<MANIFEST> <path>",
|
||||
Description: `
|
||||
Removes a path from the manifest
|
||||
`,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "db",
|
||||
Usage: "manage the local chunk database",
|
||||
ArgsUsage: "db COMMAND",
|
||||
Description: `
|
||||
Manage the local chunk database.
|
||||
`,
|
||||
Subcommands: []cli.Command{
|
||||
{
|
||||
Action: dbExport,
|
||||
Name: "export",
|
||||
Usage: "export a local chunk database as a tar archive (use - to send to stdout)",
|
||||
ArgsUsage: "<chunkdb> <file>",
|
||||
Description: `
|
||||
Export a local chunk database as a tar archive (use - to send to stdout).
|
||||
|
||||
swarm db export ~/.ethereum/swarm/bzz-KEY/chunks chunks.tar
|
||||
|
||||
The export may be quite large, consider piping the output through the Unix
|
||||
pv(1) tool to get a progress bar:
|
||||
|
||||
swarm db export ~/.ethereum/swarm/bzz-KEY/chunks - | pv > chunks.tar
|
||||
`,
|
||||
},
|
||||
{
|
||||
Action: dbImport,
|
||||
Name: "import",
|
||||
Usage: "import chunks from a tar archive into a local chunk database (use - to read from stdin)",
|
||||
ArgsUsage: "<chunkdb> <file>",
|
||||
Description: `
|
||||
Import chunks from a tar archive into a local chunk database (use - to read from stdin).
|
||||
|
||||
swarm db import ~/.ethereum/swarm/bzz-KEY/chunks chunks.tar
|
||||
|
||||
The import may be quite large, consider piping the input through the Unix
|
||||
pv(1) tool to get a progress bar:
|
||||
|
||||
pv chunks.tar | swarm db import ~/.ethereum/swarm/bzz-KEY/chunks -
|
||||
`,
|
||||
},
|
||||
{
|
||||
Action: dbClean,
|
||||
Name: "clean",
|
||||
Usage: "remove corrupt entries from a local chunk database",
|
||||
ArgsUsage: "<chunkdb>",
|
||||
Description: `
|
||||
Remove corrupt entries from a local chunk database.
|
||||
`,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Action: func(ctx *cli.Context) {
|
||||
utils.Fatalf("ERROR: 'swarm cleandb' has been removed, please use 'swarm db clean'.")
|
||||
},
|
||||
Name: "cleandb",
|
||||
Usage: "DEPRECATED: use 'swarm db clean'",
|
||||
ArgsUsage: " ",
|
||||
Description: `
|
||||
DEPRECATED: use 'swarm db clean'.
|
||||
`,
|
||||
},
|
||||
// See config.go
|
||||
DumpConfigCommand,
|
||||
}
|
||||
sort.Sort(cli.CommandsByName(app.Commands))
|
||||
|
||||
app.Flags = []cli.Flag{
|
||||
utils.IdentityFlag,
|
||||
utils.DataDirFlag,
|
||||
utils.BootnodesFlag,
|
||||
utils.KeyStoreDirFlag,
|
||||
utils.ListenPortFlag,
|
||||
utils.NoDiscoverFlag,
|
||||
utils.DiscoveryV5Flag,
|
||||
utils.NetrestrictFlag,
|
||||
utils.NodeKeyFileFlag,
|
||||
utils.NodeKeyHexFlag,
|
||||
utils.MaxPeersFlag,
|
||||
utils.NATFlag,
|
||||
utils.IPCDisabledFlag,
|
||||
utils.IPCPathFlag,
|
||||
utils.PasswordFileFlag,
|
||||
// bzzd-specific flags
|
||||
CorsStringFlag,
|
||||
EnsAPIFlag,
|
||||
EnsAddrFlag,
|
||||
SwarmTomlConfigPathFlag,
|
||||
SwarmConfigPathFlag,
|
||||
SwarmSwapEnabledFlag,
|
||||
SwarmSwapAPIFlag,
|
||||
SwarmSyncEnabledFlag,
|
||||
SwarmListenAddrFlag,
|
||||
SwarmPortFlag,
|
||||
SwarmAccountFlag,
|
||||
SwarmNetworkIdFlag,
|
||||
ChequebookAddrFlag,
|
||||
// upload flags
|
||||
SwarmApiFlag,
|
||||
SwarmRecursiveUploadFlag,
|
||||
SwarmWantManifestFlag,
|
||||
SwarmUploadDefaultPath,
|
||||
SwarmUpFromStdinFlag,
|
||||
SwarmUploadMimeType,
|
||||
//deprecated flags
|
||||
DeprecatedEthAPIFlag,
|
||||
}
|
||||
app.Flags = append(app.Flags, debug.Flags...)
|
||||
app.Before = func(ctx *cli.Context) error {
|
||||
runtime.GOMAXPROCS(runtime.NumCPU())
|
||||
return debug.Setup(ctx)
|
||||
}
|
||||
app.After = func(ctx *cli.Context) error {
|
||||
debug.Exit()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
if err := app.Run(os.Args); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func version(ctx *cli.Context) error {
|
||||
fmt.Println(strings.Title(clientIdentifier))
|
||||
fmt.Println("Version:", params.Version)
|
||||
if gitCommit != "" {
|
||||
fmt.Println("Git Commit:", gitCommit)
|
||||
}
|
||||
fmt.Println("Network Id:", ctx.GlobalInt(utils.NetworkIdFlag.Name))
|
||||
fmt.Println("Go Version:", runtime.Version())
|
||||
fmt.Println("OS:", runtime.GOOS)
|
||||
fmt.Printf("GOPATH=%s\n", os.Getenv("GOPATH"))
|
||||
fmt.Printf("GOROOT=%s\n", runtime.GOROOT())
|
||||
return nil
|
||||
}
|
||||
|
||||
func bzzd(ctx *cli.Context) error {
|
||||
//build a valid bzzapi.Config from all available sources:
|
||||
//default config, file config, command line and env vars
|
||||
bzzconfig, err := buildConfig(ctx)
|
||||
if err != nil {
|
||||
utils.Fatalf("unable to configure swarm: %v", err)
|
||||
}
|
||||
|
||||
cfg := defaultNodeConfig
|
||||
//geth only supports --datadir via command line
|
||||
//in order to be consistent within swarm, if we pass --datadir via environment variable
|
||||
//or via config file, we get the same directory for geth and swarm
|
||||
if _, err := os.Stat(bzzconfig.Path); err == nil {
|
||||
cfg.DataDir = bzzconfig.Path
|
||||
}
|
||||
//setup the ethereum node
|
||||
utils.SetNodeConfig(ctx, &cfg)
|
||||
stack, err := node.New(&cfg)
|
||||
if err != nil {
|
||||
utils.Fatalf("can't create node: %v", err)
|
||||
}
|
||||
//a few steps need to be done after the config phase is completed,
|
||||
//due to overriding behavior
|
||||
initSwarmNode(bzzconfig, stack, ctx)
|
||||
//register BZZ as node.Service in the ethereum node
|
||||
registerBzzService(bzzconfig, ctx, stack)
|
||||
//start the node
|
||||
utils.StartNode(stack)
|
||||
|
||||
go func() {
|
||||
sigc := make(chan os.Signal, 1)
|
||||
signal.Notify(sigc, syscall.SIGTERM)
|
||||
defer signal.Stop(sigc)
|
||||
<-sigc
|
||||
log.Info("Got sigterm, shutting swarm down...")
|
||||
stack.Stop()
|
||||
}()
|
||||
|
||||
// Add bootnodes as initial peers.
|
||||
if bzzconfig.BootNodes != "" {
|
||||
bootnodes := strings.Split(bzzconfig.BootNodes, ",")
|
||||
injectBootnodes(stack.Server(), bootnodes)
|
||||
} else {
|
||||
if bzzconfig.NetworkId == 3 {
|
||||
injectBootnodes(stack.Server(), testbetBootNodes)
|
||||
}
|
||||
}
|
||||
|
||||
stack.Wait()
|
||||
return nil
|
||||
}
|
||||
|
||||
// detectEnsAddr determines the ENS contract address by getting both the
|
||||
// version and genesis hash using the client and matching them to either
|
||||
// mainnet or testnet addresses
|
||||
func detectEnsAddr(client *rpc.Client) (common.Address, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var version string
|
||||
if err := client.CallContext(ctx, &version, "net_version"); err != nil {
|
||||
return common.Address{}, err
|
||||
}
|
||||
|
||||
block, err := ethclient.NewClient(client).BlockByNumber(ctx, big.NewInt(0))
|
||||
if err != nil {
|
||||
return common.Address{}, err
|
||||
}
|
||||
|
||||
switch {
|
||||
|
||||
case version == "1" && block.Hash() == params.MainnetGenesisHash:
|
||||
log.Info("using Mainnet ENS contract address", "addr", ens.MainNetAddress)
|
||||
return ens.MainNetAddress, nil
|
||||
|
||||
case version == "3" && block.Hash() == params.TestnetGenesisHash:
|
||||
log.Info("using Testnet ENS contract address", "addr", ens.TestNetAddress)
|
||||
return ens.TestNetAddress, nil
|
||||
|
||||
default:
|
||||
return common.Address{}, fmt.Errorf("unknown version and genesis hash: %s %s", version, block.Hash())
|
||||
}
|
||||
}
|
||||
|
||||
func registerBzzService(bzzconfig *bzzapi.Config, ctx *cli.Context, stack *node.Node) {
|
||||
|
||||
//define the swarm service boot function
|
||||
boot := func(ctx *node.ServiceContext) (node.Service, error) {
|
||||
var swapClient *ethclient.Client
|
||||
var err error
|
||||
if bzzconfig.SwapApi != "" {
|
||||
log.Info("connecting to SWAP API", "url", bzzconfig.SwapApi)
|
||||
swapClient, err = ethclient.Dial(bzzconfig.SwapApi)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error connecting to SWAP API %s: %s", bzzconfig.SwapApi, err)
|
||||
}
|
||||
}
|
||||
|
||||
var ensClient *ethclient.Client
|
||||
if bzzconfig.EnsApi != "" {
|
||||
log.Info("connecting to ENS API", "url", bzzconfig.EnsApi)
|
||||
client, err := rpc.Dial(bzzconfig.EnsApi)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error connecting to ENS API %s: %s", bzzconfig.EnsApi, err)
|
||||
}
|
||||
ensClient = ethclient.NewClient(client)
|
||||
|
||||
//no ENS root address set yet
|
||||
if bzzconfig.EnsRoot == (common.Address{}) {
|
||||
ensAddr, err := detectEnsAddr(client)
|
||||
if err == nil {
|
||||
bzzconfig.EnsRoot = ensAddr
|
||||
} else {
|
||||
log.Warn(fmt.Sprintf("could not determine ENS contract address, using default %s", bzzconfig.EnsRoot), "err", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return swarm.NewSwarm(ctx, swapClient, ensClient, bzzconfig, bzzconfig.SwapEnabled, bzzconfig.SyncEnabled, bzzconfig.Cors)
|
||||
}
|
||||
//register within the ethereum node
|
||||
if err := stack.Register(boot); err != nil {
|
||||
utils.Fatalf("Failed to register the Swarm service: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func getAccount(bzzaccount string, ctx *cli.Context, stack *node.Node) *ecdsa.PrivateKey {
|
||||
//an account is mandatory
|
||||
if bzzaccount == "" {
|
||||
utils.Fatalf(SWARM_ERR_NO_BZZACCOUNT)
|
||||
}
|
||||
// Try to load the arg as a hex key file.
|
||||
if key, err := crypto.LoadECDSA(bzzaccount); err == nil {
|
||||
log.Info("Swarm account key loaded", "address", crypto.PubkeyToAddress(key.PublicKey))
|
||||
return key
|
||||
}
|
||||
// Otherwise try getting it from the keystore.
|
||||
am := stack.AccountManager()
|
||||
ks := am.Backends(keystore.KeyStoreType)[0].(*keystore.KeyStore)
|
||||
|
||||
return decryptStoreAccount(ks, bzzaccount, utils.MakePasswordList(ctx))
|
||||
}
|
||||
|
||||
func decryptStoreAccount(ks *keystore.KeyStore, account string, passwords []string) *ecdsa.PrivateKey {
|
||||
var a accounts.Account
|
||||
var err error
|
||||
if common.IsHexAddress(account) {
|
||||
a, err = ks.Find(accounts.Account{Address: common.HexToAddress(account)})
|
||||
} else if ix, ixerr := strconv.Atoi(account); ixerr == nil && ix > 0 {
|
||||
if accounts := ks.Accounts(); len(accounts) > ix {
|
||||
a = accounts[ix]
|
||||
} else {
|
||||
err = fmt.Errorf("index %d higher than number of accounts %d", ix, len(accounts))
|
||||
}
|
||||
} else {
|
||||
utils.Fatalf("Can't find swarm account key %s", account)
|
||||
}
|
||||
if err != nil {
|
||||
utils.Fatalf("Can't find swarm account key: %v - Is the provided bzzaccount(%s) from the right datadir/Path?", err, account)
|
||||
}
|
||||
keyjson, err := ioutil.ReadFile(a.URL.Path)
|
||||
if err != nil {
|
||||
utils.Fatalf("Can't load swarm account key: %v", err)
|
||||
}
|
||||
for i := 0; i < 3; i++ {
|
||||
password := getPassPhrase(fmt.Sprintf("Unlocking swarm account %s [%d/3]", a.Address.Hex(), i+1), i, passwords)
|
||||
key, err := keystore.DecryptKey(keyjson, password)
|
||||
if err == nil {
|
||||
return key.PrivateKey
|
||||
}
|
||||
}
|
||||
utils.Fatalf("Can't decrypt swarm account key")
|
||||
return nil
|
||||
}
|
||||
|
||||
// getPassPhrase retrieves the password associated with bzz account, either by fetching
|
||||
// from a list of pre-loaded passwords, or by requesting it interactively from user.
|
||||
func getPassPhrase(prompt string, i int, passwords []string) string {
|
||||
// non-interactive
|
||||
if len(passwords) > 0 {
|
||||
if i < len(passwords) {
|
||||
return passwords[i]
|
||||
}
|
||||
return passwords[len(passwords)-1]
|
||||
}
|
||||
|
||||
// fallback to interactive mode
|
||||
if prompt != "" {
|
||||
fmt.Println(prompt)
|
||||
}
|
||||
password, err := console.Stdin.PromptPassword("Passphrase: ")
|
||||
if err != nil {
|
||||
utils.Fatalf("Failed to read passphrase: %v", err)
|
||||
}
|
||||
return password
|
||||
}
|
||||
|
||||
func injectBootnodes(srv *p2p.Server, nodes []string) {
|
||||
for _, url := range nodes {
|
||||
n, err := discover.ParseNode(url)
|
||||
if err != nil {
|
||||
log.Error("Invalid swarm bootnode", "err", err)
|
||||
continue
|
||||
}
|
||||
srv.AddPeer(n)
|
||||
}
|
||||
}
|
||||
331
cmd/swarm/manifest.go
Normal file
331
cmd/swarm/manifest.go
Normal file
@@ -0,0 +1,331 @@
|
||||
// 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/>.
|
||||
|
||||
// Command MANIFEST update
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"mime"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/ethereum/go-ethereum/cmd/utils"
|
||||
"github.com/ethereum/go-ethereum/swarm/api"
|
||||
swarm "github.com/ethereum/go-ethereum/swarm/api/client"
|
||||
"gopkg.in/urfave/cli.v1"
|
||||
)
|
||||
|
||||
const bzzManifestJSON = "application/bzz-manifest+json"
|
||||
|
||||
func add(ctx *cli.Context) {
|
||||
args := ctx.Args()
|
||||
if len(args) < 3 {
|
||||
utils.Fatalf("Need atleast three arguments <MHASH> <path> <HASH> [<content-type>]")
|
||||
}
|
||||
|
||||
var (
|
||||
mhash = args[0]
|
||||
path = args[1]
|
||||
hash = args[2]
|
||||
|
||||
ctype string
|
||||
wantManifest = ctx.GlobalBoolT(SwarmWantManifestFlag.Name)
|
||||
mroot api.Manifest
|
||||
)
|
||||
|
||||
if len(args) > 3 {
|
||||
ctype = args[3]
|
||||
} else {
|
||||
ctype = mime.TypeByExtension(filepath.Ext(path))
|
||||
}
|
||||
|
||||
newManifest := addEntryToManifest(ctx, mhash, path, hash, ctype)
|
||||
fmt.Println(newManifest)
|
||||
|
||||
if !wantManifest {
|
||||
// Print the manifest. This is the only output to stdout.
|
||||
mrootJSON, _ := json.MarshalIndent(mroot, "", " ")
|
||||
fmt.Println(string(mrootJSON))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func update(ctx *cli.Context) {
|
||||
|
||||
args := ctx.Args()
|
||||
if len(args) < 3 {
|
||||
utils.Fatalf("Need atleast three arguments <MHASH> <path> <HASH>")
|
||||
}
|
||||
|
||||
var (
|
||||
mhash = args[0]
|
||||
path = args[1]
|
||||
hash = args[2]
|
||||
|
||||
ctype string
|
||||
wantManifest = ctx.GlobalBoolT(SwarmWantManifestFlag.Name)
|
||||
mroot api.Manifest
|
||||
)
|
||||
if len(args) > 3 {
|
||||
ctype = args[3]
|
||||
} else {
|
||||
ctype = mime.TypeByExtension(filepath.Ext(path))
|
||||
}
|
||||
|
||||
newManifest := updateEntryInManifest(ctx, mhash, path, hash, ctype)
|
||||
fmt.Println(newManifest)
|
||||
|
||||
if !wantManifest {
|
||||
// Print the manifest. This is the only output to stdout.
|
||||
mrootJSON, _ := json.MarshalIndent(mroot, "", " ")
|
||||
fmt.Println(string(mrootJSON))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func remove(ctx *cli.Context) {
|
||||
args := ctx.Args()
|
||||
if len(args) < 2 {
|
||||
utils.Fatalf("Need atleast two arguments <MHASH> <path>")
|
||||
}
|
||||
|
||||
var (
|
||||
mhash = args[0]
|
||||
path = args[1]
|
||||
|
||||
wantManifest = ctx.GlobalBoolT(SwarmWantManifestFlag.Name)
|
||||
mroot api.Manifest
|
||||
)
|
||||
|
||||
newManifest := removeEntryFromManifest(ctx, mhash, path)
|
||||
fmt.Println(newManifest)
|
||||
|
||||
if !wantManifest {
|
||||
// Print the manifest. This is the only output to stdout.
|
||||
mrootJSON, _ := json.MarshalIndent(mroot, "", " ")
|
||||
fmt.Println(string(mrootJSON))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func addEntryToManifest(ctx *cli.Context, mhash, path, hash, ctype string) string {
|
||||
|
||||
var (
|
||||
bzzapi = strings.TrimRight(ctx.GlobalString(SwarmApiFlag.Name), "/")
|
||||
client = swarm.NewClient(bzzapi)
|
||||
longestPathEntry = api.ManifestEntry{}
|
||||
)
|
||||
|
||||
mroot, err := client.DownloadManifest(mhash)
|
||||
if err != nil {
|
||||
utils.Fatalf("Manifest download failed: %v", err)
|
||||
}
|
||||
|
||||
//TODO: check if the "hash" to add is valid and present in swarm
|
||||
_, err = client.DownloadManifest(hash)
|
||||
if err != nil {
|
||||
utils.Fatalf("Hash to add is not present: %v", err)
|
||||
}
|
||||
|
||||
// See if we path is in this Manifest or do we have to dig deeper
|
||||
for _, entry := range mroot.Entries {
|
||||
if path == entry.Path {
|
||||
utils.Fatalf("Path %s already present, not adding anything", path)
|
||||
} else {
|
||||
if entry.ContentType == bzzManifestJSON {
|
||||
prfxlen := strings.HasPrefix(path, entry.Path)
|
||||
if prfxlen && len(path) > len(longestPathEntry.Path) {
|
||||
longestPathEntry = entry
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if longestPathEntry.Path != "" {
|
||||
// Load the child Manifest add the entry there
|
||||
newPath := path[len(longestPathEntry.Path):]
|
||||
newHash := addEntryToManifest(ctx, longestPathEntry.Hash, newPath, hash, ctype)
|
||||
|
||||
// Replace the hash for parent Manifests
|
||||
newMRoot := &api.Manifest{}
|
||||
for _, entry := range mroot.Entries {
|
||||
if longestPathEntry.Path == entry.Path {
|
||||
entry.Hash = newHash
|
||||
}
|
||||
newMRoot.Entries = append(newMRoot.Entries, entry)
|
||||
}
|
||||
mroot = newMRoot
|
||||
} else {
|
||||
// Add the entry in the leaf Manifest
|
||||
newEntry := api.ManifestEntry{
|
||||
Hash: hash,
|
||||
Path: path,
|
||||
ContentType: ctype,
|
||||
}
|
||||
mroot.Entries = append(mroot.Entries, newEntry)
|
||||
}
|
||||
|
||||
newManifestHash, err := client.UploadManifest(mroot)
|
||||
if err != nil {
|
||||
utils.Fatalf("Manifest upload failed: %v", err)
|
||||
}
|
||||
return newManifestHash
|
||||
|
||||
}
|
||||
|
||||
func updateEntryInManifest(ctx *cli.Context, mhash, path, hash, ctype string) string {
|
||||
|
||||
var (
|
||||
bzzapi = strings.TrimRight(ctx.GlobalString(SwarmApiFlag.Name), "/")
|
||||
client = swarm.NewClient(bzzapi)
|
||||
newEntry = api.ManifestEntry{}
|
||||
longestPathEntry = api.ManifestEntry{}
|
||||
)
|
||||
|
||||
mroot, err := client.DownloadManifest(mhash)
|
||||
if err != nil {
|
||||
utils.Fatalf("Manifest download failed: %v", err)
|
||||
}
|
||||
|
||||
//TODO: check if the "hash" with which to update is valid and present in swarm
|
||||
|
||||
// See if we path is in this Manifest or do we have to dig deeper
|
||||
for _, entry := range mroot.Entries {
|
||||
if path == entry.Path {
|
||||
newEntry = entry
|
||||
} else {
|
||||
if entry.ContentType == bzzManifestJSON {
|
||||
prfxlen := strings.HasPrefix(path, entry.Path)
|
||||
if prfxlen && len(path) > len(longestPathEntry.Path) {
|
||||
longestPathEntry = entry
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if longestPathEntry.Path == "" && newEntry.Path == "" {
|
||||
utils.Fatalf("Path %s not present in the Manifest, not setting anything", path)
|
||||
}
|
||||
|
||||
if longestPathEntry.Path != "" {
|
||||
// Load the child Manifest add the entry there
|
||||
newPath := path[len(longestPathEntry.Path):]
|
||||
newHash := updateEntryInManifest(ctx, longestPathEntry.Hash, newPath, hash, ctype)
|
||||
|
||||
// Replace the hash for parent Manifests
|
||||
newMRoot := &api.Manifest{}
|
||||
for _, entry := range mroot.Entries {
|
||||
if longestPathEntry.Path == entry.Path {
|
||||
entry.Hash = newHash
|
||||
}
|
||||
newMRoot.Entries = append(newMRoot.Entries, entry)
|
||||
|
||||
}
|
||||
mroot = newMRoot
|
||||
}
|
||||
|
||||
if newEntry.Path != "" {
|
||||
// Replace the hash for leaf Manifest
|
||||
newMRoot := &api.Manifest{}
|
||||
for _, entry := range mroot.Entries {
|
||||
if newEntry.Path == entry.Path {
|
||||
myEntry := api.ManifestEntry{
|
||||
Hash: hash,
|
||||
Path: entry.Path,
|
||||
ContentType: ctype,
|
||||
}
|
||||
newMRoot.Entries = append(newMRoot.Entries, myEntry)
|
||||
} else {
|
||||
newMRoot.Entries = append(newMRoot.Entries, entry)
|
||||
}
|
||||
}
|
||||
mroot = newMRoot
|
||||
}
|
||||
|
||||
newManifestHash, err := client.UploadManifest(mroot)
|
||||
if err != nil {
|
||||
utils.Fatalf("Manifest upload failed: %v", err)
|
||||
}
|
||||
return newManifestHash
|
||||
}
|
||||
|
||||
func removeEntryFromManifest(ctx *cli.Context, mhash, path string) string {
|
||||
|
||||
var (
|
||||
bzzapi = strings.TrimRight(ctx.GlobalString(SwarmApiFlag.Name), "/")
|
||||
client = swarm.NewClient(bzzapi)
|
||||
entryToRemove = api.ManifestEntry{}
|
||||
longestPathEntry = api.ManifestEntry{}
|
||||
)
|
||||
|
||||
mroot, err := client.DownloadManifest(mhash)
|
||||
if err != nil {
|
||||
utils.Fatalf("Manifest download failed: %v", err)
|
||||
}
|
||||
|
||||
// See if we path is in this Manifest or do we have to dig deeper
|
||||
for _, entry := range mroot.Entries {
|
||||
if path == entry.Path {
|
||||
entryToRemove = entry
|
||||
} else {
|
||||
if entry.ContentType == bzzManifestJSON {
|
||||
prfxlen := strings.HasPrefix(path, entry.Path)
|
||||
if prfxlen && len(path) > len(longestPathEntry.Path) {
|
||||
longestPathEntry = entry
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if longestPathEntry.Path == "" && entryToRemove.Path == "" {
|
||||
utils.Fatalf("Path %s not present in the Manifest, not removing anything", path)
|
||||
}
|
||||
|
||||
if longestPathEntry.Path != "" {
|
||||
// Load the child Manifest remove the entry there
|
||||
newPath := path[len(longestPathEntry.Path):]
|
||||
newHash := removeEntryFromManifest(ctx, longestPathEntry.Hash, newPath)
|
||||
|
||||
// Replace the hash for parent Manifests
|
||||
newMRoot := &api.Manifest{}
|
||||
for _, entry := range mroot.Entries {
|
||||
if longestPathEntry.Path == entry.Path {
|
||||
entry.Hash = newHash
|
||||
}
|
||||
newMRoot.Entries = append(newMRoot.Entries, entry)
|
||||
}
|
||||
mroot = newMRoot
|
||||
}
|
||||
|
||||
if entryToRemove.Path != "" {
|
||||
// remove the entry in this Manifest
|
||||
newMRoot := &api.Manifest{}
|
||||
for _, entry := range mroot.Entries {
|
||||
if entryToRemove.Path != entry.Path {
|
||||
newMRoot.Entries = append(newMRoot.Entries, entry)
|
||||
}
|
||||
}
|
||||
mroot = newMRoot
|
||||
}
|
||||
|
||||
newManifestHash, err := client.UploadManifest(mroot)
|
||||
if err != nil {
|
||||
utils.Fatalf("Manifest upload failed: %v", err)
|
||||
}
|
||||
return newManifestHash
|
||||
}
|
||||
263
cmd/swarm/run_test.go
Normal file
263
cmd/swarm/run_test.go
Normal file
@@ -0,0 +1,263 @@
|
||||
// Copyright 2016 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 (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/pkg/reexec"
|
||||
"github.com/ethereum/go-ethereum/accounts"
|
||||
"github.com/ethereum/go-ethereum/accounts/keystore"
|
||||
"github.com/ethereum/go-ethereum/internal/cmdtest"
|
||||
"github.com/ethereum/go-ethereum/node"
|
||||
"github.com/ethereum/go-ethereum/p2p"
|
||||
"github.com/ethereum/go-ethereum/rpc"
|
||||
"github.com/ethereum/go-ethereum/swarm"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Run the app if we've been exec'd as "swarm-test" in runSwarm.
|
||||
reexec.Register("swarm-test", func() {
|
||||
if err := app.Run(os.Args); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(0)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
// check if we have been reexec'd
|
||||
if reexec.Init() {
|
||||
return
|
||||
}
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
func runSwarm(t *testing.T, args ...string) *cmdtest.TestCmd {
|
||||
tt := cmdtest.NewTestCmd(t, nil)
|
||||
|
||||
// Boot "swarm". This actually runs the test binary but the TestMain
|
||||
// function will prevent any tests from running.
|
||||
tt.Run("swarm-test", args...)
|
||||
|
||||
return tt
|
||||
}
|
||||
|
||||
type testCluster struct {
|
||||
Nodes []*testNode
|
||||
TmpDir string
|
||||
}
|
||||
|
||||
// newTestCluster starts a test swarm cluster of the given size.
|
||||
//
|
||||
// A temporary directory is created and each node gets a data directory inside
|
||||
// it.
|
||||
//
|
||||
// Each node listens on 127.0.0.1 with random ports for both the HTTP and p2p
|
||||
// ports (assigned by first listening on 127.0.0.1:0 and then passing the ports
|
||||
// as flags).
|
||||
//
|
||||
// When starting more than one node, they are connected together using the
|
||||
// admin SetPeer RPC method.
|
||||
func newTestCluster(t *testing.T, size int) *testCluster {
|
||||
cluster := &testCluster{}
|
||||
defer func() {
|
||||
if t.Failed() {
|
||||
cluster.Shutdown()
|
||||
}
|
||||
}()
|
||||
|
||||
tmpdir, err := ioutil.TempDir("", "swarm-test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cluster.TmpDir = tmpdir
|
||||
|
||||
// start the nodes
|
||||
cluster.Nodes = make([]*testNode, 0, size)
|
||||
for i := 0; i < size; i++ {
|
||||
dir := filepath.Join(cluster.TmpDir, fmt.Sprintf("swarm%02d", i))
|
||||
if err := os.Mkdir(dir, 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
node := newTestNode(t, dir)
|
||||
node.Name = fmt.Sprintf("swarm%02d", i)
|
||||
|
||||
cluster.Nodes = append(cluster.Nodes, node)
|
||||
}
|
||||
|
||||
if size == 1 {
|
||||
return cluster
|
||||
}
|
||||
|
||||
// connect the nodes together
|
||||
for _, node := range cluster.Nodes {
|
||||
if err := node.Client.Call(nil, "admin_addPeer", cluster.Nodes[0].Enode); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// wait until all nodes have the correct number of peers
|
||||
outer:
|
||||
for _, node := range cluster.Nodes {
|
||||
var peers []*p2p.PeerInfo
|
||||
for start := time.Now(); time.Since(start) < time.Minute; time.Sleep(50 * time.Millisecond) {
|
||||
if err := node.Client.Call(&peers, "admin_peers"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(peers) == len(cluster.Nodes)-1 {
|
||||
continue outer
|
||||
}
|
||||
}
|
||||
t.Fatalf("%s only has %d / %d peers", node.Name, len(peers), len(cluster.Nodes)-1)
|
||||
}
|
||||
|
||||
return cluster
|
||||
}
|
||||
|
||||
func (c *testCluster) Shutdown() {
|
||||
for _, node := range c.Nodes {
|
||||
node.Shutdown()
|
||||
}
|
||||
os.RemoveAll(c.TmpDir)
|
||||
}
|
||||
|
||||
type testNode struct {
|
||||
Name string
|
||||
Addr string
|
||||
URL string
|
||||
Enode string
|
||||
Dir string
|
||||
Client *rpc.Client
|
||||
Cmd *cmdtest.TestCmd
|
||||
}
|
||||
|
||||
const testPassphrase = "swarm-test-passphrase"
|
||||
|
||||
func getTestAccount(t *testing.T, dir string) (conf *node.Config, account accounts.Account) {
|
||||
// create key
|
||||
conf = &node.Config{
|
||||
DataDir: dir,
|
||||
IPCPath: "bzzd.ipc",
|
||||
NoUSB: true,
|
||||
}
|
||||
n, err := node.New(conf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
account, err = n.AccountManager().Backends(keystore.KeyStoreType)[0].(*keystore.KeyStore).NewAccount(testPassphrase)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// use a unique IPCPath when running tests on Windows
|
||||
if runtime.GOOS == "windows" {
|
||||
conf.IPCPath = fmt.Sprintf("bzzd-%s.ipc", account.Address.String())
|
||||
}
|
||||
|
||||
return conf, account
|
||||
}
|
||||
|
||||
func newTestNode(t *testing.T, dir string) *testNode {
|
||||
|
||||
conf, account := getTestAccount(t, dir)
|
||||
node := &testNode{Dir: dir}
|
||||
|
||||
// assign ports
|
||||
httpPort, err := assignTCPPort()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
p2pPort, err := assignTCPPort()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// start the node
|
||||
node.Cmd = runSwarm(t,
|
||||
"--port", p2pPort,
|
||||
"--nodiscover",
|
||||
"--datadir", dir,
|
||||
"--ipcpath", conf.IPCPath,
|
||||
"--ens-api", "",
|
||||
"--bzzaccount", account.Address.String(),
|
||||
"--bzznetworkid", "321",
|
||||
"--bzzport", httpPort,
|
||||
"--verbosity", "6",
|
||||
)
|
||||
node.Cmd.InputLine(testPassphrase)
|
||||
defer func() {
|
||||
if t.Failed() {
|
||||
node.Shutdown()
|
||||
}
|
||||
}()
|
||||
|
||||
// wait for the node to start
|
||||
for start := time.Now(); time.Since(start) < 10*time.Second; time.Sleep(50 * time.Millisecond) {
|
||||
node.Client, err = rpc.Dial(conf.IPCEndpoint())
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
if node.Client == nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// load info
|
||||
var info swarm.Info
|
||||
if err := node.Client.Call(&info, "bzz_info"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
node.Addr = net.JoinHostPort("127.0.0.1", info.Port)
|
||||
node.URL = "http://" + node.Addr
|
||||
|
||||
var nodeInfo p2p.NodeInfo
|
||||
if err := node.Client.Call(&nodeInfo, "admin_nodeInfo"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
node.Enode = fmt.Sprintf("enode://%s@127.0.0.1:%s", nodeInfo.ID, p2pPort)
|
||||
|
||||
return node
|
||||
}
|
||||
|
||||
func (n *testNode) Shutdown() {
|
||||
if n.Cmd != nil {
|
||||
n.Cmd.Kill()
|
||||
}
|
||||
}
|
||||
|
||||
func assignTCPPort() (string, error) {
|
||||
l, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
l.Close()
|
||||
_, port, err := net.SplitHostPort(l.Addr().String())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return port, nil
|
||||
}
|
||||
161
cmd/swarm/upload.go
Normal file
161
cmd/swarm/upload.go
Normal file
@@ -0,0 +1,161 @@
|
||||
// Copyright 2016 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/>.
|
||||
|
||||
// Command bzzup uploads files to the swarm HTTP API.
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"mime"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/user"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/ethereum/go-ethereum/cmd/utils"
|
||||
swarm "github.com/ethereum/go-ethereum/swarm/api/client"
|
||||
"gopkg.in/urfave/cli.v1"
|
||||
)
|
||||
|
||||
func upload(ctx *cli.Context) {
|
||||
|
||||
args := ctx.Args()
|
||||
var (
|
||||
bzzapi = strings.TrimRight(ctx.GlobalString(SwarmApiFlag.Name), "/")
|
||||
recursive = ctx.GlobalBool(SwarmRecursiveUploadFlag.Name)
|
||||
wantManifest = ctx.GlobalBoolT(SwarmWantManifestFlag.Name)
|
||||
defaultPath = ctx.GlobalString(SwarmUploadDefaultPath.Name)
|
||||
fromStdin = ctx.GlobalBool(SwarmUpFromStdinFlag.Name)
|
||||
mimeType = ctx.GlobalString(SwarmUploadMimeType.Name)
|
||||
client = swarm.NewClient(bzzapi)
|
||||
file string
|
||||
)
|
||||
|
||||
if len(args) != 1 {
|
||||
if fromStdin {
|
||||
tmp, err := ioutil.TempFile("", "swarm-stdin")
|
||||
if err != nil {
|
||||
utils.Fatalf("error create tempfile: %s", err)
|
||||
}
|
||||
defer os.Remove(tmp.Name())
|
||||
n, err := io.Copy(tmp, os.Stdin)
|
||||
if err != nil {
|
||||
utils.Fatalf("error copying stdin to tempfile: %s", err)
|
||||
} else if n == 0 {
|
||||
utils.Fatalf("error reading from stdin: zero length")
|
||||
}
|
||||
file = tmp.Name()
|
||||
} else {
|
||||
utils.Fatalf("Need filename as the first and only argument")
|
||||
}
|
||||
} else {
|
||||
file = expandPath(args[0])
|
||||
}
|
||||
|
||||
if !wantManifest {
|
||||
f, err := swarm.Open(file)
|
||||
if err != nil {
|
||||
utils.Fatalf("Error opening file: %s", err)
|
||||
}
|
||||
defer f.Close()
|
||||
hash, err := client.UploadRaw(f, f.Size)
|
||||
if err != nil {
|
||||
utils.Fatalf("Upload failed: %s", err)
|
||||
}
|
||||
fmt.Println(hash)
|
||||
return
|
||||
}
|
||||
|
||||
stat, err := os.Stat(file)
|
||||
if err != nil {
|
||||
utils.Fatalf("Error opening file: %s", err)
|
||||
}
|
||||
|
||||
// define a function which either uploads a directory or single file
|
||||
// based on the type of the file being uploaded
|
||||
var doUpload func() (hash string, err error)
|
||||
if stat.IsDir() {
|
||||
doUpload = func() (string, error) {
|
||||
if !recursive {
|
||||
return "", errors.New("Argument is a directory and recursive upload is disabled")
|
||||
}
|
||||
return client.UploadDirectory(file, defaultPath, "")
|
||||
}
|
||||
} else {
|
||||
doUpload = func() (string, error) {
|
||||
f, err := swarm.Open(file)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error opening file: %s", err)
|
||||
}
|
||||
defer f.Close()
|
||||
if mimeType == "" {
|
||||
mimeType = detectMimeType(file)
|
||||
}
|
||||
f.ContentType = mimeType
|
||||
return client.Upload(f, "")
|
||||
}
|
||||
}
|
||||
hash, err := doUpload()
|
||||
if err != nil {
|
||||
utils.Fatalf("Upload failed: %s", err)
|
||||
}
|
||||
fmt.Println(hash)
|
||||
}
|
||||
|
||||
// Expands a file path
|
||||
// 1. replace tilde with users home dir
|
||||
// 2. expands embedded environment variables
|
||||
// 3. cleans the path, e.g. /a/b/../c -> /a/c
|
||||
// Note, it has limitations, e.g. ~someuser/tmp will not be expanded
|
||||
func expandPath(p string) string {
|
||||
if strings.HasPrefix(p, "~/") || strings.HasPrefix(p, "~\\") {
|
||||
if home := homeDir(); home != "" {
|
||||
p = home + p[1:]
|
||||
}
|
||||
}
|
||||
return path.Clean(os.ExpandEnv(p))
|
||||
}
|
||||
|
||||
func homeDir() string {
|
||||
if home := os.Getenv("HOME"); home != "" {
|
||||
return home
|
||||
}
|
||||
if usr, err := user.Current(); err == nil {
|
||||
return usr.HomeDir
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func detectMimeType(file string) string {
|
||||
if ext := filepath.Ext(file); ext != "" {
|
||||
return mime.TypeByExtension(ext)
|
||||
}
|
||||
f, err := os.Open(file)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
defer f.Close()
|
||||
buf := make([]byte, 512)
|
||||
if n, _ := f.Read(buf); n > 0 {
|
||||
return http.DetectContentType(buf)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
76
cmd/swarm/upload_test.go
Normal file
76
cmd/swarm/upload_test.go
Normal file
@@ -0,0 +1,76 @@
|
||||
// Copyright 2016 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 (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestCLISwarmUp tests that running 'swarm up' makes the resulting file
|
||||
// available from all nodes via the HTTP API
|
||||
func TestCLISwarmUp(t *testing.T) {
|
||||
// start 3 node cluster
|
||||
t.Log("starting 3 node cluster")
|
||||
cluster := newTestCluster(t, 3)
|
||||
defer cluster.Shutdown()
|
||||
|
||||
// create a tmp file
|
||||
tmp, err := ioutil.TempFile("", "swarm-test")
|
||||
assertNil(t, err)
|
||||
defer tmp.Close()
|
||||
defer os.Remove(tmp.Name())
|
||||
_, err = io.WriteString(tmp, "data")
|
||||
assertNil(t, err)
|
||||
|
||||
// upload the file with 'swarm up' and expect a hash
|
||||
t.Log("uploading file with 'swarm up'")
|
||||
up := runSwarm(t, "--bzzapi", cluster.Nodes[0].URL, "up", tmp.Name())
|
||||
_, matches := up.ExpectRegexp(`[a-f\d]{64}`)
|
||||
up.ExpectExit()
|
||||
hash := matches[0]
|
||||
t.Logf("file uploaded with hash %s", hash)
|
||||
|
||||
// get the file from the HTTP API of each node
|
||||
for _, node := range cluster.Nodes {
|
||||
t.Logf("getting file from %s", node.Name)
|
||||
res, err := http.Get(node.URL + "/bzz:/" + hash)
|
||||
assertNil(t, err)
|
||||
assertHTTPResponse(t, res, http.StatusOK, "data")
|
||||
}
|
||||
}
|
||||
|
||||
func assertNil(t *testing.T, err error) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func assertHTTPResponse(t *testing.T, res *http.Response, expectedStatus int, expectedBody string) {
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != expectedStatus {
|
||||
t.Fatalf("expected HTTP status %d, got %s", expectedStatus, res.Status)
|
||||
}
|
||||
data, err := ioutil.ReadAll(res.Body)
|
||||
assertNil(t, err)
|
||||
if string(data) != expectedBody {
|
||||
t.Fatalf("expected HTTP body %q, got %q", expectedBody, data)
|
||||
}
|
||||
}
|
||||
187
cmd/utils/cmd.go
187
cmd/utils/cmd.go
@@ -18,156 +18,69 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"compress/gzip"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/big"
|
||||
"os"
|
||||
"os/signal"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/eth"
|
||||
"github.com/ethereum/go-ethereum/logger"
|
||||
"github.com/ethereum/go-ethereum/logger/glog"
|
||||
"github.com/ethereum/go-ethereum/params"
|
||||
"github.com/ethereum/go-ethereum/internal/debug"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/node"
|
||||
"github.com/ethereum/go-ethereum/rlp"
|
||||
"github.com/peterh/liner"
|
||||
)
|
||||
|
||||
const (
|
||||
importBatchSize = 2500
|
||||
)
|
||||
|
||||
var interruptCallbacks = []func(os.Signal){}
|
||||
|
||||
func openLogFile(Datadir string, filename string) *os.File {
|
||||
path := common.AbsolutePath(Datadir, filename)
|
||||
file, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("error opening log file '%s': %v", filename, err))
|
||||
}
|
||||
return file
|
||||
}
|
||||
|
||||
func PromptConfirm(prompt string) (bool, error) {
|
||||
var (
|
||||
input string
|
||||
err error
|
||||
)
|
||||
prompt = prompt + " [y/N] "
|
||||
|
||||
// if liner.TerminalSupported() {
|
||||
// fmt.Println("term")
|
||||
// lr := liner.NewLiner()
|
||||
// defer lr.Close()
|
||||
// input, err = lr.Prompt(prompt)
|
||||
// } else {
|
||||
fmt.Print(prompt)
|
||||
input, err = bufio.NewReader(os.Stdin).ReadString('\n')
|
||||
fmt.Println()
|
||||
// }
|
||||
|
||||
if len(input) > 0 && strings.ToUpper(input[:1]) == "Y" {
|
||||
return true, nil
|
||||
} else {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return false, err
|
||||
}
|
||||
|
||||
func PromptPassword(prompt string, warnTerm bool) (string, error) {
|
||||
if liner.TerminalSupported() {
|
||||
lr := liner.NewLiner()
|
||||
defer lr.Close()
|
||||
return lr.PasswordPrompt(prompt)
|
||||
}
|
||||
if warnTerm {
|
||||
fmt.Println("!! Unsupported terminal, password will be echoed.")
|
||||
}
|
||||
fmt.Print(prompt)
|
||||
input, err := bufio.NewReader(os.Stdin).ReadString('\n')
|
||||
fmt.Println()
|
||||
return input, err
|
||||
}
|
||||
|
||||
func CheckLegalese(datadir string) {
|
||||
// check "first run"
|
||||
if !common.FileExist(datadir) {
|
||||
r, _ := PromptConfirm(legalese)
|
||||
if !r {
|
||||
Fatalf("Must accept to continue. Shutting down...\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fatalf formats a message to standard error and exits the program.
|
||||
// The message is also printed to standard output if standard error
|
||||
// is redirected to a different file.
|
||||
func Fatalf(format string, args ...interface{}) {
|
||||
w := io.MultiWriter(os.Stdout, os.Stderr)
|
||||
outf, _ := os.Stdout.Stat()
|
||||
errf, _ := os.Stderr.Stat()
|
||||
if outf != nil && errf != nil && os.SameFile(outf, errf) {
|
||||
w = os.Stderr
|
||||
if runtime.GOOS == "windows" {
|
||||
// The SameFile check below doesn't work on Windows.
|
||||
// stdout is unlikely to get redirected though, so just print there.
|
||||
w = os.Stdout
|
||||
} else {
|
||||
outf, _ := os.Stdout.Stat()
|
||||
errf, _ := os.Stderr.Stat()
|
||||
if outf != nil && errf != nil && os.SameFile(outf, errf) {
|
||||
w = os.Stderr
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(w, "Fatal: "+format+"\n", args...)
|
||||
logger.Flush()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func StartEthereum(ethereum *eth.Ethereum) {
|
||||
glog.V(logger.Info).Infoln("Starting", ethereum.Name())
|
||||
if err := ethereum.Start(); err != nil {
|
||||
Fatalf("Error starting Ethereum: %v", err)
|
||||
func StartNode(stack *node.Node) {
|
||||
if err := stack.Start(); err != nil {
|
||||
Fatalf("Error starting protocol stack: %v", err)
|
||||
}
|
||||
go func() {
|
||||
sigc := make(chan os.Signal, 1)
|
||||
signal.Notify(sigc, os.Interrupt)
|
||||
defer signal.Stop(sigc)
|
||||
<-sigc
|
||||
glog.V(logger.Info).Infoln("Got interrupt, shutting down...")
|
||||
go ethereum.Stop()
|
||||
logger.Flush()
|
||||
log.Info("Got interrupt, shutting down...")
|
||||
go stack.Stop()
|
||||
for i := 10; i > 0; i-- {
|
||||
<-sigc
|
||||
if i > 1 {
|
||||
glog.V(logger.Info).Infoln("Already shutting down, please be patient.")
|
||||
glog.V(logger.Info).Infoln("Interrupt", i-1, "more times to induce panic.")
|
||||
log.Warn("Already shutting down, interrupt more to panic.", "times", i-1)
|
||||
}
|
||||
}
|
||||
glog.V(logger.Error).Infof("Force quitting: this might not end so well.")
|
||||
panic("boom")
|
||||
debug.Exit() // ensure trace and CPU profile data is flushed.
|
||||
debug.LoudPanic("boom")
|
||||
}()
|
||||
}
|
||||
|
||||
func InitOlympic() {
|
||||
params.DurationLimit = big.NewInt(8)
|
||||
params.GenesisGasLimit = big.NewInt(3141592)
|
||||
params.MinGasLimit = big.NewInt(125000)
|
||||
params.MaximumExtraDataSize = big.NewInt(1024)
|
||||
NetworkIdFlag.Value = 0
|
||||
core.BlockReward = big.NewInt(1.5e+18)
|
||||
}
|
||||
|
||||
func FormatTransactionData(data string) []byte {
|
||||
d := common.StringToByteFunc(data, func(s string) (ret []byte) {
|
||||
slice := regexp.MustCompile("\\n|\\s").Split(s, 1000000000)
|
||||
for _, dataItem := range slice {
|
||||
d := common.FormatData(dataItem)
|
||||
ret = append(ret, d...)
|
||||
}
|
||||
return
|
||||
})
|
||||
|
||||
return d
|
||||
}
|
||||
|
||||
func ImportChain(chain *core.ChainManager, fn string) error {
|
||||
func ImportChain(chain *core.BlockChain, fn string) error {
|
||||
// Watch for Ctrl-C while the import is running.
|
||||
// If a signal is received, the import will stop at the next batch.
|
||||
interrupt := make(chan os.Signal, 1)
|
||||
@@ -177,7 +90,7 @@ func ImportChain(chain *core.ChainManager, fn string) error {
|
||||
defer close(interrupt)
|
||||
go func() {
|
||||
if _, ok := <-interrupt; ok {
|
||||
glog.Info("caught interrupt during import, will stop at next batch")
|
||||
log.Info("Interrupted during import, stopping at next batch")
|
||||
}
|
||||
close(stop)
|
||||
}()
|
||||
@@ -190,13 +103,21 @@ func ImportChain(chain *core.ChainManager, fn string) error {
|
||||
}
|
||||
}
|
||||
|
||||
glog.Infoln("Importing blockchain", fn)
|
||||
log.Info("Importing blockchain", "file", fn)
|
||||
fh, err := os.Open(fn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fh.Close()
|
||||
stream := rlp.NewStream(fh, 0)
|
||||
|
||||
var reader io.Reader = fh
|
||||
if strings.HasSuffix(fn, ".gz") {
|
||||
if reader, err = gzip.NewReader(reader); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
stream := rlp.NewStream(reader, 0)
|
||||
|
||||
// Run actual the import.
|
||||
blocks := make(types.Blocks, importBatchSize)
|
||||
@@ -230,8 +151,7 @@ func ImportChain(chain *core.ChainManager, fn string) error {
|
||||
return fmt.Errorf("interrupted")
|
||||
}
|
||||
if hasAllBlocks(chain, blocks[:i]) {
|
||||
glog.Infof("skipping batch %d, all blocks present [%x / %x]",
|
||||
batch, blocks[0].Hash().Bytes()[:4], blocks[i-1].Hash().Bytes()[:4])
|
||||
log.Info("Skipping batch as all blocks present", "batch", batch, "first", blocks[0].Hash(), "last", blocks[i-1].Hash())
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -242,40 +162,55 @@ func ImportChain(chain *core.ChainManager, fn string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func hasAllBlocks(chain *core.ChainManager, bs []*types.Block) bool {
|
||||
func hasAllBlocks(chain *core.BlockChain, bs []*types.Block) bool {
|
||||
for _, b := range bs {
|
||||
if !chain.HasBlock(b.Hash()) {
|
||||
if !chain.HasBlock(b.Hash(), b.NumberU64()) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func ExportChain(chainmgr *core.ChainManager, fn string) error {
|
||||
glog.Infoln("Exporting blockchain to", fn)
|
||||
func ExportChain(blockchain *core.BlockChain, fn string) error {
|
||||
log.Info("Exporting blockchain", "file", fn)
|
||||
fh, err := os.OpenFile(fn, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.ModePerm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fh.Close()
|
||||
if err := chainmgr.Export(fh); err != nil {
|
||||
|
||||
var writer io.Writer = fh
|
||||
if strings.HasSuffix(fn, ".gz") {
|
||||
writer = gzip.NewWriter(writer)
|
||||
defer writer.(*gzip.Writer).Close()
|
||||
}
|
||||
|
||||
if err := blockchain.Export(writer); err != nil {
|
||||
return err
|
||||
}
|
||||
glog.Infoln("Exported blockchain to", fn)
|
||||
log.Info("Exported blockchain", "file", fn)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ExportAppendChain(chainmgr *core.ChainManager, fn string, first uint64, last uint64) error {
|
||||
glog.Infoln("Exporting blockchain to", fn)
|
||||
func ExportAppendChain(blockchain *core.BlockChain, fn string, first uint64, last uint64) error {
|
||||
log.Info("Exporting blockchain", "file", fn)
|
||||
// TODO verify mode perms
|
||||
fh, err := os.OpenFile(fn, os.O_CREATE|os.O_APPEND|os.O_WRONLY, os.ModePerm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fh.Close()
|
||||
if err := chainmgr.ExportN(fh, first, last); err != nil {
|
||||
|
||||
var writer io.Writer = fh
|
||||
if strings.HasSuffix(fn, ".gz") {
|
||||
writer = gzip.NewWriter(writer)
|
||||
defer writer.(*gzip.Writer).Close()
|
||||
}
|
||||
|
||||
if err := blockchain.ExportN(writer, first, last); err != nil {
|
||||
return err
|
||||
}
|
||||
glog.Infoln("Exported blockchain to", fn)
|
||||
log.Info("Exported blockchain to", "file", fn)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -17,14 +17,18 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"encoding"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/codegangsta/cli"
|
||||
"github.com/ethereum/go-ethereum/common/math"
|
||||
"gopkg.in/urfave/cli.v1"
|
||||
)
|
||||
|
||||
// Custom type which is registered in the flags library which cli uses for
|
||||
@@ -46,24 +50,17 @@ func (self *DirectoryString) Set(value string) error {
|
||||
// Custom cli.Flag type which expand the received string to an absolute path.
|
||||
// e.g. ~/.ethereum -> /home/username/.ethereum
|
||||
type DirectoryFlag struct {
|
||||
cli.GenericFlag
|
||||
Name string
|
||||
Value DirectoryString
|
||||
Usage string
|
||||
EnvVar string
|
||||
Name string
|
||||
Value DirectoryString
|
||||
Usage string
|
||||
}
|
||||
|
||||
func (self DirectoryFlag) String() string {
|
||||
var fmtString string
|
||||
fmtString = "%s %v\t%v"
|
||||
|
||||
fmtString := "%s %v\t%v"
|
||||
if len(self.Value.Value) > 0 {
|
||||
fmtString = "%s \"%v\"\t%v"
|
||||
} else {
|
||||
fmtString = "%s %v\t%v"
|
||||
}
|
||||
|
||||
return withEnvHint(self.EnvVar, fmt.Sprintf(fmtString, prefixedNames(self.Name), self.Value.Value, self.Usage))
|
||||
return fmt.Sprintf(fmtString, prefixedNames(self.Name), self.Value.Value, self.Usage)
|
||||
}
|
||||
|
||||
func eachName(longName string, fn func(string)) {
|
||||
@@ -77,21 +74,117 @@ func eachName(longName string, fn func(string)) {
|
||||
// called by cli library, grabs variable from environment (if in env)
|
||||
// and adds variable to flag set for parsing.
|
||||
func (self DirectoryFlag) Apply(set *flag.FlagSet) {
|
||||
if self.EnvVar != "" {
|
||||
for _, envVar := range strings.Split(self.EnvVar, ",") {
|
||||
envVar = strings.TrimSpace(envVar)
|
||||
if envVal := os.Getenv(envVar); envVal != "" {
|
||||
self.Value.Value = envVal
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
eachName(self.Name, func(name string) {
|
||||
set.Var(&self.Value, self.Name, self.Usage)
|
||||
})
|
||||
}
|
||||
|
||||
type TextMarshaler interface {
|
||||
encoding.TextMarshaler
|
||||
encoding.TextUnmarshaler
|
||||
}
|
||||
|
||||
// textMarshalerVal turns a TextMarshaler into a flag.Value
|
||||
type textMarshalerVal struct {
|
||||
v TextMarshaler
|
||||
}
|
||||
|
||||
func (v textMarshalerVal) String() string {
|
||||
if v.v == nil {
|
||||
return ""
|
||||
}
|
||||
text, _ := v.v.MarshalText()
|
||||
return string(text)
|
||||
}
|
||||
|
||||
func (v textMarshalerVal) Set(s string) error {
|
||||
return v.v.UnmarshalText([]byte(s))
|
||||
}
|
||||
|
||||
// TextMarshalerFlag wraps a TextMarshaler value.
|
||||
type TextMarshalerFlag struct {
|
||||
Name string
|
||||
Value TextMarshaler
|
||||
Usage string
|
||||
}
|
||||
|
||||
func (f TextMarshalerFlag) GetName() string {
|
||||
return f.Name
|
||||
}
|
||||
|
||||
func (f TextMarshalerFlag) String() string {
|
||||
return fmt.Sprintf("%s \"%v\"\t%v", prefixedNames(f.Name), f.Value, f.Usage)
|
||||
}
|
||||
|
||||
func (f TextMarshalerFlag) Apply(set *flag.FlagSet) {
|
||||
eachName(f.Name, func(name string) {
|
||||
set.Var(textMarshalerVal{f.Value}, f.Name, f.Usage)
|
||||
})
|
||||
}
|
||||
|
||||
// GlobalTextMarshaler returns the value of a TextMarshalerFlag from the global flag set.
|
||||
func GlobalTextMarshaler(ctx *cli.Context, name string) TextMarshaler {
|
||||
val := ctx.GlobalGeneric(name)
|
||||
if val == nil {
|
||||
return nil
|
||||
}
|
||||
return val.(textMarshalerVal).v
|
||||
}
|
||||
|
||||
// BigFlag is a command line flag that accepts 256 bit big integers in decimal or
|
||||
// hexadecimal syntax.
|
||||
type BigFlag struct {
|
||||
Name string
|
||||
Value *big.Int
|
||||
Usage string
|
||||
}
|
||||
|
||||
// bigValue turns *big.Int into a flag.Value
|
||||
type bigValue big.Int
|
||||
|
||||
func (b *bigValue) String() string {
|
||||
if b == nil {
|
||||
return ""
|
||||
}
|
||||
return (*big.Int)(b).String()
|
||||
}
|
||||
|
||||
func (b *bigValue) Set(s string) error {
|
||||
int, ok := math.ParseBig256(s)
|
||||
if !ok {
|
||||
return errors.New("invalid integer syntax")
|
||||
}
|
||||
*b = (bigValue)(*int)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f BigFlag) GetName() string {
|
||||
return f.Name
|
||||
}
|
||||
|
||||
func (f BigFlag) String() string {
|
||||
fmtString := "%s %v\t%v"
|
||||
if f.Value != nil {
|
||||
fmtString = "%s \"%v\"\t%v"
|
||||
}
|
||||
return fmt.Sprintf(fmtString, prefixedNames(f.Name), f.Value, f.Usage)
|
||||
}
|
||||
|
||||
func (f BigFlag) Apply(set *flag.FlagSet) {
|
||||
eachName(f.Name, func(name string) {
|
||||
set.Var((*bigValue)(f.Value), f.Name, f.Usage)
|
||||
})
|
||||
}
|
||||
|
||||
// GlobalBig returns the value of a BigFlag from the global flag set.
|
||||
func GlobalBig(ctx *cli.Context, name string) *big.Int {
|
||||
val := ctx.GlobalGeneric(name)
|
||||
if val == nil {
|
||||
return nil
|
||||
}
|
||||
return (*big.Int)(val.(*bigValue))
|
||||
}
|
||||
|
||||
func prefixFor(name string) (prefix string) {
|
||||
if len(name) == 1 {
|
||||
prefix = "-"
|
||||
@@ -114,15 +207,7 @@ func prefixedNames(fullName string) (prefixed string) {
|
||||
return
|
||||
}
|
||||
|
||||
func withEnvHint(envVar, str string) string {
|
||||
envText := ""
|
||||
if envVar != "" {
|
||||
envText = fmt.Sprintf(" [$%s]", strings.Join(strings.Split(envVar, ","), ", $"))
|
||||
}
|
||||
return str + envText
|
||||
}
|
||||
|
||||
func (self DirectoryFlag) getName() string {
|
||||
func (self DirectoryFlag) GetName() string {
|
||||
return self.Name
|
||||
}
|
||||
|
||||
@@ -137,12 +222,19 @@ func (self *DirectoryFlag) Set(value string) {
|
||||
// Note, it has limitations, e.g. ~someuser/tmp will not be expanded
|
||||
func expandPath(p string) string {
|
||||
if strings.HasPrefix(p, "~/") || strings.HasPrefix(p, "~\\") {
|
||||
if user, err := user.Current(); err == nil {
|
||||
if err == nil {
|
||||
p = strings.Replace(p, "~", user.HomeDir, 1)
|
||||
}
|
||||
if home := homeDir(); home != "" {
|
||||
p = home + p[1:]
|
||||
}
|
||||
}
|
||||
|
||||
return filepath.Clean(os.ExpandEnv(p))
|
||||
return path.Clean(os.ExpandEnv(p))
|
||||
}
|
||||
|
||||
func homeDir() string {
|
||||
if home := os.Getenv("HOME"); home != "" {
|
||||
return home
|
||||
}
|
||||
if usr, err := user.Current(); err == nil {
|
||||
return usr.HomeDir
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -23,18 +23,15 @@ import (
|
||||
)
|
||||
|
||||
func TestPathExpansion(t *testing.T) {
|
||||
|
||||
user, _ := user.Current()
|
||||
|
||||
tests := map[string]string{
|
||||
"/home/someuser/tmp": "/home/someuser/tmp",
|
||||
"~/tmp": user.HomeDir + "/tmp",
|
||||
"~thisOtherUser/b/": "~thisOtherUser/b",
|
||||
"$DDDXXX/a/b": "/tmp/a/b",
|
||||
"/a/b/": "/a/b",
|
||||
}
|
||||
|
||||
os.Setenv("DDDXXX", "/tmp")
|
||||
|
||||
for test, expected := range tests {
|
||||
got := expandPath(test)
|
||||
if got != expected {
|
||||
|
||||
1330
cmd/utils/flags.go
1330
cmd/utils/flags.go
File diff suppressed because it is too large
Load Diff
@@ -1,41 +0,0 @@
|
||||
// Copyright 2015 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 utils
|
||||
|
||||
const (
|
||||
legalese = `
|
||||
=======================================
|
||||
Disclaimer of Liabilites and Warranties
|
||||
=======================================
|
||||
|
||||
THE USER EXPRESSLY KNOWS AND AGREES THAT THE USER IS USING THE ETHEREUM PLATFORM AT THE USER’S SOLE
|
||||
RISK. THE USER REPRESENTS THAT THE USER HAS AN ADEQUATE UNDERSTANDING OF THE RISKS, USAGE AND
|
||||
INTRICACIES OF CRYPTOGRAPHIC TOKENS AND BLOCKCHAIN-BASED OPEN SOURCE SOFTWARE, ETH PLATFORM AND ETH.
|
||||
THE USER ACKNOWLEDGES AND AGREES THAT, TO THE FULLEST EXTENT PERMITTED BY ANY APPLICABLE LAW, THE
|
||||
DISCLAIMERS OF LIABILITY CONTAINED HEREIN APPLY TO ANY AND ALL DAMAGES OR INJURY WHATSOEVER CAUSED
|
||||
BY OR RELATED TO RISKS OF, USE OF, OR INABILITY TO USE, ETH OR THE ETHEREUM PLATFORM UNDER ANY CAUSE
|
||||
OR ACTION WHATSOEVER OF ANY KIND IN ANY JURISDICTION, INCLUDING, WITHOUT LIMITATION, ACTIONS FOR
|
||||
BREACH OF WARRANTY, BREACH OF CONTRACT OR TORT (INCLUDING NEGLIGENCE) AND THAT NEITHER STIFTUNG
|
||||
ETHEREUM NOR ETHEREUM TEAM SHALL BE LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY OR
|
||||
CONSEQUENTIAL DAMAGES, INCLUDING FOR LOSS OF PROFITS, GOODWILL OR DATA. SOME JURISDICTIONS DO NOT
|
||||
ALLOW THE EXCLUSION OF CERTAIN WARRANTIES OR THE LIMITATION OR EXCLUSION OF LIABILITY FOR CERTAIN
|
||||
TYPES OF DAMAGES. THEREFORE, SOME OF THE ABOVE LIMITATIONS IN THIS SECTION MAY NOT APPLY TO A USER.
|
||||
IN PARTICULAR, NOTHING IN THESE TERMS SHALL AFFECT THE STATUTORY RIGHTS OF ANY USER OR EXCLUDE
|
||||
INJURY ARISING FROM ANY WILLFUL MISCONDUCT OR FRAUD OF STIFTUNG ETHEREUM.
|
||||
|
||||
Do you accept this agreement?`
|
||||
)
|
||||
661
cmd/wnode/main.go
Normal file
661
cmd/wnode/main.go
Normal file
@@ -0,0 +1,661 @@
|
||||
// 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/>.
|
||||
|
||||
// This is a simple Whisper node. It could be used as a stand-alone bootstrap node.
|
||||
// Also, could be used for different test and diagnostics purposes.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/ecdsa"
|
||||
"crypto/sha512"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/cmd/utils"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/console"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/p2p"
|
||||
"github.com/ethereum/go-ethereum/p2p/discover"
|
||||
"github.com/ethereum/go-ethereum/p2p/nat"
|
||||
"github.com/ethereum/go-ethereum/whisper/mailserver"
|
||||
whisper "github.com/ethereum/go-ethereum/whisper/whisperv5"
|
||||
"golang.org/x/crypto/pbkdf2"
|
||||
)
|
||||
|
||||
const quitCommand = "~Q"
|
||||
|
||||
// singletons
|
||||
var (
|
||||
server *p2p.Server
|
||||
shh *whisper.Whisper
|
||||
done chan struct{}
|
||||
mailServer mailserver.WMailServer
|
||||
|
||||
input = bufio.NewReader(os.Stdin)
|
||||
)
|
||||
|
||||
// encryption
|
||||
var (
|
||||
symKey []byte
|
||||
pub *ecdsa.PublicKey
|
||||
asymKey *ecdsa.PrivateKey
|
||||
nodeid *ecdsa.PrivateKey
|
||||
topic whisper.TopicType
|
||||
asymKeyID string
|
||||
filterID string
|
||||
symPass string
|
||||
msPassword string
|
||||
)
|
||||
|
||||
// cmd arguments
|
||||
var (
|
||||
bootstrapMode = flag.Bool("standalone", false, "boostrap node: don't actively connect to peers, wait for incoming connections")
|
||||
forwarderMode = flag.Bool("forwarder", false, "forwarder mode: only forward messages, neither send nor decrypt messages")
|
||||
mailServerMode = flag.Bool("mailserver", false, "mail server mode: delivers expired messages on demand")
|
||||
requestMail = flag.Bool("mailclient", false, "request expired messages from the bootstrap server")
|
||||
asymmetricMode = flag.Bool("asym", false, "use asymmetric encryption")
|
||||
generateKey = flag.Bool("generatekey", false, "generate and show the private key")
|
||||
fileExMode = flag.Bool("fileexchange", false, "file exchange mode")
|
||||
testMode = flag.Bool("test", false, "use of predefined parameters for diagnostics")
|
||||
echoMode = flag.Bool("echo", false, "echo mode: prints some arguments for diagnostics")
|
||||
|
||||
argVerbosity = flag.Int("verbosity", int(log.LvlError), "log verbosity level")
|
||||
argTTL = flag.Uint("ttl", 30, "time-to-live for messages in seconds")
|
||||
argWorkTime = flag.Uint("work", 5, "work time in seconds")
|
||||
argMaxSize = flag.Uint("maxsize", uint(whisper.DefaultMaxMessageSize), "max size of message")
|
||||
argPoW = flag.Float64("pow", whisper.DefaultMinimumPoW, "PoW for normal messages in float format (e.g. 2.7)")
|
||||
argServerPoW = flag.Float64("mspow", whisper.DefaultMinimumPoW, "PoW requirement for Mail Server request")
|
||||
|
||||
argIP = flag.String("ip", "", "IP address and port of this node (e.g. 127.0.0.1:30303)")
|
||||
argPub = flag.String("pub", "", "public key for asymmetric encryption")
|
||||
argDBPath = flag.String("dbpath", "", "path to the server's DB directory")
|
||||
argIDFile = flag.String("idfile", "", "file name with node id (private key)")
|
||||
argEnode = flag.String("boot", "", "bootstrap node you want to connect to (e.g. enode://e454......08d50@52.176.211.200:16428)")
|
||||
argTopic = flag.String("topic", "", "topic in hexadecimal format (e.g. 70a4beef)")
|
||||
argSaveDir = flag.String("savedir", "", "directory where incoming messages will be saved as files")
|
||||
)
|
||||
|
||||
func main() {
|
||||
processArgs()
|
||||
initialize()
|
||||
run()
|
||||
}
|
||||
|
||||
func processArgs() {
|
||||
flag.Parse()
|
||||
|
||||
if len(*argIDFile) > 0 {
|
||||
var err error
|
||||
nodeid, err = crypto.LoadECDSA(*argIDFile)
|
||||
if err != nil {
|
||||
utils.Fatalf("Failed to load file [%s]: %s.", *argIDFile, err)
|
||||
}
|
||||
}
|
||||
|
||||
const enodePrefix = "enode://"
|
||||
if len(*argEnode) > 0 {
|
||||
if (*argEnode)[:len(enodePrefix)] != enodePrefix {
|
||||
*argEnode = enodePrefix + *argEnode
|
||||
}
|
||||
}
|
||||
|
||||
if len(*argTopic) > 0 {
|
||||
x, err := hex.DecodeString(*argTopic)
|
||||
if err != nil {
|
||||
utils.Fatalf("Failed to parse the topic: %s", err)
|
||||
}
|
||||
topic = whisper.BytesToTopic(x)
|
||||
}
|
||||
|
||||
if *asymmetricMode && len(*argPub) > 0 {
|
||||
pub = crypto.ToECDSAPub(common.FromHex(*argPub))
|
||||
if !isKeyValid(pub) {
|
||||
utils.Fatalf("invalid public key")
|
||||
}
|
||||
}
|
||||
|
||||
if len(*argSaveDir) > 0 {
|
||||
if _, err := os.Stat(*argSaveDir); os.IsNotExist(err) {
|
||||
utils.Fatalf("Download directory '%s' does not exist", *argSaveDir)
|
||||
}
|
||||
} else if *fileExMode {
|
||||
utils.Fatalf("Parameter 'savedir' is mandatory for file exchange mode")
|
||||
}
|
||||
|
||||
if *echoMode {
|
||||
echo()
|
||||
}
|
||||
}
|
||||
|
||||
func echo() {
|
||||
fmt.Printf("ttl = %d \n", *argTTL)
|
||||
fmt.Printf("workTime = %d \n", *argWorkTime)
|
||||
fmt.Printf("pow = %f \n", *argPoW)
|
||||
fmt.Printf("mspow = %f \n", *argServerPoW)
|
||||
fmt.Printf("ip = %s \n", *argIP)
|
||||
fmt.Printf("pub = %s \n", common.ToHex(crypto.FromECDSAPub(pub)))
|
||||
fmt.Printf("idfile = %s \n", *argIDFile)
|
||||
fmt.Printf("dbpath = %s \n", *argDBPath)
|
||||
fmt.Printf("boot = %s \n", *argEnode)
|
||||
}
|
||||
|
||||
func initialize() {
|
||||
log.Root().SetHandler(log.LvlFilterHandler(log.Lvl(*argVerbosity), log.StreamHandler(os.Stderr, log.TerminalFormat(false))))
|
||||
|
||||
done = make(chan struct{})
|
||||
var peers []*discover.Node
|
||||
var err error
|
||||
|
||||
if *generateKey {
|
||||
key, err := crypto.GenerateKey()
|
||||
if err != nil {
|
||||
utils.Fatalf("Failed to generate private key: %s", err)
|
||||
}
|
||||
k := hex.EncodeToString(crypto.FromECDSA(key))
|
||||
fmt.Printf("Random private key: %s \n", k)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if *testMode {
|
||||
symPass = "wwww" // ascii code: 0x77777777
|
||||
msPassword = "wwww"
|
||||
}
|
||||
|
||||
if *bootstrapMode {
|
||||
if len(*argIP) == 0 {
|
||||
argIP = scanLineA("Please enter your IP and port (e.g. 127.0.0.1:30348): ")
|
||||
}
|
||||
} else {
|
||||
if len(*argEnode) == 0 {
|
||||
argEnode = scanLineA("Please enter the peer's enode: ")
|
||||
}
|
||||
peer := discover.MustParseNode(*argEnode)
|
||||
peers = append(peers, peer)
|
||||
}
|
||||
|
||||
cfg := &whisper.Config{
|
||||
MaxMessageSize: uint32(*argMaxSize),
|
||||
MinimumAcceptedPOW: *argPoW,
|
||||
}
|
||||
|
||||
if *mailServerMode {
|
||||
if len(msPassword) == 0 {
|
||||
msPassword, err = console.Stdin.PromptPassword("Please enter the Mail Server password: ")
|
||||
if err != nil {
|
||||
utils.Fatalf("Failed to read Mail Server password: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
shh = whisper.New(cfg)
|
||||
shh.RegisterServer(&mailServer)
|
||||
mailServer.Init(shh, *argDBPath, msPassword, *argServerPoW)
|
||||
} else {
|
||||
shh = whisper.New(cfg)
|
||||
}
|
||||
|
||||
if *argPoW != whisper.DefaultMinimumPoW {
|
||||
err := shh.SetMinimumPoW(*argPoW)
|
||||
if err != nil {
|
||||
utils.Fatalf("Failed to set PoW: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
if uint32(*argMaxSize) != whisper.DefaultMaxMessageSize {
|
||||
err := shh.SetMaxMessageSize(uint32(*argMaxSize))
|
||||
if err != nil {
|
||||
utils.Fatalf("Failed to set max message size: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
asymKeyID, err = shh.NewKeyPair()
|
||||
if err != nil {
|
||||
utils.Fatalf("Failed to generate a new key pair: %s", err)
|
||||
}
|
||||
|
||||
asymKey, err = shh.GetPrivateKey(asymKeyID)
|
||||
if err != nil {
|
||||
utils.Fatalf("Failed to retrieve a new key pair: %s", err)
|
||||
}
|
||||
|
||||
if nodeid == nil {
|
||||
tmpID, err := shh.NewKeyPair()
|
||||
if err != nil {
|
||||
utils.Fatalf("Failed to generate a new key pair: %s", err)
|
||||
}
|
||||
|
||||
nodeid, err = shh.GetPrivateKey(tmpID)
|
||||
if err != nil {
|
||||
utils.Fatalf("Failed to retrieve a new key pair: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
maxPeers := 80
|
||||
if *bootstrapMode {
|
||||
maxPeers = 800
|
||||
}
|
||||
|
||||
server = &p2p.Server{
|
||||
Config: p2p.Config{
|
||||
PrivateKey: nodeid,
|
||||
MaxPeers: maxPeers,
|
||||
Name: common.MakeName("wnode", "5.0"),
|
||||
Protocols: shh.Protocols(),
|
||||
ListenAddr: *argIP,
|
||||
NAT: nat.Any(),
|
||||
BootstrapNodes: peers,
|
||||
StaticNodes: peers,
|
||||
TrustedNodes: peers,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func startServer() {
|
||||
err := server.Start()
|
||||
if err != nil {
|
||||
utils.Fatalf("Failed to start Whisper peer: %s.", err)
|
||||
}
|
||||
|
||||
fmt.Printf("my public key: %s \n", common.ToHex(crypto.FromECDSAPub(&asymKey.PublicKey)))
|
||||
fmt.Println(server.NodeInfo().Enode)
|
||||
|
||||
if *bootstrapMode {
|
||||
configureNode()
|
||||
fmt.Println("Bootstrap Whisper node started")
|
||||
} else {
|
||||
fmt.Println("Whisper node started")
|
||||
// first see if we can establish connection, then ask for user input
|
||||
waitForConnection(true)
|
||||
configureNode()
|
||||
}
|
||||
|
||||
if !*forwarderMode {
|
||||
fmt.Printf("Please type the message. To quit type: '%s'\n", quitCommand)
|
||||
}
|
||||
}
|
||||
|
||||
func isKeyValid(k *ecdsa.PublicKey) bool {
|
||||
return k.X != nil && k.Y != nil
|
||||
}
|
||||
|
||||
func configureNode() {
|
||||
var err error
|
||||
var p2pAccept bool
|
||||
|
||||
if *forwarderMode {
|
||||
return
|
||||
}
|
||||
|
||||
if *asymmetricMode {
|
||||
if len(*argPub) == 0 {
|
||||
s := scanLine("Please enter the peer's public key: ")
|
||||
b := common.FromHex(s)
|
||||
if b == nil {
|
||||
utils.Fatalf("Error: can not convert hexadecimal string")
|
||||
}
|
||||
pub = crypto.ToECDSAPub(b)
|
||||
if !isKeyValid(pub) {
|
||||
utils.Fatalf("Error: invalid public key")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if *requestMail {
|
||||
p2pAccept = true
|
||||
if len(msPassword) == 0 {
|
||||
msPassword, err = console.Stdin.PromptPassword("Please enter the Mail Server password: ")
|
||||
if err != nil {
|
||||
utils.Fatalf("Failed to read Mail Server password: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !*asymmetricMode && !*forwarderMode {
|
||||
if len(symPass) == 0 {
|
||||
symPass, err = console.Stdin.PromptPassword("Please enter the password for symmetric encryption: ")
|
||||
if err != nil {
|
||||
utils.Fatalf("Failed to read passphrase: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
symKeyID, err := shh.AddSymKeyFromPassword(symPass)
|
||||
if err != nil {
|
||||
utils.Fatalf("Failed to create symmetric key: %s", err)
|
||||
}
|
||||
symKey, err = shh.GetSymKey(symKeyID)
|
||||
if err != nil {
|
||||
utils.Fatalf("Failed to save symmetric key: %s", err)
|
||||
}
|
||||
if len(*argTopic) == 0 {
|
||||
generateTopic([]byte(symPass))
|
||||
}
|
||||
|
||||
fmt.Printf("Filter is configured for the topic: %x \n", topic)
|
||||
}
|
||||
|
||||
if *mailServerMode {
|
||||
if len(*argDBPath) == 0 {
|
||||
argDBPath = scanLineA("Please enter the path to DB file: ")
|
||||
}
|
||||
}
|
||||
|
||||
filter := whisper.Filter{
|
||||
KeySym: symKey,
|
||||
KeyAsym: asymKey,
|
||||
Topics: [][]byte{topic[:]},
|
||||
AllowP2P: p2pAccept,
|
||||
}
|
||||
filterID, err = shh.Subscribe(&filter)
|
||||
if err != nil {
|
||||
utils.Fatalf("Failed to install filter: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func generateTopic(password []byte) {
|
||||
x := pbkdf2.Key(password, password, 4096, 128, sha512.New)
|
||||
for i := 0; i < len(x); i++ {
|
||||
topic[i%whisper.TopicLength] ^= x[i]
|
||||
}
|
||||
}
|
||||
|
||||
func waitForConnection(timeout bool) {
|
||||
var cnt int
|
||||
var connected bool
|
||||
for !connected {
|
||||
time.Sleep(time.Millisecond * 50)
|
||||
connected = server.PeerCount() > 0
|
||||
if timeout {
|
||||
cnt++
|
||||
if cnt > 1000 {
|
||||
utils.Fatalf("Timeout expired, failed to connect")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("Connected to peer.")
|
||||
}
|
||||
|
||||
func run() {
|
||||
defer mailServer.Close()
|
||||
startServer()
|
||||
defer server.Stop()
|
||||
shh.Start(nil)
|
||||
defer shh.Stop()
|
||||
|
||||
if !*forwarderMode {
|
||||
go messageLoop()
|
||||
}
|
||||
|
||||
if *requestMail {
|
||||
requestExpiredMessagesLoop()
|
||||
} else if *fileExMode {
|
||||
sendFilesLoop()
|
||||
} else {
|
||||
sendLoop()
|
||||
}
|
||||
}
|
||||
|
||||
func sendLoop() {
|
||||
for {
|
||||
s := scanLine("")
|
||||
if s == quitCommand {
|
||||
fmt.Println("Quit command received")
|
||||
close(done)
|
||||
break
|
||||
}
|
||||
sendMsg([]byte(s))
|
||||
|
||||
if *asymmetricMode {
|
||||
// print your own message for convenience,
|
||||
// because in asymmetric mode it is impossible to decrypt it
|
||||
timestamp := time.Now().Unix()
|
||||
from := crypto.PubkeyToAddress(asymKey.PublicKey)
|
||||
fmt.Printf("\n%d <%x>: %s\n", timestamp, from, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func sendFilesLoop() {
|
||||
for {
|
||||
s := scanLine("")
|
||||
if s == quitCommand {
|
||||
fmt.Println("Quit command received")
|
||||
close(done)
|
||||
break
|
||||
}
|
||||
b, err := ioutil.ReadFile(s)
|
||||
if err != nil {
|
||||
fmt.Printf(">>> Error: %s \n", err)
|
||||
continue
|
||||
} else {
|
||||
h := sendMsg(b)
|
||||
if (h == common.Hash{}) {
|
||||
fmt.Printf(">>> Error: message was not sent \n")
|
||||
} else {
|
||||
timestamp := time.Now().Unix()
|
||||
from := crypto.PubkeyToAddress(asymKey.PublicKey)
|
||||
fmt.Printf("\n%d <%x>: sent message with hash %x\n", timestamp, from, h)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func scanLine(prompt string) string {
|
||||
if len(prompt) > 0 {
|
||||
fmt.Print(prompt)
|
||||
}
|
||||
txt, err := input.ReadString('\n')
|
||||
if err != nil {
|
||||
utils.Fatalf("input error: %s", err)
|
||||
}
|
||||
txt = strings.TrimRight(txt, "\n\r")
|
||||
return txt
|
||||
}
|
||||
|
||||
func scanLineA(prompt string) *string {
|
||||
s := scanLine(prompt)
|
||||
return &s
|
||||
}
|
||||
|
||||
func scanUint(prompt string) uint32 {
|
||||
s := scanLine(prompt)
|
||||
i, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
utils.Fatalf("Fail to parse the lower time limit: %s", err)
|
||||
}
|
||||
return uint32(i)
|
||||
}
|
||||
|
||||
func sendMsg(payload []byte) common.Hash {
|
||||
params := whisper.MessageParams{
|
||||
Src: asymKey,
|
||||
Dst: pub,
|
||||
KeySym: symKey,
|
||||
Payload: payload,
|
||||
Topic: topic,
|
||||
TTL: uint32(*argTTL),
|
||||
PoW: *argPoW,
|
||||
WorkTime: uint32(*argWorkTime),
|
||||
}
|
||||
|
||||
msg, err := whisper.NewSentMessage(¶ms)
|
||||
if err != nil {
|
||||
utils.Fatalf("failed to create new message: %s", err)
|
||||
}
|
||||
envelope, err := msg.Wrap(¶ms)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to seal message: %v \n", err)
|
||||
return common.Hash{}
|
||||
}
|
||||
|
||||
err = shh.Send(envelope)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to send message: %v \n", err)
|
||||
return common.Hash{}
|
||||
}
|
||||
|
||||
return envelope.Hash()
|
||||
}
|
||||
|
||||
func messageLoop() {
|
||||
f := shh.GetFilter(filterID)
|
||||
if f == nil {
|
||||
utils.Fatalf("filter is not installed")
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(time.Millisecond * 50)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
messages := f.Retrieve()
|
||||
for _, msg := range messages {
|
||||
if *fileExMode || len(msg.Payload) > 2048 {
|
||||
writeMessageToFile(*argSaveDir, msg)
|
||||
} else {
|
||||
printMessageInfo(msg)
|
||||
}
|
||||
}
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func printMessageInfo(msg *whisper.ReceivedMessage) {
|
||||
timestamp := fmt.Sprintf("%d", msg.Sent) // unix timestamp for diagnostics
|
||||
text := string(msg.Payload)
|
||||
|
||||
var address common.Address
|
||||
if msg.Src != nil {
|
||||
address = crypto.PubkeyToAddress(*msg.Src)
|
||||
}
|
||||
|
||||
if whisper.IsPubKeyEqual(msg.Src, &asymKey.PublicKey) {
|
||||
fmt.Printf("\n%s <%x>: %s\n", timestamp, address, text) // message from myself
|
||||
} else {
|
||||
fmt.Printf("\n%s [%x]: %s\n", timestamp, address, text) // message from a peer
|
||||
}
|
||||
}
|
||||
|
||||
func writeMessageToFile(dir string, msg *whisper.ReceivedMessage) {
|
||||
timestamp := fmt.Sprintf("%d", msg.Sent)
|
||||
name := fmt.Sprintf("%x", msg.EnvelopeHash)
|
||||
|
||||
var address common.Address
|
||||
if msg.Src != nil {
|
||||
address = crypto.PubkeyToAddress(*msg.Src)
|
||||
}
|
||||
|
||||
if whisper.IsPubKeyEqual(msg.Src, &asymKey.PublicKey) {
|
||||
// message from myself: don't save, only report
|
||||
fmt.Printf("\n%s <%x>: message received: '%s'\n", timestamp, address, name)
|
||||
} else if len(dir) > 0 {
|
||||
fullpath := filepath.Join(dir, name)
|
||||
err := ioutil.WriteFile(fullpath, msg.Payload, 0644)
|
||||
if err != nil {
|
||||
fmt.Printf("\n%s {%x}: message received but not saved: %s\n", timestamp, address, err)
|
||||
} else {
|
||||
fmt.Printf("\n%s {%x}: message received and saved as '%s' (%d bytes)\n", timestamp, address, name, len(msg.Payload))
|
||||
}
|
||||
} else {
|
||||
fmt.Printf("\n%s {%x}: big message received (%d bytes), but not saved: %s\n", timestamp, address, len(msg.Payload), name)
|
||||
}
|
||||
}
|
||||
|
||||
func requestExpiredMessagesLoop() {
|
||||
var key, peerID []byte
|
||||
var timeLow, timeUpp uint32
|
||||
var t string
|
||||
var xt, empty whisper.TopicType
|
||||
|
||||
keyID, err := shh.AddSymKeyFromPassword(msPassword)
|
||||
if err != nil {
|
||||
utils.Fatalf("Failed to create symmetric key for mail request: %s", err)
|
||||
}
|
||||
key, err = shh.GetSymKey(keyID)
|
||||
if err != nil {
|
||||
utils.Fatalf("Failed to save symmetric key for mail request: %s", err)
|
||||
}
|
||||
peerID = extractIdFromEnode(*argEnode)
|
||||
shh.AllowP2PMessagesFromPeer(peerID)
|
||||
|
||||
for {
|
||||
timeLow = scanUint("Please enter the lower limit of the time range (unix timestamp): ")
|
||||
timeUpp = scanUint("Please enter the upper limit of the time range (unix timestamp): ")
|
||||
t = scanLine("Please enter the topic (hexadecimal): ")
|
||||
if len(t) >= whisper.TopicLength*2 {
|
||||
x, err := hex.DecodeString(t)
|
||||
if err != nil {
|
||||
utils.Fatalf("Failed to parse the topic: %s", err)
|
||||
}
|
||||
xt = whisper.BytesToTopic(x)
|
||||
}
|
||||
if timeUpp == 0 {
|
||||
timeUpp = 0xFFFFFFFF
|
||||
}
|
||||
|
||||
data := make([]byte, 8+whisper.TopicLength)
|
||||
binary.BigEndian.PutUint32(data, timeLow)
|
||||
binary.BigEndian.PutUint32(data[4:], timeUpp)
|
||||
copy(data[8:], xt[:])
|
||||
if xt == empty {
|
||||
data = data[:8]
|
||||
}
|
||||
|
||||
var params whisper.MessageParams
|
||||
params.PoW = *argServerPoW
|
||||
params.Payload = data
|
||||
params.KeySym = key
|
||||
params.Src = nodeid
|
||||
params.WorkTime = 5
|
||||
|
||||
msg, err := whisper.NewSentMessage(¶ms)
|
||||
if err != nil {
|
||||
utils.Fatalf("failed to create new message: %s", err)
|
||||
}
|
||||
env, err := msg.Wrap(¶ms)
|
||||
if err != nil {
|
||||
utils.Fatalf("Wrap failed: %s", err)
|
||||
}
|
||||
|
||||
err = shh.RequestHistoricMessages(peerID, env)
|
||||
if err != nil {
|
||||
utils.Fatalf("Failed to send P2P message: %s", err)
|
||||
}
|
||||
|
||||
time.Sleep(time.Second * 5)
|
||||
}
|
||||
}
|
||||
|
||||
func extractIdFromEnode(s string) []byte {
|
||||
n, err := discover.ParseNode(s)
|
||||
if err != nil {
|
||||
utils.Fatalf("Failed to parse enode: %s", err)
|
||||
}
|
||||
return n.ID[:]
|
||||
}
|
||||
Reference in New Issue
Block a user