github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/engine/common/follower/pending_tree/pending_tree_test.go (about)

     1  package pending_tree
     2  
     3  import (
     4  	"fmt"
     5  	"math/rand"
     6  	"testing"
     7  
     8  	"github.com/stretchr/testify/assert"
     9  	"github.com/stretchr/testify/require"
    10  	"github.com/stretchr/testify/suite"
    11  	"golang.org/x/exp/slices"
    12  
    13  	"github.com/onflow/flow-go/consensus/hotstuff/model"
    14  	"github.com/onflow/flow-go/model/flow"
    15  	"github.com/onflow/flow-go/utils/unittest"
    16  )
    17  
    18  func TestPendingTree(t *testing.T) {
    19  	suite.Run(t, new(PendingTreeSuite))
    20  }
    21  
    22  type PendingTreeSuite struct {
    23  	suite.Suite
    24  
    25  	finalized   *flow.Header
    26  	pendingTree *PendingTree
    27  }
    28  
    29  func (s *PendingTreeSuite) SetupTest() {
    30  	s.finalized = unittest.BlockHeaderFixture()
    31  	s.pendingTree = NewPendingTree(s.finalized)
    32  }
    33  
    34  // TestBlocksConnectToFinalized tests that adding blocks that directly connect to the finalized block result
    35  // in expect chain of connected blocks.
    36  // Having: F ← B1 ← B2 ← B3
    37  // Add [B1, B2, B3], expect to get [B1;QC_B1, B2;QC_B2; B3;QC_B3]
    38  func (s *PendingTreeSuite) TestBlocksConnectToFinalized() {
    39  	blocks := certifiedBlocksFixture(3, s.finalized)
    40  	connectedBlocks, err := s.pendingTree.AddBlocks(blocks)
    41  	require.NoError(s.T(), err)
    42  	require.Equal(s.T(), blocks, connectedBlocks)
    43  }
    44  
    45  // TestBlocksAreNotConnectedToFinalized tests that adding blocks that don't connect to the finalized block result
    46  // in empty list of connected blocks.
    47  // Having: F ← B1 ← B2 ← B3
    48  // Add [B2, B3], expect to get []
    49  func (s *PendingTreeSuite) TestBlocksAreNotConnectedToFinalized() {
    50  	blocks := certifiedBlocksFixture(3, s.finalized)
    51  	connectedBlocks, err := s.pendingTree.AddBlocks(blocks[1:])
    52  	require.NoError(s.T(), err)
    53  	require.Empty(s.T(), connectedBlocks)
    54  }
    55  
    56  // TestInsertingMissingBlockToFinalized tests that adding blocks that don't connect to the finalized block result
    57  // in empty list of connected blocks. After adding missing blocks all connected blocks are correctly returned.
    58  // Having: F ← B1 ← B2 ← B3 ← B4 ← B5
    59  // Add [B3, B4, B5], expect to get []
    60  // Add [B1, B2], expect to get [B1, B2, B3, B4, B5]
    61  func (s *PendingTreeSuite) TestInsertingMissingBlockToFinalized() {
    62  	blocks := certifiedBlocksFixture(5, s.finalized)
    63  	connectedBlocks, err := s.pendingTree.AddBlocks(blocks[len(blocks)-3:])
    64  	require.NoError(s.T(), err)
    65  	require.Empty(s.T(), connectedBlocks)
    66  
    67  	connectedBlocks, err = s.pendingTree.AddBlocks(blocks[:len(blocks)-3])
    68  	require.NoError(s.T(), err)
    69  	require.Equal(s.T(), blocks, connectedBlocks)
    70  }
    71  
    72  // TestInsertingMissingBlockToFinalized tests that adding blocks that don't connect to the finalized block result
    73  // in empty list of connected blocks. After adding missing block all connected blocks across all forks are correctly collected
    74  // and returned.
    75  // Having:
    76  //
    77  //	       ↙ B2 ← B3
    78  //	F ← B1 ← B4 ← B5 ← B6 ← B7
    79  //
    80  // Add [B2, B3], expect to get []
    81  // Add [B4, B5, B6, B7], expect to get []
    82  // Add [B1], expect to get [B1, B2, B3, B4, B5, B6, B7]
    83  func (s *PendingTreeSuite) TestAllConnectedForksAreCollected() {
    84  	longestFork := certifiedBlocksFixture(5, s.finalized)
    85  	B2 := unittest.BlockWithParentFixture(longestFork[0].Block.Header)
    86  	// make sure short fork doesn't have conflicting views, so we don't trigger exception
    87  	B2.Header.View = longestFork[len(longestFork)-1].Block.Header.View + 1
    88  	B3 := unittest.BlockWithParentFixture(B2.Header)
    89  	shortFork := []flow.CertifiedBlock{{
    90  		Block:        B2,
    91  		CertifyingQC: B3.Header.QuorumCertificate(),
    92  	}, certifiedBlockFixture(B3)}
    93  
    94  	connectedBlocks, err := s.pendingTree.AddBlocks(shortFork)
    95  	require.NoError(s.T(), err)
    96  	require.Empty(s.T(), connectedBlocks)
    97  
    98  	connectedBlocks, err = s.pendingTree.AddBlocks(longestFork[1:])
    99  	require.NoError(s.T(), err)
   100  	require.Empty(s.T(), connectedBlocks)
   101  
   102  	connectedBlocks, err = s.pendingTree.AddBlocks(longestFork[:1])
   103  	require.NoError(s.T(), err)
   104  	require.ElementsMatch(s.T(), append(longestFork, shortFork...), connectedBlocks)
   105  }
   106  
   107  // TestAddingConnectedBlocks tests that adding blocks that were already reported as connected is no-op.
   108  func (s *PendingTreeSuite) TestAddingConnectedBlocks() {
   109  	blocks := certifiedBlocksFixture(3, s.finalized)
   110  	connectedBlocks, err := s.pendingTree.AddBlocks(blocks)
   111  	require.NoError(s.T(), err)
   112  	require.Equal(s.T(), blocks, connectedBlocks)
   113  
   114  	connectedBlocks, err = s.pendingTree.AddBlocks(blocks)
   115  	require.NoError(s.T(), err)
   116  	require.Empty(s.T(), connectedBlocks)
   117  }
   118  
   119  // TestByzantineThresholdExceeded tests that submitting two certified blocks for the same view is reported as
   120  // byzantine threshold reached exception. This scenario is possible only if network has reached more than 1/3 byzantine participants.
   121  func (s *PendingTreeSuite) TestByzantineThresholdExceeded() {
   122  	block := unittest.BlockWithParentFixture(s.finalized)
   123  	conflictingBlock := unittest.BlockWithParentFixture(s.finalized)
   124  	// use same view for conflicted blocks, this is not possible unless there is more than
   125  	// 1/3 byzantine participants
   126  	conflictingBlock.Header.View = block.Header.View
   127  	_, err := s.pendingTree.AddBlocks([]flow.CertifiedBlock{certifiedBlockFixture(block)})
   128  	require.NoError(s.T(), err)
   129  	// adding same block should result in no-op
   130  	_, err = s.pendingTree.AddBlocks([]flow.CertifiedBlock{certifiedBlockFixture(block)})
   131  	require.NoError(s.T(), err)
   132  	connectedBlocks, err := s.pendingTree.AddBlocks([]flow.CertifiedBlock{certifiedBlockFixture(conflictingBlock)})
   133  	require.Empty(s.T(), connectedBlocks)
   134  	require.True(s.T(), model.IsByzantineThresholdExceededError(err))
   135  }
   136  
   137  // TestBatchWithSkipsAndInRandomOrder tests that providing a batch without specific order and even with skips in height
   138  // results in expected behavior. We expect that each of those blocks will be added to tree and as soon as we find a
   139  // finalized fork we should be able to observe it as result of invocation.
   140  // Having: F ← A ← B ← C ← D ← E
   141  // Randomly shuffle [B, C, D, E] and add it as single batch, expect [] connected blocks.
   142  // Insert [A], expect [A, B, C, D, E] connected blocks.
   143  func (s *PendingTreeSuite) TestBatchWithSkipsAndInRandomOrder() {
   144  	blocks := certifiedBlocksFixture(5, s.finalized)
   145  
   146  	rand.Shuffle(len(blocks)-1, func(i, j int) {
   147  		blocks[i+1], blocks[j+1] = blocks[j+1], blocks[i+1]
   148  	})
   149  	connectedBlocks, err := s.pendingTree.AddBlocks(blocks[1:])
   150  	require.NoError(s.T(), err)
   151  	assert.Empty(s.T(), connectedBlocks)
   152  
   153  	connectedBlocks, err = s.pendingTree.AddBlocks(blocks[0:1])
   154  	require.NoError(s.T(), err)
   155  
   156  	// restore view based order since that's what we will get from PendingTree
   157  	slices.SortFunc(blocks, func(lhs flow.CertifiedBlock, rhs flow.CertifiedBlock) int {
   158  		return int(lhs.View()) - int(rhs.View())
   159  	})
   160  
   161  	assert.Equal(s.T(), blocks, connectedBlocks)
   162  }
   163  
   164  // TestResolveBlocksAfterFinalization tests that finalizing a block performs resolution against tree state and collects
   165  // newly connected blocks(with the respect to new finalized state) and returns them as result.
   166  // Having:
   167  //
   168  //	       ↙ B2 ← B3
   169  //	F ← B1 ← B4 ← B5 ← B6 ← B7
   170  //
   171  // Add [B2, B3], expect to get []
   172  // Add [B5, B6, B7], expect to get []
   173  // Finalize B4, expect to get [B5, B6, B7]
   174  func (s *PendingTreeSuite) TestResolveBlocksAfterFinalization() {
   175  	longestFork := certifiedBlocksFixture(5, s.finalized)
   176  	B2 := unittest.BlockWithParentFixture(longestFork[0].Block.Header)
   177  	// make sure short fork doesn't have conflicting views, so we don't trigger exception
   178  	B2.Header.View = longestFork[len(longestFork)-1].Block.Header.View + 1
   179  	B3 := unittest.BlockWithParentFixture(B2.Header)
   180  	shortFork := []flow.CertifiedBlock{{
   181  		Block:        B2,
   182  		CertifyingQC: B3.Header.QuorumCertificate(),
   183  	}, certifiedBlockFixture(B3)}
   184  
   185  	connectedBlocks, err := s.pendingTree.AddBlocks(shortFork)
   186  	require.NoError(s.T(), err)
   187  	require.Empty(s.T(), connectedBlocks)
   188  
   189  	connectedBlocks, err = s.pendingTree.AddBlocks(longestFork[2:])
   190  	require.NoError(s.T(), err)
   191  	require.Empty(s.T(), connectedBlocks)
   192  
   193  	connectedBlocks, err = s.pendingTree.FinalizeFork(longestFork[1].Block.Header)
   194  	require.NoError(s.T(), err)
   195  	require.ElementsMatch(s.T(), longestFork[2:], connectedBlocks)
   196  }
   197  
   198  // TestBlocksLowerThanFinalizedView tests that implementation drops blocks lower than finalized view.
   199  func (s *PendingTreeSuite) TestBlocksLowerThanFinalizedView() {
   200  	block := unittest.BlockWithParentFixture(s.finalized)
   201  	newFinalized := unittest.BlockWithParentFixture(block.Header)
   202  	_, err := s.pendingTree.FinalizeFork(newFinalized.Header)
   203  	require.NoError(s.T(), err)
   204  	_, err = s.pendingTree.AddBlocks([]flow.CertifiedBlock{certifiedBlockFixture(block)})
   205  	require.NoError(s.T(), err)
   206  	require.Equal(s.T(), uint64(0), s.pendingTree.forest.GetSize())
   207  }
   208  
   209  // TestAddingBlockAfterFinalization tests that adding a batch of blocks which includes finalized block correctly returns
   210  // a chain of connected blocks without finalized one.
   211  // Having F ← A ← B ← D.
   212  // Adding [A, B, C] returns [A, B, C].
   213  // Finalize A.
   214  // Adding [A, B, C, D] returns [D] since A is already finalized, [B, C] are already stored and connected to the finalized state.
   215  func (s *PendingTreeSuite) TestAddingBlockAfterFinalization() {
   216  	blocks := certifiedBlocksFixture(4, s.finalized)
   217  
   218  	connectedBlocks, err := s.pendingTree.AddBlocks(blocks[:3])
   219  	require.NoError(s.T(), err)
   220  	assert.Equal(s.T(), blocks[:3], connectedBlocks)
   221  
   222  	_, err = s.pendingTree.FinalizeFork(blocks[0].Block.Header)
   223  	require.NoError(s.T(), err)
   224  
   225  	connectedBlocks, err = s.pendingTree.AddBlocks(blocks)
   226  	require.NoError(s.T(), err)
   227  	assert.Equal(s.T(), blocks[3:], connectedBlocks)
   228  }
   229  
   230  // TestAddingBlocksWithSameHeight tests that adding blocks with same height(which results in multiple forks) that are connected
   231  // to finalized state are properly marked and returned as connected blocks.
   232  // / Having F ← A ← C
   233  // /          ↖ B ← D ← E
   234  // Adding [A, B, D] returns [A, B, D]
   235  // Adding [C, E] returns [C, E].
   236  func (s *PendingTreeSuite) TestAddingBlocksWithSameHeight() {
   237  	A := unittest.BlockWithParentFixture(s.finalized)
   238  	B := unittest.BlockWithParentFixture(s.finalized)
   239  	B.Header.View = A.Header.View + 1
   240  	C := unittest.BlockWithParentFixture(A.Header)
   241  	C.Header.View = B.Header.View + 1
   242  	D := unittest.BlockWithParentFixture(B.Header)
   243  	D.Header.View = C.Header.View + 1
   244  	E := unittest.BlockWithParentFixture(D.Header)
   245  	E.Header.View = D.Header.View + 1
   246  
   247  	firstBatch := []flow.CertifiedBlock{certifiedBlockFixture(A), certifiedBlockFixture(B), certifiedBlockFixture(D)}
   248  	secondBatch := []flow.CertifiedBlock{certifiedBlockFixture(C), certifiedBlockFixture(E)}
   249  
   250  	actual, err := s.pendingTree.AddBlocks(firstBatch)
   251  	require.NoError(s.T(), err)
   252  	require.Equal(s.T(), firstBatch, actual)
   253  
   254  	actual, err = s.pendingTree.AddBlocks(secondBatch)
   255  	require.NoError(s.T(), err)
   256  	require.Equal(s.T(), secondBatch, actual)
   257  }
   258  
   259  // certifiedBlocksFixture builds a chain of certified blocks starting at some block.
   260  func certifiedBlocksFixture(count int, parent *flow.Header) []flow.CertifiedBlock {
   261  	result := make([]flow.CertifiedBlock, 0, count)
   262  	blocks := unittest.ChainFixtureFrom(count, parent)
   263  	for i := 0; i < count-1; i++ {
   264  		certBlock, err := flow.NewCertifiedBlock(blocks[i], blocks[i+1].Header.QuorumCertificate())
   265  		if err != nil {
   266  			// this should never happen, as we are specifically constructing a certifying QC for the input block
   267  			panic(fmt.Sprintf("unexpected error constructing certified block: %s", err.Error()))
   268  		}
   269  		result = append(result, certBlock)
   270  	}
   271  	result = append(result, certifiedBlockFixture(blocks[len(blocks)-1]))
   272  	return result
   273  }
   274  
   275  // certifiedBlockFixture builds a certified block using a QC with fixture signatures.
   276  func certifiedBlockFixture(block *flow.Block) flow.CertifiedBlock {
   277  	certBlock, err := flow.NewCertifiedBlock(block, unittest.CertifyBlock(block.Header))
   278  	if err != nil {
   279  		// this should never happen, as we are specifically constructing a certifying QC for the input block
   280  		panic(fmt.Sprintf("unexpected error constructing certified block: %s", err.Error()))
   281  	}
   282  	return certBlock
   283  }