github.com/onflow/flow-go@v0.33.17/network/p2p/scoring/score_option.go (about)

     1  package scoring
     2  
     3  import (
     4  	"fmt"
     5  	"time"
     6  
     7  	pubsub "github.com/libp2p/go-libp2p-pubsub"
     8  	"github.com/libp2p/go-libp2p/core/peer"
     9  	"github.com/rs/zerolog"
    10  
    11  	"github.com/onflow/flow-go/module"
    12  	"github.com/onflow/flow-go/module/component"
    13  	"github.com/onflow/flow-go/module/irrecoverable"
    14  	"github.com/onflow/flow-go/module/metrics"
    15  	"github.com/onflow/flow-go/network"
    16  	"github.com/onflow/flow-go/network/channels"
    17  	"github.com/onflow/flow-go/network/p2p"
    18  	netcache "github.com/onflow/flow-go/network/p2p/cache"
    19  	p2pconfig "github.com/onflow/flow-go/network/p2p/config"
    20  	"github.com/onflow/flow-go/network/p2p/scoring/internal"
    21  	"github.com/onflow/flow-go/network/p2p/utils"
    22  	"github.com/onflow/flow-go/utils/logging"
    23  )
    24  
    25  // ScoreOption is a functional option for configuring the peer scoring system.
    26  // TODO: rename it to ScoreManager.
    27  type ScoreOption struct {
    28  	component.Component
    29  	logger zerolog.Logger
    30  
    31  	peerScoreParams         *pubsub.PeerScoreParams
    32  	peerThresholdParams     *pubsub.PeerScoreThresholds
    33  	defaultTopicScoreParams *pubsub.TopicScoreParams
    34  	validator               p2p.SubscriptionValidator
    35  	appScoreFunc            func(peer.ID) float64
    36  }
    37  
    38  type ScoreOptionConfig struct {
    39  	logger                           zerolog.Logger
    40  	params                           p2pconfig.ScoringParameters
    41  	provider                         module.IdentityProvider
    42  	heroCacheMetricsFactory          metrics.HeroCacheMetricsFactory
    43  	appScoreFunc                     func(peer.ID) float64
    44  	topicParams                      []func(map[string]*pubsub.TopicScoreParams)
    45  	registerNotificationConsumerFunc func(p2p.GossipSubInvCtrlMsgNotifConsumer)
    46  	networkingType                   network.NetworkingType
    47  }
    48  
    49  // NewScoreOptionConfig creates a new configuration for the GossipSub peer scoring option.
    50  // Args:
    51  // - logger: the logger to use.
    52  // - hcMetricsFactory: HeroCache metrics factory to create metrics for the scoring-related caches.
    53  // - idProvider: the identity provider to use.
    54  // - networkingType: the networking type to use, public or private.
    55  // Returns:
    56  // - a new configuration for the GossipSub peer scoring option.
    57  func NewScoreOptionConfig(logger zerolog.Logger,
    58  	params p2pconfig.ScoringParameters,
    59  	hcMetricsFactory metrics.HeroCacheMetricsFactory,
    60  	idProvider module.IdentityProvider,
    61  	networkingType network.NetworkingType) *ScoreOptionConfig {
    62  	return &ScoreOptionConfig{
    63  		logger:                  logger.With().Str("module", "pubsub_score_option").Logger(),
    64  		provider:                idProvider,
    65  		params:                  params,
    66  		heroCacheMetricsFactory: hcMetricsFactory,
    67  		topicParams:             make([]func(map[string]*pubsub.TopicScoreParams), 0),
    68  		networkingType:          networkingType,
    69  	}
    70  }
    71  
    72  // OverrideAppSpecificScoreFunction sets the app specific penalty function for the penalty option.
    73  // It is used to calculate the app specific penalty of a peer.
    74  // If the app specific penalty function is not set, the default one is used.
    75  // Note that it is always safer to use the default one, unless you know what you are doing.
    76  // It is safe to call this method multiple times, the last call will be used.
    77  func (c *ScoreOptionConfig) OverrideAppSpecificScoreFunction(appSpecificScoreFunction func(peer.ID) float64) {
    78  	c.appScoreFunc = appSpecificScoreFunction
    79  }
    80  
    81  // OverrideTopicScoreParams overrides the topic score parameters for the given topic.
    82  // It is used to override the default topic score parameters for a specific topic.
    83  // If the topic score parameters are not set, the default ones will be used.
    84  func (c *ScoreOptionConfig) OverrideTopicScoreParams(topic channels.Topic, topicScoreParams *pubsub.TopicScoreParams) {
    85  	c.topicParams = append(c.topicParams, func(topics map[string]*pubsub.TopicScoreParams) {
    86  		topics[topic.String()] = topicScoreParams
    87  	})
    88  }
    89  
    90  // SetRegisterNotificationConsumerFunc sets the function to register the notification consumer for the penalty option.
    91  // ScoreOption uses this function to register the notification consumer for the pubsub system so that it can receive
    92  // notifications of invalid control messages.
    93  func (c *ScoreOptionConfig) SetRegisterNotificationConsumerFunc(f func(p2p.GossipSubInvCtrlMsgNotifConsumer)) {
    94  	c.registerNotificationConsumerFunc = f
    95  }
    96  
    97  // NewScoreOption creates a new penalty option with the given configuration.
    98  func NewScoreOption(cfg *ScoreOptionConfig, provider p2p.SubscriptionProvider) (*ScoreOption, error) {
    99  	throttledSampler := logging.BurstSampler(cfg.params.PeerScoring.Protocol.MaxDebugLogs, time.Second)
   100  	logger := cfg.logger.With().
   101  		Str("module", "pubsub_score_option").
   102  		Logger().
   103  		Sample(zerolog.LevelSampler{
   104  			TraceSampler: throttledSampler,
   105  			DebugSampler: throttledSampler,
   106  		})
   107  
   108  	validator := NewSubscriptionValidator(cfg.logger, provider)
   109  	scoreRegistry, err := NewGossipSubAppSpecificScoreRegistry(&GossipSubAppSpecificScoreRegistryConfig{
   110  		Logger:                  logger,
   111  		Penalty:                 cfg.params.ScoringRegistryParameters.MisbehaviourPenalties,
   112  		Validator:               validator,
   113  		IdProvider:              cfg.provider,
   114  		HeroCacheMetricsFactory: cfg.heroCacheMetricsFactory,
   115  		AppScoreCacheFactory: func() p2p.GossipSubApplicationSpecificScoreCache {
   116  			collector := metrics.NewGossipSubApplicationSpecificScoreCacheMetrics(cfg.heroCacheMetricsFactory, cfg.networkingType)
   117  			return internal.NewAppSpecificScoreCache(cfg.params.ScoringRegistryParameters.SpamRecordCache.CacheSize, cfg.logger, collector)
   118  		},
   119  		SpamRecordCacheFactory: func() p2p.GossipSubSpamRecordCache {
   120  			collector := metrics.GossipSubSpamRecordCacheMetricsFactory(cfg.heroCacheMetricsFactory, cfg.networkingType)
   121  			return netcache.NewGossipSubSpamRecordCache(cfg.params.ScoringRegistryParameters.SpamRecordCache.CacheSize, cfg.logger, collector,
   122  				InitAppScoreRecordStateFunc(cfg.params.ScoringRegistryParameters.SpamRecordCache.Decay.MaximumSpamPenaltyDecayFactor),
   123  				DefaultDecayFunction(cfg.params.ScoringRegistryParameters.SpamRecordCache.Decay))
   124  		},
   125  		Parameters:             cfg.params.ScoringRegistryParameters.AppSpecificScore,
   126  		NetworkingType:         cfg.networkingType,
   127  		AppSpecificScoreParams: cfg.params.PeerScoring.Protocol.AppSpecificScore,
   128  	})
   129  	if err != nil {
   130  		return nil, fmt.Errorf("failed to create gossipsub app specific score registry: %w", err)
   131  	}
   132  
   133  	s := &ScoreOption{
   134  		logger:    logger,
   135  		validator: validator,
   136  		peerScoreParams: &pubsub.PeerScoreParams{
   137  			Topics: make(map[string]*pubsub.TopicScoreParams),
   138  			// we don't set all the parameters, so we skip the atomic validation.
   139  			// atomic validation fails initialization if any parameter is not set.
   140  			SkipAtomicValidation: cfg.params.PeerScoring.Internal.TopicParameters.SkipAtomicValidation,
   141  			// DecayInterval is the interval over which we decay the effect of past behavior, so that
   142  			// a good or bad behavior will not have a permanent effect on the penalty. It is also the interval
   143  			// that GossipSub uses to refresh the scores of all peers.
   144  			DecayInterval: cfg.params.PeerScoring.Internal.DecayInterval,
   145  			// DecayToZero defines the maximum value below which a peer scoring counter is reset to zero.
   146  			// This is to prevent the counter from decaying to a very small value.
   147  			// When a counter hits the DecayToZero threshold, it means that the peer did not exhibit the behavior
   148  			// for a long time, and we can reset the counter.
   149  			DecayToZero: cfg.params.PeerScoring.Internal.DecayToZero,
   150  			// AppSpecificWeight is the weight of the application specific penalty.
   151  			AppSpecificWeight: cfg.params.PeerScoring.Internal.AppSpecificScoreWeight,
   152  			// PenaltyThreshold is the threshold above which a peer is penalized for GossipSub-level misbehaviors.
   153  			BehaviourPenaltyThreshold: cfg.params.PeerScoring.Internal.Behaviour.PenaltyThreshold,
   154  			// PenaltyWeight is the weight of the GossipSub-level penalty.
   155  			BehaviourPenaltyWeight: cfg.params.PeerScoring.Internal.Behaviour.PenaltyWeight,
   156  			// PenaltyDecay is the decay of the GossipSub-level penalty (applied every decay interval).
   157  			BehaviourPenaltyDecay: cfg.params.PeerScoring.Internal.Behaviour.PenaltyDecay,
   158  		},
   159  		peerThresholdParams: &pubsub.PeerScoreThresholds{
   160  			GossipThreshold:             cfg.params.PeerScoring.Internal.Thresholds.Gossip,
   161  			PublishThreshold:            cfg.params.PeerScoring.Internal.Thresholds.Publish,
   162  			GraylistThreshold:           cfg.params.PeerScoring.Internal.Thresholds.Graylist,
   163  			AcceptPXThreshold:           cfg.params.PeerScoring.Internal.Thresholds.AcceptPX,
   164  			OpportunisticGraftThreshold: cfg.params.PeerScoring.Internal.Thresholds.OpportunisticGraft,
   165  		},
   166  		defaultTopicScoreParams: &pubsub.TopicScoreParams{
   167  			TopicWeight:                     cfg.params.PeerScoring.Internal.TopicParameters.TopicWeight,
   168  			SkipAtomicValidation:            cfg.params.PeerScoring.Internal.TopicParameters.SkipAtomicValidation,
   169  			InvalidMessageDeliveriesWeight:  cfg.params.PeerScoring.Internal.TopicParameters.InvalidMessageDeliveriesWeight,
   170  			InvalidMessageDeliveriesDecay:   cfg.params.PeerScoring.Internal.TopicParameters.InvalidMessageDeliveriesDecay,
   171  			TimeInMeshQuantum:               cfg.params.PeerScoring.Internal.TopicParameters.TimeInMeshQuantum,
   172  			MeshMessageDeliveriesWeight:     cfg.params.PeerScoring.Internal.TopicParameters.MeshDeliveriesWeight,
   173  			MeshMessageDeliveriesDecay:      cfg.params.PeerScoring.Internal.TopicParameters.MeshMessageDeliveriesDecay,
   174  			MeshMessageDeliveriesCap:        cfg.params.PeerScoring.Internal.TopicParameters.MeshMessageDeliveriesCap,
   175  			MeshMessageDeliveriesThreshold:  cfg.params.PeerScoring.Internal.TopicParameters.MeshMessageDeliveryThreshold,
   176  			MeshMessageDeliveriesWindow:     cfg.params.PeerScoring.Internal.TopicParameters.MeshMessageDeliveriesWindow,
   177  			MeshMessageDeliveriesActivation: cfg.params.PeerScoring.Internal.TopicParameters.MeshMessageDeliveryActivation,
   178  		},
   179  		appScoreFunc: scoreRegistry.AppSpecificScoreFunc(),
   180  	}
   181  
   182  	// set the app specific penalty function for the penalty option
   183  	// if the app specific penalty function is not set, use the default one
   184  	if cfg.appScoreFunc != nil {
   185  		s.appScoreFunc = cfg.appScoreFunc
   186  		s.logger.
   187  			Warn().
   188  			Str(logging.KeyNetworkingSecurity, "true").
   189  			Msg("app specific score function is overridden, should never happen in production")
   190  	}
   191  
   192  	if cfg.params.PeerScoring.Internal.DecayInterval > 0 && cfg.params.PeerScoring.Internal.DecayInterval != s.peerScoreParams.DecayInterval {
   193  		// overrides the default decay interval if the decay interval is set.
   194  		s.peerScoreParams.DecayInterval = cfg.params.PeerScoring.Internal.DecayInterval
   195  		s.logger.
   196  			Warn().
   197  			Str(logging.KeyNetworkingSecurity, "true").
   198  			Dur("decay_interval_ms", cfg.params.PeerScoring.Internal.DecayInterval).
   199  			Msg("decay interval is overridden, should never happen in production")
   200  	}
   201  
   202  	// registers the score registry as the consumer of the invalid control message notifications
   203  	if cfg.registerNotificationConsumerFunc != nil {
   204  		cfg.registerNotificationConsumerFunc(scoreRegistry)
   205  	}
   206  
   207  	s.peerScoreParams.AppSpecificScore = s.appScoreFunc
   208  
   209  	// apply the topic penalty parameters if any.
   210  	for _, topicParams := range cfg.topicParams {
   211  		topicParams(s.peerScoreParams.Topics)
   212  	}
   213  
   214  	s.Component = component.NewComponentManagerBuilder().AddWorker(func(ctx irrecoverable.SignalerContext, ready component.ReadyFunc) {
   215  		s.logger.Info().Msg("starting score registry")
   216  		scoreRegistry.Start(ctx)
   217  		select {
   218  		case <-ctx.Done():
   219  			s.logger.Warn().Msg("stopping score registry; context done")
   220  		case <-scoreRegistry.Ready():
   221  			s.logger.Info().Msg("score registry started")
   222  			ready()
   223  			s.logger.Info().Msg("score registry ready")
   224  		}
   225  
   226  		<-ctx.Done()
   227  		s.logger.Info().Msg("stopping score registry")
   228  		<-scoreRegistry.Done()
   229  		s.logger.Info().Msg("score registry stopped")
   230  	}).Build()
   231  
   232  	return s, nil
   233  }
   234  
   235  func (s *ScoreOption) BuildFlowPubSubScoreOption() (*pubsub.PeerScoreParams, *pubsub.PeerScoreThresholds) {
   236  	s.logger.Info().
   237  		Float64("gossip_threshold", s.peerThresholdParams.GossipThreshold).
   238  		Float64("publish_threshold", s.peerThresholdParams.PublishThreshold).
   239  		Float64("graylist_threshold", s.peerThresholdParams.GraylistThreshold).
   240  		Float64("accept_px_threshold", s.peerThresholdParams.AcceptPXThreshold).
   241  		Float64("opportunistic_graft_threshold", s.peerThresholdParams.OpportunisticGraftThreshold).
   242  		Msg("pubsub score thresholds are set")
   243  
   244  	for topic, topicParams := range s.peerScoreParams.Topics {
   245  		topicScoreParamLogger := utils.TopicScoreParamsLogger(s.logger, topic, topicParams)
   246  		topicScoreParamLogger.Info().
   247  			Msg("pubsub score topic parameters are set for topic")
   248  	}
   249  
   250  	return s.peerScoreParams, s.peerThresholdParams
   251  }
   252  
   253  // TopicScoreParams returns the topic score parameters for the given topic. If the topic
   254  // score parameters are not set, it returns the default topic score parameters.
   255  // The custom topic parameters are set at the initialization of the score option.
   256  // Args:
   257  // - topic: the topic for which the score parameters are requested.
   258  // Returns:
   259  //   - the topic score parameters for the given topic, or the default topic score parameters if
   260  //     the topic score parameters are not set.
   261  func (s *ScoreOption) TopicScoreParams(topic *pubsub.Topic) *pubsub.TopicScoreParams {
   262  	params, exists := s.peerScoreParams.Topics[topic.String()]
   263  	if !exists {
   264  		return s.defaultTopicScoreParams
   265  	}
   266  	return params
   267  }