Skip to content

Commit 5f320e1

Browse files
authored
app/eth2wrap: fix mutation of pointer (#4488)
* app/eth2wrap: fix mutation of pointer (#4486) * Fix mutation of pointer * Compare contents, not only lengths in the fast path; add more tests * Address PR review comments * Fix linter * Version to rc
1 parent b50f15c commit 5f320e1

3 files changed

Lines changed: 393 additions & 64 deletions

File tree

app/eth2wrap/cache.go

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

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

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

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

393-
cacheUsed = true
396+
requestedSet := make(map[eth2p0.ValidatorIndex]struct{}, len(requestVidxs))
394397

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

398-
// Filter out the found duties.
399408
for _, d := range dutiesForEpoch.duties {
400-
if slices.Contains(requestVidxs, d.ValidatorIndex) {
409+
if _, hit := requestedSet[d.ValidatorIndex]; hit {
401410
dutiesResult = append(dutiesResult, &d)
402411
}
403412
}
404413

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

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

424+
requestVidxs = missing
425+
420426
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))
421427
}
422428

@@ -462,49 +468,55 @@ func (c *DutiesCache) AttesterDutiesCache(ctx context.Context, epoch eth2p0.Epoc
462468
allActive := c.activeValIdxs.valIdxs
463469
c.activeValIdxs.RUnlock()
464470

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

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

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

481-
cacheUsed = true
490+
requestedSet := make(map[eth2p0.ValidatorIndex]struct{}, len(requestVidxs))
482491

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

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

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

502-
for _, duty := range dutiesForEpoch.duties {
503-
requestVidxs = slices.DeleteFunc(requestVidxs, func(requestVidx eth2p0.ValidatorIndex) bool {
504-
return requestVidx == duty.ValidatorIndex
505-
})
514+
if len(dutiesResult) > 0 {
515+
cacheUsed = true
506516
}
507517

518+
requestVidxs = missing
519+
508520
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))
509521
}
510522

@@ -550,49 +562,55 @@ func (c *DutiesCache) SyncCommDutiesCache(ctx context.Context, epoch eth2p0.Epoc
550562
allActive := c.activeValIdxs.valIdxs
551563
c.activeValIdxs.RUnlock()
552564

553-
requestVidxs := vidxs
565+
// Clone so requestVidxs is an independent working copy; it is narrowed to missing indices below
566+
// and must never alias either the caller's slice or the shared activeValIdxs slice.
567+
requestVidxs := slices.Clone(vidxs)
554568
if len(requestVidxs) == 0 {
555-
requestVidxs = allActive
569+
requestVidxs = slices.Clone(allActive)
556570
}
557571

558572
dutiesForEpoch, ok := c.fetchSyncDuties(epoch)
559573
dutiesResult := make([]*eth2v1.SyncCommitteeDuty, 0, len(vidxs))
560574

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

569-
cacheUsed = true
584+
requestedSet := make(map[eth2p0.ValidatorIndex]struct{}, len(requestVidxs))
570585

571-
return SyncDutyWithMeta{Duties: dutiesResult, Metadata: dutiesForEpoch.metadata}, nil
586+
var missing []eth2p0.ValidatorIndex
587+
588+
for _, idx := range requestVidxs {
589+
requestedSet[idx] = struct{}{}
590+
591+
if _, hit := previouslyRequested[idx]; !hit {
592+
missing = append(missing, idx)
593+
}
572594
}
573595

574-
// Filter out the found duties.
575596
for _, d := range dutiesForEpoch.duties {
576-
if slices.Contains(requestVidxs, d.ValidatorIndex) {
597+
if _, hit := requestedSet[d.ValidatorIndex]; hit {
577598
dutiesResult = append(dutiesResult, &d)
578599
}
579600
}
580601

581-
if len(dutiesResult) > 0 {
602+
// Fast path: every requested index has been queried previously, so the cache answer is complete.
603+
if len(missing) == 0 {
582604
cacheUsed = true
583-
}
584-
585-
// Check if all requested duties were found in the cache (= being a subset of it).
586-
if len(dutiesResult) == len(requestVidxs) {
587605
return SyncDutyWithMeta{Duties: dutiesResult, Metadata: dutiesForEpoch.metadata}, nil
588606
}
589607

590-
for _, duty := range dutiesForEpoch.duties {
591-
requestVidxs = slices.DeleteFunc(requestVidxs, func(requestVidx eth2p0.ValidatorIndex) bool {
592-
return requestVidx == duty.ValidatorIndex
593-
})
608+
if len(dutiesResult) > 0 {
609+
cacheUsed = true
594610
}
595611

612+
requestVidxs = missing
613+
596614
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))
597615
}
598616

0 commit comments

Comments
 (0)