Lighthouse
From 200+ methods to zero. 7,317 lines to 125.
Our leanest BeaconChain, by far.*
*The top-level BeaconChain<T> struct went from 7,317 to 125 lines.
Total crate line count increased by ~5,800 lines (51.8k → 57.6k) — the god object's logic now lives in focused, independently testable components.
No methods were harmed in the making of this refactor. Several were rehomed.
One struct. 7,317 lines. 40+ fields. 200+ methods. Everyone gets Arc<BeaconChain> and can reach everything — which sounds convenient until you try to test, review, or safely change any of it.
No unit-testable components. Every change requires understanding 7,000 lines to assess impact.
No type-enforced boundaries. Reviewers trace the god object to understand what a change actually touches.
Attestations, block production, fork choice all share one type and its lock scopes. Isolated testing is impossible.
Everyone gets access to everything
Focused components own their state. Callers hold typed refs, no logic. Testable in isolation.
See the Structure diagram for the full component map.
Not a workflow prescription. A foundation for new ones.
Scope a task to one component. No need to understand the rest of the chain.
Most components: construct directly, no harness, no store, no fork choice. Fast tests that validate before integration. Orchestrators still use the harness.
Smaller components mean finding things is faster and scope is clearer. Once spec-aligned test cases are human-approved, implementation details can be validated by tests rather than line-by-line review.
Refactor internals without breaking unrelated subsystems. Type-enforced boundaries replace implicit ones.
Same name. Zero methods. All logic lives in components and orchestrators.
7,317 lines to 125. 200+ methods to zero. 7 components + 2 orchestrators, each unit-testable.
unstable. Detailed benchmarking on dedicated hardware pending — local testnet measurements show no clear regression.
Same test suite (ef_tests + fork_from_env, Fulu fork), same cargo llvm-cov. One blob of coverage becomes per-component visibility.
| Module | Functions | Lines | Coverage |
|---|---|---|---|
| unstable — beacon_chain.rs | 262/363 | 3,458/4,273 |
80.9%
|
| Branch — broken down by component | |||
| AttestationManager | 24/25 | 416/461 |
90.2%
|
| OperationsManager | 13/14 | 126/136 |
92.7%
|
| SyncCommitteeManager | 8/8 | 100/112 |
89.3%
|
| BlockProducer | 47/62 | 999/1,136 |
88.0%
|
| ExecutionManager | 11/12 | 69/81 |
85.2%
|
| CanonicalHead | 73/79 | 749/896 |
83.6%
|
| BlockImporter | 69/97 | 1,006/1,208 |
83.3%
|
| DataAvailabilityMgr | 25/31 | 214/261 |
82.0%
|
| StateQuery (utility module, not a component) | 60/67 | 619/675 |
91.7%
|
| beacon_chain.rs (after) | 4/5 | 12/13 |
92.3%
|
NetworkBeaconProcessor holds direct component refs for most accesses but retains Arc<BeaconChain<T>> for ~25 external function calls. HTTP API and sync callers are next.
Born from specific BeaconChain<T> pain points.
Pass what you need, not what has what you need.
produce_block_on_state uses 4 domain deps out of 40+ fields. See the block production example.Components own verification logic. Infrastructure (chain state, slot, events) comes from the caller.
CanonicalHead and SlotClock internally. Coupling that makes isolated testing impossible.Construct, pass state, assert. Most components need no harness, no store, no fork choice. Orchestrators (BlockImporter, BlockProducer) still use the harness for integration-level validation due to cross-component dependencies.
Only block import, head recomputation, attestation application, and EL callbacks acquire write locks.
canonical_head.rs lock ordering is where deadlocks happen. Today any Arc<BeaconChain> holder can reach it. Now that boundary is enforced.Block production as a worked example. Component reference below.
Looks like it needs the whole BeaconChain. It doesn't.
&OperationPool
Pull attestations, exits, slashings, sync aggregate, BLS changes
CanonicalHead
Head slot, finalized checkpoint, forkchoice params (read-only)
ExecutionLayer
Get payload from EL, fee recipient, gas limit
AttestationManager
Early attester cache, attestation packing
Infrastructure
&ChainSpec
Spec constants (everything needs this)
BlockProductionConfig
~5 flags: paranoid mode, size limits, builder fallback
SlotClock
Current slot
TaskExecutor
Spawn blocking work
4 domain deps vs 40+ fields. Infrastructure deps (spec, clock, executor) are Rust making implicit globals explicit.
/// Owns the subsystems required to produce beacon blocks. /// Constructed once by the builder; methods use &Arc<Self>. pub struct BlockProducer<T: BeaconChainTypes> { spec: Arc<ChainSpec>, store: BeaconStore<T>, config: Arc<ChainConfig>, op_pool: Arc<OperationPool<T::EthSpec>>, canonical_head: Arc<CanonicalHead<T>>, execution_manager: Arc<ExecutionManager<T>>, attestation_manager: Arc<AttestationManager<T::EthSpec>>, // ... (20 fields total — key domain deps shown) } impl<T: BeaconChainTypes> BlockProducer<T> { pub async fn produce_block_on_state( self: &Arc<Self>, state: BeaconState<T::EthSpec>, produce_at_slot: Slot, randao_reveal: Signature, // ... ) -> Result<BeaconBlockResponseWrapper<T::EthSpec>> { // All deps accessed via self.* -- no god object needed let attestations = self.op_pool.get_attestations(&state, &self.spec)?; let health = is_healthy(&self.canonical_head, /* ... */)?; // ... } }
Owned state and shared references for each component.
Voluntary exits, slashings, BLS changes. Verification, dedup, pool insertion.
Attestation production, verification, aggregation, pool management.
Sync committee message/contribution verification, aggregation pool.
Blob/data column processing, custody, DA boundary calculations.
Execution layer integration, proposer preparation, forkchoice updates.
Validator pubkey lookups, committee cache access.
Fork choice, cached head block/state, head recomputation lock, fork choice persistence.
Orchestrator for block, blob, and data-column import. Owns import caches and observation tracking.
Orchestrator for block production: state loading, partial block assembly, execution payload integration.
Target state. NetworkBeaconProcessor has been migrated to hold direct component refs. HTTP API and sync still hold Arc<BeaconChain<T>> — migration is incremental.
The following shows what each caller actually needs, not what it currently holds.
Gossip handlers, sync
/eth/v1/beacon/pool/*
/eth/v1/validator/*