Skip to content

eRPC operator overlays are overwritten on stack up / helm sync (HyperEVM basket) #763

Description

@bussyjd

Summary

Operator-applied eRPC ConfigMap overlays (multi-upstream baskets with scoring, rate-limit budgets, cache policies, and network failsafe) are not durable. They are wiped or incomplete after obol stack up / helmfile re-render of the erpc release. We hit this live while wiring HyperEVM (chain 999 / alias hyperevm) for first-party Hyperliquid intelligence (hl-intel).

This is the same class of problem Hermes already solved for operator-edited runtime config (mergePreservedHermesConfigKeys), and partially solved for simple RPCs via host-side record-on-write ($CONFIG_DIR/rpc/recorded-upstreams.yaml + ReconcileRecordedRPCs). eRPC still lacks a durable overlay layer for rich baskets.

Context / reproduction (live)

Cluster: long-lived k3s stack (control plane + co-located hl-node host).

Desired state: in-cluster consumers call:

http://erpc.erpc.svc.cluster.local/rpc/hyperevm
# or
http://erpc.erpc.svc.cluster.local/rpc/evm/999

with a basket that prefers a local non-validator HyperEVM endpoint and fails over to public no-key RPCs.

What we did:

  1. Patched erpc/erpc-config ConfigMap to add:
    • network { alias: hyperevm, chainId: 999 } + failsafe
    • upstreams: local hl-node (http://<node-ip>:3001/evm) + public fallbacks
    • rateLimiters.budgets for public peers
    • finality: realtime cache policy for eth_call (precompile reads)
  2. Annotated obol.hl/hyperevm-basket=local-overlay so operators know it is hand-applied
  3. Verified live: /rpc/hyperevmeth_chainId = 0x3e7 (999), blockNumber advances

What will clobber it:

  • obol stack up / helmfile sync of the embedded eRPC chart values (internal/embed/infrastructure/values/erpc.yaml.gotmpl) re-materializes the base ConfigMap (mainnet / hoodi / base / base-sepolia only)
  • Our overlay is not in the chart and is not replayed by ReconcileRecordedRPCs
  • Even using obol network add --endpoint … only supports one custom-<chainId>-0 upstream, no scoring / budgets / multi-peer baskets, and local-node-style IPs are not first-class

Operator doc for the basket lives out-of-tree today:
obol-exex-indexer/services/hyperliquid/erpc-hyperevm-basket.patch.yaml (post-render patch runbook). That is a footgun, not a product surface.

Why obol network add is not enough

Capability needed for HyperEVM intel network add --endpoint today Full ConfigMap overlay
Multiple public fallbacks ❌ single custom-<id>-0
Prefer local node (score / order) ⚠️ endpoint first only ✅ scoreMultipliers / order
Per-upstream rateLimitBudget + auto-tune
Network-level hedge/retry tuned for eth_call ❌ generic failsafe
finality: realtime eth_call cache (2s)
Survive stack up / helm sync ✅ via recorded-upstreams.yaml (simple form only) ❌ today

So operators who need a real basket are forced into manual ConfigMap surgery, which silently dies on the next stack lifecycle event — same failure mode as pre-record RPC adds.

Analogy: how Hermes already handles “don’t overwrite my edits”

Hermes re-renders config.yaml on obol agent sync, but merges operator intent back in:

// internal/hermes/hermes.go — mergePreservedHermesConfigKeys
// carries operator-edited Hermes keys across obol agent sync.
// generateConfig only knows stack-managed defaults; security knobs
// like command_allowlist set via `hermes config` must survive re-render.

And eRPC already has a narrower version of the same idea for simple remotes:

// internal/network/record.go
// Record-on-write for remote RPC upstreams.
// AddPublicRPCs / AddCustomRPC update $CONFIG_DIR/rpc/recorded-upstreams.yaml
// ReconcileRecordedRPCs replays after obol stack up

Gap: there is no equivalent of “merge preserved operator overlay onto generated eRPC config” for structured multi-upstream baskets (or general YAML overlays).

Proposed solution

Goal

Make operator eRPC customizations first-class, durable, and re-applied after every stack lifecycle, without forking the embedded chart for every chain.

Design (recommended): host-side eRPC overlay + merge-on-reconcile

Mirror Hermes + recorded-RPCs, but for a mergeable overlay document.

1. Host-side source of truth

$OBOL_CONFIG_DIR/rpc/erpc-overlay.yaml   # 0600, like recorded-upstreams.yaml

Schema sketch:

version: 1
# Optional: full fragment(s) deep-merged into the rendered eRPC config after
# chart/base generation and after recorded-upstreams replay.
networks:
  - alias: hyperevm
    architecture: evm
    evm: { chainId: 999 }
    failsafe:
      timeout: { duration: 10s }
      retry: { maxAttempts: 3, delay: 100ms }
      hedge: { delay: 150ms, maxCount: 1 }
upstreams:
  - id: local-hl-node
    endpoint: http://192.168.50.21:3001/evm   # or host.k3d.internal / Service DNS
    evm: { chainId: 999 }
    routing:
      scoreMultipliers:
        - { network: "*", method: "*", overall: 5.0 }
  - id: hyperevm-official
    endpoint: https://rpc.hyperliquid.xyz/evm
    evm: { chainId: 999 }
    rateLimitBudget: hyperevm-official
    tags: ["tier:fallback"]
rateLimiters:
  budgets:
    - id: hyperevm-official
      rules: [{ method: "*", maxCount: 100, period: second }]
cachePoliciesAdd:
  - { network: "*", method: eth_call, finality: realtime, connector: memory-cache, ttl: 2s }

2. Apply path (single choke point)

Extend the post-stack up resume sequence (already ordered in cmd/obol/main.go):

helmfile render/apply eRPC base
  → ReconcileRecordedRPCs()          # existing simple remotes
  → ReconcileERPCOverlay()           # NEW: deep-merge overlay + writeERPCConfig + rollout

Also call ReconcileERPCOverlay from:

  • obol network sync (if that rewrites eRPC)
  • any path that rewrites the ConfigMap via writeERPCConfig

Implementation sketch:

  1. readERPCConfig
  2. Deep-merge overlay (networks by alias/chainId, upstreams by id, budgets by id, append unique cache policies)
  3. writeERPCConfig (existing patch + rollout)

Idempotent; safe to re-run.

3. CLI surface (operator ergonomics)

# Install / replace overlay from a file (the out-of-tree basket becomes in-tree intent)
obol network overlay apply -f services/hyperliquid/erpc-hyperevm-basket.patch.yaml

# Show effective vs overlay vs base
obol network overlay status

# Remove overlay and re-render base + recorded remotes only
obol network overlay clear

Alternatively (smaller API surface): extend obol network add with:

obol network add hyperevm --chain-id 999 \
  --endpoint http://192.168.50.21:3001/evm --primary \
  --endpoint https://rpc.hyperliquid.xyz/evm \
  --endpoint https://hyperliquid.drpc.org \
  --rate-limit-budget ...   # optional later

…and persist the multi-endpoint intent into the overlay/record format (not a single custom-999-0).

Prefer the overlay file approach for v1: it covers scoring, budgets, and cache policies without exploding CLI flags.

4. Helm ownership rules

  • Chart/base remains the generated floor (Obol-hosted remotes, base/sepolia, etc.).
  • Overlay is additive and never deleted by helm three-way merge of the chart alone, because we re-apply after helm.
  • Document: do not hand-edit the live ConfigMap; edit $CONFIG_DIR/rpc/erpc-overlay.yaml or use obol network overlay apply.

Optional hardening: set ConfigMap annotation
obol.stack/erpc-overlay-hash: <sha> and warn on stack up if live CM hash ≠ expected (detect manual drift).

5. What not to do

Anti-pattern Why
Keep teaching kubectl ConfigMap patches Breaks on every stack up / upgrade
Fork erpc.yaml.gotmpl in every consumer repo Diverges from release; no reconcile path
Stuff full baskets only into recorded-upstreams.yaml as-is Schema only supports one custom endpoint + chainlist list
Put secrets in the overlay without 0600 + redaction Same rules as paid RPC URLs in recorded remotes

Acceptance criteria

  • Operator can declare a multi-upstream HyperEVM basket that survives obol stack up and helmfile eRPC re-sync
  • /rpc/hyperevm and /rpc/evm/999 keep working after a full stack recycle without manual kubectl
  • Overlay merge is idempotent and does not drop base upstreams (mainnet/base/…)
  • obol network overlay status (or equivalent) shows overlay presence + last apply
  • Unit tests: merge by chainId/id; reconcile called from stack-up resume guard (mirror stackup_resume_guard_test.go for ReconcileERPCOverlay)
  • Docs: obol network help + short guide; migrate note from out-of-tree erpc-hyperevm-basket.patch.yaml

Suggested implementation slices

  1. P0erpc-overlay.yaml + ReconcileERPCOverlay after ReconcileRecordedRPCs; file-based apply via obol network overlay apply -f
  2. P1 — Multi-endpoint network add that writes overlay (not only single custom)
  3. P2 — First-party preset: obol network add hyperevm --preset public+local --local-endpoint … shipping the curated public basket in-tree (optional; can stay in consumer repos as overlay files)

Related code

  • internal/network/rpc.goreadERPCConfig / writeERPCConfig / AddCustomRPC
  • internal/network/record.gorecorded-upstreams.yaml, ReconcileRecordedRPCs
  • internal/hermes/hermes.gomergePreservedHermesConfigKeys (preserve operator intent across re-render)
  • cmd/obol/main.go — stack-up resume order + stackup_resume_guard_test.go
  • internal/embed/infrastructure/values/erpc.yaml.gotmpl — base eRPC generation
  • Consumer basket (today): obol-exex-indexer/services/hyperliquid/erpc-hyperevm-basket.patch.yaml

Motivation

First-party services (Hyperliquid hl-intel, future chain-local nodes) need LAN-primary + public failover baskets with real rate-limit / cache tuning. Without durable overlays, every stack upgrade silently breaks chain-oracle / HyperEVM reads and forces tribal knowledge kubectl patches. Hermes already taught us: generated config + merge preserved operator intent is the right product shape.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions