alloy_pubsub: subscription notifications silently dropped when server returns <32-byte subscription ID
- Langage dominant
- Rust
- Étoiles
- 1.3k
- Forks
- 668
- Merge moyen
- 2 j 2 h
- PR mergées (30 j)
- 29
Description
## Summary
`alloy_pubsub` silently drops every notification when the server returns a subscription ID shorter than 32 bytes. The subscription itself is reported as successful, the WebSocket stream stays open, but `subscribe_blocks().into_stream()` never yields a single header.
This affects spec-compliant Ethereum nodes — the JSON-RPC subscription spec (eth `eth_subscribe`) does not constrain the subscription ID to a fixed width. Some providers (notably `wss://ethereum-rpc.publicnode.com`) return a 16-byte hex ID; alloy's `subscribe_blocks()` deserialises the response into `FixedBytes<32>`, which causes a key mismatch in the pubsub frontend's subscription registry.
## Reproduction
```rust
// Cargo.toml
// alloy = { version = "2", features = ["full"] }
// tokio = { version = "1", features = ["full"] }
// futures = "0.3"
use alloy::providers::{Provider, ProviderBuilder};
use alloy::transports::ws::WsConnect;
use futures::StreamExt;
use std::time::Duration;
#[tokio::main]
async fn main() -> Result<(), Box> {
let url = "wss://ethereum-rpc.publicnode.com";
let provider = ProviderBuilder::new()
.connect_ws(WsConnect::new(url))
.await?;
let sub = provider.subscribe_blocks().await?;
let mut stream = sub.into_stream();
println!("subscribed; waiting up to 60s for a header…");
match tokio::time::timeout(Duration::from_secs(60), stream.next()).await {
Ok(Some(header)) => println!("OK — got header {}", header.number),
Ok(None) => eprintln!("FAIL — stream closed without yielding"),
Err(_) => eprintln!("FAIL — no header received in 60s"),
}
Ok(())
}
```
Run with:
```sh
RUST_LOG=alloy=trace cargo run
```
`websocat` against the same endpoint receives `eth_subscription` notifications every ~12 s, confirming the endpoint is healthy and pushing data. Alchemy and Infura (which return 32-byte hex IDs) work correctly with the same code.
## Expected behaviour
`stream.next()` yields a new header per block (~12 s on Ethereum mainnet).
## Actual behaviour
`stream.next()` blocks forever; no headers are emitted.
## Trace evidence
Subscription ID gets corrupted between the WS layer and the pubsub frontend:
| Layer | Subscription ID seen |
|---|---|
| `alloy_transport_ws` deserialised message | `0x7413bf1aeb8f1c0087c36b4243f7a41a` (16 bytes — server response) |
| `alloy_pubsub::frontend` retrieved response | `0x77ee11bf27e54080a8ae7be2e6b2fa08f48f6f45c9660376b0b7e2e3cc41677a` (32 bytes — derived) |
| `alloy_pubsub::service` registers | `GetSub(0x77ee...)` |
| Incoming notifications carry | `subscription: Number(154292977224430911757972734000217170970)` (= original 16-byte ID as `u128`) |
The `deserialize_response` span shows the type alloy is forcing the response into:
```
"ty":"alloy_primitives::bits::fixed::FixedBytes<32>"
```
Lookup key (32-byte derived value) ≠ notification key (original 16-byte ID) → notifications are silently dropped.
## Environment
- alloy: `2.0.4` (workspace dep `alloy = { version = "2", features = ["full"] }`)
- rustc: `1.95.0 (59807616e 2026-04-14)`
- OS: macOS 14 (arm64, Apple Silicon)
- Endpoint: `wss://ethereum-rpc.publicnode.com`
## Possible direction
The subscription ID type in `alloy_pubsub` could be loosened from `FixedBytes<32>` (i.e. `B256`) to a `SubscriptionId` enum that accepts variable-length hex strings or numeric IDs — matching `EthNotification::subscription` which is already typed as a flexible `Number` / `String` variant.
Happy to PR if there's a preferred direction.
Guide de contribution
Ouvrir le guide de contribution
Évaluation
Cette issue n'a pas encore été évaluée.