alloy-rs / alloy-rs/alloy

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

Ouverte
#3,948 0 commentaires 0 réactions 0 personnes assignées Voir sur GitHub
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.

Recevez les nouvelles issues par e-mail

Un résumé court des issues GitHub adaptées aux débutants.