Component Framework
Status: In progress. Part 1 (CLI Flag Decoupling) and Wave 1 (Foundation) are complete. Wave 2 extracts the framework and the first core components; Waves 3–4 complete the remaining extractions and packaging. 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 / Wave | Description | Status |
|---|---|---|
| Part 1: CLI Flag Decoupling | BuildEthConfig() consolidated entry point; config snapshot tests; global state eliminated | Complete |
| Wave 1: Foundation | CLI boundary enforced; no behavioral change | Complete |
| Wave 2: Framework + Core Extraction | Framework rebase; Storage, Sync, Execution components | Not started |
| Wave 3: Remaining Component Extractions | Downloader, TxPool, P2P, RPC, Polygon | Planned |
| Wave 4: Registry, NodeBuilder, Packaging | Component registry, components.cfg, NodeBuilder, slim Ethereum | 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 | Description | Status |
|---|---|---|
| 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:
| Phase | What happens |
|---|---|
| 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:
| Component | Key provider fields | Dependencies |
|---|---|---|
| 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:
runErigon(cliCtx)
→ NewNodConfigUrfave(cliCtx) → *nodecfg.Config
→ NewEthConfigUrfave(cliCtx) → *ethconfig.Config
→ node.New(ctx, nodeCfg, ethCfg) ← no cli.Context below here
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 1a: Config snapshot tests + global state elimination (done)
- PR 1b: Consolidate flag reading,
BuildEthConfig()entry point (done)
Wave 2 — Framework + Core Extraction
- PR 2: Rebase
erigon-lib/app/framework fromapp_componentsbranch onto main - PR 3:
StorageComponent— DB setup, block reader/writer, snapshots - PR 4:
SyncComponent— staged sync construction + pipeline variants (high risk) - PR 5:
ExecutionComponent— ExecModule + Start() dispatch (high risk)
Wave 3 — Remaining Component Extractions
- PR 6:
DownloaderComponent— snapshot torrent client - PR 7:
TxPoolComponent—txpool.Assemble+Run - PR 8:
P2PComponent— sentry servers, multi-client setup - PR 9:
RPCComponent— all RPC server setup - PR 10:
PolygonComponent— wrap heimdall + bridge + polygon sync
Wave 4 — Registry, NodeBuilder, and Packaging
- PR 11: Component registry +
init()registration - PR 12:
components.cfgfile + code generator (build/gen/main.go→components_gen.go) - PR 13:
NodeBuilder— composition root querying registry by mode; L1 mode = identical behavior to today - PR 14: Slim
Ethereumstruct to thin shell overComponentDomain - PR 15:
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
Related: The event topology for Downloader ↔ Sentry ↔ Storage ↔ Orchestrator is specified in ethereum/design/erigon-archive/snapshot-flow.md in erigon-documents.
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)
Follows the CoreDNS model. A components.cfg file at the repo root lists which components to compile:
storage:github.com/erigontech/erigon/node/components/storage
p2p:github.com/erigontech/erigon/node/components/p2p
caplin:github.com/erigontech/erigon/node/components/caplin
rollup_based:github.com/erigontech/erigon/rollup/based
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
| File | Role |
|---|---|
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 | Config struct (52 fields + 14 Sync fields) |
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 |