Builds a surfpool Docker image with a Yellowstone gRPC (Dragon's Mouth) geyser plugin baked in, so our indexer can stream from a surfnet over gRPC on staging instead of RPC logs.
- The stock
surfpool/surfpoolimage ships geyser support (since v1.4.0, PR #639) but does not include any geyser plugin and exposes no gRPC port. - gRPC = load a
yellowstone-grpcgeyser.soviasurfpool start -g <config>. The plugin, its config, and the port are what this image adds. - We consume surfpool as a published image — no surfpool source build here. A
two-stage Dockerfile builds only the plugin
.soand layers it on top.
Dockerfile:
- Stage 1 (
rust:bullseye) buildslibyellowstone_grpc_geyser.so. - Stage 2 (
surfpool/surfpool:1.5.0) copies the.so+geyser-config.jsonin, exposes gRPC10000, and starts surfpool with-g.
geyser-config.json: plugin config. gRPC on 0.0.0.0:10000, no x_token
(open — staging only), permissive filter limits so the indexer can subscribe
broadly. Prometheus metrics on 8999.
| Component | Pin | Why |
|---|---|---|
| surfpool base image | surfpool/surfpool:1.5.0 (Docker tags drop the v) |
geyser plugin support |
| yellowstone-grpc | v14.1.1+solana.4.1.0 (YELLOWSTONE_TAG ARG) |
the 4.0.2 plugin aborts (exit 133) encoding a live tx against 1.5.0; 4.1.0 handles the live-tx path |
Bumping: move both together. If you bump surfpool to a base built against a new agave-geyser-plugin-interface major, pick the yellowstone tag pinning the matching major, or the plugin fails to load at startup.
docker build -t surfpool-grpc .
docker run --rm -p 8899:8899 -p 8900:8900 -p 10000:10000 surfpool-grpc
# gRPC (Dragon's Mouth) is now on localhost:10000Override the yellowstone pin without editing the Dockerfile:
docker build --build-arg YELLOWSTONE_TAG=v14.1.1+solana.4.1.0 -t surfpool-grpc .- Service build: Dockerfile, context = repo root, path =
./Dockerfile. - Expose port 10000 (gRPC). Optionally 8899/8900 if clients need RPC/WS.
- Point the indexer's
GRPC_ENDPOINTat the service's10000address.
- No auth on the gRPC port (
x_token: null) — staging only. Setx_tokenbefore any exposure beyond the staging network. rust:bullseyetracks latest stable Rust; pinrust:<version>-bullseyein the Dockerfile if you need reproducible plugin builds.
- Keep
grpc.address, do NOT switch togrpc.listen. The plugin logs a deprecation warning recommendinglisten: ["0.0.0.0:10000"], but on yellowstonev13.3.1+solana.4.0.2that field crashes surfpool at plugin load (free(): invalid pointer, exit 133) — deterministic, 3/3. Theaddressfield works; the warning is cosmetic. Revisit only when bumping yellowstone. - Streaming on surfpool 1.4.0/1.5.0 — verified with a real submitted tx:
- ✅ Account subscriptions work (payer+recipient, correct post-balances/slot).
- ✅ Transaction subscriptions work — a real non-vote transfer streamed at
the exact transfer slot. (Earlier "broken" was a test error: yellowstone's
failed:truefilter means only-failed txs, so it excluded the successful transfer. Usefailedunset to get all.) - ❌ Slot (
slots) and block (blocks) subscriptions get ~nothing — the plugin drops the events (geyser_untrack_slot_event_dropped_totalclimbs ~1 per slot). - Note:
requestAirdropuses a surfpool cheatcode that BYPASSES geyser — funds accounts but emits nothing. Test streaming with real submitted txs only. - Root cause (confirmed on both sides in source): yellowstone begins
tracking a slot only on a lifecycle status —
FirstShredReceived/Completed/CreatedBank(block_reconstruction.rs::on_message_slot). The commitment statusesProcessed/Confirmed/Finalizedonly advance an already-tracked slot. surfpool emits ONLYConfirmed+Rootedand itsGeyserSlotStatusenum has no lifecycle variants at all (surfnet/mod.rs;svm.rscomment says lifecycle statuses are "intentionally not produced"). So no slot is ever tracked → block reconstruction drops all block_data (handle_block_data→ untrack counter) → no block ever assembles → the synthetic slot-status broadcast that feedsslots/blockssubscribers never fires. Account/transaction subscriptions are unaffected because those are broadcast directly on arrival. - Why it's by design, not a fixable omission: surfpool has no gossip layer
and a manipulable clock (time travel) — slots are non-contiguous (getSlot
jumped 16 in ~6s in testing; slot number ≠ one block each). Its own comment
(
svm.rs:2457) states the lifecycle statuses are "intentionally not produced." yellowstone's block state machine assumes contiguous slots with lifecycle events and skip-detection; a jumpy/time-traveling clock is fundamentally incompatible. SynthesizingFirstShredReceivedwould fight yellowstone's skipped-slot logic. So this is a design mismatch, not a missing line. - Impact: build the indexer on
accounts+transactions(work today). Do NOT rely onslots/blocksstreams for progress/commitment — unsupported on surfpool.