feat(sui-client): export the connected Sui node's server version - #2101
Merged
Conversation
Two mainnet operators' validators wedged in early boot at consecutive epoch boundaries, both with their own Sui fullnode's RPC failing for hours beforehand (~440 and ~316 errors/hour against a fleet baseline of 0-6). The leading hypothesis for the first — an operator who never upgraded their Sui node across a release rollout — could be neither confirmed nor refuted, because nothing in the fleet's telemetry says which Sui version a validator is talking to. `sui_rpc_errors` says an uplink is unhealthy; it never says what is on the other end of it. Add `ika_sui_client_sui_node_info` (info gauge, labels `server_version` and `chain_identifier`, sourced from the fullnode's own GetServiceInfo) and `ika_sui_client_sui_node_info_last_success_unixtime` for freshness. Both live on `SuiClientMetrics`, so they reach Mimir through ika-proxy on the same registry as `sui_rpc_errors`. The info gauge is registered eagerly at `server_version="unknown"`: a validator still on that label is itself the signal that its Sui RPC has never once answered, which is exactly the state both wedged nodes were in. Refreshes flip children rather than removing them, so a version change reads as a clean transition across a scrape gap. `SuiTransport::get_sui_node_info` is a new trait method defaulting to `Ok(None)` — "this transport has no service-info RPC", the honest answer for the p2p mirror relay and one that leaves the existing test doubles untouched. Only `SuiGrpcClient` overrides it. Refreshing runs on a task spawned at client construction and aborted on drop. It is never awaited by a caller: the nodes this metric most needs to describe are the ones whose RPC hangs. Every call is deadlined at 20s, failures leave the last known labels in place, and the cadence is 12 minutes after a short ramp for the first acquisition. Probe failures deliberately do NOT feed `sui_rpc_errors` — that counter's healthy baseline is 0-6/hour, too narrow to absorb a passive probe's floor without blunting the signal the probe exists to explain. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
./scripts/check-metric-names.sh --list regeneration for the two metrics this PR introduces; the Format Check convention gate enforces the block. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
At two consecutive mainnet epoch boundaries (2026-08-28 393→394 and 2026-08-29 394→395), two mainnet operators' validators wedged in early boot. In both cases the root cause was on the operator's side of the fence: their own Sui fullnode's RPC was failing — ~440 errors/hour on one, ~316 on the other, against a fleet baseline of 0–6/hour on
ika_sui_client_sui_rpc_errors.The leading hypothesis for the first is that the operator never upgraded their Sui node across a Sui release rollout. We could neither confirm nor refute that, because nothing in the fleet's telemetry says which Sui node version a validator is talking to.
sui_rpc_errorstells us an uplink is unhealthy; it never tells us what is on the other end of it. Every diagnosis of "is this operator's Sui node outdated?" currently ends in asking the operator.What this adds
Two metrics on
SuiClientMetrics, so they land in the same registry assui_rpc_errorsand reach Mimir through ika-proxy with no additional wiring:ika_sui_client_sui_node_info— info-styleIntGaugeVec, value1on exactly one child at a time. Labels:server_version—GetServiceInfoResponse.server, the fullnode's own software version string (the gRPC analogue of the HTTPserverheader).chain_identifier—GetServiceInfoResponse.chain_id, the genesis checkpoint digest verbatim. Catches an operator pointed at the wrong network.ika_sui_client_sui_node_info_last_success_unixtime—IntGauge, unix seconds of the last successfulGetServiceInfo;0until the first one lands. The info gauge holds its last known value by design, so on its own it cannot distinguish "confirmed minutes ago" from "the RPC has been dead for hours".0additionally separates never answered (a boot-time wedge) from answered once, then went stale (a mid-life failure).Semantics
Registered eagerly at client construction with
server_version="unknown"set to1. A validator whose series is still onunknownis itself the signal that its Sui RPC has never once answered — which is exactly the state both wedged nodes were in. Refreshes never remove a child: the previous one flips to0and the new one to1, so a version change reads as a clean transition across a scrape gap rather than a disappearing series.Three reserved label values, all documented at their definitions:
unknownunreportedoptional)unsupportedunknown.Refresh
A small task spawned at client construction and aborted when the client drops. It is never awaited by any caller — the nodes this metric most needs to describe are precisely the ones whose RPC is slow, hanging, or dead, so identifying them must not sit on any path a real operation waits for. Every call carries a 20s deadline, and every failure is a no-op that leaves the last known labels in place.
Cadence is 12 minutes in steady state. Before the first success it ramps 30s → 60s → 120s → … capped at 12 minutes, so a node whose uplink is merely flaky at boot is identified in tens of seconds instead of waiting out a full interval; a sustained outage settles onto the 12-minute cap. Once identified, a failed refresh is not urgent and always waits the full interval.
Refresh failures do not increment
sui_rpc_errors— deliberate, and documented in the module. That counter's healthy baseline is 0–6/hour, a band narrow enough that a passive probe contributing its own floor to it would blunt exactly the signal the probe exists to explain; and a probe failing while the node's real reads succeed is not an incident. The probe's own failure is already fully observable in its two metrics (unknown/stale labels, and a freshness gauge that stops advancing). Calls do go through the node's sharedRateLimitGate, so a rate-limited response is classified like any other and incrementsrate_limited_errors_total— at a maximum of five calls an hour, negligible, and correct if the endpoint really is refusing.Cardinality
One child at
1per node at a time, plus one retired child at0per version that node has reported during the process's life — in practice one or two, since an operator's Sui version changes only when they upgrade, and ours restarts on upgrade. Negligible.API surface
SuiTransport::get_sui_node_infois a new trait method with anOk(None)default, so the relay transport and the existing test doubles need no change. OnlySuiGrpcClientoverrides it, using the sameGetServiceInfocallget_latest_checkpoint_sequenceandget_sui_chain_identifieralready make — it takes no arguments and touches no store beyond the node's own watermarks, which is what makes it safe to poll on a timer.Tests
Nine unit tests in
node_info.rscovering the eagerunknownpublication, the flip to a real version, a version change leaving exactly one child at1, idempotent re-reporting, a failed probe changing nothing at all, theunreportedandunsupportedpaths, the per-call timeout, and the retry schedule (on a paused clock). No live Sui node required.cargo fmt --check,cargo clippy -p ika-sui-client --all-targets,cargo test --release -p ika-sui-client, andcargo check --workspace --all-targetsall clean.Follow-up
Grafana panels (fleet Sui-version spread; validators stuck on
unknown) and alerts land in the infra repo once this ships in a release.🤖 Generated with Claude Code