github.com/koko1123/flow-go-1@v0.29.6/consensus/follower_test.go (about) 1 package consensus_test 2 3 import ( 4 "os" 5 "testing" 6 "time" 7 8 "github.com/koko1123/flow-go-1/module/signature" 9 10 "github.com/rs/zerolog" 11 "github.com/stretchr/testify/mock" 12 "github.com/stretchr/testify/require" 13 "github.com/stretchr/testify/suite" 14 15 "github.com/koko1123/flow-go-1/consensus" 16 "github.com/koko1123/flow-go-1/consensus/hotstuff" 17 mockhotstuff "github.com/koko1123/flow-go-1/consensus/hotstuff/mocks" 18 "github.com/koko1123/flow-go-1/consensus/hotstuff/model" 19 "github.com/koko1123/flow-go-1/model/flow" 20 mockmodule "github.com/koko1123/flow-go-1/module/mock" 21 mockstorage "github.com/koko1123/flow-go-1/storage/mock" 22 "github.com/koko1123/flow-go-1/utils/unittest" 23 ) 24 25 // TestHotStuffFollower is a test suite for the HotStuff Follower. 26 // The main focus of this test suite is to test that the follower generates the expected callbacks to 27 // module.Finalizer and hotstuff.FinalizationConsumer. In this context, note that the Follower internally 28 // has its own processing thread. Therefore, the test must be concurrency safe and ensure that the Follower 29 // has asynchronously processed the submitted blocks _before_ we assert whether all callbacks were run. 30 // We use the following knowledge about the Follower's _internal_ processing: 31 // - The Follower is running in a single go-routine, pulling one event at a time from an 32 // _unbuffered_ channel. The test will send blocks to the Follower's input channel and block there 33 // until the Follower receives the block from the channel. Hence, when all sends have completed, the 34 // Follower has processed all blocks but the last one. Furthermore, the last block has already been 35 // received. 36 // - Therefore, the Follower will only pick up a shutdown signal _after_ it processed the last block. 37 // Hence, waiting for the Follower's `Done()` channel guarantees that it complete processing any 38 // blocks that are in the event loop. 39 // 40 // For this test, most of the Follower's injected components are mocked out. 41 // As we test the mocked components separately, we assume: 42 // - The mocked components work according to specification. 43 // - Especially, we assume that Forks works according to specification, i.e. that the determination of 44 // finalized blocks is correct and events are emitted in the desired order (both are tested separately). 45 func TestHotStuffFollower(t *testing.T) { 46 suite.Run(t, new(HotStuffFollowerSuite)) 47 } 48 49 type HotStuffFollowerSuite struct { 50 suite.Suite 51 52 committee *mockhotstuff.Committee 53 headers *mockstorage.Headers 54 updater *mockmodule.Finalizer 55 verifier *mockhotstuff.Verifier 56 notifier *mockhotstuff.FinalizationConsumer 57 rootHeader *flow.Header 58 rootQC *flow.QuorumCertificate 59 finalized *flow.Header 60 pending []*flow.Header 61 follower *hotstuff.FollowerLoop 62 63 mockConsensus *MockConsensus 64 } 65 66 // SetupTest initializes all the components needed for the Follower. 67 // The follower itself is instantiated in method BeforeTest 68 func (s *HotStuffFollowerSuite) SetupTest() { 69 identities := unittest.IdentityListFixture(4, unittest.WithRole(flow.RoleConsensus)) 70 s.mockConsensus = &MockConsensus{identities: identities} 71 72 // mock consensus committee 73 s.committee = &mockhotstuff.Committee{} 74 s.committee.On("Identities", mock.Anything).Return( 75 func(blockID flow.Identifier) flow.IdentityList { 76 return identities 77 }, 78 nil, 79 ) 80 for _, identity := range identities { 81 s.committee.On("Identity", mock.Anything, identity.NodeID).Return(identity, nil) 82 } 83 s.committee.On("LeaderForView", mock.Anything).Return( 84 func(view uint64) flow.Identifier { return identities[int(view)%len(identities)].NodeID }, 85 nil, 86 ) 87 88 // mock storage headers 89 s.headers = &mockstorage.Headers{} 90 91 // mock finalization updater 92 s.updater = &mockmodule.Finalizer{} 93 94 // mock finalization updater 95 s.verifier = &mockhotstuff.Verifier{} 96 s.verifier.On("VerifyVote", mock.Anything, mock.Anything, mock.Anything).Return(nil) 97 s.verifier.On("VerifyQC", mock.Anything, mock.Anything, mock.Anything).Return(nil) 98 99 // mock consumer for finalization notifications 100 s.notifier = &mockhotstuff.FinalizationConsumer{} 101 102 // root block and QC 103 parentID, err := flow.HexStringToIdentifier("aa7693d498e9a087b1cadf5bfe9a1ff07829badc1915c210e482f369f9a00a70") 104 require.NoError(s.T(), err) 105 s.rootHeader = &flow.Header{ 106 ParentID: parentID, 107 Timestamp: time.Now().UTC(), 108 Height: 21053, 109 View: 52078, 110 } 111 112 signerIndices, err := signature.EncodeSignersToIndices(identities.NodeIDs(), identities.NodeIDs()[:3]) 113 require.NoError(s.T(), err) 114 s.rootQC = &flow.QuorumCertificate{ 115 View: s.rootHeader.View, 116 BlockID: s.rootHeader.ID(), 117 SignerIndices: signerIndices, 118 } 119 120 // we start with the latest finalized block being the root block 121 s.finalized = s.rootHeader 122 // and no pending (unfinalized) block 123 s.pending = []*flow.Header{} 124 } 125 126 // BeforeTest instantiates and starts Follower 127 func (s *HotStuffFollowerSuite) BeforeTest(suiteName, testName string) { 128 s.notifier.On("OnBlockIncorporated", blockWithID(s.rootHeader.ID())).Return().Once() 129 130 var err error 131 s.follower, err = consensus.NewFollower( 132 zerolog.New(os.Stderr), 133 s.committee, 134 s.headers, 135 s.updater, 136 s.verifier, 137 s.notifier, 138 s.rootHeader, 139 s.rootQC, 140 s.finalized, 141 s.pending, 142 ) 143 require.NoError(s.T(), err) 144 145 select { 146 case <-s.follower.Ready(): 147 case <-time.After(time.Second): 148 s.T().Error("timeout on waiting for follower start") 149 } 150 } 151 152 // AfterTest stops follower and asserts that the Follower executed the expected callbacks 153 // to s.updater.MakeValid or s.notifier.OnBlockIncorporated 154 func (s *HotStuffFollowerSuite) AfterTest(suiteName, testName string) { 155 select { 156 case <-s.follower.Done(): 157 case <-time.After(time.Second): 158 s.T().Error("timeout on waiting for expected Follower shutdown") 159 s.T().FailNow() // stops the test 160 } 161 s.notifier.AssertExpectations(s.T()) 162 s.updater.AssertExpectations(s.T()) 163 } 164 165 // TestInitialization verifies that the basic test setup with initialization of the Follower works as expected 166 func (s *HotStuffFollowerSuite) TestInitialization() { 167 // we expect no additional calls to s.updater or s.notifier besides what is already specified in BeforeTest 168 } 169 170 // TestSubmitProposal verifies that when submitting a single valid block (child's root block), 171 // the Follower reacts with callbacks to s.updater.MakeValid and s.notifier.OnBlockIncorporated with this new block 172 func (s *HotStuffFollowerSuite) TestSubmitProposal() { 173 rootBlockView := s.rootHeader.View 174 nextBlock := s.mockConsensus.extendBlock(rootBlockView+1, s.rootHeader) 175 176 s.notifier.On("OnBlockIncorporated", blockWithID(nextBlock.ID())).Return().Once() 177 s.updater.On("MakeValid", blockID(nextBlock.ID())).Return(nil).Once() 178 s.submitWithTimeout(nextBlock, rootBlockView) 179 } 180 181 // TestFollowerFinalizedBlock verifies that when submitting 4 extra blocks 182 // the Follower reacts with callbacks to s.updater.MakeValid or s.notifier.OnBlockIncorporated 183 // for all the added blocks. Furthermore, the follower should finalize the first submitted block, 184 // i.e. call s.updater.MakeFinal and s.notifier.OnFinalizedBlock 185 func (s *HotStuffFollowerSuite) TestFollowerFinalizedBlock() { 186 expectedFinalized := s.mockConsensus.extendBlock(s.rootHeader.View+1, s.rootHeader) 187 s.notifier.On("OnBlockIncorporated", blockWithID(expectedFinalized.ID())).Return().Once() 188 s.updater.On("MakeValid", blockID(expectedFinalized.ID())).Return(nil).Once() 189 s.submitWithTimeout(expectedFinalized, s.rootHeader.View) 190 191 // direct 1-chain on top of expectedFinalized 192 nextBlock := s.mockConsensus.extendBlock(expectedFinalized.View+1, expectedFinalized) 193 s.notifier.On("OnBlockIncorporated", blockWithID(nextBlock.ID())).Return().Once() 194 s.updater.On("MakeValid", blockID(nextBlock.ID())).Return(nil).Once() 195 s.submitWithTimeout(nextBlock, expectedFinalized.View) 196 197 // direct 2-chain on top of expectedFinalized 198 lastBlock := nextBlock 199 nextBlock = s.mockConsensus.extendBlock(lastBlock.View+1, lastBlock) 200 s.notifier.On("OnBlockIncorporated", blockWithID(nextBlock.ID())).Return().Once() 201 s.updater.On("MakeValid", blockID(nextBlock.ID())).Return(nil).Once() 202 s.submitWithTimeout(nextBlock, lastBlock.View) 203 204 // indirect 3-chain on top of expectedFinalized => finalization 205 lastBlock = nextBlock 206 nextBlock = s.mockConsensus.extendBlock(lastBlock.View+5, lastBlock) 207 s.notifier.On("OnFinalizedBlock", blockWithID(expectedFinalized.ID())).Return().Once() 208 s.notifier.On("OnBlockIncorporated", blockWithID(nextBlock.ID())).Return().Once() 209 s.updater.On("MakeFinal", blockID(expectedFinalized.ID())).Return(nil).Once() 210 s.updater.On("MakeValid", blockID(nextBlock.ID())).Return(nil).Once() 211 s.submitWithTimeout(nextBlock, lastBlock.View) 212 } 213 214 // TestOutOfOrderBlocks verifies that when submitting a variety of blocks with view numbers 215 // OUT OF ORDER, the Follower reacts with callbacks to s.updater.MakeValid or s.notifier.OnBlockIncorporated 216 // for all the added blocks. Furthermore, we construct the test such that the follower should finalize 217 // eventually a bunch of blocks in one go. 218 // The following illustrates the tree of submitted blocks, with notation 219 // 220 // - [a, b] is a block at view "b" with a QC with view "a", 221 // e.g., [1, 2] means a block at view "2" with an included QC for view "1" 222 // 223 // . [52078+15, 52078+20] (should finalize this fork) 224 // . | 225 // . | 226 // . [52078+14, 52078+15] 227 // . | 228 // . | 229 // . [52078+13, 52078+14] 230 // . | 231 // . | 232 // . [52078+11, 52078+12] [52078+11, 52078+17] [52078+ 9, 52078+13] [52078+ 9, 52078+10] 233 // . \ | | / 234 // . \| | / 235 // . [52078+ 7, 52078+ 8] [52078+ 7, 52078+11] [52078+ 5, 52078+ 9] [52078+ 5, 52078+ 6] 236 // . \ | | / 237 // . \| | / 238 // . [52078+ 3, 52078+ 4] [52078+ 3, 52078+ 7] [52078+ 1, 52078+ 5] [52078+ 1, 52078+ 2] 239 // . \ | | / 240 // . \| | / 241 // . [52078+ 0, 52078+ 3] [52078+ 0, 52078+ 1] 242 // . \ / 243 // . \ / 244 // . [52078+ 0, x] (root block; no qc to parent) 245 func (s *HotStuffFollowerSuite) TestOutOfOrderBlocks() { 246 // in the following, we reference the block's by their view minus the view of the 247 // root block (52078). E.g. block [52078+ 9, 52078+10] would be referenced as `block10` 248 rootView := s.rootHeader.View 249 250 // constructing blocks bottom up, line by line, left to right 251 block03 := s.mockConsensus.extendBlock(rootView+3, s.rootHeader) 252 block01 := s.mockConsensus.extendBlock(rootView+1, s.rootHeader) 253 254 block04 := s.mockConsensus.extendBlock(rootView+4, block03) 255 block07 := s.mockConsensus.extendBlock(rootView+7, block03) 256 block05 := s.mockConsensus.extendBlock(rootView+5, block01) 257 block02 := s.mockConsensus.extendBlock(rootView+2, block01) 258 259 block08 := s.mockConsensus.extendBlock(rootView+8, block07) 260 block11 := s.mockConsensus.extendBlock(rootView+11, block07) 261 block09 := s.mockConsensus.extendBlock(rootView+9, block05) 262 block06 := s.mockConsensus.extendBlock(rootView+6, block05) 263 264 block12 := s.mockConsensus.extendBlock(rootView+12, block11) 265 block17 := s.mockConsensus.extendBlock(rootView+17, block11) 266 block13 := s.mockConsensus.extendBlock(rootView+13, block09) 267 block10 := s.mockConsensus.extendBlock(rootView+10, block09) 268 269 block14 := s.mockConsensus.extendBlock(rootView+14, block13) 270 block15 := s.mockConsensus.extendBlock(rootView+15, block14) 271 block20 := s.mockConsensus.extendBlock(rootView+20, block15) 272 273 for _, b := range []*flow.Header{block01, block02, block03, block04, block05, block06, block07, block08, block09, block10, block11, block12, block13, block14, block15, block17, block20} { 274 s.notifier.On("OnBlockIncorporated", blockWithID(b.ID())).Return().Once() 275 s.updater.On("MakeValid", blockID(b.ID())).Return(nil).Once() 276 } 277 278 // now we feed the blocks in some wild view order into the Follower 279 // (Caution: we still have to make sure the parent is known, before we give its child to the Follower) 280 s.submitWithTimeout(block03, rootView) 281 s.submitWithTimeout(block07, rootView+3) 282 s.submitWithTimeout(block11, rootView+7) 283 s.submitWithTimeout(block01, rootView) 284 s.submitWithTimeout(block12, rootView+11) 285 s.submitWithTimeout(block05, rootView+1) 286 s.submitWithTimeout(block17, rootView+11) 287 s.submitWithTimeout(block09, rootView+5) 288 s.submitWithTimeout(block06, rootView+5) 289 s.submitWithTimeout(block10, rootView+9) 290 s.submitWithTimeout(block04, rootView+3) 291 s.submitWithTimeout(block13, rootView+9) 292 s.submitWithTimeout(block14, rootView+13) 293 s.submitWithTimeout(block08, rootView+7) 294 s.submitWithTimeout(block15, rootView+14) 295 s.submitWithTimeout(block02, rootView+1) 296 297 // Block 20 should now finalize the fork up to and including block13 298 s.notifier.On("OnFinalizedBlock", blockWithID(block01.ID())).Return().Once() 299 s.updater.On("MakeFinal", blockID(block01.ID())).Return(nil).Once() 300 s.notifier.On("OnFinalizedBlock", blockWithID(block05.ID())).Return().Once() 301 s.updater.On("MakeFinal", blockID(block05.ID())).Return(nil).Once() 302 s.notifier.On("OnFinalizedBlock", blockWithID(block09.ID())).Return().Once() 303 s.updater.On("MakeFinal", blockID(block09.ID())).Return(nil).Once() 304 s.notifier.On("OnFinalizedBlock", blockWithID(block13.ID())).Return().Once() 305 s.updater.On("MakeFinal", blockID(block13.ID())).Return(nil).Once() 306 s.submitWithTimeout(block20, rootView+15) 307 } 308 309 // blockWithID returns a testify `argumentMatcher` that only accepts blocks with the given ID 310 func blockWithID(expectedBlockID flow.Identifier) interface{} { 311 return mock.MatchedBy(func(block *model.Block) bool { return expectedBlockID == block.BlockID }) 312 } 313 314 // blockID returns a testify `argumentMatcher` that only accepts the given ID 315 func blockID(expectedBlockID flow.Identifier) interface{} { 316 return mock.MatchedBy(func(blockID flow.Identifier) bool { return expectedBlockID == blockID }) 317 } 318 319 // submitWithTimeout submits the given (proposal, parentView) pair to the Follower. As the follower 320 // might block on this call, we add a timeout that fails the test, in case of a dead-lock. 321 func (s *HotStuffFollowerSuite) submitWithTimeout(proposal *flow.Header, parentView uint64) { 322 sent := make(chan struct{}) 323 go func() { 324 s.follower.SubmitProposal(proposal, parentView) 325 close(sent) 326 }() 327 select { 328 case <-sent: 329 case <-time.After(time.Second): 330 s.T().Error("timeout on waiting for expected Follower shutdown") 331 s.T().FailNow() // stops the test 332 } 333 334 } 335 336 // MockConsensus is used to generate Blocks for a mocked consensus committee 337 type MockConsensus struct { 338 identities flow.IdentityList 339 } 340 341 func (mc *MockConsensus) extendBlock(blockView uint64, parent *flow.Header) *flow.Header { 342 nextBlock := unittest.BlockHeaderWithParentFixture(parent) 343 nextBlock.View = blockView 344 nextBlock.ProposerID = mc.identities[int(blockView)%len(mc.identities)].NodeID 345 signerIndices, _ := signature.EncodeSignersToIndices(mc.identities.NodeIDs(), mc.identities.NodeIDs()) 346 nextBlock.ParentVoterIndices = signerIndices 347 return nextBlock 348 }