github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/network/p2p/scoring/registry.go (about)

     1  package scoring
     2  
     3  import (
     4  	"fmt"
     5  	"math"
     6  	"time"
     7  
     8  	"github.com/go-playground/validator/v10"
     9  	"github.com/libp2p/go-libp2p/core/peer"
    10  	"github.com/rs/zerolog"
    11  	"go.uber.org/atomic"
    12  
    13  	"github.com/onflow/flow-go/engine/common/worker"
    14  	"github.com/onflow/flow-go/model/flow"
    15  	"github.com/onflow/flow-go/module"
    16  	"github.com/onflow/flow-go/module/component"
    17  	"github.com/onflow/flow-go/module/irrecoverable"
    18  	"github.com/onflow/flow-go/module/mempool/queue"
    19  	"github.com/onflow/flow-go/module/metrics"
    20  	"github.com/onflow/flow-go/network"
    21  	"github.com/onflow/flow-go/network/p2p"
    22  	netcache "github.com/onflow/flow-go/network/p2p/cache"
    23  	p2pconfig "github.com/onflow/flow-go/network/p2p/config"
    24  	p2plogging "github.com/onflow/flow-go/network/p2p/logging"
    25  	p2pmsg "github.com/onflow/flow-go/network/p2p/message"
    26  	"github.com/onflow/flow-go/utils/logging"
    27  )
    28  
    29  const (
    30  	// NotificationSilencedMsg log messages for silenced notifications
    31  	NotificationSilencedMsg = "ignoring invalid control message notification for peer during silence period"
    32  )
    33  
    34  type SpamRecordInitFunc func() p2p.GossipSubSpamRecord
    35  
    36  // GossipSubAppSpecificScoreRegistry is the registry for the application specific score of peers in the GossipSub protocol.
    37  // The application specific score is part of the overall score of a peer, and is used to determine the peer's score based
    38  // on its behavior related to the application (Flow protocol).
    39  // This registry holds the view of the local peer of the application specific score of other peers in the network based
    40  // on what it has observed from the network.
    41  // Similar to the GossipSub score, the application specific score is meant to be private to the local peer, and is not
    42  // shared with other peers in the network.
    43  type GossipSubAppSpecificScoreRegistry struct {
    44  	component.Component
    45  	logger     zerolog.Logger
    46  	idProvider module.IdentityProvider
    47  
    48  	// spamScoreCache currently only holds the control message misbehaviour penalty (spam related penalty).
    49  	spamScoreCache p2p.GossipSubSpamRecordCache
    50  
    51  	penalty p2pconfig.MisbehaviourPenalties
    52  
    53  	// getDuplicateMessageCount callback used to get a gauge of the number of duplicate messages detected for each peer.
    54  	getDuplicateMessageCount func(id peer.ID) float64
    55  
    56  	validator p2p.SubscriptionValidator
    57  
    58  	// scoreTTL is the time to live of the application specific score of a peer; the registry keeps a cached copy of the
    59  	// application specific score of a peer for this duration. When the duration expires, the application specific score
    60  	// of the peer is updated asynchronously. As long as the update is in progress, the cached copy of the application
    61  	// specific score of the peer is used even if it is expired.
    62  	scoreTTL time.Duration
    63  
    64  	// appScoreCache is a cache that stores the application specific score of peers.
    65  	appScoreCache p2p.GossipSubApplicationSpecificScoreCache
    66  
    67  	// appScoreUpdateWorkerPool is the worker pool for handling the application specific score update of peers in a non-blocking way.
    68  	appScoreUpdateWorkerPool  *worker.Pool[peer.ID]
    69  	invCtrlMsgNotifWorkerPool *worker.Pool[*p2p.InvCtrlMsgNotif]
    70  
    71  	appSpecificScoreParams    p2pconfig.ApplicationSpecificScoreParameters
    72  	duplicateMessageThreshold float64
    73  	collector                 module.GossipSubScoringRegistryMetrics
    74  
    75  	// silencePeriodDuration duration that the startup silence period will last, during which nodes will not be penalized
    76  	silencePeriodDuration time.Duration
    77  	// silencePeriodStartTime time that the silence period begins, this is the time that the registry is started by the node.
    78  	silencePeriodStartTime time.Time
    79  	// silencePeriodElapsed atomic bool that stores a bool flag which indicates if the silence period is over or not.
    80  	silencePeriodElapsed *atomic.Bool
    81  }
    82  
    83  // GossipSubAppSpecificScoreRegistryConfig is the configuration for the GossipSubAppSpecificScoreRegistry.
    84  // Configurations are the "union of parameters and other components" that are used to compute or build components that compute or maintain the application specific score of peers.
    85  type GossipSubAppSpecificScoreRegistryConfig struct {
    86  	Parameters p2pconfig.AppSpecificScoreParameters `validate:"required"`
    87  
    88  	Logger zerolog.Logger `validate:"required"`
    89  
    90  	// Validator is the subscription validator used to validate the subscriptions of peers, and determine if a peer is
    91  	// authorized to subscribe to a topic.
    92  	Validator p2p.SubscriptionValidator `validate:"required"`
    93  
    94  	// Penalty encapsulates the penalty unit for each control message type misbehaviour.
    95  	Penalty p2pconfig.MisbehaviourPenalties `validate:"required"`
    96  
    97  	// IdProvider is the identity provider used to translate peer ids at the networking layer to Flow identifiers (if
    98  	// an authorized peer is found).
    99  	IdProvider module.IdentityProvider `validate:"required"`
   100  
   101  	// GetDuplicateMessageCount callback used to get a gauge of the number of duplicate messages detected for each peer.
   102  	GetDuplicateMessageCount func(id peer.ID) float64
   103  
   104  	// SpamRecordCacheFactory is a factory function that returns a new GossipSubSpamRecordCache. It is used to initialize the spamScoreCache.
   105  	// The cache is used to store the application specific penalty of peers.
   106  	SpamRecordCacheFactory func() p2p.GossipSubSpamRecordCache `validate:"required"`
   107  
   108  	// AppScoreCacheFactory is a factory function that returns a new GossipSubApplicationSpecificScoreCache. It is used to initialize the appScoreCache.
   109  	// The cache is used to store the application specific score of peers.
   110  	AppScoreCacheFactory func() p2p.GossipSubApplicationSpecificScoreCache `validate:"required"`
   111  
   112  	HeroCacheMetricsFactory metrics.HeroCacheMetricsFactory `validate:"required"`
   113  
   114  	NetworkingType network.NetworkingType `validate:"required"`
   115  
   116  	// ScoringRegistryStartupSilenceDuration defines the duration of time, after the node startup,
   117  	// during which the scoring registry remains inactive before penalizing nodes.
   118  	ScoringRegistryStartupSilenceDuration time.Duration
   119  
   120  	AppSpecificScoreParams p2pconfig.ApplicationSpecificScoreParameters `validate:"required"`
   121  
   122  	DuplicateMessageThreshold float64 `validate:"gt=0"`
   123  
   124  	Collector module.GossipSubScoringRegistryMetrics `validate:"required"`
   125  }
   126  
   127  // NewGossipSubAppSpecificScoreRegistry returns a new GossipSubAppSpecificScoreRegistry.
   128  // Args:
   129  //
   130  //	config: the config for the registry.
   131  //
   132  // Returns:
   133  //
   134  //	a new GossipSubAppSpecificScoreRegistry.
   135  //
   136  // error: if the configuration is invalid, an error is returned; any returned error is an irrecoverable error and indicates a bug or misconfiguration.
   137  func NewGossipSubAppSpecificScoreRegistry(config *GossipSubAppSpecificScoreRegistryConfig) (*GossipSubAppSpecificScoreRegistry, error) {
   138  	if err := validator.New().Struct(config); err != nil {
   139  		return nil, fmt.Errorf("invalid config: %w", err)
   140  	}
   141  
   142  	lg := config.Logger.With().Str("module", "app_score_registry").Logger()
   143  
   144  	reg := &GossipSubAppSpecificScoreRegistry{
   145  		logger:                    config.Logger.With().Str("module", "app_score_registry").Logger(),
   146  		getDuplicateMessageCount:  config.GetDuplicateMessageCount,
   147  		spamScoreCache:            config.SpamRecordCacheFactory(),
   148  		appScoreCache:             config.AppScoreCacheFactory(),
   149  		penalty:                   config.Penalty,
   150  		validator:                 config.Validator,
   151  		idProvider:                config.IdProvider,
   152  		scoreTTL:                  config.Parameters.ScoreTTL,
   153  		silencePeriodDuration:     config.ScoringRegistryStartupSilenceDuration,
   154  		silencePeriodElapsed:      atomic.NewBool(false),
   155  		appSpecificScoreParams:    config.AppSpecificScoreParams,
   156  		duplicateMessageThreshold: config.DuplicateMessageThreshold,
   157  		collector:                 config.Collector,
   158  	}
   159  
   160  	appSpecificScore := queue.NewHeroStore(config.Parameters.ScoreUpdateRequestQueueSize,
   161  		lg.With().Str("component", "app_specific_score_update").Logger(),
   162  		metrics.GossipSubAppSpecificScoreUpdateQueueMetricFactory(config.HeroCacheMetricsFactory, config.NetworkingType))
   163  	reg.appScoreUpdateWorkerPool = worker.NewWorkerPoolBuilder[peer.ID](lg.With().Str("component", "app_specific_score_update_worker_pool").Logger(), appSpecificScore,
   164  		reg.processAppSpecificScoreUpdateWork).Build()
   165  
   166  	invalidCtrlMsgNotificationStore := queue.NewHeroStore(config.Parameters.InvalidControlMessageNotificationQueueSize,
   167  		lg.With().Str("component", "invalid_control_message_notification_queue").Logger(),
   168  		metrics.RpcInspectorNotificationQueueMetricFactory(config.HeroCacheMetricsFactory, config.NetworkingType),
   169  		queue.WithMessageEntityFactory(queue.NewMessageEntityWithNonce))
   170  	reg.invCtrlMsgNotifWorkerPool = worker.NewWorkerPoolBuilder[*p2p.InvCtrlMsgNotif](lg, invalidCtrlMsgNotificationStore, reg.handleMisbehaviourReport).Build()
   171  
   172  	builder := component.NewComponentManagerBuilder()
   173  	builder.AddWorker(func(ctx irrecoverable.SignalerContext, ready component.ReadyFunc) {
   174  		reg.logger.Info().Msg("starting subscription validator")
   175  		reg.validator.Start(ctx)
   176  		select {
   177  		case <-ctx.Done():
   178  			reg.logger.Warn().Msg("aborting subscription validator startup, context cancelled")
   179  		case <-reg.validator.Ready():
   180  			reg.logger.Info().Msg("subscription validator started")
   181  			ready()
   182  			reg.logger.Info().Msg("subscription validator is ready")
   183  		}
   184  		<-ctx.Done()
   185  		reg.logger.Info().Msg("stopping subscription validator")
   186  		<-reg.validator.Done()
   187  		reg.logger.Info().Msg("subscription validator stopped")
   188  	}).AddWorker(func(parent irrecoverable.SignalerContext, ready component.ReadyFunc) {
   189  		if !reg.silencePeriodStartTime.IsZero() {
   190  			parent.Throw(fmt.Errorf("gossipsub scoring registry started more than once"))
   191  		}
   192  		reg.silencePeriodStartTime = time.Now()
   193  		ready()
   194  	}).AddWorker(reg.invCtrlMsgNotifWorkerPool.WorkerLogic()) // we must NOT have more than one worker for processing notifications; handling notifications are NOT idempotent.
   195  
   196  	for i := 0; i < config.Parameters.ScoreUpdateWorkerNum; i++ {
   197  		builder.AddWorker(reg.appScoreUpdateWorkerPool.WorkerLogic())
   198  	}
   199  
   200  	reg.Component = builder.Build()
   201  
   202  	return reg, nil
   203  }
   204  
   205  var _ p2p.GossipSubInvCtrlMsgNotifConsumer = (*GossipSubAppSpecificScoreRegistry)(nil)
   206  
   207  // AppSpecificScoreFunc returns the application specific score function that is called by the GossipSub protocol to determine the application specific score of a peer.
   208  // The application specific score is part of the overall score of a peer, and is used to determine the peer's score based
   209  // This function reads the application specific score of a peer from the cache, and if the penalty is not found in the cache, it computes it.
   210  // If the score is not found in the cache, it is computed and added to the cache.
   211  // Also if the score is expired, it is computed and added to the cache.
   212  // Returns:
   213  // - func(peer.ID) float64: the application specific score function.
   214  // Implementation must be thread-safe.
   215  func (r *GossipSubAppSpecificScoreRegistry) AppSpecificScoreFunc() func(peer.ID) float64 {
   216  	return func(pid peer.ID) float64 {
   217  		lg := r.logger.With().Str("remote_peer_id", p2plogging.PeerId(pid)).Logger()
   218  
   219  		// during startup silence period avoid penalizing nodes
   220  		if !r.afterSilencePeriod() {
   221  			lg.Trace().Msg("returning 0 app specific score penalty for node during silence period")
   222  			return 0
   223  		}
   224  
   225  		appSpecificScore, lastUpdated, ok := r.appScoreCache.Get(pid)
   226  		switch {
   227  		case !ok:
   228  			// record not found in the cache, or expired; submit a worker to update it.
   229  			submitted := r.appScoreUpdateWorkerPool.Submit(pid)
   230  			lg.Trace().
   231  				Bool("worker_submitted", submitted).
   232  				Msg("application specific score not found in cache, submitting worker to update it")
   233  			return 0 // in the mean time, return 0, which is a neutral score.
   234  		case time.Since(lastUpdated) > r.scoreTTL:
   235  			// record found in the cache, but expired; submit a worker to update it.
   236  			submitted := r.appScoreUpdateWorkerPool.Submit(pid)
   237  			lg.Trace().
   238  				Bool("worker_submitted", submitted).
   239  				Float64("app_specific_score", appSpecificScore).
   240  				Dur("score_ttl", r.scoreTTL).
   241  				Msg("application specific score expired, submitting worker to update it")
   242  			return appSpecificScore // in the mean time, return the expired score.
   243  		default:
   244  			// record found in the cache.
   245  			r.logger.Trace().
   246  				Float64("app_specific_score", appSpecificScore).
   247  				Msg("application specific score found in cache")
   248  			return appSpecificScore
   249  		}
   250  	}
   251  }
   252  
   253  // computeAppSpecificScore computes the application specific score of a peer.
   254  // The application specific score is computed based on the spam penalty, staking score, and subscription penalty.
   255  // The spam penalty is the penalty applied to the application specific score when a peer conducts a spamming misbehaviour.
   256  // The staking score is the reward/penalty applied to the application specific score when a peer is staked/unstaked.
   257  // The subscription penalty is the penalty applied to the application specific score when a peer is subscribed to a topic that it is not allowed to subscribe to based on its role.
   258  // Args:
   259  // - pid: the peer ID of the peer in the GossipSub protocol.
   260  // Returns:
   261  // - float64: the application specific score of the peer.
   262  func (r *GossipSubAppSpecificScoreRegistry) computeAppSpecificScore(pid peer.ID) float64 {
   263  	appSpecificScore := float64(0)
   264  
   265  	lg := r.logger.With().Str("peer_id", p2plogging.PeerId(pid)).Logger()
   266  	// (1) spam penalty: the penalty is applied to the application specific penalty when a peer conducts a spamming misbehaviour.
   267  	spamRecord, err, spamRecordExists := r.spamScoreCache.Get(pid)
   268  	if err != nil {
   269  		// the error is considered fatal as it means the cache is not working properly.
   270  		// we should not continue with the execution as it may lead to routing attack vulnerability.
   271  		r.logger.Fatal().Str("peer_id", p2plogging.PeerId(pid)).Err(err).Msg("could not get application specific penalty for peer")
   272  		return appSpecificScore // unreachable, but added to avoid proceeding with the execution if log level is changed.
   273  	}
   274  
   275  	if spamRecordExists {
   276  		lg = lg.With().Float64("spam_penalty", spamRecord.Penalty).Logger()
   277  		appSpecificScore += spamRecord.Penalty
   278  	}
   279  
   280  	// (2) staking score: for staked peers, a default positive reward is applied only if the peer has no penalty on spamming and subscription.
   281  	// for unknown peers a negative penalty is applied.
   282  	stakingScore, flowId, role := r.stakingScore(pid)
   283  	if stakingScore < 0 {
   284  		lg = lg.With().Float64("staking_penalty", stakingScore).Logger()
   285  		// staking penalty is applied right away.
   286  		appSpecificScore += stakingScore
   287  	}
   288  
   289  	if stakingScore >= 0 {
   290  		// (3) subscription penalty: the subscription penalty is applied to the application specific penalty when a
   291  		// peer is subscribed to a topic that it is not allowed to subscribe to based on its role.
   292  		// Note: subscription penalty can be considered only for staked peers, for non-staked peers, we cannot
   293  		// determine the role of the peer.
   294  		subscriptionPenalty := r.subscriptionPenalty(pid, flowId, role)
   295  		lg = lg.With().Float64("subscription_penalty", subscriptionPenalty).Logger()
   296  		if subscriptionPenalty < 0 {
   297  			appSpecificScore += subscriptionPenalty
   298  		}
   299  	}
   300  
   301  	// (4) duplicate messages penalty: the duplicate messages penalty is applied to the application specific penalty as long
   302  	// as the number of duplicate messages detected for a peer is greater than 0. This counter is decayed overtime, thus sustained
   303  	// good behavior should eventually lead to the duplicate messages penalty applied being 0.
   304  	duplicateMessagesPenalty := r.duplicateMessagesPenalty(pid)
   305  	if duplicateMessagesPenalty < 0 {
   306  		lg = lg.With().Float64("duplicate_messages_penalty", duplicateMessagesPenalty).Logger()
   307  		appSpecificScore += duplicateMessagesPenalty
   308  	}
   309  
   310  	// (5) staking reward: for staked peers, a default positive reward is applied only if the peer has no penalty on spamming and subscription.
   311  	if stakingScore > 0 && appSpecificScore == float64(0) {
   312  		lg = lg.With().Float64("staking_reward", stakingScore).Logger()
   313  		appSpecificScore += stakingScore
   314  	}
   315  
   316  	lg.Trace().
   317  		Float64("total_app_specific_score", appSpecificScore).
   318  		Msg("application specific score computed")
   319  	return appSpecificScore
   320  }
   321  
   322  // processMisbehaviorReport is the worker function that is called by the worker pool to update the application specific score of a peer.
   323  // The function is called in a non-blocking way, and the worker pool is used to limit the number of concurrent executions of the function.
   324  // Args:
   325  // - pid: the peer ID of the peer in the GossipSub protocol.
   326  // Returns:
   327  // - error: an error if the update failed; any returned error is an irrecoverable error and indicates a bug or misconfiguration.
   328  func (r *GossipSubAppSpecificScoreRegistry) processAppSpecificScoreUpdateWork(p peer.ID) error {
   329  	appSpecificScore := r.computeAppSpecificScore(p)
   330  	err := r.appScoreCache.AdjustWithInit(p, appSpecificScore, time.Now())
   331  	if err != nil {
   332  		// the error is considered fatal as it means the cache is not working properly.
   333  		return fmt.Errorf("could not add application specific score %f for peer to cache: %w", appSpecificScore, err)
   334  	}
   335  	r.logger.Trace().
   336  		Str("remote_peer_id", p2plogging.PeerId(p)).
   337  		Float64("app_specific_score", appSpecificScore).
   338  		Msg("application specific score computed and cache updated")
   339  	return nil
   340  }
   341  
   342  func (r *GossipSubAppSpecificScoreRegistry) stakingScore(pid peer.ID) (float64, flow.Identifier, flow.Role) {
   343  	lg := r.logger.With().Str("peer_id", p2plogging.PeerId(pid)).Logger()
   344  
   345  	// checks if peer has a valid Flow protocol identity.
   346  	flowId, err := HasValidFlowIdentity(r.idProvider, pid)
   347  	if err != nil {
   348  		lg.Error().
   349  			Err(err).
   350  			Bool(logging.KeySuspicious, true).
   351  			Msg("invalid peer identity, penalizing peer")
   352  		return r.appSpecificScoreParams.UnknownIdentityPenalty, flow.Identifier{}, 0
   353  	}
   354  
   355  	lg = lg.With().
   356  		Hex("flow_id", logging.ID(flowId.NodeID)).
   357  		Str("role", flowId.Role.String()).
   358  		Logger()
   359  
   360  	// checks if peer is an access node, and if so, pushes it to the
   361  	// edges of the network by giving the minimum penalty.
   362  	if flowId.Role == flow.RoleAccess {
   363  		lg.Trace().
   364  			Msg("pushing access node to edge by penalizing with minimum penalty value")
   365  		return r.appSpecificScoreParams.MinAppSpecificPenalty, flowId.NodeID, flowId.Role
   366  	}
   367  
   368  	lg.Trace().
   369  		Msg("rewarding well-behaved non-access node peer with maximum reward value")
   370  
   371  	return r.appSpecificScoreParams.StakedIdentityReward, flowId.NodeID, flowId.Role
   372  }
   373  
   374  func (r *GossipSubAppSpecificScoreRegistry) subscriptionPenalty(pid peer.ID, flowId flow.Identifier, role flow.Role) float64 {
   375  	// checks if peer has any subscription violation.
   376  	if err := r.validator.CheckSubscribedToAllowedTopics(pid, role); err != nil {
   377  		r.logger.Warn().
   378  			Err(err).
   379  			Str("peer_id", p2plogging.PeerId(pid)).
   380  			Hex("flow_id", logging.ID(flowId)).
   381  			Bool(logging.KeySuspicious, true).
   382  			Msg("invalid subscription detected, penalizing peer")
   383  		return r.appSpecificScoreParams.InvalidSubscriptionPenalty
   384  	}
   385  
   386  	return 0
   387  }
   388  
   389  // duplicateMessagesPenalty returns the duplicate message penalty for a peer. A penalty is only returned if the duplicate
   390  // message count for a peer exceeds the DefaultDuplicateMessageThreshold. A penalty is applied for the amount of duplicate
   391  // messages above the DefaultDuplicateMessageThreshold.
   392  func (r *GossipSubAppSpecificScoreRegistry) duplicateMessagesPenalty(pid peer.ID) float64 {
   393  	duplicateMessageCount, duplicateMessagePenalty := 0.0, 0.0
   394  	defer func() {
   395  		r.collector.DuplicateMessagesCounts(duplicateMessageCount)
   396  		r.collector.DuplicateMessagePenalties(duplicateMessagePenalty)
   397  	}()
   398  
   399  	duplicateMessageCount = r.getDuplicateMessageCount(pid)
   400  	if duplicateMessageCount > r.duplicateMessageThreshold {
   401  		duplicateMessagePenalty = (duplicateMessageCount - r.duplicateMessageThreshold) * r.appSpecificScoreParams.DuplicateMessagePenalty
   402  		if duplicateMessagePenalty < r.appSpecificScoreParams.MaxAppSpecificPenalty {
   403  			return r.appSpecificScoreParams.MaxAppSpecificPenalty
   404  		}
   405  	}
   406  	return duplicateMessagePenalty
   407  }
   408  
   409  // OnInvalidControlMessageNotification is called when a new invalid control message notification is distributed.
   410  // Any error on consuming event must handle internally.
   411  // The implementation must be concurrency safe, but can be blocking.
   412  // Note: there is no real-time guarantee on processing the notification.
   413  func (r *GossipSubAppSpecificScoreRegistry) OnInvalidControlMessageNotification(notification *p2p.InvCtrlMsgNotif) {
   414  	lg := r.logger.With().Str("peer_id", p2plogging.PeerId(notification.PeerID)).Logger()
   415  	if ok := r.invCtrlMsgNotifWorkerPool.Submit(notification); !ok {
   416  		// we use a queue with a fixed size, so this can happen when queue is full or when the notification is duplicate.
   417  		// TODO: we have to add a metric for this case.
   418  		// TODO: we should not have deduplication for this case, as we need to penalize the peer for each misbehaviour, we need to add a nonce to the notification.
   419  		lg.Warn().Msg("gossipsub rpc inspector notification queue is full or notification is duplicate, discarding notification")
   420  	}
   421  	lg.Trace().Msg("gossipsub rpc inspector notification submitted to the queue")
   422  }
   423  
   424  // handleMisbehaviourReport is the worker function that is called by the worker pool to handle the misbehaviour report of a peer.
   425  // The function is called in a non-blocking way, and the worker pool is used to limit the number of concurrent executions of the function.
   426  // Args:
   427  // - notification: the notification of the misbehaviour report of a peer.
   428  // Returns:
   429  // - error: an error if the update failed; any returned error is an irrecoverable error and indicates a bug or misconfiguration.
   430  func (r *GossipSubAppSpecificScoreRegistry) handleMisbehaviourReport(notification *p2p.InvCtrlMsgNotif) error {
   431  	// we use mutex to ensure the method is concurrency safe.
   432  	lg := r.logger.With().
   433  		Err(notification.Error).
   434  		Str("misbehavior_type", notification.MsgType.String()).Logger()
   435  
   436  	// during startup silence period avoid penalizing nodes, ignore all notifications
   437  	if !r.afterSilencePeriod() {
   438  		lg.Trace().Msg("ignoring invalid control message notification for peer during silence period")
   439  		return nil
   440  	}
   441  
   442  	record, err := r.spamScoreCache.Adjust(notification.PeerID, func(record p2p.GossipSubSpamRecord) p2p.GossipSubSpamRecord {
   443  		penalty := 0.0
   444  		switch notification.MsgType {
   445  		case p2pmsg.CtrlMsgGraft:
   446  			penalty += r.penalty.GraftMisbehaviour
   447  		case p2pmsg.CtrlMsgPrune:
   448  			penalty += r.penalty.PruneMisbehaviour
   449  		case p2pmsg.CtrlMsgIHave:
   450  			penalty += r.penalty.IHaveMisbehaviour
   451  		case p2pmsg.CtrlMsgIWant:
   452  			penalty += r.penalty.IWantMisbehaviour
   453  		case p2pmsg.RpcPublishMessage:
   454  			penalty += r.penalty.PublishMisbehaviour
   455  		case p2pmsg.CtrlMsgRPC:
   456  			penalty += r.penalty.PublishMisbehaviour
   457  		default:
   458  			// the error is considered fatal as it means that we have an unsupported misbehaviour type, we should crash the node to prevent routing attack vulnerability.
   459  			lg.Fatal().Str("misbehavior_type", notification.MsgType.String()).Msg("unknown misbehaviour type")
   460  		}
   461  
   462  		// reduce penalty for cluster prefixed topics allowing nodes that are potentially behind to catch up
   463  		if notification.TopicType == p2p.CtrlMsgTopicTypeClusterPrefixed {
   464  			penalty *= r.penalty.ClusterPrefixedReductionFactor
   465  		}
   466  
   467  		record.Penalty += penalty
   468  
   469  		return record
   470  	})
   471  	if err != nil {
   472  		// any returned error from adjust is non-recoverable and fatal, we crash the node.
   473  		lg.Fatal().Err(err).Msg("could not adjust application specific penalty for peer")
   474  	}
   475  
   476  	lg.Debug().
   477  		Float64("spam_record_penalty", record.Penalty).
   478  		Msg("applied misbehaviour penalty and updated application specific penalty")
   479  
   480  	return nil
   481  }
   482  
   483  // afterSilencePeriod returns true if registry silence period is over, false otherwise.
   484  func (r *GossipSubAppSpecificScoreRegistry) afterSilencePeriod() bool {
   485  	if !r.silencePeriodElapsed.Load() {
   486  		if time.Since(r.silencePeriodStartTime) > r.silencePeriodDuration {
   487  			r.silencePeriodElapsed.Store(true)
   488  			return true
   489  		}
   490  		return false
   491  	}
   492  	return true
   493  }
   494  
   495  // DefaultDecayFunction is the default decay function that is used to decay the application specific penalty of a peer.
   496  // It is used if no decay function is provided in the configuration.
   497  // It decays the application specific penalty of a peer if it is negative.
   498  func DefaultDecayFunction(cfg p2pconfig.SpamRecordCacheDecay) netcache.PreprocessorFunc {
   499  	return func(record p2p.GossipSubSpamRecord, lastUpdated time.Time) (p2p.GossipSubSpamRecord, error) {
   500  		if record.Penalty >= 0 {
   501  			// no need to decay the penalty if it is positive, the reason is currently the app specific penalty
   502  			// is only used to penalize peers. Hence, when there is no reward, there is no need to decay the positive penalty, as
   503  			// no node can accumulate a positive penalty.
   504  			return record, nil
   505  		}
   506  
   507  		if record.Penalty > cfg.SkipDecayThreshold {
   508  			// penalty is negative but greater than the threshold, we set it to 0.
   509  			record.Penalty = 0
   510  			record.Decay = cfg.MaximumSpamPenaltyDecayFactor
   511  			record.LastDecayAdjustment = time.Time{}
   512  			return record, nil
   513  		}
   514  
   515  		// penalty is negative and below the threshold, we decay it.
   516  		penalty, err := GeometricDecay(record.Penalty, record.Decay, lastUpdated)
   517  		if err != nil {
   518  			return record, fmt.Errorf("could not decay application specific penalty: %w", err)
   519  		}
   520  		record.Penalty = penalty
   521  
   522  		if record.Penalty <= cfg.PenaltyDecaySlowdownThreshold {
   523  			if time.Since(record.LastDecayAdjustment) > cfg.PenaltyDecayEvaluationPeriod || record.LastDecayAdjustment.IsZero() {
   524  				// reduces the decay speed flooring at MinimumSpamRecordDecaySpeed
   525  				record.Decay = math.Min(record.Decay+cfg.DecayRateReductionFactor, cfg.MinimumSpamPenaltyDecayFactor)
   526  				record.LastDecayAdjustment = time.Now()
   527  			}
   528  		}
   529  		return record, nil
   530  	}
   531  }
   532  
   533  // InitAppScoreRecordStateFunc returns a callback that initializes the gossipsub spam record state for a peer.
   534  // Returns:
   535  //   - a func that returns a gossipsub spam record with the default decay value and 0 penalty.
   536  func InitAppScoreRecordStateFunc(maximumSpamPenaltyDecayFactor float64) func() p2p.GossipSubSpamRecord {
   537  	return func() p2p.GossipSubSpamRecord {
   538  		return p2p.GossipSubSpamRecord{
   539  			Decay:               maximumSpamPenaltyDecayFactor,
   540  			Penalty:             0,
   541  			LastDecayAdjustment: time.Now(),
   542  		}
   543  	}
   544  }