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