Component Framework
Breaking the Ethereum god object into composable components with a lifecycle framework
Status: In progress. Part 1 (CLI Flag Decoupling) and Wave 1 (Component Framework in erigon-lib/app/) are complete. Waves 2–4 extract the 12 discrete components from the Ethereum struct. Waves 5–6 (L2 infrastructure and rollup drivers) are deferred. See the Status table below.
The Component Framework transforms Erigon's monolithic startup — an 800-line constructor and a 200-field god object — into a graph of independently activatable components. Each component has a typed provider, declares its dependencies explicitly, and follows a five-stage lifecycle. The framework resolves activation order automatically and assembles the right set of services at startup based on the requested node mode.
Without the component framework, implementing the L1/L2 Node's deployment modes (L1, L2, Combined, Validator) would require complex conditional logic spread throughout the existing eth.New() constructor and Start() dispatch. The framework makes mode selection a matter of registering the right components — not rewriting startup code.
Status (as of 2026-03-24)
Part 1: CLI Flag Decoupling
BuildEthConfig() consolidated entry point; config snapshot tests; global state eliminated
Complete
Wave 1: Component Framework
erigon-lib/app/ framework from app_components branch
Complete
Wave 2: Storage, P2P, Downloader
Extract first components
In progress
Wave 3: Consensus, Sync, Execution
Core component extractions
Planned
Wave 4: TxPool, RPC, Miner
Remaining components + NodeBuilder + registry
Planned
Wave 5: L2 Infrastructure
Hierarchical domains for combined L1+L2 mode
Deferred
Wave 6: Rollup Drivers
Based, Optimistic, Consensus, ZK drivers
Deferred
Guiding principle: A single shared domain is used for L1 in Waves 1–4. Hierarchical domains (for L1/L2 combined mode) are added in Wave 5.
Part 2: Component Extraction
12 components from Ethereum struct (59 fields)
In progress (Waves 2–4)
Part 3: NodeBuilder
Composition root at node/nodebuilder/
Planned (Wave 4)
Part 4: RollupDriver / L2 Pipeline
Driver interface, L2DefaultStages factory
Deferred (Wave 5)
Part 5: RPC Chain-ID Routing
Only needed for combined L1+L2 mode
Deferred (Wave 5)
Part 6: Build-Time Composition
components.cfg + go generate + init() registration
Planned (Wave 4)
What the Framework Provides
Typed providers: Each component exposes a Provider struct holding its live state — DB handles, service instances, channels. Other components declare dependencies on specific provider types; the framework resolves and injects them automatically.
Dependency graph: Components activate after their dependencies and deactivate before them. Activation order is derived from the dependency graph at startup — no manual ordering needed.
Five-stage lifecycle: Configure → Initialize → Recover → Activate → Deactivate. Each stage has a well-defined contract. Recover handles unclean shutdown (WAL replay, consistency checks) separately from initialization.
Event bus and service bus: Reflection-based pub/sub for async events (new block, finality notification, chain reorg). Synchronous RPC-style calls between components for request/response patterns. Together they replace the shards.Notifications global.
Build-time composition: A components.cfg file lists which components to compile. A go generate step produces blank imports; each package's init() registers with the component registry. Third-party rollup drivers or indexers are external Go modules — no fork required.
Component Lifecycle
Each component implements the five-state machine:
Configure
Apply typed app.Option values from ethconfig.Config; no I/O
Initialize
Open resources — databases, connections, snapshot files; resolve dependencies
Recover
Restore state after unclean shutdown — replay uncommitted WAL, verify consistency
Activate
Start goroutines, subscribe to events, begin serving requests
Deactivate
Flush state, close connections, stop goroutines
Component Map
The Ethereum struct's ~80 fields decompose into these components:
Storage
chainDB, blockSnapshots, blockReader, blockWriter
none (activates first)
P2P
sentryServers, sentriesClient, statusDataProvider
Storage
Consensus
engine, forkValidator
Storage
Downloader
downloader, downloaderClient
Storage
Sync
stagedSync, pipelineStagedSync, stage sets
Storage, P2P, Consensus
Execution
execModule
Sync, Consensus
TxPool
txPool, txPoolGrpcServer
Storage, P2P
RPC
ethBackendRPC, engineBackendRPC, apiList
Storage, Execution, TxPool
Miner
pendingBlocks, minedBlocks
Execution, TxPool
Polygon
polygonSyncService, polygonBridge, heimdallService
Storage, P2P, Consensus
Caplin
sentinel
Storage
Notifications
cross-cutting event bus
none
The hard-coded dispatch in backend.go:1491–1522 becomes conditional component registration in a NodeBuilder. The framework's dependency-aware activation handles ordering.
Implementation plan for contributors
Part 1: CLI Flag Decoupling — COMPLETE
Goal: Ensure *cli.Context is never accessed below cmd/erigon/node/node.go.
The boundary is already architecturally correct at the top level:
Completed work:
utils.SetEthConfig()+erigoncli.ApplyFlagsForEthConfig()merged into a singleBuildEthConfig(ctx)consolidated entry point.Config snapshot tests added to enforce the boundary.
Global state eliminated; grep for
cli.Contextbelowcmd/erigon/node/returns zero results.
Part 2: Component Extraction (6 waves)
12 components are mapped from the Ethereum struct (59 fields → discrete components): Storage, P2P, Consensus, Downloader, Sync, Execution, TxPool, RPC, Miner, Polygon, Caplin, Notifications.
Wave 1 — Foundation — COMPLETE
PR 1: Consolidate flag reading, enforce
cli.Contextboundary (done — Part 1)PR 2: Add
RollupConfigtoethconfig.Config,--rollup.modeflag (no-op defaultl1) (done)PR 3: Rebase
erigon-lib/app/framework fromapp_componentsbranch onto main (done — component framework live inerigon-lib/app/)
Wave 2 — Extract Storage, P2P, Downloader
PR 4:
StorageComponent— DB setup, block reader/writer, snapshotsPR 5:
DownloaderComponent— snapshot torrent clientPR 6:
TxPoolComponent—txpool.Assemble+RunPR 7:
P2PComponent— sentry servers, multi-client setup
Wave 3 — Extract Consensus, Sync, Execution
PR 8:
SyncComponent— staged sync construction + pipeline variants (high risk)PR 9:
ExecutionComponent— ExecModule + Start() dispatch (high risk)PR 10:
RPCComponent— all RPC server setupPR 11:
PolygonComponent— wrap heimdall + bridge + polygon sync
Wave 4 — Extract TxPool, RPC, Miner + Registry, NodeBuilder, packaging
PR 12: Component registry +
init()registrationPR 13:
components.cfgfile + code generator (build/gen/main.go→components_gen.go)PR 14:
NodeBuilder— composition root querying registry by mode; L1 mode = identical behavior to todayPR 15: Slim
Ethereumstruct to thin shell overComponentDomainPR 16:
shards.Notifications→app/event/ServiceBusmigration
Wave 5 — L2 infrastructure (Deferred)
Adds hierarchical domains for combined L1+L2 mode. Single shared domain is used for L1 in Waves 1–4.
PRs 17–22: L2 data dirs,
L2DefaultStages,rollup.Driverinterface,BorDriver,L1DataSource,NodeBuilderL2 modes, RPC chain-ID routing
Wave 6 — Add rollup drivers (Deferred)
PRs 23–25: Based rollup, Optimistic (OP Stack), Consensus (second Caplin), ZK
Part 3: NodeBuilder
Composition root at node/nodebuilder/ assembles components based on chain config. Planned for Wave 4.
Part 4: RollupDriver / L2 Pipeline — Deferred
Driver interface and L2DefaultStages factory. Deferred to Wave 5.
Part 5: RPC Chain-ID Routing — Deferred
Only required for combined L1+L2 mode. Deferred to Wave 5.
Part 6: Build-Time Composition (components.cfg)
components.cfg)Follows the CoreDNS model. A components.cfg file at the repo root lists which components to compile:
A go generate step reads the file and produces node/components/components_gen.go with blank imports. Each package's init() calls components.Register(name, factory). The NodeBuilder queries the registry at startup; missing required components fail fast with a clear error.
Third-party rollup drivers or indexers are external Go modules that call components.Register() in their init() — no fork of the Erigon repo required.
Critical Files
node/eth/backend.go
God object to decompose (1715 lines)
cmd/utils/flags.go
170+ flag reads → consolidate
node/cli/flags.go
69+ flag reads → merge
node/ethconfig/config.go
Add RollupConfig
erigon-lib/app/component/component.go
Component framework (from app_components branch)
erigon-lib/app/component/componentdomain.go
Domain lifecycle management
erigon-lib/app/event/eventbus.go
Event bus
execution/stagedsync/default_stages.go
Add L2DefaultStages factory
polygon/sync/service.go
Reference: good component composition pattern
node/direct/execution_client.go
Reference: direct adapter pattern
Last updated