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