NethermindEth / NethermindEth/pluto

Cache lifetime-constant Beacon Node API responses

Open
#481 1 comment 0 reactions 1 assignee View on GitHub

@varex83 is already working on this.

Since Sep 3, 2026.

enhancement
Dominant language
Rust
Stars
8
Forks
5
Avg merge
4d 16h
Merged PRs (30d)
37

Description

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.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.