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 }