// Copyright 2015 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 import ( "errors" "fmt" "net" "net/http" "os" "path/filepath" "reflect" "strings" "sync" "github.com/ledgerwatch/turbo-geth/accounts" "github.com/ledgerwatch/turbo-geth/ethdb" "github.com/ledgerwatch/turbo-geth/event" "github.com/ledgerwatch/turbo-geth/log" "github.com/ledgerwatch/turbo-geth/migrations" "github.com/ledgerwatch/turbo-geth/p2p" "github.com/ledgerwatch/turbo-geth/rpc" "github.com/prometheus/tsdb/fileutil" ) // Node is a container on which services can be registered. type Node struct { eventmux *event.TypeMux config *Config accman *accounts.Manager log log.Logger ephemKeystore string // if non-empty, the key directory that will be removed by Stop dirLock fileutil.Releaser // prevents concurrent use of instance directory stop chan struct{} // Channel to wait for termination notifications server *p2p.Server // Currently running P2P networking layer startStopLock sync.Mutex // Start/Stop are protected by an additional lock state int // Tracks state of node lifecycle lock sync.Mutex lifecycles []Lifecycle // All registered backends, services, and auxiliary services that have a lifecycle rpcAPIs []rpc.API // List of APIs currently provided by the node http *httpServer // ws *httpServer // ipc *ipcServer // Stores information about the ipc http server inprocHandler *rpc.Server // In-process RPC request handler to process the API requests rpcAllowList rpc.AllowList // list of RPC methods explicitly allowed for this RPC node databases []ethdb.Closer } const ( initializingState = iota runningState closedState ) // New creates a new P2P node, ready for protocol registration. func New(conf *Config) (*Node, error) { // 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 != "" { absdatadir, err := filepath.Abs(conf.DataDir) if err != nil { return nil, err } conf.DataDir = absdatadir } if conf.Logger == nil { conf.Logger = log.New() } // 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"`) } node := &Node{ config: conf, inprocHandler: rpc.NewServer(), eventmux: new(event.TypeMux), log: conf.Logger, stop: make(chan struct{}), server: &p2p.Server{Config: conf.P2P}, databases: make([]ethdb.Closer, 0), } // Register built-in APIs. node.rpcAPIs = append(node.rpcAPIs, node.apis()...) // Acquire the instance directory lock. if err := node.openDataDir(); err != nil { return nil, err } // 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 } node.accman = am node.ephemKeystore = ephemeralKeystore // Initialize the p2p server. This creates the node key and discovery databases. node.server.Config.PrivateKey, err = node.config.NodeKey() if err != nil { return nil, err } node.server.Config.Name = node.config.NodeName() node.server.Config.Logger = node.log if node.server.Config.StaticNodes == nil { node.server.Config.StaticNodes, err = node.config.StaticNodes() if err != nil { return nil, err } } if node.server.Config.TrustedNodes == nil { node.server.Config.TrustedNodes, err = node.config.TrustedNodes() if err != nil { return nil, err } } if node.server.Config.NodeDatabase == "" { node.server.Config.NodeDatabase, err = node.config.NodeDB() if err != nil { return nil, err } } // Configure RPC servers. node.http = newHTTPServer(node.log, conf.HTTPTimeouts) node.ws = newHTTPServer(node.log, rpc.DefaultHTTPTimeouts) node.ipc = newIPCServer(node.log, conf.IPCEndpoint()) return node, nil } func (n *Node) SetP2PListenFunc(listenFunc func(network, addr string) (net.Listener, error)) { n.server.SetP2PListenFunc(listenFunc) } // Start starts all registered lifecycles, RPC services and p2p networking. // Node can only be started once. func (n *Node) Start() error { n.startStopLock.Lock() defer n.startStopLock.Unlock() n.lock.Lock() switch n.state { case runningState: n.lock.Unlock() return ErrNodeRunning case closedState: n.lock.Unlock() return ErrNodeStopped } n.state = runningState err := n.startNetworking() lifecycles := make([]Lifecycle, len(n.lifecycles)) copy(lifecycles, n.lifecycles) n.lock.Unlock() // Check if networking startup failed. if err != nil { n.doClose(nil) return err } // Start all registered lifecycles. // preallocation leads to bugs here var started []Lifecycle //nolint:prealloc for _, lifecycle := range lifecycles { if err = lifecycle.Start(); err != nil { break } started = append(started, lifecycle) } // Check if any lifecycle failed to start. if err != nil { n.stopServices(started) //nolint:errcheck n.doClose(nil) } return err } // Close stops the Node and releases resources acquired in // Node constructor New. func (n *Node) Close() error { n.startStopLock.Lock() defer n.startStopLock.Unlock() n.lock.Lock() state := n.state n.lock.Unlock() switch state { case initializingState: // The node was never started. return n.doClose(nil) case runningState: // The node was started, release resources acquired by Start(). var errs []error if err := n.stopServices(n.lifecycles); err != nil { errs = append(errs, err) } return n.doClose(errs) case closedState: return ErrNodeStopped default: panic(fmt.Sprintf("node is in unknown state %d", state)) } } // doClose releases resources acquired by New(), collecting errors. func (n *Node) doClose(errs []error) error { // Close databases. This needs the lock because it needs to // synchronize with OpenDatabase*. n.lock.Lock() n.state = closedState for _, closer := range n.databases { closer.Close() } n.lock.Unlock() if err := n.accman.Close(); err != nil { errs = append(errs, err) } if n.ephemKeystore != "" { if err := os.RemoveAll(n.ephemKeystore); err != nil { errs = append(errs, err) } } // Release instance directory lock. n.closeDataDir() // Unblock n.Wait. close(n.stop) // Report any errors that might have occurred. switch len(errs) { case 0: return nil case 1: return errs[0] default: return fmt.Errorf("%v", errs) } } // startNetworking starts all network endpoints. func (n *Node) startNetworking() error { n.log.Info("Starting peer-to-peer node", "instance", n.server.Name) if err := n.server.Start(); err != nil { return convertFileLockError(err) } err := n.startRPC() if err != nil { n.stopRPC() n.server.Stop() } return err } // containsLifecycle checks if 'lfs' contains 'l'. func containsLifecycle(lfs []Lifecycle, l Lifecycle) bool { for _, obj := range lfs { if obj == l { return true } } return false } // stopServices terminates running services, RPC and p2p networking. // It is the inverse of Start. func (n *Node) stopServices(running []Lifecycle) error { n.stopRPC() // Stop running lifecycles in reverse order. failure := &StopError{Services: make(map[reflect.Type]error)} for i := len(running) - 1; i >= 0; i-- { if err := running[i].Stop(); err != nil { failure.Services[reflect.TypeOf(running[i])] = err } } // Stop p2p networking. n.server.Stop() if len(failure.Services) > 0 { return failure } return nil } func (n *Node) openDataDir() error { if n.config.DataDir == "" { return nil // ephemeral } instdir := n.config.instanceDir() if err := os.MkdirAll(instdir, 0700); err != nil { return err } // Lock the instance directory to prevent concurrent use by another instance as well as // accidental use of the instance directory as a database. release, _, err := fileutil.Flock(filepath.Join(instdir, "LOCK")) if err != nil { return convertFileLockError(err) } n.dirLock = release return nil } func (n *Node) closeDataDir() { // Release instance directory lock. if n.dirLock != nil { if err := n.dirLock.Release(); err != nil { n.log.Error("Can't release datadir lock", "err", err) } n.dirLock = nil } } // SetAllowListForRPC sets granular allow list for exposed RPC methods func (n *Node) SetAllowListForRPC(allowList rpc.AllowList) { n.rpcAllowList = allowList } // configureRPC is a helper method to configure all the various RPC endpoints 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. func (n *Node) startRPC() error { if err := n.startInProc(); err != nil { return err } // Configure IPC. if n.ipc.endpoint != "" { if err := n.ipc.start(n.rpcAPIs); err != nil { return err } } // Configure HTTP. if n.config.HTTPHost != "" { config := httpConfig{ CorsAllowedOrigins: n.config.HTTPCors, Vhosts: n.config.HTTPVirtualHosts, Modules: n.config.HTTPModules, } if err := n.http.setListenAddr(n.config.HTTPHost, n.config.HTTPPort); err != nil { return err } if err := n.http.enableRPC(n.rpcAPIs, config, n.rpcAllowList); err != nil { return err } } // Configure WebSocket. if n.config.WSHost != "" { server := n.wsServerForPort(n.config.WSPort) config := wsConfig{ Modules: n.config.WSModules, Origins: n.config.WSOrigins, } if err := server.setListenAddr(n.config.WSHost, n.config.WSPort); err != nil { return err } if err := server.enableWS(n.rpcAPIs, config, n.rpcAllowList); err != nil { return err } } if err := n.http.start(); err != nil { return err } return n.ws.start() } func (n *Node) wsServerForPort(port int) *httpServer { if n.config.HTTPHost == "" || n.http.port == port { return n.http } return n.ws } func (n *Node) stopRPC() { n.http.stop() n.ws.stop() n.ipc.stop() //nolint:errcheck n.stopInProc() } // startInProc registers all RPC APIs on the inproc server. func (n *Node) startInProc() error { for _, api := range n.rpcAPIs { if err := n.inprocHandler.RegisterName(api.Namespace, api.Service); err != nil { return err } } return nil } // stopInProc terminates the in-process RPC endpoint. func (n *Node) stopInProc() { n.inprocHandler.Stop() } // Wait blocks until the node is closed. func (n *Node) Wait() { <-n.stop } // RegisterLifecycle registers the given Lifecycle on the node. func (n *Node) RegisterLifecycle(lifecycle Lifecycle) { n.lock.Lock() defer n.lock.Unlock() if n.state != initializingState { panic("can't register lifecycle on running/stopped node") } if containsLifecycle(n.lifecycles, lifecycle) { panic(fmt.Sprintf("attempt to register lifecycle %T more than once", lifecycle)) } n.lifecycles = append(n.lifecycles, lifecycle) } // RegisterProtocols adds backend's protocols to the node's p2p server. func (n *Node) RegisterProtocols(protocols []p2p.Protocol) { n.lock.Lock() defer n.lock.Unlock() // Remove the keystore if it was created ephemerally. if n.state != initializingState { panic("can't register protocols on running/stopped node") } n.server.Protocols = append(n.server.Protocols, protocols...) } // RegisterAPIs registers the APIs a service provides on the node. func (n *Node) RegisterAPIs(apis []rpc.API) { n.lock.Lock() defer n.lock.Unlock() if n.state != initializingState { panic("can't register APIs on running/stopped node") } n.rpcAPIs = append(n.rpcAPIs, apis...) } // RegisterHandler mounts a handler on the given path on the canonical HTTP server. // // The name of the handler is shown in a log message when the HTTP server starts // and should be a descriptive term for the service provided by the handler. func (n *Node) RegisterHandler(name, path string, handler http.Handler) { n.lock.Lock() defer n.lock.Unlock() if n.state != initializingState { panic("can't register HTTP handler on running/stopped node") } n.http.mux.Handle(path, handler) n.http.handlerNames[path] = name } // Attach creates an RPC client attached to an in-process API handler. func (n *Node) Attach() (*rpc.Client, error) { return rpc.DialInProc(n.inprocHandler), nil } // RPCHandler returns the in-process RPC request handler. func (n *Node) RPCHandler() (*rpc.Server, error) { n.lock.Lock() defer n.lock.Unlock() if n.state == closedState { return nil, ErrNodeStopped } return n.inprocHandler, nil } // Config returns the configuration of node. func (n *Node) Config() *Config { return n.config } // Server retrieves the currently running P2P network layer. This method is meant // only to inspect fields of the currently running server. Callers should not // start or stop the returned server. func (n *Node) Server() *p2p.Server { n.lock.Lock() defer n.lock.Unlock() return n.server } // DataDir retrieves the current datadir used by the protocol stack. // Deprecated: No files should be stored in this directory, use InstanceDir instead. func (n *Node) DataDir() string { return n.config.DataDir } // InstanceDir retrieves the instance directory used by the protocol stack. func (n *Node) InstanceDir() string { return n.config.instanceDir() } // AccountManager retrieves the account manager used by the protocol stack. func (n *Node) AccountManager() *accounts.Manager { return n.accman } // IPCEndpoint retrieves the current IPC endpoint used by the protocol stack. func (n *Node) IPCEndpoint() string { return n.ipc.endpoint } // HTTPEndpoint returns the URL of the HTTP server. func (n *Node) HTTPEndpoint() string { return "http://" + n.http.listenAddr() } // WSEndpoint retrieves the current WS endpoint used by the protocol stack. func (n *Node) WSEndpoint() string { if n.http.wsAllowed() { return "ws://" + n.http.listenAddr() } return "ws://" + n.ws.listenAddr() } // EventMux retrieves the event multiplexer used by all the network services in // the current protocol stack. 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, tmpdir string) (*ethdb.ObjectDatabase, error) { return n.OpenDatabaseWithFreezer(name, tmpdir) } // OpenDatabaseWithFreezer opens an existing database with the given name (or // creates one if no previous can be found) from within the node's data directory, // also attaching a chain freezer to it that moves ancient chain data from the // database to immutable append-only files. If the node is an ephemeral one, a // memory database is returned. // NOTE: kept for compatibility and for easier rebases (turbo-geth) func (n *Node) OpenDatabaseWithFreezer(name string, tmpdir string) (*ethdb.ObjectDatabase, error) { n.lock.Lock() defer n.lock.Unlock() if n.state == closedState { return nil, ErrNodeStopped } var db *ethdb.ObjectDatabase if n.config.DataDir == "" { fmt.Printf("Opening In-memory Database (LMDB): %s\n", name) db = ethdb.NewMemDatabase() } else { dbPath, err := n.config.ResolvePath(name) if err != nil { return nil, err } var openFunc func(exclusive bool) (*ethdb.ObjectDatabase, error) if n.config.MDBX { log.Info("Opening Database (MDBX)", "mapSize", n.config.LMDBMapSize.HR()) openFunc = func(exclusive bool) (*ethdb.ObjectDatabase, error) { opts := ethdb.NewMDBX().Path(dbPath).MapSize(n.config.LMDBMapSize) if exclusive { opts = opts.Exclusive() } kv, err1 := opts.Open() if err1 != nil { return nil, err1 } return ethdb.NewObjectDatabase(kv), nil } } else { log.Info("Opening Database (LMDB)", "mapSize", n.config.LMDBMapSize.HR(), "maxFreelistReuse", n.config.LMDBMaxFreelistReuse) openFunc = func(exclusive bool) (*ethdb.ObjectDatabase, error) { opts := ethdb.NewLMDB().Path(dbPath).MapSize(n.config.LMDBMapSize).MaxFreelistReuse(n.config.LMDBMaxFreelistReuse) if exclusive { opts = opts.Exclusive() } kv, err1 := opts.Open() if err1 != nil { return nil, err1 } return ethdb.NewObjectDatabase(kv), nil } } db, err = openFunc(false) if err != nil { return nil, err } migrator := migrations.NewMigrator() has, err := migrator.HasPendingMigrations(db) if err != nil { return nil, err } if has { log.Info("Re-Opening DB in exclusive mode to apply migrations") db.Close() db, err = openFunc(true) if err != nil { return nil, err } if err = migrator.Apply(db, tmpdir); err != nil { return nil, err } db.Close() db, err = openFunc(false) if err != nil { return nil, err } } } n.databases = append(n.databases, db) return db, nil } // ResolvePath returns the absolute path of a resource in the instance directory. func (n *Node) ResolvePath(x string) (string, error) { return n.config.ResolvePath(x) }