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