github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/model/flow/protocol_state.go (about)

     1  package flow
     2  
     3  import (
     4  	"fmt"
     5  
     6  	"golang.org/x/exp/slices"
     7  )
     8  
     9  // DynamicIdentityEntry encapsulates nodeID and dynamic portion of identity.
    10  type DynamicIdentityEntry struct {
    11  	NodeID  Identifier
    12  	Ejected bool
    13  }
    14  
    15  type DynamicIdentityEntryList []*DynamicIdentityEntry
    16  
    17  // ProtocolStateEntry represents a snapshot of the identity table (incl. the set of all notes authorized to
    18  // be part of the network) at some point in time. It allows to reconstruct the state of identity table using
    19  // epoch setup events and dynamic identities. It tracks attempts of invalid state transitions.
    20  // It also holds information about the next epoch, if it has been already committed.
    21  // This structure is used to persist protocol state in the database.
    22  //
    23  // Note that the current implementation does not store the identity table directly. Instead, we store
    24  // the original events that constituted the _initial_ identity table at the beginning of the epoch
    25  // plus some modifiers. We intend to restructure this code soon.
    26  type ProtocolStateEntry struct {
    27  	PreviousEpoch *EpochStateContainer // minimal dynamic properties for previous epoch [optional, nil for first epoch after spork, genesis]
    28  	CurrentEpoch  EpochStateContainer  // minimal dynamic properties for current epoch
    29  	NextEpoch     *EpochStateContainer // minimal dynamic properties for next epoch [optional, nil iff we are in staking phase]
    30  
    31  	// InvalidEpochTransitionAttempted encodes whether an invalid epoch transition
    32  	// has been detected in this fork. Under normal operations, this value is false.
    33  	// Node-internally, the EpochFallback notification is emitted when a block is
    34  	// finalized that changes this flag from false to true.
    35  	//
    36  	// Currently, the only possible state transition is false → true.
    37  	// TODO for 'leaving Epoch Fallback via special service event'
    38  	InvalidEpochTransitionAttempted bool
    39  }
    40  
    41  // EpochStateContainer holds the data pertaining to a _single_ epoch but no information about
    42  // any adjacent epochs. To perform a transition from epoch N to N+1, EpochStateContainers for
    43  // both epochs are necessary.
    44  type EpochStateContainer struct {
    45  	// ID of setup event for this epoch, never nil.
    46  	SetupID Identifier
    47  	// ID of commit event for this epoch. Could be ZeroID if epoch was not committed.
    48  	CommitID Identifier
    49  	// ActiveIdentities contains the dynamic identity properties for the nodes that
    50  	// are active in this epoch. Active means that these nodes are authorized to contribute to
    51  	// extending the chain. Nodes are listed in `ActiveIdentities` if and only if
    52  	// they are part of the EpochSetup event for the respective epoch.
    53  	// The dynamic identity properties can change from block to block. Each non-deferred
    54  	// identity-mutating operation is applied independently to the `ActiveIdentities`
    55  	// of the relevant epoch's EpochStateContainer separately.
    56  	// Identities are always sorted in canonical order.
    57  	//
    58  	// Context: In comparison, nodes that are joining in the next epoch or left as of this
    59  	// epoch are only allowed to listen to the network but not actively contribute. Such
    60  	// nodes are _not_ part of `Identities`.
    61  	ActiveIdentities DynamicIdentityEntryList
    62  }
    63  
    64  // ID returns an identifier for this EpochStateContainer by hashing internal fields.
    65  // Per convention, the ID of a `nil` EpochStateContainer is `flow.ZeroID`.
    66  func (c *EpochStateContainer) ID() Identifier {
    67  	if c == nil {
    68  		return ZeroID
    69  	}
    70  	return MakeID(c)
    71  }
    72  
    73  // EventIDs returns the `flow.EventIDs` with the hashes of the EpochSetup and EpochCommit events.
    74  // Per convention, for a `nil` EpochStateContainer, we return `flow.ZeroID` for both events.
    75  func (c *EpochStateContainer) EventIDs() EventIDs {
    76  	if c == nil {
    77  		return EventIDs{ZeroID, ZeroID}
    78  	}
    79  	return EventIDs{c.SetupID, c.CommitID}
    80  }
    81  
    82  // Copy returns a full copy of the entry.
    83  // Embedded Identities are deep-copied, _except_ for their keys, which are copied by reference.
    84  // Per convention, the ID of a `nil` EpochStateContainer is `flow.ZeroID`.
    85  func (c *EpochStateContainer) Copy() *EpochStateContainer {
    86  	if c == nil {
    87  		return nil
    88  	}
    89  	return &EpochStateContainer{
    90  		SetupID:          c.SetupID,
    91  		CommitID:         c.CommitID,
    92  		ActiveIdentities: c.ActiveIdentities.Copy(),
    93  	}
    94  }
    95  
    96  // RichProtocolStateEntry is a ProtocolStateEntry which has additional fields that are cached
    97  // from storage layer for convenience.
    98  // Using this structure instead of ProtocolStateEntry allows us to avoid querying
    99  // the database for epoch setups and commits and full identity table.
   100  // It holds several invariants, such as:
   101  //   - CurrentEpochSetup and CurrentEpochCommit are for the same epoch. Never nil.
   102  //   - PreviousEpochSetup and PreviousEpochCommit are for the same epoch. Can be nil.
   103  //   - CurrentEpochIdentityTable is the full (dynamic) identity table for the current epoch.
   104  //     Identities are sorted in canonical order. Without duplicates. Never nil.
   105  //   - NextEpochIdentityTable is the full (dynamic) identity table for the next epoch. Can be nil.
   106  //
   107  // NOTE regarding `CurrentEpochIdentityTable` and `NextEpochIdentityTable`:
   108  // The Identity Table is generally a super-set of the identities listed in the Epoch
   109  // Service Events for the respective epoch. This is because the service events only list
   110  // nodes that are authorized to _actively_ contribute to extending the chain. In contrast,
   111  // the Identity Table additionally contains nodes (with weight zero) from the previous or
   112  // upcoming epoch, which are transitioning into / out of the network and are only allowed
   113  // to listen but not to actively contribute.
   114  type RichProtocolStateEntry struct {
   115  	*ProtocolStateEntry
   116  
   117  	PreviousEpochSetup        *EpochSetup
   118  	PreviousEpochCommit       *EpochCommit
   119  	CurrentEpochSetup         *EpochSetup
   120  	CurrentEpochCommit        *EpochCommit
   121  	NextEpochSetup            *EpochSetup
   122  	NextEpochCommit           *EpochCommit
   123  	CurrentEpochIdentityTable IdentityList
   124  	NextEpochIdentityTable    IdentityList
   125  }
   126  
   127  // NewRichProtocolStateEntry constructs a rich protocol state entry from a protocol state entry and additional data.
   128  // No errors are expected during normal operation. All errors indicate inconsistent or invalid inputs.
   129  func NewRichProtocolStateEntry(
   130  	protocolState *ProtocolStateEntry,
   131  	previousEpochSetup *EpochSetup,
   132  	previousEpochCommit *EpochCommit,
   133  	currentEpochSetup *EpochSetup,
   134  	currentEpochCommit *EpochCommit,
   135  	nextEpochSetup *EpochSetup,
   136  	nextEpochCommit *EpochCommit,
   137  ) (*RichProtocolStateEntry, error) {
   138  	result := &RichProtocolStateEntry{
   139  		ProtocolStateEntry:        protocolState,
   140  		PreviousEpochSetup:        previousEpochSetup,
   141  		PreviousEpochCommit:       previousEpochCommit,
   142  		CurrentEpochSetup:         currentEpochSetup,
   143  		CurrentEpochCommit:        currentEpochCommit,
   144  		NextEpochSetup:            nextEpochSetup,
   145  		NextEpochCommit:           nextEpochCommit,
   146  		CurrentEpochIdentityTable: IdentityList{},
   147  		NextEpochIdentityTable:    IdentityList{},
   148  	}
   149  
   150  	// If previous epoch is specified: ensure respective epoch service events are not nil and consistent with commitments in `ProtocolStateEntry.PreviousEpoch`
   151  	if protocolState.PreviousEpoch != nil {
   152  		if protocolState.PreviousEpoch.SetupID != previousEpochSetup.ID() { // calling ID() will panic is EpochSetup event is nil
   153  			return nil, fmt.Errorf("supplied previous epoch's setup event (%x) does not match commitment (%x) in ProtocolStateEntry", previousEpochSetup.ID(), protocolState.PreviousEpoch.SetupID)
   154  		}
   155  		if protocolState.PreviousEpoch.CommitID != previousEpochCommit.ID() { // calling ID() will panic is EpochCommit event is nil
   156  			return nil, fmt.Errorf("supplied previous epoch's commit event (%x) does not match commitment (%x) in ProtocolStateEntry", previousEpochCommit.ID(), protocolState.PreviousEpoch.CommitID)
   157  		}
   158  	}
   159  
   160  	// For current epoch: ensure respective epoch service events are not nil and consistent with commitments in `ProtocolStateEntry.CurrentEpoch`
   161  	if protocolState.CurrentEpoch.SetupID != currentEpochSetup.ID() { // calling ID() will panic is EpochSetup event is nil
   162  		return nil, fmt.Errorf("supplied current epoch's setup event (%x) does not match commitment (%x) in ProtocolStateEntry", currentEpochSetup.ID(), protocolState.CurrentEpoch.SetupID)
   163  	}
   164  	if protocolState.CurrentEpoch.CommitID != currentEpochCommit.ID() { // calling ID() will panic is EpochCommit event is nil
   165  		return nil, fmt.Errorf("supplied current epoch's commit event (%x) does not match commitment (%x) in ProtocolStateEntry", currentEpochCommit.ID(), protocolState.CurrentEpoch.CommitID)
   166  	}
   167  
   168  	// If we are in staking phase (i.e. protocolState.NextEpoch == nil):
   169  	//  (1) Full identity table contains active identities from current epoch.
   170  	//      If previous epoch exists, we add nodes from previous epoch that are leaving in the current epoch with `EpochParticipationStatusLeaving` status.
   171  	// Otherwise, we are in epoch setup or epoch commit phase (i.e. protocolState.NextEpoch ≠ nil):
   172  	//  (2a) Full identity table contains active identities from current epoch + nodes joining in next epoch with `EpochParticipationStatusJoining` status.
   173  	//  (2b) Furthermore, we also build the full identity table for the next epoch's staking phase:
   174  	//       active identities from next epoch + nodes from current epoch that are leaving at the end of the current epoch with `flow.EpochParticipationStatusLeaving` status.
   175  	var err error
   176  	nextEpoch := protocolState.NextEpoch
   177  	if nextEpoch == nil { // in staking phase: build full identity table for current epoch according to (1)
   178  		var previousEpochIdentitySkeletons IdentitySkeletonList
   179  		var previousEpochDynamicIdentities DynamicIdentityEntryList
   180  		if previousEpochSetup != nil {
   181  			previousEpochIdentitySkeletons = previousEpochSetup.Participants
   182  			previousEpochDynamicIdentities = protocolState.PreviousEpoch.ActiveIdentities
   183  		}
   184  		result.CurrentEpochIdentityTable, err = BuildIdentityTable(
   185  			currentEpochSetup.Participants,
   186  			protocolState.CurrentEpoch.ActiveIdentities,
   187  			previousEpochIdentitySkeletons,
   188  			previousEpochDynamicIdentities,
   189  			EpochParticipationStatusLeaving,
   190  		)
   191  		if err != nil {
   192  			return nil, fmt.Errorf("could not build identity table for staking phase: %w", err)
   193  		}
   194  	} else { // protocolState.NextEpoch ≠ nil, i.e. we are in epoch setup or epoch commit phase
   195  		// ensure respective epoch service events are not nil and consistent with commitments in `ProtocolStateEntry.NextEpoch`
   196  		if nextEpoch.SetupID != nextEpochSetup.ID() {
   197  			return nil, fmt.Errorf("supplied next epoch's setup event (%x) does not match commitment (%x) in ProtocolStateEntry", nextEpoch.SetupID, nextEpochSetup.ID())
   198  		}
   199  		if nextEpoch.CommitID != ZeroID {
   200  			if nextEpoch.CommitID != nextEpochCommit.ID() {
   201  				return nil, fmt.Errorf("supplied next epoch's commit event (%x) does not match commitment (%x) in ProtocolStateEntry", nextEpoch.CommitID, nextEpochCommit.ID())
   202  			}
   203  		}
   204  
   205  		result.CurrentEpochIdentityTable, err = BuildIdentityTable(
   206  			currentEpochSetup.Participants,
   207  			protocolState.CurrentEpoch.ActiveIdentities,
   208  			nextEpochSetup.Participants,
   209  			nextEpoch.ActiveIdentities,
   210  			EpochParticipationStatusJoining,
   211  		)
   212  		if err != nil {
   213  			return nil, fmt.Errorf("could not build identity table for setup/commit phase: %w", err)
   214  		}
   215  
   216  		result.NextEpochIdentityTable, err = BuildIdentityTable(
   217  			nextEpochSetup.Participants,
   218  			nextEpoch.ActiveIdentities,
   219  			currentEpochSetup.Participants,
   220  			protocolState.CurrentEpoch.ActiveIdentities,
   221  			EpochParticipationStatusLeaving,
   222  		)
   223  		if err != nil {
   224  			return nil, fmt.Errorf("could not build next epoch identity table: %w", err)
   225  		}
   226  	}
   227  	return result, nil
   228  }
   229  
   230  // ID returns hash of entry by hashing all fields.
   231  func (e *ProtocolStateEntry) ID() Identifier {
   232  	if e == nil {
   233  		return ZeroID
   234  	}
   235  	body := struct {
   236  		PreviousEpochID                 Identifier
   237  		CurrentEpochID                  Identifier
   238  		NextEpochID                     Identifier
   239  		InvalidEpochTransitionAttempted bool
   240  	}{
   241  		PreviousEpochID:                 e.PreviousEpoch.ID(),
   242  		CurrentEpochID:                  e.CurrentEpoch.ID(),
   243  		NextEpochID:                     e.NextEpoch.ID(),
   244  		InvalidEpochTransitionAttempted: e.InvalidEpochTransitionAttempted,
   245  	}
   246  	return MakeID(body)
   247  }
   248  
   249  // Copy returns a full copy of the entry.
   250  // Embedded Identities are deep-copied, _except_ for their keys, which are copied by reference.
   251  func (e *ProtocolStateEntry) Copy() *ProtocolStateEntry {
   252  	if e == nil {
   253  		return nil
   254  	}
   255  	return &ProtocolStateEntry{
   256  		PreviousEpoch:                   e.PreviousEpoch.Copy(),
   257  		CurrentEpoch:                    *e.CurrentEpoch.Copy(),
   258  		NextEpoch:                       e.NextEpoch.Copy(),
   259  		InvalidEpochTransitionAttempted: e.InvalidEpochTransitionAttempted,
   260  	}
   261  }
   262  
   263  // Copy returns a full copy of rich protocol state entry.
   264  //   - Embedded service events are copied by reference (not deep-copied).
   265  //   - CurrentEpochIdentityTable and NextEpochIdentityTable are deep-copied, _except_ for their keys, which are copied by reference.
   266  func (e *RichProtocolStateEntry) Copy() *RichProtocolStateEntry {
   267  	if e == nil {
   268  		return nil
   269  	}
   270  	return &RichProtocolStateEntry{
   271  		ProtocolStateEntry:        e.ProtocolStateEntry.Copy(),
   272  		PreviousEpochSetup:        e.PreviousEpochSetup,
   273  		PreviousEpochCommit:       e.PreviousEpochCommit,
   274  		CurrentEpochSetup:         e.CurrentEpochSetup,
   275  		CurrentEpochCommit:        e.CurrentEpochCommit,
   276  		NextEpochSetup:            e.NextEpochSetup,
   277  		NextEpochCommit:           e.NextEpochCommit,
   278  		CurrentEpochIdentityTable: e.CurrentEpochIdentityTable.Copy(),
   279  		NextEpochIdentityTable:    e.NextEpochIdentityTable.Copy(),
   280  	}
   281  }
   282  
   283  // EpochPhase returns the current epoch phase.
   284  // The receiver ProtocolStateEntry must be properly constructed.
   285  func (e *ProtocolStateEntry) EpochPhase() EpochPhase {
   286  	// The epoch phase is determined by how much information we have about the next epoch
   287  	if e.NextEpoch == nil {
   288  		return EpochPhaseStaking // if no information about the next epoch is known, we are in the Staking Phase
   289  	}
   290  	// Per convention, NextEpoch ≠ nil if and only if NextEpoch.SetupID is specified.
   291  	if e.NextEpoch.CommitID == ZeroID {
   292  		return EpochPhaseSetup // if only the Setup event is known for the next epoch but not the Commit event, we are in the Setup Phase
   293  	}
   294  	return EpochPhaseCommitted // if the Setup and Commit events are known for the next epoch, we are in the Committed Phase
   295  }
   296  
   297  // EpochCounter returns the current epoch counter.
   298  // The receiver RichProtocolStateEntry must be properly constructed.
   299  func (e *RichProtocolStateEntry) EpochCounter() uint64 {
   300  	return e.CurrentEpochSetup.Counter
   301  }
   302  
   303  func (ll DynamicIdentityEntryList) Lookup() map[Identifier]*DynamicIdentityEntry {
   304  	result := make(map[Identifier]*DynamicIdentityEntry, len(ll))
   305  	for _, entry := range ll {
   306  		result[entry.NodeID] = entry
   307  	}
   308  	return result
   309  }
   310  
   311  // Sorted returns whether the list is sorted by the input ordering.
   312  func (ll DynamicIdentityEntryList) Sorted(less IdentifierOrder) bool {
   313  	return slices.IsSortedFunc(ll, func(lhs, rhs *DynamicIdentityEntry) int {
   314  		return less(lhs.NodeID, rhs.NodeID)
   315  	})
   316  }
   317  
   318  // ByNodeID gets a node from the list by node ID.
   319  func (ll DynamicIdentityEntryList) ByNodeID(nodeID Identifier) (*DynamicIdentityEntry, bool) {
   320  	for _, identity := range ll {
   321  		if identity.NodeID == nodeID {
   322  			return identity, true
   323  		}
   324  	}
   325  	return nil, false
   326  }
   327  
   328  // Copy returns a copy of the DynamicIdentityEntryList. The resulting slice uses
   329  // a different backing array, meaning appends and insert operations on either slice
   330  // are guaranteed to only affect that slice.
   331  //
   332  // Copy should be used when modifying an existing identity list by either
   333  // appending new elements, re-ordering, or inserting new elements in an
   334  // existing index.
   335  //
   336  // CAUTION:
   337  // All Identity fields are deep-copied, _except_ for their keys, which
   338  // are copied by reference.
   339  func (ll DynamicIdentityEntryList) Copy() DynamicIdentityEntryList {
   340  	lenList := len(ll)
   341  	dup := make(DynamicIdentityEntryList, 0, lenList)
   342  	for i := 0; i < lenList; i++ {
   343  		// copy the object
   344  		next := *(ll[i])
   345  		dup = append(dup, &next)
   346  	}
   347  	return dup
   348  }
   349  
   350  // Sort sorts the list by the input ordering. Returns a new, sorted list without modifying the input.
   351  // CAUTION:
   352  // All Identity fields are deep-copied, _except_ for their keys, which are copied by reference.
   353  func (ll DynamicIdentityEntryList) Sort(less IdentifierOrder) DynamicIdentityEntryList {
   354  	dup := ll.Copy()
   355  	slices.SortFunc(dup, func(lhs, rhs *DynamicIdentityEntry) int {
   356  		return less(lhs.NodeID, rhs.NodeID)
   357  	})
   358  	return dup
   359  }
   360  
   361  // BuildIdentityTable constructs the full identity table for the target epoch by combining data from:
   362  //  1. The IdentitySkeletons for the nodes that are _active_ in the target epoch
   363  //     (recorded in EpochSetup event and immutable throughout the epoch).
   364  //  2. The Dynamic Identities for the nodes that are _active_ in the target epoch (i.e. the dynamic identity
   365  //     fields for the IdentitySkeletons contained in the EpochSetup event for the respective epoch).
   366  //
   367  // Optionally, identity information for an adjacent epoch is given if and only if an adjacent epoch exists. For
   368  // a target epoch N, the epochs N-1 and N+1 are defined to be adjacent. Adjacent epochs do not necessarily exist
   369  // (e.g. consider a spork comprising only a single epoch), in which case the respective inputs are nil or empty.
   370  //  3. [optional] An adjacent epoch's IdentitySkeletons as recorded in the adjacent epoch's setup event.
   371  //  4. [optional] An adjacent epoch's Dynamic Identities.
   372  //  5. An adjacent epoch's identities participation status, this could be joining or leaving depending on epoch phase.
   373  //
   374  // The function enforces that the input slices pertaining to the same epoch contain the same identities
   375  // (compared by nodeID) in the same order. Otherwise, an exception is returned.
   376  // No errors are expected during normal operation. All errors indicate inconsistent or invalid inputs.
   377  func BuildIdentityTable(
   378  	targetEpochIdentitySkeletons IdentitySkeletonList,
   379  	targetEpochDynamicIdentities DynamicIdentityEntryList,
   380  	adjacentEpochIdentitySkeletons IdentitySkeletonList,
   381  	adjacentEpochDynamicIdentities DynamicIdentityEntryList,
   382  	adjacentIdentitiesStatus EpochParticipationStatus,
   383  ) (IdentityList, error) {
   384  	if adjacentIdentitiesStatus != EpochParticipationStatusLeaving &&
   385  		adjacentIdentitiesStatus != EpochParticipationStatusJoining {
   386  		return nil, fmt.Errorf("invalid adjacent identity status, expect %s or %s, got %s",
   387  			EpochParticipationStatusLeaving.String(),
   388  			EpochParticipationStatusJoining.String(),
   389  			adjacentIdentitiesStatus)
   390  	}
   391  	targetEpochParticipants, err := ComposeFullIdentities(targetEpochIdentitySkeletons, targetEpochDynamicIdentities, EpochParticipationStatusActive)
   392  	if err != nil {
   393  		return nil, fmt.Errorf("could not reconstruct participants for target epoch: %w", err)
   394  	}
   395  	adjacentEpochParticipants, err := ComposeFullIdentities(adjacentEpochIdentitySkeletons, adjacentEpochDynamicIdentities, adjacentIdentitiesStatus)
   396  	if err != nil {
   397  		return nil, fmt.Errorf("could not reconstruct participants for adjacent epoch: %w", err)
   398  	}
   399  
   400  	// Combine the participants of the current and adjacent epoch. The method `GenericIdentityList.Union`
   401  	// already implements the following required conventions:
   402  	//  1. Preference for IdentitySkeleton of the target epoch:
   403  	//     In case an IdentitySkeleton with the same NodeID exists in the target epoch as well as
   404  	//     in the adjacent epoch, we use the IdentitySkeleton for the target epoch (for example,
   405  	//     to account for changes of keys, address, initial weight, etc).
   406  	//  2. Canonical ordering
   407  	return targetEpochParticipants.Union(adjacentEpochParticipants), nil
   408  }
   409  
   410  // DynamicIdentityEntryListFromIdentities converts IdentityList to DynamicIdentityEntryList.
   411  func DynamicIdentityEntryListFromIdentities(identities IdentityList) DynamicIdentityEntryList {
   412  	dynamicIdentities := make(DynamicIdentityEntryList, 0, len(identities))
   413  	for _, identity := range identities {
   414  		dynamicIdentities = append(dynamicIdentities, &DynamicIdentityEntry{
   415  			NodeID:  identity.NodeID,
   416  			Ejected: identity.IsEjected(),
   417  		})
   418  	}
   419  	return dynamicIdentities
   420  }
   421  
   422  // ComposeFullIdentities combines identity skeletons and dynamic identities to produce a flow.IdentityList.
   423  // It enforces that the input slices `skeletons` and `dynamics` list the same identities (compared by nodeID)
   424  // in the same order. Otherwise, an exception is returned. For each identity i, we set
   425  // `i.EpochParticipationStatus` to the `defaultEpochParticipationStatus` _unless_ i is ejected.
   426  // No errors are expected during normal operations.
   427  func ComposeFullIdentities(
   428  	skeletons IdentitySkeletonList,
   429  	dynamics DynamicIdentityEntryList,
   430  	defaultEpochParticipationStatus EpochParticipationStatus,
   431  ) (IdentityList, error) {
   432  	// sanity check: list of skeletons and dynamic should be the same
   433  	if len(skeletons) != len(dynamics) {
   434  		return nil, fmt.Errorf("invalid number of identities to reconstruct: expected %d, got %d", len(skeletons), len(dynamics))
   435  	}
   436  
   437  	// reconstruct identities from skeleton and dynamic parts
   438  	var result IdentityList
   439  	for i := range dynamics {
   440  		// sanity check: identities should be sorted in the same order
   441  		if dynamics[i].NodeID != skeletons[i].NodeID {
   442  			return nil, fmt.Errorf("identites in protocol state are not consistently ordered: expected %s, got %s", skeletons[i].NodeID, dynamics[i].NodeID)
   443  		}
   444  		status := defaultEpochParticipationStatus
   445  		if dynamics[i].Ejected {
   446  			status = EpochParticipationStatusEjected
   447  		}
   448  		result = append(result, &Identity{
   449  			IdentitySkeleton: *skeletons[i],
   450  			DynamicIdentity: DynamicIdentity{
   451  				EpochParticipationStatus: status,
   452  			},
   453  		})
   454  	}
   455  	return result, nil
   456  }
   457  
   458  // PSKeyValueStoreData is a binary blob with a version attached, specifying the format
   459  // of the marshaled data. In a nutshell, it serves as a binary snapshot of a ProtocolKVStore.
   460  // This structure is useful for version-agnostic storage, where snapshots with different versions
   461  // can co-exist. The PSKeyValueStoreData is a generic format that can be later decoded to
   462  // potentially different strongly typed structures based on version. When reading from the store,
   463  // callers must know how to deal with the binary representation.
   464  type PSKeyValueStoreData struct {
   465  	Version uint64
   466  	Data    []byte
   467  }