github.com/koko1123/flow-go-1@v0.29.6/module/mempool/consensus/exec_fork_suppressor_test.go (about) 1 package consensus 2 3 import ( 4 "os" 5 "testing" 6 7 "github.com/dgraph-io/badger/v3" 8 "github.com/rs/zerolog" 9 "github.com/stretchr/testify/mock" 10 "github.com/stretchr/testify/require" 11 "go.uber.org/atomic" 12 13 "github.com/koko1123/flow-go-1/engine" 14 "github.com/koko1123/flow-go-1/model/flow" 15 actormock "github.com/koko1123/flow-go-1/module/mempool/consensus/mock" 16 poolmock "github.com/koko1123/flow-go-1/module/mempool/mock" 17 "github.com/koko1123/flow-go-1/module/mempool/stdmap" 18 mockstorage "github.com/koko1123/flow-go-1/storage/mock" 19 "github.com/koko1123/flow-go-1/utils/unittest" 20 ) 21 22 // Test_Construction verifies correctness of the initial size and limit values 23 func Test_Construction(t *testing.T) { 24 WithExecStateForkSuppressor(t, func(wrapper *ExecForkSuppressor, wrappedMempool *poolmock.IncorporatedResultSeals, execForkActor *actormock.ExecForkActorMock) { 25 wrappedMempool.On("Size").Return(uint(0)).Once() 26 require.Equal(t, uint(0), wrapper.Size()) 27 wrappedMempool.On("Limit").Return(uint(0)).Once() 28 require.Equal(t, uint(0), wrapper.Limit()) 29 wrappedMempool.AssertExpectations(t) 30 }) 31 } 32 33 // Test_Size checks that ExecForkSuppressor is reporting the size of the wrapped mempool 34 func Test_Size(t *testing.T) { 35 WithExecStateForkSuppressor(t, func(wrapper *ExecForkSuppressor, wrappedMempool *poolmock.IncorporatedResultSeals, execForkActor *actormock.ExecForkActorMock) { 36 wrappedMempool.On("Size").Return(uint(139)).Once() 37 require.Equal(t, uint(139), wrapper.Size()) 38 wrappedMempool.AssertExpectations(t) 39 }) 40 } 41 42 // Test_Limit checks that ExecForkSuppressor is reporting the capacity limit of the wrapped mempool 43 func Test_Limit(t *testing.T) { 44 WithExecStateForkSuppressor(t, func(wrapper *ExecForkSuppressor, wrappedMempool *poolmock.IncorporatedResultSeals, execForkActor *actormock.ExecForkActorMock) { 45 wrappedMempool.On("Limit").Return(uint(227)).Once() 46 require.Equal(t, uint(227), wrapper.Limit()) 47 wrappedMempool.AssertExpectations(t) 48 }) 49 } 50 51 // Test_Clear checks that, when clearing the ExecForkSuppressor: 52 // - the wrapper also clears the wrapped mempool; 53 // - the reported mempool size, _after_ clearing should be zero 54 func Test_Clear(t *testing.T) { 55 WithExecStateForkSuppressor(t, func(wrapper *ExecForkSuppressor, wrappedMempool *poolmock.IncorporatedResultSeals, execForkActor *actormock.ExecForkActorMock) { 56 wrappedMempool.On("Clear").Return().Once() 57 58 wrapper.Clear() 59 wrappedMempool.On("Size").Return(uint(0)) 60 require.Equal(t, uint(0), wrapper.Size()) 61 wrappedMempool.AssertExpectations(t) 62 }) 63 } 64 65 // Test_All checks that ExecForkSuppressor.All() is returning the elements of the wrapped mempool 66 func Test_All(t *testing.T) { 67 WithExecStateForkSuppressor(t, func(wrapper *ExecForkSuppressor, wrappedMempool *poolmock.IncorporatedResultSeals, execForkActor *actormock.ExecForkActorMock) { 68 expectedSeals := unittest.IncorporatedResultSeal.Fixtures(7) 69 wrappedMempool.On("All").Return(expectedSeals) 70 retrievedSeals := wrapper.All() 71 require.ElementsMatch(t, expectedSeals, retrievedSeals) 72 }) 73 } 74 75 // Test_Add adds IncorporatedResultSeals for 76 // - 2 different blocks 77 // - for each block, we generate one specific result, 78 // for which we add 3 IncorporatedResultSeals 79 // o IncorporatedResultSeal (1): 80 // incorporated in block B1 81 // o IncorporatedResultSeal (2): 82 // incorporated in block B2 83 // o IncorporatedResultSeal (3): 84 // same result as (1) and incorporated in same block B1; 85 // should be automatically de-duplicated (irrespective of approvals on the seal). 86 func Test_Add(t *testing.T) { 87 WithExecStateForkSuppressor(t, func(wrapper *ExecForkSuppressor, wrappedMempool *poolmock.IncorporatedResultSeals, execForkActor *actormock.ExecForkActorMock) { 88 for _, block := range unittest.BlockFixtures(2) { 89 result := unittest.ExecutionResultFixture(unittest.WithBlock(block)) 90 91 // IncorporatedResultSeal (1): 92 irSeal1 := unittest.IncorporatedResultSeal.Fixture(unittest.IncorporatedResultSeal.WithResult(result)) 93 wrappedMempool.On("Add", irSeal1).Return(true, nil).Once() 94 added, err := wrapper.Add(irSeal1) 95 require.NoError(t, err) 96 require.True(t, added) 97 wrappedMempool.AssertExpectations(t) 98 99 // IncorporatedResultSeal (2): 100 // the value for IncorporatedResultSeal.IncorporatedResult.IncorporatedBlockID is randomly 101 // generated and therefore, will be different from for irSeal1 102 irSeal2 := unittest.IncorporatedResultSeal.Fixture(unittest.IncorporatedResultSeal.WithResult(result)) 103 require.False(t, irSeal1.ID() == irSeal2.ID()) // incorporated in different block => different seal ID expected 104 wrappedMempool.On("Add", irSeal2).Return(true, nil).Once() 105 added, err = wrapper.Add(irSeal2) 106 require.NoError(t, err) 107 require.True(t, added) 108 wrappedMempool.AssertExpectations(t) 109 110 // IncorporatedResultSeal (3): 111 irSeal3 := unittest.IncorporatedResultSeal.Fixture( 112 unittest.IncorporatedResultSeal.WithResult(result), 113 unittest.IncorporatedResultSeal.WithIncorporatedBlockID(irSeal1.IncorporatedResult.IncorporatedBlockID), 114 ) 115 require.True(t, irSeal1.ID() == irSeal3.ID()) // same result incorporated same block as (1) => identical ID expected 116 wrappedMempool.On("Add", irSeal3).Return(false, nil).Once() // deduplicate 117 added, err = wrapper.Add(irSeal3) 118 require.NoError(t, err) 119 require.False(t, added) 120 wrappedMempool.AssertExpectations(t) 121 } 122 }) 123 } 124 125 // Test_Remove checks that ExecForkSuppressor.Remove() 126 // - delegates the call to the underlying mempool 127 func Test_Remove(t *testing.T) { 128 WithExecStateForkSuppressor(t, func(wrapper *ExecForkSuppressor, wrappedMempool *poolmock.IncorporatedResultSeals, execForkActor *actormock.ExecForkActorMock) { 129 // element is in wrapped mempool: Remove should be called 130 seal := unittest.IncorporatedResultSeal.Fixture() 131 wrappedMempool.On("Add", seal).Return(true, nil).Once() 132 wrappedMempool.On("ByID", seal.ID()).Return(seal, true) 133 added, err := wrapper.Add(seal) 134 require.NoError(t, err) 135 require.True(t, added) 136 137 wrappedMempool.On("ByID", seal.ID()).Return(seal, true) 138 wrappedMempool.On("Remove", seal.ID()).Return(true).Once() 139 removed := wrapper.Remove(seal.ID()) 140 require.True(t, removed) 141 wrappedMempool.AssertExpectations(t) 142 143 // element _not_ in wrapped mempool: Remove might be called 144 seal = unittest.IncorporatedResultSeal.Fixture() 145 wrappedMempool.On("ByID", seal.ID()).Return(seal, false) 146 wrappedMempool.On("Remove", seal.ID()).Return(false).Maybe() 147 removed = wrapper.Remove(seal.ID()) 148 require.False(t, removed) 149 wrappedMempool.AssertExpectations(t) 150 }) 151 } 152 153 // Test_RejectInvalidSeals verifies that ExecForkSuppressor rejects seals whose 154 // which don't have a chunk (i.e. their start and end state of the result cannot be determined) 155 func Test_RejectInvalidSeals(t *testing.T) { 156 WithExecStateForkSuppressor(t, func(wrapper *ExecForkSuppressor, wrappedMempool *poolmock.IncorporatedResultSeals, execForkActor *actormock.ExecForkActorMock) { 157 irSeal := unittest.IncorporatedResultSeal.Fixture() 158 irSeal.IncorporatedResult.Result.Chunks = make(flow.ChunkList, 0) 159 irSeal.Seal.FinalState = flow.DummyStateCommitment 160 161 added, err := wrapper.Add(irSeal) 162 require.Error(t, err) 163 require.True(t, engine.IsInvalidInputError(err)) 164 require.False(t, added) 165 }) 166 } 167 168 // Test_ConflictingResults verifies that ExecForkSuppressor detects a fork in the execution chain. 169 // The expected behaviour is: 170 // - clear the wrapped mempool 171 // - reject addition of all further entities (even valid seals) 172 // 173 // This logic has to be executed for all queries(`ByID`, `All`) 174 func Test_ConflictingResults(t *testing.T) { 175 assertConflictingResult := func(t *testing.T, action func(irSeals []*flow.IncorporatedResultSeal, conflictingSeal *flow.IncorporatedResultSeal, wrapper *ExecForkSuppressor, wrappedMempool *poolmock.IncorporatedResultSeals)) { 176 WithExecStateForkSuppressor(t, func(wrapper *ExecForkSuppressor, wrappedMempool *poolmock.IncorporatedResultSeals, execForkActor *actormock.ExecForkActorMock) { 177 // add 3 random irSeals 178 irSeals := unittest.IncorporatedResultSeal.Fixtures(3) 179 for _, s := range irSeals { 180 wrappedMempool.On("Add", s).Return(true, nil).Once() 181 added, err := wrapper.Add(s) 182 require.NoError(t, err) 183 require.True(t, added) 184 } 185 186 // add seal for result that is _conflicting_ with irSeals[1] 187 result := unittest.ExecutionResultFixture() 188 result.BlockID = irSeals[1].Seal.BlockID 189 for _, c := range result.Chunks { 190 c.BlockID = result.BlockID 191 } 192 conflictingSeal := unittest.IncorporatedResultSeal.Fixture(unittest.IncorporatedResultSeal.WithResult(result)) 193 194 wrappedMempool.On("Clear").Return().Once() 195 wrappedMempool.On("Add", conflictingSeal).Return(true, nil).Once() 196 // add conflicting seal 197 added, err := wrapper.Add(conflictingSeal) 198 require.NoError(t, err) 199 require.True(t, added) 200 201 execForkActor.On("OnExecFork", mock.Anything).Run(func(args mock.Arguments) { 202 conflictingSeals := args.Get(0).([]*flow.IncorporatedResultSeal) 203 require.ElementsMatch(t, []*flow.IncorporatedResultSeal{irSeals[1], conflictingSeal}, conflictingSeals) 204 }).Return().Once() 205 action(irSeals, conflictingSeal, wrapper, wrappedMempool) 206 207 wrappedMempool.On("ByID", conflictingSeal.ID()).Return(nil, false).Once() 208 byID, found := wrapper.ByID(conflictingSeal.ID()) 209 require.False(t, found) 210 require.Nil(t, byID) 211 212 // mempool should be cleared 213 wrappedMempool.On("Size").Return(uint(0)) // we asserted that Clear was called on wrappedMempool 214 require.Equal(t, uint(0), wrapper.Size()) 215 216 // additional seals should not be accepted anymore 217 added, err = wrapper.Add(unittest.IncorporatedResultSeal.Fixture()) 218 require.NoError(t, err) 219 require.False(t, added) 220 require.Equal(t, uint(0), wrapper.Size()) 221 222 wrappedMempool.AssertExpectations(t) 223 execForkActor.AssertExpectations(t) 224 }) 225 } 226 227 t.Run("all-query", func(t *testing.T) { 228 assertConflictingResult(t, func(irSeals []*flow.IncorporatedResultSeal, conflictingSeal *flow.IncorporatedResultSeal, wrapper *ExecForkSuppressor, wrappedMempool *poolmock.IncorporatedResultSeals) { 229 wrappedMempool.On("All").Return(append(irSeals, conflictingSeal)).Once() 230 allSeals := wrapper.All() 231 require.Len(t, allSeals, 0) 232 }) 233 }) 234 t.Run("by-id-query", func(t *testing.T) { 235 assertConflictingResult(t, func(irSeals []*flow.IncorporatedResultSeal, conflictingSeal *flow.IncorporatedResultSeal, wrapper *ExecForkSuppressor, wrappedMempool *poolmock.IncorporatedResultSeals) { 236 wrappedMempool.On("ByID", conflictingSeal.ID()).Return(conflictingSeal, true).Once() 237 byID, found := wrapper.ByID(conflictingSeal.ID()) 238 require.False(t, found) 239 require.Nil(t, byID) 240 }) 241 }) 242 243 } 244 245 // Test_ForkDetectionPersisted verifies that, when ExecForkSuppressor detects a fork, this information is 246 // persisted in the data base 247 func Test_ForkDetectionPersisted(t *testing.T) { 248 unittest.RunWithTempDir(t, func(dir string) { 249 db := unittest.BadgerDB(t, dir) 250 defer db.Close() 251 252 // initialize ExecForkSuppressor 253 wrappedMempool := &poolmock.IncorporatedResultSeals{} 254 execForkActor := &actormock.ExecForkActorMock{} 255 wrapper, _ := NewExecStateForkSuppressor(wrappedMempool, execForkActor.OnExecFork, db, zerolog.New(os.Stderr)) 256 257 // add seal 258 block := unittest.BlockFixture() 259 sealA := unittest.IncorporatedResultSeal.Fixture(unittest.IncorporatedResultSeal.WithResult(unittest.ExecutionResultFixture(unittest.WithBlock(&block)))) 260 wrappedMempool.On("Add", sealA).Return(true, nil).Once() 261 _, _ = wrapper.Add(sealA) 262 263 // add conflicting seal 264 sealB := unittest.IncorporatedResultSeal.Fixture(unittest.IncorporatedResultSeal.WithResult(unittest.ExecutionResultFixture(unittest.WithBlock(&block)))) 265 wrappedMempool.On("Add", sealB).Return(true, nil).Once() 266 added, _ := wrapper.Add(sealB) // should be rejected because it is conflicting with sealA 267 require.True(t, added) 268 269 wrappedMempool.On("ByID", sealA.ID()).Return(sealA, true).Once() 270 execForkActor.On("OnExecFork", mock.Anything).Run(func(args mock.Arguments) { 271 conflictingSeals := args.Get(0).([]*flow.IncorporatedResultSeal) 272 require.ElementsMatch(t, []*flow.IncorporatedResultSeal{sealA, sealB}, conflictingSeals) 273 }).Return().Once() 274 wrappedMempool.On("Clear").Return().Once() 275 // try to query, at this point we will detect a conflicting seal 276 wrapper.ByID(sealA.ID()) 277 278 wrappedMempool.AssertExpectations(t) 279 execForkActor.AssertExpectations(t) 280 281 // crash => re-initialization 282 db.Close() 283 db2 := unittest.BadgerDB(t, dir) 284 wrappedMempool2 := &poolmock.IncorporatedResultSeals{} 285 execForkActor2 := &actormock.ExecForkActorMock{} 286 execForkActor2.On("OnExecFork", mock.Anything). 287 Run(func(args mock.Arguments) { 288 conflictingSeals := args.Get(0).([]*flow.IncorporatedResultSeal) 289 require.ElementsMatch(t, []*flow.IncorporatedResultSeal{sealA, sealB}, conflictingSeals) 290 }).Return().Once() 291 wrapper2, _ := NewExecStateForkSuppressor(wrappedMempool2, execForkActor2.OnExecFork, db2, zerolog.New(os.Stderr)) 292 293 // add another (non-conflicting) seal to ExecForkSuppressor 294 // fail test if seal is added to wrapped mempool 295 wrappedMempool2.On("Add", mock.Anything). 296 Run(func(args mock.Arguments) { require.Fail(t, "seal was added to wrapped mempool") }). 297 Return(true, nil).Maybe() 298 added, _ = wrapper2.Add(unittest.IncorporatedResultSeal.Fixture()) 299 require.False(t, added) 300 wrappedMempool2.On("Size").Return(uint(0)) // we asserted that Clear was called on wrappedMempool 301 require.Equal(t, uint(0), wrapper2.Size()) 302 303 wrappedMempool2.AssertExpectations(t) 304 execForkActor2.AssertExpectations(t) 305 }) 306 } 307 308 // Test_AddRemove_SmokeTest tests a real system of stdmap.IncorporatedResultSeals mempool 309 // which is wrapped in an ExecForkSuppressor. 310 // We add and remove lots of different seals. 311 func Test_AddRemove_SmokeTest(t *testing.T) { 312 onExecFork := func([]*flow.IncorporatedResultSeal) { 313 require.Fail(t, "no call to onExecFork expected ") 314 } 315 unittest.RunWithBadgerDB(t, func(db *badger.DB) { 316 wrappedMempool := stdmap.NewIncorporatedResultSeals(100) 317 wrapper, err := NewExecStateForkSuppressor(wrappedMempool, onExecFork, db, zerolog.New(os.Stderr)) 318 require.NoError(t, err) 319 require.NotNil(t, wrapper) 320 321 // Run 100 experiments of the following kind: 322 // * add 10 seals to mempool, which should eject 7 seals 323 // * test that ejected seals are not in mempool anymore 324 // * remove remaining seals 325 for i := 0; i < 100; i++ { 326 seals := unittest.IncorporatedResultSeal.Fixtures(10) 327 for j, s := range seals { 328 // fix height for each seal 329 s.Header.Height = uint64(i*100 + j) 330 added, err := wrapper.Add(s) 331 require.NoError(t, err) 332 require.True(t, added) 333 } 334 335 require.Equal(t, uint(10), wrapper.seals.Size()) 336 require.Equal(t, uint(10), wrapper.Size()) 337 338 err := wrapper.PruneUpToHeight(uint64((i + 1) * 100)) 339 require.NoError(t, err) 340 341 require.Equal(t, uint(0), wrapper.seals.Size()) 342 require.Equal(t, uint(0), wrapper.Size()) 343 require.Len(t, wrapper.sealsForBlock, 0) 344 } 345 }) 346 } 347 348 // Test_ConflictingSeal_SmokeTest tests a real system where we combine stdmap.IncorporatedResultSeals, consensus.IncorporatedResultSeals and 349 // ExecForkSuppressor. We wrap stdmap.IncorporatedResultSeals with consensus.IncorporatedResultSeals which is wrapped with ExecForkSuppressor. 350 // Test adding conflicting seals with different number of matching receipts. 351 func Test_ConflictingSeal_SmokeTest(t *testing.T) { 352 unittest.RunWithBadgerDB(t, func(db *badger.DB) { 353 executingForkDetected := atomic.NewBool(false) 354 onExecFork := func([]*flow.IncorporatedResultSeal) { 355 executingForkDetected.Store(true) 356 } 357 358 rawMempool := stdmap.NewIncorporatedResultSeals(100) 359 receiptsDB := mockstorage.NewExecutionReceipts(t) 360 wrappedMempool := NewIncorporatedResultSeals(rawMempool, receiptsDB) 361 wrapper, err := NewExecStateForkSuppressor(wrappedMempool, onExecFork, db, zerolog.New(os.Stderr)) 362 require.NoError(t, err) 363 require.NotNil(t, wrapper) 364 365 // add three seals 366 // two of them are non-conflicting but for same block and one is conflicting. 367 368 block := unittest.BlockFixture() 369 sealA := unittest.IncorporatedResultSeal.Fixture(unittest.IncorporatedResultSeal.WithResult(unittest.ExecutionResultFixture(unittest.WithBlock(&block)))) 370 _, _ = wrapper.Add(sealA) 371 372 // different seal but for same result 373 sealB := unittest.IncorporatedResultSeal.Fixture(unittest.IncorporatedResultSeal.WithResult(sealA.IncorporatedResult.Result)) 374 _, _ = wrapper.Add(sealB) 375 376 receiptsDB.On("ByBlockID", block.ID()).Return(nil, nil).Twice() 377 378 // two seals, but they are not confirmed with receipts 379 seals := wrapper.All() 380 require.Empty(t, seals) 381 382 receipts := flow.ExecutionReceiptList{ 383 unittest.ExecutionReceiptFixture(unittest.WithResult(sealA.IncorporatedResult.Result)), 384 unittest.ExecutionReceiptFixture(unittest.WithResult(sealB.IncorporatedResult.Result)), 385 } 386 receiptsDB.On("ByBlockID", block.ID()).Return(receipts, nil).Twice() 387 388 // at this point we have two seals, confirmed by two receipts but no execution fork 389 seals = wrapper.All() 390 require.ElementsMatch(t, []*flow.IncorporatedResultSeal{sealA, sealB}, seals) 391 392 // add conflicting seal, which doesn't have any receipts yet 393 conflictingSeal := unittest.IncorporatedResultSeal.Fixture(unittest.IncorporatedResultSeal.WithResult(unittest.ExecutionResultFixture(unittest.WithBlock(&block)))) 394 _, _ = wrapper.Add(conflictingSeal) 395 396 // conflicting seal doesn't have any receipts yet 397 receiptsDB.On("ByBlockID", block.ID()).Return(receipts, nil).Times(3) 398 399 seals = wrapper.All() 400 require.ElementsMatch(t, []*flow.IncorporatedResultSeal{sealA, sealB}, seals) 401 402 // add two receipts for conflicting result 403 receipts = append(receipts, 404 unittest.ExecutionReceiptFixture(unittest.WithResult(conflictingSeal.IncorporatedResult.Result)), 405 unittest.ExecutionReceiptFixture(unittest.WithResult(conflictingSeal.IncorporatedResult.Result)), 406 ) 407 408 receiptsDB.On("ByBlockID", block.ID()).Return(receipts, nil).Times(3) 409 410 // querying should detect execution fork 411 seals = wrapper.All() 412 require.Empty(t, seals) 413 require.True(t, executingForkDetected.Load()) 414 }) 415 } 416 417 // WithExecStateForkSuppressor 418 // 1. constructs a mock (aka `wrappedMempool`) of an IncorporatedResultSeals mempool 419 // 2. wraps `wrappedMempool` in a ExecForkSuppressor 420 // 3. ensures that initializing the wrapper did not error 421 // 4. executes the `testLogic` 422 func WithExecStateForkSuppressor(t testing.TB, testLogic func(wrapper *ExecForkSuppressor, wrappedMempool *poolmock.IncorporatedResultSeals, execForkActor *actormock.ExecForkActorMock)) { 423 unittest.RunWithBadgerDB(t, func(db *badger.DB) { 424 wrappedMempool := &poolmock.IncorporatedResultSeals{} 425 execForkActor := &actormock.ExecForkActorMock{} 426 wrapper, err := NewExecStateForkSuppressor(wrappedMempool, execForkActor.OnExecFork, db, zerolog.New(os.Stderr)) 427 require.NoError(t, err) 428 require.NotNil(t, wrapper) 429 testLogic(wrapper, wrappedMempool, execForkActor) 430 }) 431 }