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  }