github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/network/alsp/internal/cache.go (about) 1 package internal 2 3 import ( 4 "fmt" 5 6 "github.com/rs/zerolog" 7 8 "github.com/onflow/flow-go/model/flow" 9 "github.com/onflow/flow-go/module" 10 herocache "github.com/onflow/flow-go/module/mempool/herocache/backdata" 11 "github.com/onflow/flow-go/module/mempool/herocache/backdata/heropool" 12 "github.com/onflow/flow-go/module/mempool/stdmap" 13 "github.com/onflow/flow-go/network/alsp" 14 "github.com/onflow/flow-go/network/alsp/model" 15 ) 16 17 // SpamRecordCache is a cache that stores spam records at the protocol layer for ALSP. 18 type SpamRecordCache struct { 19 recordFactory model.SpamRecordFactoryFunc // recordFactory is a factory function that creates a new spam record. 20 c *stdmap.Backend // c is the underlying cache. 21 } 22 23 var _ alsp.SpamRecordCache = (*SpamRecordCache)(nil) 24 25 // NewSpamRecordCache creates a new SpamRecordCache. 26 // Args: 27 // - sizeLimit: the maximum number of records that the cache can hold. 28 // - logger: the logger used by the cache. 29 // - collector: the metrics collector used by the cache. 30 // - recordFactory: a factory function that creates a new spam record. 31 // Returns: 32 // - *SpamRecordCache, the created cache. 33 // Note that the cache is supposed to keep the spam record for the authorized (staked) nodes. Since the number of such nodes is 34 // expected to be small, we do not eject any records from the cache. The cache size must be large enough to hold all 35 // the spam records of the authorized nodes. Also, this cache is keeping at most one record per origin id, so the 36 // size of the cache must be at least the number of authorized nodes. 37 func NewSpamRecordCache(sizeLimit uint32, logger zerolog.Logger, collector module.HeroCacheMetrics, recordFactory model.SpamRecordFactoryFunc) *SpamRecordCache { 38 backData := herocache.NewCache(sizeLimit, 39 herocache.DefaultOversizeFactor, 40 heropool.LRUEjection, 41 logger.With().Str("mempool", "aslp-spam-records").Logger(), 42 collector) 43 44 return &SpamRecordCache{ 45 recordFactory: recordFactory, 46 c: stdmap.NewBackend(stdmap.WithBackData(backData)), 47 } 48 } 49 50 // AdjustWithInit applies the given adjust function to the spam record of the given origin id. 51 // Returns the Penalty value of the record after the adjustment. 52 // It returns an error if the adjustFunc returns an error or if the record does not exist. 53 // Note that if the record does not exist, the record is initialized and the 54 // adjust function is applied to the initialized record again. 55 // Args: 56 // - originId: the origin id of the spam record. 57 // - adjustFunc: the function that adjusts the spam record. 58 // Returns: 59 // - Penalty value of the record after the adjustment. 60 // - error any returned error should be considered as an irrecoverable error and indicates a bug. 61 func (s *SpamRecordCache) AdjustWithInit(originId flow.Identifier, adjustFunc model.RecordAdjustFunc) (float64, error) { 62 var rErr error 63 wrapAdjustFunc := func(entity flow.Entity) flow.Entity { 64 record := mustBeProtocolSpamRecordEntity(entity) 65 66 // Adjust the record. 67 adjustedRecord, err := adjustFunc(record.ProtocolSpamRecord) 68 if err != nil { 69 rErr = fmt.Errorf("adjust function failed: %w", err) 70 return entity // returns the original entity (reverse the adjustment). 71 } 72 73 // Return the adjusted record. 74 return ProtocolSpamRecordEntity{adjustedRecord} 75 } 76 initFunc := func() flow.Entity { 77 return ProtocolSpamRecordEntity{s.recordFactory(originId)} 78 } 79 80 adjustedEntity, adjusted := s.c.AdjustWithInit(originId, wrapAdjustFunc, initFunc) 81 if rErr != nil { 82 return 0, fmt.Errorf("failed to adjust record: %w", rErr) 83 } 84 85 if !adjusted { 86 return 0, fmt.Errorf("adjustment failed for origin id %s", originId) 87 } 88 89 record := mustBeProtocolSpamRecordEntity(adjustedEntity) 90 return record.Penalty, nil 91 } 92 93 // Get returns the spam record of the given origin id. 94 // Returns the record and true if the record exists, nil and false otherwise. 95 // Args: 96 // - originId: the origin id of the spam record. 97 // Returns: 98 // - the record and true if the record exists, nil and false otherwise. 99 // Note that the returned record is a copy of the record in the cache (we do not want the caller to modify the record). 100 func (s *SpamRecordCache) Get(originId flow.Identifier) (*model.ProtocolSpamRecord, bool) { 101 entity, ok := s.c.ByID(originId) 102 if !ok { 103 return nil, false 104 } 105 106 record := mustBeProtocolSpamRecordEntity(entity) 107 108 // return a copy of the record (we do not want the caller to modify the record). 109 return &model.ProtocolSpamRecord{ 110 OriginId: record.OriginId, 111 Decay: record.Decay, 112 CutoffCounter: record.CutoffCounter, 113 Penalty: record.Penalty, 114 DisallowListed: record.DisallowListed, 115 }, true 116 } 117 118 // Identities returns the list of identities of the nodes that have a spam record in the cache. 119 func (s *SpamRecordCache) Identities() []flow.Identifier { 120 return flow.GetIDs(s.c.All()) 121 } 122 123 // Remove removes the spam record of the given origin id from the cache. 124 // Returns true if the record is removed, false otherwise (i.e., the record does not exist). 125 // Args: 126 // - originId: the origin id of the spam record. 127 // Returns: 128 // - true if the record is removed, false otherwise (i.e., the record does not exist). 129 func (s *SpamRecordCache) Remove(originId flow.Identifier) bool { 130 return s.c.Remove(originId) 131 } 132 133 // Size returns the number of spam records in the cache. 134 func (s *SpamRecordCache) Size() uint { 135 return s.c.Size() 136 } 137 138 // mustBeProtocolSpamRecordEntity returns the given entity as a ProtocolSpamRecordEntity. 139 // It panics if the given entity is not a ProtocolSpamRecordEntity. 140 // Args: 141 // - entity: the entity to be converted. 142 // Returns: 143 // - ProtocolSpamRecordEntity, the converted entity. 144 func mustBeProtocolSpamRecordEntity(entity flow.Entity) ProtocolSpamRecordEntity { 145 record, ok := entity.(ProtocolSpamRecordEntity) 146 if !ok { 147 // sanity check 148 // This should never happen, because the cache only contains ProtocolSpamRecordEntity entities. 149 panic(fmt.Sprintf("invalid entity type, expected ProtocolSpamRecordEntity type, got: %T", entity)) 150 } 151 return record 152 }