alloy-rs / alloy-rs/alloy

alloy_pubsub: subscription notifications silently dropped when server returns <32-byte subscription ID

Abierto
#3,948 0 comentarios 0 reacciones 0 asignados Ver en GitHub
Lenguaje dominante
Rust
Estrellas
1.3k
Forks
668
Merge medio
2 d 1 h
PR fusionados (30 d)
29

Descripción

## 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.

Guía de contribución

Abrir la guía de contribución

Evaluación

Este issue todavía no se ha evaluado.

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.