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