Skip to content

Commit 733e549

Browse files
authored
app/eth2wrap: fix mutation of pointer (#4486) (#4489)
* Fix mutation of pointer * Compare contents, not only lengths in the fast path; add more tests * Address PR review comments * Fix linter
1 parent 3418547 commit 733e549

2 files changed

Lines changed: 392 additions & 63 deletions

File tree

app/eth2wrap/cache.go

Lines changed: 81 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -375,49 +375,55 @@ func (c *DutiesCache) ProposerDutiesCache(ctx context.Context, epoch eth2p0.Epoc
375375
allActive := c.activeValIdxs.valIdxs
376376
c.activeValIdxs.RUnlock()
377377

378-
requestVidxs := vidxs
378+
// Clone so requestVidxs is an independent working copy; it is narrowed to missing indices below
379+
// and must never alias either the caller's slice or the shared activeValIdxs slice.
380+
requestVidxs := slices.Clone(vidxs)
379381
if len(requestVidxs) == 0 {
380-
requestVidxs = allActive
382+
requestVidxs = slices.Clone(allActive)
381383
}
382384

383385
dutiesForEpoch, ok := c.fetchProposerDuties(epoch)
384386
dutiesResult := make([]*eth2v1.ProposerDuty, 0, len(vidxs))
385387

386388
if ok {
387-
// If the request was for all validators and also all duties are already cached, skip more expensive operations.
388-
// This is the common case for most validator clients and Charon, which usually request duties for all active validators.
389-
if len(allActive) == len(requestVidxs) && len(allActive) == len(dutiesForEpoch.requestedIdxs) {
390-
for _, d := range dutiesForEpoch.duties {
391-
dutiesResult = append(dutiesResult, &d)
392-
}
389+
// previouslyRequested is the set of indices already queried from the beacon for this epoch.
390+
// A validator with no duty for the epoch is absent from dutiesForEpoch.duties but present
391+
// in dutiesForEpoch.requestedIdxs, so this set (not the duties list) determines cache hits.
392+
previouslyRequested := make(map[eth2p0.ValidatorIndex]struct{}, len(dutiesForEpoch.requestedIdxs))
393+
for _, idx := range dutiesForEpoch.requestedIdxs {
394+
previouslyRequested[idx] = struct{}{}
395+
}
393396

394-
cacheUsed = true
397+
requestedSet := make(map[eth2p0.ValidatorIndex]struct{}, len(requestVidxs))
395398

396-
return ProposerDutyWithMeta{Duties: dutiesResult, Metadata: dutiesForEpoch.metadata}, nil
399+
var missing []eth2p0.ValidatorIndex
400+
401+
for _, idx := range requestVidxs {
402+
requestedSet[idx] = struct{}{}
403+
404+
if _, hit := previouslyRequested[idx]; !hit {
405+
missing = append(missing, idx)
406+
}
397407
}
398408

399-
// Filter out the found duties.
400409
for _, d := range dutiesForEpoch.duties {
401-
if slices.Contains(requestVidxs, d.ValidatorIndex) {
410+
if _, hit := requestedSet[d.ValidatorIndex]; hit {
402411
dutiesResult = append(dutiesResult, &d)
403412
}
404413
}
405414

406-
if len(dutiesResult) > 0 {
415+
// Fast path: every requested index has been queried previously, so the cache answer is complete.
416+
if len(missing) == 0 {
407417
cacheUsed = true
408-
}
409-
410-
// Check if all requested duties were found in the cache (= being a subset of it).
411-
if len(dutiesResult) == len(requestVidxs) {
412418
return ProposerDutyWithMeta{Duties: dutiesResult, Metadata: dutiesForEpoch.metadata}, nil
413419
}
414420

415-
for _, duty := range dutiesForEpoch.duties {
416-
requestVidxs = slices.DeleteFunc(requestVidxs, func(requestVidx eth2p0.ValidatorIndex) bool {
417-
return requestVidx == duty.ValidatorIndex
418-
})
421+
if len(dutiesResult) > 0 {
422+
cacheUsed = true
419423
}
420424

425+
requestVidxs = missing
426+
421427
log.Debug(ctx, "Cached proposer duties do not contain all requested validator indices, fetching from beacon node...", z.Any("missing_validator_indices", requestVidxs), z.Any("requested_validator_indices", vidxs))
422428
}
423429

@@ -464,49 +470,55 @@ func (c *DutiesCache) AttesterDutiesCache(ctx context.Context, epoch eth2p0.Epoc
464470
allActive := c.activeValIdxs.valIdxs
465471
c.activeValIdxs.RUnlock()
466472

467-
requestVidxs := vidxs
473+
// Clone so requestVidxs is an independent working copy; it is narrowed to missing indices below
474+
// and must never alias either the caller's slice or the shared activeValIdxs slice.
475+
requestVidxs := slices.Clone(vidxs)
468476
if len(requestVidxs) == 0 {
469-
requestVidxs = allActive
477+
requestVidxs = slices.Clone(allActive)
470478
}
471479

472480
dutiesForEpoch, ok := c.fetchAttesterDuties(epoch)
473481
dutiesResult := make([]*eth2v1.AttesterDuty, 0, len(vidxs))
474482

475483
if ok {
476-
// If the request was for all validators and also all duties are already cached, this is done to skip more expensive operations.
477-
// This is the common case for most validator clients and Charon, which usually request duties for all active validators.
478-
if len(allActive) == len(requestVidxs) && len(allActive) == len(dutiesForEpoch.requestedIdxs) {
479-
for _, d := range dutiesForEpoch.duties {
480-
dutiesResult = append(dutiesResult, &d)
481-
}
484+
// previouslyRequested is the set of indices already queried from the beacon for this epoch.
485+
// A validator with no duty for the epoch is absent from dutiesForEpoch.duties but present
486+
// in dutiesForEpoch.requestedIdxs, so this set (not the duties list) determines cache hits.
487+
previouslyRequested := make(map[eth2p0.ValidatorIndex]struct{}, len(dutiesForEpoch.requestedIdxs))
488+
for _, idx := range dutiesForEpoch.requestedIdxs {
489+
previouslyRequested[idx] = struct{}{}
490+
}
482491

483-
cacheUsed = true
492+
requestedSet := make(map[eth2p0.ValidatorIndex]struct{}, len(requestVidxs))
484493

485-
return AttesterDutyWithMeta{Duties: dutiesResult, Metadata: dutiesForEpoch.metadata}, nil
494+
var missing []eth2p0.ValidatorIndex
495+
496+
for _, idx := range requestVidxs {
497+
requestedSet[idx] = struct{}{}
498+
499+
if _, hit := previouslyRequested[idx]; !hit {
500+
missing = append(missing, idx)
501+
}
486502
}
487503

488-
// Filter out the found duties.
489504
for _, d := range dutiesForEpoch.duties {
490-
if slices.Contains(requestVidxs, d.ValidatorIndex) {
505+
if _, hit := requestedSet[d.ValidatorIndex]; hit {
491506
dutiesResult = append(dutiesResult, &d)
492507
}
493508
}
494509

495-
if len(dutiesResult) > 0 {
510+
// Fast path: every requested index has been queried previously, so the cache answer is complete.
511+
if len(missing) == 0 {
496512
cacheUsed = true
497-
}
498-
499-
// Check if all requested duties were found in the cache (= being a subset of it).
500-
if len(dutiesResult) == len(requestVidxs) {
501513
return AttesterDutyWithMeta{Duties: dutiesResult, Metadata: dutiesForEpoch.metadata}, nil
502514
}
503515

504-
for _, duty := range dutiesForEpoch.duties {
505-
requestVidxs = slices.DeleteFunc(requestVidxs, func(requestVidx eth2p0.ValidatorIndex) bool {
506-
return requestVidx == duty.ValidatorIndex
507-
})
516+
if len(dutiesResult) > 0 {
517+
cacheUsed = true
508518
}
509519

520+
requestVidxs = missing
521+
510522
log.Debug(ctx, "Cached attester duties do not contain all requested validator indices, fetching from beacon node...", z.Any("missing_validator_indices", requestVidxs), z.Any("requested_validator_indices", vidxs))
511523
}
512524

@@ -553,49 +565,55 @@ func (c *DutiesCache) SyncCommDutiesCache(ctx context.Context, epoch eth2p0.Epoc
553565
allActive := c.activeValIdxs.valIdxs
554566
c.activeValIdxs.RUnlock()
555567

556-
requestVidxs := vidxs
568+
// Clone so requestVidxs is an independent working copy; it is narrowed to missing indices below
569+
// and must never alias either the caller's slice or the shared activeValIdxs slice.
570+
requestVidxs := slices.Clone(vidxs)
557571
if len(requestVidxs) == 0 {
558-
requestVidxs = allActive
572+
requestVidxs = slices.Clone(allActive)
559573
}
560574

561575
dutiesForEpoch, ok := c.fetchSyncDuties(epoch)
562576
dutiesResult := make([]*eth2v1.SyncCommitteeDuty, 0, len(vidxs))
563577

564578
if ok {
565-
// If the request was for all validators and also all duties are already cached, skip more expensive operations.
566-
// This is the common case for most validator clients and Charon, which usually request duties for all active validators.
567-
if len(allActive) == len(requestVidxs) && len(allActive) == len(dutiesForEpoch.requestedIdxs) {
568-
for _, d := range dutiesForEpoch.duties {
569-
dutiesResult = append(dutiesResult, &d)
570-
}
579+
// previouslyRequested is the set of indices already queried from the beacon for this epoch.
580+
// A validator with no duty for the epoch is absent from dutiesForEpoch.duties but present
581+
// in dutiesForEpoch.requestedIdxs, so this set (not the duties list) determines cache hits.
582+
previouslyRequested := make(map[eth2p0.ValidatorIndex]struct{}, len(dutiesForEpoch.requestedIdxs))
583+
for _, idx := range dutiesForEpoch.requestedIdxs {
584+
previouslyRequested[idx] = struct{}{}
585+
}
571586

572-
cacheUsed = true
587+
requestedSet := make(map[eth2p0.ValidatorIndex]struct{}, len(requestVidxs))
573588

574-
return SyncDutyWithMeta{Duties: dutiesResult, Metadata: dutiesForEpoch.metadata}, nil
589+
var missing []eth2p0.ValidatorIndex
590+
591+
for _, idx := range requestVidxs {
592+
requestedSet[idx] = struct{}{}
593+
594+
if _, hit := previouslyRequested[idx]; !hit {
595+
missing = append(missing, idx)
596+
}
575597
}
576598

577-
// Filter out the found duties.
578599
for _, d := range dutiesForEpoch.duties {
579-
if slices.Contains(requestVidxs, d.ValidatorIndex) {
600+
if _, hit := requestedSet[d.ValidatorIndex]; hit {
580601
dutiesResult = append(dutiesResult, &d)
581602
}
582603
}
583604

584-
if len(dutiesResult) > 0 {
605+
// Fast path: every requested index has been queried previously, so the cache answer is complete.
606+
if len(missing) == 0 {
585607
cacheUsed = true
586-
}
587-
588-
// Check if all requested duties were found in the cache (= being a subset of it).
589-
if len(dutiesResult) == len(requestVidxs) {
590608
return SyncDutyWithMeta{Duties: dutiesResult, Metadata: dutiesForEpoch.metadata}, nil
591609
}
592610

593-
for _, duty := range dutiesForEpoch.duties {
594-
requestVidxs = slices.DeleteFunc(requestVidxs, func(requestVidx eth2p0.ValidatorIndex) bool {
595-
return requestVidx == duty.ValidatorIndex
596-
})
611+
if len(dutiesResult) > 0 {
612+
cacheUsed = true
597613
}
598614

615+
requestVidxs = missing
616+
599617
log.Debug(ctx, "Cached sync duties do not contain all requested validator indices, fetching from beacon node...", z.Any("missing_validator_indices", requestVidxs), z.Any("requested_validator_indices", vidxs))
600618
}
601619

0 commit comments

Comments
 (0)