github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/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  	appScoreRegistry        *GossipSubAppSpecificScoreRegistry
    37  }
    38  
    39  type ScoreOptionConfig struct {
    40  	logger                          zerolog.Logger
    41  	params                          p2pconfig.ScoringParameters
    42  	provider                        module.IdentityProvider
    43  	heroCacheMetricsFactory         metrics.HeroCacheMetricsFactory
    44  	appScoreFunc                    func(peer.ID) float64
    45  	topicParams                     []func(map[string]*pubsub.TopicScoreParams)
    46  	getDuplicateMessageCount        func(id peer.ID) float64
    47  	scoringRegistryMetricsCollector module.GossipSubScoringRegistryMetrics
    48  	networkingType                  network.NetworkingType
    49  }
    50  
    51  // NewScoreOptionConfig creates a new configuration for the GossipSub peer scoring option.
    52  // Args:
    53  // - logger: the logger to use.
    54  // - hcMetricsFactory: HeroCache metrics factory to create metrics for the scoring-related caches.
    55  // - idProvider: the identity provider to use.
    56  // - networkingType: the networking type to use, public or private.
    57  // Returns:
    58  // - a new configuration for the GossipSub peer scoring option.
    59  func NewScoreOptionConfig(logger zerolog.Logger,
    60  	params p2pconfig.ScoringParameters,
    61  	hcMetricsFactory metrics.HeroCacheMetricsFactory,
    62  	scoringRegistryMetricsCollector module.GossipSubScoringRegistryMetrics,
    63  	idProvider module.IdentityProvider,
    64  	getDuplicateMessageCount func(id peer.ID) float64,
    65  	networkingType network.NetworkingType) *ScoreOptionConfig {
    66  	return &ScoreOptionConfig{
    67  		logger:                          logger.With().Str("module", "pubsub_score_option").Logger(),
    68  		provider:                        idProvider,
    69  		params:                          params,
    70  		heroCacheMetricsFactory:         hcMetricsFactory,
    71  		topicParams:                     make([]func(map[string]*pubsub.TopicScoreParams), 0),
    72  		networkingType:                  networkingType,
    73  		getDuplicateMessageCount:        getDuplicateMessageCount,
    74  		scoringRegistryMetricsCollector: scoringRegistryMetricsCollector,
    75  	}
    76  }
    77  
    78  // OverrideAppSpecificScoreFunction sets the app specific penalty function for the penalty option.
    79  // It is used to calculate the app specific penalty of a peer.
    80  // If the app specific penalty function is not set, the default one is used.
    81  // Note that it is always safer to use the default one, unless you know what you are doing.
    82  // It is safe to call this method multiple times, the last call will be used.
    83  func (c *ScoreOptionConfig) OverrideAppSpecificScoreFunction(appSpecificScoreFunction func(peer.ID) float64) {
    84  	c.appScoreFunc = appSpecificScoreFunction
    85  }
    86  
    87  // OverrideTopicScoreParams overrides the topic score parameters for the given topic.
    88  // It is used to override the default topic score parameters for a specific topic.
    89  // If the topic score parameters are not set, the default ones will be used.
    90  func (c *ScoreOptionConfig) OverrideTopicScoreParams(topic channels.Topic, topicScoreParams *pubsub.TopicScoreParams) {
    91  	c.topicParams = append(c.topicParams, func(topics map[string]*pubsub.TopicScoreParams) {
    92  		topics[topic.String()] = topicScoreParams
    93  	})
    94  }
    95  
    96  // NewScoreOption creates a new penalty option with the given configuration.
    97  func NewScoreOption(cfg *ScoreOptionConfig, provider p2p.SubscriptionProvider) (*ScoreOption, error) {
    98  	throttledSampler := logging.BurstSampler(cfg.params.PeerScoring.Protocol.MaxDebugLogs, time.Second)
    99  	logger := cfg.logger.With().
   100  		Str("module", "pubsub_score_option").
   101  		Logger().
   102  		Sample(zerolog.LevelSampler{
   103  			TraceSampler: throttledSampler,
   104  			DebugSampler: throttledSampler,
   105  		})
   106  
   107  	validator := NewSubscriptionValidator(cfg.logger, provider)
   108  	scoreRegistry, err := NewGossipSubAppSpecificScoreRegistry(&GossipSubAppSpecificScoreRegistryConfig{
   109  		Logger:                  logger,
   110  		Penalty:                 cfg.params.ScoringRegistryParameters.MisbehaviourPenalties,
   111  		Validator:               validator,
   112  		IdProvider:              cfg.provider,
   113  		HeroCacheMetricsFactory: cfg.heroCacheMetricsFactory,
   114  		AppScoreCacheFactory: func() p2p.GossipSubApplicationSpecificScoreCache {
   115  			collector := metrics.NewGossipSubApplicationSpecificScoreCacheMetrics(cfg.heroCacheMetricsFactory, cfg.networkingType)
   116  			return internal.NewAppSpecificScoreCache(cfg.params.ScoringRegistryParameters.SpamRecordCache.CacheSize, cfg.logger, collector)
   117  		},
   118  		SpamRecordCacheFactory: func() p2p.GossipSubSpamRecordCache {
   119  			collector := metrics.GossipSubSpamRecordCacheMetricsFactory(cfg.heroCacheMetricsFactory, cfg.networkingType)
   120  			return netcache.NewGossipSubSpamRecordCache(cfg.params.ScoringRegistryParameters.SpamRecordCache.CacheSize, cfg.logger, collector,
   121  				InitAppScoreRecordStateFunc(cfg.params.ScoringRegistryParameters.SpamRecordCache.Decay.MaximumSpamPenaltyDecayFactor),
   122  				DefaultDecayFunction(cfg.params.ScoringRegistryParameters.SpamRecordCache.Decay))
   123  		},
   124  		GetDuplicateMessageCount: func(id peer.ID) float64 {
   125  			return cfg.getDuplicateMessageCount(id)
   126  		},
   127  		Parameters:                cfg.params.ScoringRegistryParameters.AppSpecificScore,
   128  		NetworkingType:            cfg.networkingType,
   129  		AppSpecificScoreParams:    cfg.params.PeerScoring.Protocol.AppSpecificScore,
   130  		DuplicateMessageThreshold: cfg.params.PeerScoring.Protocol.AppSpecificScore.DuplicateMessageThreshold,
   131  		Collector:                 cfg.scoringRegistryMetricsCollector,
   132  	})
   133  	if err != nil {
   134  		return nil, fmt.Errorf("failed to create gossipsub app specific score registry: %w", err)
   135  	}
   136  
   137  	s := &ScoreOption{
   138  		logger:    logger,
   139  		validator: validator,
   140  		peerScoreParams: &pubsub.PeerScoreParams{
   141  			Topics: make(map[string]*pubsub.TopicScoreParams),
   142  			// we don't set all the parameters, so we skip the atomic validation.
   143  			// atomic validation fails initialization if any parameter is not set.
   144  			SkipAtomicValidation: cfg.params.PeerScoring.Internal.TopicParameters.SkipAtomicValidation,
   145  			// DecayInterval is the interval over which we decay the effect of past behavior, so that
   146  			// a good or bad behavior will not have a permanent effect on the penalty. It is also the interval
   147  			// that GossipSub uses to refresh the scores of all peers.
   148  			DecayInterval: cfg.params.PeerScoring.Internal.DecayInterval,
   149  			// DecayToZero defines the maximum value below which a peer scoring counter is reset to zero.
   150  			// This is to prevent the counter from decaying to a very small value.
   151  			// When a counter hits the DecayToZero threshold, it means that the peer did not exhibit the behavior
   152  			// for a long time, and we can reset the counter.
   153  			DecayToZero: cfg.params.PeerScoring.Internal.DecayToZero,
   154  			// AppSpecificWeight is the weight of the application specific penalty.
   155  			AppSpecificWeight: cfg.params.PeerScoring.Internal.AppSpecificScoreWeight,
   156  			// PenaltyThreshold is the threshold above which a peer is penalized for GossipSub-level misbehaviors.
   157  			BehaviourPenaltyThreshold: cfg.params.PeerScoring.Internal.Behaviour.PenaltyThreshold,
   158  			// PenaltyWeight is the weight of the GossipSub-level penalty.
   159  			BehaviourPenaltyWeight: cfg.params.PeerScoring.Internal.Behaviour.PenaltyWeight,
   160  			// PenaltyDecay is the decay of the GossipSub-level penalty (applied every decay interval).
   161  			BehaviourPenaltyDecay: cfg.params.PeerScoring.Internal.Behaviour.PenaltyDecay,
   162  		},
   163  		peerThresholdParams: &pubsub.PeerScoreThresholds{
   164  			GossipThreshold:             cfg.params.PeerScoring.Internal.Thresholds.Gossip,
   165  			PublishThreshold:            cfg.params.PeerScoring.Internal.Thresholds.Publish,
   166  			GraylistThreshold:           cfg.params.PeerScoring.Internal.Thresholds.Graylist,
   167  			AcceptPXThreshold:           cfg.params.PeerScoring.Internal.Thresholds.AcceptPX,
   168  			OpportunisticGraftThreshold: cfg.params.PeerScoring.Internal.Thresholds.OpportunisticGraft,
   169  		},
   170  		defaultTopicScoreParams: &pubsub.TopicScoreParams{
   171  			TopicWeight:                     cfg.params.PeerScoring.Internal.TopicParameters.TopicWeight,
   172  			SkipAtomicValidation:            cfg.params.PeerScoring.Internal.TopicParameters.SkipAtomicValidation,
   173  			InvalidMessageDeliveriesWeight:  cfg.params.PeerScoring.Internal.TopicParameters.InvalidMessageDeliveriesWeight,
   174  			InvalidMessageDeliveriesDecay:   cfg.params.PeerScoring.Internal.TopicParameters.InvalidMessageDeliveriesDecay,
   175  			TimeInMeshQuantum:               cfg.params.PeerScoring.Internal.TopicParameters.TimeInMeshQuantum,
   176  			MeshMessageDeliveriesWeight:     cfg.params.PeerScoring.Internal.TopicParameters.MeshDeliveriesWeight,
   177  			MeshMessageDeliveriesDecay:      cfg.params.PeerScoring.Internal.TopicParameters.MeshMessageDeliveriesDecay,
   178  			MeshMessageDeliveriesCap:        cfg.params.PeerScoring.Internal.TopicParameters.MeshMessageDeliveriesCap,
   179  			MeshMessageDeliveriesThreshold:  cfg.params.PeerScoring.Internal.TopicParameters.MeshMessageDeliveryThreshold,
   180  			MeshMessageDeliveriesWindow:     cfg.params.PeerScoring.Internal.TopicParameters.MeshMessageDeliveriesWindow,
   181  			MeshMessageDeliveriesActivation: cfg.params.PeerScoring.Internal.TopicParameters.MeshMessageDeliveryActivation,
   182  		},
   183  		appScoreFunc:     scoreRegistry.AppSpecificScoreFunc(),
   184  		appScoreRegistry: scoreRegistry,
   185  	}
   186  
   187  	// set the app specific penalty function for the penalty option
   188  	// if the app specific penalty function is not set, use the default one
   189  	if cfg.appScoreFunc != nil {
   190  		s.appScoreFunc = cfg.appScoreFunc
   191  		s.logger.
   192  			Warn().
   193  			Str(logging.KeyNetworkingSecurity, "true").
   194  			Msg("app specific score function is overridden, should never happen in production")
   195  	}
   196  
   197  	if cfg.params.PeerScoring.Internal.DecayInterval > 0 && cfg.params.PeerScoring.Internal.DecayInterval != s.peerScoreParams.DecayInterval {
   198  		// overrides the default decay interval if the decay interval is set.
   199  		s.peerScoreParams.DecayInterval = cfg.params.PeerScoring.Internal.DecayInterval
   200  		s.logger.
   201  			Warn().
   202  			Str(logging.KeyNetworkingSecurity, "true").
   203  			Dur("decay_interval_ms", cfg.params.PeerScoring.Internal.DecayInterval).
   204  			Msg("decay interval is overridden, should never happen in production")
   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  }
   268  
   269  // OnInvalidControlMessageNotification is called when a new invalid control message notification is distributed.
   270  // Any error on consuming event must handle internally.
   271  // The implementation must be concurrency safe and non-blocking.
   272  // Note: there is no real-time guarantee on processing the notification.
   273  func (s *ScoreOption) OnInvalidControlMessageNotification(notif *p2p.InvCtrlMsgNotif) {
   274  	s.appScoreRegistry.OnInvalidControlMessageNotification(notif)
   275  }