Skip to content

chain#442 phase 3: /v1/cluster/nodes proxy#66

Merged
proofmancer merged 1 commit into
mainfrom
chain-442-phase-3-api-cluster-proxy
May 21, 2026
Merged

chain#442 phase 3: /v1/cluster/nodes proxy#66
proofmancer merged 1 commit into
mainfrom
chain-442-phase-3-api-cluster-proxy

Conversation

@proofmancer
Copy link
Copy Markdown
Member

Summary

Phase 3 of ligate-io/ligate-chain#442: api.ligate.io/v1/cluster/nodes proxies the chain's internal cluster topology to the public, stripping private VPC addresses on the way out and tagging an aggregate cluster_health field.

What

  • crates/types/src/lib.rs: adds the wire types — public ClusterTopology / ClusterNode / ClusterHealth (no addresses) plus the internal ChainClusterTopology / ChainClusterNode (with addresses) used to deserialize the chain response.
  • crates/indexer/src/client.rs: NodeClient::cluster_nodes(auth_token: Option<&str>) GETs the chain's /v1/cluster/nodes. Sends an Authorization: Bearer <token> header when configured, matching the Caddy auth gate that chain#442 adds on the gateway VM.
  • crates/api/src/cluster.rs: handler + 5-second ClusterCache + transformer. Strips per-node address fields, computes cluster_health (healthy if every heartbeat < 2 s, degraded if any > 2 s, leaderless if no leader is held, unknown if the api can't reach the chain). Returns Cache-Control: public, max-age=5 on the happy path, max-age=10 on the degraded unknown path so partner dashboards back off cleanly during chain outages.
  • crates/api/src/main.rs: route registration .route("/v1/cluster/nodes", get(cluster::nodes)), plus ClusterCache on AppState.
  • crates/api/src/config.rs: new optional env var CHAIN_CLUSTER_AUTH_TOKEN. Unset = cluster_health: "unknown"; set = the api includes the Bearer header on chain fetches.

Reachability gate (operator side, NOT in this PR)

The api lives on Railway, outside the chain's VPC. The chain endpoint is Caddy-blocked publicly. Two operator steps need to happen before this proxy starts returning live data:

  1. Add a Bearer auth gate on Caddy for /v1/cluster/nodes:
    handle /v1/cluster/nodes {
        @authed header Authorization "Bearer {$CLUSTER_AUTH_TOKEN}"
        handle @authed { reverse_proxy 127.0.0.1:12346 }
        handle { respond "Internal endpoint." 404 }
    }
  2. Generate a token, stash in GCP Secret Manager, drop-in Environment="CLUSTER_AUTH_TOKEN=..." on the Caddy systemd unit, reload Caddy.
  3. Set CHAIN_CLUSTER_AUTH_TOKEN on the Railway api service.

Both can land as a follow-up PR + ops change. Until then this proxy ships and returns cluster_health: "unknown" (cleanly degraded; tested in unknown_topology_has_empty_nodes_and_unknown_health).

Public shape

{
  \"leader_node_id\": \"ligate-devnet-1-sequencer-2\",
  \"leader_acquired_at_epoch_ms\": 1779387589432,
  \"generated_at_epoch_ms\":       1779389534187,
  \"cluster_health\": \"healthy\",
  \"nodes\": [
    { \"node_id\": \"ligate-devnet-1-sequencer-2\", \"is_leader\": true,  \"last_heartbeat_age_ms\": 29 },
    { \"node_id\": \"ligate-devnet-1-sequencer\",   \"is_leader\": false, \"last_heartbeat_age_ms\": 40 },
    { \"node_id\": \"ligate-devnet-1-sequencer-3\", \"is_leader\": false, \"last_heartbeat_age_ms\": 71 }
  ]
}

Internal addresses stripped. Caller can branch on cluster_health for a UI badge without parsing the full list.

Test plan

  • cargo test -p ligate-api cluster (5/5 passes — transform strips addresses, healthy/degraded/leaderless/unknown classifications, default empty topology shape)
  • cargo clippy --workspace --all-targets -- -D warnings clean
  • cargo fmt --all -- --check clean
  • CI
  • After merge + Railway deploy: curl https://api.ligate.io/v1/cluster/nodes → expect cluster_health: \"unknown\" until the Caddy gate + Railway env var land

Refs: chain#442.

@proofmancer proofmancer merged commit 0d23ad3 into main May 21, 2026
7 checks passed
@proofmancer proofmancer deleted the chain-442-phase-3-api-cluster-proxy branch May 21, 2026 23:36
@github-actions github-actions Bot locked and limited conversation to collaborators May 21, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant