aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorFelix Lange <fjl@twurst.com>2016-08-18 19:28:17 +0800
committerFelix Lange <fjl@twurst.com>2016-09-16 21:24:31 +0800
commiteeb322ae649c4a1a32430cdddfffed70f509181e (patch)
tree35622201208afb98665743d9bcf88883058e772a
parent52ede09b172094f8fd85f8b10e7d0578059353fb (diff)
downloaddexon-eeb322ae649c4a1a32430cdddfffed70f509181e.tar.gz
dexon-eeb322ae649c4a1a32430cdddfffed70f509181e.tar.zst
dexon-eeb322ae649c4a1a32430cdddfffed70f509181e.zip
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.
-rw-r--r--cmd/geth/chaincmd.go51
-rw-r--r--cmd/geth/consolecmd.go4
-rw-r--r--cmd/geth/dao_test.go4
-rw-r--r--cmd/geth/main.go16
-rw-r--r--cmd/geth/monitorcmd.go2
-rw-r--r--cmd/gethrpctest/main.go2
-rw-r--r--cmd/utils/flags.go50
-rw-r--r--node/api.go18
-rw-r--r--node/config.go129
-rw-r--r--node/config_test.go42
-rw-r--r--node/doc.go90
-rw-r--r--node/node.go175
-rw-r--r--node/service.go19
-rw-r--r--node/service_test.go6
14 files changed, 422 insertions, 186 deletions
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...")
+ 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(filepath.Join(ctx.GlobalString(utils.DataDirFlag.Name), "chaindata"))
-
+ os.RemoveAll(dbdir)
fmt.Printf("Removed in %v\n", time.Since(start))
- } else {
- fmt.Println("Operation aborted")
}
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 := &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) {
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 <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
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 <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
- 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)