Q1: How does geth assemble and start all subsystems at startup?
Startup pipeline overview
main() │ ▼app.Run(os.Args) urfave/cli framework parses command line │ ▼geth() default action when no subcommand given ├─ prepare() log network type, bump mainnet cache to 4096MB ├─ makeFullNode() build Node container + create all subsystems ← core ├─ startNode() start all services + install signal handler └─ stack.Wait() block until Close() is calledThe geth() function itself is very concise:
func geth(ctx *cli.Context) error { prepare(ctx) stack := makeFullNode(ctx) defer stack.Close()
startNode(ctx, stack, false) stack.Wait() // blocks on n.stop channel until shutdown return nil}Four lines, four stages. Let’s expand each layer.
Stage 1: Configuration loading
Configuration is layered from three sources:
Hardcoded defaults (ethconfig.Defaults, defaultNodeConfig()) │ ▼ overrideTOML config file (if --config is set) │ ▼ overrideCLI flags (--syncmode, --cache, --maxpeers, etc.)The final output is two config structs: node.Config (P2P, RPC, data directory, etc.) and ethconfig.Config (sync mode, gas price, cache allocation, etc.).
Stage 2: Node container creation
node.New() creates the Node but does not start it:
func New(conf *Config) (*Node, error) { node := &Node{ config: conf, inprocHandler: rpc.NewServer(), // in-process RPC server server: &p2p.Server{...}, // P2P server (not started) databases: make(map[*closeTrackingDB]struct{}), stop: make(chan struct{}), // channel Wait() blocks on } node.rpcAPIs = append(node.rpcAPIs, node.apis()...) // admin, debug, web3 node.openDataDir() // file lock prevents duplicate instances // Create HTTP, WS, IPC server objects (not started) return node, nil}Key design: Node has a three-state state machine:
initializingState(0) ──Start()──→ runningState(1) ──Close()──→ closedState(2)All registrations (RegisterLifecycle, RegisterAPIs, RegisterProtocols) must complete before Start() — calling them in non-initializing state panics.
Stage 3: eth.New() — core assembly
This is the chapter’s most important function. It creates and connects all components from the previous 13 chapters in dependency order:
func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { // Step 1-2: Open database chainDb := stack.OpenDatabaseWithOptions("chaindata", ...)
// Step 3: Determine state storage scheme scheme := rawdb.ParseStateScheme(config.StateScheme, chainDb)
// Step 4: Load chain config, create consensus engine chainConfig := core.LoadChainConfig(chainDb, config.Genesis) engine := ethconfig.CreateConsensusEngine(chainConfig, chainDb)
// Step 5: Assemble Ethereum struct eth := &Ethereum{config, chainDb, engine, ...}
// Step 6: Create BlockChain eth.blockchain = core.NewBlockChain(chainDb, config.Genesis, engine, ...)
// Step 7: Log index eth.filterMaps = filtermaps.NewFilterMaps(...)
// Step 8: Create transaction pools legacyPool := legacypool.New(config.TxPool, eth.blockchain) eth.blobTxPool = blobpool.New(config.BlobPool, eth.blockchain, ...) eth.txPool = txpool.New(..., []txpool.SubPool{legacyPool, eth.blobTxPool})
// Step 9: Create protocol handler eth.handler = newHandler(&handlerConfig{...})
// Step 10: Create miner eth.miner = miner.New(eth, config.Miner, engine)
// Step 11: Create API backend eth.APIBackend = &EthAPIBackend{...}
// Step 12: Register on Node stack.RegisterAPIs(eth.APIs()) // JSON-RPC methods stack.RegisterProtocols(eth.Protocols()) // eth/68, snap/1 sub-protocols stack.RegisterLifecycle(eth) // Start/Stop lifecycle}Dependency chain visualization
The order is not arbitrary — each step depends on the previous ones:
chainDb (Ch.5: storage layer) │ ▼engine (Ch.9: consensus engine) ─── depends on chainDb for chain config │ ▼BlockChain (Ch.10) ──────────── depends on chainDb + engine │ ▼TxPool (Ch.8) ───────────────── depends on BlockChain (chain head, state validation) │ ▼handler (Ch.12) ─────────────── depends on BlockChain + TxPool (sync and broadcast) │ ▼miner (Ch.9) ────────────────── depends on Ethereum + engine (block building) │ ▼APIBackend (Ch.13) ──────────── depends on all of the above (RPC method entry point)If TxPool were created before BlockChain, TxPool would have no chain head state to validate transactions against. If handler were created before TxPool, handler would have no pool to route transactions to.
Stage 4: Optional services and Engine API
After the core Ethereum service, optional components are registered:
// Log filter APIfilterSystem := utils.RegisterFilterAPI(stack, backend, &cfg.Eth)
// GraphQL (optional)if ctx.IsSet(utils.GraphQLEnabledFlag.Name) { utils.RegisterGraphQLService(stack, backend, filterSystem, &cfg.Node)}
// Engine API (three modes)if --dev { SimulatedBeacon // dev mode: auto-seal blocks} else if --beacon.api { BLSync // experimental light sync} else { catalyst.Register // normal mode: Engine API connects to external CL}Stage 5: Node.Start()
After all registration is complete, startNode() calls stack.Start():
func (n *Node) Start() error { // 1. State check: must be initializingState n.state = runningState
// 2. Start network endpoints n.openEndpoints() // ├─ n.server.Start() → P2P server starts (Chapter 11) // └─ n.startRPC() → HTTP, WS, IPC, auth endpoints all start
// 3. Start all lifecycles in registration order for _, lifecycle := range lifecycles { lifecycle.Start() // Ethereum.Start() is called here } // If any fails, already-started ones are stopped in reverse order}Ethereum.Start() starts the network layer:
func (s *Ethereum) Start() error { s.setupDiscovery() // DNS + DHT hybrid discovery (Chapter 11) s.handler.Start(...) // sync + tx/block broadcast loops (Chapter 12) s.dropper.Start(...) // connection quality management s.filterMaps.Start() // log indexer}After startup completes
stack.Wait() blocks on n.stop channel │ Concurrently running goroutines: ├─ P2P server: accept inbound connections, discover new nodes ├─ handler: sync blockchain, broadcast transactions ├─ miner: wait for CL's ForkchoiceUpdated to build blocks ├─ txPool: receive and manage transactions ├─ RPC servers: handle external requests └─ signal handler: wait for SIGINT/SIGTERMQ2: How does geth shut down gracefully? Why is teardown order reversed?
Signal handling
utils.StartNode() installs a signal handler:
func StartNode(ctx *cli.Context, stack *node.Node, isConsole bool) { stack.Start()
go func() { sigc := make(chan os.Signal, 1) signal.Notify(sigc, syscall.SIGINT, syscall.SIGTERM)
<-sigc // first signal log.Info("Got interrupt, shutting down...") go stack.Close() // close in separate goroutine (may take time)
// Wait for up to 10 more signals for i := 10; i > 0; i-- { <-sigc log.Warn("Already shutting down, interrupt more to panic.", "times", i-1) } debug.LoudPanic("boom") // 11th time: force kill process }()}Design:
1st Ctrl-C → start graceful shutdown2nd-10th → print warning + countdown11th → panic force exit (last resort for stuck shutdown)In console mode (geth attach), SIGINT is ignored (left for the JavaScript console), only SIGTERM triggers shutdown.
Disk space monitoring
There’s a hidden shutdown trigger — low disk space:
Background goroutine checks available disk space every 30 seconds │ If < 2 × TrieDirtyCache (default 512MB) │ ▼Automatically send SIGTERM → trigger graceful shutdownWhy? Because databases (Pebble/LevelDB) corrupt when disk is full. Shutting down early is much better than data corruption.
Node.Close() teardown order
func (n *Node) Close() error { // 1. Stop all services n.stopServices(n.lifecycles) // 2. Release remaining resources n.doClose(errs)}The key in stopServices(): reverse order:
func (n *Node) stopServices(running []Lifecycle) error { // First stop RPC n.stopRPC()
// Stop all lifecycles in reverse for i := len(running) - 1; i >= 0; i-- { running[i].Stop() }
// Last stop P2P server n.server.Stop()}Ethereum.Stop() internals
When Node calls eth.Stop():
func (s *Ethereum) Stop() error { // Layer 1: Stop network (no more incoming data) s.discmix.Close() // stop node discovery s.dropper.Stop() // stop connection management s.handler.Stop() // stop sync and broadcast
// Layer 2: Stop internal processing s.filterMaps.Stop() // stop log indexer s.txPool.Close() // close transaction pool s.blockchain.Stop() // stop blockchain (flush trie to disk) s.engine.Close() // close consensus engine
// Layer 3: Persistence and cleanup s.shutdownTracker.Stop() // mark clean shutdown s.chainDb.Close() // close database s.eventMux.Stop() // stop event dispatch}Why reverse teardown is necessary
Illustrating with an incorrect order:
Wrong example: close database before stopping handler
handler is syncing blocks → calls blockchain.InsertChain() → calls chainDb.Write() → database is already closed! → panic or data corruptionThe correct order follows the reverse of the dependency chain:
Startup order: chainDb → engine → blockchain → txPool → handler → minerShutdown order: handler → txPool → blockchain → engine → chainDb
Principle: stop data consumers first, then data producers, then storage lastComplete shutdown hierarchy:
Layer 1: Cut off external input ├─ stopRPC() → stop accepting RPC requests ├─ discmix.Close() → stop discovering new nodes └─ handler.Stop() → stop sync and broadcast
Layer 2: Stop internal processing ├─ txPool.Close() → stop transaction processing ├─ blockchain.Stop() → flush trie cache to disk └─ engine.Close() → close consensus engine
Layer 3: Close infrastructure ├─ chainDb.Close() → close database ├─ server.Stop() → close P2P server └─ close(n.stop) → unblock Wait() → process exitsdoClose() final cleanup
After all lifecycles stop, doClose() releases remaining resources:
func (n *Node) doClose(errs []error) error { n.state = closedState n.closeDatabases() // close all tracked databases n.accman.Close() // stop hardware wallet USB monitoring if n.keyDirTemp { os.RemoveAll(n.keyDir) // delete temp key directory } n.closeDataDir() // release file lock close(n.stop) // unblock stack.Wait() in geth()}close(n.stop) is the final step — it unblocks stack.Wait() in the geth() function, geth() returns, main() returns, process exits.
Some information may be outdated






