@@ -25,6 +25,7 @@ import (
2525 "strconv"
2626 "strings"
2727
28+ lru "github.com/hashicorp/golang-lru/v2"
2829 "github.com/onflow/flow-go/engine/common/rpc/convert"
2930 "github.com/onflow/flow-go/fvm/environment"
3031 "github.com/onflow/flow-go/fvm/errors"
@@ -276,7 +277,18 @@ func (s *Store) LedgerByHeight(
276277 ctx context.Context ,
277278 blockHeight uint64 ,
278279) (snapshot.StorageSnapshot , error ) {
280+ // Create a snapshot with LRU cache to avoid duplicate RPC calls within the same snapshot
281+ // LRU cache with max 1000 entries to prevent memory bloat
282+ cache , err := lru.New [string , flowgo.RegisterValue ](1000 )
283+ if err != nil {
284+ return nil , fmt .Errorf ("failed to create LRU cache: %w" , err )
285+ }
286+
279287 return snapshot .NewReadFuncStorageSnapshot (func (id flowgo.RegisterID ) (flowgo.RegisterValue , error ) {
288+ // Check LRU cache first to avoid duplicate RPC calls within this snapshot
289+ if cachedValue , exists := cache .Get (id .String ()); exists {
290+ return cachedValue , nil
291+ }
280292 // create a copy so updating it doesn't affect future calls
281293 lookupHeight := blockHeight
282294
@@ -378,6 +390,8 @@ func (s *Store) LedgerByHeight(
378390 return nil , fmt .Errorf ("could not cache ledger value: %w" , err )
379391 }
380392
393+ // Cache in LRU cache for this snapshot to avoid duplicate RPC calls
394+ cache .Add (id .String (), value )
381395 return value , nil
382396 }), nil
383397}
@@ -458,6 +472,40 @@ func (s *Store) fetchRemoteRegister(ctx context.Context, owner string, key strin
458472 return nil , nil
459473}
460474
475+ // COMPATIBILITY SHIM: Batch register fetching to reduce RPC calls
476+ // TODO: Remove after Flow release - batches multiple register lookups into single RPC call
477+ func (s * Store ) fetchRemoteRegisters (ctx context.Context , owner string , keys []string , height uint64 ) (map [string ][]byte , error ) {
478+ if len (keys ) == 0 {
479+ return make (map [string ][]byte ), nil
480+ }
481+
482+ registerIDs := make ([]* entities.RegisterID , len (keys ))
483+ for i , key := range keys {
484+ registerIDs [i ] = convert .RegisterIDToMessage (flowgo.RegisterID {Key : key , Owner : owner })
485+ }
486+
487+ response , err := s .executionClient .GetRegisterValues (ctx , & executiondata.GetRegisterValuesRequest {
488+ BlockHeight : height ,
489+ RegisterIds : registerIDs ,
490+ })
491+ if err != nil {
492+ if status .Code (err ) == codes .NotFound {
493+ return make (map [string ][]byte ), nil
494+ }
495+ return nil , err
496+ }
497+
498+ result := make (map [string ][]byte )
499+ if response != nil && len (response .Values ) > 0 {
500+ for i , key := range keys {
501+ if i < len (response .Values ) && len (response .Values [i ]) > 0 {
502+ result [key ] = response .Values [i ]
503+ }
504+ }
505+ }
506+ return result , nil
507+ }
508+
461509// COMPATIBILITY SHIM: Batch public key synthesis
462510// TODO: Remove after Flow release - builds batch public key payloads from individual public_key_* registers
463511func (s * Store ) synthesizeBatchPublicKeys (ctx context.Context , owner string , batchIndex uint32 , height uint64 ) ([]byte , error ) {
@@ -496,16 +544,28 @@ func (s *Store) synthesizeBatchPublicKeys(ctx context.Context, owner string, bat
496544 batch = append (batch , 0x00 )
497545 }
498546
547+ // Batch fetch all legacy public key registers for this batch to reduce RPC calls
548+ legacyKeys := make ([]string , 0 , end - start + 1 )
549+ for i := start ; i <= end ; i ++ {
550+ if i == 0 {
551+ continue // skip key 0
552+ }
553+ legacyKeys = append (legacyKeys , fmt .Sprintf ("public_key_%d" , i ))
554+ }
555+
556+ legacyValues , err := s .fetchRemoteRegisters (ctx , owner , legacyKeys , height )
557+ if err != nil {
558+ return nil , err
559+ }
560+
499561 for i := start ; i <= end ; i ++ {
500562 if i == 0 {
501563 // stored key 0 is apk_0 and not included in batch payload (nil placeholder already added)
502564 continue
503565 }
504566 legacyKey := fmt .Sprintf ("public_key_%d" , i )
505- legacyVal , err := s .fetchRemoteRegister (ctx , owner , legacyKey , height )
506- if err != nil {
507- return nil , err
508- }
567+ legacyVal := legacyValues [legacyKey ]
568+
509569 if len (legacyVal ) == 0 {
510570 // keep index alignment with zero-length entry
511571 batch = append (batch , 0x00 )
@@ -628,15 +688,24 @@ func (s *Store) buildKeyMetadataFromLegacy(ctx context.Context, owner string, co
628688 return nil , nil
629689 }
630690
691+ // Batch fetch all legacy public key registers to reduce RPC calls
692+ legacyKeys := make ([]string , count - 1 )
693+ for i := uint32 (1 ); i < count ; i ++ {
694+ legacyKeys [i - 1 ] = fmt .Sprintf ("public_key_%d" , i )
695+ }
696+
697+ legacyValues , err := s .fetchRemoteRegisters (ctx , owner , legacyKeys , height )
698+ if err != nil {
699+ return nil , err
700+ }
701+
631702 // Build weight and revoked status for keys 1..count-1
632703 var weightAndRevoked []byte
633704
634705 for i := uint32 (1 ); i < count ; i ++ {
635706 legacyKey := fmt .Sprintf ("public_key_%d" , i )
636- legacyVal , err := s .fetchRemoteRegister (ctx , owner , legacyKey , height )
637- if err != nil {
638- return nil , err
639- }
707+ legacyVal := legacyValues [legacyKey ]
708+
640709 if len (legacyVal ) == 0 {
641710 // Default values for missing keys
642711 weightAndRevoked = append (weightAndRevoked , 0 , 0 ) // weight=0, revoked=false
0 commit comments