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  }