-
Notifications
You must be signed in to change notification settings - Fork 134
*: add duties cache #4227
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
*: add duties cache #4227
Conversation
e3878a1 to
697631c
Compare
697631c to
b5f3d01
Compare
There was a problem hiding this 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
DutiesCachetoapp/eth2wrapand wired it into the app’s eth2 client viaSetDutiesCacheand 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.
| // 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 | ||
| } |
Copilot
AI
Jan 23, 2026
There was a problem hiding this comment.
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).
| // 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 | ||
| } |
Copilot
AI
Jan 23, 2026
There was a problem hiding this comment.
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.
| eth2Resp, err := c.eth2Cl.AttesterDuties(ctx, opts) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| c.attesterDuties[epoch] = eth2Resp.Data | ||
|
|
||
| return eth2Resp.Data, nil |
Copilot
AI
Jan 23, 2026
There was a problem hiding this comment.
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.
| // 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) | ||
|
|
Copilot
AI
Jan 23, 2026
There was a problem hiding this comment.
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).
| } | ||
|
|
||
| func (m Mock) UpdateCacheIndices(ctx context.Context, idxs []eth2p0.ValidatorIndex) { | ||
| m.UpdateCacheIndicesFunc(ctx, idxs) |
Copilot
AI
Jan 23, 2026
There was a problem hiding this comment.
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.
| m.UpdateCacheIndicesFunc(ctx, idxs) | |
| if m.UpdateCacheIndicesFunc != nil { | |
| m.UpdateCacheIndicesFunc(ctx, idxs) | |
| } |
| if duties[i] == nil { | ||
| return nil, errors.New("proposer duty cannot be nil") | ||
| } | ||
| for _, d := range cachedResp { |
Copilot
AI
Jan 23, 2026
There was a problem hiding this comment.
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.
| for _, d := range cachedResp { | |
| for _, d := range cachedResp { | |
| if d == nil { | |
| return nil, errors.New("nil proposer duty from cache") | |
| } |
|



WIP
category: feature
ticket: #4009