@@ -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