Skip to main content

Component Framework

note

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 / WaveDescriptionStatus
Part 1: CLI Flag DecouplingBuildEthConfig() consolidated entry point; config snapshot tests; global state eliminatedComplete
Wave 1: FoundationCLI boundary enforced; no behavioral changeComplete
Wave 2: Framework + Core ExtractionFramework rebase; Storage, Sync, Execution componentsNot started
Wave 3: Remaining Component ExtractionsDownloader, TxPool, P2P, RPC, PolygonPlanned
Wave 4: Registry, NodeBuilder, PackagingComponent registry, components.cfg, NodeBuilder, slim EthereumPlanned
Wave 5: L2 InfrastructureHierarchical domains for combined L1+L2 modeDeferred
Wave 6: Rollup DriversBased, Optimistic, Consensus, ZK driversDeferred

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.

PartDescriptionStatus
Part 2: Component Extraction12 components from Ethereum struct (59 fields)In progress (Waves 2–4)
Part 3: NodeBuilderComposition root at node/nodebuilder/Planned (Wave 4)
Part 4: RollupDriver / L2 PipelineDriver interface, L2DefaultStages factoryDeferred (Wave 5)
Part 5: RPC Chain-ID RoutingOnly needed for combined L1+L2 modeDeferred (Wave 5)
Part 6: Build-Time Compositioncomponents.cfg + go generate + init() registrationPlanned (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:

PhaseWhat happens
ConfigureApply typed app.Option values from ethconfig.Config; no I/O
InitializeOpen resources — databases, connections, snapshot files; resolve dependencies
RecoverRestore state after unclean shutdown — replay uncommitted WAL, verify consistency
ActivateStart goroutines, subscribe to events, begin serving requests
DeactivateFlush state, close connections, stop goroutines

Component Map

The Ethereum struct's ~80 fields decompose into these components:

ComponentKey provider fieldsDependencies
StoragechainDB, blockSnapshots, blockReader, blockWriternone (activates first)
P2PsentryServers, sentriesClient, statusDataProviderStorage
Consensusengine, forkValidatorStorage
Downloaderdownloader, downloaderClientStorage
SyncstagedSync, pipelineStagedSync, stage setsStorage, P2P, Consensus
ExecutionexecModuleSync, Consensus
TxPooltxPool, txPoolGrpcServerStorage, P2P
RPCethBackendRPC, engineBackendRPC, apiListStorage, Execution, TxPool
MinerpendingBlocks, minedBlocksExecution, TxPool
PolygonpolygonSyncService, polygonBridge, heimdallServiceStorage, P2P, Consensus
CaplinsentinelStorage
Notificationscross-cutting event busnone

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 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 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 from app_components branch 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: TxPoolComponenttxpool.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.cfg file + code generator (build/gen/main.gocomponents_gen.go)
  • PR 13: NodeBuilder — composition root querying registry by mode; L1 mode = identical behavior to today
  • PR 14: Slim Ethereum struct to thin shell over ComponentDomain
  • PR 15: 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

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

FileRole
node/eth/backend.goGod object to decompose (1715 lines)
cmd/utils/flags.go170+ flag reads → consolidate
node/cli/flags.go69+ flag reads → merge
node/ethconfig/config.goConfig struct (52 fields + 14 Sync fields)
erigon-lib/app/component/component.goComponent framework (from app_components branch)
erigon-lib/app/component/componentdomain.goDomain lifecycle management
erigon-lib/app/event/eventbus.goEvent bus
execution/stagedsync/default_stages.goAdd L2DefaultStages factory
polygon/sync/service.goReference: good component composition pattern
node/direct/execution_client.goReference: direct adapter pattern