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  }