github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/network/p2p/scoring/registry_test.go (about) 1 package scoring_test 2 3 import ( 4 "context" 5 "fmt" 6 "io" 7 "math" 8 "sync" 9 "testing" 10 "time" 11 12 "github.com/libp2p/go-libp2p/core/peer" 13 "github.com/rs/zerolog" 14 "github.com/stretchr/testify/assert" 15 testifymock "github.com/stretchr/testify/mock" 16 "github.com/stretchr/testify/require" 17 "go.uber.org/atomic" 18 19 "github.com/onflow/flow-go/config" 20 "github.com/onflow/flow-go/model/flow" 21 "github.com/onflow/flow-go/module/irrecoverable" 22 "github.com/onflow/flow-go/module/metrics" 23 "github.com/onflow/flow-go/module/mock" 24 "github.com/onflow/flow-go/network" 25 "github.com/onflow/flow-go/network/p2p" 26 netcache "github.com/onflow/flow-go/network/p2p/cache" 27 p2pconfig "github.com/onflow/flow-go/network/p2p/config" 28 p2pmsg "github.com/onflow/flow-go/network/p2p/message" 29 mockp2p "github.com/onflow/flow-go/network/p2p/mock" 30 "github.com/onflow/flow-go/network/p2p/scoring" 31 "github.com/onflow/flow-go/network/p2p/scoring/internal" 32 "github.com/onflow/flow-go/utils/unittest" 33 ) 34 35 // TestScoreRegistry_FreshStart tests the app specific score computation of the node when there is no spam record for the peer id upon fresh start of the registry. 36 // It tests the state that a staked peer with a valid role and valid subscriptions has no spam records; hence it should "eventually" be rewarded with the default reward 37 // for its GossipSub app specific score. The "eventually" comes from the fact that the app specific score is updated asynchronously in the cache, and the cache is 38 // updated when the app specific score function is called by GossipSub. 39 func TestScoreRegistry_FreshStart(t *testing.T) { 40 peerID := peer.ID("peer-1") 41 42 cfg, err := config.DefaultConfig() 43 require.NoError(t, err) 44 // refresh cached app-specific score every 100 milliseconds to speed up the test. 45 cfg.NetworkConfig.GossipSub.ScoringParameters.ScoringRegistryParameters.AppSpecificScore.ScoreTTL = 100 * time.Millisecond 46 47 maximumSpamPenaltyDecayFactor := cfg.NetworkConfig.GossipSub.ScoringParameters.ScoringRegistryParameters.SpamRecordCache.Decay.MaximumSpamPenaltyDecayFactor 48 reg, spamRecords, appScoreCache := newGossipSubAppSpecificScoreRegistry(t, 49 cfg.NetworkConfig.GossipSub.ScoringParameters, 50 scoring.InitAppScoreRecordStateFunc(maximumSpamPenaltyDecayFactor), 51 withStakedIdentities(peerID), 52 withValidSubscriptions(peerID)) 53 ctx, cancel := context.WithCancel(context.Background()) 54 signalerCtx := irrecoverable.NewMockSignalerContext(t, ctx) 55 reg.Start(signalerCtx) 56 unittest.RequireCloseBefore(t, reg.Ready(), 1*time.Second, "registry did not start in time") 57 58 defer stopRegistry(t, cancel, reg) 59 60 // initially, the spamRecords should not have the peer id, and there should be no app-specific score in the cache. 61 require.False(t, spamRecords.Has(peerID)) 62 score, updated, exists := appScoreCache.Get(peerID) // get the score from the cache. 63 require.False(t, exists) 64 require.Equal(t, time.Time{}, updated) 65 require.Equal(t, float64(0), score) 66 67 maxAppSpecificReward := cfg.NetworkConfig.GossipSub.ScoringParameters.PeerScoring.Protocol.AppSpecificScore.MaxAppSpecificReward 68 69 queryTime := time.Now() 70 require.Eventually(t, func() bool { 71 // calling the app specific score function when there is no app specific score in the cache should eventually update the cache. 72 score := reg.AppSpecificScoreFunc()(peerID) 73 // since the peer id does not have a spam record, the app specific score should be the max app specific reward, which 74 // is the default reward for a staked peer that has valid subscriptions. 75 return score == maxAppSpecificReward 76 }, 5*time.Second, 100*time.Millisecond) 77 78 // still the spamRecords should not have the peer id (as there is no spam record for the peer id). 79 require.False(t, spamRecords.Has(peerID)) 80 81 // however, the app specific score should be updated in the cache. 82 score, updated, exists = appScoreCache.Get(peerID) // get the score from the cache. 83 require.True(t, exists) 84 require.True(t, updated.After(queryTime)) 85 require.Equal(t, maxAppSpecificReward, score) 86 87 // stop the registry. 88 cancel() 89 unittest.RequireCloseBefore(t, reg.Done(), 1*time.Second, "failed to stop GossipSubAppSpecificScoreRegistry") 90 } 91 92 // TestScoreRegistry_PeerWithSpamRecord is a test suite designed to assess the app-specific penalty computation 93 // in a scenario where a peer with a staked identity and valid subscriptions has a spam record. The suite runs multiple 94 // sub-tests, each targeting a specific type of control message (graft, prune, ihave, iwant, RpcPublishMessage). The focus 95 // is on the impact of spam records on the app-specific score, specifically how such records negate the default reward 96 // a staked peer would otherwise receive, leaving only the penalty as the app-specific score. This testing reflects the 97 // asynchronous nature of app-specific score updates in GossipSub's cache. 98 func TestScoreRegistry_PeerWithSpamRecord(t *testing.T) { 99 t.Run("graft", func(t *testing.T) { 100 testScoreRegistryPeerWithSpamRecord(t, p2pmsg.CtrlMsgGraft, penaltyValueFixtures().GraftMisbehaviour) 101 }) 102 t.Run("prune", func(t *testing.T) { 103 testScoreRegistryPeerWithSpamRecord(t, p2pmsg.CtrlMsgPrune, penaltyValueFixtures().PruneMisbehaviour) 104 }) 105 t.Run("ihave", func(t *testing.T) { 106 testScoreRegistryPeerWithSpamRecord(t, p2pmsg.CtrlMsgIHave, penaltyValueFixtures().IHaveMisbehaviour) 107 }) 108 t.Run("iwant", func(t *testing.T) { 109 testScoreRegistryPeerWithSpamRecord(t, p2pmsg.CtrlMsgIWant, penaltyValueFixtures().IWantMisbehaviour) 110 }) 111 t.Run("RpcPublishMessage", func(t *testing.T) { 112 testScoreRegistryPeerWithSpamRecord(t, p2pmsg.RpcPublishMessage, penaltyValueFixtures().PublishMisbehaviour) 113 }) 114 } 115 116 // testScoreRegistryPeerWithSpamRecord conducts an individual test within the TestScoreRegistry_PeerWithSpamRecord suite. 117 // It evaluates the ScoreRegistry's handling of a staked peer with valid subscriptions when a spam record is present for 118 // the peer ID. The function simulates the process of starting the registry, recording a misbehavior, and then verifying the 119 // updates to the spam records and app-specific score cache based on the type of control message received. 120 // Parameters: 121 // - t *testing.T: The test context. 122 // - messageType p2pmsg.ControlMessageType: The type of control message being tested. 123 // - expectedPenalty float64: The expected penalty value for the given control message type. 124 // This function specifically tests how the ScoreRegistry updates a peer's app-specific score in response to spam records, 125 // emphasizing the removal of the default reward for staked peers with valid roles and focusing on the asynchronous update 126 // mechanism of the app-specific score in the cache. 127 func testScoreRegistryPeerWithSpamRecord(t *testing.T, messageType p2pmsg.ControlMessageType, expectedPenalty float64) { 128 peerID := peer.ID("peer-1") 129 130 cfg, err := config.DefaultConfig() 131 require.NoError(t, err) 132 // refresh cached app-specific score every 100 milliseconds to speed up the test. 133 cfg.NetworkConfig.GossipSub.ScoringParameters.ScoringRegistryParameters.AppSpecificScore.ScoreTTL = 10 * time.Millisecond 134 135 maximumSpamPenaltyDecayFactor := cfg.NetworkConfig.GossipSub.ScoringParameters.ScoringRegistryParameters.SpamRecordCache.Decay.MaximumSpamPenaltyDecayFactor 136 reg, spamRecords, appScoreCache := newGossipSubAppSpecificScoreRegistry(t, 137 cfg.NetworkConfig.GossipSub.ScoringParameters, 138 scoring.InitAppScoreRecordStateFunc(maximumSpamPenaltyDecayFactor), 139 withStakedIdentities(peerID), 140 withValidSubscriptions(peerID)) 141 ctx, cancel := context.WithCancel(context.Background()) 142 signalerCtx := irrecoverable.NewMockSignalerContext(t, ctx) 143 reg.Start(signalerCtx) 144 unittest.RequireCloseBefore(t, reg.Ready(), 1*time.Second, "registry did not start in time") 145 146 defer stopRegistry(t, cancel, reg) 147 148 // initially, the spamRecords should not have the peer id; also the app specific score record should not be in the cache. 149 require.False(t, spamRecords.Has(peerID)) 150 score, updated, exists := appScoreCache.Get(peerID) // get the score from the cache. 151 require.False(t, exists) 152 require.Equal(t, time.Time{}, updated) 153 require.Equal(t, float64(0), score) 154 155 scoreOptParameters := cfg.NetworkConfig.GossipSub.ScoringParameters.PeerScoring.Protocol.AppSpecificScore 156 157 // eventually, the app specific score should be updated in the cache. 158 require.Eventually(t, func() bool { 159 // calling the app specific score function when there is no app specific score in the cache should eventually update the cache. 160 score := reg.AppSpecificScoreFunc()(peerID) 161 // since the peer id does not have a spam record, the app specific score should be the max app specific reward, which 162 // is the default reward for a staked peer that has valid subscriptions. 163 return scoreOptParameters.MaxAppSpecificReward == score 164 }, 5*time.Second, 100*time.Millisecond) 165 166 // report a misbehavior for the peer id. 167 reg.OnInvalidControlMessageNotification(&p2p.InvCtrlMsgNotif{ 168 PeerID: peerID, 169 MsgType: messageType, 170 }) 171 172 queryTime := time.Now() 173 require.Eventually(t, func() bool { 174 // the notification is processed asynchronously, and the penalty should eventually be updated in the spamRecords 175 record, err, ok := spamRecords.Get(peerID) // get the record from the spamRecords. 176 if !ok { 177 return false 178 } 179 require.NoError(t, err) 180 if !unittest.AreNumericallyClose(expectedPenalty, record.Penalty, 10e-2) { 181 return false 182 } 183 require.Equal(t, scoring.InitAppScoreRecordStateFunc(maximumSpamPenaltyDecayFactor)().Decay, record.Decay) // decay should be initialized to the initial state. 184 185 // eventually, the app specific score should be updated in the cache. 186 // this peer has a spam record, with no subscription penalty. Hence, the app specific score should only be the spam penalty, 187 // and the peer should be deprived of the default reward for its valid staked role. 188 // As the app specific score in the cache and spam penalty in the spamRecords are updated at different times, we account for 5% error. 189 return unittest.AreNumericallyClose(expectedPenalty, reg.AppSpecificScoreFunc()(peerID), 0.05) 190 }, 5*time.Second, 10*time.Millisecond) 191 192 // the app specific score should now be updated in the cache. 193 score, updated, exists = appScoreCache.Get(peerID) // get the score from the cache. 194 require.True(t, exists) 195 require.True(t, updated.After(queryTime)) 196 require.True(t, unittest.AreNumericallyClose(expectedPenalty, score, 0.1)) // account for maximum 10% error due to decays and asynchrony. 197 198 // stop the registry. 199 cancel() 200 unittest.RequireCloseBefore(t, reg.Done(), 1*time.Second, "failed to stop GossipSubAppSpecificScoreRegistry") 201 } 202 203 // TestScoreRegistry_SpamRecordWithUnknownIdentity is a test suite for verifying the behavior of the ScoreRegistry 204 // when handling spam records associated with unknown identities. It tests various scenarios based on different control 205 // message types, including graft, prune, ihave, iwant, and RpcPublishMessage. Each sub-test validates the app-specific 206 // penalty computation and updates to the score registry when a peer with an unknown identity sends these control messages. 207 func TestScoreRegistry_SpamRecordWithUnknownIdentity(t *testing.T) { 208 t.Run("graft", func(t *testing.T) { 209 testScoreRegistrySpamRecordWithUnknownIdentity(t, p2pmsg.CtrlMsgGraft, penaltyValueFixtures().GraftMisbehaviour) 210 }) 211 t.Run("prune", func(t *testing.T) { 212 testScoreRegistrySpamRecordWithUnknownIdentity(t, p2pmsg.CtrlMsgPrune, penaltyValueFixtures().PruneMisbehaviour) 213 }) 214 t.Run("ihave", func(t *testing.T) { 215 testScoreRegistrySpamRecordWithUnknownIdentity(t, p2pmsg.CtrlMsgIHave, penaltyValueFixtures().IHaveMisbehaviour) 216 }) 217 t.Run("iwant", func(t *testing.T) { 218 testScoreRegistrySpamRecordWithUnknownIdentity(t, p2pmsg.CtrlMsgIWant, penaltyValueFixtures().IWantMisbehaviour) 219 }) 220 t.Run("RpcPublishMessage", func(t *testing.T) { 221 testScoreRegistrySpamRecordWithUnknownIdentity(t, p2pmsg.RpcPublishMessage, penaltyValueFixtures().PublishMisbehaviour) 222 }) 223 } 224 225 // testScoreRegistrySpamRecordWithUnknownIdentity tests the app-specific penalty computation of the node when there 226 // is a spam record for a peer ID with an unknown identity. It examines the functionality of the GossipSubAppSpecificScoreRegistry 227 // under various conditions, including the initialization state, spam record creation, and the impact of different control message types. 228 // Parameters: 229 // - t *testing.T: The testing context. 230 // - messageType p2pmsg.ControlMessageType: The type of control message being tested. 231 // - expectedPenalty float64: The expected penalty value for the given control message type. 232 // The function simulates the process of starting the registry, reporting a misbehavior for the peer ID, and verifying the 233 // updates to the spam records and app-specific score cache. It ensures that the penalties are correctly computed and applied 234 // based on the given control message type and the state of the peer ID (unknown identity and spam record presence). 235 func testScoreRegistrySpamRecordWithUnknownIdentity(t *testing.T, messageType p2pmsg.ControlMessageType, expectedPenalty float64) { 236 peerID := peer.ID("peer-1") 237 cfg, err := config.DefaultConfig() 238 require.NoError(t, err) 239 // refresh cached app-specific score every 100 milliseconds to speed up the test. 240 cfg.NetworkConfig.GossipSub.ScoringParameters.ScoringRegistryParameters.AppSpecificScore.ScoreTTL = 100 * time.Millisecond 241 maximumSpamPenaltyDecayFactor := cfg.NetworkConfig.GossipSub.ScoringParameters.ScoringRegistryParameters.SpamRecordCache.Decay.MaximumSpamPenaltyDecayFactor 242 reg, spamRecords, appScoreCache := newGossipSubAppSpecificScoreRegistry(t, 243 cfg.NetworkConfig.GossipSub.ScoringParameters, 244 scoring.InitAppScoreRecordStateFunc(maximumSpamPenaltyDecayFactor), 245 withUnknownIdentity(peerID), 246 withValidSubscriptions(peerID)) 247 ctx, cancel := context.WithCancel(context.Background()) 248 signalerCtx := irrecoverable.NewMockSignalerContext(t, ctx) 249 reg.Start(signalerCtx) 250 unittest.RequireCloseBefore(t, reg.Ready(), 1*time.Second, "registry did not start in time") 251 252 defer stopRegistry(t, cancel, reg) 253 254 // initially, the spamRecords should not have the peer id; also the app specific score record should not be in the cache. 255 require.False(t, spamRecords.Has(peerID)) 256 score, updated, exists := appScoreCache.Get(peerID) // get the score from the cache. 257 require.False(t, exists) 258 require.Equal(t, time.Time{}, updated) 259 require.Equal(t, float64(0), score) 260 261 scoreOptParameters := cfg.NetworkConfig.GossipSub.ScoringParameters.PeerScoring.Protocol.AppSpecificScore 262 263 // eventually the app specific score should be updated in the cache to the penalty value for unknown identity. 264 require.Eventually(t, func() bool { 265 // calling the app specific score function when there is no app specific score in the cache should eventually update the cache. 266 score := reg.AppSpecificScoreFunc()(peerID) 267 // peer does not have spam record, but has an unknown identity. Hence, the app specific score should be the staking penalty. 268 return scoreOptParameters.UnknownIdentityPenalty == score 269 }, 5*time.Second, 100*time.Millisecond) 270 271 // queryTime := time.Now() 272 // report a misbehavior for the peer id. 273 reg.OnInvalidControlMessageNotification(&p2p.InvCtrlMsgNotif{ 274 PeerID: peerID, 275 MsgType: messageType, 276 }) 277 278 queryTime := time.Now() 279 require.Eventually(t, func() bool { 280 // the notification is processed asynchronously, and the penalty should eventually be updated in the spamRecords 281 record, err, ok := spamRecords.Get(peerID) // get the record from the spamRecords. 282 if !ok { 283 return false 284 } 285 require.NoError(t, err) 286 if !unittest.AreNumericallyClose(expectedPenalty, record.Penalty, 10e-2) { 287 return false 288 } 289 require.Equal(t, scoring.InitAppScoreRecordStateFunc(maximumSpamPenaltyDecayFactor)().Decay, record.Decay) // decay should be initialized to the initial state. 290 291 // eventually, the app specific score should be updated in the cache. 292 // the peer has spam record as well as an unknown identity. Hence, the app specific score should be the spam penalty 293 // and the staking penalty. 294 // As the app specific score in the cache and spam penalty in the spamRecords are updated at different times, we account for 5% error. 295 return unittest.AreNumericallyClose(expectedPenalty+scoreOptParameters.UnknownIdentityPenalty, reg.AppSpecificScoreFunc()(peerID), 0.05) 296 }, 5*time.Second, 100*time.Millisecond) 297 298 // the app specific score should now be updated in the cache. 299 score, updated, exists = appScoreCache.Get(peerID) // get the score from the cache. 300 require.True(t, exists) 301 fmt.Println("updated", updated, "queryTime", queryTime) 302 require.True(t, updated.After(queryTime)) 303 fmt.Println("score", score, "expected", expectedPenalty+scoreOptParameters.UnknownIdentityPenalty) 304 unittest.RequireNumericallyClose(t, expectedPenalty+scoreOptParameters.UnknownIdentityPenalty, score, 0.1) // account for maximum 10% error due to decays and asynchrony. 305 306 // stop the registry. 307 cancel() 308 unittest.RequireCloseBefore(t, reg.Done(), 1*time.Second, "failed to stop GossipSubAppSpecificScoreRegistry") 309 } 310 311 // TestScoreRegistry_SpamRecordWithSubscriptionPenalty is a test suite for verifying the behavior of the ScoreRegistry 312 // in handling spam records associated with invalid subscriptions. It encompasses a series of sub-tests, each focusing on 313 // a different control message type: graft, prune, ihave, iwant, and RpcPublishMessage. These sub-tests are designed to 314 // validate the appropriate application of penalties in the ScoreRegistry when a peer with an invalid subscription is involved 315 // in spam activities, as indicated by these control messages. 316 func TestScoreRegistry_SpamRecordWithSubscriptionPenalty(t *testing.T) { 317 t.Run("graft", func(t *testing.T) { 318 testScoreRegistrySpamRecordWithSubscriptionPenalty(t, p2pmsg.CtrlMsgGraft, penaltyValueFixtures().GraftMisbehaviour) 319 }) 320 t.Run("prune", func(t *testing.T) { 321 testScoreRegistrySpamRecordWithSubscriptionPenalty(t, p2pmsg.CtrlMsgPrune, penaltyValueFixtures().PruneMisbehaviour) 322 }) 323 t.Run("ihave", func(t *testing.T) { 324 testScoreRegistrySpamRecordWithSubscriptionPenalty(t, p2pmsg.CtrlMsgIHave, penaltyValueFixtures().IHaveMisbehaviour) 325 }) 326 t.Run("iwant", func(t *testing.T) { 327 testScoreRegistrySpamRecordWithSubscriptionPenalty(t, p2pmsg.CtrlMsgIWant, penaltyValueFixtures().IWantMisbehaviour) 328 }) 329 t.Run("RpcPublishMessage", func(t *testing.T) { 330 testScoreRegistrySpamRecordWithSubscriptionPenalty(t, p2pmsg.RpcPublishMessage, penaltyValueFixtures().PublishMisbehaviour) 331 }) 332 } 333 334 // testScoreRegistrySpamRecordWithSubscriptionPenalty tests the application-specific penalty computation in the ScoreRegistry 335 // when a spam record exists for a peer ID that also has an invalid subscription. The function simulates the process of 336 // initializing the registry, handling spam records, and updating penalties based on various control message types. 337 // Parameters: 338 // - t *testing.T: The testing context. 339 // - messageType p2pmsg.ControlMessageType: The type of control message being tested. 340 // - expectedPenalty float64: The expected penalty value for the given control message type. 341 // The function focuses on evaluating the registry's response to spam activities (as represented by control messages) from a 342 // peer with invalid subscriptions. It verifies that penalties are accurately computed and applied, taking into account both 343 // the spam record and the invalid subscription status of the peer. 344 func testScoreRegistrySpamRecordWithSubscriptionPenalty(t *testing.T, messageType p2pmsg.ControlMessageType, expectedPenalty float64) { 345 peerID := peer.ID("peer-1") 346 cfg, err := config.DefaultConfig() 347 require.NoError(t, err) 348 // refresh cached app-specific score every 100 milliseconds to speed up the test. 349 cfg.NetworkConfig.GossipSub.ScoringParameters.ScoringRegistryParameters.AppSpecificScore.ScoreTTL = 100 * time.Millisecond 350 maximumSpamPenaltyDecayFactor := cfg.NetworkConfig.GossipSub.ScoringParameters.ScoringRegistryParameters.SpamRecordCache.Decay.MaximumSpamPenaltyDecayFactor 351 reg, spamRecords, appScoreCache := newGossipSubAppSpecificScoreRegistry(t, 352 cfg.NetworkConfig.GossipSub.ScoringParameters, 353 scoring.InitAppScoreRecordStateFunc(maximumSpamPenaltyDecayFactor), 354 withStakedIdentities(peerID), 355 withInvalidSubscriptions(peerID)) 356 ctx, cancel := context.WithCancel(context.Background()) 357 signalerCtx := irrecoverable.NewMockSignalerContext(t, ctx) 358 reg.Start(signalerCtx) 359 unittest.RequireCloseBefore(t, reg.Ready(), 1*time.Second, "registry did not start in time") 360 361 defer stopRegistry(t, cancel, reg) 362 363 // initially, the spamRecords should not have the peer id; also the app specific score record should not be in the cache. 364 require.False(t, spamRecords.Has(peerID)) 365 score, updated, exists := appScoreCache.Get(peerID) // get the score from the cache. 366 require.False(t, exists) 367 require.Equal(t, time.Time{}, updated) 368 require.Equal(t, float64(0), score) 369 370 scoreOptParameters := cfg.NetworkConfig.GossipSub.ScoringParameters.PeerScoring.Protocol.AppSpecificScore 371 372 // peer does not have spam record, but has invalid subscription. Hence, the app specific score should be subscription penalty. 373 // eventually the app specific score should be updated in the cache to the penalty value for subscription penalty. 374 require.Eventually(t, func() bool { 375 // calling the app specific score function when there is no app specific score in the cache should eventually update the cache. 376 score := reg.AppSpecificScoreFunc()(peerID) 377 // peer does not have spam record, but has an invalid subscription penalty. 378 return scoreOptParameters.InvalidSubscriptionPenalty == score 379 }, 5*time.Second, 100*time.Millisecond) 380 381 // report a misbehavior for the peer id. 382 reg.OnInvalidControlMessageNotification(&p2p.InvCtrlMsgNotif{ 383 PeerID: peerID, 384 MsgType: messageType, 385 }) 386 387 queryTime := time.Now() 388 require.Eventually(t, func() bool { 389 // the notification is processed asynchronously, and the penalty should eventually be updated in the spamRecords 390 record, err, ok := spamRecords.Get(peerID) // get the record from the spamRecords. 391 if !ok { 392 return false 393 } 394 require.NoError(t, err) 395 if !unittest.AreNumericallyClose(expectedPenalty, record.Penalty, 10e-2) { 396 return false 397 } 398 require.Equal(t, scoring.InitAppScoreRecordStateFunc(maximumSpamPenaltyDecayFactor)().Decay, record.Decay) // decay should be initialized to the initial state. 399 400 // eventually, the app specific score should be updated in the cache. 401 // the peer has spam record as well as an unknown identity. Hence, the app specific score should be the spam penalty 402 // and the staking penalty. 403 // As the app specific score in the cache and spam penalty in the spamRecords are updated at different times, we account for 5% error. 404 return unittest.AreNumericallyClose(expectedPenalty+scoreOptParameters.InvalidSubscriptionPenalty, reg.AppSpecificScoreFunc()(peerID), 0.05) 405 }, 5*time.Second, 100*time.Millisecond) 406 407 // the app specific score should now be updated in the cache. 408 score, updated, exists = appScoreCache.Get(peerID) // get the score from the cache. 409 require.True(t, exists) 410 require.True(t, updated.After(queryTime)) 411 unittest.RequireNumericallyClose(t, expectedPenalty+scoreOptParameters.InvalidSubscriptionPenalty, score, 0.1) // account for maximum 10% error due to decays and asynchrony. 412 413 // stop the registry. 414 cancel() 415 unittest.RequireCloseBefore(t, reg.Done(), 1*time.Second, "failed to stop GossipSubAppSpecificScoreRegistry") 416 } 417 418 // TestScoreRegistry_SpamRecordWithDuplicateMessagesPenalty is a test suite for verifying the behavior of the ScoreRegistry 419 // in handling spam records when duplicate messages penalty is applied. It encompasses a series of sub-tests, each focusing on 420 // a different control message type: graft, prune, ihave, iwant, and RpcPublishMessage. These sub-tests are designed to 421 // validate the appropriate application of penalties in the ScoreRegistry when a peer has sent duplicate messages. 422 func TestScoreRegistry_SpamRecordWithDuplicateMessagesPenalty(t *testing.T) { 423 t.Run("graft", func(t *testing.T) { 424 testScoreRegistrySpamRecordWithDuplicateMessagesPenalty(t, p2pmsg.CtrlMsgGraft, penaltyValueFixtures().GraftMisbehaviour) 425 }) 426 t.Run("prune", func(t *testing.T) { 427 testScoreRegistrySpamRecordWithDuplicateMessagesPenalty(t, p2pmsg.CtrlMsgPrune, penaltyValueFixtures().PruneMisbehaviour) 428 }) 429 t.Run("ihave", func(t *testing.T) { 430 testScoreRegistrySpamRecordWithDuplicateMessagesPenalty(t, p2pmsg.CtrlMsgIHave, penaltyValueFixtures().IHaveMisbehaviour) 431 }) 432 t.Run("iwant", func(t *testing.T) { 433 testScoreRegistrySpamRecordWithDuplicateMessagesPenalty(t, p2pmsg.CtrlMsgIWant, penaltyValueFixtures().IWantMisbehaviour) 434 }) 435 t.Run("RpcPublishMessage", func(t *testing.T) { 436 testScoreRegistrySpamRecordWithDuplicateMessagesPenalty(t, p2pmsg.RpcPublishMessage, penaltyValueFixtures().PublishMisbehaviour) 437 }) 438 } 439 440 // testScoreRegistryPeerDuplicateMessagesPenalty conducts an individual test within the TestScoreRegistry_SpamRecordWithDuplicateMessagesPenalty suite. 441 // It evaluates the ScoreRegistry's handling of a staked peer with valid subscriptions and when a record is present for 442 // the peer ID, and the peer has sent some duplicate messages. The function simulates the process of starting the registry, recording a misbehavior, receiving duplicate messages tracked via 443 // the mesh tracer duplicate messages tracker, and then verifying the expected app specific score. 444 // Parameters: 445 // - t *testing.T: The test context. 446 // - messageType p2pmsg.ControlMessageType: The type of control message being tested. 447 // - expectedPenalty float64: The expected penalty value for the given control message type. 448 // The function focuses on evaluating the registry's response to spam activities (as represented by control messages) from a 449 // peer that has sent duplicate messages. It verifies that penalties are accurately computed and applied, taking into account both 450 // the spam record and the duplicate message's penalty. 451 func testScoreRegistrySpamRecordWithDuplicateMessagesPenalty(t *testing.T, messageType p2pmsg.ControlMessageType, expectedPenalty float64) { 452 peerID := unittest.PeerIdFixture(t) 453 cfg, err := config.DefaultConfig() 454 require.NoError(t, err) 455 // refresh cached app-specific score every 100 milliseconds to speed up the test. 456 cfg.NetworkConfig.GossipSub.ScoringParameters.ScoringRegistryParameters.AppSpecificScore.ScoreTTL = 10 * time.Millisecond 457 duplicateMessageThreshold := cfg.NetworkConfig.GossipSub.ScoringParameters.PeerScoring.Protocol.AppSpecificScore.DuplicateMessageThreshold 458 duplicateMessagePenalty := cfg.NetworkConfig.GossipSub.ScoringParameters.PeerScoring.Protocol.AppSpecificScore.DuplicateMessagePenalty 459 maximumSpamPenaltyDecayFactor := cfg.NetworkConfig.GossipSub.ScoringParameters.ScoringRegistryParameters.SpamRecordCache.Decay.MaximumSpamPenaltyDecayFactor 460 duplicateMessagesCount := 10000.0 461 reg, spamRecords, appScoreCache := newGossipSubAppSpecificScoreRegistry(t, cfg.NetworkConfig.GossipSub.ScoringParameters, 462 scoring.InitAppScoreRecordStateFunc(maximumSpamPenaltyDecayFactor), 463 withStakedIdentities(peerID), 464 withValidSubscriptions(peerID), 465 func(registryConfig *scoring.GossipSubAppSpecificScoreRegistryConfig) { 466 registryConfig.GetDuplicateMessageCount = func(_ peer.ID) float64 { 467 // we add the duplicate message threshold so that penalization is triggered 468 return duplicateMessagesCount + duplicateMessageThreshold 469 } 470 }) 471 472 // starts the registry. 473 ctx, cancel := context.WithCancel(context.Background()) 474 signalerCtx := irrecoverable.NewMockSignalerContext(t, ctx) 475 reg.Start(signalerCtx) 476 unittest.RequireCloseBefore(t, reg.Ready(), 1*time.Second, "failed to start GossipSubAppSpecificScoreRegistry") 477 478 // initially, the spamRecords should not have the peer id; also the app specific score record should not be in the cache. 479 require.False(t, spamRecords.Has(peerID)) 480 score, updated, exists := appScoreCache.Get(peerID) // get the score from the cache. 481 require.False(t, exists) 482 require.Equal(t, time.Time{}, updated) 483 require.Equal(t, float64(0), score) 484 485 expectedDuplicateMessagesPenalty := duplicateMessagesCount * duplicateMessagePenalty 486 // eventually, the app specific score should be updated in the cache. 487 require.Eventually(t, func() bool { 488 // calling the app specific score function when there is no app specific score in the cache should eventually update the cache. 489 score := reg.AppSpecificScoreFunc()(peerID) 490 // since the peer id does no other penalties the score is eventually expected to be the expected penalty for 10000 duplicate messages 491 return score == expectedDuplicateMessagesPenalty 492 }, 5*time.Second, 100*time.Millisecond) 493 494 // report a misbehavior for the peer id. 495 reg.OnInvalidControlMessageNotification(&p2p.InvCtrlMsgNotif{ 496 PeerID: peerID, 497 MsgType: messageType, 498 }) 499 500 queryTime := time.Now() 501 require.Eventually(t, func() bool { 502 // the notification is processed asynchronously, and the penalty should eventually be updated in the spamRecords 503 record, err, ok := spamRecords.Get(peerID) // get the record from the spamRecords. 504 if !ok { 505 return false 506 } 507 require.NoError(t, err) 508 if !unittest.AreNumericallyClose(expectedPenalty, record.Penalty, 10e-2) { 509 return false 510 } 511 require.Equal(t, scoring.InitAppScoreRecordStateFunc(maximumSpamPenaltyDecayFactor)().Decay, record.Decay) // decay should be initialized to the initial state. 512 513 // eventually, the app specific score should be updated in the cache. 514 // As the app specific score in the cache and spam penalty in the spamRecords are updated at different times, we account for 5% error. 515 return unittest.AreNumericallyClose(expectedPenalty+expectedDuplicateMessagesPenalty, reg.AppSpecificScoreFunc()(peerID), 0.05) 516 }, 5*time.Second, 100*time.Millisecond) 517 518 // the app specific score should now be updated in the cache. 519 score, updated, exists = appScoreCache.Get(peerID) // get the score from the cache. 520 require.True(t, exists) 521 require.True(t, updated.After(queryTime)) 522 unittest.RequireNumericallyClose(t, expectedPenalty+expectedDuplicateMessagesPenalty, score, 0.1) // account for maximum 10% error due to decays and asynchrony. 523 524 // stop the registry. 525 cancel() 526 unittest.RequireCloseBefore(t, reg.Done(), 1*time.Second, "failed to stop GossipSubAppSpecificScoreRegistry") 527 } 528 529 // TestScoreRegistry_SpamRecordWithoutDuplicateMessagesPenalty is a test suite for verifying the behavior of the ScoreRegistry 530 // in handling spam records when duplicate messages exist but do not exceed the scoring.DefaultDuplicateMessageThreshold no penalty is applied. 531 // It encompasses a series of sub-tests, each focusing on a different control message type: graft, prune, ihave, iwant, and RpcPublishMessage. These sub-tests are designed to 532 // validate the appropriate application of penalties in the ScoreRegistry when a peer has sent duplicate messages. 533 func TestScoreRegistry_SpamRecordWithoutDuplicateMessagesPenalty(t *testing.T) { 534 t.Run("graft", func(t *testing.T) { 535 testScoreRegistrySpamRecordWithoutDuplicateMessagesPenalty(t, p2pmsg.CtrlMsgGraft, penaltyValueFixtures().GraftMisbehaviour) 536 }) 537 t.Run("prune", func(t *testing.T) { 538 testScoreRegistrySpamRecordWithoutDuplicateMessagesPenalty(t, p2pmsg.CtrlMsgPrune, penaltyValueFixtures().PruneMisbehaviour) 539 }) 540 t.Run("ihave", func(t *testing.T) { 541 testScoreRegistrySpamRecordWithoutDuplicateMessagesPenalty(t, p2pmsg.CtrlMsgIHave, penaltyValueFixtures().IHaveMisbehaviour) 542 }) 543 t.Run("iwant", func(t *testing.T) { 544 testScoreRegistrySpamRecordWithoutDuplicateMessagesPenalty(t, p2pmsg.CtrlMsgIWant, penaltyValueFixtures().IWantMisbehaviour) 545 }) 546 t.Run("RpcPublishMessage", func(t *testing.T) { 547 testScoreRegistrySpamRecordWithoutDuplicateMessagesPenalty(t, p2pmsg.RpcPublishMessage, penaltyValueFixtures().PublishMisbehaviour) 548 }) 549 } 550 551 // testScoreRegistrySpamRecordWithoutDuplicateMessagesPenalty conducts an individual test within the TestScoreRegistry_SpamRecordWithoutDuplicateMessagesPenalty suite. 552 // It evaluates the ScoreRegistry's handling of a staked peer with valid subscriptions and when a record is present for 553 // the peer ID, and the peer has sent some duplicate messages. The function simulates the process of starting the registry, recording a misbehavior, receiving duplicate messages tracked via 554 // the mesh tracer duplicate messages tracker, and then verifying the expected app specific score. 555 // Parameters: 556 // - t *testing.T: The test context. 557 // - messageType p2pmsg.ControlMessageType: The type of control message being tested. 558 // The function focuses on evaluating the registry's response to spam activities (as represented by control messages) from a 559 // peer that has sent duplicate messages. It verifies that duplicate message penalty is not applied if the duplicate message count for a peer 560 // does not exceed scoring.DefaultDuplicateMessageThreshold. 561 func testScoreRegistrySpamRecordWithoutDuplicateMessagesPenalty(t *testing.T, messageType p2pmsg.ControlMessageType, expectedPenalty float64) { 562 peerID := unittest.PeerIdFixture(t) 563 cfg, err := config.DefaultConfig() 564 require.NoError(t, err) 565 // refresh cached app-specific score every 100 milliseconds to speed up the test. 566 cfg.NetworkConfig.GossipSub.ScoringParameters.ScoringRegistryParameters.AppSpecificScore.ScoreTTL = 10 * time.Millisecond 567 duplicateMessageThreshold := cfg.NetworkConfig.GossipSub.ScoringParameters.PeerScoring.Protocol.AppSpecificScore.DuplicateMessagePenalty 568 maximumSpamPenaltyDecayFactor := cfg.NetworkConfig.GossipSub.ScoringParameters.ScoringRegistryParameters.SpamRecordCache.Decay.MaximumSpamPenaltyDecayFactor 569 reg, spamRecords, appScoreCache := newGossipSubAppSpecificScoreRegistry(t, cfg.NetworkConfig.GossipSub.ScoringParameters, 570 scoring.InitAppScoreRecordStateFunc(maximumSpamPenaltyDecayFactor), 571 withStakedIdentities(peerID), 572 withValidSubscriptions(peerID), 573 func(registryConfig *scoring.GossipSubAppSpecificScoreRegistryConfig) { 574 registryConfig.GetDuplicateMessageCount = func(_ peer.ID) float64 { 575 // duplicate message count never exceeds scoring.DefaultDuplicateMessageThreshold so a penalty should never be applied 576 return duplicateMessageThreshold - 1 577 } 578 }) 579 580 // starts the registry. 581 ctx, cancel := context.WithCancel(context.Background()) 582 signalerCtx := irrecoverable.NewMockSignalerContext(t, ctx) 583 reg.Start(signalerCtx) 584 unittest.RequireCloseBefore(t, reg.Ready(), 1*time.Second, "failed to start GossipSubAppSpecificScoreRegistry") 585 586 // initially, the spamRecords should not have the peer id; also the app specific score record should not be in the cache. 587 require.False(t, spamRecords.Has(peerID)) 588 score, updated, exists := appScoreCache.Get(peerID) // get the score from the cache. 589 require.False(t, exists) 590 require.Equal(t, time.Time{}, updated) 591 require.Equal(t, float64(0), score) 592 593 // initial score will be 0 subsequent calls to get app specific score 594 // should reward the peer with the scoring.MaxAppSpecificPenalty for not having any spam record, staking, or subscription penalties 595 score = reg.AppSpecificScoreFunc()(peerID) 596 require.Equal(t, 0.0, score) 597 598 // app specific score should not be effected by duplicate messages count 599 require.Never(t, func() bool { 600 score := reg.AppSpecificScoreFunc()(peerID) 601 return score != cfg.NetworkConfig.GossipSub.ScoringParameters.PeerScoring.Protocol.AppSpecificScore.MaxAppSpecificReward 602 }, 5*time.Second, 10*time.Millisecond) 603 604 // report a misbehavior for the peer id. 605 reg.OnInvalidControlMessageNotification(&p2p.InvCtrlMsgNotif{ 606 PeerID: peerID, 607 MsgType: messageType, 608 }) 609 610 require.Eventually(t, func() bool { 611 // the notification is processed asynchronously, and the penalty should eventually be updated in the spamRecords 612 record, err, ok := spamRecords.Get(peerID) // get the record from the spamRecords. 613 if !ok { 614 return false 615 } 616 require.NoError(t, err) 617 if !unittest.AreNumericallyClose(expectedPenalty, record.Penalty, 10e-2) { 618 return false 619 } 620 require.Equal(t, scoring.InitAppScoreRecordStateFunc(maximumSpamPenaltyDecayFactor)().Decay, record.Decay) // decay should be initialized to the initial state. 621 622 return true 623 }, 5*time.Second, 10*time.Millisecond) 624 625 queryTime := time.Now() 626 // eventually, the app specific score should be updated in the cache. 627 require.Eventually(t, func() bool { 628 score := reg.AppSpecificScoreFunc()(peerID) 629 return unittest.AreNumericallyClose(expectedPenalty, score, 0.2) 630 }, 5*time.Second, 10*time.Millisecond) 631 632 // the app specific score should now be updated in the cache. 633 score, updated, exists = appScoreCache.Get(peerID) // get the score from the cache. 634 require.True(t, exists) 635 require.True(t, updated.After(queryTime)) 636 unittest.RequireNumericallyClose(t, expectedPenalty, score, 0.01) 637 638 // stop the registry. 639 cancel() 640 unittest.RequireCloseBefore(t, reg.Done(), 1*time.Second, "failed to stop GossipSubAppSpecificScoreRegistry") 641 } 642 643 // TestSpamPenaltyDecaysInCache tests that the spam penalty records decay over time in the cache. 644 func TestScoreRegistry_SpamPenaltyDecaysInCache(t *testing.T) { 645 peerID := peer.ID("peer-1") 646 cfg, err := config.DefaultConfig() 647 require.NoError(t, err) 648 // refresh cached app-specific score every 100 milliseconds to speed up the test. 649 cfg.NetworkConfig.GossipSub.ScoringParameters.ScoringRegistryParameters.AppSpecificScore.ScoreTTL = 100 * time.Millisecond 650 maximumSpamPenaltyDecayFactor := cfg.NetworkConfig.GossipSub.ScoringParameters.ScoringRegistryParameters.SpamRecordCache.Decay.MaximumSpamPenaltyDecayFactor 651 reg, _, _ := newGossipSubAppSpecificScoreRegistry(t, 652 cfg.NetworkConfig.GossipSub.ScoringParameters, 653 scoring.InitAppScoreRecordStateFunc(maximumSpamPenaltyDecayFactor), 654 withStakedIdentities(peerID), 655 withValidSubscriptions(peerID)) 656 ctx, cancel := context.WithCancel(context.Background()) 657 signalerCtx := irrecoverable.NewMockSignalerContext(t, ctx) 658 reg.Start(signalerCtx) 659 unittest.RequireCloseBefore(t, reg.Ready(), 1*time.Second, "registry did not start in time") 660 661 defer stopRegistry(t, cancel, reg) 662 663 // report a misbehavior for the peer id. 664 reg.OnInvalidControlMessageNotification(&p2p.InvCtrlMsgNotif{ 665 PeerID: peerID, 666 MsgType: p2pmsg.CtrlMsgPrune, 667 }) 668 669 time.Sleep(1 * time.Second) // wait for the penalty to decay. 670 671 reg.OnInvalidControlMessageNotification(&p2p.InvCtrlMsgNotif{ 672 PeerID: peerID, 673 MsgType: p2pmsg.CtrlMsgGraft, 674 }) 675 676 time.Sleep(1 * time.Second) // wait for the penalty to decay. 677 678 reg.OnInvalidControlMessageNotification(&p2p.InvCtrlMsgNotif{ 679 PeerID: peerID, 680 MsgType: p2pmsg.CtrlMsgIHave, 681 }) 682 683 time.Sleep(1 * time.Second) // wait for the penalty to decay. 684 685 reg.OnInvalidControlMessageNotification(&p2p.InvCtrlMsgNotif{ 686 PeerID: peerID, 687 MsgType: p2pmsg.CtrlMsgIWant, 688 }) 689 690 time.Sleep(1 * time.Second) // wait for the penalty to decay. 691 692 reg.OnInvalidControlMessageNotification(&p2p.InvCtrlMsgNotif{ 693 PeerID: peerID, 694 MsgType: p2pmsg.RpcPublishMessage, 695 }) 696 697 time.Sleep(1 * time.Second) // wait for the penalty to decay. 698 699 // the upper bound is the sum of the penalties without decay. 700 scoreUpperBound := penaltyValueFixtures().PruneMisbehaviour + 701 penaltyValueFixtures().GraftMisbehaviour + 702 penaltyValueFixtures().IHaveMisbehaviour + 703 penaltyValueFixtures().IWantMisbehaviour + 704 penaltyValueFixtures().PublishMisbehaviour 705 // the lower bound is the sum of the penalties with decay assuming the decay is applied 4 times to the sum of the penalties. 706 // in reality, the decay is applied 4 times to the first penalty, then 3 times to the second penalty, and so on. 707 r := scoring.InitAppScoreRecordStateFunc(maximumSpamPenaltyDecayFactor)() 708 scoreLowerBound := scoreUpperBound * math.Pow(r.Decay, 4) 709 710 // eventually, the app specific score should be updated in the cache. 711 require.Eventually(t, func() bool { 712 // when the app specific penalty function is called for the first time, the decay functionality should be kicked in 713 // the cache, and the penalty should be updated. Note that since the penalty values are negative, the default staked identity 714 // reward is not applied. Hence, the penalty is only comprised of the penalties. 715 score := reg.AppSpecificScoreFunc()(peerID) 716 // with decay, the penalty should be between the upper and lower bounds. 717 return score > scoreUpperBound && score < scoreLowerBound 718 }, 5*time.Second, 100*time.Millisecond) 719 720 // stop the registry. 721 cancel() 722 unittest.RequireCloseBefore(t, reg.Done(), 1*time.Second, "failed to stop GossipSubAppSpecificScoreRegistry") 723 } 724 725 // TestSpamPenaltyDecayToZero tests that the spam penalty decays to zero over time, and when the spam penalty of 726 // a peer is set back to zero, its app specific penalty is also reset to the initial state. 727 func TestScoreRegistry_SpamPenaltyDecayToZero(t *testing.T) { 728 peerID := peer.ID("peer-1") 729 cfg, err := config.DefaultConfig() 730 require.NoError(t, err) 731 // refresh cached app-specific score every 100 milliseconds to speed up the test. 732 cfg.NetworkConfig.GossipSub.ScoringParameters.ScoringRegistryParameters.AppSpecificScore.ScoreTTL = 100 * time.Millisecond 733 734 reg, spamRecords, _ := newGossipSubAppSpecificScoreRegistry(t, 735 cfg.NetworkConfig.GossipSub.ScoringParameters, 736 func() p2p.GossipSubSpamRecord { 737 return p2p.GossipSubSpamRecord{ 738 Decay: 0.02, // we choose a small decay value to speed up the test. 739 Penalty: 0, 740 } 741 }, 742 withStakedIdentities(peerID), 743 withValidSubscriptions(peerID)) 744 745 // starts the registry. 746 ctx, cancel := context.WithCancel(context.Background()) 747 signalerCtx := irrecoverable.NewMockSignalerContext(t, ctx) 748 reg.Start(signalerCtx) 749 unittest.RequireCloseBefore(t, reg.Ready(), 1*time.Second, "registry did not start in time") 750 751 defer stopRegistry(t, cancel, reg) 752 753 scoreOptParameters := cfg.NetworkConfig.GossipSub.ScoringParameters.PeerScoring.Protocol.AppSpecificScore 754 755 // report a misbehavior for the peer id. 756 reg.OnInvalidControlMessageNotification(&p2p.InvCtrlMsgNotif{ 757 PeerID: peerID, 758 MsgType: p2pmsg.CtrlMsgGraft, 759 }) 760 761 // decays happen every second, so we wait for 1 second to make sure the penalty is updated. 762 time.Sleep(1 * time.Second) 763 // the penalty should now be updated, it should be still negative but greater than the penalty value (due to decay). 764 require.Eventually(t, func() bool { 765 score := reg.AppSpecificScoreFunc()(peerID) 766 // the penalty should be less than zero and greater than the penalty value (due to decay). 767 return score < 0 && score > penaltyValueFixtures().GraftMisbehaviour 768 }, 5*time.Second, 100*time.Millisecond) 769 770 require.Eventually(t, func() bool { 771 // the spam penalty should eventually decay to zero. 772 r, err, ok := spamRecords.Get(peerID) 773 return ok && err == nil && r.Penalty == 0.0 774 }, 5*time.Second, 100*time.Millisecond) 775 776 require.Eventually(t, func() bool { 777 // when the spam penalty is decayed to zero, the app specific penalty of the node should reset back to default staking reward. 778 return reg.AppSpecificScoreFunc()(peerID) == scoreOptParameters.StakedIdentityReward 779 }, 5*time.Second, 100*time.Millisecond) 780 781 // the penalty should now be zero. 782 record, err, ok := spamRecords.Get(peerID) // get the record from the spamRecords. 783 assert.True(t, ok) 784 assert.NoError(t, err) 785 assert.Equal(t, 0.0, record.Penalty) // penalty should be zero. 786 787 // stop the registry. 788 cancel() 789 unittest.RequireCloseBefore(t, reg.Done(), 1*time.Second, "failed to stop GossipSubAppSpecificScoreRegistry") 790 } 791 792 // TestPersistingUnknownIdentityPenalty tests that even though the spam penalty is decayed to zero, the unknown identity penalty 793 // is persisted. This is because the unknown identity penalty is not decayed. 794 func TestScoreRegistry_PersistingUnknownIdentityPenalty(t *testing.T) { 795 peerID := peer.ID("peer-1") 796 797 cfg, err := config.DefaultConfig() 798 require.NoError(t, err) 799 // refresh cached app-specific score every 100 milliseconds to speed up the test. 800 cfg.NetworkConfig.GossipSub.ScoringParameters.ScoringRegistryParameters.AppSpecificScore.ScoreTTL = 100 * time.Millisecond 801 802 reg, spamRecords, _ := newGossipSubAppSpecificScoreRegistry(t, 803 cfg.NetworkConfig.GossipSub.ScoringParameters, 804 func() p2p.GossipSubSpamRecord { 805 return p2p.GossipSubSpamRecord{ 806 Decay: 0.02, // we choose a small decay value to speed up the test. 807 Penalty: 0, 808 } 809 }, 810 withUnknownIdentity(peerID), // the peer id has an unknown identity. 811 withValidSubscriptions(peerID)) 812 813 // starts the registry. 814 ctx, cancel := context.WithCancel(context.Background()) 815 signalerCtx := irrecoverable.NewMockSignalerContext(t, ctx) 816 reg.Start(signalerCtx) 817 unittest.RequireCloseBefore(t, reg.Ready(), 1*time.Second, "registry did not start in time") 818 819 defer stopRegistry(t, cancel, reg) 820 821 scoreOptParameters := cfg.NetworkConfig.GossipSub.ScoringParameters.PeerScoring.Protocol.AppSpecificScore 822 823 // initially, the app specific score should be the default unknown identity penalty. 824 require.Eventually(t, func() bool { 825 score := reg.AppSpecificScoreFunc()(peerID) 826 return score == scoreOptParameters.UnknownIdentityPenalty 827 }, 5*time.Second, 100*time.Millisecond) 828 829 // report a misbehavior for the peer id. 830 reg.OnInvalidControlMessageNotification(&p2p.InvCtrlMsgNotif{ 831 PeerID: peerID, 832 MsgType: p2pmsg.CtrlMsgGraft, 833 }) 834 835 // decays happen every second, so we wait for 1 second to make sure the penalty is updated. 836 time.Sleep(1 * time.Second) 837 838 // the penalty should now be updated, it should be still negative but greater than the penalty value (due to decay). 839 require.Eventually(t, func() bool { 840 score := reg.AppSpecificScoreFunc()(peerID) 841 // Ideally, the score should be the sum of the default invalid subscription penalty and the graft penalty, however, 842 // due to exponential decay of the spam penalty and asynchronous update the app specific score; score should be in the range of [scoring. 843 // (scoring.DefaultUnknownIdentityPenalty+penaltyValueFixtures().GraftMisbehaviour, scoring.DefaultUnknownIdentityPenalty). 844 return score < scoreOptParameters.UnknownIdentityPenalty && score > scoreOptParameters.UnknownIdentityPenalty+penaltyValueFixtures().GraftMisbehaviour 845 }, 5*time.Second, 100*time.Millisecond) 846 847 require.Eventually(t, func() bool { 848 // the spam penalty should eventually decay to zero. 849 r, err, ok := spamRecords.Get(peerID) 850 return ok && err == nil && r.Penalty == 0.0 851 }, 5*time.Second, 100*time.Millisecond) 852 853 require.Eventually(t, func() bool { 854 // when the spam penalty is decayed to zero, the app specific penalty of the node should only contain the unknown identity penalty. 855 return reg.AppSpecificScoreFunc()(peerID) == scoreOptParameters.UnknownIdentityPenalty 856 }, 5*time.Second, 100*time.Millisecond) 857 858 // the spam penalty should now be zero in spamRecords. 859 record, err, ok := spamRecords.Get(peerID) // get the record from the spamRecords. 860 assert.True(t, ok) 861 assert.NoError(t, err) 862 assert.Equal(t, 0.0, record.Penalty) // penalty should be zero. 863 864 // stop the registry. 865 cancel() 866 unittest.RequireCloseBefore(t, reg.Done(), 1*time.Second, "failed to stop GossipSubAppSpecificScoreRegistry") 867 } 868 869 // TestPersistingInvalidSubscriptionPenalty tests that even though the spam penalty is decayed to zero, the invalid subscription penalty 870 // is persisted. This is because the invalid subscription penalty is not decayed. 871 func TestScoreRegistry_PersistingInvalidSubscriptionPenalty(t *testing.T) { 872 peerID := peer.ID("peer-1") 873 874 cfg, err := config.DefaultConfig() 875 require.NoError(t, err) 876 // refresh cached app-specific score every 100 milliseconds to speed up the test. 877 cfg.NetworkConfig.GossipSub.ScoringParameters.ScoringRegistryParameters.AppSpecificScore.ScoreTTL = 100 * time.Millisecond 878 879 reg, spamRecords, _ := newGossipSubAppSpecificScoreRegistry(t, 880 cfg.NetworkConfig.GossipSub.ScoringParameters, 881 func() p2p.GossipSubSpamRecord { 882 return p2p.GossipSubSpamRecord{ 883 Decay: 0.02, // we choose a small decay value to speed up the test. 884 Penalty: 0, 885 } 886 }, 887 withStakedIdentities(peerID), 888 withInvalidSubscriptions(peerID)) // the peer id has an invalid subscription 889 890 // starts the registry. 891 ctx, cancel := context.WithCancel(context.Background()) 892 signalerCtx := irrecoverable.NewMockSignalerContext(t, ctx) 893 reg.Start(signalerCtx) 894 unittest.RequireCloseBefore(t, reg.Ready(), 1*time.Second, "failed to start GossipSubAppSpecificScoreRegistry") 895 896 scoreOptParameters := cfg.NetworkConfig.GossipSub.ScoringParameters.PeerScoring.Protocol.AppSpecificScore 897 898 // initially, the app specific score should be the default invalid subscription penalty. 899 require.Eventually(t, func() bool { 900 score := reg.AppSpecificScoreFunc()(peerID) 901 return score == scoreOptParameters.InvalidSubscriptionPenalty 902 }, 5*time.Second, 100*time.Millisecond) 903 904 // report a misbehavior for the peer id. 905 reg.OnInvalidControlMessageNotification(&p2p.InvCtrlMsgNotif{ 906 PeerID: peerID, 907 MsgType: p2pmsg.CtrlMsgGraft, 908 }) 909 910 // with reported spam, the app specific score should be the default invalid subscription penalty + the spam penalty. 911 require.Eventually(t, func() bool { 912 score := reg.AppSpecificScoreFunc()(peerID) 913 // Ideally, the score should be the sum of the default invalid subscription penalty and the graft penalty, however, 914 // due to exponential decay of the spam penalty and asynchronous update the app specific score; score should be in the range of [scoring. 915 // (DefaultInvalidSubscriptionPenalty+penaltyValueFixtures().GraftMisbehaviour, scoring.DefaultInvalidSubscriptionPenalty). 916 return score < scoreOptParameters.InvalidSubscriptionPenalty && score > scoreOptParameters.InvalidSubscriptionPenalty+penaltyValueFixtures().GraftMisbehaviour 917 }, 5*time.Second, 100*time.Millisecond) 918 919 require.Eventually(t, func() bool { 920 // the spam penalty should eventually decay to zero. 921 r, err, ok := spamRecords.Get(peerID) 922 return ok && err == nil && r.Penalty == 0.0 923 }, 5*time.Second, 100*time.Millisecond) 924 925 require.Eventually(t, func() bool { 926 // when the spam penalty is decayed to zero, the app specific penalty of the node should only contain the default invalid subscription penalty. 927 return reg.AppSpecificScoreFunc()(peerID) == scoreOptParameters.UnknownIdentityPenalty 928 }, 5*time.Second, 100*time.Millisecond) 929 930 // the spam penalty should now be zero in spamRecords. 931 record, err, ok := spamRecords.Get(peerID) // get the record from the spamRecords. 932 assert.True(t, ok) 933 assert.NoError(t, err) 934 assert.Equal(t, 0.0, record.Penalty) // penalty should be zero. 935 936 // stop the registry. 937 cancel() 938 unittest.RequireCloseBefore(t, reg.Done(), 1*time.Second, "failed to stop GossipSubAppSpecificScoreRegistry") 939 } 940 941 // TestScoreRegistry_TestSpamRecordDecayAdjustment ensures that spam record decay is increased each time a peers score reaches the scoring.IncreaseDecayThreshold eventually 942 // sustained misbehavior will result in the spam record decay reaching the minimum decay speed .99, and the decay speed is reset to the max decay speed .8. 943 func TestScoreRegistry_TestSpamRecordDecayAdjustment(t *testing.T) { 944 cfg, err := config.DefaultConfig() 945 require.NoError(t, err) 946 // refresh cached app-specific score every 100 milliseconds to speed up the test. 947 cfg.NetworkConfig.GossipSub.ScoringParameters.ScoringRegistryParameters.AppSpecificScore.ScoreTTL = 100 * time.Millisecond 948 // increase configured DecayRateReductionFactor so that the decay time is increased faster 949 cfg.NetworkConfig.GossipSub.ScoringParameters.ScoringRegistryParameters.SpamRecordCache.Decay.DecayRateReductionFactor = .1 950 cfg.NetworkConfig.GossipSub.ScoringParameters.ScoringRegistryParameters.SpamRecordCache.Decay.PenaltyDecayEvaluationPeriod = time.Second 951 maximumSpamPenaltyDecayFactor := cfg.NetworkConfig.GossipSub.ScoringParameters.ScoringRegistryParameters.SpamRecordCache.Decay.MaximumSpamPenaltyDecayFactor 952 peer1 := unittest.PeerIdFixture(t) 953 peer2 := unittest.PeerIdFixture(t) 954 reg, spamRecords, _ := newGossipSubAppSpecificScoreRegistry(t, 955 cfg.NetworkConfig.GossipSub.ScoringParameters, 956 scoring.InitAppScoreRecordStateFunc(maximumSpamPenaltyDecayFactor), 957 withStakedIdentities(peer1, peer2), 958 withValidSubscriptions(peer1, peer2)) 959 960 ctx, cancel := context.WithCancel(context.Background()) 961 signalerCtx := irrecoverable.NewMockSignalerContext(t, ctx) 962 reg.Start(signalerCtx) 963 unittest.RequireCloseBefore(t, reg.Ready(), 1*time.Second, "failed to start GossipSubAppSpecificScoreRegistry") 964 965 // initially, the spamRecords should not have the peer ids. 966 assert.False(t, spamRecords.Has(peer1)) 967 assert.False(t, spamRecords.Has(peer2)) 968 969 scoreOptParameters := cfg.NetworkConfig.GossipSub.ScoringParameters.PeerScoring.Protocol.AppSpecificScore 970 scoringRegistryParameters := cfg.NetworkConfig.GossipSub.ScoringParameters.ScoringRegistryParameters 971 // since the both peers do not have a spam record, their app specific score should be the max app specific reward, which 972 // is the default reward for a staked peer that has valid subscriptions. 973 require.Eventually(t, func() bool { 974 // when the spam penalty is decayed to zero, the app specific penalty of the node should only contain the unknown identity penalty. 975 return scoreOptParameters.MaxAppSpecificReward == reg.AppSpecificScoreFunc()(peer1) && scoreOptParameters.MaxAppSpecificReward == reg.AppSpecificScoreFunc()(peer2) 976 }, 5*time.Second, 100*time.Millisecond) 977 978 // simulate sustained malicious activity from peer1, eventually the decay speed 979 // for a spam record should be reduced to the MinimumSpamPenaltyDecayFactor 980 prevDecay := scoringRegistryParameters.SpamRecordCache.Decay.MaximumSpamPenaltyDecayFactor 981 tolerance := 0.1 982 983 require.Eventually(t, func() bool { 984 reg.OnInvalidControlMessageNotification(&p2p.InvCtrlMsgNotif{ 985 PeerID: peer1, 986 MsgType: p2pmsg.CtrlMsgPrune, 987 }) 988 989 // the spam penalty should eventually updated in the spamRecords 990 record, err, ok := spamRecords.Get(peer1) 991 require.NoError(t, err) 992 if !ok { 993 return false 994 } 995 if math.Abs(prevDecay-record.Decay) > tolerance { 996 return false 997 } 998 prevDecay = record.Decay 999 return record.Decay == scoringRegistryParameters.SpamRecordCache.Decay.MinimumSpamPenaltyDecayFactor 1000 }, 5*time.Second, 500*time.Millisecond) 1001 1002 // initialize a spam record for peer2 1003 reg.OnInvalidControlMessageNotification(&p2p.InvCtrlMsgNotif{ 1004 PeerID: peer2, 1005 MsgType: p2pmsg.CtrlMsgPrune, 1006 }) 1007 1008 // eventually the spam record should appear in the cache 1009 require.Eventually(t, func() bool { 1010 _, err, ok := spamRecords.Get(peer2) 1011 require.NoError(t, err) 1012 return ok 1013 }, 5*time.Second, 10*time.Millisecond) 1014 1015 // reduce penalty and increase Decay to scoring.MinimumSpamPenaltyDecayFactor 1016 record, err := spamRecords.Adjust(peer2, func(record p2p.GossipSubSpamRecord) p2p.GossipSubSpamRecord { 1017 record.Penalty = -.1 1018 record.Decay = scoringRegistryParameters.SpamRecordCache.Decay.MinimumSpamPenaltyDecayFactor 1019 return record 1020 }) 1021 require.NoError(t, err) 1022 require.True(t, record.Decay == scoringRegistryParameters.SpamRecordCache.Decay.MinimumSpamPenaltyDecayFactor) 1023 require.True(t, record.Penalty == -.1) 1024 // simulate sustained good behavior from peer 2, each time the spam record is read from the cache 1025 // using Get method the record penalty will be decayed until it is eventually reset to 1026 // 0 at this point the decay speed for the record should be reset to MaximumSpamPenaltyDecayFactor 1027 // eventually after penalty reaches the skipDecaThreshold the record decay will be reset to scoringRegistryParameters.MaximumSpamPenaltyDecayFactor 1028 require.Eventually(t, func() bool { 1029 record, err, ok := spamRecords.Get(peer2) 1030 require.NoError(t, err) 1031 require.True(t, ok) 1032 return record.Decay == scoringRegistryParameters.SpamRecordCache.Decay.MaximumSpamPenaltyDecayFactor && 1033 record.Penalty == 0 && 1034 record.LastDecayAdjustment.IsZero() 1035 }, 5*time.Second, time.Second) 1036 1037 // ensure decay can be reduced again after recovery for peerID 2 1038 require.Eventually(t, func() bool { 1039 reg.OnInvalidControlMessageNotification(&p2p.InvCtrlMsgNotif{ 1040 PeerID: peer2, 1041 MsgType: p2pmsg.CtrlMsgPrune, 1042 }) 1043 // the spam penalty should eventually updated in the spamRecords 1044 record, err, ok := spamRecords.Get(peer1) 1045 require.NoError(t, err) 1046 if !ok { 1047 return false 1048 } 1049 return record.Decay == scoringRegistryParameters.SpamRecordCache.Decay.MinimumSpamPenaltyDecayFactor 1050 }, 5*time.Second, 500*time.Millisecond) 1051 1052 // stop the registry. 1053 cancel() 1054 unittest.RequireCloseBefore(t, reg.Done(), 1*time.Second, "failed to stop GossipSubAppSpecificScoreRegistry") 1055 } 1056 1057 // TestPeerSpamPenaltyClusterPrefixed evaluates the application-specific penalty calculation for a node when a spam record is present 1058 // for cluster-prefixed topics. In the case of an invalid control message notification marked as cluster-prefixed, 1059 // the application-specific penalty should be reduced by the default reduction factor. This test verifies the accurate computation 1060 // of the application-specific score under these conditions. 1061 func TestPeerSpamPenaltyClusterPrefixed(t *testing.T) { 1062 ctlMsgTypes := p2pmsg.ControlMessageTypes() 1063 peerIds := unittest.PeerIdFixtures(t, len(ctlMsgTypes)) 1064 1065 cfg, err := config.DefaultConfig() 1066 require.NoError(t, err) 1067 // refresh cached app-specific score every 100 milliseconds to speed up the test. 1068 cfg.NetworkConfig.GossipSub.ScoringParameters.ScoringRegistryParameters.AppSpecificScore.ScoreTTL = 100 * time.Millisecond 1069 maximumSpamPenaltyDecayFactor := cfg.NetworkConfig.GossipSub.ScoringParameters.ScoringRegistryParameters.SpamRecordCache.Decay.MaximumSpamPenaltyDecayFactor 1070 reg, spamRecords, _ := newGossipSubAppSpecificScoreRegistry(t, 1071 cfg.NetworkConfig.GossipSub.ScoringParameters, 1072 scoring.InitAppScoreRecordStateFunc(maximumSpamPenaltyDecayFactor), 1073 withStakedIdentities(peerIds...), 1074 withValidSubscriptions(peerIds...)) 1075 1076 // starts the registry. 1077 ctx, cancel := context.WithCancel(context.Background()) 1078 signalerCtx := irrecoverable.NewMockSignalerContext(t, ctx) 1079 reg.Start(signalerCtx) 1080 unittest.RequireCloseBefore(t, reg.Ready(), 1*time.Second, "failed to start GossipSubAppSpecificScoreRegistry") 1081 1082 scoreOptParameters := cfg.NetworkConfig.GossipSub.ScoringParameters.PeerScoring.Protocol.AppSpecificScore 1083 1084 for _, peerID := range peerIds { 1085 // initially, the spamRecords should not have the peer id. 1086 assert.False(t, spamRecords.Has(peerID)) 1087 // since the peer id does not have a spam record, the app specific score should (eventually, due to caching) be the max app specific reward, which 1088 // is the default reward for a staked peer that has valid subscriptions. 1089 require.Eventually(t, func() bool { 1090 // calling the app specific score function when there is no app specific score in the cache should eventually update the cache. 1091 score := reg.AppSpecificScoreFunc()(peerID) 1092 // since the peer id does not have a spam record, the app specific score should be the max app specific reward, which 1093 // is the default reward for a staked peer that has valid subscriptions. 1094 return score == scoreOptParameters.MaxAppSpecificReward 1095 }, 5*time.Second, 100*time.Millisecond) 1096 1097 } 1098 1099 // Report consecutive misbehavior's for the specified peer ID. Two misbehavior's are reported concurrently: 1100 // 1. With IsClusterPrefixed set to false, ensuring the penalty applied to the application-specific score is not reduced. 1101 // 2. With IsClusterPrefixed set to true, reducing the penalty added to the overall app-specific score by the default reduction factor. 1102 for i, ctlMsgType := range ctlMsgTypes { 1103 peerID := peerIds[i] 1104 var wg sync.WaitGroup 1105 wg.Add(2) 1106 go func() { 1107 defer wg.Done() 1108 reg.OnInvalidControlMessageNotification(&p2p.InvCtrlMsgNotif{ 1109 PeerID: peerID, 1110 MsgType: ctlMsgType, 1111 TopicType: p2p.CtrlMsgNonClusterTopicType, 1112 }) 1113 }() 1114 go func() { 1115 defer wg.Done() 1116 reg.OnInvalidControlMessageNotification(&p2p.InvCtrlMsgNotif{ 1117 PeerID: peerID, 1118 MsgType: ctlMsgType, 1119 TopicType: p2p.CtrlMsgTopicTypeClusterPrefixed, 1120 }) 1121 }() 1122 unittest.RequireReturnsBefore(t, wg.Wait, 100*time.Millisecond, "timed out waiting for goroutines to finish") 1123 1124 // expected penalty should be penaltyValueFixtures().GraftMisbehaviour * (1 + clusterReductionFactor) 1125 expectedPenalty := penaltyValueFixture(ctlMsgType) * (1 + penaltyValueFixtures().ClusterPrefixedReductionFactor) 1126 1127 require.Eventually(t, func() bool { 1128 // the notification is processed asynchronously, and the penalty should eventually be updated in the spamRecords 1129 record, err, ok := spamRecords.Get(peerID) // get the record from the spamRecords. 1130 if !ok { 1131 return false 1132 } 1133 require.NoError(t, err) 1134 if !unittest.AreNumericallyClose(expectedPenalty, record.Penalty, 10e-2) { 1135 return false 1136 } 1137 require.Equal(t, scoring.InitAppScoreRecordStateFunc(maximumSpamPenaltyDecayFactor)().Decay, record.Decay) // decay should be initialized to the initial state. 1138 return true 1139 }, 5*time.Second, 100*time.Millisecond) 1140 1141 // this peer has a spam record, with no subscription penalty. Hence, the app specific score should only be the spam penalty, 1142 // and the peer should be deprived of the default reward for its valid staked role. 1143 score := reg.AppSpecificScoreFunc()(peerID) 1144 tolerance := 0.02 // 0.1% 1145 if expectedPenalty == 0 { 1146 assert.Less(t, math.Abs(expectedPenalty), tolerance) 1147 } else { 1148 assert.Less(t, math.Abs(expectedPenalty-score)/expectedPenalty, tolerance) 1149 } 1150 } 1151 1152 // stop the registry. 1153 cancel() 1154 unittest.RequireCloseBefore(t, reg.Done(), 1*time.Second, "failed to stop GossipSubAppSpecificScoreRegistry") 1155 } 1156 1157 // TestScoringRegistrySilencePeriod ensures that the scoring registry does not penalize nodes during the silence period, and 1158 // starts to penalize nodes only after the silence period is over. 1159 func TestScoringRegistrySilencePeriod(t *testing.T) { 1160 unittest.SkipUnless(t, unittest.TEST_FLAKY, "flakey tests") 1161 peerID := unittest.PeerIdFixture(t) 1162 silenceDuration := 5 * time.Second 1163 silencedNotificationLogs := atomic.NewInt32(0) 1164 hook := zerolog.HookFunc(func(e *zerolog.Event, level zerolog.Level, message string) { 1165 if level == zerolog.TraceLevel { 1166 if message == scoring.NotificationSilencedMsg { 1167 silencedNotificationLogs.Inc() 1168 } 1169 } 1170 }) 1171 logger := zerolog.New(io.Discard).Level(zerolog.TraceLevel).Hook(hook) 1172 1173 cfg, err := config.DefaultConfig() 1174 require.NoError(t, err) 1175 // refresh cached app-specific score every 10 milliseconds to speed up the test. 1176 cfg.NetworkConfig.GossipSub.ScoringParameters.ScoringRegistryParameters.AppSpecificScore.ScoreTTL = 10 * time.Millisecond 1177 cfg.NetworkConfig.GossipSub.ScoringParameters.ScoringRegistryParameters.SpamRecordCache.Decay.MaximumSpamPenaltyDecayFactor = .99 1178 maximumSpamPenaltyDecayFactor := cfg.NetworkConfig.GossipSub.ScoringParameters.ScoringRegistryParameters.SpamRecordCache.Decay.MaximumSpamPenaltyDecayFactor 1179 reg, spamRecords, _ := newGossipSubAppSpecificScoreRegistry(t, 1180 cfg.NetworkConfig.GossipSub.ScoringParameters, 1181 scoring.InitAppScoreRecordStateFunc(maximumSpamPenaltyDecayFactor), 1182 withUnknownIdentity(peerID), 1183 withInvalidSubscriptions(peerID), 1184 func(cfg *scoring.GossipSubAppSpecificScoreRegistryConfig) { 1185 // we set the scoring registry silence duration 10 seconds 1186 // the peer is not expected to be penalized for the first 5 seconds of the test 1187 // after that an invalid control message notification is processed and the peer 1188 // should be penalized 1189 cfg.ScoringRegistryStartupSilenceDuration = silenceDuration 1190 // hooked logger will capture the number of logs related to ignored notifications 1191 cfg.Logger = logger 1192 }) 1193 1194 ctx, cancel := context.WithCancel(context.Background()) 1195 signalerCtx := irrecoverable.NewMockSignalerContext(t, ctx) 1196 defer stopRegistry(t, cancel, reg) 1197 // capture approximate registry start time 1198 reg.Start(signalerCtx) 1199 unittest.RequireCloseBefore(t, reg.Ready(), 1*time.Second, "registry did not start in time") 1200 1201 registryStartTime := time.Now() 1202 expectedNumOfSilencedNotif := 0 1203 // while we are in the silence period all notifications should be ignored and the 1204 // invalid subscription penalty should not be applied to the app specific score 1205 // we ensure we stay within the silence duration by iterating only up until 1 second 1206 // before silence period is over 1207 for time.Since(registryStartTime) < (silenceDuration - time.Second) { 1208 // report a misbehavior for the peer id. 1209 reg.OnInvalidControlMessageNotification(&p2p.InvCtrlMsgNotif{ 1210 PeerID: peerID, 1211 MsgType: p2pmsg.CtrlMsgGraft, 1212 }) 1213 expectedNumOfSilencedNotif++ 1214 // spam records should not be created during the silence period 1215 _, err, ok := spamRecords.Get(peerID) 1216 assert.False(t, ok) 1217 assert.NoError(t, err) 1218 // initially, the app specific score should be the default invalid subscription penalty. 1219 require.Equal(t, float64(0), reg.AppSpecificScoreFunc()(peerID)) 1220 } 1221 1222 invalidSubscriptionPenalty := cfg.NetworkConfig.GossipSub.ScoringParameters.PeerScoring.Protocol.AppSpecificScore.InvalidSubscriptionPenalty 1223 1224 require.Eventually(t, func() bool { 1225 // we expect to have logged a debug message for all notifications ignored. 1226 require.Equal(t, int32(expectedNumOfSilencedNotif), silencedNotificationLogs.Load()) 1227 // after silence period the invalid subscription penalty should be applied to the app specific score 1228 return invalidSubscriptionPenalty == reg.AppSpecificScoreFunc()(peerID) 1229 }, 2*time.Second, 200*time.Millisecond) 1230 1231 // after silence period the peer has spam record as well as an unknown identity. Hence, the app specific score should be the spam penalty— 1232 // and the staking penalty. 1233 reg.OnInvalidControlMessageNotification(&p2p.InvCtrlMsgNotif{ 1234 PeerID: peerID, 1235 MsgType: p2pmsg.CtrlMsgGraft, 1236 }) 1237 1238 require.Eventually(t, func() bool { 1239 return spamRecords.Has(peerID) 1240 }, time.Second, 100*time.Millisecond) 1241 1242 // the penalty should now be applied and spam records created. 1243 record, err, ok := spamRecords.Get(peerID) 1244 assert.True(t, ok) 1245 assert.NoError(t, err) 1246 expectedPenalty := penaltyValueFixtures().GraftMisbehaviour 1247 unittest.RequireNumericallyClose(t, expectedPenalty, record.Penalty, 10e-3) 1248 assert.Equal(t, scoring.InitAppScoreRecordStateFunc(maximumSpamPenaltyDecayFactor)().Decay, record.Decay) // decay should be initialized to the initial state. 1249 } 1250 1251 // withStakedIdentities returns a function that sets the identity provider to return staked identities for the given peer ids. 1252 // It is used for testing purposes, and causes the given peer id to benefit from the staked identity reward in GossipSub. 1253 func withStakedIdentities(peerIds ...peer.ID) func(cfg *scoring.GossipSubAppSpecificScoreRegistryConfig) { 1254 return func(cfg *scoring.GossipSubAppSpecificScoreRegistryConfig) { 1255 cfg.IdProvider.(*mock.IdentityProvider).On("ByPeerID", testifymock.AnythingOfType("peer.ID")). 1256 Return(func(pid peer.ID) *flow.Identity { 1257 for _, peerID := range peerIds { 1258 if peerID == pid { 1259 return unittest.IdentityFixture() 1260 } 1261 } 1262 return nil 1263 }, func(pid peer.ID) bool { 1264 for _, peerID := range peerIds { 1265 if peerID == pid { 1266 return true 1267 } 1268 } 1269 return false 1270 }).Maybe() 1271 } 1272 } 1273 1274 // withValidSubscriptions returns a function that sets the subscription validator to return nil for the given peer ids. 1275 // It is used for testing purposes and causes the given peer id to never be penalized for subscribing to invalid topics. 1276 func withValidSubscriptions(peerIds ...peer.ID) func(cfg *scoring.GossipSubAppSpecificScoreRegistryConfig) { 1277 return func(cfg *scoring.GossipSubAppSpecificScoreRegistryConfig) { 1278 cfg.Validator.(*mockp2p.SubscriptionValidator). 1279 On("CheckSubscribedToAllowedTopics", testifymock.AnythingOfType("peer.ID"), testifymock.Anything). 1280 Return(func(pid peer.ID, _ flow.Role) error { 1281 for _, peerID := range peerIds { 1282 if peerID == pid { 1283 return nil 1284 } 1285 } 1286 return fmt.Errorf("invalid subscriptions") 1287 }).Maybe() 1288 } 1289 } 1290 1291 // withUnknownIdentity returns a function that sets the identity provider to return an error for the given peer id. 1292 // It is used for testing purposes, and causes the given peer id to be penalized for not having a staked identity. 1293 func withUnknownIdentity(peer peer.ID) func(cfg *scoring.GossipSubAppSpecificScoreRegistryConfig) { 1294 return func(cfg *scoring.GossipSubAppSpecificScoreRegistryConfig) { 1295 cfg.IdProvider.(*mock.IdentityProvider).On("ByPeerID", peer).Return(nil, false).Maybe() 1296 } 1297 } 1298 1299 // withInvalidSubscriptions returns a function that sets the subscription validator to return an error for the given peer id. 1300 // It is used for testing purposes and causes the given peer id to be penalized for subscribing to invalid topics. 1301 func withInvalidSubscriptions(peer peer.ID) func(cfg *scoring.GossipSubAppSpecificScoreRegistryConfig) { 1302 return func(cfg *scoring.GossipSubAppSpecificScoreRegistryConfig) { 1303 cfg.Validator.(*mockp2p.SubscriptionValidator).On("CheckSubscribedToAllowedTopics", 1304 peer, 1305 testifymock.Anything).Return(fmt.Errorf("invalid subscriptions")).Maybe() 1306 } 1307 } 1308 1309 // newGossipSubAppSpecificScoreRegistry creates a new instance of GossipSubAppSpecificScoreRegistry along with its associated 1310 // GossipSubSpamRecordCache and AppSpecificScoreCache. This function is primarily used in testing scenarios to set up a controlled 1311 // environment for evaluating the behavior of the GossipSub scoring mechanism. 1312 // 1313 // The function accepts a variable number of options to configure the GossipSubAppSpecificScoreRegistryConfig, allowing for 1314 // customization of the registry's behavior in tests. These options can modify various aspects of the configuration, such as 1315 // penalty values, identity providers, validators, and caching mechanisms. 1316 // 1317 // Parameters: 1318 // - t *testing.T: The test context, used for asserting the absence of errors during the setup. 1319 // - params p2pconfig.ScoringParameters: The scoring parameters used to configure the registry. 1320 // - initFunction scoring.SpamRecordInitFunc: The function used to initialize the spam records. 1321 // - opts ...func(*scoring.GossipSubAppSpecificScoreRegistryConfig): A variadic set of functions that modify the registry's configuration. 1322 // 1323 // Returns: 1324 // - *scoring.GossipSubAppSpecificScoreRegistry: The configured GossipSub application-specific score registry. 1325 // - *netcache.GossipSubSpamRecordCache: The cache used for storing spam records. 1326 // - *internal.AppSpecificScoreCache: The cache for storing application-specific scores. 1327 // 1328 // This function initializes and configures the scoring registry with default and test-specific settings. It sets up a spam record cache 1329 // and an application-specific score cache with predefined sizes and functionalities. The function also configures the scoring parameters 1330 // with test-specific values, particularly modifying the ScoreTTL value for the purpose of the tests. The creation and configuration of 1331 // the GossipSubAppSpecificScoreRegistry are validated to ensure no errors occur during the process. 1332 func newGossipSubAppSpecificScoreRegistry(t *testing.T, 1333 params p2pconfig.ScoringParameters, 1334 initFunction scoring.SpamRecordInitFunc, 1335 opts ...func(*scoring.GossipSubAppSpecificScoreRegistryConfig)) (*scoring.GossipSubAppSpecificScoreRegistry, 1336 *netcache.GossipSubSpamRecordCache, 1337 *internal.AppSpecificScoreCache) { 1338 cache := netcache.NewGossipSubSpamRecordCache(100, 1339 unittest.Logger(), 1340 metrics.NewNoopCollector(), 1341 initFunction, 1342 scoring.DefaultDecayFunction(params.ScoringRegistryParameters.SpamRecordCache.Decay)) 1343 appSpecificScoreCache := internal.NewAppSpecificScoreCache(100, unittest.Logger(), metrics.NewNoopCollector()) 1344 1345 validator := mockp2p.NewSubscriptionValidator(t) 1346 validator.On("Start", testifymock.Anything).Return().Maybe() 1347 done := make(chan struct{}) 1348 close(done) 1349 f := func() <-chan struct{} { 1350 return done 1351 } 1352 validator.On("Ready").Return(f()).Maybe() 1353 validator.On("Done").Return(f()).Maybe() 1354 cfg := &scoring.GossipSubAppSpecificScoreRegistryConfig{ 1355 Logger: unittest.Logger(), 1356 Penalty: penaltyValueFixtures(), 1357 IdProvider: mock.NewIdentityProvider(t), 1358 Validator: validator, 1359 AppScoreCacheFactory: func() p2p.GossipSubApplicationSpecificScoreCache { 1360 return appSpecificScoreCache 1361 }, 1362 SpamRecordCacheFactory: func() p2p.GossipSubSpamRecordCache { 1363 return cache 1364 }, 1365 GetDuplicateMessageCount: func(id peer.ID) float64 { 1366 return 0 1367 }, 1368 Parameters: params.ScoringRegistryParameters.AppSpecificScore, 1369 HeroCacheMetricsFactory: metrics.NewNoopHeroCacheMetricsFactory(), 1370 NetworkingType: network.PrivateNetwork, 1371 AppSpecificScoreParams: params.PeerScoring.Protocol.AppSpecificScore, 1372 DuplicateMessageThreshold: params.PeerScoring.Protocol.AppSpecificScore.DuplicateMessageThreshold, 1373 Collector: metrics.NewNoopCollector(), 1374 ScoringRegistryStartupSilenceDuration: 0, // turn off silence period by default 1375 } 1376 for _, opt := range opts { 1377 opt(cfg) 1378 } 1379 1380 reg, err := scoring.NewGossipSubAppSpecificScoreRegistry(cfg) 1381 require.NoError(t, err, "failed to create GossipSubAppSpecificScoreRegistry") 1382 1383 return reg, cache, appSpecificScoreCache 1384 } 1385 1386 // penaltyValueFixtures returns a set of penalty values for testing purposes. 1387 // The values are not realistic. The important thing is that they are different from each other. This is to make sure 1388 // that the tests are not passing because of the default values. 1389 func penaltyValueFixtures() p2pconfig.MisbehaviourPenalties { 1390 return p2pconfig.MisbehaviourPenalties{ 1391 GraftMisbehaviour: -100, 1392 PruneMisbehaviour: -50, 1393 IHaveMisbehaviour: -20, 1394 IWantMisbehaviour: -10, 1395 ClusterPrefixedReductionFactor: .5, 1396 PublishMisbehaviour: -10, 1397 } 1398 } 1399 1400 // penaltyValueFixture returns the set penalty of the provided control message type returned from the fixture func penaltyValueFixtures. 1401 func penaltyValueFixture(msgType p2pmsg.ControlMessageType) float64 { 1402 penaltyValues := penaltyValueFixtures() 1403 switch msgType { 1404 case p2pmsg.CtrlMsgGraft: 1405 return penaltyValues.GraftMisbehaviour 1406 case p2pmsg.CtrlMsgPrune: 1407 return penaltyValues.PruneMisbehaviour 1408 case p2pmsg.CtrlMsgIHave: 1409 return penaltyValues.IHaveMisbehaviour 1410 case p2pmsg.CtrlMsgIWant: 1411 return penaltyValues.IWantMisbehaviour 1412 case p2pmsg.RpcPublishMessage: 1413 return penaltyValues.PublishMisbehaviour 1414 default: 1415 return penaltyValues.ClusterPrefixedReductionFactor 1416 } 1417 } 1418 1419 func stopRegistry(t *testing.T, cancel context.CancelFunc, registry *scoring.GossipSubAppSpecificScoreRegistry) { 1420 cancel() 1421 unittest.RequireCloseBefore(t, registry.Done(), 5*time.Second, "registry did not stop") 1422 }