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 all 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
362 changes: 163 additions & 199 deletions consensus/hotstuff/committees/consensus_committee.go
jordanschalm marked this conversation as resolved.
Show resolved Hide resolved

Large diffs are not rendered by default.

209 changes: 133 additions & 76 deletions consensus/hotstuff/committees/consensus_committee_test.go

Large diffs are not rendered by default.

35 changes: 22 additions & 13 deletions consensus/hotstuff/committees/leader/consensus.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,24 @@ package leader
import (
"fmt"

"github.com/onflow/flow-go/model/flow"
"github.com/onflow/flow-go/model/flow/filter"
"github.com/onflow/flow-go/state/protocol"
"github.com/onflow/flow-go/state/protocol/prg"
)

// SelectionForConsensus pre-computes and returns leaders for the consensus committee
// in the given epoch. The consensus committee spans multiple epochs and the leader
// selection returned here is only valid for the input epoch, so it is necessary to
// call this for each upcoming epoch.
func SelectionForConsensus(epoch protocol.Epoch) (*LeaderSelection, error) {
// SelectionForConsensusFromEpoch is a ...
func SelectionForConsensusFromEpoch(epoch protocol.Epoch) (*LeaderSelection, error) {

// pre-compute leader selection for the epoch
identities, err := epoch.InitialIdentities()
if err != nil {
return nil, fmt.Errorf("could not get epoch initial identities: %w", err)
}

// get the epoch source of randomness
randomSeed, err := epoch.RandomSource()
if err != nil {
return nil, fmt.Errorf("could not get epoch seed: %w", err)
}
// create random number generator from the seed and customizer
rng, err := prg.New(randomSeed, prg.ConsensusLeaderSelection, nil)
if err != nil {
return nil, fmt.Errorf("could not create rng: %w", err)
}
firstView, err := epoch.FirstView()
if err != nil {
return nil, fmt.Errorf("could not get epoch first view: %w", err)
Expand All @@ -39,11 +30,29 @@ func SelectionForConsensus(epoch protocol.Epoch) (*LeaderSelection, error) {
return nil, fmt.Errorf("could not get epoch final view: %w", err)
}

leaders, err := SelectionForConsensus(
identities,
randomSeed,
firstView,
finalView,
)
return leaders, err
}

// SelectionForConsensus pre-computes and returns leaders for the consensus committee
// in the given epoch. The consensus committee spans multiple epochs and the leader
// selection returned here is only valid for the input epoch, so it is necessary to
// call this for each upcoming epoch.
func SelectionForConsensus(initialIdentities flow.IdentitySkeletonList, randomSeed []byte, firstView, finalView uint64) (*LeaderSelection, error) {
rng, err := prg.New(randomSeed, prg.ConsensusLeaderSelection, nil)
if err != nil {
return nil, fmt.Errorf("could not create rng: %w", err)
}
leaders, err := ComputeLeaderSelection(
firstView,
rng,
int(finalView-firstView+1), // add 1 because both first/final view are inclusive
identities.Filter(filter.IsConsensusCommitteeMember),
initialIdentities.Filter(filter.IsConsensusCommitteeMember),
)
return leaders, err
}
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 monotonically increasing order (for indices i,j with i<j it must hold that arr[i] ≤ arr[j])
// - `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
7 changes: 5 additions & 2 deletions state/protocol/badger/mutator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2335,8 +2335,6 @@ func TestRecoveryFromEpochFallbackMode(t *testing.T) {
err = state.Extend(context.Background(), block9)
require.NoError(t, err)

// TODO: check EpochExtension notification using pub/sub mechanism when it is implemented.

epochProtocolState, err := state.AtBlockID(block9.ID()).EpochProtocolState()
require.NoError(t, err)
epochExtensions := epochProtocolState.Entry().CurrentEpoch.EpochExtensions
Expand All @@ -2348,6 +2346,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
4 changes: 1 addition & 3 deletions state/protocol/badger/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -756,9 +756,7 @@ func (state *State) Final() protocol.Snapshot {
// -> if the given height is below the root height
// - exception for critical unexpected storage errors
func (state *State) AtHeight(height uint64) protocol.Snapshot {
// retrieve the block ID for the finalized height
var blockID flow.Identifier
err := state.db.View(operation.LookupBlockHeight(height, &blockID))
blockID, err := state.headers.BlockIDByHeight(height)
if err != nil {
if errors.Is(err, storage.ErrNotFound) {
return invalid.NewSnapshotf("unknown finalized height %d: %w", height, statepkg.ErrUnknownSnapshotReference)
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
Loading
Loading