github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/state/fork/traversal_test.go (about)

     1  package fork
     2  
     3  import (
     4  	"errors"
     5  	"testing"
     6  
     7  	"github.com/stretchr/testify/mock"
     8  	"github.com/stretchr/testify/suite"
     9  
    10  	"github.com/onflow/flow-go/model/flow"
    11  	"github.com/onflow/flow-go/storage"
    12  	mockstorage "github.com/onflow/flow-go/storage/mock"
    13  	"github.com/onflow/flow-go/utils/unittest"
    14  )
    15  
    16  func TestTraverse(t *testing.T) {
    17  	suite.Run(t, new(TraverseSuite))
    18  }
    19  
    20  type TraverseSuite struct {
    21  	suite.Suite
    22  
    23  	byID     map[flow.Identifier]*flow.Header
    24  	byHeight map[uint64]*flow.Header
    25  	headers  *mockstorage.Headers
    26  	genesis  *flow.Header
    27  }
    28  
    29  func (s *TraverseSuite) SetupTest() {
    30  	// create a storage.Headers mock with a backing map
    31  	s.byID = make(map[flow.Identifier]*flow.Header)
    32  	s.byHeight = make(map[uint64]*flow.Header)
    33  	s.headers = new(mockstorage.Headers)
    34  	s.headers.On("ByBlockID", mock.Anything).Return(
    35  		func(id flow.Identifier) *flow.Header {
    36  			return s.byID[id]
    37  		},
    38  		func(id flow.Identifier) error {
    39  			_, ok := s.byID[id]
    40  			if !ok {
    41  				return storage.ErrNotFound
    42  			}
    43  			return nil
    44  		})
    45  
    46  	// populate the mocked header storage with genesis and 10 child blocks
    47  	genesis := unittest.BlockHeaderFixture()
    48  	genesis.Height = 0
    49  	s.byID[genesis.ID()] = genesis
    50  	s.byHeight[genesis.Height] = genesis
    51  	s.genesis = genesis
    52  
    53  	parent := genesis
    54  	for i := 0; i < 10; i++ {
    55  		child := unittest.BlockHeaderWithParentFixture(parent)
    56  		s.byID[child.ID()] = child
    57  		s.byHeight[child.Height] = child
    58  		parent = child
    59  	}
    60  }
    61  
    62  // TestTraverse_MissingForkHead tests the behaviour of block traversing for the
    63  // case where the fork head is an unknown block. We expect:
    64  // * traversal errors
    65  // * traversal does _not_ invoke the visitor callback
    66  func (s *TraverseSuite) TestTraverse_MissingForkHead() {
    67  	unknownForkHead := unittest.IdentifierFixture()
    68  
    69  	visitor := func(_ *flow.Header) error {
    70  		s.Require().Fail("visitor should not be called")
    71  		return nil
    72  	}
    73  
    74  	s.Run("TraverseBackward from non-existent start block", func() {
    75  		err := TraverseBackward(s.headers, unknownForkHead, visitor, IncludingBlock(s.genesis.ID()))
    76  		s.Require().Error(err)
    77  	})
    78  
    79  	// should return error and not call callback when start block doesn't exist
    80  	s.Run("non-existent start block", func() {
    81  		err := TraverseForward(s.headers, unknownForkHead, visitor, IncludingBlock(s.genesis.ID()))
    82  		s.Require().Error(err)
    83  	})
    84  }
    85  
    86  // TestTraverse_VisitorError tests the behaviour of block traversing for the
    87  // case where the visitor callback errors. We expect
    88  // * the visitor error is propagated by the block traversal
    89  func (s *TraverseSuite) TestTraverse_VisitorError() {
    90  	forkHead := s.byHeight[8].ID()
    91  
    92  	visitorError := errors.New("some visitor error")
    93  	visitor := func(_ *flow.Header) error { return visitorError }
    94  
    95  	s.Run("TraverseBackward with visitor error", func() {
    96  		err := TraverseBackward(s.headers, forkHead, visitor, IncludingHeight(1))
    97  		s.Require().ErrorIs(err, visitorError)
    98  	})
    99  
   100  	s.Run("TraverseForward with visitor error", func() {
   101  		err := TraverseForward(s.headers, forkHead, visitor, IncludingHeight(1))
   102  		s.Require().ErrorIs(err, visitorError)
   103  	})
   104  }
   105  
   106  // TestTraverse_UnknownTerminalBlock tests the behaviour of block traversing
   107  // for the case where the terminal block is unknown
   108  func (s *TraverseSuite) TestTraverse_UnknownTerminalBlock() {
   109  	forkHead := s.byHeight[8].ID()
   110  	unknownTerminal := unittest.IdentifierFixture()
   111  	visitor := func(_ *flow.Header) error {
   112  		s.Require().Fail("visitor should not be called")
   113  		return nil
   114  	}
   115  
   116  	s.Run("backwards traversal with non-existent terminal block (inclusive)", func() {
   117  		err := TraverseBackward(s.headers, forkHead, visitor, IncludingBlock(unknownTerminal))
   118  		s.Require().Error(err)
   119  	})
   120  
   121  	s.Run("backwards traversal with non-existent terminal block (exclusive)", func() {
   122  		err := TraverseBackward(s.headers, forkHead, visitor, ExcludingBlock(unknownTerminal))
   123  		s.Require().Error(err)
   124  	})
   125  
   126  	s.Run("forward traversal with non-existent terminal block (inclusive)", func() {
   127  		err := TraverseForward(s.headers, forkHead, visitor, IncludingBlock(unknownTerminal))
   128  		s.Require().Error(err)
   129  	})
   130  
   131  	s.Run("forward traversal with non-existent terminal block (exclusive)", func() {
   132  		err := TraverseForward(s.headers, forkHead, visitor, ExcludingBlock(unknownTerminal))
   133  		s.Require().Error(err)
   134  	})
   135  }
   136  
   137  // TestTraverseBackward_DownToBlock tests different happy-path scenarios for reverse
   138  // block traversing where the terminal block (lowest block) is specified by its ID
   139  func (s *TraverseSuite) TestTraverseBackward_DownToBlock() {
   140  
   141  	// edge case where start == end and the end block is _excluded_
   142  	s.Run("zero blocks to traverse", func() {
   143  		start := s.byHeight[5].ID()
   144  		end := s.byHeight[5].ID()
   145  
   146  		err := TraverseBackward(s.headers, start, func(header *flow.Header) error {
   147  			s.Require().Fail("visitor should not be called")
   148  			return nil
   149  		}, ExcludingBlock(end))
   150  		s.Require().NoError(err)
   151  	})
   152  
   153  	// edge case where start == end and the end block is _included_
   154  	s.Run("single block to traverse", func() {
   155  		start := s.byHeight[5].ID()
   156  		end := s.byHeight[5].ID()
   157  
   158  		called := 0
   159  		err := TraverseBackward(s.headers, start, func(header *flow.Header) error {
   160  			// should call callback for single block in traversal path
   161  			s.Require().Equal(start, header.ID())
   162  			// track calls - should only be called once
   163  			called++
   164  			return nil
   165  		}, IncludingBlock(end))
   166  		s.Require().NoError(err)
   167  		s.Require().Equal(1, called)
   168  	})
   169  
   170  	// should call the callback exactly once for each block in traversal path
   171  	// and not return an error
   172  	s.Run("multi-block traversal including terminal block", func() {
   173  		startHeight := uint64(8)
   174  		endHeight := uint64(4)
   175  
   176  		start := s.byHeight[startHeight].ID()
   177  		end := s.byHeight[endHeight].ID()
   178  
   179  		// assert that we are receiving the correct block at each height
   180  		height := startHeight
   181  		err := TraverseBackward(s.headers, start, func(header *flow.Header) error {
   182  			expectedID := s.byHeight[height].ID()
   183  			s.Require().Equal(expectedID, header.ID())
   184  			height--
   185  			return nil
   186  		}, IncludingBlock(end))
   187  		s.Require().NoError(err)
   188  		s.Require().Equal(endHeight, height+1)
   189  	})
   190  
   191  	// should call the callback exactly once for each block in traversal path
   192  	// and not return an error
   193  	s.Run("multi-block traversal excluding terminal block", func() {
   194  		startHeight := uint64(8)
   195  		endHeight := uint64(4)
   196  
   197  		start := s.byHeight[startHeight].ID()
   198  		end := s.byHeight[endHeight].ID()
   199  
   200  		// assert that we are receiving the correct block at each height
   201  		height := startHeight
   202  		err := TraverseBackward(s.headers, start, func(header *flow.Header) error {
   203  			expectedID := s.byHeight[height].ID()
   204  			s.Require().Equal(expectedID, header.ID())
   205  			height--
   206  			return nil
   207  		}, ExcludingBlock(end))
   208  		s.Require().NoError(err)
   209  		s.Require().Equal(endHeight, height)
   210  	})
   211  
   212  	// edge case where we traverse only the genesis block
   213  	s.Run("traversing only genesis block", func() {
   214  		genesisID := s.genesis.ID()
   215  
   216  		called := 0
   217  		err := TraverseBackward(s.headers, genesisID, func(header *flow.Header) error {
   218  			// should call callback for single block in traversal path
   219  			s.Require().Equal(genesisID, header.ID())
   220  			// track calls - should only be called once
   221  			called++
   222  			return nil
   223  		}, IncludingBlock(genesisID))
   224  		s.Require().NoError(err)
   225  		s.Require().Equal(1, called)
   226  	})
   227  }
   228  
   229  // TestTraverseBackward_DownToHeight tests different happy-path scenarios for reverse
   230  // block traversing where the terminal block (lowest block) is specified by height
   231  func (s *TraverseSuite) TestTraverseBackward_DownToHeight() {
   232  
   233  	// edge case where start == end and the end block is _excluded_
   234  	s.Run("zero blocks to traverse", func() {
   235  		startHeight := uint64(5)
   236  		start := s.byHeight[startHeight].ID()
   237  
   238  		err := TraverseBackward(s.headers, start, func(header *flow.Header) error {
   239  			s.Require().Fail("visitor should not be called")
   240  			return nil
   241  		}, ExcludingHeight(startHeight))
   242  		s.Require().NoError(err)
   243  	})
   244  
   245  	// edge case where start == end and the end block is _included_
   246  	s.Run("single block to traverse", func() {
   247  		startHeight := uint64(5)
   248  		start := s.byHeight[startHeight].ID()
   249  
   250  		called := 0
   251  		err := TraverseBackward(s.headers, start, func(header *flow.Header) error {
   252  			// should call callback for single block in traversal path
   253  			s.Require().Equal(start, header.ID())
   254  			// track calls - should only be called once
   255  			called++
   256  			return nil
   257  		}, IncludingHeight(startHeight))
   258  		s.Require().NoError(err)
   259  		s.Require().Equal(1, called)
   260  	})
   261  
   262  	// should call the callback exactly once for each block in traversal path
   263  	// and not return an error
   264  	s.Run("multi-block traversal including terminal block", func() {
   265  		startHeight := uint64(8)
   266  		endHeight := uint64(4)
   267  		start := s.byHeight[startHeight].ID()
   268  
   269  		// assert that we are receiving the correct block at each height
   270  		height := startHeight
   271  		err := TraverseBackward(s.headers, start, func(header *flow.Header) error {
   272  			expectedID := s.byHeight[height].ID()
   273  			s.Require().Equal(expectedID, header.ID())
   274  			height--
   275  			return nil
   276  		}, IncludingHeight(endHeight))
   277  		s.Require().NoError(err)
   278  		s.Require().Equal(endHeight, height+1)
   279  	})
   280  
   281  	// should call the callback exactly once for each block in traversal path
   282  	// and not return an error
   283  	s.Run("multi-block traversal excluding terminal block", func() {
   284  		startHeight := uint64(8)
   285  		endHeight := uint64(4)
   286  		start := s.byHeight[startHeight].ID()
   287  
   288  		// assert that we are receiving the correct block at each height
   289  		height := startHeight
   290  		err := TraverseBackward(s.headers, start, func(header *flow.Header) error {
   291  			expectedID := s.byHeight[height].ID()
   292  			s.Require().Equal(expectedID, header.ID())
   293  			height--
   294  			return nil
   295  		}, ExcludingHeight(endHeight))
   296  		s.Require().NoError(err)
   297  		s.Require().Equal(endHeight, height)
   298  	})
   299  
   300  	// edge case where we traverse only the genesis block
   301  	s.Run("traversing only genesis block", func() {
   302  		genesisID := s.genesis.ID()
   303  
   304  		called := 0
   305  		err := TraverseBackward(s.headers, genesisID, func(header *flow.Header) error {
   306  			// should call callback for single block in traversal path
   307  			s.Require().Equal(genesisID, header.ID())
   308  			// track calls - should only be called once
   309  			called++
   310  			return nil
   311  		}, IncludingHeight(s.genesis.Height))
   312  		s.Require().NoError(err)
   313  		s.Require().Equal(1, called)
   314  	})
   315  }
   316  
   317  // TestTraverseForward_UpFromBlock tests different happy-path scenarios for parent-first
   318  // block traversing where the terminal block (lowest block) is specified by its ID
   319  func (s *TraverseSuite) TestTraverseForward_UpFromBlock() {
   320  
   321  	// edge case where start == end and the terminal block is _excluded_
   322  	s.Run("zero blocks to traverse", func() {
   323  		upperBlock := s.byHeight[5].ID()
   324  		lowerBlock := s.byHeight[5].ID()
   325  
   326  		err := TraverseForward(s.headers, upperBlock, func(header *flow.Header) error {
   327  			s.Require().Fail("visitor should not be called")
   328  			return nil
   329  		}, ExcludingBlock(lowerBlock))
   330  		s.Require().NoError(err)
   331  	})
   332  
   333  	// should call the callback exactly once and not return an error when start == end
   334  	s.Run("single-block traversal", func() {
   335  		upperBlock := s.byHeight[5].ID()
   336  		lowerBlock := s.byHeight[5].ID()
   337  
   338  		called := 0
   339  		err := TraverseForward(s.headers, upperBlock, func(header *flow.Header) error {
   340  			// should call callback for single block in traversal path
   341  			s.Require().Equal(upperBlock, header.ID())
   342  			// track calls - should only be called once
   343  			called++
   344  			return nil
   345  		}, IncludingBlock(lowerBlock))
   346  		s.Require().NoError(err)
   347  		s.Require().Equal(1, called)
   348  	})
   349  
   350  	// should call the callback exactly once for each block in traversal path
   351  	// and not return an error
   352  	s.Run("multi-block traversal including terminal block", func() {
   353  		upperHeight := uint64(8)
   354  		lowerHeight := uint64(4)
   355  
   356  		upperBlock := s.byHeight[upperHeight].ID()
   357  		lowerBlock := s.byHeight[lowerHeight].ID()
   358  
   359  		// assert that we are receiving the correct block at each height
   360  		height := lowerHeight
   361  		err := TraverseForward(s.headers, upperBlock, func(header *flow.Header) error {
   362  			expectedID := s.byHeight[height].ID()
   363  			s.Require().Equal(height, header.Height)
   364  			s.Require().Equal(expectedID, header.ID())
   365  			height++
   366  			return nil
   367  		}, IncludingBlock(lowerBlock))
   368  		s.Require().NoError(err)
   369  		s.Require().Equal(height, upperHeight+1)
   370  	})
   371  
   372  	// should call the callback exactly once for each block in traversal path
   373  	// and not return an error
   374  	s.Run("multi-block traversal excluding terminal block", func() {
   375  		upperHeight := uint64(8)
   376  		lowerHeight := uint64(4)
   377  
   378  		upperBlock := s.byHeight[upperHeight].ID()
   379  		lowerBlock := s.byHeight[lowerHeight].ID()
   380  
   381  		// assert that we are receiving the correct block at each height
   382  		height := lowerHeight + 1
   383  		err := TraverseForward(s.headers, upperBlock, func(header *flow.Header) error {
   384  			expectedID := s.byHeight[height].ID()
   385  			s.Require().Equal(height, header.Height)
   386  			s.Require().Equal(expectedID, header.ID())
   387  			height++
   388  			return nil
   389  		}, ExcludingBlock(lowerBlock))
   390  		s.Require().NoError(err)
   391  		s.Require().Equal(height, upperHeight+1)
   392  	})
   393  
   394  	// edge case where we traverse only the genesis block
   395  	s.Run("traversing only genesis block", func() {
   396  		genesisID := s.genesis.ID()
   397  
   398  		called := 0
   399  		err := TraverseForward(s.headers, genesisID, func(header *flow.Header) error {
   400  			// should call callback for single block in traversal path
   401  			s.Require().Equal(genesisID, header.ID())
   402  			// track calls - should only be called once
   403  			called++
   404  			return nil
   405  		}, IncludingBlock(genesisID))
   406  		s.Require().NoError(err)
   407  		s.Require().Equal(1, called)
   408  	})
   409  }
   410  
   411  // TestTraverseForward_UpFromHeight tests different happy-path scenarios for parent-first
   412  // block traversing where the terminal block (lowest block) is specified by height
   413  func (s *TraverseSuite) TestTraverseForward_UpFromHeight() {
   414  
   415  	// edge case where start == end and the terminal block is _excluded_
   416  	s.Run("zero blocks to traverse", func() {
   417  		upperHeight := uint64(5)
   418  		upperBlock := s.byHeight[upperHeight].ID()
   419  
   420  		err := TraverseForward(s.headers, upperBlock, func(header *flow.Header) error {
   421  			s.Require().Fail("visitor should not be called")
   422  			return nil
   423  		}, ExcludingHeight(upperHeight))
   424  		s.Require().NoError(err)
   425  	})
   426  
   427  	// should call the callback exactly once and not return an error when start == end
   428  	s.Run("single-block traversal", func() {
   429  		upperHeight := uint64(5)
   430  		upperBlock := s.byHeight[upperHeight].ID()
   431  
   432  		called := 0
   433  		err := TraverseForward(s.headers, upperBlock, func(header *flow.Header) error {
   434  			// should call callback for single block in traversal path
   435  			s.Require().Equal(upperBlock, header.ID())
   436  			// track calls - should only be called once
   437  			called++
   438  			return nil
   439  		}, IncludingHeight(upperHeight))
   440  		s.Require().NoError(err)
   441  		s.Require().Equal(1, called)
   442  	})
   443  
   444  	// should call the callback exactly once for each block in traversal path
   445  	// and not return an error
   446  	s.Run("multi-block traversal including terminal block", func() {
   447  		upperHeight := uint64(8)
   448  		lowerHeight := uint64(4)
   449  		upperBlock := s.byHeight[upperHeight].ID()
   450  
   451  		// assert that we are receiving the correct block at each height
   452  		height := lowerHeight
   453  		err := TraverseForward(s.headers, upperBlock, func(header *flow.Header) error {
   454  			expectedID := s.byHeight[height].ID()
   455  			s.Require().Equal(height, header.Height)
   456  			s.Require().Equal(expectedID, header.ID())
   457  			height++
   458  			return nil
   459  		}, IncludingHeight(lowerHeight))
   460  		s.Require().NoError(err)
   461  		s.Require().Equal(height, upperHeight+1)
   462  	})
   463  
   464  	// should call the callback exactly once for each block in traversal path
   465  	// and not return an error
   466  	s.Run("multi-block traversal excluding terminal block", func() {
   467  		upperHeight := uint64(8)
   468  		lowerHeight := uint64(4)
   469  		upperBlock := s.byHeight[upperHeight].ID()
   470  
   471  		// assert that we are receiving the correct block at each height
   472  		height := lowerHeight + 1
   473  		err := TraverseForward(s.headers, upperBlock, func(header *flow.Header) error {
   474  			expectedID := s.byHeight[height].ID()
   475  			s.Require().Equal(height, header.Height)
   476  			s.Require().Equal(expectedID, header.ID())
   477  			height++
   478  			return nil
   479  		}, ExcludingHeight(lowerHeight))
   480  		s.Require().NoError(err)
   481  		s.Require().Equal(height, upperHeight+1)
   482  	})
   483  
   484  	// edge case where we traverse only the genesis block
   485  	s.Run("traversing only genesis block", func() {
   486  		genesisID := s.genesis.ID()
   487  
   488  		called := 0
   489  		err := TraverseForward(s.headers, genesisID, func(header *flow.Header) error {
   490  			// should call callback for single block in traversal path
   491  			s.Require().Equal(genesisID, header.ID())
   492  			// track calls - should only be called once
   493  			called++
   494  			return nil
   495  		}, IncludingHeight(s.genesis.Height))
   496  		s.Require().NoError(err)
   497  		s.Require().Equal(1, called)
   498  	})
   499  }
   500  
   501  // TestTraverse_OnDifferentForkThanTerminalBlock tests that block traversing
   502  // errors if the end block is on a different Fork. This is only applicable
   503  // when terminal block (lowest block) is specified by its ID.
   504  func (s *TraverseSuite) TestTraverse_OnDifferentForkThanTerminalBlock() {
   505  	forkHead := s.byHeight[8].ID()
   506  	noopVisitor := func(header *flow.Header) error { return nil }
   507  
   508  	// make other fork
   509  	otherForkHead := s.genesis
   510  	otherForkByHeight := make(map[uint64]*flow.Header)
   511  	for i := 0; i < 10; i++ {
   512  		child := unittest.BlockHeaderWithParentFixture(otherForkHead)
   513  		s.byID[child.ID()] = child
   514  		otherForkByHeight[child.Height] = child
   515  		otherForkHead = child
   516  	}
   517  	terminalBlockID := otherForkByHeight[2].ID()
   518  
   519  	s.Run("forwards traversal with terminal block (on different fork) included ", func() {
   520  		// assert that we are receiving the correct block at each height
   521  		err := TraverseForward(s.headers, forkHead, noopVisitor, ExcludingBlock(terminalBlockID))
   522  		s.Require().Error(err)
   523  	})
   524  
   525  	s.Run("forwards traversal with terminal block (on different fork) excluded ", func() {
   526  		// assert that we are receiving the correct block at each height
   527  		err := TraverseForward(s.headers, forkHead, noopVisitor, IncludingBlock(terminalBlockID))
   528  		s.Require().Error(err)
   529  	})
   530  
   531  	s.Run("backwards traversal with terminal block (on different fork) included ", func() {
   532  		// assert that we are receiving the correct block at each height
   533  		err := TraverseBackward(s.headers, forkHead, noopVisitor, ExcludingBlock(terminalBlockID))
   534  		s.Require().Error(err)
   535  	})
   536  
   537  	s.Run("backwards traversal with terminal block (on different fork) excluded ", func() {
   538  		// assert that we are receiving the correct block at each height
   539  		err := TraverseBackward(s.headers, forkHead, noopVisitor, IncludingBlock(terminalBlockID))
   540  		s.Require().Error(err)
   541  	})
   542  
   543  }