github.com/onflow/flow-go@v0.33.17/engine/consensus/dkg/reactor_engine_test.go (about) 1 package dkg_test 2 3 import ( 4 "math/rand" 5 "os" 6 "testing" 7 "time" 8 9 "github.com/rs/zerolog" 10 "github.com/stretchr/testify/assert" 11 "github.com/stretchr/testify/mock" 12 "github.com/stretchr/testify/require" 13 "github.com/stretchr/testify/suite" 14 15 "github.com/onflow/flow-go/crypto" 16 "github.com/onflow/flow-go/engine/consensus/dkg" 17 "github.com/onflow/flow-go/model/flow" 18 dkgmodule "github.com/onflow/flow-go/module/dkg" 19 module "github.com/onflow/flow-go/module/mock" 20 "github.com/onflow/flow-go/state/protocol/events/gadgets" 21 protocol "github.com/onflow/flow-go/state/protocol/mock" 22 storerr "github.com/onflow/flow-go/storage" 23 storage "github.com/onflow/flow-go/storage/mock" 24 "github.com/onflow/flow-go/utils/unittest" 25 "github.com/onflow/flow-go/utils/unittest/mocks" 26 ) 27 28 // ReactorEngineSuite_SetupPhase is a test suite for the Reactor engine which encompasses 29 // test cases from when the DKG is instantiated to when it terminates locally. 30 // 31 // For tests of the Reactor engine's reaction to the global end of the DKG, see 32 // ReactorEngineSuite_CommittedPhase. 33 type ReactorEngineSuite_SetupPhase struct { 34 suite.Suite 35 36 // config 37 dkgStartView uint64 38 dkgPhase1FinalView uint64 39 dkgPhase2FinalView uint64 40 dkgPhase3FinalView uint64 41 42 epochCounter uint64 // current epoch counter 43 myIndex int // my index in the DKG 44 committee flow.IdentityList // the DKG committee 45 expectedPrivateKey crypto.PrivateKey 46 firstBlock *flow.Header 47 blocksByView map[uint64]*flow.Header 48 49 // track how many warn-level logs are logged 50 warnsLogged int 51 logger zerolog.Logger 52 53 local *module.Local 54 currentEpoch *protocol.Epoch 55 nextEpoch *protocol.Epoch 56 epochQuery *mocks.EpochQuery 57 snapshot *protocol.Snapshot 58 state *protocol.State 59 viewEvents *gadgets.Views 60 61 dkgState *storage.DKGState 62 controller *module.DKGController 63 factory *module.DKGControllerFactory 64 65 engine *dkg.ReactorEngine 66 } 67 68 func (suite *ReactorEngineSuite_SetupPhase) NextEpochCounter() uint64 { 69 return suite.epochCounter + 1 70 } 71 72 func TestReactorEngineSuite_SetupPhase(t *testing.T) { 73 suite.Run(t, new(ReactorEngineSuite_SetupPhase)) 74 } 75 76 // SetupTest prepares the DKG test. 77 // 78 // The EpochSetup event is received at view 100. 79 // The current epoch is configured with DKG phase transitions at views 150, 200, 80 // and 250. In between phase transitions, the controller calls the DKG 81 // smart-contract every 10 views. 82 // 83 // VIEWS 84 // setup : 100 85 // polling : 110 120 130 140 150 86 // Phase1Final: 150 87 // polling : 160 170 180 190 200 88 // Phase2Final: 200 89 // polling : 210 220 230 240 250 90 // Phase3Final: 250 91 func (suite *ReactorEngineSuite_SetupPhase) SetupTest() { 92 93 suite.dkgStartView = 100 94 suite.dkgPhase1FinalView = 150 95 suite.dkgPhase2FinalView = 200 96 suite.dkgPhase3FinalView = 250 97 98 suite.epochCounter = rand.Uint64() 99 suite.committee = unittest.IdentityListFixture(10) 100 suite.myIndex = 5 101 102 suite.local = new(module.Local) 103 suite.local.On("NodeID").Return(suite.committee[suite.myIndex].NodeID) 104 105 // create a block for each view of interest 106 suite.blocksByView = make(map[uint64]*flow.Header) 107 for view := suite.dkgStartView; view <= suite.dkgPhase3FinalView; view += dkg.DefaultPollStep { 108 header := unittest.BlockHeaderFixture(unittest.HeaderWithView(view)) 109 suite.blocksByView[view] = header 110 } 111 suite.firstBlock = suite.blocksByView[100] 112 113 // expectedPrivKey is the expected private share produced by the dkg run. We 114 // will mock the controller to return this value, and we will check it 115 // against the value that gets inserted in the DB at the end. 116 suite.expectedPrivateKey = unittest.PrivateKeyFixture(crypto.BLSBLS12381, 48) 117 118 // mock protocol state 119 suite.currentEpoch = new(protocol.Epoch) 120 suite.currentEpoch.On("Counter").Return(suite.epochCounter, nil) 121 suite.currentEpoch.On("DKGPhase1FinalView").Return(suite.dkgPhase1FinalView, nil) 122 suite.currentEpoch.On("DKGPhase2FinalView").Return(suite.dkgPhase2FinalView, nil) 123 suite.currentEpoch.On("DKGPhase3FinalView").Return(suite.dkgPhase3FinalView, nil) 124 suite.nextEpoch = new(protocol.Epoch) 125 suite.nextEpoch.On("Counter").Return(suite.NextEpochCounter(), nil) 126 suite.nextEpoch.On("InitialIdentities").Return(suite.committee, nil) 127 128 suite.epochQuery = mocks.NewEpochQuery(suite.T(), suite.epochCounter) 129 suite.epochQuery.Add(suite.currentEpoch) 130 suite.epochQuery.Add(suite.nextEpoch) 131 suite.snapshot = new(protocol.Snapshot) 132 suite.snapshot.On("Epochs").Return(suite.epochQuery) 133 suite.snapshot.On("Head").Return(suite.firstBlock, nil) 134 suite.state = new(protocol.State) 135 suite.state.On("AtBlockID", suite.firstBlock.ID()).Return(suite.snapshot) 136 suite.state.On("Final").Return(suite.snapshot) 137 138 // ensure that an attempt is made to insert the expected dkg private share 139 // for the next epoch. 140 suite.dkgState = new(storage.DKGState) 141 suite.dkgState.On("SetDKGStarted", suite.NextEpochCounter()).Return(nil).Once() 142 suite.dkgState.On("InsertMyBeaconPrivateKey", mock.Anything, mock.Anything).Run( 143 func(args mock.Arguments) { 144 epochCounter := args.Get(0).(uint64) 145 require.Equal(suite.T(), suite.NextEpochCounter(), epochCounter) 146 dkgPriv := args.Get(1).(crypto.PrivateKey) 147 require.Equal(suite.T(), suite.expectedPrivateKey, dkgPriv) 148 }). 149 Return(nil). 150 Once() 151 152 // we will ensure that the controller state transitions get called appropriately 153 suite.controller = new(module.DKGController) 154 suite.controller.On("Run").Return(nil).Once() 155 suite.controller.On("EndPhase1").Return(nil).Once() 156 suite.controller.On("EndPhase2").Return(nil).Once() 157 suite.controller.On("End").Return(nil).Once() 158 suite.controller.On("Poll", mock.Anything).Return(nil).Times(15) 159 suite.controller.On("GetArtifacts").Return(suite.expectedPrivateKey, nil, nil).Once() 160 suite.controller.On("SubmitResult").Return(nil).Once() 161 162 suite.factory = new(module.DKGControllerFactory) 163 suite.factory.On("Create", 164 dkgmodule.CanonicalInstanceID(suite.firstBlock.ChainID, suite.NextEpochCounter()), 165 suite.committee, 166 mock.Anything, 167 ).Return(suite.controller, nil) 168 169 suite.warnsLogged = 0 170 suite.logger = hookedLogger(&suite.warnsLogged) 171 172 suite.viewEvents = gadgets.NewViews() 173 suite.engine = dkg.NewReactorEngine( 174 suite.logger, 175 suite.local, 176 suite.state, 177 suite.dkgState, 178 suite.factory, 179 suite.viewEvents, 180 ) 181 } 182 183 // TestRunDKG_PhaseTransition tests that the DKG is started and completed successfully 184 // after a phase transition from StakingPhase->SetupPhase. 185 func (suite *ReactorEngineSuite_SetupPhase) TestRunDKG_PhaseTransition() { 186 187 // the dkg for this epoch has not been started 188 suite.dkgState.On("GetDKGStarted", suite.NextEpochCounter()).Return(false, nil).Once() 189 // protocol event indicating the setup phase is starting 190 suite.engine.EpochSetupPhaseStarted(suite.epochCounter, suite.firstBlock) 191 192 for view := uint64(100); view <= 250; view += dkg.DefaultPollStep { 193 suite.viewEvents.BlockFinalized(suite.blocksByView[view]) 194 } 195 196 // check that the appropriate callbacks were registered 197 time.Sleep(50 * time.Millisecond) 198 suite.controller.AssertExpectations(suite.T()) 199 suite.dkgState.AssertExpectations(suite.T()) 200 // happy path - no warn logs expected 201 suite.Assert().Equal(0, suite.warnsLogged) 202 } 203 204 // TestRunDKG_StartupInSetupPhase tests that the DKG is started and completed 205 // successfully when the engine starts up during the EpochSetup phase, and the 206 // DKG for this epoch has not been started previously. This is the case for 207 // consensus nodes joining the network at an epoch boundary. 208 func (suite *ReactorEngineSuite_SetupPhase) TestRunDKG_StartupInSetupPhase() { 209 210 // we are in the EpochSetup phase 211 suite.snapshot.On("Phase").Return(flow.EpochPhaseSetup, nil).Once() 212 // the dkg for this epoch has not been started 213 suite.dkgState.On("GetDKGStarted", suite.NextEpochCounter()).Return(false, nil).Once() 214 215 // start up the engine 216 unittest.AssertClosesBefore(suite.T(), suite.engine.Ready(), time.Second) 217 218 // keyStorage := new(storage.DKGKeys) 219 // keyStorage.On("RetrieveMyDKGPrivateInfo", currentCounter+1).Return(dkgParticipantPrivInfo, true, nil) 220 // factory := new(module.DKGControllerFactory) 221 for view := uint64(100); view <= 250; view += dkg.DefaultPollStep { 222 suite.viewEvents.BlockFinalized(suite.blocksByView[view]) 223 } 224 225 // check that the appropriate callbacks were registered 226 time.Sleep(50 * time.Millisecond) 227 suite.controller.AssertExpectations(suite.T()) 228 suite.dkgState.AssertExpectations(suite.T()) 229 // happy path - no warn logs expected 230 suite.Assert().Equal(0, suite.warnsLogged) 231 } 232 233 // TestRunDKG_StartupInSetupPhase_DKGAlreadyStarted tests that the DKG is NOT 234 // started, when the engine starts up during the EpochSetup phase, and the DKG 235 // for this epoch HAS been started previously. This will be the case for 236 // consensus nodes which restart during the DKG. 237 func (suite *ReactorEngineSuite_SetupPhase) TestRunDKG_StartupInSetupPhase_DKGAlreadyStarted() { 238 239 // we are in the EpochSetup phase 240 suite.snapshot.On("Phase").Return(flow.EpochPhaseSetup, nil).Once() 241 // the dkg for this epoch has been started 242 suite.dkgState.On("GetDKGStarted", suite.NextEpochCounter()).Return(true, nil).Once() 243 244 // start up the engine 245 unittest.AssertClosesBefore(suite.T(), suite.engine.Ready(), time.Second) 246 247 // we should not have instantiated the DKG 248 suite.factory.AssertNotCalled(suite.T(), "Create", 249 dkgmodule.CanonicalInstanceID(suite.firstBlock.ChainID, suite.NextEpochCounter()), 250 suite.committee, 251 mock.Anything, 252 ) 253 254 // we should log a warning that the DKG has already started 255 suite.Assert().Equal(1, suite.warnsLogged) 256 } 257 258 // ReactorEngineSuite_CommittedPhase tests the Reactor engine's operation 259 // during the transition to the EpochCommitted phase, after the DKG has 260 // completed locally, and we are comparing our local results to the 261 // canonical DKG results. 262 type ReactorEngineSuite_CommittedPhase struct { 263 suite.Suite 264 265 epochCounter uint64 // current epoch counter 266 myLocalBeaconKey crypto.PrivateKey // my locally computed beacon key 267 myGlobalBeaconPubKey crypto.PublicKey // my public key, as dictated by global DKG 268 dkgEndState flow.DKGEndState // backend for DGKState. 269 firstBlock *flow.Header // first block of EpochCommitted phase 270 warnsLogged int // count # of warn-level logs 271 272 me *module.Local 273 dkgState *storage.DKGState 274 state *protocol.State 275 snap *protocol.Snapshot 276 factory *module.DKGControllerFactory 277 278 engine *dkg.ReactorEngine 279 } 280 281 func TestReactorEngineSuite_CommittedPhase(t *testing.T) { 282 suite.Run(t, new(ReactorEngineSuite_CommittedPhase)) 283 } 284 285 func (suite *ReactorEngineSuite_CommittedPhase) NextEpochCounter() uint64 { 286 return suite.epochCounter + 1 287 } 288 289 func (suite *ReactorEngineSuite_CommittedPhase) SetupTest() { 290 291 suite.epochCounter = rand.Uint64() 292 suite.dkgEndState = flow.DKGEndStateUnknown 293 suite.me = new(module.Local) 294 295 id := unittest.IdentifierFixture() 296 suite.me.On("NodeID").Return(id) 297 298 // by default we seed the test suite with consistent keys 299 suite.myLocalBeaconKey = unittest.RandomBeaconPriv().PrivateKey 300 suite.myGlobalBeaconPubKey = suite.myLocalBeaconKey.PublicKey() 301 302 suite.dkgState = new(storage.DKGState) 303 suite.dkgState.On("RetrieveMyBeaconPrivateKey", suite.NextEpochCounter()).Return( 304 func(_ uint64) crypto.PrivateKey { return suite.myLocalBeaconKey }, 305 func(_ uint64) error { 306 if suite.myLocalBeaconKey == nil { 307 return storerr.ErrNotFound 308 } 309 return nil 310 }, 311 ) 312 suite.dkgState.On("SetDKGEndState", suite.NextEpochCounter(), mock.Anything). 313 Run(func(args mock.Arguments) { 314 assert.Equal(suite.T(), flow.DKGEndStateUnknown, suite.dkgEndState) // must be unset 315 endState := args[1].(flow.DKGEndState) 316 suite.dkgEndState = endState 317 }). 318 Return(nil) 319 suite.dkgState.On("GetDKGEndState", suite.NextEpochCounter()).Return( 320 func(_ uint64) flow.DKGEndState { return suite.dkgEndState }, 321 func(_ uint64) error { 322 if suite.dkgEndState == flow.DKGEndStateUnknown { 323 return storerr.ErrNotFound 324 } 325 return nil 326 }, 327 ) 328 329 currentEpoch := new(protocol.Epoch) 330 currentEpoch.On("Counter").Return(suite.epochCounter, nil) 331 332 nextDKG := new(protocol.DKG) 333 nextDKG.On("KeyShare", id).Return( 334 func(_ flow.Identifier) crypto.PublicKey { return suite.myGlobalBeaconPubKey }, 335 func(_ flow.Identifier) error { return nil }, 336 ) 337 338 nextEpoch := new(protocol.Epoch) 339 nextEpoch.On("Counter").Return(suite.NextEpochCounter(), nil) 340 nextEpoch.On("DKG").Return(nextDKG, nil) 341 342 epochQuery := mocks.NewEpochQuery(suite.T(), suite.epochCounter) 343 epochQuery.Add(currentEpoch) 344 epochQuery.Add(nextEpoch) 345 346 firstBlock := unittest.BlockHeaderFixture(unittest.HeaderWithView(100)) 347 suite.firstBlock = firstBlock 348 349 suite.snap = new(protocol.Snapshot) 350 suite.snap.On("Epochs").Return(epochQuery) 351 suite.snap.On("Head").Return(firstBlock, nil) 352 353 suite.state = new(protocol.State) 354 suite.state.On("AtBlockID", firstBlock.ID()).Return(suite.snap) 355 suite.state.On("Final").Return(suite.snap) 356 357 // count number of warn-level logs 358 suite.warnsLogged = 0 359 logger := hookedLogger(&suite.warnsLogged) 360 361 suite.factory = new(module.DKGControllerFactory) 362 viewEvents := gadgets.NewViews() 363 364 suite.engine = dkg.NewReactorEngine( 365 logger, 366 suite.me, 367 suite.state, 368 suite.dkgState, 369 suite.factory, 370 viewEvents, 371 ) 372 } 373 374 // TestDKGSuccess tests the path where we are checking the global DKG 375 // results and observe our key is consistent. 376 // We should: 377 // * set the DKG end state to Success 378 func (suite *ReactorEngineSuite_CommittedPhase) TestDKGSuccess() { 379 380 // no change to suite - this is the happy path 381 382 suite.engine.EpochCommittedPhaseStarted(suite.epochCounter, suite.firstBlock) 383 suite.Require().Equal(0, suite.warnsLogged) 384 suite.Assert().Equal(flow.DKGEndStateSuccess, suite.dkgEndState) 385 } 386 387 // TestInconsistentKey tests the path where we are checking the global DKG 388 // results and observe that our locally computed key is inconsistent. 389 // We should: 390 // * log a warning 391 // * set the DKG end state accordingly 392 func (suite *ReactorEngineSuite_CommittedPhase) TestInconsistentKey() { 393 394 // set our global pub key to a random value 395 suite.myGlobalBeaconPubKey = unittest.RandomBeaconPriv().PublicKey() 396 397 suite.engine.EpochCommittedPhaseStarted(suite.epochCounter, suite.firstBlock) 398 suite.Require().Equal(1, suite.warnsLogged) 399 suite.Assert().Equal(flow.DKGEndStateInconsistentKey, suite.dkgEndState) 400 } 401 402 // TestMissingKey tests the path where we are checking the global DKG results 403 // and observe that we have not stored a locally computed key. 404 // We should: 405 // * log a warning 406 // * set the DKG end state accordingly 407 func (suite *ReactorEngineSuite_CommittedPhase) TestMissingKey() { 408 409 // remove our key 410 suite.myLocalBeaconKey = nil 411 412 suite.engine.EpochCommittedPhaseStarted(suite.epochCounter, suite.firstBlock) 413 suite.Require().Equal(1, suite.warnsLogged) 414 suite.Assert().Equal(flow.DKGEndStateNoKey, suite.dkgEndState) 415 } 416 417 // TestLocalDKGFailure tests the path where we are checking the global DKG 418 // results and observe that we have already set the DKG end state as a failure. 419 // We should: 420 // * log a warning 421 // * keep the dkg end state as it is 422 func (suite *ReactorEngineSuite_CommittedPhase) TestLocalDKGFailure() { 423 424 // set dkg end state as failure 425 suite.dkgEndState = flow.DKGEndStateDKGFailure 426 427 suite.engine.EpochCommittedPhaseStarted(suite.epochCounter, suite.firstBlock) 428 suite.Require().Equal(1, suite.warnsLogged) 429 suite.Assert().Equal(flow.DKGEndStateDKGFailure, suite.dkgEndState) 430 } 431 432 // TestStartupInCommittedPhase_DKGSuccess tests that the dkg end state is correctly 433 // set when starting in EpochCommitted phase and a successful DKG 434 func (suite *ReactorEngineSuite_CommittedPhase) TestStartupInCommittedPhase_DKGSuccess() { 435 436 // we are in the EpochSetup phase 437 suite.snap.On("Phase").Return(flow.EpochPhaseCommitted, nil).Once() 438 // the dkg for this epoch has been started but not ended 439 suite.dkgState.On("GetDKGStarted", suite.NextEpochCounter()).Return(true, nil).Once() 440 suite.dkgState.On("GetDKGEndState", suite.NextEpochCounter()).Return(flow.DKGEndStateUnknown, storerr.ErrNotFound).Once() 441 442 // start up the engine 443 unittest.AssertClosesBefore(suite.T(), suite.engine.Ready(), time.Second) 444 445 // we should not have instantiated the DKG 446 suite.factory.AssertNotCalled(suite.T(), "Create", 447 dkgmodule.CanonicalInstanceID(suite.firstBlock.ChainID, suite.NextEpochCounter()), 448 mock.Anything, 449 mock.Anything, 450 ) 451 // should set DKG end state 452 suite.Assert().Equal(flow.DKGEndStateSuccess, suite.dkgEndState) 453 } 454 455 // TestStartupInCommittedPhase_DKGSuccess tests that the dkg end state is correctly 456 // set when starting in EpochCommitted phase and the DKG end state is already set. 457 func (suite *ReactorEngineSuite_CommittedPhase) TestStartupInCommittedPhase_DKGEndStateAlreadySet() { 458 459 // we are in the EpochSetup phase 460 suite.snap.On("Phase").Return(flow.EpochPhaseCommitted, nil).Once() 461 // the dkg for this epoch has been started and ended 462 suite.dkgState.On("GetDKGStarted", suite.NextEpochCounter()).Return(true, nil).Once() 463 suite.dkgState.On("GetDKGEndState", suite.NextEpochCounter()).Return(flow.DKGEndStateNoKey, nil).Once() 464 465 // start up the engine 466 unittest.AssertClosesBefore(suite.T(), suite.engine.Ready(), time.Second) 467 468 // we should not have instantiated the DKG 469 suite.factory.AssertNotCalled(suite.T(), "Create", 470 dkgmodule.CanonicalInstanceID(suite.firstBlock.ChainID, suite.NextEpochCounter()), 471 mock.Anything, 472 mock.Anything, 473 ) 474 } 475 476 // TestStartupInCommittedPhase_InconsistentKey tests that the dkg end state is correctly 477 // set when starting in EpochCommitted phase and we have stored an inconsistent key. 478 func (suite *ReactorEngineSuite_CommittedPhase) TestStartupInCommittedPhase_InconsistentKey() { 479 480 // we are in the EpochSetup phase 481 suite.snap.On("Phase").Return(flow.EpochPhaseCommitted, nil).Once() 482 // the dkg for this epoch has been started but not ended 483 suite.dkgState.On("GetDKGStarted", suite.NextEpochCounter()).Return(true, nil).Once() 484 suite.dkgState.On("GetDKGEndState", suite.NextEpochCounter()).Return(flow.DKGEndStateUnknown, storerr.ErrNotFound).Once() 485 486 // set our global pub key to a random value 487 suite.myGlobalBeaconPubKey = unittest.RandomBeaconPriv().PublicKey() 488 489 // start up the engine 490 unittest.AssertClosesBefore(suite.T(), suite.engine.Ready(), time.Second) 491 492 // we should not have instantiated the DKG 493 suite.factory.AssertNotCalled(suite.T(), "Create", 494 dkgmodule.CanonicalInstanceID(suite.firstBlock.ChainID, suite.NextEpochCounter()), 495 mock.Anything, 496 mock.Anything, 497 ) 498 // should set DKG end state 499 suite.Assert().Equal(flow.DKGEndStateInconsistentKey, suite.dkgEndState) 500 } 501 502 // TestStartupInCommittedPhase_MissingKey tests that the dkg end state is correctly 503 // set when starting in EpochCommitted phase and we have not stored any key. 504 func (suite *ReactorEngineSuite_CommittedPhase) TestStartupInCommittedPhase_MissingKey() { 505 506 // we are in the EpochSetup phase 507 suite.snap.On("Phase").Return(flow.EpochPhaseCommitted, nil).Once() 508 // the dkg for this epoch has been started but not ended 509 suite.dkgState.On("GetDKGStarted", suite.NextEpochCounter()).Return(true, nil).Once() 510 suite.dkgState.On("GetDKGEndState", suite.NextEpochCounter()).Return(flow.DKGEndStateUnknown, storerr.ErrNotFound).Once() 511 512 // remove our key 513 suite.myLocalBeaconKey = nil 514 515 // start up the engine 516 unittest.AssertClosesBefore(suite.T(), suite.engine.Ready(), time.Second) 517 518 // we should not have instantiated the DKG 519 suite.factory.AssertNotCalled(suite.T(), "Create", 520 dkgmodule.CanonicalInstanceID(suite.firstBlock.ChainID, suite.NextEpochCounter()), 521 mock.Anything, 522 mock.Anything, 523 ) 524 // should set DKG end state 525 suite.Assert().Equal(flow.DKGEndStateNoKey, suite.dkgEndState) 526 } 527 528 // utility function to track the number of warn-level calls to a logger 529 func hookedLogger(calls *int) zerolog.Logger { 530 hook := zerolog.HookFunc(func(e *zerolog.Event, level zerolog.Level, message string) { 531 if level == zerolog.WarnLevel { 532 *calls++ 533 } 534 }) 535 return zerolog.New(os.Stdout).Level(zerolog.InfoLevel).Hook(hook) 536 }