Skip to content

Cache lifetime-constant Beacon Node API responses #481

Description

@emlautarom1-agent

Summary

Several Beacon Node API responses are constant for the lifetime of the client (genesis data, the chain spec, slots-per-epoch / slot duration, the deposit contract, …) yet Pluto re-fetches them over HTTP on every call. We should cache these once and reuse them, while being conservative: only cache what is genuinely immutable, and when in doubt make the extra call rather than risk serving stale data.

This came out of the core/scheduler review (PR #466). SchedulerActor::get_duty_definition issues a live fetch_slots_config() round-trip on every query just to derive slots_per_epoch:

slots_per_epoch never changes, so this is a per-call beacon round-trip on a hot path that blocks the actor. A general cache for lifetime-constant responses fixes this class of problem rather than just the one call site.

Where caching should live

Almost all the constant-data helpers funnel through two private base fetchers in extensions.rs, which makes them the natural memoization points:

Memoizing just these two makes every downstream helper (fetch_genesis_time, fetch_slots_config, fetch_fork_config, fetch_domain*, …) cheap.

Note that EthBeaconNodeApiClient itself is an OpenAPI-generated struct, so a cache field can't simply be added to it without changing the generator:

A caching layer (or a small wrapper type) is therefore preferable. There is already precedent for this pattern in the crate — the epoch-aware ValidatorCache:

Reference: what Charon caches

Charon delegates this caching to go-eth2-client. Its eth2wrap code generator marks each endpoint with a Latency flag; Latency: false means "this endpoint is cached in go-eth2-client" (the generator literally emits that comment):

The endpoints Charon/go-eth2-client cache (Latency: false) are: BeaconBlockRoot, DepositContract, Domain, Genesis, NodePeerCount, NodeVersion, SlotDuration, SlotsPerEpoch, Spec. Notably ForkSchedule and Fork are NOT cached by Charon (Latency: true).

Category 1 — cache these (lifetime-constant; Charon caches them too)

Data Pluto method Backing endpoint
Genesis (time, validators root, genesis fork version) fetch_genesis_datafetch_genesis_time, fetch_genesis_validators_root, fetch_genesis_fork_version get_genesis
Chain spec fetch_spec_data / fetch_spec get_spec
Slot duration & slots-per-epoch fetch_slots_config derived from spec
Signing domains fetch_domain_type, fetch_genesis_domain, fetch_domain derived from spec + genesis
Deposit contract (address, chain id) — (not yet wrapped) get_deposit_contract
Beacon node version — (not yet wrapped) get_node_version

Caching fetch_spec_data and fetch_genesis_data covers the whole first four rows at once.

Category 2 — semantically constant-ish, but do NOT cache (err on the side of caution)

Data Pluto method Why not
Fork schedule fetch_fork_configget_fork_schedule Future forks can be (re)scheduled. Charon explicitly does not cache this (Latency: true). In Pluto it is derived from the spec, so it rides along with the spec cache only as far as the spec itself; treat new forks as a reason to re-read.
State fork get_state_fork Per-state / per-fork-epoch, not lifetime-constant. Charon does not cache it.
Peer count get_peer_count go-eth2-client marks it Latency: false, but it is semantically dynamic — do not cache in Pluto.
Block root get_block_root Cached by go-eth2-client keyed by block_id; a single value cache is wrong here. Out of scope.

Everything else (duties, validator state, syncing status, head/finality, pools, block production/submission, rewards, …) is dynamic and must not be cached.

Proposed scope

  1. Introduce a memoization layer for the two base fetchers (fetch_spec_data, fetch_genesis_data) so all constant-data helpers become effectively free after the first call.
  2. Use it to make slots_per_epoch available synchronously to the scheduler, removing the per-call fetch_slots_config() round-trip at scheduler.rs#L341.
  3. Optionally extend the same mechanism to get_deposit_contract and get_node_version if/when they get high-level wrappers.
  4. Leave Category 2 endpoints uncached.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions