github.com/onflow/flow-go@v0.33.17/utils/unittest/epoch_builder.go (about) 1 package unittest 2 3 import ( 4 "context" 5 "math/rand" 6 "testing" 7 8 "github.com/stretchr/testify/require" 9 10 "github.com/onflow/flow-go/model/flow" 11 "github.com/onflow/flow-go/model/flow/filter" 12 "github.com/onflow/flow-go/state/protocol" 13 ) 14 15 // EpochHeights is a structure caching the results of building an epoch with 16 // EpochBuilder. It contains the first block height for each phase of the epoch. 17 type EpochHeights struct { 18 Counter uint64 // which epoch this is 19 Staking uint64 // first height of staking phase 20 Setup uint64 // first height of setup phase 21 Committed uint64 // first height of committed phase 22 CommittedFinal uint64 // final height of the committed phase 23 } 24 25 // FirstHeight returns the height of the first block in the epoch. 26 func (epoch EpochHeights) FirstHeight() uint64 { 27 return epoch.Staking 28 } 29 30 // FinalHeight returns the height of the first block in the epoch. 31 func (epoch EpochHeights) FinalHeight() uint64 { 32 return epoch.CommittedFinal 33 } 34 35 // Range returns the range of all heights that are in this epoch. 36 func (epoch EpochHeights) Range() []uint64 { 37 var heights []uint64 38 for height := epoch.Staking; height <= epoch.CommittedFinal; height++ { 39 heights = append(heights, height) 40 } 41 return heights 42 } 43 44 // StakingRange returns the range of all heights in the staking phase. 45 func (epoch EpochHeights) StakingRange() []uint64 { 46 var heights []uint64 47 for height := epoch.Staking; height < epoch.Setup; height++ { 48 heights = append(heights, height) 49 } 50 return heights 51 } 52 53 // SetupRange returns the range of all heights in the setup phase. 54 func (epoch EpochHeights) SetupRange() []uint64 { 55 var heights []uint64 56 for height := epoch.Setup; height < epoch.Committed; height++ { 57 heights = append(heights, height) 58 } 59 return heights 60 } 61 62 // CommittedRange returns the range of all heights in the committed phase. 63 func (epoch EpochHeights) CommittedRange() []uint64 { 64 var heights []uint64 65 for height := epoch.Committed; height < epoch.CommittedFinal; height++ { 66 heights = append(heights, height) 67 } 68 return heights 69 } 70 71 // EpochBuilder is a testing utility for building epochs into chain state. 72 type EpochBuilder struct { 73 t *testing.T 74 states []protocol.FollowerState 75 blocksByID map[flow.Identifier]*flow.Block 76 blocks []*flow.Block 77 built map[uint64]*EpochHeights 78 setupOpts []func(*flow.EpochSetup) // options to apply to the EpochSetup event 79 commitOpts []func(*flow.EpochCommit) // options to apply to the EpochCommit event 80 } 81 82 // NewEpochBuilder returns a new EpochBuilder which will build epochs using the 83 // given states. At least one state must be provided. If more than one are 84 // provided they must have the same initial state. 85 func NewEpochBuilder(t *testing.T, states ...protocol.FollowerState) *EpochBuilder { 86 require.True(t, len(states) >= 1, "must provide at least one state") 87 88 builder := &EpochBuilder{ 89 t: t, 90 states: states, 91 blocksByID: make(map[flow.Identifier]*flow.Block), 92 blocks: make([]*flow.Block, 0), 93 built: make(map[uint64]*EpochHeights), 94 } 95 return builder 96 } 97 98 // UsingSetupOpts sets options for the epoch setup event. For options 99 // targeting the same field, those added here will take precedence 100 // over defaults. 101 func (builder *EpochBuilder) UsingSetupOpts(opts ...func(*flow.EpochSetup)) *EpochBuilder { 102 builder.setupOpts = opts 103 return builder 104 } 105 106 // UsingCommitOpts sets options for the epoch setup event. For options 107 // targeting the same field, those added here will take precedence 108 // over defaults. 109 func (builder *EpochBuilder) UsingCommitOpts(opts ...func(*flow.EpochCommit)) *EpochBuilder { 110 builder.commitOpts = opts 111 return builder 112 } 113 114 // EpochHeights returns heights of each phase within about a built epoch. 115 func (builder *EpochBuilder) EpochHeights(counter uint64) (*EpochHeights, bool) { 116 epoch, ok := builder.built[counter] 117 return epoch, ok 118 } 119 120 // BuildEpoch builds and finalizes a sequence of blocks comprising a minimal full 121 // epoch (epoch N). We assume the latest finalized block is within staking phase 122 // in epoch N. 123 // 124 // | EPOCH N | 125 // | | 126 // P A B C D E F 127 // 128 // +------------+ +------------+ +-----------+ +-----------+ +----------+ +----------+ +----------+ 129 // | ER(P-1) |->| ER(P) |->| ER(A) |->| ER(B) |->| ER(C) |->| ER(D) |->| ER(E) | 130 // | S(ER(P-2)) | | S(ER(P-1)) | | S(ER(P)) | | S(ER(A)) | | S(ER(B)) | | S(ER(C)) | | S(ER(D)) | 131 // +------------+ +------------+ +-----------+ +-----------+ +----------+ +----------+ +----------+ 132 // | | 133 // Setup Commit 134 // 135 // ER(X) := ExecutionReceipt for block X 136 // S(ER(X)) := Seal for the ExecutionResult contained in ER(X) (seals block X) 137 // 138 // A is the latest finalized block. Every block contains a receipt for the 139 // previous block and a seal for the receipt contained in the previous block. 140 // The only exception is when A is the root block, in which case block B does 141 // not contain a receipt for block A, and block C does not contain a seal for 142 // block A. This is because the root block is sealed from genesis, and we 143 // can't insert duplicate seals. 144 // 145 // D contains a seal for block B containing the EpochSetup service event, 146 // processing D causes the EpochSetup to become activated. 147 // 148 // F contains a seal for block D containing the EpochCommit service event. 149 // processing F causes the EpochCommit to become activated. 150 // 151 // To build a sequence of epochs, we call BuildEpoch, then CompleteEpoch, and so on. 152 // 153 // Upon building an epoch N (preparing epoch N+1), we store some information 154 // about the heights of blocks in the BUILT epoch (epoch N). These can be 155 // queried with EpochHeights. 156 func (builder *EpochBuilder) BuildEpoch() *EpochBuilder { 157 158 state := builder.states[0] 159 160 // prepare default values for the service events based on the current state 161 identities, err := state.Final().Identities(filter.Any) 162 require.Nil(builder.t, err) 163 epoch := state.Final().Epochs().Current() 164 counter, err := epoch.Counter() 165 require.Nil(builder.t, err) 166 finalView, err := epoch.FinalView() 167 require.Nil(builder.t, err) 168 169 // retrieve block A 170 A, err := state.Final().Head() 171 require.Nil(builder.t, err) 172 173 // check that block A satisfies initial condition 174 phase, err := state.Final().Phase() 175 require.Nil(builder.t, err) 176 require.Equal(builder.t, flow.EpochPhaseStaking, phase) 177 178 // Define receipts and seals for block B payload. They will be nil if A is 179 // the root block 180 var receiptA *flow.ExecutionReceipt 181 var prevReceipts []*flow.ExecutionReceiptMeta 182 var prevResults []*flow.ExecutionResult 183 var sealsForPrev []*flow.Seal 184 185 aBlock, ok := builder.blocksByID[A.ID()] 186 if ok { 187 // A is not the root block. B will contain a receipt for A, and a seal 188 // for the receipt contained in A. 189 receiptA = ReceiptForBlockFixture(aBlock) 190 prevReceipts = []*flow.ExecutionReceiptMeta{ 191 receiptA.Meta(), 192 } 193 prevResults = []*flow.ExecutionResult{ 194 &receiptA.ExecutionResult, 195 } 196 resultByID := aBlock.Payload.Results.Lookup() 197 sealsForPrev = []*flow.Seal{ 198 Seal.Fixture(Seal.WithResult(resultByID[aBlock.Payload.Receipts[0].ResultID])), 199 } 200 } 201 202 // defaults for the EpochSetup event 203 setupDefaults := []func(*flow.EpochSetup){ 204 WithParticipants(identities), 205 SetupWithCounter(counter + 1), 206 WithFirstView(finalView + 1), 207 WithFinalView(finalView + 1_000_000), 208 } 209 setup := EpochSetupFixture(append(setupDefaults, builder.setupOpts...)...) 210 211 // build block B, sealing up to and including block A 212 B := BlockWithParentFixture(A) 213 B.SetPayload(flow.Payload{ 214 Receipts: prevReceipts, 215 Results: prevResults, 216 Seals: sealsForPrev, 217 }) 218 219 builder.addBlock(B) 220 221 // create a receipt for block B, to be included in block C 222 // the receipt for B contains the EpochSetup event 223 receiptB := ReceiptForBlockFixture(B) 224 receiptB.ExecutionResult.ServiceEvents = []flow.ServiceEvent{setup.ServiceEvent()} 225 226 // insert block C with a receipt for block B, and a seal for the receipt in 227 // block B if there was one 228 C := BlockWithParentFixture(B.Header) 229 var sealsForA []*flow.Seal 230 if receiptA != nil { 231 sealsForA = []*flow.Seal{ 232 Seal.Fixture(Seal.WithResult(&receiptA.ExecutionResult)), 233 } 234 } 235 C.SetPayload(flow.Payload{ 236 Receipts: []*flow.ExecutionReceiptMeta{receiptB.Meta()}, 237 Results: []*flow.ExecutionResult{&receiptB.ExecutionResult}, 238 Seals: sealsForA, 239 }) 240 builder.addBlock(C) 241 // create a receipt for block C, to be included in block D 242 receiptC := ReceiptForBlockFixture(C) 243 244 // build block D 245 // D contains a seal for block B and a receipt for block C 246 D := BlockWithParentFixture(C.Header) 247 sealForB := Seal.Fixture( 248 Seal.WithResult(&receiptB.ExecutionResult), 249 ) 250 D.SetPayload(flow.Payload{ 251 Receipts: []*flow.ExecutionReceiptMeta{receiptC.Meta()}, 252 Results: []*flow.ExecutionResult{&receiptC.ExecutionResult}, 253 Seals: []*flow.Seal{sealForB}, 254 }) 255 builder.addBlock(D) 256 257 // defaults for the EpochCommit event 258 commitDefaults := []func(*flow.EpochCommit){ 259 CommitWithCounter(counter + 1), 260 WithDKGFromParticipants(setup.Participants), 261 WithClusterQCsFromAssignments(setup.Assignments), 262 } 263 commit := EpochCommitFixture(append(commitDefaults, builder.commitOpts...)...) 264 265 // create receipt for block D, to be included in block E 266 // the receipt for block D contains the EpochCommit event 267 receiptD := ReceiptForBlockFixture(D) 268 receiptD.ExecutionResult.ServiceEvents = []flow.ServiceEvent{commit.ServiceEvent()} 269 270 // build block E 271 // E contains a seal for C and a receipt for D 272 E := BlockWithParentFixture(D.Header) 273 sealForC := Seal.Fixture( 274 Seal.WithResult(&receiptC.ExecutionResult), 275 ) 276 E.SetPayload(flow.Payload{ 277 Receipts: []*flow.ExecutionReceiptMeta{receiptD.Meta()}, 278 Results: []*flow.ExecutionResult{&receiptD.ExecutionResult}, 279 Seals: []*flow.Seal{sealForC}, 280 }) 281 builder.addBlock(E) 282 // create receipt for block E 283 receiptE := ReceiptForBlockFixture(E) 284 285 // build block F 286 // F contains a seal for block D and the EpochCommit event, as well as a 287 // receipt for block E 288 F := BlockWithParentFixture(E.Header) 289 sealForD := Seal.Fixture( 290 Seal.WithResult(&receiptD.ExecutionResult), 291 ) 292 F.SetPayload(flow.Payload{ 293 Receipts: []*flow.ExecutionReceiptMeta{receiptE.Meta()}, 294 Results: []*flow.ExecutionResult{&receiptE.ExecutionResult}, 295 Seals: []*flow.Seal{sealForD}, 296 }) 297 builder.addBlock(F) 298 299 // cache information about the built epoch 300 builder.built[counter] = &EpochHeights{ 301 Counter: counter, 302 Staking: A.Height, 303 Setup: D.Header.Height, 304 Committed: F.Header.Height, 305 CommittedFinal: F.Header.Height, 306 } 307 308 return builder 309 } 310 311 // CompleteEpoch caps off the current epoch by building the first block of the next 312 // epoch. We must be in the Committed phase to call CompleteEpoch. Once the epoch 313 // has been capped off, we can build the next epoch with BuildEpoch. 314 func (builder *EpochBuilder) CompleteEpoch() *EpochBuilder { 315 316 state := builder.states[0] 317 318 phase, err := state.Final().Phase() 319 require.Nil(builder.t, err) 320 require.Equal(builder.t, flow.EpochPhaseCommitted, phase) 321 finalView, err := state.Final().Epochs().Current().FinalView() 322 require.Nil(builder.t, err) 323 324 final, err := state.Final().Head() 325 require.Nil(builder.t, err) 326 327 finalBlock, ok := builder.blocksByID[final.ID()] 328 require.True(builder.t, ok) 329 330 // A is the first block of the next epoch (see diagram in BuildEpoch) 331 A := BlockWithParentFixture(final) 332 // first view is not necessarily exactly final view of previous epoch 333 A.Header.View = finalView + (rand.Uint64() % 4) + 1 334 finalReceipt := ReceiptForBlockFixture(finalBlock) 335 A.SetPayload(flow.Payload{ 336 Receipts: []*flow.ExecutionReceiptMeta{ 337 finalReceipt.Meta(), 338 }, 339 Results: []*flow.ExecutionResult{ 340 &finalReceipt.ExecutionResult, 341 }, 342 Seals: []*flow.Seal{ 343 Seal.Fixture( 344 Seal.WithResult(finalBlock.Payload.Results[0]), 345 ), 346 }, 347 }) 348 builder.addBlock(A) 349 350 return builder 351 } 352 353 // BuildBlocks builds empty blocks on top of the finalized state. It is used 354 // to build epochs that are not the minimum possible length, which is the 355 // default result from chaining BuildEpoch and CompleteEpoch. 356 func (builder *EpochBuilder) BuildBlocks(n uint) { 357 head, err := builder.states[0].Final().Head() 358 require.NoError(builder.t, err) 359 for i := uint(0); i < n; i++ { 360 next := BlockWithParentFixture(head) 361 builder.addBlock(next) 362 head = next.Header 363 } 364 } 365 366 // addBlock adds the given block to the state by: extending the state, 367 // finalizing the block, marking the block as valid, and caching the block. 368 func (builder *EpochBuilder) addBlock(block *flow.Block) { 369 blockID := block.ID() 370 for _, state := range builder.states { 371 err := state.ExtendCertified(context.Background(), block, CertifyBlock(block.Header)) 372 require.NoError(builder.t, err) 373 374 err = state.Finalize(context.Background(), blockID) 375 require.NoError(builder.t, err) 376 } 377 378 builder.blocksByID[block.ID()] = block 379 builder.blocks = append(builder.blocks, block) 380 } 381 382 // AddBlocksWithSeals for the n number of blocks specified this func 383 // will add a seal for the second highest block in the state and a 384 // receipt for the highest block in state to the given block before adding it to the state. 385 // NOTE: This func should only be used after BuildEpoch to extend the commit phase 386 func (builder *EpochBuilder) AddBlocksWithSeals(n int, counter uint64) *EpochBuilder { 387 for i := 0; i < n; i++ { 388 // Given the last 2 blocks in state A <- B when we add block C it will contain the following. 389 // - seal for A 390 // - execution result for B 391 b := builder.blocks[len(builder.blocks)-1] 392 393 receiptB := ReceiptForBlockFixture(b) 394 395 block := BlockWithParentFixture(b.Header) 396 seal := Seal.Fixture( 397 Seal.WithResult(b.Payload.Results[0]), 398 ) 399 400 payload := PayloadFixture( 401 WithReceipts(receiptB), 402 WithSeals(seal), 403 ) 404 block.SetPayload(payload) 405 406 builder.addBlock(block) 407 408 // update cache information about the built epoch 409 // we have extended the commit phase 410 builder.built[counter].CommittedFinal = block.Header.Height 411 } 412 413 return builder 414 }