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 }