github.com/koko1123/flow-go-1@v0.29.6/engine/consensus/compliance/core_test.go (about) 1 package compliance 2 3 import ( 4 "errors" 5 "math/rand" 6 "testing" 7 "time" 8 9 "github.com/stretchr/testify/assert" 10 "github.com/stretchr/testify/mock" 11 "github.com/stretchr/testify/require" 12 "github.com/stretchr/testify/suite" 13 14 hotstuff "github.com/koko1123/flow-go-1/consensus/hotstuff/mocks" 15 "github.com/koko1123/flow-go-1/consensus/hotstuff/model" 16 "github.com/koko1123/flow-go-1/model/flow" 17 "github.com/koko1123/flow-go-1/model/messages" 18 realModule "github.com/koko1123/flow-go-1/module" 19 real "github.com/koko1123/flow-go-1/module/buffer" 20 "github.com/koko1123/flow-go-1/module/compliance" 21 "github.com/koko1123/flow-go-1/module/metrics" 22 module "github.com/koko1123/flow-go-1/module/mock" 23 "github.com/koko1123/flow-go-1/module/trace" 24 netint "github.com/koko1123/flow-go-1/network" 25 "github.com/koko1123/flow-go-1/network/channels" 26 "github.com/koko1123/flow-go-1/network/mocknetwork" 27 protint "github.com/koko1123/flow-go-1/state/protocol" 28 protocol "github.com/koko1123/flow-go-1/state/protocol/mock" 29 storerr "github.com/koko1123/flow-go-1/storage" 30 storage "github.com/koko1123/flow-go-1/storage/mock" 31 "github.com/koko1123/flow-go-1/utils/unittest" 32 ) 33 34 func TestComplianceCore(t *testing.T) { 35 suite.Run(t, new(ComplianceCoreSuite)) 36 } 37 38 type ComplianceCoreSuite struct { 39 suite.Suite 40 41 // engine parameters 42 participants flow.IdentityList 43 myID flow.Identifier 44 head *flow.Header 45 46 // storage data 47 headerDB map[flow.Identifier]*flow.Header 48 payloadDB map[flow.Identifier]*flow.Payload 49 pendingDB map[flow.Identifier]flow.Slashable[flow.Block] 50 childrenDB map[flow.Identifier][]flow.Slashable[flow.Block] 51 52 // mocked dependencies 53 me *module.Local 54 metrics *metrics.NoopCollector 55 tracer realModule.Tracer 56 cleaner *storage.Cleaner 57 headers *storage.Headers 58 payloads *storage.Payloads 59 state *protocol.MutableState 60 snapshot *protocol.Snapshot 61 con *mocknetwork.Conduit 62 net *mocknetwork.Network 63 prov *mocknetwork.Engine 64 pending *module.PendingBlockBuffer 65 hotstuff *module.HotStuff 66 sync *module.BlockRequester 67 voteAggregator *hotstuff.VoteAggregator 68 69 // engine under test 70 core *Core 71 } 72 73 func doneChan() <-chan struct{} { 74 c := make(chan struct{}) 75 close(c) 76 return c 77 } 78 79 func (cs *ComplianceCoreSuite) SetupTest() { 80 // seed the RNG 81 rand.Seed(time.Now().UnixNano()) 82 83 // initialize the paramaters 84 cs.participants = unittest.IdentityListFixture(3, 85 unittest.WithRole(flow.RoleConsensus), 86 unittest.WithWeight(1000), 87 ) 88 cs.myID = cs.participants[0].NodeID 89 block := unittest.BlockFixture() 90 cs.head = block.Header 91 92 // initialize the storage data 93 cs.headerDB = make(map[flow.Identifier]*flow.Header) 94 cs.payloadDB = make(map[flow.Identifier]*flow.Payload) 95 cs.pendingDB = make(map[flow.Identifier]flow.Slashable[flow.Block]) 96 cs.childrenDB = make(map[flow.Identifier][]flow.Slashable[flow.Block]) 97 98 // store the head header and payload 99 cs.headerDB[block.ID()] = block.Header 100 cs.payloadDB[block.ID()] = block.Payload 101 102 // set up local module mock 103 cs.me = &module.Local{} 104 cs.me.On("NodeID").Return( 105 func() flow.Identifier { 106 return cs.myID 107 }, 108 ) 109 110 // set up storage cleaner 111 cs.cleaner = &storage.Cleaner{} 112 cs.cleaner.On("RunGC").Return() 113 114 // set up header storage mock 115 cs.headers = &storage.Headers{} 116 cs.headers.On("Store", mock.Anything).Return( 117 func(header *flow.Header) error { 118 cs.headerDB[header.ID()] = header 119 return nil 120 }, 121 ) 122 cs.headers.On("ByBlockID", mock.Anything).Return( 123 func(blockID flow.Identifier) *flow.Header { 124 return cs.headerDB[blockID] 125 }, 126 func(blockID flow.Identifier) error { 127 _, exists := cs.headerDB[blockID] 128 if !exists { 129 return storerr.ErrNotFound 130 } 131 return nil 132 }, 133 ) 134 135 // set up payload storage mock 136 cs.payloads = &storage.Payloads{} 137 cs.payloads.On("Store", mock.Anything, mock.Anything).Return( 138 func(header *flow.Header, payload *flow.Payload) error { 139 cs.payloadDB[header.ID()] = payload 140 return nil 141 }, 142 ) 143 cs.payloads.On("ByBlockID", mock.Anything).Return( 144 func(blockID flow.Identifier) *flow.Payload { 145 return cs.payloadDB[blockID] 146 }, 147 func(blockID flow.Identifier) error { 148 _, exists := cs.payloadDB[blockID] 149 if !exists { 150 return storerr.ErrNotFound 151 } 152 return nil 153 }, 154 ) 155 156 // set up protocol state mock 157 cs.state = &protocol.MutableState{} 158 cs.state.On("Final").Return( 159 func() protint.Snapshot { 160 return cs.snapshot 161 }, 162 ) 163 cs.state.On("AtBlockID", mock.Anything).Return( 164 func(blockID flow.Identifier) protint.Snapshot { 165 return cs.snapshot 166 }, 167 ) 168 cs.state.On("Extend", mock.Anything, mock.Anything).Return(nil) 169 170 // set up protocol snapshot mock 171 cs.snapshot = &protocol.Snapshot{} 172 cs.snapshot.On("Identities", mock.Anything).Return( 173 func(filter flow.IdentityFilter) flow.IdentityList { 174 return cs.participants.Filter(filter) 175 }, 176 nil, 177 ) 178 cs.snapshot.On("Head").Return( 179 func() *flow.Header { 180 return cs.head 181 }, 182 nil, 183 ) 184 185 // set up network conduit mock 186 cs.con = &mocknetwork.Conduit{} 187 cs.con.On("Publish", mock.Anything, mock.Anything).Return(nil) 188 cs.con.On("Publish", mock.Anything, mock.Anything, mock.Anything).Return(nil) 189 cs.con.On("Publish", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) 190 cs.con.On("Unicast", mock.Anything, mock.Anything).Return(nil) 191 192 // set up network module mock 193 cs.net = &mocknetwork.Network{} 194 cs.net.On("Register", mock.Anything, mock.Anything).Return( 195 func(channel channels.Channel, engine netint.MessageProcessor) netint.Conduit { 196 return cs.con 197 }, 198 nil, 199 ) 200 201 // set up the provider engine 202 cs.prov = &mocknetwork.Engine{} 203 cs.prov.On("SubmitLocal", mock.Anything).Return() 204 205 // set up pending module mock 206 cs.pending = &module.PendingBlockBuffer{} 207 cs.pending.On("Add", mock.Anything, mock.Anything).Return(true) 208 cs.pending.On("ByID", mock.Anything).Return( 209 func(blockID flow.Identifier) flow.Slashable[flow.Block] { 210 return cs.pendingDB[blockID] 211 }, 212 func(blockID flow.Identifier) bool { 213 _, ok := cs.pendingDB[blockID] 214 return ok 215 }, 216 ) 217 cs.pending.On("ByParentID", mock.Anything).Return( 218 func(blockID flow.Identifier) []flow.Slashable[flow.Block] { 219 return cs.childrenDB[blockID] 220 }, 221 func(blockID flow.Identifier) bool { 222 _, ok := cs.childrenDB[blockID] 223 return ok 224 }, 225 ) 226 cs.pending.On("DropForParent", mock.Anything).Return() 227 cs.pending.On("Size").Return(uint(0)) 228 cs.pending.On("PruneByView", mock.Anything).Return() 229 230 closed := func() <-chan struct{} { 231 channel := make(chan struct{}) 232 close(channel) 233 return channel 234 }() 235 236 // set up hotstuff module mock 237 cs.hotstuff = &module.HotStuff{} 238 239 cs.voteAggregator = &hotstuff.VoteAggregator{} 240 241 // set up synchronization module mock 242 cs.sync = &module.BlockRequester{} 243 cs.sync.On("RequestBlock", mock.Anything, mock.Anything).Return(nil) 244 cs.sync.On("Done", mock.Anything).Return(closed) 245 246 // set up no-op metrics mock 247 cs.metrics = metrics.NewNoopCollector() 248 249 // set up no-op tracer 250 cs.tracer = trace.NewNoopTracer() 251 252 // initialize the engine 253 e, err := NewCore( 254 unittest.Logger(), 255 cs.metrics, 256 cs.tracer, 257 cs.metrics, 258 cs.metrics, 259 cs.cleaner, 260 cs.headers, 261 cs.payloads, 262 cs.state, 263 cs.pending, 264 cs.sync, 265 cs.voteAggregator, 266 ) 267 require.NoError(cs.T(), err, "engine initialization should pass") 268 269 cs.core = e 270 // assign engine with consensus & synchronization 271 cs.core.hotstuff = cs.hotstuff 272 } 273 274 func (cs *ComplianceCoreSuite) TestOnBlockProposalValidParent() { 275 276 // create a proposal that directly descends from the latest finalized header 277 originID := cs.participants[1].NodeID 278 block := unittest.BlockWithParentFixture(cs.head) 279 proposal := unittest.ProposalFromBlock(block) 280 281 // store the data for retrieval 282 cs.headerDB[block.Header.ParentID] = cs.head 283 284 cs.hotstuff.On("SubmitProposal", block.Header, cs.head.View).Return(doneChan()) 285 286 // it should be processed without error 287 err := cs.core.OnBlockProposal(originID, proposal, false) 288 require.NoError(cs.T(), err, "valid block proposal should pass") 289 290 // we should extend the state with the header 291 cs.state.AssertCalled(cs.T(), "Extend", mock.Anything, block) 292 293 // we should submit the proposal to hotstuff 294 cs.hotstuff.AssertExpectations(cs.T()) 295 } 296 297 func (cs *ComplianceCoreSuite) TestOnBlockProposalValidAncestor() { 298 299 // create a proposal that has two ancestors in the cache 300 originID := cs.participants[1].NodeID 301 ancestor := unittest.BlockWithParentFixture(cs.head) 302 parent := unittest.BlockWithParentFixture(ancestor.Header) 303 block := unittest.BlockWithParentFixture(parent.Header) 304 proposal := unittest.ProposalFromBlock(block) 305 306 // store the data for retrieval 307 cs.headerDB[parent.ID()] = parent.Header 308 cs.headerDB[ancestor.ID()] = ancestor.Header 309 310 cs.hotstuff.On("SubmitProposal", block.Header, parent.Header.View).Return(doneChan()) 311 312 // it should be processed without error 313 err := cs.core.OnBlockProposal(originID, proposal, false) 314 require.NoError(cs.T(), err, "valid block proposal should pass") 315 316 // we should extend the state with the header 317 cs.state.AssertCalled(cs.T(), "Extend", mock.Anything, block) 318 319 // we should submit the proposal to hotstuff 320 cs.hotstuff.AssertExpectations(cs.T()) 321 } 322 323 func (cs *ComplianceCoreSuite) TestOnBlockProposalSkipProposalThreshold() { 324 325 // create a proposal which is far enough ahead to be dropped 326 originID := cs.participants[1].NodeID 327 block := unittest.BlockFixture() 328 block.Header.Height = cs.head.Height + compliance.DefaultConfig().SkipNewProposalsThreshold + 1 329 proposal := unittest.ProposalFromBlock(&block) 330 331 err := cs.core.OnBlockProposal(originID, proposal, false) 332 require.NoError(cs.T(), err) 333 334 // block should be dropped - not added to state or cache 335 cs.state.AssertNotCalled(cs.T(), "Extend", mock.Anything) 336 cs.pending.AssertNotCalled(cs.T(), "Add", originID, mock.Anything) 337 } 338 339 func (cs *ComplianceCoreSuite) TestOnBlockProposalInvalidExtension() { 340 341 // create a proposal that has two ancestors in the cache 342 originID := cs.participants[1].NodeID 343 ancestor := unittest.BlockWithParentFixture(cs.head) 344 parent := unittest.BlockWithParentFixture(ancestor.Header) 345 block := unittest.BlockWithParentFixture(parent.Header) 346 proposal := unittest.ProposalFromBlock(block) 347 348 // store the data for retrieval 349 cs.headerDB[parent.ID()] = parent.Header 350 cs.headerDB[ancestor.ID()] = ancestor.Header 351 352 // make sure we fail to extend the state 353 *cs.state = protocol.MutableState{} 354 cs.state.On("Final").Return( 355 func() protint.Snapshot { 356 return cs.snapshot 357 }, 358 ) 359 cs.state.On("Extend", mock.Anything, mock.Anything).Return(errors.New("dummy error")) 360 361 // it should be processed without error 362 err := cs.core.OnBlockProposal(originID, proposal, false) 363 require.Error(cs.T(), err, "proposal with invalid extension should fail") 364 365 // we should extend the state with the header 366 cs.state.AssertCalled(cs.T(), "Extend", mock.Anything, block) 367 368 // we should not submit the proposal to hotstuff 369 cs.hotstuff.AssertExpectations(cs.T()) 370 } 371 372 func (cs *ComplianceCoreSuite) TestProcessBlockAndDescendants() { 373 374 // create three children blocks 375 parent := unittest.BlockWithParentFixture(cs.head) 376 block1 := unittest.BlockWithParentFixture(parent.Header) 377 block2 := unittest.BlockWithParentFixture(parent.Header) 378 block3 := unittest.BlockWithParentFixture(parent.Header) 379 380 // create the pending blocks 381 pending1 := unittest.AsSlashable(block1) 382 pending2 := unittest.AsSlashable(block2) 383 pending3 := unittest.AsSlashable(block3) 384 385 // store the parent on disk 386 parentID := parent.ID() 387 cs.headerDB[parentID] = parent.Header 388 389 // store the pending children in the cache 390 cs.childrenDB[parentID] = append(cs.childrenDB[parentID], pending1) 391 cs.childrenDB[parentID] = append(cs.childrenDB[parentID], pending2) 392 cs.childrenDB[parentID] = append(cs.childrenDB[parentID], pending3) 393 394 cs.hotstuff.On("SubmitProposal", parent.Header, cs.head.View).Return(doneChan()).Once() 395 cs.hotstuff.On("SubmitProposal", block1.Header, parent.Header.View).Return(doneChan()).Once() 396 cs.hotstuff.On("SubmitProposal", block2.Header, parent.Header.View).Return(doneChan()).Once() 397 cs.hotstuff.On("SubmitProposal", block3.Header, parent.Header.View).Return(doneChan()).Once() 398 399 // execute the connected children handling 400 err := cs.core.processBlockAndDescendants(parent, false) 401 require.NoError(cs.T(), err, "should pass handling children") 402 403 // check that we submitted each child to hotstuff 404 cs.hotstuff.AssertExpectations(cs.T()) 405 406 // make sure we drop the cache after trying to process 407 cs.pending.AssertCalled(cs.T(), "DropForParent", parent.Header.ID()) 408 } 409 410 func (cs *ComplianceCoreSuite) TestOnSubmitVote() { 411 // create a vote 412 originID := unittest.IdentifierFixture() 413 vote := messages.BlockVote{ 414 BlockID: unittest.IdentifierFixture(), 415 View: rand.Uint64(), 416 SigData: unittest.SignatureFixture(), 417 } 418 419 cs.voteAggregator.On("AddVote", &model.Vote{ 420 View: vote.View, 421 BlockID: vote.BlockID, 422 SignerID: originID, 423 SigData: vote.SigData, 424 }).Return() 425 426 // execute the vote submission 427 err := cs.core.OnBlockVote(originID, &vote) 428 require.NoError(cs.T(), err, "block vote should pass") 429 430 // check that submit vote was called with correct parameters 431 cs.hotstuff.AssertExpectations(cs.T()) 432 } 433 434 func (cs *ComplianceCoreSuite) TestProposalBufferingOrder() { 435 436 // create a proposal that we will not submit until the end 437 originID := cs.participants[1].NodeID 438 block := unittest.BlockWithParentFixture(cs.head) 439 missing := unittest.ProposalFromBlock(block) 440 441 // create a chain of descendants 442 var proposals []*messages.BlockProposal 443 parent := missing 444 for i := 0; i < 3; i++ { 445 descendant := unittest.BlockWithParentFixture(&parent.Block.Header) 446 proposal := unittest.ProposalFromBlock(descendant) 447 proposals = append(proposals, proposal) 448 parent = proposal 449 } 450 451 // replace the engine buffer with the real one 452 cs.core.pending = real.NewPendingBlocks() 453 454 // process all of the descendants 455 for _, proposal := range proposals { 456 457 // check that we request the ancestor block each time 458 cs.sync.On("RequestBlock", mock.Anything, mock.Anything).Once().Run( 459 func(args mock.Arguments) { 460 ancestorID := args.Get(0).(flow.Identifier) 461 assert.Equal(cs.T(), missing.Block.Header.ID(), ancestorID, "should always request root block") 462 }, 463 ) 464 465 // process and make sure no error occurs (as they are unverifiable) 466 err := cs.core.OnBlockProposal(originID, proposal, false) 467 require.NoError(cs.T(), err, "proposal buffering should pass") 468 469 // make sure no block is forwarded to hotstuff 470 cs.hotstuff.AssertExpectations(cs.T()) 471 } 472 473 // check that we submit ech proposal in order 474 *cs.hotstuff = module.HotStuff{} 475 index := 0 476 order := []flow.Identifier{ 477 missing.Block.Header.ID(), 478 proposals[0].Block.Header.ID(), 479 proposals[1].Block.Header.ID(), 480 proposals[2].Block.Header.ID(), 481 } 482 cs.hotstuff.On("SubmitProposal", mock.Anything, mock.Anything).Times(4).Run( 483 func(args mock.Arguments) { 484 header := args.Get(0).(*flow.Header) 485 assert.Equal(cs.T(), order[index], header.ID(), "should submit correct header to hotstuff") 486 index++ 487 cs.headerDB[header.ID()] = header 488 }, 489 ).Return(doneChan()) 490 491 // process the root proposal 492 err := cs.core.OnBlockProposal(originID, missing, false) 493 require.NoError(cs.T(), err, "root proposal should pass") 494 495 // make sure we submitted all four proposals 496 cs.hotstuff.AssertExpectations(cs.T()) 497 }