Component Framework

Breaking the Ethereum god object into composable components with a lifecycle framework

circle-info

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 / Wave
Description
Status

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
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.

chevron-rightImplementation plan for contributorshashtag

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 single BuildEthConfig(ctx) consolidated entry point.

  • Config snapshot tests added to enforce the boundary.

  • Global state eliminated; grep for cli.Context below cmd/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.Context boundary (done — Part 1)

  • PR 2: Add RollupConfig to ethconfig.Config, --rollup.mode flag (no-op default l1) (done)

  • PR 3: Rebase erigon-lib/app/ framework from app_components branch onto main (done — component framework live in erigon-lib/app/)

Wave 2 — Extract Storage, P2P, Downloader

  • PR 4: StorageComponent — DB setup, block reader/writer, snapshots

  • PR 5: DownloaderComponent — snapshot torrent client

  • PR 6: TxPoolComponenttxpool.Assemble + Run

  • PR 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 setup

  • PR 11: PolygonComponent — wrap heimdall + bridge + polygon sync

Wave 4 — Extract TxPool, RPC, Miner + Registry, NodeBuilder, packaging

  • PR 12: Component registry + init() registration

  • PR 13: components.cfg file + code generator (build/gen/main.gocomponents_gen.go)

  • PR 14: NodeBuilder — composition root querying registry by mode; L1 mode = identical behavior to today

  • PR 15: Slim Ethereum struct to thin shell over ComponentDomain

  • PR 16: shards.Notificationsapp/event/ServiceBus migration

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.Driver interface, BorDriver, L1DataSource, NodeBuilder L2 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)

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

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

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