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  }