Skip to content

feat: invalidate OPRF node RP signer cache on RpUpdated events#821

Draft
Takaros999 wants to merge 1 commit into
mainfrom
feat/oprf-rp-cache-event-invalidation
Draft

feat: invalidate OPRF node RP signer cache on RpUpdated events#821
Takaros999 wants to merge 1 commit into
mainfrom
feat/oprf-rp-cache-event-invalidation

Conversation

@Takaros999

Copy link
Copy Markdown
Contributor

Summary

The RpRegistryWatcher in the OPRF node caches each RP's signer purely by TTL (default 10m) with no event-driven invalidation, diverging from WIP-101 §8 — "OPRF Nodes MUST invalidate the signer cache on any RpUpdated event emission from the RpRegistry."

For EOA signers (the common case), rotation is an atomic swap of the single registered signer address in RpRegistry. With TTL-only caching, a rotation — including revoking a compromised signer via updateRp (rotate) or toggleActive=false (kill switch) — only takes effect once the cached entry expires. That leaves up to a full TTL window where the stale/stolen signer is still accepted, and during a planned rotation a window where neither key is accepted fleet-wide.

This PR adds a background loop that polls RpRegistry for RpUpdated logs over HTTP (eth_getLogs) and invalidates the cached entry of each updated RP, collapsing the propagation delay from the TTL to roughly one poll interval (default 2s). The TTL stays as the backstop.

Changes

  • rp_registry_watcher.rs: run_invalidation_loop() — polls RpUpdated, calls rp_store.invalidate(rpId). Cancellation-aware, fail-safe.
  • lib.rs: spawn the loop at startup, wired to the existing cancellation token.
  • config.rs: new rp_registry_poll_interval (default 2s).
  • metrics.rs: …cache.invalidations counter + …cache.last_polled_block heartbeat gauge.

Reliability notes

  • No request-path impact: detached task; if polling breaks, requests still serve off the cache/TTL as before.
  • TTL remains the backstop — events make revocation fast; TTL guarantees it eventually even if the loop stalls.
  • Fail-safe loop: RPC errors are logged and the same block range is retried next tick; the loop only exits on cancellation.
  • Over-invalidation is safe (worst case: one extra getRp), so reorgs are ignored rather than precisely tracked.
  • Observability: last_polled_block lets us alert when the poller stops advancing (a stalled poller silently degrades back to TTL-only — security-relevant).

Test plan

  • cargo check / cargo clippy (crate denies clippy::all + pedantic + unwrap_used) clean
  • New test_event_driven_invalidation (anvil): prime cache → on-chain signer rotation → loop evicts entry → refetch returns rotated signer
  • Existing rp_registry_watcher tests pass

Follow-ups (not in this PR)

  • Datadog monitor on last_polled_block not advancing.
  • Staged rollout across operators (poll interval is the natural throttle; set high to effectively disable per-operator).

🤖 Generated with Claude Code

The RpRegistryWatcher previously cached an RP's signer purely by TTL
(default 10m) with no event-driven invalidation, diverging from WIP-101
§8 ("OPRF Nodes MUST invalidate the signer cache on any RpUpdated
event"). For EOA signers this means a rotation or deactivation — e.g.
revoking a compromised signer — only takes effect once the cached entry
expires, leaving up to a full TTL window in which the stale signer is
still accepted.

Add a background loop that polls the RpRegistry for RpUpdated logs over
HTTP (eth_getLogs) and invalidates the cached entry of each updated RP,
collapsing the propagation delay to roughly one poll interval. The TTL
remains the backstop if the loop stalls.

Reliability:
- Fail-safe: RPC errors are logged and the same block range is retried
  on the next tick; the loop only exits on cancellation, never failing
  the request path.
- Over-invalidation is safe (worst case is one getRp refetch), so reorgs
  are ignored rather than tracked.
- Observability: adds an invalidations counter and a last_polled_block
  heartbeat gauge so a stalled poller can be alerted on.

Config: new rp_registry_poll_interval (default 2s).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@0xThemis

0xThemis commented Jun 29, 2026

Copy link
Copy Markdown
Collaborator

We discussed this during implementing wip101 and agreed that the TTL of 10 minutes if fine and we don't need an instant cache eviction on such cases.

I will reject this except there is a really (really) strong reason why this might be a problem (which I don't see and was already deemed ok 1 month ago).

Edit: happy do discuss smaller TTLs

@0xThemis

0xThemis commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

Will close this at end of the week if there is no activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants