github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/state/protocol/validity.go (about)

     1  package protocol
     2  
     3  import (
     4  	"fmt"
     5  
     6  	"github.com/onflow/flow-go/model/flow"
     7  	"github.com/onflow/flow-go/model/flow/factory"
     8  	"github.com/onflow/flow-go/model/flow/filter"
     9  )
    10  
    11  // IsValidExtendingEpochSetup checks whether an EpochSetup service event being added to the state is valid.
    12  // In addition to intrinsic validity, we also check that it is valid w.r.t. the previous epoch setup event,
    13  // and the current epoch status.
    14  // CAUTION: This function assumes that all inputs besides extendingCommit are already validated.
    15  // Expected errors during normal operations:
    16  // * protocol.InvalidServiceEventError if the input service event is invalid to extend the currently active epoch status
    17  func IsValidExtendingEpochSetup(extendingSetup *flow.EpochSetup, protocolStateEntry *flow.ProtocolStateEntry, currentEpochSetupEvent *flow.EpochSetup) error {
    18  	// Enforce EpochSetup is valid w.r.t to current epoch state
    19  	if protocolStateEntry.NextEpoch != nil { // We should only have a single epoch setup event per epoch.
    20  		// true iff EpochSetup event for NEXT epoch was already included before
    21  		return NewInvalidServiceEventErrorf("duplicate epoch setup service event: %x", protocolStateEntry.NextEpoch.SetupID)
    22  	}
    23  	if extendingSetup.Counter != currentEpochSetupEvent.Counter+1 { // The setup event should have the counter increased by one.
    24  		return NewInvalidServiceEventErrorf("next epoch setup has invalid counter (%d => %d)", currentEpochSetupEvent.Counter, extendingSetup.Counter)
    25  	}
    26  	if extendingSetup.FirstView != currentEpochSetupEvent.FinalView+1 { // The first view needs to be exactly one greater than the current epoch final view
    27  		return NewInvalidServiceEventErrorf(
    28  			"next epoch first view must be exactly 1 more than current epoch final view (%d != %d+1)",
    29  			extendingSetup.FirstView,
    30  			currentEpochSetupEvent.FinalView,
    31  		)
    32  	}
    33  
    34  	// Enforce the EpochSetup event is syntactically correct
    35  	err := IsValidEpochSetup(extendingSetup, true)
    36  	if err != nil {
    37  		return NewInvalidServiceEventErrorf("invalid epoch setup: %w", err)
    38  	}
    39  	return nil
    40  }
    41  
    42  // IsValidEpochSetup checks whether an `EpochSetup` event is syntactically correct. The boolean parameter `verifyNetworkAddress`
    43  // controls, whether we want to permit nodes to share a networking address.
    44  // This is a side-effect-free function. Any error return indicates that the EpochSetup event is not compliant with protocol rules.
    45  func IsValidEpochSetup(setup *flow.EpochSetup, verifyNetworkAddress bool) error {
    46  	// 1. CHECK: Enforce protocol compliance of Epoch parameters:
    47  	// - RandomSource of entropy in Epoch Setup event should the protocol-prescribed length
    48  	// - first view must be before final view
    49  	if len(setup.RandomSource) != flow.EpochSetupRandomSourceLength {
    50  		return fmt.Errorf("seed has incorrect length (%d != %d)", len(setup.RandomSource), flow.EpochSetupRandomSourceLength)
    51  	}
    52  	if setup.FirstView >= setup.FinalView {
    53  		return fmt.Errorf("first view (%d) must be before final view (%d)", setup.FirstView, setup.FinalView)
    54  	}
    55  
    56  	// 2. CHECK: Enforce protocol compliance active participants:
    57  	// (a) each has a unique node ID,
    58  	// (b) each has a unique network address (if `verifyNetworkAddress` is true),
    59  	// (c) participants are sorted in canonical order.
    60  	//     Note that the system smart contracts manage the identity table as an unordered set! For the protocol state, we desire a fixed
    61  	//     ordering to simplify various implementation details, like the DKG. Therefore, we order identities in `flow.EpochSetup` during
    62  	//     conversion from cadence to Go in the function `convert.ServiceEvent(flow.ChainID, flow.Event)` in package `model/convert`
    63  	identLookup := make(map[flow.Identifier]struct{})
    64  	for _, participant := range setup.Participants { // (a) enforce uniqueness of NodeIDs
    65  		_, ok := identLookup[participant.NodeID]
    66  		if ok {
    67  			return fmt.Errorf("duplicate node identifier (%x)", participant.NodeID)
    68  		}
    69  		identLookup[participant.NodeID] = struct{}{}
    70  	}
    71  
    72  	if verifyNetworkAddress { // (b) enforce uniqueness of networking address
    73  		addrLookup := make(map[string]struct{})
    74  		for _, participant := range setup.Participants {
    75  			_, ok := addrLookup[participant.Address]
    76  			if ok {
    77  				return fmt.Errorf("duplicate node address (%x)", participant.Address)
    78  			}
    79  			addrLookup[participant.Address] = struct{}{}
    80  		}
    81  	}
    82  
    83  	if !setup.Participants.Sorted(flow.Canonical[flow.IdentitySkeleton]) { // (c) enforce canonical ordering
    84  		return fmt.Errorf("participants are not canonically ordered")
    85  	}
    86  
    87  	// 3. CHECK: Enforce sufficient number of nodes for each role
    88  	// IMPORTANT: here we remove all nodes with zero weight, as they are allowed to partake in communication but not in respective node functions
    89  	activeParticipants := setup.Participants.Filter(filter.HasInitialWeight[flow.IdentitySkeleton](true))
    90  	activeNodeCountByRole := make(map[flow.Role]uint)
    91  	for _, participant := range activeParticipants {
    92  		activeNodeCountByRole[participant.Role]++
    93  	}
    94  	if activeNodeCountByRole[flow.RoleConsensus] < 1 {
    95  		return fmt.Errorf("need at least one consensus node")
    96  	}
    97  	if activeNodeCountByRole[flow.RoleCollection] < 1 {
    98  		return fmt.Errorf("need at least one collection node")
    99  	}
   100  	if activeNodeCountByRole[flow.RoleExecution] < 1 {
   101  		return fmt.Errorf("need at least one execution node")
   102  	}
   103  	if activeNodeCountByRole[flow.RoleVerification] < 1 {
   104  		return fmt.Errorf("need at least one verification node")
   105  	}
   106  
   107  	// 4. CHECK: Enforce protocol compliance of collector cluster assignment
   108  	//   (0) there is at least one collector cluster
   109  	//   (a) assignment only contains nodes with collector role and positive weight
   110  	//   (b) collectors have unique node IDs
   111  	//   (c) each collector is assigned exactly to one cluster and is only listed once within that cluster
   112  	//   (d) cluster contains at least one collector (i.e. is not empty)
   113  	//   (e) cluster is composed of known nodes
   114  	//   (f) cluster assignment lists the nodes in canonical ordering
   115  	if len(setup.Assignments) == 0 { // enforce (0): at least one cluster
   116  		return fmt.Errorf("need at least one collection cluster")
   117  	}
   118  	// Unpacking the cluster assignments (NodeIDs → IdentitySkeletons) enforces (a) - (f)
   119  	_, err := factory.NewClusterList(setup.Assignments, activeParticipants.Filter(filter.HasRole[flow.IdentitySkeleton](flow.RoleCollection)))
   120  	if err != nil {
   121  		return fmt.Errorf("invalid cluster assignments: %w", err)
   122  	}
   123  	return nil
   124  }
   125  
   126  // IsValidExtendingEpochCommit checks whether an EpochCommit service event being added to the state is valid.
   127  // In addition to intrinsic validity, we also check that it is valid w.r.t. the previous epoch setup event, and
   128  // the current epoch status.
   129  // CAUTION: This function assumes that all inputs besides extendingCommit are already validated.
   130  // Expected errors during normal operations:
   131  // * protocol.InvalidServiceEventError if the input service event is invalid to extend the currently active epoch
   132  func IsValidExtendingEpochCommit(extendingCommit *flow.EpochCommit, protocolStateEntry *flow.ProtocolStateEntry, nextEpochSetupEvent *flow.EpochSetup) error {
   133  	// The epoch setup event needs to happen before the commit.
   134  	if protocolStateEntry.NextEpoch == nil {
   135  		return NewInvalidServiceEventErrorf("missing epoch setup for epoch commit")
   136  	}
   137  	// Enforce EpochSetup is valid w.r.t to current epoch state
   138  	if protocolStateEntry.NextEpoch.CommitID != flow.ZeroID { // We should only have a single epoch commit event per epoch.
   139  		return NewInvalidServiceEventErrorf("duplicate epoch commit service event: %x", protocolStateEntry.NextEpoch.CommitID)
   140  	}
   141  	// Enforce the EpochSetup event is syntactically correct and compatible with the respective EpochSetup
   142  	err := IsValidEpochCommit(extendingCommit, nextEpochSetupEvent)
   143  	if err != nil {
   144  		return NewInvalidServiceEventErrorf("invalid epoch commit: %s", err)
   145  	}
   146  	return nil
   147  }
   148  
   149  // IsValidEpochCommit checks whether an epoch commit service event is intrinsically valid.
   150  // Assumes the input flow.EpochSetup event has already been validated.
   151  // Expected errors during normal operations:
   152  // * protocol.InvalidServiceEventError if the EpochCommit is invalid
   153  func IsValidEpochCommit(commit *flow.EpochCommit, setup *flow.EpochSetup) error {
   154  	if len(setup.Assignments) != len(commit.ClusterQCs) {
   155  		return NewInvalidServiceEventErrorf("number of clusters (%d) does not number of QCs (%d)", len(setup.Assignments), len(commit.ClusterQCs))
   156  	}
   157  
   158  	if commit.Counter != setup.Counter {
   159  		return NewInvalidServiceEventErrorf("inconsistent epoch counter between commit (%d) and setup (%d) events in same epoch", commit.Counter, setup.Counter)
   160  	}
   161  
   162  	// make sure we have a valid DKG public key
   163  	if commit.DKGGroupKey == nil {
   164  		return NewInvalidServiceEventErrorf("missing DKG public group key")
   165  	}
   166  
   167  	participants := setup.Participants.Filter(filter.IsValidDKGParticipant)
   168  	if len(participants) != len(commit.DKGParticipantKeys) {
   169  		return NewInvalidServiceEventErrorf("participant list (len=%d) does not match dkg key list (len=%d)", len(participants), len(commit.DKGParticipantKeys))
   170  	}
   171  	return nil
   172  }