Skip to content

Commit

Permalink
Merge pull request #6154 from onflow/jord/5730-hotstuff-committee
Browse files Browse the repository at this point in the history
Support Epoch Extensions in HotStuff Committee
  • Loading branch information
jordanschalm authored Jul 9, 2024
2 parents 9c3ed48 + 43754d1 commit edc301d
Show file tree
Hide file tree
Showing 13 changed files with 372 additions and 380 deletions.
362 changes: 163 additions & 199 deletions consensus/hotstuff/committees/consensus_committee.go

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)

// 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)
}
// 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) {

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

0 comments on commit edc301d

Please sign in to comment.