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  }