diff --git a/cmd/bootstrap/cmd/block.go b/cmd/bootstrap/cmd/block.go index b1ac37ba910..b582eb21b3b 100644 --- a/cmd/bootstrap/cmd/block.go +++ b/cmd/bootstrap/cmd/block.go @@ -42,7 +42,7 @@ func constructRootEpochEvents( participants flow.IdentityList, assignments flow.AssignmentList, clusterQCs []*flow.QuorumCertificate, - dkgData dkg.DKGData, + dkgData dkg.ThresholdKeySet, dkgIndexMap flow.DKGIndexMap, ) (*flow.EpochSetup, *flow.EpochCommit) { diff --git a/cmd/bootstrap/cmd/dkg.go b/cmd/bootstrap/cmd/dkg.go index 37245272d82..9914c02507d 100644 --- a/cmd/bootstrap/cmd/dkg.go +++ b/cmd/bootstrap/cmd/dkg.go @@ -14,30 +14,27 @@ import ( "github.com/onflow/flow-go/state/protocol/inmem" ) -func runBeaconKG(nodes []model.NodeInfo) (dkg.DKGData, flow.DKGIndexMap) { +func runBeaconKG(nodes []model.NodeInfo) (dkg.ThresholdKeySet, flow.DKGIndexMap) { n := len(nodes) log.Info().Msgf("read %v node infos for DKG", n) log.Debug().Msgf("will run DKG") - var dkgData dkg.DKGData - var err error - dkgData, err = bootstrapDKG.RandomBeaconKG(n, GenerateRandomSeed(crypto.KeyGenSeedMinLen)) + randomBeaconData, err := bootstrapDKG.RandomBeaconKG(n, GenerateRandomSeed(crypto.KeyGenSeedMinLen)) if err != nil { log.Fatal().Err(err).Msg("error running DKG") } log.Info().Msgf("finished running DKG") - pubKeyShares := make([]encodable.RandomBeaconPubKey, 0, len(dkgData.PubKeyShares)) - for _, pubKey := range dkgData.PubKeyShares { - pubKeyShares = append(pubKeyShares, encodable.RandomBeaconPubKey{PublicKey: pubKey}) - } - - privKeyShares := make([]encodable.RandomBeaconPrivKey, 0, len(dkgData.PrivKeyShares)) - for i, privKey := range dkgData.PrivKeyShares { + encodableParticipants := make([]inmem.ThresholdParticipant, 0, len(nodes)) + for i, privKey := range randomBeaconData.PrivKeyShares { nodeID := nodes[i].NodeID encKey := encodable.RandomBeaconPrivKey{PrivateKey: privKey} - privKeyShares = append(privKeyShares, encKey) + encodableParticipants = append(encodableParticipants, inmem.ThresholdParticipant{ + PrivKeyShare: encKey, + PubKeyShare: encodable.RandomBeaconPubKey{PublicKey: randomBeaconData.PubKeyShares[i]}, + NodeID: nodeID, + }) err = common.WriteJSON(fmt.Sprintf(model.PathRandomBeaconPriv, nodeID), flagOutdir, encKey) if err != nil { @@ -46,23 +43,22 @@ func runBeaconKG(nodes []model.NodeInfo) (dkg.DKGData, flow.DKGIndexMap) { log.Info().Msgf("wrote file %s/%s", flagOutdir, fmt.Sprintf(model.PathRandomBeaconPriv, nodeID)) } - indexMap := make(flow.DKGIndexMap, len(pubKeyShares)) + indexMap := make(flow.DKGIndexMap, len(nodes)) for i, node := range nodes { indexMap[node.NodeID] = i } // write full DKG info that will be used to construct QC - err = common.WriteJSON(model.PathRootDKGData, flagOutdir, inmem.EncodableFullDKG{ + err = common.WriteJSON(model.PathRootDKGData, flagOutdir, inmem.ThresholdKeySet{ GroupKey: encodable.RandomBeaconPubKey{ - PublicKey: dkgData.PubGroupKey, + PublicKey: randomBeaconData.PubGroupKey, }, - PubKeyShares: pubKeyShares, - PrivKeyShares: privKeyShares, + Participants: encodableParticipants, }) if err != nil { log.Fatal().Err(err).Msg("failed to write json") } log.Info().Msgf("wrote file %s/%s", flagOutdir, model.PathRootDKGData) - return dkgData, indexMap + return randomBeaconData, indexMap } diff --git a/cmd/bootstrap/cmd/finalize.go b/cmd/bootstrap/cmd/finalize.go index 4c258b1ab30..43da00b83d9 100644 --- a/cmd/bootstrap/cmd/finalize.go +++ b/cmd/bootstrap/cmd/finalize.go @@ -149,7 +149,7 @@ func finalize(cmd *cobra.Command, args []string) { log.Info().Msgf("received votes total: %v", len(votes)) log.Info().Msg("reading dkg data") - dkgData := readDKGData() + dkgData, _ := readRandomBeaconKeys() log.Info().Msg("") log.Info().Msg("reading intermediary bootstrapping data") @@ -347,29 +347,28 @@ func readRootBlock() *flow.Block { return rootBlock } -// readDKGData reads DKG data from disc, this file needs to be prepared with -// rootblock command -func readDKGData() dkg.DKGData { - encodableDKG, err := utils.ReadData[inmem.EncodableFullDKG](flagDKGDataPath) +// readRandomBeaconKeys reads the threshold key data from disc. +// This file needs to be prepared with rootblock command +func readRandomBeaconKeys() (dkg.ThresholdKeySet, flow.DKGIndexMap) { + encodableDKG, err := utils.ReadData[inmem.ThresholdKeySet](flagDKGDataPath) if err != nil { - log.Fatal().Err(err).Msg("could not read DKG data") + log.Fatal().Err(err).Msg("loading threshold key data for Random Beacon failed") } - dkgData := dkg.DKGData{ + dkgData := dkg.ThresholdKeySet{ PrivKeyShares: nil, PubGroupKey: encodableDKG.GroupKey.PublicKey, PubKeyShares: nil, } - for _, pubKey := range encodableDKG.PubKeyShares { - dkgData.PubKeyShares = append(dkgData.PubKeyShares, pubKey.PublicKey) - } - - for _, privKey := range encodableDKG.PrivKeyShares { - dkgData.PrivKeyShares = append(dkgData.PrivKeyShares, privKey.PrivateKey) + indexMap := make(flow.DKGIndexMap, len(encodableDKG.Participants)) + for i, participant := range encodableDKG.Participants { + dkgData.PubKeyShares = append(dkgData.PubKeyShares, participant.PubKeyShare.PublicKey) + dkgData.PrivKeyShares = append(dkgData.PrivKeyShares, participant.PrivKeyShare.PrivateKey) + indexMap[participant.NodeID] = i } - return dkgData + return dkgData, indexMap } // Validation utility methods ------------------------------------------------ diff --git a/cmd/bootstrap/cmd/qc.go b/cmd/bootstrap/cmd/qc.go index 22474ed1d19..1acb5fa51f7 100644 --- a/cmd/bootstrap/cmd/qc.go +++ b/cmd/bootstrap/cmd/qc.go @@ -13,10 +13,10 @@ import ( ) // constructRootQC constructs root QC based on root block, votes and dkg info -func constructRootQC(block *flow.Block, votes []*model.Vote, allNodes, internalNodes []bootstrap.NodeInfo, dkgData dkg.DKGData) *flow.QuorumCertificate { +func constructRootQC(block *flow.Block, votes []*model.Vote, allNodes, internalNodes []bootstrap.NodeInfo, randomBeaconData dkg.ThresholdKeySet) *flow.QuorumCertificate { identities := bootstrap.ToIdentityList(allNodes) - participantData, err := run.GenerateQCParticipantData(allNodes, internalNodes, dkgData) + participantData, err := run.GenerateQCParticipantData(allNodes, internalNodes, randomBeaconData) if err != nil { log.Fatal().Err(err).Msg("failed to generate QC participant data") } @@ -36,8 +36,8 @@ func constructRootQC(block *flow.Block, votes []*model.Vote, allNodes, internalN } // NOTE: allNodes must be in the same order as when generating the DKG -func constructRootVotes(block *flow.Block, allNodes, internalNodes []bootstrap.NodeInfo, dkgData dkg.DKGData) { - participantData, err := run.GenerateQCParticipantData(allNodes, internalNodes, dkgData) +func constructRootVotes(block *flow.Block, allNodes, internalNodes []bootstrap.NodeInfo, randomBeaconData dkg.ThresholdKeySet) { + participantData, err := run.GenerateQCParticipantData(allNodes, internalNodes, randomBeaconData) if err != nil { log.Fatal().Err(err).Msg("failed to generate QC participant data") } diff --git a/cmd/bootstrap/cmd/rootblock.go b/cmd/bootstrap/cmd/rootblock.go index 64aea3789ba..54316b5a917 100644 --- a/cmd/bootstrap/cmd/rootblock.go +++ b/cmd/bootstrap/cmd/rootblock.go @@ -174,7 +174,7 @@ func rootBlock(cmd *cobra.Command, args []string) { log.Info().Msg("") log.Info().Msg("running DKG for consensus nodes") - dkgData, dkgIndexMap := runBeaconKG(model.FilterByRole(stakingNodes, flow.RoleConsensus)) + randomBeaconData, dkgIndexMap := runBeaconKG(model.FilterByRole(stakingNodes, flow.RoleConsensus)) log.Info().Msg("") // create flow.IdentityList representation of the participant set @@ -200,8 +200,8 @@ func rootBlock(cmd *cobra.Command, args []string) { log.Info().Msg("") log.Info().Msg("constructing intermediary bootstrapping data") - epochSetup, epochCommit := constructRootEpochEvents(header.View, participants, assignments, clusterQCs, dkgData, dkgIndexMap) - epochConfig := generateExecutionStateEpochConfig(epochSetup, clusterQCs, dkgData) + epochSetup, epochCommit := constructRootEpochEvents(header.View, participants, assignments, clusterQCs, randomBeaconData, dkgIndexMap) + epochConfig := generateExecutionStateEpochConfig(epochSetup, clusterQCs, randomBeaconData) intermediaryEpochData := IntermediaryEpochData{ RootEpochSetup: epochSetup, RootEpochCommit: epochCommit, @@ -245,7 +245,7 @@ func rootBlock(cmd *cobra.Command, args []string) { block, model.FilterByRole(stakingNodes, flow.RoleConsensus), model.FilterByRole(internalNodes, flow.RoleConsensus), - dkgData, + randomBeaconData, ) log.Info().Msg("") } @@ -286,7 +286,7 @@ func validateEpochConfig() error { func generateExecutionStateEpochConfig( epochSetup *flow.EpochSetup, clusterQCs []*flow.QuorumCertificate, - dkgData dkg.DKGData, + dkgData dkg.ThresholdKeySet, ) epochs.EpochConfig { randomSource := make([]byte, flow.EpochSetupRandomSourceLength) diff --git a/cmd/bootstrap/dkg/dkg.go b/cmd/bootstrap/dkg/dkg.go index 8b740d85434..e6dfe1788d4 100644 --- a/cmd/bootstrap/dkg/dkg.go +++ b/cmd/bootstrap/dkg/dkg.go @@ -10,15 +10,15 @@ import ( ) // RandomBeaconKG is centralized BLS threshold signature key generation. -func RandomBeaconKG(n int, seed []byte) (model.DKGData, error) { +func RandomBeaconKG(n int, seed []byte) (model.ThresholdKeySet, error) { if n == 1 { sk, pk, pkGroup, err := thresholdSignKeyGenOneNode(seed) if err != nil { - return model.DKGData{}, fmt.Errorf("Beacon KeyGen failed: %w", err) + return model.ThresholdKeySet{}, fmt.Errorf("Beacon KeyGen failed: %w", err) } - dkgData := model.DKGData{ + dkgData := model.ThresholdKeySet{ PrivKeyShares: sk, PubGroupKey: pkGroup, PubKeyShares: pk, @@ -29,16 +29,16 @@ func RandomBeaconKG(n int, seed []byte) (model.DKGData, error) { skShares, pkShares, pkGroup, err := crypto.BLSThresholdKeyGen(int(n), signature.RandomBeaconThreshold(int(n)), seed) if err != nil { - return model.DKGData{}, fmt.Errorf("Beacon KeyGen failed: %w", err) + return model.ThresholdKeySet{}, fmt.Errorf("Beacon KeyGen failed: %w", err) } - dkgData := model.DKGData{ + randomBeaconData := model.ThresholdKeySet{ PrivKeyShares: skShares, PubGroupKey: pkGroup, PubKeyShares: pkShares, } - return dkgData, nil + return randomBeaconData, nil } // Beacon KG with one node diff --git a/cmd/bootstrap/run/qc.go b/cmd/bootstrap/run/qc.go index 6f6e304b159..dccfb3ba178 100644 --- a/cmd/bootstrap/run/qc.go +++ b/cmd/bootstrap/run/qc.go @@ -24,7 +24,7 @@ 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 +// ParticipantData represents a subset of all consensus participants that contribute 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 @@ -190,7 +190,7 @@ func createValidator(committee hotstuff.DynamicCommittee) (hotstuff.Validator, e // LIMITATION: this function only supports the 'trusted dealer' model, where for the consensus committee (`allNodes`) // a trusted dealer generated the threshold-signature key (`dkgData` containing key shares and group key). Therefore, // `allNodes` must be in the same order that was used when running the DKG. -func GenerateQCParticipantData(allNodes, internalNodes []bootstrap.NodeInfo, dkgData dkg.DKGData) (*ParticipantData, error) { +func GenerateQCParticipantData(allNodes, internalNodes []bootstrap.NodeInfo, dkgData dkg.ThresholdKeySet) (*ParticipantData, error) { // stakingNodes can include external validators, so it can be longer than internalNodes if len(allNodes) < len(internalNodes) { return nil, fmt.Errorf("need at least as many staking public keys as private keys (pub=%d, priv=%d)", len(allNodes), len(internalNodes)) @@ -236,7 +236,7 @@ func GenerateQCParticipantData(allNodes, internalNodes []bootstrap.NodeInfo, dkg dkgParticipant, ok := participantLookup[node.NodeID] if !ok { - return nil, fmt.Errorf("nonexistannt node id (%x) in participant lookup", node.NodeID) + return nil, fmt.Errorf("nonexistent node id (%x) in participant lookup", node.NodeID) } dkgIndex := dkgParticipant.Index diff --git a/cmd/util/cmd/epochs/cmd/recover.go b/cmd/util/cmd/epochs/cmd/recover.go index 4ae6d1759d8..89cd8abc7ab 100644 --- a/cmd/util/cmd/epochs/cmd/recover.go +++ b/cmd/util/cmd/epochs/cmd/recover.go @@ -20,7 +20,7 @@ import ( // EFM can be exited only by a special service event, EpochRecover, which initially originates from a manual service account transaction. // The full epoch data must be generated manually and submitted with this transaction in order for an // EpochRecover event to be emitted. This command retrieves the current protocol state identities, computes the cluster assignment using those -// identities, generates the cluster QCs and retrieves the DKG key vector of the last successful epoch. +// identities, generates the cluster QCs and retrieves the Random Beacon key vector of the last successful epoch. // This recovery process has some constraints: // - The RecoveryEpoch must have exactly the same consensus committee as participated in the most recent successful DKG. // - The RecoveryEpoch must contain enough "internal" collection nodes so that all clusters contain a supermajority of "internal" collection nodes (same constraint as sporks) diff --git a/consensus/hotstuff/signature/randombeacon_signer_store.go b/consensus/hotstuff/signature/randombeacon_signer_store.go index c5ff9d13f4a..f0f993f7cbc 100644 --- a/consensus/hotstuff/signature/randombeacon_signer_store.go +++ b/consensus/hotstuff/signature/randombeacon_signer_store.go @@ -42,9 +42,9 @@ func (s *EpochAwareRandomBeaconKeyStore) ByView(view uint64) (crypto.PrivateKey, } // When DKG has completed, - // - if a node successfully generated the DKG key, the valid private key will be stored in database. - // - if a node failed to generate the DKG key, we will save a record in database to indicate this - // node has no private key for this epoch. + // - if a node successfully generated the Random Beacon key, the valid private key will be stored in database. + // - if a node failed to generate the Random Beacon key, we will save a record in database to indicate this + // node has no private key for this epoch. // Within the epoch, we can look up my random beacon private key for the epoch. There are 3 cases: // 1. DKG has completed, and the private key is stored in database, and we can retrieve it (happy path) // 2. DKG has completed, but we failed to generate a private key (unhappy path) diff --git a/consensus/hotstuff/verification/combined_verifier_v3.go b/consensus/hotstuff/verification/combined_verifier_v3.go index 6b2c0507381..5aff5e352a3 100644 --- a/consensus/hotstuff/verification/combined_verifier_v3.go +++ b/consensus/hotstuff/verification/combined_verifier_v3.go @@ -174,7 +174,7 @@ func (c *CombinedVerifierV3) VerifyQC(signers flow.IdentitySkeletonList, sigData if protocol.IsIdentityNotFound(err) { return model.NewInvalidSignerErrorf("%v is not a random beacon participant: %w", signerID, err) } - return fmt.Errorf("unexpected error retrieving dkg key share for signer %v: %w", signerID, err) + return fmt.Errorf("unexpected error retrieving Random Beacon key share for signer %v: %w", signerID, err) } beaconPubKeys = append(beaconPubKeys, keyShare) } diff --git a/consensus/hotstuff/votecollector/combined_vote_processor_v2_test.go b/consensus/hotstuff/votecollector/combined_vote_processor_v2_test.go index 74387d32433..da25cc6ddb9 100644 --- a/consensus/hotstuff/votecollector/combined_vote_processor_v2_test.go +++ b/consensus/hotstuff/votecollector/combined_vote_processor_v2_test.go @@ -812,7 +812,7 @@ func TestCombinedVoteProcessorV2_BuildVerifyQC(t *testing.T) { identity.StakingPubKey = stakingPriv.PublicKey() keys := &storagemock.SafeBeaconKeys{} - // there is no DKG key for this epoch + // there is no Random Beacon key for this epoch keys.On("RetrieveMyBeaconPrivateKey", epochCounter).Return(nil, false, nil) beaconSignerStore := hsig.NewEpochAwareRandomBeaconKeyStore(epochLookup, keys) @@ -833,7 +833,7 @@ func TestCombinedVoteProcessorV2_BuildVerifyQC(t *testing.T) { } keys := &storagemock.SafeBeaconKeys{} - // there is DKG key for this epoch + // there is Random Beacon key for this epoch keys.On("RetrieveMyBeaconPrivateKey", epochCounter).Return(dkgKey, true, nil) beaconSignerStore := hsig.NewEpochAwareRandomBeaconKeyStore(epochLookup, keys) diff --git a/consensus/hotstuff/votecollector/combined_vote_processor_v3_test.go b/consensus/hotstuff/votecollector/combined_vote_processor_v3_test.go index ad684ee3d83..56bba752881 100644 --- a/consensus/hotstuff/votecollector/combined_vote_processor_v3_test.go +++ b/consensus/hotstuff/votecollector/combined_vote_processor_v3_test.go @@ -948,7 +948,7 @@ func TestCombinedVoteProcessorV3_BuildVerifyQC(t *testing.T) { identity.StakingPubKey = stakingPriv.PublicKey() keys := &storagemock.SafeBeaconKeys{} - // there is no DKG key for this epoch + // there is no Random Beacon key for this epoch keys.On("RetrieveMyBeaconPrivateKey", epochCounter).Return(nil, false, nil) beaconSignerStore := hsig.NewEpochAwareRandomBeaconKeyStore(epochLookup, keys) @@ -970,7 +970,7 @@ func TestCombinedVoteProcessorV3_BuildVerifyQC(t *testing.T) { } keys := &storagemock.SafeBeaconKeys{} - // there is DKG key for this epoch + // there is Random Beacon key for this epoch keys.On("RetrieveMyBeaconPrivateKey", epochCounter).Return(dkgKey, true, nil) beaconSignerStore := hsig.NewEpochAwareRandomBeaconKeyStore(epochLookup, keys) diff --git a/consensus/integration/epoch_test.go b/consensus/integration/epoch_test.go index 87e802a74cc..0b9b364cb1f 100644 --- a/consensus/integration/epoch_test.go +++ b/consensus/integration/epoch_test.go @@ -117,7 +117,7 @@ func TestEpochTransition_IdentitiesOverlap(t *testing.T) { newIdentity, ) - // generate new identities for next epoch, it will generate new DKG keys for random beacon participants + // generate new identities for next epoch, it will generate new Random Beacon keys for random beacon participants nextEpochParticipantData := completeConsensusIdentities(t, privateNodeInfos[1:]) rootSnapshot = withNextEpoch(t, rootSnapshot, nextEpochIdentities, nextEpochParticipantData, consensusParticipants, 4, func(block *flow.Block) *flow.QuorumCertificate { return createRootQC(t, block, firstEpochConsensusParticipants) diff --git a/consensus/integration/nodes_test.go b/consensus/integration/nodes_test.go index f75256d3739..4426c018ec9 100644 --- a/consensus/integration/nodes_test.go +++ b/consensus/integration/nodes_test.go @@ -530,7 +530,7 @@ func createNode( require.NoError(t, err) keys := &storagemock.SafeBeaconKeys{} - // there is DKG key for this epoch + // there is Random Beacon key for this epoch keys.On("RetrieveMyBeaconPrivateKey", mock.Anything).Return( func(epochCounter uint64) crypto.PrivateKey { dkgInfo, ok := participant.beaconInfoByEpoch[epochCounter] diff --git a/integration/testnet/network.go b/integration/testnet/network.go index 217078aaded..9815d181bd0 100644 --- a/integration/testnet/network.go +++ b/integration/testnet/network.go @@ -1072,8 +1072,8 @@ func BootstrapNetwork(networkConf NetworkConfig, bootstrapDir string, chainID fl allNodeInfos := append(toNodeInfos(stakedConfs), followerInfos...) - // IMPORTANT: we must use this ordering when writing the DKG keys as - // this ordering defines the DKG participant's indices + // IMPORTANT: we must use this ordering when writing the Random Beacon keys as + // this ordering defines the DKG participants' indices stakedNodeInfos := bootstrap.Sort(toNodeInfos(stakedConfs), flow.Canonical[flow.Identity]) dkg, dkgIndexMap, err := runBeaconKG(stakedConfs) @@ -1344,7 +1344,7 @@ func setupKeys(networkConf NetworkConfig) ([]ContainerConfig, error) { // and returns all DKG data. This includes the group private key, node indices, // and per-node public and private key-shares. // Only consensus nodes participate in the DKG. -func runBeaconKG(confs []ContainerConfig) (dkgmod.DKGData, flow.DKGIndexMap, error) { +func runBeaconKG(confs []ContainerConfig) (dkgmod.ThresholdKeySet, flow.DKGIndexMap, error) { // filter by consensus nodes consensusNodes := bootstrap.Sort(bootstrap.FilterByRole(toNodeInfos(confs), flow.RoleConsensus), flow.Canonical[flow.Identity]) @@ -1352,12 +1352,12 @@ func runBeaconKG(confs []ContainerConfig) (dkgmod.DKGData, flow.DKGIndexMap, err dkgSeed, err := getSeed() if err != nil { - return dkgmod.DKGData{}, nil, err + return dkgmod.ThresholdKeySet{}, nil, err } - dkg, err := dkg.RandomBeaconKG(nConsensusNodes, dkgSeed) + randomBeaconData, err := dkg.RandomBeaconKG(nConsensusNodes, dkgSeed) if err != nil { - return dkgmod.DKGData{}, nil, err + return dkgmod.ThresholdKeySet{}, nil, err } indexMap := make(flow.DKGIndexMap, nConsensusNodes) @@ -1365,7 +1365,7 @@ func runBeaconKG(confs []ContainerConfig) (dkgmod.DKGData, flow.DKGIndexMap, err indexMap[node.NodeID] = i } - return dkg, indexMap, nil + return randomBeaconData, indexMap, nil } // setupClusterGenesisBlockQCs generates bootstrapping resources necessary for each collector cluster: diff --git a/model/convert/service_event.go b/model/convert/service_event.go index 9687ba98c50..43f63cb85c1 100644 --- a/model/convert/service_event.go +++ b/model/convert/service_event.go @@ -275,13 +275,13 @@ func convertServiceEventEpochCommit(event flow.Event) (*flow.ServiceEvent, error // parse DKG participants commit.DKGParticipantKeys, err = convertDKGKeys(cdcDKGKeys.Values) if err != nil { - return nil, fmt.Errorf("could not convert DKG keys: %w", err) + return nil, fmt.Errorf("could not convert Random Beacon keys: %w", err) } // parse DKG group key commit.DKGGroupKey, err = convertDKGKey(cdcDKGGroupKey) if err != nil { - return nil, fmt.Errorf("could not convert DKG group key: %w", err) + return nil, fmt.Errorf("could not convert Random Beacon group key: %w", err) } // parse DKG Index Map @@ -469,13 +469,13 @@ func convertServiceEventEpochRecover(event flow.Event) (*flow.ServiceEvent, erro // parse DKG participants commit.DKGParticipantKeys, err = convertDKGKeys(cdcDKGKeys.Values) if err != nil { - return nil, fmt.Errorf("failed to decode DKG key shares from EpochRecover event: %w", err) + return nil, fmt.Errorf("failed to decode Random Beacon key shares from EpochRecover event: %w", err) } // parse DKG group key commit.DKGGroupKey, err = convertDKGKey(cdcDKGGroupKey) if err != nil { - return nil, fmt.Errorf("failed to decode DKG group key from EpochRecover event: %w", err) + return nil, fmt.Errorf("failed to decode Random Beacon group key from EpochRecover event: %w", err) } // parse DKG Index Map @@ -986,7 +986,7 @@ func convertClusterQCVotes(cdcClusterQCs []cadence.Value) ( return qcVoteDatas, nil } -// convertDKGKeys converts hex-encoded DKG public keys as received by the DKG +// convertDKGKeys converts hex-encoded public beacon keys as received by the DKG // smart contract into crypto.PublicKey representations suitable for inclusion // in the protocol state. func convertDKGKeys(cdcDKGKeys []cadence.Value) ([]crypto.PublicKey, error) { @@ -994,14 +994,14 @@ func convertDKGKeys(cdcDKGKeys []cadence.Value) ([]crypto.PublicKey, error) { for _, value := range cdcDKGKeys { pubKey, err := convertDKGKey(value) if err != nil { - return nil, fmt.Errorf("could not decode dkg public key: %w", err) + return nil, fmt.Errorf("could not decode public beacon key share: %w", err) } convertedKeys = append(convertedKeys, pubKey) } return convertedKeys, nil } -// convertDKGKey converts a single hex-encoded DKG public key as received by the DKG +// convertDKGKey converts a single hex-encoded public beacon keys as received by the DKG // smart contract into crypto.PublicKey representations suitable for inclusion // in the protocol state. func convertDKGKey(cdcDKGKeys cadence.Value) (crypto.PublicKey, error) { @@ -1014,11 +1014,11 @@ func convertDKGKey(cdcDKGKeys cadence.Value) (crypto.PublicKey, error) { // decode individual public keys pubKeyBytes, err := hex.DecodeString(string(keyHex)) if err != nil { - return nil, fmt.Errorf("could not decode individual public key into bytes: %w", err) + return nil, fmt.Errorf("converting hex to bytes failed: %w", err) } pubKey, err := crypto.DecodePublicKey(crypto.BLSBLS12381, pubKeyBytes) if err != nil { - return nil, fmt.Errorf("could not decode dkg public key: %w", err) + return nil, fmt.Errorf("could not decode bytes into a public key: %w", err) } return pubKey, nil } diff --git a/model/dkg/dkg.go b/model/dkg/dkg.go index d1481cc1fd3..30bfb8cedfa 100644 --- a/model/dkg/dkg.go +++ b/model/dkg/dkg.go @@ -4,9 +4,10 @@ import ( "github.com/onflow/crypto" ) -// DKGData represents all the output data from the DKG process, including private information. -// It is used while running the DKG during bootstrapping. -type DKGData struct { +// ThresholdKeySet represents all the output data from the KG process needed for a threshold signature scheme that +// is used in random beacon protocol, including private information. +// Typically, the ThresholdKeySet is used with a trusted setup during bootstrapping. +type ThresholdKeySet struct { PrivKeyShares []crypto.PrivateKey PubGroupKey crypto.PublicKey PubKeyShares []crypto.PublicKey diff --git a/model/flow/dkg.go b/model/flow/dkg.go index f4ba41ab267..e9825d86fc8 100644 --- a/model/flow/dkg.go +++ b/model/flow/dkg.go @@ -36,52 +36,66 @@ func (state DKGEndState) String() string { } } -// DKGIndexMap describes the membership of the DKG committee π’Ÿ. Flow's random beacon utilizes -// a threshold signature scheme, which requires a Distributed Key Generation [DKG] to generate the -// key shares for each committee member. In the formal cryptographic protocol for DKG with n parties, -// the individual participants are solely identified by indices {0, 1, ..., n-1} and the fact that these -// are non-negative integer values is actively used by the DKG protocol. Accordingly, our implementation -// of the lower-level cryptographic primitives work with these DKG index values. -// On the protocol level, only consensus nodes (identified by their nodeIDs) are allowed to contribute -// random beacon signature shares. Hence, the protocol level needs to map nodeIDs to DKG indices when -// calling into the lower-level cryptographic primitives. +// DKGIndexMap completely describes the DKG committee π’Ÿ of size |π’Ÿ| = n. // // Formal specification: -// - DKGIndexMap completely describes the DKG committee. If there were n parties authorized to participate -// in the DKG, DKGIndexMap must contain exactly n elements, i.e. n = len(DKGIndexMap) -// - The values in DKGIndexMap must form the set {0, 1, …, n-1}. +// - If n parties are authorized to participate in the DKG, DKGIndexMap must contain exactly n +// elements, i.e. n = len(DKGIndexMap) +// - The values in DKGIndexMap must form the set {0, 1, …, n-1}, as required by the low level cryptography +// module (convention simplifying the implementation). // -// CAUTION: It is important to cleanly differentiate between the consensus committee π’ž, the random beacon -// committee β„› and the DKG committee π’Ÿ: +// Flow's random beacon utilizes a threshold signature scheme run by the committee π’Ÿ. +// In the formal cryptographic protocol for a threshold signature with n parties, the +// individual participants are identified by n public distinct non-negative integers, or simply indices. +// These public indices are agreed upon by all participants and are used by the low-level +// Shamir Secret Sharing [SSS]. +// In Flow, the threshold signature keys are generated by a Distributed Key Generation [DKG]. The DKG +// therefore requires the same SSS indices as an input to generate the private key shares of each participant. +// Accordingly, the lower-level cryptographic implementation of the threshold signature and DKG +// works with these indices. The lower-level cryptographic interface requires that the indices are exactly +// the set {0, 1, ..., n-1}. +// +// On the protocol level, only consensus nodes (identified by their nodeIDs) are allowed to contribute +// random beacon signature shares. Hence, the protocol level needs to map nodeIDs to the indices when +// calling into the lower-level cryptographic primitives. +// +// CAUTION: It is important to cleanly differentiate between the consensus committee π’ž, the DKG committee π’Ÿ +// and the committee β„›: // - For an epoch, the consensus committee π’ž contains all nodes that are authorized to vote for blocks. Authority // to vote (i.e. membership in the consensus committee) is irrevocably granted for an epoch (though, honest nodes // will reject votes and proposals from ejected nodes; nevertheless, ejected nodes formally remain members of // the consensus committee). -// - Only consensus nodes are allowed to contribute to the random beacon. We define the random beacon committee β„› -// as the subset of the consensus nodes, which _successfully_ completed the DKG. Hence, β„› βŠ† π’ž. -// - Lastly, there is the DKG committee π’Ÿ, which is the set of parties that were authorized to -// participate in the DKG. Mathematically, the DKGIndexMap is an injective function -// DKGIndexMap: π’Ÿ ↦ {0,1,…,n-1}. +// - The DKG committee π’Ÿ is the set of parties that were authorized to participate in the DKG (happy path; or +// eligible to receive a private key share from an alternative source on the fallback path). Mathematically, +// the DKGIndexMap is a bijective function DKGIndexMap: π’Ÿ ↦ {0,1,…,n-1}. +// - Only consensus nodes are allowed to contribute to the random beacon. Informally, we define β„› as the +// as the subset of the consensus committee (β„› βŠ† π’ž), which _successfully_ completed the DKG (hence β„› βŠ† π’Ÿ). +// Specifically, r ∈ β„› iff and only if r has a private Random Beacon key share matching the respective public +// key share in the `EpochCommit` event. In other words, consensus nodes are in β„› iff and only if they are able +// to submit valid random beacon votes. Based on this definition we note that β„› βŠ† (π’Ÿ ∩ π’ž). // // The protocol explicitly ALLOWS additional parties outside the current epoch's consensus committee to participate. // In particular, there can be a key-value pair (d,i) ∈ DKGIndexMap, such that the nodeID d is *not* a consensus -// committee member, i.e. d βˆ‰ π’ž. In terms of sets, this implies we must consistently work with the relatively -// general assumption that π’Ÿ \ π’ž β‰  βˆ… and π’ž \ π’Ÿ β‰  βˆ…. +// committee member, i.e. d βˆ‰ π’ž. This may be the case when a DKG is run off-protocol to bootstrap the network. +// In terms of sets, this implies we must consistently work with the relatively general +// assumption that π’Ÿ \ π’ž β‰  βˆ… and π’ž \ π’Ÿ β‰  βˆ…. // Nevertheless, in the vast majority of cases (happy path, roughly 98% of epochs) it will be the case that π’Ÿ = π’ž. // Therefore, we can optimize for the case π’Ÿ = π’ž, as long as we still support the more general case π’Ÿ β‰  π’ž. // Broadly, this makes the protocol more robust against temporary disruptions and sudden, large fluctuations in node // participation. -// Nevertheless, there is an important liveness constraint: the intersection, π’Ÿ ∩ π’ž = β„› should be a larger number of -// nodes. Specifically, an honest supermajority of consensus nodes must contain enough successful DKG participants -// (about n/2) to produce a valid group signature for the random beacon [1, 3]. Therefore, we have the approximate -// lower bound |β„›| = |π’Ÿ ∩ π’ž| = n/2 = |π’Ÿ|/2 = len(DKGIndexMap)/2. Operating close to this lower bound would -// require that every random beacon key-holder r ∈ β„› remaining in the consensus committee is honest -// (incl. quickly responsive) *all the time*. This is a lower bound, unsuited for decentralized production networks. +// +// Nevertheless, there is an important liveness constraint: the committee β„› should be a large number of nodes. +// Specifically, an honest supermajority of consensus nodes must contain enough successful DKG participants +// (about |π’Ÿ|/2 + 1) to produce a valid group signature for the random beacon at each block [1, 3]. +// Therefore, we have the approximate lower bound |β„›| ≳ n/2 + 1 = |π’Ÿ|/2 + 1 = len(DKGIndexMap)/2 + 1. +// Operating close to this lower bound would require that every random beacon key-holder Ο± ∈ β„› remaining in the consensus committee is honest +// (incl. quickly responsive) *all the time*. Such a reliability assumption is unsuited for decentralized production networks. // To reject configurations that are vulnerable to liveness failures, the protocol uses the threshold `t_safety` -// (heuristic, see [2]), which is implemented on the smart contract level. In a nutshell, the cardinality of intersection π’Ÿ ∩ π’ž -// (wrt both sets π’Ÿ ∩ π’ž) should be well above 70%, values in the range 70-62% should be considered for short-term -// recovery cases. Values of 62% or lower (i.e. |β„›| ≀ 0.62Β·|π’Ÿ| or |β„›| ≀ 0.62Β·|π’ž|) are not recommended for any -// production network, as single-node crashes are already enough to halt consensus. +// (heuristic, see [2]), which is implemented on the smart contract level. +// Ideally, |β„›| and therefore |π’Ÿ ∩ π’ž| (given that |β„›| <= |π’Ÿ ∩ π’ž|) should be well above 70% . |π’Ÿ|. +// Values in the range 70%-62% of |π’Ÿ| should be considered for short-term recovery cases. +// Values of 62% * |π’Ÿ| or lower (i.e. |β„›| ≀ 0.62Β·|π’Ÿ|) are not recommended for any +// production network, as single-node crashes may already be enough to halt consensus. // // For further details, see // - [1] https://www.notion.so/flowfoundation/Threshold-Signatures-7e26c6dd46ae40f7a83689ba75a785e3?pvs=4 diff --git a/module/dkg.go b/module/dkg.go index 412a8b71235..db23a27ec50 100644 --- a/module/dkg.go +++ b/module/dkg.go @@ -33,11 +33,11 @@ type DKGContractClient interface { ReadBroadcast(fromIndex uint, referenceBlock flow.Identifier) ([]messages.BroadcastDKGMessage, error) // SubmitParametersAndResult posts the DKG setup parameters (`flow.DKGIndexMap`) and the node's locally-computed DKG result to - // the DKG white-board smart contract. The DKG result are the group public key and the node's local computation of the public - // keys for each DKG participant. Serialized public keys are encoded as hex. - // Conceptually the flow.DKGIndexMap is not and output of the DKG protocol. Rather, it is part of the configuration/initialization + // the DKG white-board smart contract. The DKG results are the node's local computation of the group public key and the public + // key shares. Serialized public keys are encoded as lower-case hex strings. + // Conceptually the flow.DKGIndexMap is not an output of the DKG protocol. Rather, it is part of the configuration/initialization // information of the DKG. Before an epoch transition on the happy path (using the data in the EpochSetup event), each consensus - // participant locally fixes the DKG committee π’Ÿ including the order of the respective nodes order to be identical to the consensus + // participant locally fixes the DKG committee π’Ÿ including the respective nodes' order to be identical to the consensus // committee π’ž. However, in case of a failed epoch transition, we desire the ability to manually provide the result of a successful // DKG for the immediately next epoch (so-called recovery epoch). The DKG committee π’Ÿ must have a sufficiently large overlap with // the recovery epoch's consensus committee π’ž -- though for flexibility, we do *not* want to require that both committees are identical. @@ -45,13 +45,15 @@ type DKGContractClient interface { // same also on the happy path. SubmitParametersAndResult(indexMap flow.DKGIndexMap, groupPublicKey crypto.PublicKey, publicKeys []crypto.PublicKey) error - // SubmitEmptyResult submits an empty result of the DKG protocol. The empty result is obtained by a node when - // it realizes locally that its DKG participation was unsuccessful (either because the DKG failed as a whole, - // or because the node received too many byzantine inputs). However, a node obtaining an empty result can - // happen in both cases of the DKG succeeding or failing. For further details, please see: + // SubmitEmptyResult submits an empty result of the DKG protocol. + // The empty result is obtained by a node when it realizes locally that its DKG participation + // was unsuccessful (possible reasons include: node received too many byzantine inputs; + // node has networking issues; locally computed key is invalid…). However, a node obtaining an + // empty result can happen in both cases of the DKG succeeding or failing globally. + // For further details, please see: // https://flowfoundation.notion.site/Random-Beacon-2d61f3b3ad6e40ee9f29a1a38a93c99c // Honest nodes would call `SubmitEmptyResult` strictly after the final phase has ended if DKG has ended. - // Though, `SubmitEmptyResult` also supports implementing byzantine participants for testing that submit an + // However, `SubmitEmptyResult` also supports implementing byzantine participants for testing that submit an // empty result too early (intentional protocol violation), *before* the final DKG phase concluded. SubmitEmptyResult() error } diff --git a/module/dkg/client.go b/module/dkg/client.go index f20b391a836..42865ebeef2 100644 --- a/module/dkg/client.go +++ b/module/dkg/client.go @@ -181,11 +181,11 @@ func (c *Client) Broadcast(msg model.BroadcastDKGMessage) error { } // SubmitParametersAndResult posts the DKG setup parameters (`flow.DKGIndexMap`) and the node's locally-computed DKG result to -// the DKG white-board smart contract. The DKG result are the group public key and the node's local computation of the public -// keys for each DKG participant. Serialized public keys are encoded as hex. -// Conceptually the flow.DKGIndexMap is not and output of the DKG protocol. Rather, it is part of the configuration/initialization +// the DKG white-board smart contract. The DKG results are the node's local computation of the group public key and the public +// key shares. Serialized public keys are encoded as lower-case hex strings. +// Conceptually the flow.DKGIndexMap is not an output of the DKG protocol. Rather, it is part of the configuration/initialization // information of the DKG. Before an epoch transition on the happy path (using the data in the EpochSetup event), each consensus -// participant locally fixes the DKG committee π’Ÿ including the order of the respective nodes order to be identical to the consensus +// participant locally fixes the DKG committee π’Ÿ including the respective nodes' order to be identical to the consensus // committee π’ž. However, in case of a failed epoch transition, we desire the ability to manually provide the result of a successful // DKG for the immediately next epoch (so-called recovery epoch). The DKG committee π’Ÿ must have a sufficiently large overlap with // the recovery epoch's consensus committee π’ž -- though for flexibility, we do *not* want to require that both committees are identical. diff --git a/module/signature/signing_tags.go b/module/signature/signing_tags.go index f2d142b4253..00d7e06903c 100644 --- a/module/signature/signing_tags.go +++ b/module/signature/signing_tags.go @@ -61,7 +61,7 @@ var ( // NewBLSHasher returns a hasher to be used for BLS signing and verifying // in the protocol and abstracts the hasher details from the protocol logic. // -// The hasher returned is the the expand-message step in the BLS hash-to-curve. +// The hasher returned is the expand-message step in the BLS hash-to-curve. // It uses a xof (extendable output function) based on KMAC128. It therefore has // 128-bytes outputs. func NewBLSHasher(tag string) hash.Hasher { diff --git a/state/protocol/badger/mutator_test.go b/state/protocol/badger/mutator_test.go index 69eb60daf38..220ad8901bf 100644 --- a/state/protocol/badger/mutator_test.go +++ b/state/protocol/badger/mutator_test.go @@ -1653,7 +1653,7 @@ func TestExtendEpochCommitInvalid(t *testing.T) { unittest.InsertAndFinalize(t, state, block3) _, receipt, seal := createCommit(block3, func(commit *flow.EpochCommit) { - // add an extra dkg key + // add an extra Random Beacon key commit.DKGParticipantKeys = append(commit.DKGParticipantKeys, unittest.KeyFixture(crypto.BLSBLS12381).PublicKey()) }) diff --git a/state/protocol/defaults.go b/state/protocol/defaults.go index 72d08f1cdce..6faaaa61276 100644 --- a/state/protocol/defaults.go +++ b/state/protocol/defaults.go @@ -32,15 +32,23 @@ func DefaultEpochSafetyParams(chain flow.ChainID) (SafetyParams, error) { } // RandomBeaconSafetyThreshold defines a production network safety threshold for random beacon protocol based on the size -// of the DKG committee π’Ÿ which is a subset of consensus committee π’ž. +// of the random beacon committee β„› and the DKG committee π’Ÿ. +// +// We recall that the committee β„› is defined as the subset of the consensus committee (β„› βŠ† π’ž) and the DKG +// committee (β„› βŠ† π’Ÿ) that _successfully_ completed the DKG and is able to contribute with a random beacon share. +// // An honest supermajority of consensus nodes must contain enough successful DKG participants -// (about |π’Ÿ|/2) to produce a valid group signature for the random beacon [1, 3]. Therefore, we have the approximate -// lower bound |π’Ÿ|/2. This is a lower bound, unsuited for decentralized production networks. +// (about |π’Ÿ|/2 + 1) to produce a valid group signature for the random beacon at each block [1, 3]. +// Therefore, we have the approximate lower bound |β„›| ≳ n/2 + 1 = |π’Ÿ|/2 + 1 = len(DKGIndexMap)/2 + 1. +// Operating close to this lower bound would require that every random beacon key-holder Ο± ∈ β„› remaining in the consensus committee is honest +// (incl. quickly responsive) *all the time*. Such a reliability assumption is unsuited for decentralized production networks. // To reject configurations that are vulnerable to liveness failures, the protocol uses the threshold `t_safety` -// (heuristic, see [2]), which is implemented on the smart contract level. In a nutshell, the cardinality of intersection π’Ÿ ∩ π’ž -// (wrt both sets π’Ÿ ∩ π’ž) should be well above 70%, values in the range 70-62% should be considered for short-term -// recovery cases. Values of 62% or lower are not recommended for any -// production network, as single-node crashes are already enough to halt consensus. +// (heuristic, see [2]), which is implemented on the smart contract level. +// Ideally, |β„›| and therefore |π’Ÿ ∩ π’ž| (given that |β„›| <= |π’Ÿ ∩ π’ž|) should be well above 70% . |π’Ÿ|. +// Values in the range 70%-62% of |π’Ÿ| should be considered for short-term recovery cases. +// Values of 62% * |π’Ÿ| or lower (i.e. |β„›| ≀ 0.62Β·|π’Ÿ|) are not recommended for any +// production network, as single-node crashes may already be enough to halt consensus. +// // For further details, see // - godoc for [flow.DKGIndexMap] // - [1] https://www.notion.so/flowfoundation/Threshold-Signatures-7e26c6dd46ae40f7a83689ba75a785e3?pvs=4 diff --git a/state/protocol/inmem/encodable.go b/state/protocol/inmem/encodable.go index 3c78775860c..d6a9a8c0cf1 100644 --- a/state/protocol/inmem/encodable.go +++ b/state/protocol/inmem/encodable.go @@ -86,10 +86,23 @@ func (snap EncodableSnapshot) LatestSealedResult() (*flow.ExecutionResult, error return nil, fmt.Errorf("LatestSealedResult: unreachable for correctly formatted sealing segments") } -type EncodableFullDKG struct { - GroupKey encodable.RandomBeaconPubKey - PrivKeyShares []encodable.RandomBeaconPrivKey - PubKeyShares []encodable.RandomBeaconPubKey +// ThresholdKeySet contains the key set for a threshold signature scheme. Typically, the ThresholdKeySet is used to +// encode the output of a trusted setup. In general, signature scheme is configured with a threshold parameter t, +// which is the number of malicious colluding nodes the signature scheme is safe against. To balance liveness and +// safety, the Flow protocol fixes threshold to t = floor((n-1)/2), for n the number of parties in the threshold +// cryptography scheme, specifically n = len(Participants). +// Without loss of generality, our threshold cryptography protocol with n parties identifies the individual +// participants by the indices {0, 1, …, n-1}. The slice Participants is ordered accordingly. +type ThresholdKeySet struct { + GroupKey encodable.RandomBeaconPubKey + Participants []ThresholdParticipant +} + +// ThresholdParticipant encodes the threshold key data for single participant. +type ThresholdParticipant struct { + PrivKeyShare encodable.RandomBeaconPrivKey + PubKeyShare encodable.RandomBeaconPubKey + NodeID flow.Identifier } // EncodableCluster is the encoding format for protocol.Cluster diff --git a/state/protocol/prg/prg.go b/state/protocol/prg/prg.go index 36b3b77751d..d17fd1a9ac0 100644 --- a/state/protocol/prg/prg.go +++ b/state/protocol/prg/prg.go @@ -17,7 +17,7 @@ const RandomSourceLength = crypto.SignatureLenBLSBLS12381 // The diversifier is used to further diversify the PRGs beyond the customizer. A diversifier // can be a slice of any length. If no diversification is needed, `diversifier` can be `nil`. // -// The function uses an extendable-output function (xof) to extract and expand the the input source, +// The function uses an extendable-output function (xof) to extract and expand the input source, // so that any source with enough entropy (at least 128 bits) can be used (no need to pre-hash). // Current implementation generates a ChaCha20-based CSPRG. // diff --git a/state/protocol/validity.go b/state/protocol/validity.go index 47f059073a8..8704a8290b7 100644 --- a/state/protocol/validity.go +++ b/state/protocol/validity.go @@ -163,7 +163,7 @@ func IsValidEpochCommit(commit *flow.EpochCommit, setup *flow.EpochSetup) error return NewInvalidServiceEventErrorf("inconsistent epoch counter between commit (%d) and setup (%d) events in same epoch", commit.Counter, setup.Counter) } - // make sure we have a valid DKG public key + // make sure we have a Random Beacon group key: if commit.DKGGroupKey == nil { return NewInvalidServiceEventErrorf("missing DKG public group key") } @@ -171,7 +171,7 @@ func IsValidEpochCommit(commit *flow.EpochCommit, setup *flow.EpochSetup) error // enforce invariant: len(DKGParticipantKeys) == len(DKGIndexMap) n := len(commit.DKGIndexMap) // size of the DKG committee if len(commit.DKGParticipantKeys) != n { - return NewInvalidServiceEventErrorf("dkg key list (len=%d) does not match index map (len=%d)", len(commit.DKGParticipantKeys), len(commit.DKGIndexMap)) + return NewInvalidServiceEventErrorf("number of %d Random Beacon key shares is inconsistent with number of DKG participatns (len=%d)", len(commit.DKGParticipantKeys), len(commit.DKGIndexMap)) } // enforce invariant: DKGIndexMap values form the set {0, 1, ..., n-1} where n=len(DKGParticipantKeys) @@ -192,9 +192,25 @@ func IsValidEpochCommit(commit *flow.EpochCommit, setup *flow.EpochSetup) error numberOfRandomBeaconParticipants++ } } - // enforce invariant: RandomBeaconSafetyThreshold ≀ |π’ž ∩ π’Ÿ| where: + // Important SANITY CHECK: reject configurations where too few consensus nodes have valid random beacon key shares to + // reliably reach the required threshold of signers. Specifically, we enforce RandomBeaconSafetyThreshold ≀ |π’ž ∩ π’Ÿ|. // - π’ž is the set of all consensus committee members // - π’Ÿ is the set of all DKG participants + // - β„› is the subset of the consensus committee (β„› βŠ† π’ž): it contains consensus nodes (and only those) with a + // private Random Beacon key share matching the respective public key share in the `EpochCommit` event. + // + // This is only a sanity check: on the protocol level, we only know which nodes (set π’Ÿ) could participate in the DKG, + // but not which consensus nodes obtained a *valid* random beacon key share. In other words, we only have access to the + // superset π’Ÿ ∩ π’ž βŠ‡ β„› here. If π’Ÿ ∩ π’ž is already too small, we are certain that too few consensus nodes have valid random + // beacon keys (RandomBeaconSafetyThreshold > |π’ž ∩ π’Ÿ| entails RandomBeaconSafetyThreshold > |β„›|) and we reject the + // Epoch configuration. However, enough nodes in the superset |π’ž ∩ π’Ÿ| does not guarantee that |β„›| is above the critical + // threshold (e.g. too many nodes |π’ž ∩ π’Ÿ| could have failed the DKG and therefore not be in β„›). + // + // This is different than the check in the DKG smart contract, where the value of |β„›| is known and compared + // to the threshold. Unlike the DKG contract, the protocol state does not have access to the value of |β„›| from a past + // key generation (decentralized or not). + // + // [2] https://www.notion.so/flowfoundation/DKG-contract-success-threshold-86c6bf2b92034855b3c185d7616eb6f1?pvs=4 if RandomBeaconSafetyThreshold(uint(n)) > numberOfRandomBeaconParticipants { return NewInvalidServiceEventErrorf("not enough random beacon participants required %d, got %d", signature.RandomBeaconThreshold(n), numberOfRandomBeaconParticipants)