# Lighthouse Threading Architecture > A comprehensive guide to the execution model, work flows, and shared state in the Lighthouse Ethereum consensus client. > > **Target audience:** Developers onboarding to the Lighthouse codebase. > **Source:** `beacon_node/`, `common/task_executor/`, `beacon_node/beacon_processor/` --- ## Table of Contents 1. [Tier Overview](#1-tier-overview) 2. [Tier 1 — Tokio Async Executor](#2-tier-1--tokio-async-executor) 3. [Tier 2 — Tokio Blocking Thread Pool](#3-tier-2--tokio-blocking-thread-pool) 4. [Tier 3 — Rayon Thread Pools](#4-tier-3--rayon-thread-pools) 5. [Tier Trigger Relationships](#5-tier-trigger-relationships) 6. [Flow: Gossip Block Import](#6-flow-gossip-block-import) 7. [Flow: Gossip Attestation (BLS Batching)](#7-flow-gossip-attestation-bls-batching) 8. [Flow: P2P RPC Request Serving](#8-flow-p2p-rpc-request-serving) 9. [Flow: RPC Block Import (Sync)](#9-flow-rpc-block-import-sync) 10. [Shared Objects Across Tiers](#10-shared-objects-across-tiers) 11. [Controllers & Orchestrators](#11-controllers--orchestrators) 12. [BeaconProcessor Queue Priority](#12-beaconprocessor-queue-priority) --- ## 1. Tier Overview Lighthouse uses three distinct execution tiers. No tier has its own dedicated OS thread — all tiers share a common Rust async runtime (`tokio`) and purpose-built thread pools. ```mermaid graph TB subgraph T1["⚡ Tier 1 — Tokio Async Executor (N OS threads, N ≈ CPU count)"] direction LR NET["🌐 network task\nlibp2p Swarm"] RTR["🔀 router task"] MGR["📋 BeaconProcessor\nmanager task"] SYN["🔄 sync manager"] API["🌐 HTTP API\naxum server"] WRK["⚙️ async workers\n(GossipBlock, RpcBlock,\nChainSegment, ...)"] NET --> RTR --> MGR MGR --> WRK end subgraph T2["🔒 Tier 2 — Tokio Blocking Pool (max = CPU count)"] direction LR BLS["🔐 BLS verification\n(attestations, aggregates)"] SLH["⚠️ slashings / exits"] SRV["📤 blob/column serving"] BCK["📦 backfill\n(rate limit OFF)"] end subgraph T3["🧵 Tier 3 — Rayon Pools"] direction LR LP["🐢 LowPriority\n~25% CPUs\nBackfill sync"] HP["🚀 HighPriority\n~80% CPUs\nColumn reconstruction"] end MGR -->|spawn_blocking| T2 MGR -->|spawn_blocking_with_rayon\nLowPriority| LP WRK -->|spawn_blocking_with_rayon_async\nHighPriority .await| HP classDef t1 fill:#87CEEB,stroke:#1a6691,stroke-width:2px,color:darkblue classDef t2 fill:#90EE90,stroke:#2d6a2d,stroke-width:2px,color:darkgreen classDef t3 fill:#E6E6FA,stroke:#5555aa,stroke-width:2px,color:#333366 class NET,RTR,MGR,SYN,API,WRK t1 class BLS,SLH,SRV,BCK t2 class LP,HP t3 ``` --- ## 2. Tier 1 — Tokio Async Executor Tier 1 is the tokio multi-threaded runtime. It runs `Future`s cooperatively — tasks yield at `.await` points, freeing the OS thread for other tasks. **Nothing here may block.** ### Permanent Tasks (always alive) | Task name | Struct | Owns | |---|---|---| | `"network"` | `NetworkService<T>` | libp2p `Swarm` — the only owner of TCP/QUIC sockets | | `"router"` | `Router<T>` | Routes `RouterMessage` from network to beacon processor / sync | | `"beacon_processor_manager"` | `BeaconProcessor<E>` | All 30+ work queues, worker slot counter | | `"sync_manager"` | `SyncManager` | Range sync, backfill sync state machines | | HTTP API | axum server | Serves `/eth/...` endpoints | | `"reprocess_scheduler"` | internal | Holds delayed work items until their slot arrives | ### Short-lived Async Workers (spawned per work item) Spawned by the BeaconProcessor manager via `task_spawner.spawn_async()`. Each consumes one worker slot. They run on the tokio executor (not a blocking thread) because they need to `.await` — e.g. waiting for the Execution Layer over the Engine API. | Work type | Spawned for | |---|---| | `GossipBlock` | Gossip block verification + import | | `GossipBlobSidecar` | Gossip blob sidecar | | `GossipDataColumnSidecar` | PeerDAS data column via gossip | | `RpcBlock` | Block received from sync peer | | `RpcBlobs` / `RpcCustodyColumn` | Blob/column from sync peer | | `ChainSegment` | Batch of blocks from range sync | | `DelayedImportBlock` | Block re-queued from reprocess scheduler | | `BlocksByRangeRequest` / `BlocksByRootsRequest` | Serving a peer's block request | | `ColumnReconstruction` | PeerDAS reconstruction trigger | ### Key source files - [`common/task_executor/src/lib.rs`](../common/task_executor/src/lib.rs) — `TaskExecutor`: `spawn()`, `spawn_blocking()`, `spawn_blocking_with_rayon()` - [`beacon_node/network/src/service.rs`](../beacon_node/network/src/service.rs) — `NetworkService::spawn_service()` - [`beacon_node/network/src/router.rs`](../beacon_node/network/src/router.rs) — `Router::spawn()` - [`beacon_node/beacon_processor/src/lib.rs`](../beacon_node/beacon_processor/src/lib.rs) — `BeaconProcessor::spawn_manager()` --- ## 3. Tier 2 — Tokio Blocking Thread Pool Tokio's blocking pool (`spawn_blocking`) provides OS threads for CPU-bound synchronous work that must not block the async executor. Capped at CPU count. **Triggered exclusively by:** Tier 1 BeaconProcessor manager via `task_spawner.spawn_blocking()`. ### What runs in Tier 2 | Work type | Queue type | Max queue size | |---|---|---| | `GossipAttestation` / `GossipAttestationBatch` | LIFO | `active_validators / slots_per_epoch` | | `GossipAggregate` / `GossipAggregateBatch` | LIFO | 4096 | | `GossipVoluntaryExit` | FIFO | 4096 | | `GossipProposerSlashing` / `GossipAttesterSlashing` | FIFO | 4096 | | `GossipSyncSignature` / `GossipSyncContribution` | LIFO | 2048 / 1024 | | `BlobsByRangeRequest` / `BlobsByRootsRequest` | FIFO | 1024 | | `DataColumnsByRootsRequest` / `DataColumnsByRangeRequest` | FIFO | 1024 | | `LightClientBootstrapRequest` + LC updates | FIFO | 512–1024 | | `ApiRequestP0` / `ApiRequestP1` (blocking variant) | FIFO | 1024 | | `ChainSegmentBackfill` (rate limiting **OFF**) | FIFO | 64 | | `Status` | FIFO | 1024 | ### LIFO vs FIFO queue choice - **LIFO** (attestations, aggregates, sync messages): freshest items are most valuable. When overloaded, process newest and drop stale. - **FIFO** (slashings, exits, blocks): ordered processing prevents censoring and preserves safety. ### Worker lifecycle (RAII pattern) ```mermaid flowchart LR MGR["📋 Manager\n(Tier 1)"] -->|spawn_blocking closure| OS["🔒 OS Thread\n(Tier 2)"] OS -->|runs closure| WORK["⚙️ CPU work\n(BLS verify, DB read...)"] WORK -->|completes or panics| DROP["💧 SendOnDrop\n(Rust Drop trait)"] DROP -->|fires idle_tx| MGR classDef tier1 fill:#87CEEB,stroke:#1a6691,stroke-width:2px,color:darkblue classDef tier2 fill:#90EE90,stroke:#2d6a2d,stroke-width:2px,color:darkgreen classDef raii fill:#FFD700,stroke:#997700,stroke-width:2px,color:black class MGR tier1 class OS,WORK tier2 class DROP raii ``` `SendOnDrop` is a RAII guard: it holds the `idle_tx` channel sender and sends a `WorkType` message when dropped — even on panic. This guarantees worker slots are always freed. --- ## 4. Tier 3 — Rayon Thread Pools Rayon provides data-parallel thread pools. Lighthouse creates **two named pools**, never uses the global rayon pool (which would cause CPU oversubscription). ```mermaid graph TB subgraph Provider["RayonPoolProvider (inside TaskExecutor)"] LP["🐢 LowPriority Pool\n~25% of CPUs\nmin 1 thread"] HP["🚀 HighPriority Pool\n~80% of CPUs\nmin 1 thread"] end MGR["📋 BeaconProcessor manager\n(Tier 1)"] -->|"spawn_blocking_with_rayon\n(LowPriority)\nwrapped inside Tier 2 slot"| LP WRK["⚙️ Async worker\n(Tier 1)"] -->|"spawn_blocking_with_rayon_async\n(HighPriority) .await\nbypasses Tier 2"| HP LP -->|"process_chain_segment_backfill()\nhistorical block import"| BCK["📦 Backfill Sync\n(rate limiting ON)"] HP -->|"data_availability_checker\n.reconstruct_data_columns()"| REC["🔬 PeerDAS Column\nReconstruction"] classDef t1 fill:#87CEEB,stroke:#1a6691,stroke-width:2px,color:darkblue classDef t3lo fill:#E6E6FA,stroke:#5555aa,stroke-width:2px,color:#333366 classDef t3hi fill:#FFD700,stroke:#997700,stroke-width:2px,color:black classDef work fill:#f0f0f0,stroke:#666,stroke-width:1px,color:#333 class MGR,WRK t1 class LP,BCK t3lo class HP,REC t3hi ``` | Pool | Trigger path | Consumes Tier 2 slot? | Used for | |---|---|---|---| | LowPriority (~25% CPUs) | Manager → `spawn_blocking_with_rayon()` | Yes (installs rayon inside a blocking task) | `ChainSegmentBackfill` when rate limiting ON | | HighPriority (~80% CPUs) | Async worker → `spawn_blocking_with_rayon_async().await` | No (direct rayon spawn + oneshot channel) | `reconstruct_data_columns()` (PeerDAS) | --- ## 5. Tier Trigger Relationships ```mermaid flowchart TD OS_KERNEL["🐧 OS Kernel\nepoll / kqueue"] TOKIO["⚡ Tokio Reactor\nasync I/O readiness"] T1["⚡ TIER 1\nTokio Async Executor"] T2["🔒 TIER 2\nTokio Blocking Pool"] T3LP["🐢 TIER 3 LowPriority\nRayon Pool ~25% CPUs"] T3HP["🚀 TIER 3 HighPriority\nRayon Pool ~80% CPUs"] OS_KERNEL -->|"socket/timer events"| TOKIO TOKIO -->|"wakes futures"| T1 T1 -->|"spawn_blocking(closure)\nBeaconProcessor manager only"| T2 T2 -->|"SendOnDrop fires idle_tx"| T1 T1 -->|"spawn_blocking_with_rayon\n(LowPriority)\nwraps inside Tier 2 slot"| T3LP T3LP -->|"SendOnDrop fires idle_tx"| T1 T1 -->|"spawn_blocking_with_rayon_async\n(HighPriority) .await\nbypasses Tier 2"| T3HP T3HP -->|"oneshot channel result"| T1 classDef kernel fill:#FF6B6B,stroke:#cc0000,stroke-width:2px,color:white classDef t1 fill:#87CEEB,stroke:#1a6691,stroke-width:2px,color:darkblue classDef t2 fill:#90EE90,stroke:#2d6a2d,stroke-width:2px,color:darkgreen classDef t3lo fill:#E6E6FA,stroke:#5555aa,stroke-width:2px,color:#333366 classDef t3hi fill:#FFD700,stroke:#997700,stroke-width:2px,color:black class OS_KERNEL kernel class TOKIO,T1 t1 class T2 t2 class T3LP t3lo class T3HP t3hi ``` **Rule:** Tier 1 is the only tier that can trigger other tiers. Tier 2 and Tier 3 never spawn further work — they only signal back to Tier 1 (via `idle_tx` or `oneshot`) when done. --- ## 6. Flow: Gossip Block Import A gossip block travels through all three major components before reaching the chain. ```mermaid sequenceDiagram participant P2P as 🌐 libp2p Swarm<br/>(Tier 1: network task) participant NS as 📡 NetworkService<br/>(Tier 1: network task) participant RT as 🔀 Router<br/>(Tier 1: router task) participant NBP as 🔗 NetworkBeaconProcessor<br/>(Tier 1) participant CH as 📋 BP Channel<br/>mpsc cap=16384 participant MGR as 📋 BP Manager<br/>(Tier 1) participant WRK as ⚙️ Async Worker<br/>(Tier 1: short-lived) participant EL as ⛏️ Execution Layer<br/>(engine_newPayload) participant BC as 🔗 BeaconChain<br/>(shared Arc) participant T2 as 🔒 Blocking Worker<br/>(Tier 2) P2P->>NS: NetworkEvent::PubsubMessage<br/>(BeaconBlock) NS->>RT: RouterMessage::PubsubMessage<br/>(unbounded channel) RT->>NBP: handle_gossip()<br/>→ send_gossip_beacon_block() NBP->>CH: Work::GossipBlock(AsyncFn)<br/>try_send WorkEvent alt Worker slot available (current_workers < max_workers) MGR->>WRK: task_spawner.spawn_async() else All slots busy MGR->>MGR: push to gossip_block_queue<br/>(FIFO, cap=1024) Note over MGR: Wait for idle_rx signal MGR->>WRK: spawn when slot freed end rect rgb(200, 230, 255) Note over WRK,BC: process_gossip_block() — fully on Tier 1 async task WRK->>BC: 1. GossipVerifiedBlock<br/>(seen cache, gossip rules — sync, inline) WRK->>BC: 2. SignatureVerifiedBlock<br/>(BLS sig check — sync, inline) Note over WRK: 3. into_execution_pending_block() — SYNC, Tier 1<br/>per_slot_processing() + per_block_processing()<br/>run inline on the async worker (no spawn_blocking) WRK->>EL: 4. engine_newPayload()<br/>.await response — async, Tier 1 EL-->>WRK: VALID / SYNCING end rect rgb(200, 255, 200) Note over WRK,T2: import_available_block() — crosses to Tier 2 WRK->>T2: spawn_blocking_handle(|| chain.import_block(...))<br/>(DB writes, fork choice update) T2-->>WRK: block_root (await result) WRK->>BC: recompute_head_at_current_slot().await<br/>(updates cached_head RwLock — Tier 1) end WRK->>MGR: SendOnDrop fires idle_tx<br/>(even on panic) MGR->>MGR: current_workers -= 1<br/>check queues ``` ### State Transition: Which Tier Does What The gossip block pipeline has a deliberate split across tiers: | Step | Function | Tier | Why | |---|---|---|---| | Gossip rules | `GossipVerifiedBlock::new()` | **Tier 1** (sync, inline) | Fast checks (seen cache, topic filter, slot range) | | BLS signature | `SignatureVerifiedBlock::from_gossip_verified_block_check_slashable()` | **Tier 1** (sync, inline) | Needed before state transition; cheap single sig verify | | **State transition** | `per_slot_processing()` + `per_block_processing()` | **Tier 1** (sync, inline) | Must complete before EL notification; worker is dedicated so blocking it is fine | | EL notification | `engine_newPayload()` via `ExecutionPendingBlock::into_executed_block()` | **Tier 1** (async `.await`) | I/O over HTTP — never blocks a thread | | DB write + fork choice | `import_block()` inside `import_available_block()` | **Tier 2** (`spawn_blocking_handle`) | CPU+IO-heavy; must not block the Tier 1 event loop | | Head update | `recompute_head_at_current_slot()` | **Tier 1** (async `.await`) | Acquires `recompute_head_lock` Mutex then updates `cached_head` RwLock | **Key insight:** `per_slot_processing` and `per_block_processing` run **inline on the Tier 1 async worker** because: 1. `into_execution_pending_block_slashable()` is a plain synchronous function (not async), called directly from the async context. 2. The GossipBlock async worker is a **dedicated task** — blocking it synchronously does not starve other concurrent work. 3. State transition must complete before `engine_newPayload` can be sent (the EL needs the block body). 4. DB writes and fork choice update (`import_block`) are the truly expensive part and are explicitly offloaded to Tier 2. **Call chain (simplified):** ``` process_gossip_block() ← async, Tier 1 (gossip_methods.rs) └─ process_gossip_verified_block() ← async, Tier 1 └─ chain.process_block() ← async, Tier 1 (beacon_chain.rs) ├─ unverified_block.into_execution_pending_block() ← SYNC, Tier 1 │ └─ per_slot_processing() + per_block_processing() ← SYNC, Tier 1 ├─ chain.into_executed_block().await ← async Tier 1 (awaits EL oneshot) └─ self.import_available_block().await ← async, crosses to Tier 2 └─ spawn_blocking_handle(|| chain.import_block(...)) ← Tier 2 ``` **Why async (not blocking) for the outer task?** Block import must `.await` the Execution Layer's `engine_newPayload` response over HTTP. Blocking a Tier 2 OS thread on network I/O would waste the thread pool. The worker is therefore async (Tier 1), and only the CPU+DB-heavy `import_block` is explicitly moved to Tier 2. --- ## 7. Flow: Gossip Attestation (BLS Batching) Attestations are the highest-volume message type. The BeaconProcessor batches them to amortize BLS verification cost. ```mermaid sequenceDiagram participant P2P as 🌐 libp2p Swarm<br/>(Tier 1) participant RT as 🔀 Router<br/>(Tier 1) participant CH as 📋 BP Channel participant MGR as 📋 BP Manager<br/>(Tier 1) participant WRK as 🔒 Blocking Worker<br/>(Tier 2) participant BC as 🔗 BeaconChain<br/>(shared Arc) P2P->>RT: PubsubMessage::Attestation RT->>CH: Work::GossipAttestation<br/>try_send WorkEvent alt Worker slot free MGR->>MGR: spawn immediately else Slot busy MGR->>MGR: push to attestation_queue<br/>(LIFO — freshest first) end Note over MGR: When slot becomes free, check queue depth alt Only 1 attestation queued MGR->>WRK: spawn_blocking(process_individual) else ≥2 attestations queued (up to 64) MGR->>MGR: Collect batch of min(queue_len, 64)<br/>into Work::GossipAttestationBatch MGR->>WRK: spawn_blocking(process_batch) end rect rgb(200, 255, 200) Note over WRK,BC: process_gossip_attestation_batch() — Tier 2 blocking thread WRK->>BC: Phase 1: Index all attestations<br/>(subnet, slot, validator lookup) WRK->>BC: Phase 2: Read validator_pubkey_cache<br/>(RwLock) → build SignatureSets WRK->>WRK: Phase 3: verify_signature_sets()<br/>ONE batch blst C call (pure CPU) alt Batch passes WRK->>WRK: CheckAttestationSignature::No<br/>(skip per-sig verify) else Batch fails (one bad sig poisons all) WRK->>WRK: Fallback: verify each sig individually end WRK->>BC: Phase 4 (per attestation):<br/>apply_attestation_to_fork_choice() WRK->>BC: add_to_naive_aggregation_pool() WRK->>RT: propagate_attestation_if_timely()<br/>(network_tx → ValidationResult) end WRK->>MGR: SendOnDrop fires idle_tx ``` **Key insight:** Processing 64 attestations in one batch BLS call is significantly faster than 64 individual calls. The LIFO queue ensures the freshest attestations are processed first when overloaded — older ones are dropped from the back. --- ## 8. Flow: P2P RPC Request Serving When a peer requests blocks/blobs/columns via the Req/Resp protocol. ```mermaid sequenceDiagram participant PEER as 🤝 Remote Peer participant P2P as 🌐 libp2p Swarm<br/>(Tier 1) participant NS as 📡 NetworkService<br/>(Tier 1) participant RT as 🔀 Router<br/>(Tier 1) participant MGR as 📋 BP Manager<br/>(Tier 1) participant WRK as ⚙️ Worker<br/>(Tier 1 or 2) participant BC as 🔗 BeaconChain / DB<br/>(shared Arc) PEER->>P2P: RPC Request<br/>(BlocksByRange / BlobsByRoot / etc.) P2P->>NS: NetworkEvent::RequestReceived NS->>RT: RouterMessage::RPCRequestReceived<br/>(unbounded channel) RT->>MGR: send_blocks_by_range_request()<br/>→ Work::BlocksByRangeRequest(AsyncFn)<br/>or Work::BlobsByRangeRequest(BlockingFn) alt Async request (BlocksByRange, BlocksByRoot) MGR->>WRK: task_spawner.spawn_async()<br/>runs on Tier 1 else Blocking request (BlobsByRange, BlobsByRoot, DataColumns) MGR->>WRK: task_spawner.spawn_blocking()<br/>runs on Tier 2 end WRK->>BC: Read blocks/blobs from HotColdDB BC-->>WRK: data loop Stream response chunks WRK->>NS: network_tx: NetworkMessage::SendResponse<br/>(unbounded channel) NS->>P2P: libp2p send RPC response chunk P2P->>PEER: response chunk end WRK->>MGR: SendOnDrop fires idle_tx ``` --- ## 9. Flow: RPC Block Import (Sync) When the Sync manager downloads blocks from peers and imports them into the chain. ```mermaid sequenceDiagram participant PEER as 🤝 Remote Peer participant P2P as 🌐 libp2p Swarm<br/>(Tier 1) participant SYN as 🔄 Sync Manager<br/>(Tier 1) participant NBP as 🔗 NetworkBeaconProcessor participant MGR as 📋 BP Manager<br/>(Tier 1) participant WRK as ⚙️ Async Worker<br/>(Tier 1) participant BC as 🔗 BeaconChain<br/>(shared Arc) PEER->>P2P: RPC Response (blocks) P2P->>SYN: RouterMessage::RPCResponseReceived<br/>→ SyncMessage SYN->>SYN: Update sync state machine<br/>(range sync / backfill) SYN->>NBP: send_chain_segment() or<br/>send_rpc_beacon_block() NBP->>MGR: Work::ChainSegment(AsyncFn)<br/>or Work::RpcBlock(AsyncFn)<br/>[HIGH PRIORITY queue, FIFO] Note over MGR: Priority: chain_segment > rpc_block ><br/>gossip_block > attestations MGR->>WRK: task_spawner.spawn_async() rect rgb(200, 230, 255) Note over WRK,BC: Tier 1 async block import WRK->>BC: verify_block_for_import() WRK->>BC: import_block() → state_transition() BC->>BC: recompute_head() WRK->>SYN: sync_tx: SyncMessage::BlockProcessed<br/>(advance sync state machine) end WRK->>MGR: SendOnDrop fires idle_tx ``` --- ## 10. Shared Objects Across Tiers All shared objects are `Arc<T>` — reference-counted heap allocations. Each tier holds a clone of the `Arc` pointer, all pointing to the same allocation. Internal mutability is provided by `RwLock` or `Mutex`. ```mermaid graph TB subgraph SharedState["🔒 Shared State (Arc-wrapped)"] BC["🔗 Arc&lt;BeaconChain&lt;T&gt;&gt; ───────────────────────────── .canonical_head: CanonicalHead ├─ fork_choice: RwLock&lt;BeaconForkChoice&gt; ├─ cached_head: RwLock&lt;CachedHead&gt; └─ recompute_head_lock: Mutex&lt;()&gt; .store: Arc&lt;HotColdDB&gt; .op_pool: OperationPool .naive_aggregation_pool: RwLock .observed_attestations: RwLock .validator_pubkey_cache: RwLock .execution_layer: ExecutionLayer .task_executor: TaskExecutor"] NG["🌐 Arc&lt;NetworkGlobals&lt;E&gt;&gt; ───────────────────────────── .peers: RwLock&lt;PeerDB&gt; .sync_state: RwLock&lt;SyncState&gt; .backfill_state: RwLock .local_enr: RwLock .gossipsub_subscriptions: RwLock .config: Arc&lt;NetworkConfig&gt; .spec: Arc&lt;ChainSpec&gt;"] NBP["🔗 Arc&lt;NetworkBeaconProcessor&lt;T&gt;&gt; ───────────────────────────── .beacon_processor_send: mpsc::Sender .duplicate_cache: Arc&lt;Mutex&lt;HashSet&gt;&gt; .chain: Arc&lt;BeaconChain&gt; .network_tx: mpsc::UnboundedSender .sync_tx: mpsc::UnboundedSender .network_globals: Arc&lt;NetworkGlobals&gt; .executor: TaskExecutor"] DB["💾 Arc&lt;HotColdDB&gt; ───────────────────────────── Hot store (recent blocks/states) Cold store (finalized history)"] TE["⚙️ TaskExecutor (Clone — wraps Weak&lt;Runtime&gt;) ───────────────────────────── .spawn() → Tier 1 .spawn_blocking() → Tier 2 .spawn_blocking_with_rayon() → Tier 3"] end subgraph Tiers["Execution Tiers"] T1NET["🌐 network task"] T1RTR["🔀 router task"] T1MGR["📋 BP manager"] T1SYN["🔄 sync manager"] T1API["🌐 HTTP API"] T1WRK["⚙️ async workers"] T2WRK["🔒 blocking workers"] end T1NET --> BC T1NET --> NG T1RTR --> BC T1RTR --> NBP T1MGR --> NG T1SYN --> NBP T1API --> BC T1API --> NG T1WRK --> BC T2WRK --> BC BC --> DB classDef shared fill:#FFD700,stroke:#997700,stroke-width:2px,color:black classDef tier1 fill:#87CEEB,stroke:#1a6691,stroke-width:2px,color:darkblue classDef tier2 fill:#90EE90,stroke:#2d6a2d,stroke-width:2px,color:darkgreen class BC,NG,NBP,DB,TE shared class T1NET,T1RTR,T1MGR,T1SYN,T1API,T1WRK tier1 class T2WRK tier2 ``` ### Channels (tier boundaries) | Channel | Type | Cap | Sender(s) | Receiver | Purpose | |---|---|---|---|---|---| | `beacon_processor_send` | `mpsc` | 16 384 | Router, Sync | BP Manager | All work enters BeaconProcessor | | `idle_tx` / `idle_rx` | `mpsc` | 16 384 | Every Tier 2/3 worker (SendOnDrop) | BP Manager | Worker completion signal | | `network_tx` | `mpsc unbounded` | ∞ | Workers, Router | NetworkService | Send responses / validations to libp2p | | `router_send` | `mpsc unbounded` | ∞ | NetworkService | Router | Network events → Router | | `sync_tx` | `mpsc unbounded` | ∞ | Router, workers | Sync Manager | Sync state machine messages | | `ready_work_rx` | `mpsc` | 12 288 | Reprocess scheduler | BP Manager | Delayed blocks now ready | --- ## 11. Controllers & Orchestrators ```mermaid graph TB subgraph Controllers["🎛️ Controllers (who drives what)"] direction TB BP["📋 BeaconProcessor Manager ══════════════════════════════ THE central work dispatcher • Owns 30+ priority queues • Manages worker slot count (max=CPUs) • Polls: idle_rx → ready_work_rx → event_rx • Forms attestation batches (up to 64) • Drops work flagged drop_during_sync • Never blocks — pure async event loop"] TE["⚙️ TaskExecutor ══════════════════════════════ Controls HOW work is spawned • .spawn() → Tier 1 async • .spawn_blocking() → Tier 2 OS thread • .spawn_blocking_with_rayon() → Tier 3 LowPriority • .spawn_blocking_with_rayon_async() → Tier 3 HighPriority"] NS["🌐 NetworkService ══════════════════════════════ Controls all libp2p I/O • Owns the Swarm (sole owner) • tokio::select! event loop • Routes events to Router • Sends gossip validation results"] CH["🔗 CanonicalHead ══════════════════════════════ Controls chain state reads/writes • fork_choice RwLock (heavy writes) • cached_head RwLock (fast reads) • recompute_head Mutex (serializes updates) • Two locks by design: fast path for networking (cached_head) vs slow path for fork choice computation"] SYN["🔄 Sync Manager ══════════════════════════════ Controls what to download • Range sync (forward) • Backfill sync (historical) • Drives block/blob requests to peers • Advances state machines on import result"] BCI["🔗 BeaconChain ══════════════════════════════ THE shared state hub (not a controller) • Every meaningful operation routes through it • Owns: canonical_head, store, op_pool, execution_layer, task_executor • Accessed concurrently by all tiers via Arc"] end BP -->|uses| TE BP -->|reads sync_state from| NG["Arc&lt;NetworkGlobals&gt;"] NS -->|feeds events to| RT["Router"] RT -->|sends work via| BP SYN -->|sends work via| BP CH -->|field of| BCI classDef controller fill:#FF6B6B,stroke:#cc0000,stroke-width:2px,color:white classDef state fill:#FFD700,stroke:#997700,stroke-width:2px,color:black class BP,TE,NS,SYN,CH controller class BCI state ``` ### Decision: who controls what | Question | Answer | |---|---| | What work runs next? | **BeaconProcessor manager** — priority queue + worker slot count | | Which OS thread runs work? | **TaskExecutor** — `spawn` / `spawn_blocking` / `spawn_blocking_with_rayon` | | What is the canonical head? | **CanonicalHead** — `cached_head` RwLock (fast) or `fork_choice` RwLock (authoritative) | | What blocks to download from peers? | **Sync Manager** — range/backfill state machines | | What comes in from the network? | **NetworkService** — sole owner of libp2p Swarm | | Where does consensus state live? | **BeaconChain** — shared Arc, accessed from all tiers | --- ## 12. BeaconProcessor Queue Priority When a worker slot becomes free, the manager drains queues in this strict order: ```mermaid flowchart TD FREE["⚡ Worker slot free\n(idle_rx received)"] --> Q1 Q1{"chain_segment_queue\n(FIFO, cap=64)"} Q1 -->|item| SPAWN["⚙️ spawn_worker()"] Q1 -->|empty| Q2 Q2{"rpc_block / rpc_blob /\nrpc_custody_column\n(FIFO)"} Q2 -->|item| SPAWN Q2 -->|empty| Q3 Q3{"delayed_block_queue\n(FIFO, cap=1024)"} Q3 -->|item| SPAWN Q3 -->|empty| Q4 Q4{"gossip_block / execution_payload /\ngossip_blob / data_column\n(FIFO)"} Q4 -->|item| SPAWN Q4 -->|empty| Q5 Q5{"api_request_p0\n(FIFO, high-priority API)"} Q5 -->|item| SPAWN Q5 -->|empty| Q6 Q6{"aggregate_queue ≥ 2?\n(LIFO → batch up to 64)"} Q6 -->|yes| BATCH_AGG["Collect GossipAggregateBatch"] Q6 -->|no / 1| SPAWN BATCH_AGG --> SPAWN Q6 -->|empty| Q7 Q7{"attestation_queue ≥ 2?\n(LIFO → batch up to 64)"} Q7 -->|yes| BATCH_ATT["Collect GossipAttestationBatch"] Q7 -->|no / 1| SPAWN BATCH_ATT --> SPAWN Q7 -->|empty| Q8{"sync_contribution /\nsync_message\n(LIFO)"} Q8 -->|item| SPAWN Q8 -->|empty| Q9{"unknown_block_att /\nunknown_block_agg\n(LIFO)"} Q9 -->|item| SPAWN Q9 -->|empty| Q10{"status / blocks_by_range /\nblobs_by_range / ...\n(FIFO)"} Q10 -->|item| SPAWN Q10 -->|empty| Q11{"slashings / exits /\nbls_execution_change\n(FIFO)"} Q11 -->|item| SPAWN Q11 -->|empty| Q12{"api_request_p1\n(FIFO, low-priority API)"} Q12 -->|item| SPAWN Q12 -->|empty| Q13{"backfill_chain_segment\n(FIFO, lowest priority)"} Q13 -->|item| SPAWN Q13 -->|empty| Q14{"light_client_*\n(FIFO)"} Q14 -->|item| SPAWN Q14 -->|empty| NOTHING["NOTHING_TO_DO\n(no work available)"] classDef decision fill:#FFD700,stroke:#997700,stroke-width:2px,color:black classDef spawn fill:#90EE90,stroke:#2d6a2d,stroke-width:2px,color:darkgreen classDef batch fill:#87CEEB,stroke:#1a6691,stroke-width:2px,color:darkblue classDef nothing fill:#FFB6C1,stroke:#DC143C,stroke-width:2px,color:black class Q1,Q2,Q3,Q4,Q5,Q6,Q7,Q8,Q9,Q10,Q11,Q12,Q13,Q14 decision class SPAWN,FREE spawn class BATCH_AGG,BATCH_ATT batch class NOTHING nothing ``` ### Priority rationale | Priority group | Reason | |---|---| | Chain segments | Most efficient path to advancing the head — batch of blocks already downloaded | | RPC blocks/blobs | Already explicitly requested from peers, peer is waiting | | Delayed blocks | May be parent of pending gossip blocks | | Gossip blocks | Must be imported before attestations referencing them can be verified | | API P0 | High-priority client requests (e.g. validator block production) | | Aggregates → Attestations | Validator rewards; aggregates more efficient (more info, same verification time) | | Sync committee | Rewards but no fork choice influence | | Unknown block att/agg | Older items held for retry — less fresh | | Status / range requests | Serving peers (important but not chain-critical) | | Slashings / exits | Safety-important but low urgency | | API P1 | Low-priority background API requests | | Backfill | Historical data, zero urgency for current consensus | | Light client | Minimal consensus impact | --- ## Key Design Principles 1. **No blocking on Tier 1 — with one deliberate exception.** CPU work is generally offloaded to Tier 2 (`spawn_blocking`). However, state transition (`per_slot_processing` + `per_block_processing`) runs inline on the Tier 1 async worker for gossip blocks. This is safe because each GossipBlock worker is a dedicated task and the state transition must complete synchronously before the EL can be notified. The expensive part — `import_block()` (DB writes + fork choice) — is still explicitly moved to Tier 2. 2. **Single manager, many workers.** One BeaconProcessor manager event loop serializes queue decisions; N workers run in parallel. This avoids lock contention on queue state. 3. **RAII worker slots.** `SendOnDrop` guarantees the idle signal fires even on panic — worker slots are never leaked. 4. **Two `CanonicalHead` locks by design.** `cached_head` (fast RwLock) serves the networking stack without blocking. `fork_choice` (heavy RwLock) is only taken for actual fork choice computation. 5. **LIFO for attestations.** When overloaded, freshest attestations have the most value for block production. Stale attestations are silently dropped from the back of the LIFO queue. 6. **Scoped rayon pools, never global.** The global rayon pool interacts badly with tokio thread counts. Lighthouse creates its own `LowPriority` and `HighPriority` pools sized to leave headroom for the async executor. 7. **Channels are tier boundaries.** `beacon_processor_send`, `idle_tx`, `network_tx`, `sync_tx` are the only crossing points between components. Shared `Arc<T>` provides concurrent data access within a tier.