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

[EFM] Update protocol.DKG to use IndexMap #6338

Merged
merged 50 commits into from
Sep 13, 2024
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
98477f4
Updated inmem.DKG to implement it's logic using flow.EpochCommit. Rem…
durkmurder Aug 13, 2024
6c04ecf
Extended validation to the epoch commit fields. Updated fixtures and …
durkmurder Aug 13, 2024
b9c4970
Extended DKG interface to return NodeID by index. Updated convert fun…
durkmurder Aug 13, 2024
3613ffe
Fixed assertions in rapid test
durkmurder Aug 14, 2024
490a6d2
Fixed broken vote collector tests
durkmurder Aug 14, 2024
5fecb6b
Fixed consensus integration tests to correctly construct EpochCommit …
durkmurder Aug 14, 2024
67eaae3
Updated mocks. Pretify test
durkmurder Aug 14, 2024
58d03c8
Fixed serialization of EpochCommit
durkmurder Aug 14, 2024
7108bd0
Linted
durkmurder Aug 14, 2024
269b3c8
Reverted changes to the fuzzy test. Added test data
durkmurder Aug 15, 2024
0adebff
Removed unneeded converting code
durkmurder Aug 15, 2024
9cd363c
Updated godoc
durkmurder Aug 15, 2024
4292b2b
Merge branch 'feature/efm-recovery' into yurii/6321-update-DKG-implem…
durkmurder Aug 15, 2024
b98fb7c
extended documentation for DKG
Sep 2, 2024
71da7cf
adding TODO and panic wrt possibly incorrect usage of `ParticipantDat…
Sep 2, 2024
ead242d
Merge pull request #6428 from onflow/alex/6321-update-DKG-implementor…
durkmurder Sep 2, 2024
0eac564
Merge branch 'feature/efm-recovery' into yurii/6321-update-DKG-implem…
durkmurder Sep 2, 2024
1a0c5be
Updated methods of PariticipantData. Updated usages
durkmurder Sep 2, 2024
8321167
Updated usages of DKG index map
durkmurder Sep 2, 2024
0dd7995
Apply suggestions from code review
durkmurder Sep 2, 2024
86a6bb1
Simplified implementation of inmem.DKG
durkmurder Sep 2, 2024
23fb431
Apply suggestions from PR review
durkmurder Sep 2, 2024
58df507
Update state/protocol/inmem/dkg.go
durkmurder Sep 3, 2024
68e2979
Update consensus/hotstuff/committees/static.go
durkmurder Sep 3, 2024
ab66a71
Fixed broken tests
durkmurder Sep 3, 2024
137a36c
Fixed broken tests
durkmurder Sep 3, 2024
23daff9
Apply suggestions from code review
durkmurder Sep 3, 2024
e14c993
Extended validity checks for EpochCommit. Updated tests
durkmurder Sep 3, 2024
3ed8f4c
Linted
durkmurder Sep 3, 2024
29ce699
Merge branch 'yurii/6321-update-DKG-implementors' of https://github.c…
durkmurder Sep 3, 2024
8226c1e
Added DKG index map for integration tests
durkmurder Sep 4, 2024
7298d52
Removed extra checks for consistensy of epoch commit
durkmurder Sep 4, 2024
c5e0f0a
extended documentation where generalized protocol logic meets bootstr…
Sep 9, 2024
0e6cd5d
used `flow.DKGIndexMap` in different places, as the struct definition…
Sep 10, 2024
abf93fa
Merge pull request #6452 from onflow/alex/6321-bootstrapping-doc
durkmurder Sep 10, 2024
6f09828
Update state/protocol/validity_test.go
durkmurder Sep 10, 2024
2789404
Update state/protocol/inmem/dkg.go
durkmurder Sep 10, 2024
92c2efb
Update state/protocol/validity.go
durkmurder Sep 10, 2024
756d15b
Linted
durkmurder Sep 10, 2024
ec92c60
Extended EpochCommit checks to account for insufficient participants …
durkmurder Sep 10, 2024
f639b4d
Linted
durkmurder Sep 10, 2024
49a76dd
Update cmd/bootstrap/run/qc.go
durkmurder Sep 11, 2024
0a45ee0
Update cmd/bootstrap/run/qc.go
durkmurder Sep 11, 2024
73b1d60
Update cmd/bootstrap/run/qc.go
durkmurder Sep 11, 2024
221abb7
Update cmd/bootstrap/run/qc.go
durkmurder Sep 11, 2024
3981e6c
Introduced a random beacon safety threshold
durkmurder Sep 13, 2024
790b016
Added godoc which explains RandomBeaconSafetyThreshold
durkmurder Sep 13, 2024
3626853
Update model/flow/dkg.go
durkmurder Sep 13, 2024
dadeae3
Temporary disabled integration tests that are related to the DKG
durkmurder Sep 13, 2024
0ca2400
Merge branch 'yurii/6321-update-DKG-implementors' of https://github.c…
durkmurder Sep 13, 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
55 changes: 40 additions & 15 deletions cmd/bootstrap/run/qc.go
AlexHentschel marked this conversation as resolved.
Show resolved Hide resolved
AlexHentschel marked this conversation as resolved.
Show resolved Hide resolved
AlexHentschel marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -24,21 +24,26 @@ type Participant struct {
RandomBeaconPrivKey crypto.PrivateKey
}

// ParticipantData represents a subset of all consensus participants that contributing to some signing process (at the moment, we only use
// it for the contributors for the root QC). For mainnet, this a *strict subset* of all consensus participants:
// - In an early step during the bootstrapping process, every node operator locally generates votes for the root block from the nodes they
// operate. During the vote-generation step, (see function `constructRootVotes`), `Participants` represents only the operator's own
// nodes (generally a small minority of the entire consensus committee).
// - During a subsequent step of the bootstrapping process, the Flow Foundation collects a supermajority of votes from the consensus
// participants and constructs the root QC (see function `constructRootQC`). Here, `Participants` is only populated with
// the information of the internal does that the Flow Foundation runs, but not used.
durkmurder marked this conversation as resolved.
Show resolved Hide resolved
//
// Furthermore, ParticipantData contains auxiliary data about the DKG to set up the random beacon. Note that the consensus committee 𝒞 and the
// DKG committee 𝒟 are generally _not_ identical. We explicitly want to support that _either_ set can have nodes that are not in
// the other set (formally 𝒟 \ 𝒞 ≠ ∅ and 𝒞 \ 𝒟 ≠ ∅). ParticipantData has no direct representation of the consensus committee 𝒞.
type ParticipantData struct {
// Participants of the signing process: only members of the consensus committee can vote (formally Participants ⊊ 𝒞).
// However, we allow for consensus committee members that are _not_ part of the random beacon committee 𝒟 (formally
// ∅ ≠ Participants \ 𝒟).
Participants []Participant
Lookup map[flow.Identifier]flow.DKGParticipant
GroupKey crypto.PublicKey
}

// PublicBeaconKeys returns the nodes' individual public random-beacon keys (excluding
// the group public key). The keys are returned in the same order as the nodes appear
// in the Participants list, which must be the DKG index order.
func (pd *ParticipantData) PublicBeaconKeys() []crypto.PublicKey {
keys := make([]crypto.PublicKey, len(pd.Participants))
for i, participant := range pd.Participants {
keys[i] = participant.RandomBeaconPrivKey.PublicKey()
}
return keys
DKGCommittee map[flow.Identifier]flow.DKGParticipant // DKG committee 𝒟 (to set up the random beacon)
DKGGroupKey crypto.PublicKey // group key for the DKG committee 𝒟
}

func (pd *ParticipantData) Identities() flow.IdentityList {
Expand All @@ -49,6 +54,26 @@ func (pd *ParticipantData) Identities() flow.IdentityList {
return bootstrap.ToIdentityList(nodes)
}

// DKGData returns a map from node ID to DKG index and a list of public keys, one per DKG participant, ordered by Random Beacon index.
// Can be used to reconstruct EpochCommit.
// This implementation guarantees the following protocol-mandated invariants (or returns an
// exception for non-compliant `ParticipantData`):
// - len(DKGParticipantKeys) == len(DKGData)
// - DKGData values form the set {0, 1, ..., n-1} where n=len(DKGParticipantKeys)
//
// CAUTION: This mapping may include identifiers for nodes which do not exist in the consensus committee
//
// and may NOT include identifiers for all nodes in the consensus committee.
func (pd *ParticipantData) DKGData() (flow.DKGIndexMap, []crypto.PublicKey) {
indexMap := make(flow.DKGIndexMap, len(pd.DKGCommittee))
keys := make([]crypto.PublicKey, len(pd.DKGCommittee))
for nodeID, participant := range pd.DKGCommittee {
indexMap[nodeID] = int(participant.Index)
keys[participant.Index] = participant.KeyShare
}
return indexMap, keys
}

// GenerateRootQC generates QC for root block, caller needs to provide votes for root QC and
// participantData to build the QC.
// NOTE: at the moment, we require private keys for one node because we we re-using the full business logic,
Expand All @@ -64,7 +89,7 @@ func GenerateRootQC(block *flow.Block, votes []*model.Vote, participantData *Par
error, // exception or could not construct qc
) {
// create consensus committee's state
committee, err := committees.NewStaticCommittee(identities, flow.Identifier{}, participantData.Lookup, participantData.GroupKey)
committee, err := committees.NewStaticCommittee(identities, flow.Identifier{}, participantData.DKGCommittee, participantData.DKGGroupKey)
if err != nil {
return nil, nil, err
}
Expand Down Expand Up @@ -209,8 +234,8 @@ func GenerateQCParticipantData(allNodes, internalNodes []bootstrap.NodeInfo, dkg
})
}

qcData.Lookup = participantLookup
qcData.GroupKey = dkgData.PubGroupKey
qcData.DKGCommittee = participantLookup
qcData.DKGGroupKey = dkgData.PubGroupKey

return qcData, nil
}
4 changes: 2 additions & 2 deletions cmd/bootstrap/run/qc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,8 @@ func createSignerData(t *testing.T, n int) *ParticipantData {

participantData := &ParticipantData{
Participants: participants,
Lookup: participantLookup,
GroupKey: groupKey,
DKGCommittee: participantLookup,
DKGGroupKey: groupKey,
}

return participantData
Expand Down
13 changes: 13 additions & 0 deletions consensus/hotstuff/committees/static.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,3 +147,16 @@ func (s staticDKG) KeyShare(nodeID flow.Identifier) (crypto.PublicKey, error) {
}
return participant.KeyShare, nil
}

// NodeID returns the node identifier for the given index.
// An exception is returned if the index is ≥ Size().
// Intended for use outside the hotpath, with runtime
// scaling linearly in the number of DKG participants (ie. Size())
func (s staticDKG) NodeID(index uint) (flow.Identifier, error) {
for nodeID, participant := range s.dkgParticipants {
if participant.Index == index {
return nodeID, nil
}
}
return flow.ZeroID, fmt.Errorf("index %d not found in DKG", index)
}
30 changes: 30 additions & 0 deletions consensus/hotstuff/mocks/dkg.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import (
// TestRandomBeaconReconstructor_InvalidSignerID tests that RandomBeaconReconstructor doesn't forward calls to
// RandomBeaconInspector if it fails to map signerID to signerIndex
func TestRandomBeaconReconstructor_InvalidSignerID(t *testing.T) {
dkg := &mockhotstuff.DKG{}
inspector := &mockhotstuff.RandomBeaconInspector{}
dkg := mockhotstuff.NewDKG(t)
inspector := mockhotstuff.NewRandomBeaconInspector(t)
reconstructor := NewRandomBeaconReconstructor(dkg, inspector)
exception := errors.New("invalid-signer-id")
t.Run("trustedAdd", func(t *testing.T) {
Expand All @@ -31,7 +31,4 @@ func TestRandomBeaconReconstructor_InvalidSignerID(t *testing.T) {
require.ErrorAs(t, err, &exception)
inspector.AssertNotCalled(t, "Verify")
})

dkg.AssertExpectations(t)
inspector.AssertExpectations(t)
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"go.uber.org/atomic"
"golang.org/x/exp/slices"
"pgregory.net/rapid"

bootstrapDKG "github.com/onflow/flow-go/cmd/bootstrap/dkg"
Expand All @@ -28,7 +29,6 @@ import (
"github.com/onflow/flow-go/module/local"
modulemock "github.com/onflow/flow-go/module/mock"
msig "github.com/onflow/flow-go/module/signature"
"github.com/onflow/flow-go/state/protocol/inmem"
storagemock "github.com/onflow/flow-go/storage/mock"
"github.com/onflow/flow-go/utils/unittest"
)
Expand Down Expand Up @@ -796,6 +796,7 @@ func TestCombinedVoteProcessorV2_BuildVerifyQC(t *testing.T) {
stakingSigners := unittest.IdentityListFixture(3)
beaconSigners := unittest.IdentityListFixture(8)
allIdentities := append(stakingSigners, beaconSigners...)
slices.SortFunc(allIdentities, flow.Canonical[flow.Identity]) // sort in place to avoid taking a copy.
require.Equal(t, len(dkgData.PubKeyShares), len(allIdentities))
dkgParticipants := make(map[flow.Identifier]flow.DKGParticipant)
// fill dkg participants data
Expand Down Expand Up @@ -827,7 +828,6 @@ func TestCombinedVoteProcessorV2_BuildVerifyQC(t *testing.T) {
identity.StakingPubKey = stakingPriv.PublicKey()

participantData := dkgParticipants[identity.NodeID]

dkgKey := encodable.RandomBeaconPrivKey{
PrivateKey: dkgData.PrivKeyShares[participantData.Index],
}
Expand All @@ -844,25 +844,14 @@ func TestCombinedVoteProcessorV2_BuildVerifyQC(t *testing.T) {
signers[identity.NodeID] = verification.NewCombinedSigner(me, beaconSignerStore)
}

leader := stakingSigners[0]
leader := allIdentities[0]

block := helper.MakeBlock(helper.WithBlockView(view),
helper.WithBlockProposer(leader.NodeID))

inmemDKG, err := inmem.DKGFromEncodable(inmem.EncodableDKG{
GroupKey: encodable.RandomBeaconPubKey{
PublicKey: dkgData.PubGroupKey,
},
Participants: dkgParticipants,
})
committee, err := committees.NewStaticCommittee(allIdentities, flow.ZeroID, dkgParticipants, dkgData.PubGroupKey)
require.NoError(t, err)

committee := &mockhotstuff.DynamicCommittee{}
committee.On("QuorumThresholdForView", mock.Anything).Return(committees.WeightThresholdToBuildQC(allIdentities.ToSkeleton().TotalWeight()), nil)
committee.On("IdentitiesByEpoch", block.View).Return(allIdentities.ToSkeleton(), nil)
committee.On("IdentitiesByBlock", block.BlockID).Return(allIdentities, nil)
committee.On("DKG", block.View).Return(inmemDKG, nil)

votes := make([]*model.Vote, 0, len(allIdentities))

// first staking signer will be leader collecting votes for proposal
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"go.uber.org/atomic"
"golang.org/x/exp/slices"
"pgregory.net/rapid"

bootstrapDKG "github.com/onflow/flow-go/cmd/bootstrap/dkg"
Expand All @@ -27,7 +28,6 @@ import (
"github.com/onflow/flow-go/module/local"
modulemock "github.com/onflow/flow-go/module/mock"
"github.com/onflow/flow-go/module/signature"
"github.com/onflow/flow-go/state/protocol/inmem"
storagemock "github.com/onflow/flow-go/storage/mock"
"github.com/onflow/flow-go/utils/unittest"
)
Expand Down Expand Up @@ -932,6 +932,7 @@ func TestCombinedVoteProcessorV3_BuildVerifyQC(t *testing.T) {
stakingSigners := unittest.IdentityListFixture(3)
beaconSigners := unittest.IdentityListFixture(8)
allIdentities := append(stakingSigners, beaconSigners...)
slices.SortFunc(allIdentities, flow.Canonical[flow.Identity]) // sort in place to avoid taking a copy.
require.Equal(t, len(dkgData.PubKeyShares), len(allIdentities))
dkgParticipants := make(map[flow.Identifier]flow.DKGParticipant)
// fill dkg participants data
Expand Down Expand Up @@ -980,25 +981,14 @@ func TestCombinedVoteProcessorV3_BuildVerifyQC(t *testing.T) {
signers[identity.NodeID] = verification.NewCombinedSignerV3(me, beaconSignerStore)
}

leader := stakingSigners[0]
leader := allIdentities[0]

block := helper.MakeBlock(helper.WithBlockView(view),
helper.WithBlockProposer(leader.NodeID))

inmemDKG, err := inmem.DKGFromEncodable(inmem.EncodableDKG{
GroupKey: encodable.RandomBeaconPubKey{
PublicKey: dkgData.PubGroupKey,
},
Participants: dkgParticipants,
})
committee, err := committees.NewStaticCommittee(allIdentities, flow.ZeroID, dkgParticipants, dkgData.PubGroupKey)
require.NoError(t, err)

committee := &mockhotstuff.DynamicCommittee{}
committee.On("IdentitiesByBlock", block.BlockID).Return(allIdentities, nil)
committee.On("IdentitiesByEpoch", block.View).Return(allIdentities.ToSkeleton(), nil)
committee.On("QuorumThresholdForView", mock.Anything).Return(committees.WeightThresholdToBuildQC(allIdentities.ToSkeleton().TotalWeight()), nil)
committee.On("DKG", block.View).Return(inmemDKG, nil)

votes := make([]*model.Vote, 0, len(allIdentities))

// first staking signer will be leader collecting votes for proposal
Expand Down
6 changes: 4 additions & 2 deletions consensus/integration/epoch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,11 +226,13 @@ func withNextEpoch(
Participants: nextEpochIdentities.ToSkeleton(),
Assignments: unittest.ClusterAssignment(1, nextEpochIdentities.ToSkeleton()),
}
dkgIndexMap, dkgParticipantKeys := nextEpochParticipantData.DKGData()
nextEpochCommit := &flow.EpochCommit{
Counter: nextEpochSetup.Counter,
ClusterQCs: currEpochCommit.ClusterQCs,
DKGParticipantKeys: nextEpochParticipantData.PublicBeaconKeys(),
DKGGroupKey: nextEpochParticipantData.GroupKey,
DKGParticipantKeys: dkgParticipantKeys,
DKGGroupKey: nextEpochParticipantData.DKGGroupKey,
DKGIndexMap: dkgIndexMap,
}

// Construct the new min epoch state entry
Expand Down
24 changes: 13 additions & 11 deletions consensus/integration/nodes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ func NewConsensusParticipants(data *run.ParticipantData) *ConsensusParticipants
beaconInfoByEpoch: map[uint64]RandomBeaconNodeInfo{
1: {
RandomBeaconPrivKey: participant.RandomBeaconPrivKey,
DKGParticipant: data.Lookup[participant.NodeID],
DKGParticipant: data.DKGCommittee[participant.NodeID],
},
},
}
Expand All @@ -118,7 +118,7 @@ func (p *ConsensusParticipants) Lookup(nodeID flow.Identifier) *ConsensusPartici
// If this node was part of previous epoch it will get updated, if not created.
func (p *ConsensusParticipants) Update(epochCounter uint64, data *run.ParticipantData) {
for _, participant := range data.Participants {
dkgParticipant := data.Lookup[participant.NodeID]
dkgParticipant := data.DKGCommittee[participant.NodeID]
entry, ok := p.lookup[participant.NodeID]
if !ok {
entry = ConsensusParticipant{
Expand Down Expand Up @@ -257,12 +257,13 @@ func createRootBlockData(t *testing.T, participantData *run.ParticipantData) (*f
consensusParticipants := participantData.Identities()

// add other roles to create a complete identity list
participants := unittest.CompleteIdentitySet(consensusParticipants...)
participants.Sort(flow.Canonical[flow.Identity])

participants := unittest.CompleteIdentitySet(consensusParticipants...).Sort(flow.Canonical[flow.Identity])
dkgParticipants := participants.ToSkeleton().Filter(filter.IsValidDKGParticipant)
dkgParticipantsKeys := make([]crypto.PublicKey, 0, len(consensusParticipants))
for _, participant := range participants.Filter(filter.HasRole[flow.Identity](flow.RoleConsensus)) {
dkgParticipantsKeys = append(dkgParticipantsKeys, participantData.Lookup[participant.NodeID].KeyShare)
dkgIndexMap := make(flow.DKGIndexMap)
for index, participant := range dkgParticipants {
dkgParticipantsKeys = append(dkgParticipantsKeys, participantData.DKGCommittee[participant.NodeID].KeyShare)
dkgIndexMap[participant.NodeID] = index
}

counter := uint64(1)
Expand All @@ -276,8 +277,9 @@ func createRootBlockData(t *testing.T, participantData *run.ParticipantData) (*f
unittest.CommitWithCounter(counter),
unittest.WithClusterQCsFromAssignments(setup.Assignments),
func(commit *flow.EpochCommit) {
commit.DKGGroupKey = participantData.GroupKey
commit.DKGGroupKey = participantData.DKGGroupKey
commit.DKGParticipantKeys = dkgParticipantsKeys
commit.DKGIndexMap = dkgIndexMap
},
)

Expand Down Expand Up @@ -327,16 +329,16 @@ func completeConsensusIdentities(t *testing.T, nodeInfos []bootstrap.NodeInfo) *

participantData := &run.ParticipantData{
Participants: make([]run.Participant, 0, len(nodeInfos)),
Lookup: make(map[flow.Identifier]flow.DKGParticipant),
GroupKey: dkgData.PubGroupKey,
DKGCommittee: make(map[flow.Identifier]flow.DKGParticipant),
DKGGroupKey: dkgData.PubGroupKey,
}
for index, node := range nodeInfos {
participant := run.Participant{
NodeInfo: node,
RandomBeaconPrivKey: dkgData.PrivKeyShares[index],
}
participantData.Participants = append(participantData.Participants, participant)
participantData.Lookup[node.NodeID] = flow.DKGParticipant{
participantData.DKGCommittee[node.NodeID] = flow.DKGParticipant{
Index: uint(index),
KeyShare: dkgData.PubKeyShares[index],
}
Expand Down
Loading
Loading