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