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  }