From eeb322ae649c4a1a32430cdddfffed70f509181e Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Thu, 18 Aug 2016 13:28:17 +0200 Subject: [PATCH] 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. --- cmd/geth/chaincmd.go | 53 ++++++++---- cmd/geth/consolecmd.go | 4 +- cmd/geth/dao_test.go | 4 +- cmd/geth/main.go | 16 ++-- cmd/geth/monitorcmd.go | 2 +- cmd/gethrpctest/main.go | 2 +- cmd/utils/flags.go | 50 ++++++------ node/api.go | 18 ++--- node/config.go | 129 +++++++++++++++++++++++------ node/config_test.go | 42 +++++----- node/doc.go | 90 +++++++++++++++++++++ node/node.go | 175 +++++++++++++++++++++++++--------------- node/service.go | 19 ++--- node/service_test.go | 6 +- 14 files changed, 423 insertions(+), 187 deletions(-) create mode 100644 node/doc.go diff --git a/cmd/geth/chaincmd.go b/cmd/geth/chaincmd.go index 54984d6e0..553e5367c 100644 --- a/cmd/geth/chaincmd.go +++ b/cmd/geth/chaincmd.go @@ -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) { diff --git a/cmd/geth/consolecmd.go b/cmd/geth/consolecmd.go index 92d6f7f86..066247303 100644 --- a/cmd/geth/consolecmd.go +++ b/cmd/geth/consolecmd.go @@ -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. diff --git a/cmd/geth/dao_test.go b/cmd/geth/dao_test.go index 7058fb385..59730b17f 100644 --- a/cmd/geth/dao_test.go +++ b/cmd/geth/dao_test.go @@ -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 { diff --git a/cmd/geth/main.go b/cmd/geth/main.go index a7b332d0f..65311ca41 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -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) diff --git a/cmd/geth/monitorcmd.go b/cmd/geth/monitorcmd.go index d1490dce2..b74315dab 100644 --- a/cmd/geth/monitorcmd.go +++ b/cmd/geth/monitorcmd.go @@ -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{ diff --git a/cmd/gethrpctest/main.go b/cmd/gethrpctest/main.go index d267dbf58..2c35ec943 100644 --- a/cmd/gethrpctest/main.go +++ b/cmd/gethrpctest/main.go @@ -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"}, diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index 3ab556a8f..47a8ebd41 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -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 := ð.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) { diff --git a/node/api.go b/node/api.go index 3523874ab..2942705d9 100644 --- a/node/api.go +++ b/node/api.go @@ -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, ",") { diff --git a/node/config.go b/node/config.go index 432da7015..4a18432c7 100644 --- a/node/config.go +++ b/node/config.go @@ -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 diff --git a/node/config_test.go b/node/config_test.go index 45a54d184..b258d2a8b 100644 --- a/node/config_test.go +++ b/node/config_test.go @@ -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") } } diff --git a/node/doc.go b/node/doc.go new file mode 100644 index 000000000..f009e6f85 --- /dev/null +++ b/node/doc.go @@ -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 . + +/* +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 diff --git a/node/node.go b/node/node.go index f3be2f763..41c9eb27f 100644 --- a/node/node.go +++ b/node/node.go @@ -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 . -// 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 - accman *accounts.Manager - ephemeralKeystore string // if non-empty, the key directory that will be removed by Stop + 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,21 +68,14 @@ 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 + wsEndpoint string // Websocket endpoint (interface + port) to listen at (empty = websocket disabled) + wsListener net.Listener // Websocket RPC listener socket to server API requests + wsHandler *rpc.Server // Websocket RPC request handler to process the API requests stop chan struct{} // Channel to wait for termination notifications lock sync.RWMutex @@ -88,54 +83,45 @@ 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, - }, - 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), + config: conf, + serviceFuncs: []ServiceConstructor{}, + ipcEndpoint: conf.IPCEndpoint(), + httpEndpoint: conf.HTTPEndpoint(), + wsEndpoint: conf.WSEndpoint(), + 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{ diff --git a/node/service.go b/node/service.go index 51531466b..1cd1fe808 100644 --- a/node/service.go +++ b/node/service.go @@ -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,11 +63,13 @@ 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 -// every time a service is started. +// +// • 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. Protocols() []p2p.Protocol diff --git a/node/service_test.go b/node/service_test.go index 7bd94a52e..a7ae439e0 100644 --- a/node/service_test.go +++ b/node/service_test.go @@ -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)