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