Skip to content

Commit c20b72c

Browse files
committed
feat(rpc): require the events ?topics= query parameter
Match the Beacon API eventstream, which makes `topics` mandatory: a missing or empty parameter now returns 400 instead of defaulting to all topics. This removes the divergence the endpoint previously documented. Also drop the now-unused TopicSet::ALL sentinel.
1 parent 55d0b77 commit c20b72c

3 files changed

Lines changed: 33 additions & 23 deletions

File tree

crates/blockchain/src/events.rs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -140,10 +140,6 @@ impl EventBus {
140140
}
141141

142142
/// Subscribe a new receiver observing every event emitted from now on.
143-
///
144-
/// The bus is a plain fan-out: it does no filtering. A consumer that wants
145-
/// only some topics filters on its own side (see the SSE `?topics=`
146-
/// handler in `ethlambda-rpc`).
147143
pub fn subscribe(&self) -> broadcast::Receiver<ChainEvent> {
148144
self.tx.subscribe()
149145
}

crates/net/rpc/src/events.rs

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@
1010
//! ([`ethlambda_blockchain::ChainEvent::topic`]) and the `data:` line carries
1111
//! the event's flat JSON payload; the topic is never repeated inside the body.
1212
//!
13-
//! Filtering: `?topics=head,block` (comma-separated [`Topic`] names) narrows
14-
//! the stream server-side. An unknown name is a 400; a missing or empty
15-
//! parameter selects every topic.
13+
//! Filtering: `?topics=head,block` (comma-separated [`Topic`] names) selects
14+
//! which events to stream. `topics` is required (matching the Beacon API): a
15+
//! missing, empty, or unknown value is a 400.
1616
1717
use std::{convert::Infallible, str::FromStr};
1818

@@ -44,9 +44,6 @@ struct EventsParams {
4444
struct TopicSet(u16);
4545

4646
impl TopicSet {
47-
/// Every topic, including ones added later.
48-
const ALL: TopicSet = TopicSet(u16::MAX);
49-
5047
fn contains(self, topic: Topic) -> bool {
5148
self.0 & (1 << topic as u16) != 0
5249
}
@@ -64,10 +61,16 @@ async fn get_events(
6461
Extension(events): Extension<EventBus>,
6562
Query(params): Query<EventsParams>,
6663
) -> Response {
67-
// A missing or empty `topics` selects every topic. This diverges from the
68-
// Beacon API, which makes the parameter mandatory; see docs/rpc.md.
64+
// `topics` is required, matching the Beacon API: a missing or empty value
65+
// is a 400, as is any unknown topic name. See docs/rpc.md.
6966
let topics = match params.topics.as_deref() {
70-
None | Some("") => TopicSet::ALL,
67+
None | Some("") => {
68+
return (
69+
StatusCode::BAD_REQUEST,
70+
"missing required query parameter: topics",
71+
)
72+
.into_response();
73+
}
7174
Some(list) => {
7275
let parsed: Result<TopicSet, _> = list.split(',').map(Topic::from_str).collect();
7376
match parsed {
@@ -160,16 +163,17 @@ mod tests {
160163
assert!(!set.contains(Topic::Block));
161164
assert!(!set.contains(Topic::JustifiedCheckpoint));
162165

163-
// The empty set matches nothing; ALL matches every topic.
166+
// The empty set matches nothing; a fully-populated set matches all.
164167
let all = [
165168
Topic::Head,
166169
Topic::Block,
167170
Topic::JustifiedCheckpoint,
168171
Topic::FinalizedCheckpoint,
169172
];
173+
let full: TopicSet = all.into_iter().collect();
170174
for topic in all {
171175
assert!(!TopicSet(0).contains(topic));
172-
assert!(TopicSet::ALL.contains(topic));
176+
assert!(full.contains(topic));
173177
}
174178
}
175179

@@ -193,7 +197,7 @@ mod tests {
193197

194198
// Issue the request first so the handler subscribes its receiver
195199
// before we publish — `emit` drops events with no live receivers.
196-
let resp = events_response(&events, "/lean/v0/events").await;
200+
let resp = events_response(&events, "/lean/v0/events?topics=head").await;
197201
assert_eq!(resp.status(), StatusCode::OK);
198202

199203
events.emit(ChainEvent::Head {
@@ -267,7 +271,7 @@ mod tests {
267271
// next recv() reports RecvError::Lagged(1).
268272
let events = EventBus::new(2);
269273

270-
let resp = events_response(&events, "/lean/v0/events").await;
274+
let resp = events_response(&events, "/lean/v0/events?topics=head").await;
271275
assert_eq!(resp.status(), StatusCode::OK);
272276

273277
// Emit before polling the body: the handler already subscribed while
@@ -313,4 +317,16 @@ mod tests {
313317
"unhelpful 400 body: {text}"
314318
);
315319
}
320+
321+
#[tokio::test]
322+
async fn events_missing_or_empty_topics_returns_400() {
323+
let events = EventBus::new(16);
324+
325+
// `topics` is required (Beacon-API-aligned): a fully absent parameter
326+
// and a present-but-empty one are both rejected, never defaulted.
327+
for uri in ["/lean/v0/events", "/lean/v0/events?topics="] {
328+
let resp = events_response(&events, uri).await;
329+
assert_eq!(resp.status(), StatusCode::BAD_REQUEST, "uri: {uri}");
330+
}
331+
}
316332
}

docs/rpc.md

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -106,20 +106,18 @@ data: {"slot":128,"block":"0x1a2b…","state":"0x3c4d…"}
106106

107107
#### Filtering with `?topics=`
108108

109-
An optional comma-separated list of event names narrows the stream server-side:
109+
A **required** comma-separated list of event names selects which events to stream:
110110

111111
```bash
112112
curl -N 'http://127.0.0.1:5052/lean/v0/events?topics=head,finalized_checkpoint'
113113
```
114114

115-
Valid values are exactly the event names above: `head`, `block`, `justified_checkpoint`, `finalized_checkpoint`.
115+
Valid values are exactly the event names above: `head`, `block`, `justified_checkpoint`, `finalized_checkpoint`. As in the Beacon API `eventstream` endpoint, `topics` is mandatory: there is no "subscribe to everything" default; list the topics you want.
116116

117117
| Status | Condition |
118118
|--------|-----------|
119-
| `200` | Stream opened (filtered, or unfiltered when the parameter is missing/empty) |
120-
| `400` | Any listed name is not a known topic (body names the offending value) |
121-
122-
> **Divergence from the Beacon API:** the Beacon `eventstream` endpoint makes `topics` required; lean defaults a missing or empty parameter to all topics.
119+
| `200` | Stream opened for the listed topics |
120+
| `400` | `topics` is missing or empty, or any listed name is not a known topic (body names the offending value) |
123121

124122
Events are fanned out over a bounded broadcast channel. A client that reads too slowly skips past the events it missed: they are dropped for that subscriber rather than back-pressured onto the actor, so treat the stream as best-effort and re-sync via the blocks endpoints after a gap. A client that falls behind receives an SSE comment line `: error - dropped N messages` marking the gap (wire-compatible with Lighthouse) before the stream continues; re-sync via the blocks endpoints rather than trusting the skipped range. Keep-alive comments are sent periodically to hold idle connections open.
125123

0 commit comments

Comments
 (0)