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  }