Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Epoch Extensions in HotStuff Committee #6154

Merged
merged 36 commits into from
Jul 9, 2024
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
426fa9a
sketch new test cases
jordanschalm Jun 24, 2024
59ed5fa
Merge branch 'feature/efm-recovery' into jord/5730-hotstuff-committee
jordanschalm Jun 25, 2024
c605b49
sketch changes
jordanschalm Jun 25, 2024
0eff8a1
sketch: minor updates to leader selection
jordanschalm Jun 25, 2024
33e199a
FinalView modifications to support epoch extensions
jordanschalm Jun 26, 2024
f2f9192
Merge branch 'feature/efm-recovery' into jord/5730-hotstuff-committee
jordanschalm Jun 26, 2024
0b1efe2
add epoch extensions to setupEpoch etc
jordanschalm Jun 26, 2024
52dee4b
add logic for processing events
jordanschalm Jun 26, 2024
ef21d09
remove unneeded fallback fields, funcs
jordanschalm Jun 26, 2024
0c83d60
simplify epochInfo struct
jordanschalm Jun 26, 2024
11c9fd6
remove static terminology
jordanschalm Jun 26, 2024
0fe06d0
update documentation, remove unneeded fields, etc
jordanschalm Jun 26, 2024
64619f3
update tests
jordanschalm Jun 27, 2024
4b26f72
doc tweaks, add sanity check
jordanschalm Jun 27, 2024
80f569b
update FinalView docs
jordanschalm Jun 27, 2024
7dd487d
fix constructor change in test
jordanschalm Jun 27, 2024
2ac1c0e
check FinalView in epoch extension test
jordanschalm Jun 27, 2024
66c3b84
correct mock expectations in test
jordanschalm Jun 27, 2024
465d66f
remove unused functions
jordanschalm Jun 27, 2024
d724014
add test case for multiple extensions
jordanschalm Jun 27, 2024
c456b82
Apply suggestions from code review
jordanschalm Jul 4, 2024
430a00c
address review feedback
jordanschalm Jul 4, 2024
05498d0
Apply suggestions from code review
jordanschalm Jul 8, 2024
b400f38
fail fast on final view sanity check
jordanschalm Jul 8, 2024
80339f8
rename events channel
jordanschalm Jul 8, 2024
4fbd98b
Apply suggestions from code review
jordanschalm Jul 8, 2024
d4d743a
wrap InvalidViewError as irrecoverable
jordanschalm Jul 8, 2024
3e1b46f
remove todo
jordanschalm Jul 8, 2024
2b0d7e2
avoid shadowing epochInfo
jordanschalm Jul 8, 2024
486c076
only pass extension to recompute leader selection
jordanschalm Jul 8, 2024
5d59791
tmp: quick+dirty make tests pass
jordanschalm Jul 8, 2024
4c47895
lint
jordanschalm Jul 8, 2024
62c5d69
use cache if possible for AtHeight snapshot queries
jordanschalm Jul 8, 2024
66f6e7f
clean up tests
jordanschalm Jul 8, 2024
cd6e188
check broader range of views in committee tests
jordanschalm Jul 8, 2024
43754d1
rename
jordanschalm Jul 8, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
304 changes: 121 additions & 183 deletions consensus/hotstuff/committees/consensus_committee.go
jordanschalm marked this conversation as resolved.
Show resolved Hide resolved

Large diffs are not rendered by default.

175 changes: 104 additions & 71 deletions consensus/hotstuff/committees/consensus_committee_test.go

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions consensus/hotstuff/committees/leader/leader_selection.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,12 +167,12 @@ func weightedRandomSelection(
return leaders, nil
}

// binarySearchStriclyBigger finds the index of the first item in the given array that is
// binarySearchStrictlyBigger finds the index of the first item in the given array that is
// strictly bigger to the given value.
// There are a few assumptions on inputs:
// - `arr` must be non-empty
// - items in `arr` must be in non-decreasing order
// - `value` must be less than the last item in `arr`
// - `arr` must be non-empty
// - items in `arr` must be in non-decreasing order
jordanschalm marked this conversation as resolved.
Show resolved Hide resolved
// - `value` must be less than the last item in `arr`
func binarySearchStrictlyBigger(value uint64, arr []uint64) int {
left := 0
arrayLen := len(arr)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,7 @@ func TestLeaderSelectionAreWeighted(t *testing.T) {

func BenchmarkLeaderSelection(b *testing.B) {

const N_VIEWS = 15000000
const N_VIEWS = EstimatedSixMonthOfViews
const N_NODES = 20

identities := make(flow.IdentityList, 0, N_NODES)
Expand Down
5 changes: 5 additions & 0 deletions state/protocol/badger/mutator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2348,6 +2348,11 @@ func TestRecoveryFromEpochFallbackMode(t *testing.T) {
err = state.Finalize(context.Background(), block9.ID())
require.NoError(t, err)

jordanschalm marked this conversation as resolved.
Show resolved Hide resolved
// After epoch extension, FinalView must be updated accordingly
finalView, err := state.Final().Epochs().Current().FinalView()
require.NoError(t, err)
assert.Equal(t, epochExtensions[0].FinalView, finalView)

// Constructing blocks
// ... <- B10 <- B11(ER(B4, EpochRecover)) <- B12(S(ER(B4))) <- ...
// B10 will be the first block past the epoch extension. Block B11 incorporates the Execution Result [ER]
Expand Down
17 changes: 9 additions & 8 deletions state/protocol/badger/snapshot.go
Original file line number Diff line number Diff line change
Expand Up @@ -397,9 +397,9 @@ func (q *EpochQuery) Current() protocol.Epoch {
return invalid.NewEpochf("could not get current epoch height bounds: %s", err.Error())
}
if isFirstHeightKnown {
return inmem.NewEpochWithStartBoundary(setup, commit, firstHeight)
return inmem.NewEpochWithStartBoundary(setup, epochState.EpochExtensions(), commit, firstHeight)
}
return inmem.NewCommittedEpoch(setup, commit)
return inmem.NewCommittedEpoch(setup, epochState.EpochExtensions(), commit)
}

// Next returns the next epoch, if it is available.
Expand All @@ -419,12 +419,12 @@ func (q *EpochQuery) Next() protocol.Epoch {
// if we are in setup phase, return a SetupEpoch
nextSetup := entry.NextEpochSetup
if phase == flow.EpochPhaseSetup {
return inmem.NewSetupEpoch(nextSetup)
return inmem.NewSetupEpoch(nextSetup, entry.NextEpoch.EpochExtensions)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would suggest adding TODO to indicate that epochs will only be exposed once they are committed.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}
// if we are in committed phase, return a CommittedEpoch
nextCommit := entry.NextEpochCommit
if phase == flow.EpochPhaseCommitted {
return inmem.NewCommittedEpoch(nextSetup, nextCommit)
return inmem.NewCommittedEpoch(nextSetup, entry.NextEpoch.EpochExtensions, nextCommit)
}
return invalid.NewEpochf("data corruption: unknown epoch phase implies malformed protocol state epoch data")
}
Expand All @@ -450,29 +450,30 @@ func (q *EpochQuery) Previous() protocol.Epoch {
// for the previous epoch
setup := entry.PreviousEpochSetup
commit := entry.PreviousEpochCommit
extensions := entry.PreviousEpoch.EpochExtensions

firstHeight, finalHeight, firstHeightKnown, finalHeightKnown, err := q.retrieveEpochHeightBounds(setup.Counter)
if err != nil {
return invalid.NewEpochf("could not get epoch height bounds: %w", err)
}
if firstHeightKnown && finalHeightKnown {
// typical case - we usually know both boundaries for a past epoch
return inmem.NewEpochWithStartAndEndBoundaries(setup, commit, firstHeight, finalHeight)
return inmem.NewEpochWithStartAndEndBoundaries(setup, extensions, commit, firstHeight, finalHeight)
}
if firstHeightKnown && !finalHeightKnown {
// this case is possible when the snapshot reference block is un-finalized
// and is past an un-finalized epoch boundary
return inmem.NewEpochWithStartBoundary(setup, commit, firstHeight)
return inmem.NewEpochWithStartBoundary(setup, extensions, commit, firstHeight)
}
if !firstHeightKnown && finalHeightKnown {
// this case is possible when this node's lowest known block is after
// the queried epoch's start boundary
return inmem.NewEpochWithEndBoundary(setup, commit, finalHeight)
return inmem.NewEpochWithEndBoundary(setup, extensions, commit, finalHeight)
}
if !firstHeightKnown && !finalHeightKnown {
// this case is possible when this node's lowest known block is after
// the queried epoch's end boundary
return inmem.NewCommittedEpoch(setup, commit)
return inmem.NewCommittedEpoch(setup, extensions, commit)
}
return invalid.NewEpochf("sanity check failed: impossible combination of boundaries for previous epoch")
}
Expand Down
27 changes: 0 additions & 27 deletions state/protocol/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,33 +156,6 @@ func GetDKGParticipantKeys(dkg DKG, participants flow.IdentitySkeletonList) ([]c
return keys, nil
}

// ToDKGParticipantLookup computes the nodeID -> DKGParticipant lookup for a
// DKG instance. The participants must exactly match the DKG instance configuration.
// All errors indicate inconsistent or invalid inputs.
// No errors are expected during normal operation.
func ToDKGParticipantLookup(dkg DKG, participants flow.IdentitySkeletonList) (map[flow.Identifier]flow.DKGParticipant, error) {
jordanschalm marked this conversation as resolved.
Show resolved Hide resolved

lookup := make(map[flow.Identifier]flow.DKGParticipant)
for _, identity := range participants {

index, err := dkg.Index(identity.NodeID)
if err != nil {
return nil, fmt.Errorf("could not get index (node=%x): %w", identity.NodeID, err)
}
key, err := dkg.KeyShare(identity.NodeID)
if err != nil {
return nil, fmt.Errorf("could not get key share (node=%x): %w", identity.NodeID, err)
}

lookup[identity.NodeID] = flow.DKGParticipant{
Index: index,
KeyShare: key,
}
}

return lookup, nil
}

// DKGPhaseViews returns the DKG final phase views for an epoch.
// Error returns:
// * protocol.ErrNoPreviousEpoch - if the epoch represents a previous epoch which does not exist.
Expand Down
4 changes: 2 additions & 2 deletions state/protocol/convert_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (

func TestToEpochSetup(t *testing.T) {
expected := unittest.EpochSetupFixture()
epoch := inmem.NewSetupEpoch(expected)
epoch := inmem.NewSetupEpoch(expected, nil)

got, err := protocol.ToEpochSetup(epoch)
require.NoError(t, err)
Expand All @@ -26,7 +26,7 @@ func TestToEpochCommit(t *testing.T) {
unittest.CommitWithCounter(setup.Counter),
unittest.WithDKGFromParticipants(setup.Participants),
unittest.WithClusterQCsFromAssignments(setup.Assignments))
epoch := inmem.NewCommittedEpoch(setup, expected)
epoch := inmem.NewCommittedEpoch(setup, nil, expected)

got, err := protocol.ToEpochCommit(epoch)
require.NoError(t, err)
Expand Down
6 changes: 6 additions & 0 deletions state/protocol/epoch.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,12 @@ type Epoch interface {
DKGPhase3FinalView() (uint64, error)

// FinalView returns the largest view number which still belongs to this epoch.
// The largest view number is the greatest of:
// - the FinalView field of the flow.EpochSetup event for this epoch
// - the FinalView field of the most recent flow.EpochExtension for this epoch
// If EFM is not triggered during this epoch, this value will be static.
// If EFM is triggered during this epoch, this value may increase with increasing
// reference block heights, as new epoch extensions are included.
// Error returns:
// * protocol.ErrNoPreviousEpoch - if the epoch represents a previous epoch which does not exist.
// * protocol.ErrNextEpochNotSetup - if the epoch represents a next epoch which has not been set up.
Expand Down
31 changes: 0 additions & 31 deletions state/protocol/inmem/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,37 +63,6 @@ func FromParams(from protocol.GlobalParams) (*Params, error) {
return &Params{params}, nil
}

// FromCluster converts any protocol.Cluster to a memory-backed Cluster.
// No errors are expected during normal operation.
func FromCluster(from protocol.Cluster) (*Cluster, error) {
cluster := EncodableCluster{
Counter: from.EpochCounter(),
Index: from.Index(),
Members: from.Members(),
RootBlock: from.RootBlock(),
RootQC: from.RootQC(),
}
return &Cluster{cluster}, nil
}

// FromDKG converts any protocol.DKG to a memory-backed DKG.
//
// The given participant list must exactly match the DKG members.
// All errors indicate inconsistent or invalid inputs.
// No errors are expected during normal operation.
func FromDKG(from protocol.DKG, participants flow.IdentitySkeletonList) (*DKG, error) {
var dkg EncodableDKG
dkg.GroupKey = encodable.RandomBeaconPubKey{PublicKey: from.GroupKey()}

lookup, err := protocol.ToDKGParticipantLookup(from, participants)
if err != nil {
return nil, fmt.Errorf("could not generate dkg participant lookup: %w", err)
}
dkg.Participants = lookup

return &DKG{dkg}, nil
}

// DKGFromEncodable returns a DKG backed by the given encodable representation.
func DKGFromEncodable(enc EncodableDKG) (*DKG, error) {
return &DKG{enc}, nil
Expand Down
40 changes: 26 additions & 14 deletions state/protocol/inmem/epoch.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,30 +23,32 @@ func (eq Epochs) Previous() protocol.Epoch {
if eq.entry.PreviousEpoch == nil {
return invalid.NewEpoch(protocol.ErrNoPreviousEpoch)
}
return NewCommittedEpoch(eq.entry.PreviousEpochSetup, eq.entry.PreviousEpochCommit)
return NewCommittedEpoch(eq.entry.PreviousEpochSetup, eq.entry.PreviousEpoch.EpochExtensions, eq.entry.PreviousEpochCommit)
}

func (eq Epochs) Current() protocol.Epoch {
return NewCommittedEpoch(eq.entry.CurrentEpochSetup, eq.entry.CurrentEpochCommit)
return NewCommittedEpoch(eq.entry.CurrentEpochSetup, eq.entry.CurrentEpoch.EpochExtensions, eq.entry.CurrentEpochCommit)
}

func (eq Epochs) Next() protocol.Epoch {
switch eq.entry.EpochPhase() {
case flow.EpochPhaseStaking, flow.EpochPhaseFallback:
return invalid.NewEpoch(protocol.ErrNextEpochNotSetup)
case flow.EpochPhaseSetup:
return NewSetupEpoch(eq.entry.NextEpochSetup)
return NewSetupEpoch(eq.entry.NextEpochSetup, eq.entry.NextEpoch.EpochExtensions)
Copy link
Member

@AlexHentschel AlexHentschel Jul 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my opinion, this should yield an error ErrNextEpochNotCommitted

return invalid.NewEpoch(protocol.ErrNextEpochNotCommitted)

Though, this would require an update of the EpochQuery API ... so maybe add a todo to the issue is mentioned in my previous comment.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

case flow.EpochPhaseCommitted:
return NewCommittedEpoch(eq.entry.NextEpochSetup, eq.entry.NextEpochCommit)
return NewCommittedEpoch(eq.entry.NextEpochSetup, eq.entry.NextEpoch.EpochExtensions, eq.entry.NextEpochCommit)
}
return invalid.NewEpochf("unexpected unknown phase in protocol state entry")
}

// setupEpoch is an implementation of protocol.Epoch backed by an EpochSetup
// service event. This is used for converting service events to inmem.Epoch.
// setupEpoch is an implementation of protocol.Epoch backed by an EpochSetup service event.
// Includes any extensions which have been included as of the reference block.
// This is used for converting service events to inmem.Epoch.
type setupEpoch struct {
// EpochSetup service event
setupEvent *flow.EpochSetup
extensions []flow.EpochExtension
}
Comment on lines +45 to 52
Copy link
Member

@AlexHentschel AlexHentschel Jul 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ I find this a problematic structure, because it is inconsistent with the protocol definition: only committed epochs can have extensions. Previously (without extensions), our model was quite a bit simpler. But now with extension driving up the mental complexity I think it becomes very important to have a clear consistent implementation.

In my opinion, setupEpoch simply cannot be a valid representation of protocol.Epoch as it misses a bunch of the important information. We have the convention that an epoch essentially only starts to exist after the EpochCommit Service event has been finalized. I think we had the plan to remove any exposure of uncommitted epochs from the major APIs. Though, NewSetupEpoch is still used and returned in some of the broadly-used interfaces.

  • For the scope of this PR, I would not include extensions in the setupEpoch because it further increases the inconsistency of protocol definition and implementation. Instead, I would include the extensions into committedEpoch and override the FinalView() method inherited from setupEpoch.
  • Not sure if we have already an issue for removing setupEpoch as a return value from the EpochQuery API and only return epochs once they are committed. Is such issue already exists, maybe add a reference to this comment. If not, would you mind creating such issue please? 🙏 🙇

Copy link
Member Author

@jordanschalm jordanschalm Jul 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

only committed epochs can have extensions

👍 Agree, will move the extensions to committedEpoch.

We have the convention that an epoch essentially only starts to exist after the EpochCommit Service event has been finalized

Epoch APIs cannot entirely avoid exposing data for un-committed epochs, because the node software requires access to a subset of un-committed epoch data in order to commit that epoch, by completing the DKG and generating root cluster QCs.

There are other ways we can improve the epoch APIs, but we will need something similar to the progressive disclosure of setupEpoch and committedEpoch. For example, we could hide some info from the EpochSetup event until the epoch is committed (like the view range), and we could use different interface types to represent the two states.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added this tech debt issue as a place to capture these ideas for improving the API: #6191


func (es *setupEpoch) Counter() (uint64, error) {
Expand All @@ -69,7 +71,13 @@ func (es *setupEpoch) DKGPhase3FinalView() (uint64, error) {
return es.setupEvent.DKGPhase3FinalView, nil
}

// FinalView returns the final view of the epoch, taking into account possible epoch extensions.
// If there are no epoch extensions, the final view is the final view of the current epoch setup,
// otherwise it is the final view of the last epoch extension.
func (es *setupEpoch) FinalView() (uint64, error) {
if len(es.extensions) > 0 {
return es.extensions[len(es.extensions)-1].FinalView, nil
}
return es.setupEvent.FinalView, nil
}

Expand Down Expand Up @@ -235,23 +243,24 @@ func (e *heightBoundedEpoch) FinalHeight() (uint64, error) {
return 0, protocol.ErrUnknownEpochBoundary
}

// NewSetupEpoch returns a memory-backed epoch implementation based on an
// EpochSetup event. Epoch information available after the setup phase will
// not be accessible in the resulting epoch instance.
// NewSetupEpoch returns a memory-backed epoch implementation based on an EpochSetup event.
// Epoch information available after the setup phase will not be accessible in the resulting epoch instance.
// No errors are expected during normal operations.
func NewSetupEpoch(setupEvent *flow.EpochSetup) protocol.Epoch {
func NewSetupEpoch(setupEvent *flow.EpochSetup, extensions []flow.EpochExtension) protocol.Epoch {
Comment on lines 248 to +249
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my opinion, this method should be deprecated for the scope of this Pr, including a TODO to remove it entirely

Suggested change
// No errors are expected during normal operations.
func NewSetupEpoch(setupEvent *flow.EpochSetup) protocol.Epoch {
func NewSetupEpoch(setupEvent *flow.EpochSetup, extensions []flow.EpochExtension) protocol.Epoch {
//
// DEPRECATED and TO BE REMOVED, because setupEpoch simply cannot be a valid representation of protocol.Epoch
// as it misses a bunch of the important information. We have updated our convention such that an epoch
// essentially only starts to exist after the EpochCommit Service event has been finalized. The plan is to
// remove any exposure of uncommitted epochs from the `EpochQuery` API.
//
// No errors are expected during normal operations.
func NewSetupEpoch(setupEvent *flow.EpochSetup) protocol.Epoch {

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return &setupEpoch{
setupEvent: setupEvent,
extensions: extensions,
}
}

// NewCommittedEpoch returns a memory-backed epoch implementation based on an
// EpochSetup and EpochCommit events.
// No errors are expected during normal operations.
func NewCommittedEpoch(setupEvent *flow.EpochSetup, commitEvent *flow.EpochCommit) protocol.Epoch {
func NewCommittedEpoch(setupEvent *flow.EpochSetup, extensions []flow.EpochExtension, commitEvent *flow.EpochCommit) protocol.Epoch {
return &committedEpoch{
setupEpoch: setupEpoch{
setupEvent: setupEvent,
extensions: extensions,
},
commitEvent: commitEvent,
}
Expand All @@ -260,11 +269,12 @@ func NewCommittedEpoch(setupEvent *flow.EpochSetup, commitEvent *flow.EpochCommi
// NewEpochWithStartBoundary returns a memory-backed epoch implementation based on an
// EpochSetup and EpochCommit events, and the epoch's first block height (start boundary).
// No errors are expected during normal operations.
func NewEpochWithStartBoundary(setupEvent *flow.EpochSetup, commitEvent *flow.EpochCommit, firstHeight uint64) protocol.Epoch {
func NewEpochWithStartBoundary(setupEvent *flow.EpochSetup, extensions []flow.EpochExtension, commitEvent *flow.EpochCommit, firstHeight uint64) protocol.Epoch {
return &heightBoundedEpoch{
committedEpoch: committedEpoch{
setupEpoch: setupEpoch{
setupEvent: setupEvent,
extensions: extensions,
},
commitEvent: commitEvent,
},
Expand All @@ -276,11 +286,12 @@ func NewEpochWithStartBoundary(setupEvent *flow.EpochSetup, commitEvent *flow.Ep
// NewEpochWithEndBoundary returns a memory-backed epoch implementation based on an
// EpochSetup and EpochCommit events, and the epoch's final block height (end boundary).
// No errors are expected during normal operations.
func NewEpochWithEndBoundary(setupEvent *flow.EpochSetup, commitEvent *flow.EpochCommit, finalHeight uint64) protocol.Epoch {
func NewEpochWithEndBoundary(setupEvent *flow.EpochSetup, extensions []flow.EpochExtension, commitEvent *flow.EpochCommit, finalHeight uint64) protocol.Epoch {
return &heightBoundedEpoch{
committedEpoch: committedEpoch{
setupEpoch: setupEpoch{
setupEvent: setupEvent,
extensions: extensions,
},
commitEvent: commitEvent,
},
Expand All @@ -292,11 +303,12 @@ func NewEpochWithEndBoundary(setupEvent *flow.EpochSetup, commitEvent *flow.Epoc
// NewEpochWithStartAndEndBoundaries returns a memory-backed epoch implementation based on an
// EpochSetup and EpochCommit events, and the epoch's first and final block heights (start+end boundaries).
// No errors are expected during normal operations.
func NewEpochWithStartAndEndBoundaries(setupEvent *flow.EpochSetup, commitEvent *flow.EpochCommit, firstHeight, finalHeight uint64) protocol.Epoch {
func NewEpochWithStartAndEndBoundaries(setupEvent *flow.EpochSetup, extensions []flow.EpochExtension, commitEvent *flow.EpochCommit, firstHeight, finalHeight uint64) protocol.Epoch {
return &heightBoundedEpoch{
committedEpoch: committedEpoch{
setupEpoch: setupEpoch{
setupEvent: setupEvent,
extensions: extensions,
},
commitEvent: commitEvent,
},
Expand Down
Loading