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 }