Mobile wallpaper 1Mobile wallpaper 2Mobile wallpaper 3
1339 words
7 minutes
Geth(14) QA

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 called

The 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())
▼ override
TOML config file (if --config is set)
▼ override
CLI 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 API
filterSystem := 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/SIGTERM

Q2: 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 shutdown
2nd-10th → print warning + countdown
11th → 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 shutdown

Why? 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 corruption

The correct order follows the reverse of the dependency chain:

Startup order: chainDb → engine → blockchain → txPool → handler → miner
Shutdown order: handler → txPool → blockchain → engine → chainDb
Principle: stop data consumers first, then data producers, then storage last

Complete 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 exits

doClose() 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.

Geth(14) QA
https://kehaozheng.vercel.app/posts/chainethgeth/14_qa/
Author
Kehao Zheng
Published at
2026-04-23
License
CC BY-NC-SA 4.0

Some information may be outdated