diff --git a/cmd/bootstrap/run/qc.go b/cmd/bootstrap/run/qc.go index 6f6e304b159..86010ad95d4 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/model/convert/service_event.go b/model/convert/service_event.go index f9e9e51e860..1420c5cbd93 100644 --- a/model/convert/service_event.go +++ b/model/convert/service_event.go @@ -972,7 +972,7 @@ 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) } @@ -992,11 +992,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("could not decode hex into bytes: %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 00a610eb610..da31566c98b 100644 --- a/model/flow/dkg.go +++ b/model/flow/dkg.go @@ -31,51 +31,64 @@ 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. +// DKGIndexMap completely describes the DKG committee ๐’Ÿ of size |๐’Ÿ| = n. +// 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 DKG indices when +// random beacon signature shares. Hence, the protocol level needs to map nodeIDs to the indices when // calling into the lower-level cryptographic primitives. // // Formal specification: -// - DKGIndexMap completely describes the DKG committee. If there were n parties authorized to participate +// - DKGIndexMap completely describes the DKG committee. 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}. +// - The values in DKGIndexMap must form the set {0, 1, โ€ฆ, n-1}, as required by the low level cryptography +// module. // -// CAUTION: It is important to cleanly differentiate between the consensus committee ๐’ž, the random beacon -// committee โ„› and the DKG committee ๐’Ÿ: +// 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 +// - The DKG committee ๐’Ÿ is the set of parties that were authorized to +// participate in the DKG. Mathematically, the DKGIndexMap is an bijective function // DKGIndexMap: ๐’Ÿ โ†ฆ {0,1,โ€ฆ,n-1}. +// - Only consensus nodes are allowed to contribute to the random beacon. We define the committee โ„› +// as the subset of the consensus committee (โ„› โŠ† ๐’ž), which _successfully_ completed the DKG, +// i.e which completed participation in DKG (โ„› โŠ† ๐’Ÿ) with a private key that matches the public key assigned to them by +// the DKG committee (or a trusted dealer). Nodes in โ„› are therefore 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 +// +// 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 n/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*. This is a lower bound, 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 +// (heuristic, see [2]), which is implemented on the smart contract level. +// In a nutshell, |โ„›| and therefore |๐’Ÿ โˆฉ ๐’ž| (given that |โ„›| <= |๐’Ÿ โˆฉ ๐’ž|) should be well above 70% * |๐’Ÿ| = 0.7 * n, +// 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 are already enough to halt consensus. // // For further details, see diff --git a/module/dkg.go b/module/dkg.go index 412a8b71235..456d19f424f 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,16 @@ 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 (for various reasons: because the node received too many byzantine inputs, + // because the node has networking issues, because a 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..ea67cede449 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/defaults.go b/state/protocol/defaults.go index 72d08f1cdce..06c52b996d5 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 (โ„› โŠ† ๐’Ÿ) which _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 n/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*. This is a lower bound, 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 +// (heuristic, see [2]), which is implemented on the smart contract level. +// In a nutshell, |โ„›| and therefore |๐’Ÿ โˆฉ ๐’ž| (given that |โ„›| <= |๐’Ÿ โˆฉ ๐’ž|) should be well above 70% * |๐’Ÿ| = 0.7 * n, +// 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 are already 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..984a1c1c285 100644 --- a/state/protocol/validity.go +++ b/state/protocol/validity.go @@ -195,6 +195,17 @@ func IsValidEpochCommit(commit *flow.EpochCommit, setup *flow.EpochSetup) error // enforce invariant: RandomBeaconSafetyThreshold โ‰ค |๐’ž โˆฉ ๐’Ÿ| where: // - ๐’ž is the set of all consensus committee members // - ๐’Ÿ is the set of all DKG participants + // + // Note that this is only a sanity check that makes sure the cardinality of โ„› โŠ† ๐’Ÿ โˆฉ ๐’ž does not go below the + // critical liveness threshold of 0.62 * |๐’Ÿ| (details in [2]). + // If RandomBeaconSafetyThreshold > |๐’ž โˆฉ ๐’Ÿ|, we are certain that RandomBeaconSafetyThreshold > |โ„›|. However, + // making sure that RandomBeaconSafetyThreshold <= |๐’ž โˆฉ ๐’Ÿ| does not prove that |โ„›| is above the critical threshold. + // + // This is different than the check implemented by the DKG 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)