Skip to content

feat(sui-client): export the connected Sui node's server version - #2101

Merged
omersadika merged 2 commits into
mainfrom
feat/sui-node-version-metric
Aug 30, 2026
Merged

feat(sui-client): export the connected Sui node's server version#2101
omersadika merged 2 commits into
mainfrom
feat/sui-node-version-metric

Conversation

@omersadika

Copy link
Copy Markdown
Contributor

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_errors tells 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 as sui_rpc_errors and reach Mimir through ika-proxy with no additional wiring:

  • ika_sui_client_sui_node_info — info-style IntGaugeVec, value 1 on exactly one child at a time. Labels:
    • server_versionGetServiceInfoResponse.server, the fullnode's own software version string (the gRPC analogue of the HTTP server header).
    • chain_identifierGetServiceInfoResponse.chain_id, the genesis checkpoint digest verbatim. Catches an operator pointed at the wrong network.
  • ika_sui_client_sui_node_info_last_success_unixtimeIntGauge, unix seconds of the last successful GetServiceInfo; 0 until 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". 0 additionally 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 to 1. A validator whose series is still on unknown is 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 to 0 and the new one to 1, 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:

value meaning
unknown eager default; the RPC has never answered
unreported the node answered but left the field empty (both proto fields are optional)
unsupported this transport has no service-info RPC at all — a peer-only validator reading Sui over the Ika p2p mirror relay. A healthy terminal state, deliberately not unknown.

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 shared RateLimitGate, so a rate-limited response is classified like any other and increments rate_limited_errors_total — at a maximum of five calls an hour, negligible, and correct if the endpoint really is refusing.

Cardinality

One child at 1 per node at a time, plus one retired child at 0 per 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_info is a new trait method with an Ok(None) default, so the relay transport and the existing test doubles need no change. Only SuiGrpcClient overrides it, using the same GetServiceInfo call get_latest_checkpoint_sequence and get_sui_chain_identifier already 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.rs covering the eager unknown publication, the flip to a real version, a version change leaving exactly one child at 1, idempotent re-reporting, a failed probe changing nothing at all, the unreported and unsupported paths, 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, and cargo check --workspace --all-targets all 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

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>
@omersadika
omersadika merged commit 905965f into main Aug 30, 2026
11 checks passed
@omersadika
omersadika deleted the feat/sui-node-version-metric branch August 30, 2026 17:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant