diff --git a/cmd/bootstrap/run/qc.go b/cmd/bootstrap/run/qc.go index 958c1ab911c..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 @@ -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 bbc8b0a6935..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) 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/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/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)