github.com/onflow/flow-go@v0.33.17/network/p2p/inspector/internal/cache/cache.go (about)

     1  package cache
     2  
     3  import (
     4  	"fmt"
     5  	"time"
     6  
     7  	"github.com/rs/zerolog"
     8  
     9  	"github.com/onflow/flow-go/model/flow"
    10  	"github.com/onflow/flow-go/module"
    11  	herocache "github.com/onflow/flow-go/module/mempool/herocache/backdata"
    12  	"github.com/onflow/flow-go/module/mempool/herocache/backdata/heropool"
    13  	"github.com/onflow/flow-go/module/mempool/stdmap"
    14  	"github.com/onflow/flow-go/network/p2p/scoring"
    15  )
    16  
    17  type recordEntityFactory func(identifier flow.Identifier) ClusterPrefixedMessagesReceivedRecord
    18  
    19  type RecordCacheConfig struct {
    20  	sizeLimit uint32
    21  	logger    zerolog.Logger
    22  	collector module.HeroCacheMetrics
    23  	// recordDecay decay factor used by the cache to perform geometric decay on gauge values.
    24  	recordDecay float64
    25  }
    26  
    27  // RecordCache is a cache that stores ClusterPrefixedMessagesReceivedRecord by peer node ID. Each record
    28  // contains a float64 Gauge field that indicates the current approximate number cluster prefixed control messages that were allowed to bypass
    29  // validation due to some error that will prevent the message from being validated.
    30  // Each record contains a float64 Gauge field that is decayed overtime back to 0. This ensures that nodes that fall
    31  // behind in the protocol can catch up.
    32  type RecordCache struct {
    33  	// recordEntityFactory is a factory function that creates a new ClusterPrefixedMessagesReceivedRecord.
    34  	recordEntityFactory recordEntityFactory
    35  	// c is the underlying cache.
    36  	c *stdmap.Backend
    37  	// decayFunc decay func used by the cache to perform decay on gauges.
    38  	decayFunc decayFunc
    39  }
    40  
    41  // NewRecordCache creates a new *RecordCache.
    42  // Args:
    43  // - config: record cache config.
    44  // - recordEntityFactory: a factory function that creates a new spam record.
    45  // Returns:
    46  // - *RecordCache, the created cache.
    47  // Note that this cache is supposed to keep the cluster prefix control messages received record for the authorized (staked) nodes. Since the number of such nodes is
    48  // expected to be small, we do not eject any records from the cache. The cache size must be large enough to hold all
    49  // the records of the authorized nodes. Also, this cache is keeping at most one record per peer id, so the
    50  // size of the cache must be at least the number of authorized nodes.
    51  func NewRecordCache(config *RecordCacheConfig, recordEntityFactory recordEntityFactory) (*RecordCache, error) {
    52  	backData := herocache.NewCache(config.sizeLimit,
    53  		herocache.DefaultOversizeFactor,
    54  		heropool.LRUEjection,
    55  		config.logger.With().Str("mempool", "gossipsub=cluster-prefix-control-messages-received-records").Logger(),
    56  		config.collector)
    57  	return &RecordCache{
    58  		recordEntityFactory: recordEntityFactory,
    59  		decayFunc:           defaultDecayFunction(config.recordDecay),
    60  		c:                   stdmap.NewBackend(stdmap.WithBackData(backData)),
    61  	}, nil
    62  }
    63  
    64  // ReceivedClusterPrefixedMessage applies an adjustment that increments the number of cluster prefixed control messages received by a peer.
    65  // Returns number of cluster prefix control messages received after the adjustment. The record is initialized before
    66  // the adjustment func is applied that will increment the Gauge.
    67  // Args:
    68  // - nodeID: the node ID of the sender of the control message.
    69  // Returns:
    70  //   - The cluster prefix control messages received gauge value after the adjustment.
    71  //   - exception only in cases of internal data inconsistency or bugs. No errors are expected.
    72  func (r *RecordCache) ReceivedClusterPrefixedMessage(nodeID flow.Identifier) (float64, error) {
    73  	var err error
    74  	adjustFunc := func(entity flow.Entity) flow.Entity {
    75  		entity, err = r.decayAdjustment(entity) // first decay the record
    76  		if err != nil {
    77  			return entity
    78  		}
    79  		return r.incrementAdjustment(entity) // then increment the record
    80  	}
    81  
    82  	adjustedEntity, adjusted := r.c.AdjustWithInit(nodeID, adjustFunc, func() flow.Entity {
    83  		return r.recordEntityFactory(nodeID)
    84  	})
    85  
    86  	if err != nil {
    87  		return 0, fmt.Errorf("unexpected error while applying decay and increment adjustments for node %s: %w", nodeID, err)
    88  	}
    89  
    90  	if !adjusted {
    91  		return 0, fmt.Errorf("adjustment failed for node %s", nodeID)
    92  	}
    93  
    94  	record := mustBeClusterPrefixedMessageReceivedRecordEntity(adjustedEntity)
    95  
    96  	return record.Gauge, nil
    97  }
    98  
    99  // GetWithInit returns the current number of cluster prefixed control messages received from a peer.
   100  // The record is initialized before the count is returned.
   101  // Before the control messages received gauge value is returned it is decayed using the configured decay function.
   102  // Returns the record and true if the record exists, nil and false otherwise.
   103  // Args:
   104  // - nodeID: the node ID of the sender of the control message.
   105  // Returns:
   106  // - The cluster prefixed control messages received gauge value after the decay and true if the record exists, 0 and false otherwise.
   107  // No errors are expected during normal operation.
   108  func (r *RecordCache) GetWithInit(nodeID flow.Identifier) (float64, bool, error) {
   109  	var err error
   110  	adjustLogic := func(entity flow.Entity) flow.Entity {
   111  		// perform decay on gauge value
   112  		entity, err = r.decayAdjustment(entity)
   113  		return entity
   114  	}
   115  	adjustedEntity, adjusted := r.c.AdjustWithInit(nodeID, adjustLogic, func() flow.Entity {
   116  		return r.recordEntityFactory(nodeID)
   117  	})
   118  	if err != nil {
   119  		return 0, false, fmt.Errorf("unexpected error while applying decay adjustment for node %s: %w", nodeID, err)
   120  	}
   121  	if !adjusted {
   122  		return 0, false, fmt.Errorf("decay adjustment failed for node %s", nodeID)
   123  	}
   124  
   125  	record := mustBeClusterPrefixedMessageReceivedRecordEntity(adjustedEntity)
   126  
   127  	return record.Gauge, true, nil
   128  }
   129  
   130  // NodeIDs returns the list of identities of the nodes that have a spam record in the cache.
   131  func (r *RecordCache) NodeIDs() []flow.Identifier {
   132  	return flow.GetIDs(r.c.All())
   133  }
   134  
   135  // Remove removes the record of the given peer id from the cache.
   136  // Returns true if the record is removed, false otherwise (i.e., the record does not exist).
   137  // Args:
   138  // - nodeID: the node ID of the sender of the control message.
   139  // Returns:
   140  // - true if the record is removed, false otherwise (i.e., the record does not exist).
   141  func (r *RecordCache) Remove(nodeID flow.Identifier) bool {
   142  	return r.c.Remove(nodeID)
   143  }
   144  
   145  // Size returns the number of records in the cache.
   146  func (r *RecordCache) Size() uint {
   147  	return r.c.Size()
   148  }
   149  
   150  func (r *RecordCache) incrementAdjustment(entity flow.Entity) flow.Entity {
   151  	record, ok := entity.(ClusterPrefixedMessagesReceivedRecord)
   152  	if !ok {
   153  		// sanity check
   154  		// This should never happen, because the cache only contains ClusterPrefixedMessagesReceivedRecord entities.
   155  		panic(fmt.Sprintf("invalid entity type, expected ClusterPrefixedMessagesReceivedRecord type, got: %T", entity))
   156  	}
   157  	record.Gauge++
   158  	record.lastUpdated = time.Now()
   159  	// Return the adjusted record.
   160  	return record
   161  }
   162  
   163  // All errors returned from this function are unexpected and irrecoverable.
   164  func (r *RecordCache) decayAdjustment(entity flow.Entity) (flow.Entity, error) {
   165  	record, ok := entity.(ClusterPrefixedMessagesReceivedRecord)
   166  	if !ok {
   167  		// sanity check
   168  		// This should never happen, because the cache only contains ClusterPrefixedMessagesReceivedRecord entities.
   169  		panic(fmt.Sprintf("invalid entity type, expected ClusterPrefixedMessagesReceivedRecord type, got: %T", entity))
   170  	}
   171  	var err error
   172  	record, err = r.decayFunc(record)
   173  	if err != nil {
   174  		return record, err
   175  	}
   176  	record.lastUpdated = time.Now()
   177  	// Return the adjusted record.
   178  	return record, nil
   179  }
   180  
   181  // decayFunc the callback used to apply a decay method to the record.
   182  // All errors returned from this callback are unexpected and irrecoverable.
   183  type decayFunc func(recordEntity ClusterPrefixedMessagesReceivedRecord) (ClusterPrefixedMessagesReceivedRecord, error)
   184  
   185  // defaultDecayFunction is the default decay function that is used to decay the cluster prefixed control message received gauge of a peer.
   186  // All errors returned are unexpected and irrecoverable.
   187  func defaultDecayFunction(decay float64) decayFunc {
   188  	return func(recordEntity ClusterPrefixedMessagesReceivedRecord) (ClusterPrefixedMessagesReceivedRecord, error) {
   189  		received := recordEntity.Gauge
   190  		if received == 0 {
   191  			return recordEntity, nil
   192  		}
   193  
   194  		decayedVal, err := scoring.GeometricDecay(received, decay, recordEntity.lastUpdated)
   195  		if err != nil {
   196  			return recordEntity, fmt.Errorf("could not decay cluster prefixed control messages received gauge: %w", err)
   197  		}
   198  		recordEntity.Gauge = decayedVal
   199  		return recordEntity, nil
   200  	}
   201  }
   202  
   203  // mustBeClusterPrefixedMessageReceivedRecordEntity is a helper function for type assertion of the flow.Entity to ClusterPrefixedMessagesReceivedRecord.
   204  // It panics if the type assertion fails.
   205  // Args:
   206  // - entity: the flow.Entity to be type asserted.
   207  // Returns:
   208  // - the ClusterPrefixedMessagesReceivedRecord entity.
   209  func mustBeClusterPrefixedMessageReceivedRecordEntity(entity flow.Entity) ClusterPrefixedMessagesReceivedRecord {
   210  	c, ok := entity.(ClusterPrefixedMessagesReceivedRecord)
   211  	if !ok {
   212  		// sanity check
   213  		// This should never happen, because the cache only contains ClusterPrefixedMessagesReceivedRecord entities.
   214  		panic(fmt.Sprintf("invalid entity type, expected ClusterPrefixedMessagesReceivedRecord type, got: %T", entity))
   215  	}
   216  	return c
   217  }