github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/engine/common/synchronization/engine_spam_test.go (about) 1 package synchronization 2 3 import ( 4 "context" 5 "fmt" 6 "testing" 7 "time" 8 9 "github.com/onflow/flow-go/model/flow" 10 11 "github.com/stretchr/testify/assert" 12 "github.com/stretchr/testify/mock" 13 "github.com/stretchr/testify/require" 14 15 "github.com/onflow/flow-go/model/chainsync" 16 "github.com/onflow/flow-go/model/messages" 17 "github.com/onflow/flow-go/module/irrecoverable" 18 "github.com/onflow/flow-go/network/channels" 19 "github.com/onflow/flow-go/utils/rand" 20 "github.com/onflow/flow-go/utils/unittest" 21 ) 22 23 // TestLoad_Process_SyncRequest_HigherThanReceiver_OutsideTolerance_AlwaysReportSpam is a load test that ensures that 24 // a misbehavior report is generated every time when the probability factor is set to 1.0. 25 // It checks that a sync request that's higher than the receiver's height doesn't trigger a response, even if outside tolerance. 26 func (ss *SyncSuite) TestLoad_Process_SyncRequest_HigherThanReceiver_OutsideTolerance_AlwaysReportSpam() { 27 ctx, cancel := irrecoverable.NewMockSignalerContextWithCancel(ss.T(), context.Background()) 28 ss.e.Start(ctx) 29 unittest.AssertClosesBefore(ss.T(), ss.e.Ready(), time.Second) 30 defer cancel() 31 32 load := 1000 33 34 // reset misbehavior report counter for each subtest 35 misbehaviorsCounter := 0 36 37 for i := 0; i < load; i++ { 38 // generate origin and request message 39 originID := unittest.IdentifierFixture() 40 41 nonce, err := rand.Uint64() 42 require.NoError(ss.T(), err, "should generate nonce") 43 44 req := &messages.SyncRequest{ 45 Nonce: nonce, 46 Height: 0, 47 } 48 49 // if request height is higher than local finalized, we should not respond 50 req.Height = ss.head.Height + 1 51 52 ss.core.On("HandleHeight", ss.head, req.Height) 53 ss.core.On("WithinTolerance", ss.head, req.Height).Return(false) 54 ss.con.AssertNotCalled(ss.T(), "Unicast", mock.Anything, mock.Anything) 55 56 // maybe function calls that might or might not occur over the course of the load test 57 ss.core.On("ScanPending", ss.head).Return([]chainsync.Range{}, []chainsync.Batch{}).Maybe() 58 ss.con.On("Multicast", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe() 59 60 // count misbehavior reports over the course of a load test 61 ss.con.On("ReportMisbehavior", mock.Anything).Return(mock.Anything).Run( 62 func(args mock.Arguments) { 63 misbehaviorsCounter++ 64 }, 65 ) 66 67 // force creating misbehavior report by setting syncRequestProb to 1.0 (i.e. report misbehavior 100% of the time) 68 ss.e.spamDetectionConfig.syncRequestProb = 1.0 69 70 require.NoError(ss.T(), ss.e.Process(channels.SyncCommittee, originID, req)) 71 } 72 73 ss.core.AssertExpectations(ss.T()) 74 ss.con.AssertExpectations(ss.T()) 75 assert.Equal(ss.T(), misbehaviorsCounter, load) // should generate misbehavior report every time 76 } 77 78 // TestLoad_Process_SyncRequest_HigherThanReceiver_OutsideTolerance_SometimesReportSpam is a load test that ensures that a 79 // misbehavior report is generated an appropriate range of times when the probability factor is set to different values. 80 // It checks that a sync request that's higher than the receiver's height doesn't trigger a response, even if 81 // outside tolerance. 82 func (ss *SyncSuite) TestLoad_Process_SyncRequest_HigherThanReceiver_OutsideTolerance_SometimesReportSpam() { 83 ctx, cancel := irrecoverable.NewMockSignalerContextWithCancel(ss.T(), context.Background()) 84 ss.e.Start(ctx) 85 unittest.AssertClosesBefore(ss.T(), ss.e.Ready(), time.Second) 86 defer cancel() 87 88 load := 1000 89 90 // each load test is a load group that contains a set of factors with unique values to test how many misbehavior reports are generated 91 // Due to the probabilistic nature of how misbehavior reports are generated, we use an expected lower and 92 // upper range of expected misbehaviors to determine if the load test passed or failed. As long as the number of misbehavior reports 93 // falls within the expected range, the load test passes. 94 type loadGroup struct { 95 syncRequestProbabilityFactor float32 // probability factor that will be used to generate misbehavior reports 96 expectedMisbehaviorsLower int // lower range of expected misbehavior reports 97 expectedMisbehaviorsUpper int // upper range of expected misbehavior reports 98 } 99 100 loadGroups := []loadGroup{} 101 102 // expect to never get misbehavior report 103 loadGroups = append(loadGroups, loadGroup{0.0, 0, 0}) 104 105 // expect to get misbehavior report about 0.1% of the time (1 in 1000 requests) 106 loadGroups = append(loadGroups, loadGroup{0.001, 0, 7}) 107 108 // expect to get misbehavior report about 1% of the time 109 loadGroups = append(loadGroups, loadGroup{0.01, 5, 15}) 110 111 // expect to get misbehavior report about 10% of the time 112 loadGroups = append(loadGroups, loadGroup{0.1, 75, 140}) 113 114 // expect to get misbehavior report about 50% of the time 115 loadGroups = append(loadGroups, loadGroup{0.5, 450, 550}) 116 117 // expect to get misbehavior report about 90% of the time 118 loadGroups = append(loadGroups, loadGroup{0.9, 850, 950}) 119 120 // reset misbehavior report counter for each subtest 121 misbehaviorsCounter := 0 122 123 for _, loadGroup := range loadGroups { 124 ss.T().Run(fmt.Sprintf("load test; pfactor=%f lower=%d upper=%d", loadGroup.syncRequestProbabilityFactor, loadGroup.expectedMisbehaviorsLower, loadGroup.expectedMisbehaviorsUpper), func(t *testing.T) { 125 for i := 0; i < load; i++ { 126 ss.T().Log("load iteration", i) 127 nonce, err := rand.Uint64() 128 require.NoError(ss.T(), err, "should generate nonce") 129 130 // generate origin and request message 131 originID := unittest.IdentifierFixture() 132 req := &messages.SyncRequest{ 133 Nonce: nonce, 134 Height: 0, 135 } 136 137 // if request height is higher than local finalized, we should not respond 138 req.Height = ss.head.Height + 1 139 140 ss.core.On("HandleHeight", ss.head, req.Height) 141 ss.core.On("WithinTolerance", ss.head, req.Height).Return(false) 142 ss.con.AssertNotCalled(ss.T(), "Unicast", mock.Anything, mock.Anything) 143 144 // maybe function calls that might or might not occur over the course of the load test 145 ss.core.On("ScanPending", ss.head).Return([]chainsync.Range{}, []chainsync.Batch{}).Maybe() 146 ss.con.On("Multicast", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe() 147 148 // count misbehavior reports over the course of a load test 149 ss.con.On("ReportMisbehavior", mock.Anything).Return(mock.Anything).Maybe().Run( 150 func(args mock.Arguments) { 151 misbehaviorsCounter++ 152 }, 153 ) 154 ss.e.spamDetectionConfig.syncRequestProb = loadGroup.syncRequestProbabilityFactor 155 require.NoError(ss.T(), ss.e.Process(channels.SyncCommittee, originID, req)) 156 } 157 158 // check function call expectations at the end of the load test; otherwise, load test would take much longer 159 ss.core.AssertExpectations(ss.T()) 160 ss.con.AssertExpectations(ss.T()) 161 162 // check that correct range of misbehavior reports were generated 163 // since we're using a probabilistic approach to generate misbehavior reports, we can't guarantee the exact number, 164 // so we check that it's within an expected range 165 ss.T().Logf("misbehaviors counter after load test: %d (expected lower bound: %d expected upper bound: %d)", misbehaviorsCounter, loadGroup.expectedMisbehaviorsLower, loadGroup.expectedMisbehaviorsUpper) 166 assert.GreaterOrEqual(ss.T(), misbehaviorsCounter, loadGroup.expectedMisbehaviorsLower) 167 assert.LessOrEqual(ss.T(), misbehaviorsCounter, loadGroup.expectedMisbehaviorsUpper) 168 169 misbehaviorsCounter = 0 // reset counter for next subtest 170 }) 171 } 172 } 173 174 // TestLoad_Process_RangeRequest_SometimesReportSpam is a load test that ensures that a misbehavior report is generated 175 // an appropriate range of times when the base probability factor and range are set to different values. 176 func (ss *SyncSuite) TestLoad_Process_RangeRequest_SometimesReportSpam() { 177 ctx, cancel := irrecoverable.NewMockSignalerContextWithCancel(ss.T(), context.Background()) 178 ss.e.Start(ctx) 179 unittest.AssertClosesBefore(ss.T(), ss.e.Ready(), time.Second) 180 defer cancel() 181 182 load := 1000 183 184 // each load test is a load group that contains a set of factors with unique values to test how many misbehavior reports are generated. 185 // Due to the probabilistic nature of how misbehavior reports are generated, we use an expected lower and 186 // upper range of expected misbehaviors to determine if the load test passed or failed. As long as the number of misbehavior reports 187 // falls within the expected range, the load test passes. 188 type loadGroup struct { 189 rangeRequestBaseProb float32 // base probability factor that will be used to calculate the final probability factor 190 expectedMisbehaviorsLower int // lower range of expected misbehavior reports 191 expectedMisbehaviorsUpper int // upper range of expected misbehavior reports 192 fromHeight uint64 // from height of the range request 193 toHeight uint64 // to height of the range request 194 } 195 196 loadGroups := []loadGroup{} 197 198 // using a very small range (1) with a 10% base probability factor, expect to almost never get misbehavior report, about 0.003% of the time (3 in 1000 requests) 199 // expected probability factor: 0.1 * ((10-9) + 1)/64 = 0.003125 200 loadGroups = append(loadGroups, loadGroup{0.1, 0, 15, 9, 10}) 201 202 // using a small range (10) with a 10% base probability factor, expect to get misbehavior report about 1.7% of the time (17 in 1000 requests) 203 // expected probability factor: 0.1 * ((11-1) + 1)/64 = 0.0171875 204 loadGroups = append(loadGroups, loadGroup{0.1, 5, 31, 1, 11}) 205 206 // using a large range (99) with a 10% base probability factor, expect to get misbehavior report about 15% of the time (150 in 1000 requests) 207 // expected probability factor: 0.1 * ((100-1) + 1)/64 = 0.15625 208 loadGroups = append(loadGroups, loadGroup{0.1, 110, 200, 1, 100}) 209 210 // using a flat range (0) (from height == to height) with a 1% base probability factor, expect to almost never get a misbehavior report, about 0.16% of the time (2 in 1000 requests) 211 // expected probability factor: 0.01 * ((1-1) + 1)/64 = 0.0015625 212 // Note: the expected upper misbehavior count is 5 even though the expected probability is close to 0 to cover outlier cases during the load test to avoid flakiness in CI. 213 // Due of the probabilistic nature of the load tests, you sometimes get edge cases (that cover outliers where out of a 1000 messages, up to 5 could be reported as spam. 214 // 5/1000 = 0.005 and the calculated probability is 0.00171875 which 2.9x as small. 215 loadGroups = append(loadGroups, loadGroup{0.01, 0, 5, 1, 1}) 216 217 // using a small range (10) with a 1% base probability factor, expect to almost never get misbehavior report, about 0.17% of the time (2 in 1000 requests) 218 // expected probability factor: 0.01 * ((11-1) + 1)/64 = 0.00171875 219 loadGroups = append(loadGroups, loadGroup{0.01, 0, 7, 1, 11}) 220 221 // using a very large range (999) with a 1% base probability factor, expect to get misbehavior report about 15% of the time (150 in 1000 requests) 222 // expected probability factor: 0.01 * ((1000-1) + 1)/64 = 0.15625 223 loadGroups = append(loadGroups, loadGroup{0.01, 110, 200, 1, 1000}) 224 225 // ALWAYS REPORT SPAM FOR INVALID RANGE REQUESTS OR RANGE REQUESTS THAT ARE FAR OUTSIDE OF THE TOLERANCE 226 227 // using an inverted range (from height > to height) always results in a misbehavior report, no matter how small the range is or how small the base probability factor is 228 loadGroups = append(loadGroups, loadGroup{0.001, 1000, 1000, 2, 1}) 229 230 // using a very large range (999) with a 10% base probability factor, expect to get misbehavior report 100% of the time (1000 in 1000 requests) 231 // expected probability factor: 0.1 * ((1000-1) + 1)/64 = 1.5625 232 loadGroups = append(loadGroups, loadGroup{0.1, 1000, 1000, 1, 1000}) 233 234 // reset misbehavior report counter for each subtest 235 misbehaviorsCounter := 0 236 237 for _, loadGroup := range loadGroups { 238 for i := 0; i < load; i++ { 239 ss.T().Log("load iteration", i) 240 241 nonce, err := rand.Uint64() 242 require.NoError(ss.T(), err, "should generate nonce") 243 244 // generate origin and request message 245 originID := unittest.IdentifierFixture() 246 req := &messages.RangeRequest{ 247 Nonce: nonce, 248 FromHeight: loadGroup.fromHeight, 249 ToHeight: loadGroup.toHeight, 250 } 251 252 // count misbehavior reports over the course of a load test 253 ss.con.On("ReportMisbehavior", mock.Anything).Return(mock.Anything).Maybe().Run( 254 func(args mock.Arguments) { 255 misbehaviorsCounter++ 256 }, 257 ) 258 ss.e.spamDetectionConfig.rangeRequestBaseProb = loadGroup.rangeRequestBaseProb 259 require.NoError(ss.T(), ss.e.Process(channels.SyncCommittee, originID, req)) 260 } 261 // check function call expectations at the end of the load test; otherwise, load test would take much longer 262 ss.core.AssertExpectations(ss.T()) 263 ss.con.AssertExpectations(ss.T()) 264 265 // check that correct range of misbehavior reports were generated 266 // since we're using a probabilistic approach to generate misbehavior reports, we can't guarantee the exact number, 267 // so we check that it's within an expected range 268 ss.T().Logf("misbehaviors counter after load test: %d (expected lower bound: %d expected upper bound: %d)", misbehaviorsCounter, loadGroup.expectedMisbehaviorsLower, loadGroup.expectedMisbehaviorsUpper) 269 assert.GreaterOrEqual(ss.T(), misbehaviorsCounter, loadGroup.expectedMisbehaviorsLower) 270 assert.LessOrEqual(ss.T(), misbehaviorsCounter, loadGroup.expectedMisbehaviorsUpper) 271 272 misbehaviorsCounter = 0 // reset counter for next subtest 273 } 274 } 275 276 // TestLoad_Process_BatchRequest_SometimesReportSpam is a load test that ensures that a misbehavior report is generated 277 // an appropriate range of times when the base probability factor and number of block IDs are set to different values. 278 func (ss *SyncSuite) TestLoad_Process_BatchRequest_SometimesReportSpam() { 279 ctx, cancel := irrecoverable.NewMockSignalerContextWithCancel(ss.T(), context.Background()) 280 ss.e.Start(ctx) 281 unittest.AssertClosesBefore(ss.T(), ss.e.Ready(), time.Second) 282 defer cancel() 283 284 load := 1000 285 286 // each load test is a load group that contains a set of factors with unique values to test how many misbehavior reports are generated. 287 // Due to the probabilistic nature of how misbehavior reports are generated, we use an expected lower and 288 // upper range of expected misbehaviors to determine if the load test passed or failed. As long as the number of misbehavior reports 289 // falls within the expected range, the load test passes. 290 type loadGroup struct { 291 batchRequestBaseProb float32 292 expectedMisbehaviorsLower int 293 expectedMisbehaviorsUpper int 294 blockIDs []flow.Identifier 295 } 296 297 loadGroups := []loadGroup{} 298 299 // using a very small batch request (1 block ID) with a 10% base probability factor, expect to almost never get misbehavior report, about 0.003% of the time (3 in 1000 requests) 300 // expected probability factor: 0.1 * ((10-9) + 1)/64 = 0.003125 301 loadGroups = append(loadGroups, loadGroup{0.1, 0, 15, repeatedBlockIDs(1)}) 302 303 // using a small batch request (10 block IDs) with a 10% base probability factor, expect to get misbehavior report about 1.7% of the time (17 in 1000 requests) 304 // expected probability factor: 0.1 * ((11-1) + 1)/64 = 0.0171875 305 loadGroups = append(loadGroups, loadGroup{0.1, 5, 31, repeatedBlockIDs(10)}) 306 307 // using a large batch request (99 block IDs) with a 10% base probability factor, expect to get misbehavior report about 15% of the time (150 in 1000 requests) 308 // expected probability factor: 0.1 * ((100-1) + 1)/64 = 0.15625 309 loadGroups = append(loadGroups, loadGroup{0.1, 110, 200, repeatedBlockIDs(99)}) 310 311 // using a small batch request (10 block IDs) with a 1% base probability factor, expect to almost never get misbehavior report, about 0.17% of the time (2 in 1000 requests) 312 // expected probability factor: 0.01 * ((11-1) + 1)/64 = 0.00171875 313 loadGroups = append(loadGroups, loadGroup{0.01, 0, 7, repeatedBlockIDs(10)}) 314 315 // using a very large batch request (999 block IDs) with a 1% base probability factor, expect to get misbehavior report about 15% of the time (150 in 1000 requests) 316 // expected probability factor: 0.01 * ((1000-1) + 1)/64 = 0.15625 317 loadGroups = append(loadGroups, loadGroup{0.01, 110, 200, repeatedBlockIDs(999)}) 318 319 // ALWAYS REPORT SPAM FOR INVALID BATCH REQUESTS OR BATCH REQUESTS THAT ARE FAR OUTSIDE OF THE TOLERANCE 320 321 // using an empty batch request (0 block IDs) always results in a misbehavior report, no matter how small the base probability factor is 322 loadGroups = append(loadGroups, loadGroup{0.001, 1000, 1000, []flow.Identifier{}}) 323 324 // using a very large batch request (999 block IDs) with a 10% base probability factor, expect to get misbehavior report 100% of the time (1000 in 1000 requests) 325 // expected probability factor: 0.1 * ((999 + 1)/64 = 1.5625 326 loadGroups = append(loadGroups, loadGroup{0.1, 1000, 1000, repeatedBlockIDs(999)}) 327 328 // reset misbehavior report counter for each subtest 329 misbehaviorsCounter := 0 330 331 for _, loadGroup := range loadGroups { 332 for i := 0; i < load; i++ { 333 ss.T().Log("load iteration", i) 334 335 nonce, err := rand.Uint64() 336 require.NoError(ss.T(), err, "should generate nonce") 337 338 // generate origin and request message 339 originID := unittest.IdentifierFixture() 340 req := &messages.BatchRequest{ 341 Nonce: nonce, 342 BlockIDs: loadGroup.blockIDs, 343 } 344 345 // count misbehavior reports over the course of a load test 346 ss.con.On("ReportMisbehavior", mock.Anything).Return(mock.Anything).Maybe().Run( 347 func(args mock.Arguments) { 348 misbehaviorsCounter++ 349 }, 350 ) 351 ss.e.spamDetectionConfig.batchRequestBaseProb = loadGroup.batchRequestBaseProb 352 require.NoError(ss.T(), ss.e.Process(channels.SyncCommittee, originID, req)) 353 } 354 // check function call expectations at the end of the load test; otherwise, load test would take much longer 355 ss.core.AssertExpectations(ss.T()) 356 ss.con.AssertExpectations(ss.T()) 357 358 // check that correct range of misbehavior reports were generated 359 // since we're using a probabilistic approach to generate misbehavior reports, we can't guarantee the exact number, 360 // so we check that it's within an expected range 361 ss.T().Logf("misbehaviors counter after load test: %d (expected lower bound: %d expected upper bound: %d)", misbehaviorsCounter, loadGroup.expectedMisbehaviorsLower, loadGroup.expectedMisbehaviorsUpper) 362 assert.GreaterOrEqual(ss.T(), misbehaviorsCounter, loadGroup.expectedMisbehaviorsLower) 363 assert.LessOrEqual(ss.T(), misbehaviorsCounter, loadGroup.expectedMisbehaviorsUpper) 364 365 misbehaviorsCounter = 0 // reset counter for next subtest 366 } 367 } 368 369 func repeatedBlockIDs(n int) []flow.Identifier { 370 blockID := unittest.BlockFixture().ID() 371 372 arr := make([]flow.Identifier, n) 373 for i := 0; i < n; i++ { 374 arr[i] = blockID 375 } 376 return arr 377 }