github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/consensus/hotstuff/committees/cluster_committee.go (about) 1 package committees 2 3 import ( 4 "fmt" 5 6 "github.com/onflow/flow-go/consensus/hotstuff" 7 "github.com/onflow/flow-go/consensus/hotstuff/committees/leader" 8 "github.com/onflow/flow-go/consensus/hotstuff/model" 9 "github.com/onflow/flow-go/model/flow" 10 "github.com/onflow/flow-go/model/flow/filter" 11 "github.com/onflow/flow-go/state/protocol" 12 "github.com/onflow/flow-go/storage" 13 ) 14 15 // Cluster represents the committee for a cluster of collection nodes. Cluster 16 // committees are epoch-scoped. 17 // 18 // Clusters build blocks on a cluster chain but must obtain identity table 19 // information from the main chain. Thus, block ID parameters in this DynamicCommittee 20 // implementation reference blocks on the cluster chain, which in turn reference 21 // blocks on the main chain - this implementation manages that translation. 22 type Cluster struct { 23 state protocol.State 24 payloads storage.ClusterPayloads 25 me flow.Identifier 26 selection *leader.LeaderSelection // pre-computed leader selection for the full lifecycle of the cluster 27 28 clusterMembers flow.IdentitySkeletonList // cluster members in canonical order as specified by the epoch smart contract 29 clusterMemberFilter flow.IdentityFilter[flow.Identity] // filter that returns true for all members of the cluster committee allowed to vote 30 weightThresholdForQC uint64 // computed based on initial cluster committee weights 31 weightThresholdForTO uint64 // computed based on initial cluster committee weights 32 33 // initialClusterIdentities lists full Identities for cluster members (in canonical order) at time of cluster initialization by Epoch smart contract 34 initialClusterIdentities flow.IdentityList 35 } 36 37 var _ hotstuff.Replicas = (*Cluster)(nil) 38 var _ hotstuff.DynamicCommittee = (*Cluster)(nil) 39 40 func NewClusterCommittee( 41 state protocol.State, 42 payloads storage.ClusterPayloads, 43 cluster protocol.Cluster, 44 epoch protocol.Epoch, 45 me flow.Identifier, 46 ) (*Cluster, error) { 47 selection, err := leader.SelectionForCluster(cluster, epoch) 48 if err != nil { 49 return nil, fmt.Errorf("could not compute leader selection for cluster: %w", err) 50 } 51 52 initialClusterIdentities := votingClusterParticipants(cluster.Members()) // drops nodes with `InitialWeight=0` 53 initialClusterMembersSelector := initialClusterIdentities.Selector() // hence, any node accepted by this selector has `InitialWeight>0` 54 totalWeight := initialClusterIdentities.TotalWeight() 55 56 com := &Cluster{ 57 state: state, 58 payloads: payloads, 59 me: me, 60 selection: selection, 61 clusterMemberFilter: filter.And[flow.Identity]( 62 initialClusterMembersSelector, 63 filter.IsValidCurrentEpochParticipant, 64 ), 65 clusterMembers: initialClusterIdentities.ToSkeleton(), 66 initialClusterIdentities: initialClusterIdentities, 67 weightThresholdForQC: WeightThresholdToBuildQC(totalWeight), 68 weightThresholdForTO: WeightThresholdToTimeout(totalWeight), 69 } 70 return com, nil 71 } 72 73 // IdentitiesByBlock returns the identities of all cluster members that are authorized to 74 // participate at the given block. The order of the identities is the canonical order. 75 func (c *Cluster) IdentitiesByBlock(blockID flow.Identifier) (flow.IdentityList, error) { 76 // blockID is a collection block not a block produced by consensus, 77 // to query the identities from protocol state, we need to use the reference block id from the payload 78 // 79 // first retrieve the cluster block's payload 80 payload, err := c.payloads.ByBlockID(blockID) 81 if err != nil { 82 return nil, fmt.Errorf("could not get cluster payload: %w", err) 83 } 84 85 // An empty reference block ID indicates a root block. In this case, use the initial cluster members for root block 86 if isRootBlock := payload.ReferenceBlockID == flow.ZeroID; isRootBlock { 87 return c.initialClusterIdentities, nil 88 } 89 90 // otherwise use the snapshot given by the reference block 91 identities, err := c.state.AtBlockID(payload.ReferenceBlockID).Identities(c.clusterMemberFilter) 92 return identities, err 93 } 94 95 func (c *Cluster) IdentityByBlock(blockID flow.Identifier, nodeID flow.Identifier) (*flow.Identity, error) { 96 // first retrieve the cluster block's payload 97 payload, err := c.payloads.ByBlockID(blockID) 98 if err != nil { 99 return nil, fmt.Errorf("could not get cluster payload: %w", err) 100 } 101 102 // An empty reference block ID indicates a root block. In this case, use the initial cluster members for root block 103 if isRootBlock := payload.ReferenceBlockID == flow.ZeroID; isRootBlock { 104 identity, ok := c.initialClusterIdentities.ByNodeID(nodeID) 105 if !ok { 106 return nil, model.NewInvalidSignerErrorf("node %v is not an authorized hotstuff participant", nodeID) 107 } 108 return identity, nil 109 } 110 111 // otherwise use the snapshot given by the reference block 112 identity, err := c.state.AtBlockID(payload.ReferenceBlockID).Identity(nodeID) 113 if protocol.IsIdentityNotFound(err) { 114 return nil, model.NewInvalidSignerErrorf("%v is not a valid node id at block %v: %w", nodeID, payload.ReferenceBlockID, err) 115 } 116 if err != nil { 117 return nil, fmt.Errorf("could not get identity for node (id=%x): %w", nodeID, err) 118 } 119 if !c.clusterMemberFilter(identity) { 120 return nil, model.NewInvalidSignerErrorf("node %v is not an authorized hotstuff cluster member", nodeID) 121 } 122 return identity, nil 123 } 124 125 // IdentitiesByEpoch returns the IdentitySkeletons of the cluster members in canonical order. 126 // This represents the cluster composition at the time the cluster was specified by the epoch smart 127 // contract (hence, we return IdentitySkeletons as opposed to full identities). Since clusters only 128 // exist for one epoch, we don't need to check the view. 129 func (c *Cluster) IdentitiesByEpoch(_ uint64) (flow.IdentitySkeletonList, error) { 130 return c.clusterMembers, nil 131 } 132 133 // IdentityByEpoch returns the node from the initial cluster members for this epoch. 134 // The view parameter is the view in the cluster consensus. Since clusters only exist 135 // for one epoch, we don't need to check the view. 136 // 137 // Returns: 138 // - model.InvalidSignerError if nodeID was not listed by the Epoch Setup event as an 139 // authorized participant in this cluster 140 func (c *Cluster) IdentityByEpoch(view uint64, participantID flow.Identifier) (*flow.IdentitySkeleton, error) { 141 identity, ok := c.clusterMembers.ByNodeID(participantID) 142 if !ok { 143 return nil, model.NewInvalidSignerErrorf("node %v is not an authorized hotstuff participant", participantID) 144 } 145 return identity, nil 146 } 147 148 func (c *Cluster) LeaderForView(view uint64) (flow.Identifier, error) { 149 return c.selection.LeaderForView(view) 150 } 151 152 // QuorumThresholdForView returns the weight threshold required to build a QC 153 // for the given view. The view parameter is the view in the cluster consensus. 154 // Since clusters only exist for one epoch, and the weight threshold is static 155 // over the course of an epoch, we don't need to check the view. 156 // 157 // No errors are expected during normal operation. 158 func (c *Cluster) QuorumThresholdForView(_ uint64) (uint64, error) { 159 return c.weightThresholdForQC, nil 160 } 161 162 // TimeoutThresholdForView returns the minimum weight of observed timeout objects to 163 // safely immediately timeout for the current view. The view parameter is the view 164 // in the cluster consensus. Since clusters only exist for one epoch, and the weight 165 // threshold is static over the course of an epoch, we don't need to check the view. 166 // 167 // No errors are expected during normal operation. 168 func (c *Cluster) TimeoutThresholdForView(_ uint64) (uint64, error) { 169 return c.weightThresholdForTO, nil 170 } 171 172 func (c *Cluster) Self() flow.Identifier { 173 return c.me 174 } 175 176 func (c *Cluster) DKG(_ uint64) (hotstuff.DKG, error) { 177 panic("queried DKG of cluster committee") 178 } 179 180 // votingClusterParticipants extends the IdentitySkeletons of the cluster members to their full Identities 181 // at the time of cluster initialization by EpochSetup event. 182 // IMPORTANT CONVENTIONS: 183 // 1. clusterMembers with zero `InitialWeight` are _not included_ as "contributing" cluster participants. 184 // In accordance with their zero weight, they cannot contribute to advancing the cluster consensus. 185 // For example, the consensus leader selection allows zero-weighted nodes among the weighted participants, 186 // but these nodes have zero probability to be selected as leader. Similarly, they cannot meaningfully contribute 187 // votes or Timeouts to QCs or TC, due to their zero weight. Therefore, we do not consider them a valid signer. 188 // 2. This operation maintains the relative order. In other words, if `clusterMembers` is in canonical order, 189 // then the output `IdentityList` is also canonically ordered. 190 // 191 // CONTEXT: The EpochSetup event contains the IdentitySkeletons for each cluster, thereby specifying cluster membership. 192 // While ejection status is not part of the EpochSetup event, we can supplement this information as follows: 193 // - Per convention, service events are delivered (asynchronously) in an *order-preserving* manner. Furthermore, 194 // node ejection is also mediated by system smart contracts and delivered via service events. 195 // - Therefore, the EpochSetup event contains the up-to-date snapshot of the cluster members. Any node ejection 196 // that happened before should be reflected in the EpochSetup event. Specifically, ejected nodes 197 // should be no longer listed in the EpochSetup event. Hence, when the EpochSetup event is emitted / processed, 198 // the participation status of all cluster members equals flow.EpochParticipationStatusActive. 199 func votingClusterParticipants(clusterMembers flow.IdentitySkeletonList) flow.IdentityList { 200 initialClusterIdentities := make(flow.IdentityList, 0, len(clusterMembers)) 201 for _, skeleton := range clusterMembers { 202 if skeleton.InitialWeight == 0 { 203 continue 204 } 205 initialClusterIdentities = append(initialClusterIdentities, &flow.Identity{ 206 IdentitySkeleton: *skeleton, 207 DynamicIdentity: flow.DynamicIdentity{ 208 EpochParticipationStatus: flow.EpochParticipationStatusActive, 209 }, 210 }) 211 } 212 return initialClusterIdentities 213 }