node: ensure datadir can be co-inhabited by different instances

This change ensures that nodes started with different Name but same
DataDir values don't use the same nodekey and IPC socket.
This commit is contained in:
Felix Lange 2016-08-18 13:28:17 +02:00
parent 52ede09b17
commit eeb322ae64
14 changed files with 423 additions and 187 deletions

View File

@ -79,7 +79,8 @@ func importChain(ctx *cli.Context) error {
if ctx.GlobalBool(utils.TestNetFlag.Name) {
state.StartingNonce = 1048576 // (2**20)
}
chain, chainDb := utils.MakeChain(ctx)
stack := makeFullNode(ctx)
chain, chainDb := utils.MakeChain(ctx, stack)
start := time.Now()
err := utils.ImportChain(chain, ctx.Args().First())
chainDb.Close()
@ -94,7 +95,8 @@ func exportChain(ctx *cli.Context) error {
if len(ctx.Args()) < 1 {
utils.Fatalf("This command requires an argument.")
}
chain, _ := utils.MakeChain(ctx)
stack := makeFullNode(ctx)
chain, _ := utils.MakeChain(ctx, stack)
start := time.Now()
var err error
@ -122,20 +124,25 @@ func exportChain(ctx *cli.Context) error {
}
func removeDB(ctx *cli.Context) error {
confirm, err := console.Stdin.PromptConfirm("Remove local database?")
if err != nil {
utils.Fatalf("%v", err)
stack := utils.MakeNode(ctx, clientIdentifier, gitCommit)
dbdir := stack.ResolvePath("chaindata")
if !common.FileExist(dbdir) {
fmt.Println(dbdir, "does not exist")
return nil
}
if confirm {
fmt.Println("Removing chaindata...")
start := time.Now()
os.RemoveAll(filepath.Join(ctx.GlobalString(utils.DataDirFlag.Name), "chaindata"))
fmt.Printf("Removed in %v\n", time.Since(start))
} else {
fmt.Println(dbdir)
confirm, err := console.Stdin.PromptConfirm("Remove this database?")
switch {
case err != nil:
utils.Fatalf("%v", err)
case !confirm:
fmt.Println("Operation aborted")
default:
fmt.Println("Removing...")
start := time.Now()
os.RemoveAll(dbdir)
fmt.Printf("Removed in %v\n", time.Since(start))
}
return nil
}
@ -143,7 +150,8 @@ func removeDB(ctx *cli.Context) error {
func upgradeDB(ctx *cli.Context) error {
glog.Infoln("Upgrading blockchain database")
chain, chainDb := utils.MakeChain(ctx)
stack := utils.MakeNode(ctx, clientIdentifier, gitCommit)
chain, chainDb := utils.MakeChain(ctx, stack)
bcVersion := core.GetBlockChainVersion(chainDb)
if bcVersion == 0 {
bcVersion = core.BlockChainVersion
@ -156,10 +164,12 @@ func upgradeDB(ctx *cli.Context) error {
utils.Fatalf("Unable to export chain for reimport %s", err)
}
chainDb.Close()
os.RemoveAll(filepath.Join(ctx.GlobalString(utils.DataDirFlag.Name), "chaindata"))
if dir := dbDirectory(chainDb); dir != "" {
os.RemoveAll(dir)
}
// Import the chain file.
chain, chainDb = utils.MakeChain(ctx)
chain, chainDb = utils.MakeChain(ctx, stack)
core.WriteBlockChainVersion(chainDb, core.BlockChainVersion)
err := utils.ImportChain(chain, exportFile)
chainDb.Close()
@ -172,8 +182,17 @@ func upgradeDB(ctx *cli.Context) error {
return nil
}
func dbDirectory(db ethdb.Database) string {
ldb, ok := db.(*ethdb.LDBDatabase)
if !ok {
return ""
}
return ldb.Path()
}
func dump(ctx *cli.Context) error {
chain, chainDb := utils.MakeChain(ctx)
stack := makeFullNode(ctx)
chain, chainDb := utils.MakeChain(ctx, stack)
for _, arg := range ctx.Args() {
var block *types.Block
if hashish(arg) {

View File

@ -107,7 +107,7 @@ func remoteConsole(ctx *cli.Context) error {
utils.Fatalf("Unable to attach to remote geth: %v", err)
}
config := console.Config{
DataDir: utils.MustMakeDataDir(ctx),
DataDir: utils.MakeDataDir(ctx),
DocRoot: ctx.GlobalString(utils.JSpathFlag.Name),
Client: client,
Preload: utils.MakeConsolePreloads(ctx),
@ -135,7 +135,7 @@ func remoteConsole(ctx *cli.Context) error {
// for "geth attach" and "geth monitor" with no argument.
func dialRPC(endpoint string) (*rpc.Client, error) {
if endpoint == "" {
endpoint = node.DefaultIPCEndpoint()
endpoint = node.DefaultIPCEndpoint(clientIdentifier)
} else if strings.HasPrefix(endpoint, "rpc:") || strings.HasPrefix(endpoint, "ipc:") {
// Backwards compatibility with geth < 1.5 which required
// these prefixes.

View File

@ -195,9 +195,9 @@ func testDAOForkBlockNewChain(t *testing.T, testnet bool, genesis string, votes
geth.cmd.Wait()
}
// Retrieve the DAO config flag from the database
path := filepath.Join(datadir, "chaindata")
path := filepath.Join(datadir, "geth", "chaindata")
if testnet && genesis == "" {
path = filepath.Join(datadir, "testnet", "chaindata")
path = filepath.Join(datadir, "testnet", "geth", "chaindata")
}
db, err := ethdb.NewLDBDatabase(path, 0, 0)
if err != nil {

View File

@ -36,7 +36,6 @@ import (
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/eth"
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/internal/debug"
"github.com/ethereum/go-ethereum/logger"
"github.com/ethereum/go-ethereum/logger/glog"
@ -46,7 +45,7 @@ import (
)
const (
clientIdentifier = "Geth" // Client identifier to advertise over the network
clientIdentifier = "geth" // Client identifier to advertise over the network
)
var (
@ -245,17 +244,15 @@ func initGenesis(ctx *cli.Context) error {
state.StartingNonce = 1048576 // (2**20)
}
chainDb, err := ethdb.NewLDBDatabase(filepath.Join(utils.MustMakeDataDir(ctx), "chaindata"), 0, 0)
if err != nil {
utils.Fatalf("could not open database: %v", err)
}
stack := makeFullNode(ctx)
chaindb := utils.MakeChainDatabase(ctx, stack)
genesisFile, err := os.Open(genesisPath)
if err != nil {
utils.Fatalf("failed to read genesis file: %v", err)
}
block, err := core.WriteGenesisBlock(chainDb, genesisFile)
block, err := core.WriteGenesisBlock(chaindb, genesisFile)
if err != nil {
utils.Fatalf("failed to write genesis block: %v", err)
}
@ -296,9 +293,6 @@ func makeFullNode(ctx *cli.Context) *node.Node {
// it unlocks any requested accounts, and starts the RPC/IPC interfaces and the
// miner.
func startNode(ctx *cli.Context, stack *node.Node) {
// Report geth version
glog.V(logger.Info).Infof("instance: Geth/%s/%s/%s\n", utils.Version, runtime.Version(), runtime.GOOS)
// Start up the node itself
utils.StartNode(stack)
@ -379,7 +373,7 @@ func gpubench(ctx *cli.Context) error {
}
func version(c *cli.Context) error {
fmt.Println(clientIdentifier)
fmt.Println(strings.Title(clientIdentifier))
fmt.Println("Version:", utils.Version)
if gitCommit != "" {
fmt.Println("Git Commit:", gitCommit)

View File

@ -35,7 +35,7 @@ import (
var (
monitorCommandAttachFlag = cli.StringFlag{
Name: "attach",
Value: node.DefaultIPCEndpoint(),
Value: node.DefaultIPCEndpoint(clientIdentifier),
Usage: "API endpoint to attach to",
}
monitorCommandRowsFlag = cli.IntFlag{

View File

@ -88,7 +88,7 @@ func MakeSystemNode(privkey string, test *tests.BlockTest) (*node.Node, error) {
// Create a networkless protocol stack
stack, err := node.New(&node.Config{
UseLightweightKDF: true,
IPCPath: node.DefaultIPCEndpoint(),
IPCPath: node.DefaultIPCEndpoint(""),
HTTPHost: common.DefaultHTTPHost,
HTTPPort: common.DefaultHTTPPort,
HTTPModules: []string{"admin", "db", "eth", "debug", "miner", "net", "shh", "txpool", "personal", "web3"},

View File

@ -396,13 +396,14 @@ var (
}
)
// MustMakeDataDir retrieves the currently requested data directory, terminating
// MakeDataDir retrieves the currently requested data directory, terminating
// if none (or the empty string) is specified. If the node is starting a testnet,
// the a subdirectory of the specified datadir will be used.
func MustMakeDataDir(ctx *cli.Context) string {
func MakeDataDir(ctx *cli.Context) string {
if path := ctx.GlobalString(DataDirFlag.Name); path != "" {
// TODO: choose a different location outside of the regular datadir.
if ctx.GlobalBool(TestNetFlag.Name) {
return filepath.Join(path, "/testnet")
return filepath.Join(path, "testnet")
}
return path
}
@ -447,16 +448,16 @@ func MakeNodeKey(ctx *cli.Context) *ecdsa.PrivateKey {
return key
}
// MakeNodeName creates a node name from a base set and the command line flags.
func MakeNodeName(client, version string, ctx *cli.Context) string {
name := common.MakeName(client, version)
// makeNodeUserIdent creates the user identifier from CLI flags.
func makeNodeUserIdent(ctx *cli.Context) string {
var comps []string
if identity := ctx.GlobalString(IdentityFlag.Name); len(identity) > 0 {
name += "/" + identity
comps = append(comps, identity)
}
if ctx.GlobalBool(VMEnableJitFlag.Name) {
name += "/JIT"
comps = append(comps, "JIT")
}
return name
return strings.Join(comps, "/")
}
// MakeBootstrapNodes creates a list of bootstrap nodes from the command line
@ -612,11 +613,13 @@ func MakeNode(ctx *cli.Context, name, gitCommit string) *node.Node {
}
config := &node.Config{
DataDir: MustMakeDataDir(ctx),
DataDir: MakeDataDir(ctx),
KeyStoreDir: ctx.GlobalString(KeyStoreDirFlag.Name),
UseLightweightKDF: ctx.GlobalBool(LightKDFFlag.Name),
PrivateKey: MakeNodeKey(ctx),
Name: MakeNodeName(name, vsn, ctx),
Name: name,
Version: vsn,
UserIdent: makeNodeUserIdent(ctx),
NoDiscovery: ctx.GlobalBool(NoDiscoverFlag.Name),
BootstrapNodes: MakeBootstrapNodes(ctx),
ListenAddr: MakeListenAddress(ctx),
@ -674,7 +677,7 @@ func RegisterEthService(ctx *cli.Context, stack *node.Node, extra []byte) {
ethConf := &eth.Config{
Etherbase: MakeEtherbase(stack.AccountManager(), ctx),
ChainConfig: MustMakeChainConfig(ctx),
ChainConfig: MakeChainConfig(ctx, stack),
FastSync: ctx.GlobalBool(FastSyncFlag.Name),
DatabaseCache: ctx.GlobalInt(CacheFlag.Name),
DatabaseHandles: MakeDatabaseHandles(),
@ -748,16 +751,16 @@ func SetupNetwork(ctx *cli.Context) {
params.TargetGasLimit = common.String2Big(ctx.GlobalString(TargetGasLimitFlag.Name))
}
// MustMakeChainConfig reads the chain configuration from the database in ctx.Datadir.
func MustMakeChainConfig(ctx *cli.Context) *core.ChainConfig {
db := MakeChainDatabase(ctx)
// MakeChainConfig reads the chain configuration from the database in ctx.Datadir.
func MakeChainConfig(ctx *cli.Context, stack *node.Node) *core.ChainConfig {
db := MakeChainDatabase(ctx, stack)
defer db.Close()
return MustMakeChainConfigFromDb(ctx, db)
return MakeChainConfigFromDb(ctx, db)
}
// MustMakeChainConfigFromDb reads the chain configuration from the given database.
func MustMakeChainConfigFromDb(ctx *cli.Context, db ethdb.Database) *core.ChainConfig {
// MakeChainConfigFromDb reads the chain configuration from the given database.
func MakeChainConfigFromDb(ctx *cli.Context, db ethdb.Database) *core.ChainConfig {
// If the chain is already initialized, use any existing chain configs
config := new(core.ChainConfig)
@ -800,14 +803,13 @@ func MustMakeChainConfigFromDb(ctx *cli.Context, db ethdb.Database) *core.ChainC
}
// MakeChainDatabase open an LevelDB using the flags passed to the client and will hard crash if it fails.
func MakeChainDatabase(ctx *cli.Context) ethdb.Database {
func MakeChainDatabase(ctx *cli.Context, stack *node.Node) ethdb.Database {
var (
datadir = MustMakeDataDir(ctx)
cache = ctx.GlobalInt(CacheFlag.Name)
handles = MakeDatabaseHandles()
)
chainDb, err := ethdb.NewLDBDatabase(filepath.Join(datadir, "chaindata"), cache, handles)
chainDb, err := stack.OpenDatabase("chaindata", cache, handles)
if err != nil {
Fatalf("Could not open database: %v", err)
}
@ -815,9 +817,9 @@ func MakeChainDatabase(ctx *cli.Context) ethdb.Database {
}
// MakeChain creates a chain manager from set command line flags.
func MakeChain(ctx *cli.Context) (chain *core.BlockChain, chainDb ethdb.Database) {
func MakeChain(ctx *cli.Context, stack *node.Node) (chain *core.BlockChain, chainDb ethdb.Database) {
var err error
chainDb = MakeChainDatabase(ctx)
chainDb = MakeChainDatabase(ctx, stack)
if ctx.GlobalBool(OlympicFlag.Name) {
_, err := core.WriteTestNetGenesisBlock(chainDb)
@ -825,7 +827,7 @@ func MakeChain(ctx *cli.Context) (chain *core.BlockChain, chainDb ethdb.Database
glog.Fatalln(err)
}
}
chainConfig := MustMakeChainConfigFromDb(ctx, chainDb)
chainConfig := MakeChainConfigFromDb(ctx, chainDb)
pow := pow.PoW(core.FakePow{})
if !ctx.GlobalBool(FakePoWFlag.Name) {

View File

@ -85,16 +85,16 @@ func (api *PrivateAdminAPI) StartRPC(host *string, port *rpc.HexNumber, cors *st
if host == nil {
h := common.DefaultHTTPHost
if api.node.httpHost != "" {
h = api.node.httpHost
if api.node.config.HTTPHost != "" {
h = api.node.config.HTTPHost
}
host = &h
}
if port == nil {
port = rpc.NewHexNumber(api.node.httpPort)
port = rpc.NewHexNumber(api.node.config.HTTPPort)
}
if cors == nil {
cors = &api.node.httpCors
cors = &api.node.config.HTTPCors
}
modules := api.node.httpWhitelist
@ -134,19 +134,19 @@ func (api *PrivateAdminAPI) StartWS(host *string, port *rpc.HexNumber, allowedOr
if host == nil {
h := common.DefaultWSHost
if api.node.wsHost != "" {
h = api.node.wsHost
if api.node.config.WSHost != "" {
h = api.node.config.WSHost
}
host = &h
}
if port == nil {
port = rpc.NewHexNumber(api.node.wsPort)
port = rpc.NewHexNumber(api.node.config.WSPort)
}
if allowedOrigins == nil {
allowedOrigins = &api.node.wsOrigins
allowedOrigins = &api.node.config.WSOrigins
}
modules := api.node.wsWhitelist
modules := api.node.config.WSModules
if apis != nil {
modules = nil
for _, m := range strings.Split(*apis, ",") {

View File

@ -18,7 +18,6 @@ package node
import (
"crypto/ecdsa"
"encoding/json"
"fmt"
"io/ioutil"
"net"
@ -48,6 +47,18 @@ var (
// P2P network layer of a protocol stack. These values can be further extended by
// all registered services.
type Config struct {
// Name sets the instance name of the node. It must not contain the / character and is
// used in the devp2p node identifier. The instance name of geth is "geth". If no
// value is specified, the basename of the current executable is used.
Name string
// UserIdent, if set, is used as an additional component in the devp2p node identifier.
UserIdent string
// Version should be set to the version number of the program. It is used
// in the devp2p node identifier.
Version string
// DataDir is the file system folder the node should use for any data storage
// requirements. The configured data directory will not be directly shared with
// registered services, instead those can use utility methods to create/access
@ -80,10 +91,6 @@ type Config struct {
// needed.
PrivateKey *ecdsa.PrivateKey
// Name sets the node name of this server. Use common.MakeName to create a name
// that follows existing conventions.
Name string
// NoDiscovery specifies whether the peer discovery mechanism should be started
// or not. Disabling is usually useful for protocol debugging (manual topology).
NoDiscovery bool
@ -178,9 +185,23 @@ func (c *Config) IPCEndpoint() string {
return c.IPCPath
}
// NodeDB returns the path to the discovery node database.
func (c *Config) NodeDB() string {
if c.DataDir == "" {
return "" // ephemeral
}
return c.resolvePath("nodes")
}
// DefaultIPCEndpoint returns the IPC path used by default.
func DefaultIPCEndpoint() string {
config := &Config{DataDir: common.DefaultDataDir(), IPCPath: common.DefaultIPCSocket}
func DefaultIPCEndpoint(clientIdentifier string) string {
if clientIdentifier == "" {
clientIdentifier = strings.TrimSuffix(filepath.Base(os.Args[0]), ".exe")
if clientIdentifier == "" {
panic("empty executable name")
}
}
config := &Config{DataDir: common.DefaultDataDir(), IPCPath: clientIdentifier + ".ipc"}
return config.IPCEndpoint()
}
@ -214,15 +235,76 @@ func DefaultWSEndpoint() string {
return config.WSEndpoint()
}
// NodeName returns the devp2p node identifier.
func (c *Config) NodeName() string {
name := c.name()
// Backwards compatibility: previous versions used title-cased "Geth", keep that.
if name == "geth" || name == "geth-testnet" {
name = "Geth"
}
if c.UserIdent != "" {
name += "/" + c.UserIdent
}
if c.Version != "" {
name += "/v" + c.Version
}
name += "/" + runtime.GOOS
name += "/" + runtime.Version()
return name
}
func (c *Config) name() string {
if c.Name == "" {
progname := strings.TrimSuffix(filepath.Base(os.Args[0]), ".exe")
if progname == "" {
panic("empty executable name, set Config.Name")
}
return progname
}
return c.Name
}
// These resources are resolved differently for the "geth" and "geth-testnet" instances.
var isOldGethResource = map[string]bool{
"chaindata": true,
"nodes": true,
"nodekey": true,
"static-nodes.json": true,
"trusted-nodes.json": true,
}
// resolvePath resolves path in the instance directory.
func (c *Config) resolvePath(path string) string {
if filepath.IsAbs(path) {
return path
}
if c.DataDir == "" {
return ""
}
// Backwards-compatibility: ensure that data directory files created
// by geth 1.4 are used if they exist.
if c.name() == "geth" && isOldGethResource[path] {
oldpath := ""
if c.Name == "geth" {
oldpath = filepath.Join(c.DataDir, path)
}
if oldpath != "" && common.FileExist(oldpath) {
// TODO: print warning
return oldpath
}
}
return filepath.Join(c.DataDir, c.name(), path)
}
// NodeKey retrieves the currently configured private key of the node, checking
// first any manually set key, falling back to the one found in the configured
// data folder. If no key can be found, a new one is generated.
func (c *Config) NodeKey() *ecdsa.PrivateKey {
// Use any specifically configured key
// Use any specifically configured key.
if c.PrivateKey != nil {
return c.PrivateKey
}
// Generate ephemeral key if no datadir is being used
// Generate ephemeral key if no datadir is being used.
if c.DataDir == "" {
key, err := crypto.GenerateKey()
if err != nil {
@ -230,16 +312,22 @@ func (c *Config) NodeKey() *ecdsa.PrivateKey {
}
return key
}
// Fall back to persistent key from the data directory
keyfile := filepath.Join(c.DataDir, datadirPrivateKey)
keyfile := c.resolvePath(datadirPrivateKey)
if key, err := crypto.LoadECDSA(keyfile); err == nil {
return key
}
// No persistent key found, generate and store a new one
// No persistent key found, generate and store a new one.
key, err := crypto.GenerateKey()
if err != nil {
glog.Fatalf("Failed to generate node key: %v", err)
}
instanceDir := filepath.Join(c.DataDir, c.name())
if err := os.MkdirAll(instanceDir, 0700); err != nil {
glog.V(logger.Error).Infof("Failed to persist node key: %v", err)
return key
}
keyfile = filepath.Join(instanceDir, datadirPrivateKey)
if err := crypto.SaveECDSA(keyfile, key); err != nil {
glog.V(logger.Error).Infof("Failed to persist node key: %v", err)
}
@ -248,12 +336,12 @@ func (c *Config) NodeKey() *ecdsa.PrivateKey {
// StaticNodes returns a list of node enode URLs configured as static nodes.
func (c *Config) StaticNodes() []*discover.Node {
return c.parsePersistentNodes(datadirStaticNodes)
return c.parsePersistentNodes(c.resolvePath(datadirStaticNodes))
}
// TrusterNodes returns a list of node enode URLs configured as trusted nodes.
func (c *Config) TrusterNodes() []*discover.Node {
return c.parsePersistentNodes(datadirTrustedNodes)
return c.parsePersistentNodes(c.resolvePath(datadirTrustedNodes))
}
// parsePersistentNodes parses a list of discovery node URLs loaded from a .json
@ -267,15 +355,10 @@ func (c *Config) parsePersistentNodes(file string) []*discover.Node {
if _, err := os.Stat(path); err != nil {
return nil
}
// Load the nodes from the config file
blob, err := ioutil.ReadFile(path)
if err != nil {
glog.V(logger.Error).Infof("Failed to access nodes: %v", err)
return nil
}
nodelist := []string{}
if err := json.Unmarshal(blob, &nodelist); err != nil {
glog.V(logger.Error).Infof("Failed to load nodes: %v", err)
// Load the nodes from the config file.
var nodelist []string
if err := common.LoadJSON(path, &nodelist); err != nil {
glog.V(logger.Error).Infof("Can't load node file %s: %v", path, err)
return nil
}
// Interpret the list as a discovery node array

View File

@ -96,57 +96,55 @@ func TestIPCPathResolution(t *testing.T) {
// ephemeral.
func TestNodeKeyPersistency(t *testing.T) {
// Create a temporary folder and make sure no key is present
dir, err := ioutil.TempDir("", "")
dir, err := ioutil.TempDir("", "node-test")
if err != nil {
t.Fatalf("failed to create temporary data directory: %v", err)
}
defer os.RemoveAll(dir)
if _, err := os.Stat(filepath.Join(dir, datadirPrivateKey)); err == nil {
t.Fatalf("non-created node key already exists")
}
keyfile := filepath.Join(dir, "unit-test", datadirPrivateKey)
// Configure a node with a preset key and ensure it's not persisted
key, err := crypto.GenerateKey()
if err != nil {
t.Fatalf("failed to generate one-shot node key: %v", err)
}
if _, err := New(&Config{DataDir: dir, PrivateKey: key}); err != nil {
t.Fatalf("failed to create empty stack: %v", err)
}
if _, err := os.Stat(filepath.Join(dir, datadirPrivateKey)); err == nil {
config := &Config{Name: "unit-test", DataDir: dir, PrivateKey: key}
config.NodeKey()
if _, err := os.Stat(filepath.Join(keyfile)); err == nil {
t.Fatalf("one-shot node key persisted to data directory")
}
// Configure a node with no preset key and ensure it is persisted this time
if _, err := New(&Config{DataDir: dir}); err != nil {
t.Fatalf("failed to create newly keyed stack: %v", err)
}
if _, err := os.Stat(filepath.Join(dir, datadirPrivateKey)); err != nil {
config = &Config{Name: "unit-test", DataDir: dir}
config.NodeKey()
if _, err := os.Stat(keyfile); err != nil {
t.Fatalf("node key not persisted to data directory: %v", err)
}
key, err = crypto.LoadECDSA(filepath.Join(dir, datadirPrivateKey))
key, err = crypto.LoadECDSA(keyfile)
if err != nil {
t.Fatalf("failed to load freshly persisted node key: %v", err)
}
blob1, err := ioutil.ReadFile(filepath.Join(dir, datadirPrivateKey))
blob1, err := ioutil.ReadFile(keyfile)
if err != nil {
t.Fatalf("failed to read freshly persisted node key: %v", err)
}
// Configure a new node and ensure the previously persisted key is loaded
if _, err := New(&Config{DataDir: dir}); err != nil {
t.Fatalf("failed to create previously keyed stack: %v", err)
}
blob2, err := ioutil.ReadFile(filepath.Join(dir, datadirPrivateKey))
config = &Config{Name: "unit-test", DataDir: dir}
config.NodeKey()
blob2, err := ioutil.ReadFile(filepath.Join(keyfile))
if err != nil {
t.Fatalf("failed to read previously persisted node key: %v", err)
}
if bytes.Compare(blob1, blob2) != 0 {
t.Fatalf("persisted node key mismatch: have %x, want %x", blob2, blob1)
}
// Configure ephemeral node and ensure no key is dumped locally
if _, err := New(&Config{DataDir: ""}); err != nil {
t.Fatalf("failed to create ephemeral stack: %v", err)
}
if _, err := os.Stat(filepath.Join(".", datadirPrivateKey)); err == nil {
config = &Config{Name: "unit-test", DataDir: ""}
config.NodeKey()
if _, err := os.Stat(filepath.Join(".", "unit-test", datadirPrivateKey)); err == nil {
t.Fatalf("ephemeral node key persisted to disk")
}
}

90
node/doc.go Normal file
View File

@ -0,0 +1,90 @@
// Copyright 2016 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 node sets up multi-protocol Ethereum nodes.
In the model exposed by this package, a node is a collection of services which use shared
resources to provide RPC APIs. Services can also offer devp2p protocols, which are wired
up to the devp2p network when the node instance is started.
Resources Managed By Node
All file-system resources used by a node instance are located in a directory called the
data directory. The location of each resource can be overridden through additional node
configuration. The data directory is optional. If it is not set and the location of a
resource is otherwise unspecified, package node will create the resource in memory.
To access to the devp2p network, Node configures and starts p2p.Server. Each host on the
devp2p network has a unique identifier, the node key. The Node instance persists this key
across restarts. Node also loads static and trusted node lists and ensures that knowledge
about other hosts is persisted.
JSON-RPC servers which run HTTP, WebSocket or IPC can be started on a Node. RPC modules
offered by registered services will be offered on those endpoints. Users can restrict any
endpoint to a subset of RPC modules. Node itself offers the "debug", "admin" and "web3"
modules.
Service implementations can open LevelDB databases through the service context. Package
node chooses the file system location of each database. If the node is configured to run
without a data directory, databases are opened in memory instead.
Node also creates the shared store of encrypted Ethereum account keys. Services can access
the account manager through the service context.
Sharing Data Directory Among Instances
Multiple node instances can share a single data directory if they have distinct instance
names (set through the Name config option). Sharing behaviour depends on the type of
resource.
devp2p-related resources (node key, static/trusted node lists, known hosts database) are
stored in a directory with the same name as the instance. Thus, multiple node instances
using the same data directory will store this information in different subdirectories of
the data directory.
LevelDB databases are also stored within the instance subdirectory. If multiple node
instances use the same data directory, openening the databases with identical names will
create one database for each instance.
The account key store is shared among all node instances using the same data directory
unless its location is changed through the KeyStoreDir configuration option.
Data Directory Sharing Example
In this exanple, two node instances named A and B are started with the same data
directory. Mode instance A opens the database "db", node instance B opens the databases
"db" and "db-2". The following files will be created in the data directory:
data-directory/
A/
nodekey -- devp2p node key of instance A
nodes/ -- devp2p discovery knowledge database of instance A
db/ -- LevelDB content for "db"
A.ipc -- JSON-RPC UNIX domain socket endpoint of instance A
B/
nodekey -- devp2p node key of node B
nodes/ -- devp2p discovery knowledge database of instance B
static-nodes.json -- devp2p static node list of instance B
db/ -- LevelDB content for "db"
db-2/ -- LevelDB content for "db-2"
B.ipc -- JSON-RPC UNIX domain socket endpoint of instance A
keystore/ -- account key store, used by both instances
*/
package node

View File

@ -14,7 +14,6 @@
// 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 node represents the Ethereum protocol stack container.
package node
import (
@ -23,16 +22,19 @@ import (
"os"
"path/filepath"
"reflect"
"strings"
"sync"
"syscall"
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/event"
"github.com/ethereum/go-ethereum/internal/debug"
"github.com/ethereum/go-ethereum/logger"
"github.com/ethereum/go-ethereum/logger/glog"
"github.com/ethereum/go-ethereum/p2p"
"github.com/ethereum/go-ethereum/rpc"
"github.com/syndtr/goleveldb/leveldb/storage"
)
var (
@ -44,14 +46,14 @@ var (
datadirInUseErrnos = map[uint]bool{11: true, 32: true, 35: true}
)
// Node represents a P2P node into which arbitrary (uniquely typed) services might
// be registered.
// Node is a container on which services can be registered.
type Node struct {
datadir string // Path to the currently used data directory
eventmux *event.TypeMux // Event multiplexer used between the services of a stack
config *Config
accman *accounts.Manager
ephemeralKeystore string // if non-empty, the key directory that will be removed by Stop
instanceDirLock storage.Storage // prevents concurrent use of instance directory
serverConfig p2p.Config
server *p2p.Server // Currently running P2P networking layer
@ -66,19 +68,12 @@ type Node struct {
ipcListener net.Listener // IPC RPC listener socket to serve API requests
ipcHandler *rpc.Server // IPC RPC request handler to process the API requests
httpHost string // HTTP hostname
httpPort int // HTTP post
httpEndpoint string // HTTP endpoint (interface + port) to listen at (empty = HTTP disabled)
httpWhitelist []string // HTTP RPC modules to allow through this endpoint
httpCors string // HTTP RPC Cross-Origin Resource Sharing header
httpListener net.Listener // HTTP RPC listener socket to server API requests
httpHandler *rpc.Server // HTTP RPC request handler to process the API requests
wsHost string // Websocket host
wsPort int // Websocket post
wsEndpoint string // Websocket endpoint (interface + port) to listen at (empty = websocket disabled)
wsWhitelist []string // Websocket RPC modules to allow through this endpoint
wsOrigins string // Websocket RPC allowed origin domains
wsListener net.Listener // Websocket RPC listener socket to server API requests
wsHandler *rpc.Server // Websocket RPC request handler to process the API requests
@ -88,53 +83,44 @@ type Node struct {
// New creates a new P2P node, ready for protocol registration.
func New(conf *Config) (*Node, error) {
// Ensure the data directory exists, failing if it cannot be created
// Copy config and resolve the datadir so future changes to the current
// working directory don't affect the node.
confCopy := *conf
conf = &confCopy
if conf.DataDir != "" {
if err := os.MkdirAll(conf.DataDir, 0700); err != nil {
absdatadir, err := filepath.Abs(conf.DataDir)
if err != nil {
return nil, err
}
conf.DataDir = absdatadir
}
// Ensure that the instance name doesn't cause weird conflicts with
// other files in the data directory.
if strings.ContainsAny(conf.Name, `/\`) {
return nil, errors.New(`Config.Name must not contain '/' or '\'`)
}
if conf.Name == datadirDefaultKeyStore {
return nil, errors.New(`Config.Name cannot be "` + datadirDefaultKeyStore + `"`)
}
if strings.HasSuffix(conf.Name, ".ipc") {
return nil, errors.New(`Config.Name cannot end in ".ipc"`)
}
// Ensure that the AccountManager method works before the node has started.
// We rely on this in cmd/geth.
am, ephemeralKeystore, err := makeAccountManager(conf)
if err != nil {
return nil, err
}
// Assemble the networking layer and the node itself
nodeDbPath := ""
if conf.DataDir != "" {
nodeDbPath = filepath.Join(conf.DataDir, datadirNodeDatabase)
}
// Note: any interaction with Config that would create/touch files
// in the data directory or instance directory is delayed until Start.
return &Node{
datadir: conf.DataDir,
accman: am,
ephemeralKeystore: ephemeralKeystore,
serverConfig: p2p.Config{
PrivateKey: conf.NodeKey(),
Name: conf.Name,
Discovery: !conf.NoDiscovery,
BootstrapNodes: conf.BootstrapNodes,
StaticNodes: conf.StaticNodes(),
TrustedNodes: conf.TrusterNodes(),
NodeDatabase: nodeDbPath,
ListenAddr: conf.ListenAddr,
NAT: conf.NAT,
Dialer: conf.Dialer,
NoDial: conf.NoDial,
MaxPeers: conf.MaxPeers,
MaxPendingPeers: conf.MaxPendingPeers,
},
config: conf,
serviceFuncs: []ServiceConstructor{},
ipcEndpoint: conf.IPCEndpoint(),
httpHost: conf.HTTPHost,
httpPort: conf.HTTPPort,
httpEndpoint: conf.HTTPEndpoint(),
httpWhitelist: conf.HTTPModules,
httpCors: conf.HTTPCors,
wsHost: conf.WSHost,
wsPort: conf.WSPort,
wsEndpoint: conf.WSEndpoint(),
wsWhitelist: conf.WSModules,
wsOrigins: conf.WSOrigins,
eventmux: new(event.TypeMux),
}, nil
}
@ -161,13 +147,36 @@ func (n *Node) Start() error {
if n.server != nil {
return ErrNodeRunning
}
// Otherwise copy and specialize the P2P configuration
if err := n.openDataDir(); err != nil {
return err
}
// Initialize the p2p server. This creates the node key and
// discovery databases.
n.serverConfig = p2p.Config{
PrivateKey: n.config.NodeKey(),
Name: n.config.NodeName(),
Discovery: !n.config.NoDiscovery,
BootstrapNodes: n.config.BootstrapNodes,
StaticNodes: n.config.StaticNodes(),
TrustedNodes: n.config.TrusterNodes(),
NodeDatabase: n.config.NodeDB(),
ListenAddr: n.config.ListenAddr,
NAT: n.config.NAT,
Dialer: n.config.Dialer,
NoDial: n.config.NoDial,
MaxPeers: n.config.MaxPeers,
MaxPendingPeers: n.config.MaxPendingPeers,
}
running := &p2p.Server{Config: n.serverConfig}
glog.V(logger.Info).Infoln("instance:", n.serverConfig.Name)
// Otherwise copy and specialize the P2P configuration
services := make(map[reflect.Type]Service)
for _, constructor := range n.serviceFuncs {
// Create a new context for the particular service
ctx := &ServiceContext{
datadir: n.datadir,
config: n.config,
services: make(map[reflect.Type]Service),
EventMux: n.eventmux,
AccountManager: n.accman,
@ -227,6 +236,26 @@ func (n *Node) Start() error {
return nil
}
func (n *Node) openDataDir() error {
if n.config.DataDir == "" {
return nil // ephemeral
}
instdir := filepath.Join(n.config.DataDir, n.config.name())
if err := os.MkdirAll(instdir, 0700); err != nil {
return err
}
// Try to open the instance directory as LevelDB storage. This creates a lock file
// which prevents concurrent use by another instance as well as accidental use of the
// instance directory as a database.
storage, err := storage.OpenFile(instdir, true)
if err != nil {
return err
}
n.instanceDirLock = storage
return nil
}
// startRPC is a helper method to start all the various RPC endpoint during node
// startup. It's not meant to be called at any time afterwards as it makes certain
// assumptions about the state of the node.
@ -244,12 +273,12 @@ func (n *Node) startRPC(services map[reflect.Type]Service) error {
n.stopInProc()
return err
}
if err := n.startHTTP(n.httpEndpoint, apis, n.httpWhitelist, n.httpCors); err != nil {
if err := n.startHTTP(n.httpEndpoint, apis, n.config.HTTPModules, n.config.HTTPCors); err != nil {
n.stopIPC()
n.stopInProc()
return err
}
if err := n.startWS(n.wsEndpoint, apis, n.wsWhitelist, n.wsOrigins); err != nil {
if err := n.startWS(n.wsEndpoint, apis, n.config.WSModules, n.config.WSOrigins); err != nil {
n.stopHTTP()
n.stopIPC()
n.stopInProc()
@ -381,7 +410,6 @@ func (n *Node) startHTTP(endpoint string, apis []rpc.API, modules []string, cors
n.httpEndpoint = endpoint
n.httpListener = listener
n.httpHandler = handler
n.httpCors = cors
return nil
}
@ -436,7 +464,6 @@ func (n *Node) startWS(endpoint string, apis []rpc.API, modules []string, wsOrig
n.wsEndpoint = endpoint
n.wsListener = listener
n.wsHandler = handler
n.wsOrigins = wsOrigins
return nil
}
@ -465,12 +492,12 @@ func (n *Node) Stop() error {
if n.server == nil {
return ErrNodeStopped
}
// Otherwise terminate the API, all services and the P2P server too
// Terminate the API, services and the p2p server.
n.stopWS()
n.stopHTTP()
n.stopIPC()
n.rpcAPIs = nil
failure := &StopError{
Services: make(map[reflect.Type]error),
}
@ -480,9 +507,16 @@ func (n *Node) Stop() error {
}
}
n.server.Stop()
n.services = nil
n.server = nil
// Release instance directory lock.
if n.instanceDirLock != nil {
n.instanceDirLock.Close()
n.instanceDirLock = nil
}
// unblock n.Wait
close(n.stop)
// Remove the keystore if it was created ephemerally.
@ -566,7 +600,7 @@ func (n *Node) Service(service interface{}) error {
// DataDir retrieves the current datadir used by the protocol stack.
func (n *Node) DataDir() string {
return n.datadir
return n.config.DataDir
}
// AccountManager retrieves the account manager used by the protocol stack.
@ -595,6 +629,21 @@ func (n *Node) EventMux() *event.TypeMux {
return n.eventmux
}
// OpenDatabase opens an existing database with the given name (or creates one if no
// previous can be found) from within the node's instance directory. If the node is
// ephemeral, a memory database is returned.
func (n *Node) OpenDatabase(name string, cache, handles int) (ethdb.Database, error) {
if n.config.DataDir == "" {
return ethdb.NewMemDatabase()
}
return ethdb.NewLDBDatabase(n.config.resolvePath(name), cache, handles)
}
// ResolvePath returns the absolute path of a resource in the instance directory.
func (n *Node) ResolvePath(x string) string {
return n.config.resolvePath(x)
}
// apis returns the collection of RPC descriptors this node offers.
func (n *Node) apis() []rpc.API {
return []rpc.API{

View File

@ -17,7 +17,6 @@
package node
import (
"path/filepath"
"reflect"
"github.com/ethereum/go-ethereum/accounts"
@ -31,7 +30,7 @@ import (
// the protocol stack, that is passed to all constructors to be optionally used;
// as well as utility methods to operate on the service environment.
type ServiceContext struct {
datadir string // Data directory for protocol persistence
config *Config
services map[reflect.Type]Service // Index of the already constructed services
EventMux *event.TypeMux // Event multiplexer used for decoupled notifications
AccountManager *accounts.Manager // Account manager created by the node.
@ -41,10 +40,10 @@ type ServiceContext struct {
// if no previous can be found) from within the node's data directory. If the
// node is an ephemeral one, a memory database is returned.
func (ctx *ServiceContext) OpenDatabase(name string, cache int, handles int) (ethdb.Database, error) {
if ctx.datadir == "" {
if ctx.config.DataDir == "" {
return ethdb.NewMemDatabase()
}
return ethdb.NewLDBDatabase(filepath.Join(ctx.datadir, name), cache, handles)
return ethdb.NewLDBDatabase(ctx.config.resolvePath(name), cache, handles)
}
// Service retrieves a currently running service registered of a specific type.
@ -64,10 +63,12 @@ type ServiceConstructor func(ctx *ServiceContext) (Service, error)
// Service is an individual protocol that can be registered into a node.
//
// Notes:
// - Service life-cycle management is delegated to the node. The service is
// allowed to initialize itself upon creation, but no goroutines should be
// spun up outside of the Start method.
// - Restart logic is not required as the node will create a fresh instance
//
// • Service life-cycle management is delegated to the node. The service is allowed to
// initialize itself upon creation, but no goroutines should be spun up outside of the
// Start method.
//
// • Restart logic is not required as the node will create a fresh instance
// every time a service is started.
type Service interface {
// Protocols retrieves the P2P protocols the service wishes to start.

View File

@ -38,18 +38,18 @@ func TestContextDatabases(t *testing.T) {
t.Fatalf("non-created database already exists")
}
// Request the opening/creation of a database and ensure it persists to disk
ctx := &ServiceContext{datadir: dir}
ctx := &ServiceContext{config: &Config{Name: "unit-test", DataDir: dir}}
db, err := ctx.OpenDatabase("persistent", 0, 0)
if err != nil {
t.Fatalf("failed to open persistent database: %v", err)
}
db.Close()
if _, err := os.Stat(filepath.Join(dir, "persistent")); err != nil {
if _, err := os.Stat(filepath.Join(dir, "unit-test", "persistent")); err != nil {
t.Fatalf("persistent database doesn't exists: %v", err)
}
// Request th opening/creation of an ephemeral database and ensure it's not persisted
ctx = &ServiceContext{datadir: ""}
ctx = &ServiceContext{config: &Config{DataDir: ""}}
db, err = ctx.OpenDatabase("ephemeral", 0, 0)
if err != nil {
t.Fatalf("failed to open ephemeral database: %v", err)