Skip to content

Conversation

@KaloyanTanev
Copy link
Collaborator

WIP

category: feature
ticket: #4009

@KaloyanTanev KaloyanTanev self-assigned this Jan 16, 2026
@KaloyanTanev KaloyanTanev marked this pull request as draft January 16, 2026 13:46
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Implements a duties cache to reduce duplicate beacon-node duty requests (scheduler + VC) by caching proposer/attester/sync-committee duties and routing call sites through cached accessors.

Changes:

  • Added DutiesCache to app/eth2wrap and wired it into the app’s eth2 client via SetDutiesCache and reorg invalidation.
  • Updated scheduler, inclusion tracker, and validator API component to use cached duty fetch paths.
  • Extended beaconmock and client wrappers (multi/lazy/http adapter) plus tests/mocks to support duties-cache interfaces.

Reviewed changes

Copilot reviewed 29 out of 29 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
testutil/integration/simnet_test.go Removes Teku simnet integration bits and related test scaffolding.
testutil/integration/helpers_test.go Removes externalIP helper previously used by Teku simnet wiring.
testutil/beaconmock/options.go Adds default/option wiring for cached duty funcs in beaconmock.
testutil/beaconmock/beaconmock_fuzz.go Adds cached duty funcs to fuzz option.
testutil/beaconmock/beaconmock.go Extends mock client with duties-cache funcs and UpdateCacheIndices.
core/validatorapi/validatorapi_test.go Updates tests to use cached duties funcs.
core/validatorapi/validatorapi.go Routes duties endpoints through cached duty calls and rewrites pubkey mapping.
core/tracker/inclusion.go Uses cached attester duties for inclusion tracking.
core/scheduler/scheduler.go Switches duty resolution to cached duty calls.
core/fetcher/fetcher_test.go Minor formatting-only adjustments.
core/fetcher/fetcher.go Minor formatting-only adjustments.
app/vmock.go Wires duties cache into vmock eth2 provider.
app/sse/listener_internal_test.go Uses require.Len for map length assertion.
app/sse/listener.go Groups handler func types and minor formatting.
app/featureset/config.go Minor formatting-only adjustments.
app/eth2wrap/valcache.go Removes old validator cache file (moved into new cache module).
app/eth2wrap/synthproposer_test.go Updates tests to provide cached duties funcs on mocks.
app/eth2wrap/synthproposer.go Extends synth wrapper provider interface to include cached duties provider.
app/eth2wrap/multi_test.go Adds coverage for new duties-cache forwarding methods.
app/eth2wrap/multi.go Adds multi-client forwarding for duties-cache APIs.
app/eth2wrap/mocks/client.go Regenerates mock client to include duties-cache APIs.
app/eth2wrap/lazy_test.go Adds tests for lazy wrapper duties-cache APIs.
app/eth2wrap/lazy.go Adds lazy wrapper support for duties-cache APIs.
app/eth2wrap/httpwrap.go Adds duties-cache hooks to the HTTP adapter wrapper.
app/eth2wrap/genwrap/genwrap.go Updates generated interface template to include duties-cache APIs.
app/eth2wrap/eth2wrap_gen.go Updates generated client interface to include duties-cache APIs.
app/eth2wrap/cache_test.go Adds initial tests for duties cache behavior (proposer only).
app/eth2wrap/cache.go Introduces new cache module containing validator cache + duties cache implementation.
app/app.go Wires duties cache into core workflow and hooks reorg invalidation + index updates.
Comments suppressed due to low confidence (1)

app/eth2wrap/cache_test.go:295

  • The duties cache test only covers proposer duties with a single (epoch, indices) shape. Given the new behavior, it would be valuable to add coverage for: (1) different index subsets in the same epoch (to ensure correct keying/filtering), (2) attester/sync duties cache paths, and (3) chain reorg invalidation (InvalidateCache).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +296 to +302
// ProposerDutiesCache returns the cached proposer duties, or fetches them if not available populating the cache.
func (c *DutiesCache) ProposerDutiesCache(ctx context.Context, epoch eth2p0.Epoch, vidxs []eth2p0.ValidatorIndex) ([]*eth2v1.ProposerDuty, error) {
duties, ok := c.cachedProposerDuties(epoch)

if ok {
return duties, nil
}
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This cache is keyed only by epoch: if duties for an epoch are cached from a request with a subset of validator indices, subsequent calls for the same epoch with a different vidxs will incorrectly return the cached subset. This can produce missing duties for the VC or scheduler. Either key the cache by (epoch, requested indices) or always populate the cache for the configured validatorIndices and filter to the requested vidxs on return (making UpdateCacheIndices meaningful).

Copilot uses AI. Check for mistakes.
Comment on lines +334 to +340
// AttesterDutiesCache returns the cached attester duties, or fetches them if not available populating the cache.
func (c *DutiesCache) AttesterDutiesCache(ctx context.Context, epoch eth2p0.Epoch, vidx []eth2p0.ValidatorIndex) ([]*eth2v1.AttesterDuty, error) {
duties, ok := c.cachedAttesterDuties(epoch)

if ok {
return duties, nil
}
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AttesterDutiesCache has the same issue as ProposerDutiesCache: once an epoch is cached it ignores the requested indices (vidx) and always returns the cached slice. This can cause the validator API to return incomplete duties when different subsets are requested within the same epoch.

Copilot uses AI. Check for mistakes.
Comment on lines +350 to +357
eth2Resp, err := c.eth2Cl.AttesterDuties(ctx, opts)
if err != nil {
return nil, err
}

c.attesterDuties[epoch] = eth2Resp.Data

return eth2Resp.Data, nil
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AttesterDutiesCache stores eth2Resp.Data directly in the cache without validating entries or copying the slice. If the underlying response contains nil elements or a caller mutates the returned duties, it can corrupt the cached data. Consider validating for nil duties (as done for proposer/sync) and returning/storing a defensive copy.

Copilot uses AI. Check for mistakes.
Comment on lines +486 to +491
// Setup duties cache, refreshing it every epoch.
dutiesCache := eth2wrap.NewDutiesCache(eth2Cl, []eth2p0.ValidatorIndex{})
eth2Cl.SetDutiesCache(dutiesCache.ProposerDutiesCache, dutiesCache.AttesterDutiesCache, dutiesCache.SyncCommDutiesCache)

sseListener.SubscribeChainReorgEvent(dutiesCache.InvalidateCache)

Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DutiesCache is created and hooked into the client, but it is never trimmed on epoch boundaries (only validator cache is trimmed). Since DutiesCache retains maps keyed by epoch, this will grow unbounded over time unless Trim is called periodically (e.g., alongside valCache.Trim() on refresh).

Copilot uses AI. Check for mistakes.
}

func (m Mock) UpdateCacheIndices(ctx context.Context, idxs []eth2p0.ValidatorIndex) {
m.UpdateCacheIndicesFunc(ctx, idxs)
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UpdateCacheIndices calls UpdateCacheIndicesFunc unconditionally. defaultMock doesn't set this function, so calling UpdateCacheIndices on a default beaconmock instance will panic (contradicting the "doesn't panic" intent of defaultMock). Consider defaulting UpdateCacheIndicesFunc to a no-op and/or guarding against nil here.

Suggested change
m.UpdateCacheIndicesFunc(ctx, idxs)
if m.UpdateCacheIndicesFunc != nil {
m.UpdateCacheIndicesFunc(ctx, idxs)
}

Copilot uses AI. Check for mistakes.
if duties[i] == nil {
return nil, errors.New("proposer duty cannot be nil")
}
for _, d := range cachedResp {
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ProposerDutiesCache returns pointers; this loop dereferences d without checking for nil. If the cache (or underlying BN response) contains a nil duty, this will panic. Add a nil check (like AttesterDuties/SyncCommitteeDuties do) before duty := *d and return a clear error.

Suggested change
for _, d := range cachedResp {
for _, d := range cachedResp {
if d == nil {
return nil, errors.New("nil proposer duty from cache")
}

Copilot uses AI. Check for mistakes.
@sonarqubecloud
Copy link

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