github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/network/p2p/cache/gossipsub_spam_records.go (about) 1 package cache 2 3 import ( 4 "fmt" 5 "time" 6 7 "github.com/libp2p/go-libp2p/core/peer" 8 "github.com/rs/zerolog" 9 10 "github.com/onflow/flow-go/model/flow" 11 "github.com/onflow/flow-go/module" 12 herocache "github.com/onflow/flow-go/module/mempool/herocache/backdata" 13 "github.com/onflow/flow-go/module/mempool/herocache/backdata/heropool" 14 "github.com/onflow/flow-go/module/mempool/stdmap" 15 "github.com/onflow/flow-go/network/p2p" 16 p2plogging "github.com/onflow/flow-go/network/p2p/logging" 17 ) 18 19 // GossipSubSpamRecordCache is a cache for storing the gossipsub spam records of peers. It is thread-safe. 20 // The spam records of peers is used to calculate the application specific score, which is part of the GossipSub score of a peer. 21 // Note that neither of the spam records, application specific score, and GossipSub score are shared publicly with other peers. 22 // Rather they are solely used by the current peer to select the peers to which it will connect on a topic mesh. 23 type GossipSubSpamRecordCache struct { 24 // the in-memory and thread-safe cache for storing the spam records of peers. 25 c *stdmap.Backend 26 27 // Optional: the pre-processors to be called upon reading or updating a record in the cache. 28 // The pre-processors are called in the order they are added to the cache. 29 // The pre-processors are used to perform any necessary pre-processing on the record before returning it. 30 // Primary use case is to perform decay operations on the record before reading or updating it. In this way, a 31 // record is only decayed when it is read or updated without the need to explicitly iterating over the cache. 32 preprocessFns []PreprocessorFunc 33 34 // initFn is a function that is called to initialize a new record in the cache. 35 initFn func() p2p.GossipSubSpamRecord 36 } 37 38 var _ p2p.GossipSubSpamRecordCache = (*GossipSubSpamRecordCache)(nil) 39 40 // PreprocessorFunc is a function that is called by the cache upon reading or updating a record in the cache. 41 // It is used to perform any necessary pre-processing on the record before returning it when reading or changing it when updating. 42 // The effect of the pre-processing is that the record is updated in the cache. 43 // If there are multiple pre-processors, they are called in the order they are added to the cache. 44 // Args: 45 // 46 // record: the record to be pre-processed. 47 // lastUpdated: the last time the record was updated. 48 // 49 // Returns: 50 // 51 // GossipSubSpamRecord: the pre-processed record. 52 // error: an error if the pre-processing failed. The error is considered irrecoverable (unless the parameters can be adjusted and the pre-processing can be retried). The caller is 53 // advised to crash the node upon an error if failure to read or update the record is not acceptable. 54 type PreprocessorFunc func(record p2p.GossipSubSpamRecord, lastUpdated time.Time) (p2p.GossipSubSpamRecord, error) 55 56 // NewGossipSubSpamRecordCache returns a new HeroCache-based application specific Penalty cache. 57 // Args: 58 // 59 // sizeLimit: the maximum number of entries that can be stored in the cache. 60 // logger: the logger to be used by the cache. 61 // collector: the metrics collector to be used by the cache. 62 // 63 // Returns: 64 // 65 // *GossipSubSpamRecordCache: the newly created cache with a HeroCache-based backend. 66 func NewGossipSubSpamRecordCache(sizeLimit uint32, 67 logger zerolog.Logger, 68 collector module.HeroCacheMetrics, 69 initFn func() p2p.GossipSubSpamRecord, 70 prFns ...PreprocessorFunc) *GossipSubSpamRecordCache { 71 backData := herocache.NewCache(sizeLimit, 72 herocache.DefaultOversizeFactor, 73 heropool.LRUEjection, 74 logger.With().Str("mempool", "gossipsub-app-Penalty-cache").Logger(), 75 collector) 76 return &GossipSubSpamRecordCache{ 77 c: stdmap.NewBackend(stdmap.WithBackData(backData)), 78 preprocessFns: prFns, 79 initFn: initFn, 80 } 81 } 82 83 // Adjust updates the GossipSub spam penalty of a peer in the cache. If the peer does not have a record in the cache, a new record is created. 84 // The order of the pre-processing functions is the same as the order in which they were added to the cache. 85 // Args: 86 // - peerID: the peer ID of the peer in the GossipSub protocol. 87 // - updateFn: the update function to be applied to the record. 88 // Returns: 89 // - *GossipSubSpamRecord: the updated record. 90 // - error on failure to update the record. The returned error is irrecoverable and indicates an exception. 91 // Note that if any of the pre-processing functions returns an error, the record is reverted to its original state (prior to applying the update function). 92 func (a *GossipSubSpamRecordCache) Adjust(peerID peer.ID, updateFn p2p.UpdateFunction) (*p2p.GossipSubSpamRecord, error) { 93 entityId := entityIdOf(peerID) 94 95 var err error 96 adjustFunc := func(entity flow.Entity) flow.Entity { 97 e := entity.(gossipsubSpamRecordEntity) 98 99 currentRecord := e.GossipSubSpamRecord 100 // apply the pre-processing functions to the record. 101 for _, apply := range a.preprocessFns { 102 e.GossipSubSpamRecord, err = apply(e.GossipSubSpamRecord, e.lastUpdated) 103 if err != nil { 104 e.GossipSubSpamRecord = currentRecord 105 return e // return the original record if the pre-processing fails (atomic abort). 106 } 107 } 108 109 // apply the update function to the record. 110 e.GossipSubSpamRecord = updateFn(e.GossipSubSpamRecord) 111 112 if e.GossipSubSpamRecord != currentRecord { 113 e.lastUpdated = time.Now() 114 } 115 return e 116 } 117 118 initFunc := func() flow.Entity { 119 return gossipsubSpamRecordEntity{ 120 entityId: entityId, 121 peerID: peerID, 122 GossipSubSpamRecord: a.initFn(), 123 } 124 } 125 126 adjustedEntity, adjusted := a.c.AdjustWithInit(entityId, adjustFunc, initFunc) 127 if err != nil { 128 return nil, fmt.Errorf("error while applying pre-processing functions to cache record for peer %s: %w", p2plogging.PeerId(peerID), err) 129 } 130 if !adjusted { 131 return nil, fmt.Errorf("could not adjust cache record for peer %s", p2plogging.PeerId(peerID)) 132 } 133 134 r := adjustedEntity.(gossipsubSpamRecordEntity).GossipSubSpamRecord 135 return &r, nil 136 } 137 138 // Has returns true if the spam record of a peer is found in the cache, false otherwise. 139 // Args: 140 // - peerID: the peer ID of the peer in the GossipSub protocol. 141 // Returns: 142 // - true if the gossipsub spam record of the peer is found in the cache, false otherwise. 143 func (a *GossipSubSpamRecordCache) Has(peerID peer.ID) bool { 144 entityId := entityIdOf(peerID) 145 return a.c.Has(entityId) 146 } 147 148 // Get returns the spam record of a peer from the cache. 149 // Args: 150 // 151 // -peerID: the peer ID of the peer in the GossipSub protocol. 152 // 153 // Returns: 154 // - the application specific score record of the peer. 155 // - error if the underlying cache update fails, or any of the pre-processors fails. The error is considered irrecoverable, and 156 // the caller is advised to crash the node. 157 // - true if the record is found in the cache, false otherwise. 158 func (a *GossipSubSpamRecordCache) Get(peerID peer.ID) (*p2p.GossipSubSpamRecord, error, bool) { 159 entityId := entityIdOf(peerID) 160 if !a.c.Has(entityId) { 161 return nil, nil, false 162 } 163 164 var err error 165 record, updated := a.c.Adjust(entityId, func(entity flow.Entity) flow.Entity { 166 e := mustBeGossipSubSpamRecordEntity(entity) 167 168 currentRecord := e.GossipSubSpamRecord 169 for _, apply := range a.preprocessFns { 170 e.GossipSubSpamRecord, err = apply(e.GossipSubSpamRecord, e.lastUpdated) 171 if err != nil { 172 e.GossipSubSpamRecord = currentRecord 173 return e // return the original record if the pre-processing fails (atomic abort). 174 } 175 } 176 if e.GossipSubSpamRecord != currentRecord { 177 e.lastUpdated = time.Now() 178 } 179 return e 180 }) 181 if err != nil { 182 return nil, fmt.Errorf("error while applying pre-processing functions to cache record for peer %s: %w", p2plogging.PeerId(peerID), err), false 183 } 184 if !updated { 185 return nil, fmt.Errorf("could not decay cache record for peer %s", p2plogging.PeerId(peerID)), false 186 } 187 188 r := mustBeGossipSubSpamRecordEntity(record).GossipSubSpamRecord 189 return &r, nil, true 190 } 191 192 // GossipSubSpamRecord represents an Entity implementation GossipSubSpamRecord. 193 // It is internally used by the HeroCache to store the GossipSubSpamRecord. 194 type gossipsubSpamRecordEntity struct { 195 entityId flow.Identifier // the ID of the record (used to identify the record in the cache). 196 // lastUpdated is the time at which the record was last updated. 197 // the peer ID of the peer in the GossipSub protocol. 198 peerID peer.ID 199 lastUpdated time.Time 200 p2p.GossipSubSpamRecord 201 } 202 203 // In order to use HeroCache, the gossipsubSpamRecordEntity must implement the flow.Entity interface. 204 var _ flow.Entity = (*gossipsubSpamRecordEntity)(nil) 205 206 // ID returns the ID of the gossipsubSpamRecordEntity. As the ID is used to identify the record in the cache, it must be unique. 207 // Also, as the ID is used frequently in the cache, it is stored in the record to avoid recomputing it. 208 // ID is never exposed outside the cache. 209 func (a gossipsubSpamRecordEntity) ID() flow.Identifier { 210 return a.entityId 211 } 212 213 // Checksum returns the same value as ID. Checksum is implemented to satisfy the flow.Entity interface. 214 // HeroCache does not use the checksum of the gossipsubSpamRecordEntity. 215 func (a gossipsubSpamRecordEntity) Checksum() flow.Identifier { 216 return a.entityId 217 } 218 219 // entityIdOf converts a peer ID to a flow ID by taking the hash of the peer ID. 220 // This is used to convert the peer ID in a notion that is compatible with HeroCache. 221 // This is not a protocol-level conversion, and is only used internally by the cache, MUST NOT be exposed outside the cache. 222 // Args: 223 // - peerId: the peer ID of the peer in the GossipSub protocol. 224 // Returns: 225 // - flow.Identifier: the flow ID of the peer. 226 func entityIdOf(peerId peer.ID) flow.Identifier { 227 return flow.MakeID(peerId) 228 } 229 230 // mustBeGossipSubSpamRecordEntity converts a flow.Entity to a gossipsubSpamRecordEntity. 231 // This is used to convert the flow.Entity returned by HeroCache to a gossipsubSpamRecordEntity. 232 // If the conversion fails, it panics. 233 // Args: 234 // - entity: the flow.Entity to be converted. 235 // Returns: 236 // - gossipsubSpamRecordEntity: the converted gossipsubSpamRecordEntity. 237 func mustBeGossipSubSpamRecordEntity(entity flow.Entity) gossipsubSpamRecordEntity { 238 record, ok := entity.(gossipsubSpamRecordEntity) 239 if !ok { 240 // sanity check 241 // This should never happen, because the cache only contains gossipsubSpamRecordEntity entities. 242 panic(fmt.Sprintf("invalid entity type, expected gossipsubSpamRecordEntity type, got: %T", entity)) 243 } 244 return record 245 }