github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/module/mempool/consensus/execution_tree_test.go (about) 1 package consensus 2 3 import ( 4 "reflect" 5 "testing" 6 7 "github.com/stretchr/testify/assert" 8 "github.com/stretchr/testify/suite" 9 10 "github.com/onflow/flow-go/module/mempool" 11 12 "github.com/onflow/flow-go/model/flow" 13 14 "github.com/onflow/flow-go/utils/unittest" 15 ) 16 17 func TestReceiptsForest(t *testing.T) { 18 suite.Run(t, new(ExecutionTreeTestSuite)) 19 } 20 21 type ExecutionTreeTestSuite struct { 22 suite.Suite 23 Forest *ExecutionTree 24 } 25 26 func (et *ExecutionTreeTestSuite) SetupTest() { 27 et.Forest = NewExecutionTree() 28 } 29 30 // addReceiptForest creates an Execution Tree for testing purposes and stores it in the ResultForest. 31 // Nomenclature: 32 // - `r[A]` Execution Result for block A 33 // if we have multiple _different_ results for block A, 34 // we denote them as `r[A]_1`, `r[A]_2` ... 35 // - `ER[r]` Execution Receipt committing to result `r` 36 // if we have multiple _different_ receipts committing to the same result `r`, 37 // we denote them as `ER[r]_1`, `ER[r]_2` ... 38 // - Multiple Execution Receipts for the same result form an Equivalence Class, e.g. {`ER[r]_1`, `ER[r]_2`, ...} 39 // For brevity, we denote the Equivalence Class as `r{ER_1, ER_2, ...}` 40 // 41 // We consider the following forest of blocks where the number indicates the block height: 42 // 43 // : <- A10 <- A11 44 // : 45 // : <- B10 <- B11 <- B12 46 // : ^-- C11 <- C12 <- C13 47 // : 48 // : ?<- D13 49 // pruned height 50 // 51 // We construct the following Execution Tree: 52 // 53 // : 54 // : <- r[A10]{ER} <- r[A11]{ER} 55 // : 56 // : <- r[B10]{ER} <- r[B11]_1 {ER_1, ER_2} <- r[B12]_1 {ER} 57 // : ^-- r[B11]_2 {ER} <- r[B12]_2 {ER} 58 // : ^ 59 // : └-- r[C11] {ER_1, ER_2} < . . . ? ? ? ? . . <- r[C13] {ER} 60 // : 61 // : ? ? ? ? <- r[C13] {ER} 62 // pruned height 63 func (et *ExecutionTreeTestSuite) createExecutionTree() (map[string]*flow.Block, map[string]*flow.ExecutionResult, map[string]*flow.ExecutionReceipt) { 64 // Make blocks 65 blocks := make(map[string]*flow.Block) 66 67 blocks["A10"] = makeBlockWithHeight(10) 68 blocks["A11"] = makeChildBlock(blocks["A10"]) 69 70 blocks["B10"] = makeBlockWithHeight(10) 71 blocks["B11"] = makeChildBlock(blocks["B10"]) 72 blocks["B12"] = makeChildBlock(blocks["B11"]) 73 74 blocks["C11"] = makeChildBlock(blocks["B10"]) 75 blocks["C12"] = makeChildBlock(blocks["C11"]) 76 blocks["C13"] = makeChildBlock(blocks["C12"]) 77 78 blocks["D13"] = makeBlockWithHeight(13) 79 80 // Make Results 81 results := make(map[string]*flow.ExecutionResult) 82 results["r[A10]"] = unittest.ExecutionResultFixture(unittest.WithBlock(blocks["A10"])) 83 results["r[A11]"] = unittest.ExecutionResultFixture(unittest.WithBlock(blocks["A11"]), unittest.WithPreviousResult(*results["r[A10]"])) 84 85 results["r[B10]"] = unittest.ExecutionResultFixture(unittest.WithBlock(blocks["B10"])) 86 results["r[B11_1]"] = unittest.ExecutionResultFixture(unittest.WithBlock(blocks["B11"]), unittest.WithPreviousResult(*results["r[B10]"])) 87 results["r[B12_1]"] = unittest.ExecutionResultFixture(unittest.WithBlock(blocks["B12"]), unittest.WithPreviousResult(*results["r[B11_1]"])) 88 results["r[B11_2]"] = unittest.ExecutionResultFixture(unittest.WithBlock(blocks["B11"]), unittest.WithPreviousResult(*results["r[B10]"])) 89 results["r[B12_2]"] = unittest.ExecutionResultFixture(unittest.WithBlock(blocks["B12"]), unittest.WithPreviousResult(*results["r[B11_2]"])) 90 91 results["r[C11]"] = unittest.ExecutionResultFixture(unittest.WithBlock(blocks["C11"]), unittest.WithPreviousResult(*results["r[B10]"])) 92 results["r[C12]"] = unittest.ExecutionResultFixture(unittest.WithBlock(blocks["C12"]), unittest.WithPreviousResult(*results["r[C11]"])) 93 results["r[C13]"] = unittest.ExecutionResultFixture(unittest.WithBlock(blocks["C13"]), unittest.WithPreviousResult(*results["r[C12]"])) 94 95 results["r[D13]"] = unittest.ExecutionResultFixture(unittest.WithBlock(blocks["D13"])) 96 97 // Make Receipts 98 receipts := make(map[string]*flow.ExecutionReceipt) 99 100 receipts["ER[r[A10]]"] = unittest.ExecutionReceiptFixture(unittest.WithResult(results["r[A10]"])) 101 receipts["ER[r[A11]]"] = unittest.ExecutionReceiptFixture(unittest.WithResult(results["r[A11]"])) 102 103 receipts["ER[r[B10]]"] = unittest.ExecutionReceiptFixture(unittest.WithResult(results["r[B10]"])) 104 receipts["ER[r[B11]_1]_1"] = unittest.ExecutionReceiptFixture(unittest.WithResult(results["r[B11_1]"])) 105 receipts["ER[r[B11]_1]_2"] = unittest.ExecutionReceiptFixture(unittest.WithResult(results["r[B11_1]"])) 106 receipts["ER[r[B12]_1]"] = unittest.ExecutionReceiptFixture(unittest.WithResult(results["r[B12_1]"])) 107 108 receipts["ER[r[B11]_2]"] = unittest.ExecutionReceiptFixture(unittest.WithResult(results["r[B11_2]"])) 109 receipts["ER[r[B12]_2]"] = unittest.ExecutionReceiptFixture(unittest.WithResult(results["r[B12_2]"])) 110 111 receipts["ER[r[C11]]_1"] = unittest.ExecutionReceiptFixture(unittest.WithResult(results["r[C11]"])) 112 receipts["ER[r[C11]]_2"] = unittest.ExecutionReceiptFixture(unittest.WithResult(results["r[C11]"])) 113 114 receipts["ER[r[C13]]"] = unittest.ExecutionReceiptFixture(unittest.WithResult(results["r[C13]"])) 115 receipts["ER[r[D13]]"] = unittest.ExecutionReceiptFixture(unittest.WithResult(results["r[D13]"])) 116 117 return blocks, results, receipts 118 } 119 120 func (et *ExecutionTreeTestSuite) addReceipts2ReceiptsForest(receipts map[string]*flow.ExecutionReceipt, blocks map[string]*flow.Block) { 121 blockById := make(map[flow.Identifier]*flow.Block) 122 for _, block := range blocks { 123 blockById[block.ID()] = block 124 } 125 for name, rcpt := range receipts { 126 block := blockById[rcpt.ExecutionResult.BlockID] 127 _, err := et.Forest.AddReceipt(rcpt, block.Header) 128 if err != nil { 129 et.FailNow("failed to add receipt '%s'", name) 130 } 131 } 132 } 133 134 // Receipts that are already included in the fork should be skipped. 135 func (et *ExecutionTreeTestSuite) Test_Initialization() { 136 assert.Equal(et.T(), uint(0), et.Forest.Size()) 137 assert.Equal(et.T(), uint64(0), et.Forest.LowestHeight()) 138 } 139 140 // Test_AddReceipt checks the Forest's AddReceipt method. 141 // Receipts that are already included in the fork should be skipped. 142 func (et *ExecutionTreeTestSuite) Test_AddReceipt() { 143 block := unittest.BlockFixture() 144 receipt := unittest.ReceiptForBlockFixture(&block) 145 146 // add should succeed and increase size 147 added, err := et.Forest.AddReceipt(receipt, block.Header) 148 assert.NoError(et.T(), err) 149 assert.True(et.T(), added) 150 assert.Equal(et.T(), uint(1), et.Forest.Size()) 151 152 // adding different receipt for same result 153 receipt2 := unittest.ExecutionReceiptFixture(unittest.WithResult(&receipt.ExecutionResult)) 154 added, err = et.Forest.AddReceipt(receipt2, block.Header) 155 assert.NoError(et.T(), err) 156 assert.True(et.T(), added) 157 assert.Equal(et.T(), uint(2), et.Forest.Size()) 158 159 // repeated addition should be idempotent 160 added, err = et.Forest.AddReceipt(receipt, block.Header) 161 assert.NoError(et.T(), err) 162 assert.False(et.T(), added) 163 assert.Equal(et.T(), uint(2), et.Forest.Size()) 164 } 165 166 // Test_AddResult_Detached verifies that vertices can be added to the Execution Tree without requiring 167 // an Execution Receipt. Here, we add a result for a completely detached block. Starting a tree search 168 // from this result should not yield any receipts. 169 func (et *ExecutionTreeTestSuite) Test_AddResult_Detached() { 170 miscBlock := makeBlockWithHeight(101) 171 miscResult := unittest.ExecutionResultFixture(unittest.WithBlock(miscBlock)) 172 173 err := et.Forest.AddResult(miscResult, miscBlock.Header) 174 assert.NoError(et.T(), err) 175 collectedReceipts, err := et.Forest.ReachableReceipts(miscResult.ID(), anyBlock(), anyReceipt()) 176 assert.NoError(et.T(), err) 177 et.Assert().Empty(collectedReceipts) 178 } 179 180 // Test_AddResult_Bridge verifies that vertices can be added to the Execution Tree without requiring 181 // an Execution Receipt. Here, we add the result r[C12], which closes the gap between r[C11] and r[C13]. 182 // Hence, the tree search should reach r[C13]. 183 func (et *ExecutionTreeTestSuite) Test_AddResult_Bridge() { 184 blocks, results, receipts := et.createExecutionTree() 185 et.addReceipts2ReceiptsForest(receipts, blocks) 186 187 // restrict traversal to B10 <- C11 <- C12 <- C13 188 blockFilter := func(h *flow.Header) bool { 189 for _, blockName := range []string{"B10", "C11", "C12", "C13"} { 190 if blocks[blockName].ID() == h.ID() { 191 return true 192 } 193 } 194 return false 195 } 196 197 // before we add result r[C12], tree search should not be able to reach r[C13] 198 collectedReceipts, err := et.Forest.ReachableReceipts(results["r[B10]"].ID(), blockFilter, anyReceipt()) 199 assert.NoError(et.T(), err) 200 expected := et.toSet("ER[r[B10]]", "ER[r[C11]]_1", "ER[r[C11]]_2") 201 et.Assert().True(reflect.DeepEqual(expected, et.receiptSet(collectedReceipts, receipts))) 202 203 // after we added r[C12], tree search should reach r[C13] and hence include the corresponding receipt ER[r[C13]] 204 err = et.Forest.AddResult(results["r[C12]"], blocks["C12"].Header) 205 assert.NoError(et.T(), err) 206 collectedReceipts, err = et.Forest.ReachableReceipts(results["r[B10]"].ID(), blockFilter, anyReceipt()) 207 assert.NoError(et.T(), err) 208 expected = et.toSet("ER[r[B10]]", "ER[r[C11]]_1", "ER[r[C11]]_2", "ER[r[C13]]") 209 et.Assert().True(reflect.DeepEqual(expected, et.receiptSet(collectedReceipts, receipts))) 210 } 211 212 // Test_FullTreeSearch verifies that Receipt Forest enumerates all receipts that are 213 // reachable from the given result 214 func (et *ExecutionTreeTestSuite) Test_FullTreeSearch() { 215 blocks, _, receipts := et.createExecutionTree() 216 et.addReceipts2ReceiptsForest(receipts, blocks) 217 218 // search Execution Tree starting from result `r[A10]` 219 collectedReceipts, err := et.Forest.ReachableReceipts(receipts["ER[r[A10]]"].ExecutionResult.ID(), anyBlock(), anyReceipt()) 220 assert.NoError(et.T(), err) 221 et.Assert().True(reflect.DeepEqual(et.toSet("ER[r[A10]]", "ER[r[A11]]"), et.receiptSet(collectedReceipts, receipts))) 222 223 // search Execution Tree starting from result `r[B10]` 224 collectedReceipts, err = et.Forest.ReachableReceipts(receipts["ER[r[B10]]"].ExecutionResult.ID(), anyBlock(), anyReceipt()) 225 assert.NoError(et.T(), err) 226 expected := et.toSet( 227 "ER[r[B10]]", "ER[r[B11]_1]_1", "ER[r[B11]_1]_2", "ER[r[B12]_1]", 228 "ER[r[B11]_2]", "ER[r[B12]_2]", 229 "ER[r[C11]]_1", "ER[r[C11]]_2", 230 ) 231 et.Assert().True(reflect.DeepEqual(expected, et.receiptSet(collectedReceipts, receipts))) 232 233 // search Execution Tree starting from result `r[B11]_2` 234 collectedReceipts, err = et.Forest.ReachableReceipts(receipts["ER[r[B11]_2]"].ExecutionResult.ID(), anyBlock(), anyReceipt()) 235 assert.NoError(et.T(), err) 236 et.Assert().True(reflect.DeepEqual(et.toSet("ER[r[B11]_2]", "ER[r[B12]_2]"), et.receiptSet(collectedReceipts, receipts))) 237 238 // search Execution Tree starting from result `r[C13]` 239 collectedReceipts, err = et.Forest.ReachableReceipts(receipts["ER[r[C13]]"].ExecutionResult.ID(), anyBlock(), anyReceipt()) 240 assert.NoError(et.T(), err) 241 et.Assert().True(reflect.DeepEqual(et.toSet("ER[r[C13]]"), et.receiptSet(collectedReceipts, receipts))) 242 } 243 244 // Test_ReceiptSorted verifies that receipts are ordered in a "parent first" manner 245 func (et *ExecutionTreeTestSuite) Test_ReceiptOrdered() { 246 blocks, results, receipts := et.createExecutionTree() 247 et.addReceipts2ReceiptsForest(receipts, blocks) 248 249 // search Execution Tree starting from result `r[B10]` 250 collectedReceipts, err := et.Forest.ReachableReceipts(receipts["ER[r[B10]]"].ExecutionResult.ID(), anyBlock(), anyReceipt()) 251 assert.NoError(et.T(), err) 252 253 // first receipt must be for `r[B10]` 254 id := collectedReceipts[0].ExecutionResult.ID() 255 et.Assert().Equal(results["r[B10]"].ID(), id) 256 // for all subsequent receipts, a receipt committing to the parent result must have been listed before 257 knownResults := make(map[flow.Identifier]struct{}) 258 knownResults[id] = struct{}{} 259 for _, rcpt := range collectedReceipts[1:] { 260 _, found := knownResults[rcpt.ExecutionResult.PreviousResultID] 261 et.Assert().True(found) 262 knownResults[rcpt.ExecutionResult.ID()] = struct{}{} 263 } 264 } 265 266 // Test_FilterReceipts checks that ExecutionTree does filter Receipts as directed by the ReceiptFilter. 267 func (et *ExecutionTreeTestSuite) Test_FilterReceipts() { 268 blocks, _, receipts := et.createExecutionTree() 269 et.addReceipts2ReceiptsForest(receipts, blocks) 270 271 receiptFilter := func(rcpt *flow.ExecutionReceipt) bool { 272 for _, receiptName := range []string{"ER[r[B10]]", "ER[r[B11]_1]_2", "ER[r[B12]_2]", "ER[r[C11]]_1"} { 273 if receipts[receiptName].ID() == rcpt.ID() { 274 return false 275 } 276 } 277 return true 278 } 279 280 // search Execution Tree starting from result `r[B10]` 281 collectedReceipts, err := et.Forest.ReachableReceipts(receipts["ER[r[B10]]"].ExecutionResult.ID(), anyBlock(), receiptFilter) 282 assert.NoError(et.T(), err) 283 expected := et.toSet("ER[r[B11]_1]_1", "ER[r[B12]_1]", "ER[r[B11]_2]", "ER[r[C11]]_2") 284 et.Assert().True(reflect.DeepEqual(expected, et.receiptSet(collectedReceipts, receipts))) 285 } 286 287 // Test_RootBlockExcluded checks that ExecutionTree does not traverses results for excluded forks. 288 // In this specific test, we set the root results' block to be excluded. Therefore, the 289 // tree search should stop immediately and no result should be returned. 290 func (et *ExecutionTreeTestSuite) Test_RootBlockExcluded() { 291 blocks, _, receipts := et.createExecutionTree() 292 et.addReceipts2ReceiptsForest(receipts, blocks) 293 294 blockFilter := func(h *flow.Header) bool { 295 return blocks["B10"].ID() != h.ID() 296 } 297 298 // search Execution Tree starting from result `r[B10]` 299 collectedReceipts, err := et.Forest.ReachableReceipts(receipts["ER[r[B10]]"].ExecutionResult.ID(), blockFilter, anyReceipt()) 300 assert.NoError(et.T(), err) 301 assert.Empty(et.T(), collectedReceipts) 302 } 303 304 // Test_FilterChainForks checks that ExecutionTree does not traverses results for excluded forks 305 func (et *ExecutionTreeTestSuite) Test_FilterChainForks() { 306 blocks, _, receipts := et.createExecutionTree() 307 et.addReceipts2ReceiptsForest(receipts, blocks) 308 309 blockFilter := func(h *flow.Header) bool { 310 return blocks["B11"].ID() != h.ID() 311 } 312 313 // search Execution Tree starting from result `r[B10]`: fork starting from B11 should be excluded 314 collectedReceipts, err := et.Forest.ReachableReceipts(receipts["ER[r[B10]]"].ExecutionResult.ID(), blockFilter, anyReceipt()) 315 assert.NoError(et.T(), err) 316 expected := et.toSet("ER[r[B10]]", "ER[r[C11]]_1", "ER[r[C11]]_2") 317 et.Assert().True(reflect.DeepEqual(expected, et.receiptSet(collectedReceipts, receipts))) 318 } 319 320 // Test_ExcludeReceiptsForSealedBlock verifies that, even though we are filtering out the 321 // receipts for the root result, the tree search still traverses to the derived results 322 func (et *ExecutionTreeTestSuite) Test_ExcludeReceiptsForSealedBlock() { 323 blocks, _, receipts := et.createExecutionTree() 324 et.addReceipts2ReceiptsForest(receipts, blocks) 325 326 receiptFilter := func(rcpt *flow.ExecutionReceipt) bool { 327 // exclude all receipts for block B11 328 return rcpt.ExecutionResult.BlockID != blocks["B11"].ID() 329 } 330 331 // search Execution Tree starting from result `r[B11]_1` 332 collectedReceipts, err := et.Forest.ReachableReceipts(receipts["ER[r[B11]_1]_1"].ExecutionResult.ID(), anyBlock(), receiptFilter) 333 assert.NoError(et.T(), err) 334 et.Assert().True(reflect.DeepEqual(et.toSet("ER[r[B12]_1]"), et.receiptSet(collectedReceipts, receipts))) 335 } 336 337 // Test_UnknownResult checks the behaviour of ExecutionTree when the search is started on an unknown result 338 func (et *ExecutionTreeTestSuite) Test_UnknownResult() { 339 blocks, _, receipts := et.createExecutionTree() 340 et.addReceipts2ReceiptsForest(receipts, blocks) 341 342 // search Execution Tree starting from result random result 343 _, err := et.Forest.ReachableReceipts(unittest.IdentifierFixture(), anyBlock(), anyReceipt()) 344 assert.Error(et.T(), err) 345 346 // search Execution Tree starting from parent result of "ER[r[D13]]"; While the result is referenced, 347 // a receipt committing to this result was never added. Hence the search should error 348 _, err = et.Forest.ReachableReceipts(receipts["ER[r[D13]]"].ExecutionResult.PreviousResultID, anyBlock(), anyReceipt()) 349 assert.Error(et.T(), err) 350 } 351 352 // Receipts that are already included in the fork should be skipped. 353 func (et *ExecutionTreeTestSuite) Test_Prune() { 354 blocks, _, receipts := et.createExecutionTree() 355 et.addReceipts2ReceiptsForest(receipts, blocks) 356 357 assert.Equal(et.T(), uint(12), et.Forest.Size()) 358 assert.Equal(et.T(), uint64(0), et.Forest.LowestHeight()) 359 360 // prunes all receipts for blocks with height _smaller_ than 12 361 err := et.Forest.PruneUpToHeight(12) 362 assert.NoError(et.T(), err) 363 assert.Equal(et.T(), uint(4), et.Forest.Size()) 364 365 // now, searching results from r[B11] should fail as the receipts were pruned 366 _, err = et.Forest.ReachableReceipts(receipts["ER[r[B11]_1]_2"].ExecutionResult.PreviousResultID, anyBlock(), anyReceipt()) 367 assert.Error(et.T(), err) 368 369 // now, searching results from r[B12] should fail as the receipts were pruned 370 collectedReceipts, err := et.Forest.ReachableReceipts(receipts["ER[r[B12]_1]"].ExecutionResult.ID(), anyBlock(), anyReceipt()) 371 assert.NoError(et.T(), err) 372 expected := et.toSet("ER[r[B12]_1]") 373 et.Assert().True(reflect.DeepEqual(expected, et.receiptSet(collectedReceipts, receipts))) 374 } 375 376 func anyBlock() mempool.BlockFilter { 377 return func(*flow.Header) bool { return true } 378 } 379 380 func anyReceipt() mempool.ReceiptFilter { 381 return func(*flow.ExecutionReceipt) bool { return true } 382 } 383 384 func makeBlockWithHeight(height uint64) *flow.Block { 385 block := unittest.BlockFixture() 386 block.Header.Height = height 387 return &block 388 } 389 390 func makeChildBlock(parent *flow.Block) *flow.Block { 391 return unittest.BlockWithParentFixture(parent.Header) 392 } 393 394 func (et *ExecutionTreeTestSuite) receiptSet(selected []*flow.ExecutionReceipt, receipts map[string]*flow.ExecutionReceipt) map[string]struct{} { 395 id2Name := make(map[flow.Identifier]string) 396 for name, rcpt := range receipts { 397 id2Name[rcpt.ID()] = name 398 } 399 400 names := make(map[string]struct{}) 401 for _, r := range selected { 402 name, found := id2Name[r.ID()] 403 if !found { 404 et.FailNow("unknown execution receipt %x", r.ID()) 405 } 406 names[name] = struct{}{} 407 } 408 return names 409 } 410 411 func (et *ExecutionTreeTestSuite) toSet(receiptNames ...string) map[string]struct{} { 412 set := make(map[string]struct{}) 413 for _, name := range receiptNames { 414 set[name] = struct{}{} 415 } 416 if len(set) != len(receiptNames) { 417 et.FailNow("repeated receipts") 418 } 419 return set 420 }