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  }