Skip to content

Commit 405a258

Browse files
authored
feat(blockchain): add chain-event bus (head/block/justified/finalized) (#516)
Adds a chain-event pub-sub mechanism to the blockchain actor: a `ChainEvent` enum (`head`, `block`, `justified_checkpoint`, `finalized_checkpoint`) published through an `EventBus` facade over a bounded tokio broadcast channel. The actor is the sole publisher; subscribers attach their own receivers, and a slow consumer skips missed events instead of back-pressuring the actor. Near-zero cost with no subscribers (`receiver_count` guard). No HTTP surface in this PR: the RPC consumer arrives in the next PR of the series. Emission is actor-layer only, not threaded through the store. Before each state-changing store call (`store::on_tick`, `store::on_block`) `BlockChainServer` snapshots `(head, latest_justified, latest_finalized)`, runs the store function completely unchanged, then diffs the snapshot against the post-call state and emits in order `block` → `head` → `justified_checkpoint` → `finalized_checkpoint`. `store.rs`, both spec-test files, and `test_driver.rs` stay byte-identical to `main`: no `Option<&EventBus>` parameter spreads across `on_tick`/`on_block`/`update_head` or any of their test call sites, and the sole-publisher property is structural (only `BlockChainServer` ever holds a live `EventBus`) rather than something every call site has to preserve by convention. Two consequences of diffing at this level, both accepted: - `block` needs a was-it-new guard: `on_block` returns `Ok` early for an already-imported root, so the actor checks block-known-ness (the same `has_state` check the store itself uses) before importing, and only emits `block` for a genuinely new root. - Several head moves inside a single tick or block import coalesce into one `head` event carrying the net move; fine for subscribers, since the beacon-API analog is also last-value-wins per slot. The `head` event is additionally recency-gated: it only fires when the new head is within `HEAD_EVENT_RECENCY_SLOTS` (32) of the wall-clock slot, so catch-up/backfill never spams head events (adopted from Lighthouse's head-SSE recency filter); the other events stay ungated. Payload serialization is untagged, so the JSON body stays flat and topic names live only on the transport's `event:` line. Supersedes #460, re-cut as a stacked series for easier review: 1. this PR: the event mechanism 2. `GET /lean/v0/events` SSE endpoint (all events) 3. `?topics=` server-side filtering 4. `chain_reorg` + `safe_target` events 5. high-rate `attestation`/`aggregate` events Has unit tests and passes clippy with `-D warnings`.
1 parent d0e549d commit 405a258

5 files changed

Lines changed: 560 additions & 17 deletions

File tree

Cargo.lock

Lines changed: 0 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

bin/ethlambda/src/main.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ use serde::Deserialize;
5252
use tracing::{error, info, warn};
5353
use tracing_subscriber::{EnvFilter, Layer, Registry, layer::SubscriberExt};
5454

55-
use ethlambda_blockchain::{BlockChain, BlockChainConfig, SyncStatusController};
55+
use ethlambda_blockchain::{BlockChain, BlockChainConfig, EventBus, SyncStatusController};
5656
use ethlambda_rpc::RpcConfig;
5757
use ethlambda_storage::{
5858
MAX_RESUMABLE_DB_STATE_AGE, StorageBackend, Store, backend::RocksDBBackend,
@@ -235,6 +235,12 @@ async fn main() -> eyre::Result<()> {
235235
// metric's startup value.
236236
let sync_status = SyncStatusController::default();
237237

238+
// Chain-event bus: the blockchain actor is the sole publisher. No consumer
239+
// subscribes yet — the RPC SSE endpoint (follow-up PR) will attach here;
240+
// until then the receiver-count guard in `emit` makes every emission a
241+
// no-op.
242+
let events = EventBus::default();
243+
238244
let blockchain_config = BlockChainConfig {
239245
aggregator: aggregator.clone(),
240246
sync_status_controller: sync_status.clone(),
@@ -247,7 +253,7 @@ async fn main() -> eyre::Result<()> {
247253
},
248254
};
249255

250-
let blockchain = BlockChain::spawn(store.clone(), validator_keys, blockchain_config);
256+
let blockchain = BlockChain::spawn(store.clone(), validator_keys, blockchain_config, events);
251257

252258
let built = build_swarm(SwarmConfig {
253259
node_key: node_p2p_key,

crates/blockchain/Cargo.toml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,6 @@ tracing.workspace = true
3434
hex.workspace = true
3535

3636
[dev-dependencies]
37-
serde = { workspace = true }
38-
serde_json = { workspace = true }
3937
hex = { workspace = true }
4038
libssz.workspace = true
4139
libssz-types.workspace = true

0 commit comments

Comments
 (0)