Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Random beacon comments #6590

Merged
merged 10 commits into from
Dec 2, 2024
4 changes: 2 additions & 2 deletions cmd/bootstrap/run/qc.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion cmd/util/cmd/epochs/cmd/recover.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
6 changes: 3 additions & 3 deletions consensus/hotstuff/signature/randombeacon_signer_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion consensus/hotstuff/verification/combined_verifier_v3.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion consensus/integration/epoch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion consensus/integration/nodes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
4 changes: 2 additions & 2 deletions integration/testnet/network.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
18 changes: 9 additions & 9 deletions model/convert/service_event.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -986,22 +986,22 @@ 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) {
convertedKeys := make([]crypto.PublicKey, 0, len(cdcDKGKeys))
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) {
Expand All @@ -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)
tarakby marked this conversation as resolved.
Show resolved Hide resolved
}
return pubKey, nil
}
Expand Down
76 changes: 45 additions & 31 deletions model/flow/dkg.go
Original file line number Diff line number Diff line change
Expand Up @@ -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}.
tarakby marked this conversation as resolved.
Show resolved Hide resolved
//
// 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
Expand Down
Loading
Loading