github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/state/protocol/protocol_state/epochs/statemachine.go (about) 1 package epochs 2 3 import ( 4 "fmt" 5 6 "github.com/onflow/flow-go/model/flow" 7 "github.com/onflow/flow-go/module/irrecoverable" 8 "github.com/onflow/flow-go/state/protocol" 9 "github.com/onflow/flow-go/state/protocol/protocol_state" 10 "github.com/onflow/flow-go/storage" 11 "github.com/onflow/flow-go/storage/badger/operation" 12 "github.com/onflow/flow-go/storage/badger/transaction" 13 ) 14 15 // StateMachine implements a low-level interface for state-changing operations on the Epoch state. 16 // It is used by higher level logic to coordinate the Epoch handover, evolving its internal state 17 // when Epoch-related Service Events are sealed or specific view-thresholds are reached. 18 // 19 // The StateMachine is fork-aware, in that it starts with the Epoch state of the parent block and 20 // evolves the state based on the relevant information in the child block (specifically Service Events 21 // sealed in the child block and the child block's view). A separate instance must be created for each 22 // block that is being processed. Calling `Build()` constructs a snapshot of the resulting Epoch state. 23 type StateMachine interface { 24 // Build returns updated protocol state entry, state ID and a flag indicating if there were any changes. 25 // CAUTION: 26 // Do NOT call Build, if the StateMachine instance has returned a `protocol.InvalidServiceEventError` 27 // at any time during its lifetime. After this error, the StateMachine is left with a potentially 28 // dysfunctional state and should be discarded. 29 Build() (updatedState *flow.ProtocolStateEntry, stateID flow.Identifier, hasChanges bool) 30 31 // ProcessEpochSetup updates the internally-maintained interim Epoch state with data from epoch setup event. 32 // Processing an epoch setup event also affects the identity table for the current epoch. 33 // Specifically, we transition the Epoch state from staking to setup phase, we stop returning 34 // identities from previous+current epochs and start returning identities from current+next epochs. 35 // As a result of this operation protocol state for the next epoch will be created. 36 // Returned boolean indicates if event triggered a transition in the state machine or not. 37 // Implementors must never return (true, error). 38 // Expected errors indicating that we are leaving the happy-path of the epoch transitions 39 // - `protocol.InvalidServiceEventError` - if the service event is invalid or is not a valid state transition for the current protocol state. 40 // CAUTION: the StateMachine is left with a potentially dysfunctional state when this error occurs. Do NOT call the Build method 41 // after such error and discard the StateMachine! 42 ProcessEpochSetup(epochSetup *flow.EpochSetup) (bool, error) 43 44 // ProcessEpochCommit updates the internally-maintained interim Epoch state with data from epoch commit event. 45 // Observing an epoch setup commit, transitions protocol state from setup to commit phase. 46 // At this point, we have finished construction of the next epoch. 47 // As a result of this operation protocol state for next epoch will be committed. 48 // Returned boolean indicates if event triggered a transition in the state machine or not. 49 // Implementors must never return (true, error). 50 // Expected errors indicating that we are leaving the happy-path of the epoch transitions 51 // - `protocol.InvalidServiceEventError` - if the service event is invalid or is not a valid state transition for the current protocol state. 52 // CAUTION: the StateMachine is left with a potentially dysfunctional state when this error occurs. Do NOT call the Build method 53 // after such error and discard the StateMachine! 54 ProcessEpochCommit(epochCommit *flow.EpochCommit) (bool, error) 55 56 // EjectIdentity updates identity table by changing the node's participation status to 'ejected'. 57 // Should pass identity which is already present in the table, otherwise an exception will be raised. 58 // Expected errors during normal operations: 59 // - `protocol.InvalidServiceEventError` if the updated identity is not found in current and adjacent epochs. 60 EjectIdentity(nodeID flow.Identifier) error 61 62 // TransitionToNextEpoch transitions our reference frame of 'current epoch' to the pending but committed epoch. 63 // Epoch transition is only allowed when: 64 // - next epoch has been committed, 65 // - candidate block is in the next epoch. 66 // No errors are expected during normal operations. 67 TransitionToNextEpoch() error 68 69 // View returns the view associated with this state machine. 70 // The view of the state machine equals the view of the block carrying the respective updates. 71 View() uint64 72 73 // ParentState returns parent protocol state associated with this state machine. 74 ParentState() *flow.RichProtocolStateEntry 75 } 76 77 // StateMachineFactoryMethod is a factory method to create state machines for evolving the protocol's epoch state. 78 // Currently, we have `HappyPathStateMachine` and `FallbackStateMachine` as StateMachine 79 // implementations, whose constructors both have the same signature as StateMachineFactoryMethod. 80 type StateMachineFactoryMethod func(candidateView uint64, parentState *flow.RichProtocolStateEntry) (StateMachine, error) 81 82 // EpochStateMachine is a hierarchical state machine that encapsulates the logic for protocol-compliant evolution of Epoch-related sub-state. 83 // EpochStateMachine processes a subset of service events that are relevant for the Epoch state, and ignores all other events. 84 // EpochStateMachine delegates the processing of service events to an embedded StateMachine, 85 // which is either a HappyPathStateMachine or a FallbackStateMachine depending on the operation mode of the protocol. 86 // It relies on Key-Value Store to read the parent state and to persist the snapshot of the updated Epoch state. 87 type EpochStateMachine struct { 88 activeStateMachine StateMachine 89 parentState protocol.KVStoreReader 90 mutator protocol_state.KVStoreMutator 91 epochFallbackStateMachineFactory func() (StateMachine, error) 92 93 setups storage.EpochSetups 94 commits storage.EpochCommits 95 epochProtocolStateDB storage.ProtocolState 96 pendingDbUpdates *transaction.DeferredBlockPersist 97 } 98 99 var _ protocol_state.KeyValueStoreStateMachine = (*EpochStateMachine)(nil) 100 101 // NewEpochStateMachine creates a new higher-level hierarchical state machine for protocol-compliant evolution of Epoch-related sub-state. 102 // NewEpochStateMachine performs initialization of state machine depending on the operation mode of the protocol. 103 // - for the happy path, it initializes a HappyPathStateMachine, 104 // - for the epoch fallback mode it initializes a FallbackStateMachine. 105 // No errors are expected during normal operations. 106 func NewEpochStateMachine( 107 candidateView uint64, 108 parentBlockID flow.Identifier, 109 params protocol.GlobalParams, 110 setups storage.EpochSetups, 111 commits storage.EpochCommits, 112 epochProtocolStateDB storage.ProtocolState, 113 parentState protocol.KVStoreReader, 114 mutator protocol_state.KVStoreMutator, 115 happyPathStateMachineFactory StateMachineFactoryMethod, 116 epochFallbackStateMachineFactory StateMachineFactoryMethod, 117 ) (*EpochStateMachine, error) { 118 parentEpochState, err := epochProtocolStateDB.ByBlockID(parentBlockID) 119 if err != nil { 120 return nil, fmt.Errorf("could not query parent protocol state at block (%x): %w", parentBlockID, err) 121 } 122 123 // sanity check: the parent epoch state ID must be set in KV store 124 if parentEpochState.ID() != parentState.GetEpochStateID() { 125 return nil, irrecoverable.NewExceptionf("broken invariant: parent epoch state ID mismatch, expected %x, got %x", 126 parentState.GetEpochStateID(), parentEpochState.ID()) 127 } 128 129 var stateMachine StateMachine 130 candidateAttemptsInvalidEpochTransition := epochFallbackTriggeredByIncorporatingCandidate(candidateView, params, parentEpochState) 131 if parentEpochState.InvalidEpochTransitionAttempted || candidateAttemptsInvalidEpochTransition { 132 // Case 1: InvalidEpochTransitionAttempted is true, indicating that we have encountered an invalid 133 // epoch service event or an invalid state transition previously in this fork. 134 // Case 2: Incorporating the candidate block is itself an invalid epoch transition. 135 // 136 // In either case, Epoch Fallback Mode [EFM] has been tentatively triggered on this fork, 137 // and we must use only the `epochFallbackStateMachine` along this fork. 138 // 139 // TODO for 'leaving Epoch Fallback via special service event': this might need to change. 140 stateMachine, err = epochFallbackStateMachineFactory(candidateView, parentEpochState) 141 } else { 142 stateMachine, err = happyPathStateMachineFactory(candidateView, parentEpochState) 143 } 144 if err != nil { 145 return nil, fmt.Errorf("could not initialize protocol state machine: %w", err) 146 } 147 148 return &EpochStateMachine{ 149 activeStateMachine: stateMachine, 150 parentState: parentState, 151 mutator: mutator, 152 epochFallbackStateMachineFactory: func() (StateMachine, error) { 153 return epochFallbackStateMachineFactory(candidateView, parentEpochState) 154 }, 155 setups: setups, 156 commits: commits, 157 epochProtocolStateDB: epochProtocolStateDB, 158 pendingDbUpdates: transaction.NewDeferredBlockPersist(), 159 }, nil 160 } 161 162 // Build schedules updates to the protocol state by obtaining the updated state from the active state machine, 163 // preparing deferred DB updates and committing updated sub-state ID to the KV store. 164 // ATTENTION: In mature implementation all parts of the Dynamic Protocol State will rely on the Key-Value Store as storage 165 // but to avoid a large refactoring we are using a hybrid approach where only the epoch state ID is stored in the KV Store 166 // but the actual epoch state is stored separately, nevertheless, the epoch state ID is used to sanity check if the 167 // epoch state is consistent with the KV Store. Using this approach, we commit the epoch sub-state to the KV Store which in 168 // affects the Dynamic Protocol State ID which is essentially hash of the KV Store. 169 func (e *EpochStateMachine) Build() (*transaction.DeferredBlockPersist, error) { 170 updatedEpochState, updatedStateID, hasChanges := e.activeStateMachine.Build() 171 e.pendingDbUpdates.AddIndexingOp(func(blockID flow.Identifier, tx *transaction.Tx) error { 172 return e.epochProtocolStateDB.Index(blockID, updatedStateID)(tx) 173 }) 174 if hasChanges { 175 e.pendingDbUpdates.AddDbOp(operation.SkipDuplicatesTx(e.epochProtocolStateDB.StoreTx(updatedStateID, updatedEpochState))) 176 } 177 e.mutator.SetEpochStateID(updatedStateID) 178 179 return e.pendingDbUpdates, nil 180 } 181 182 // EvolveState applies the state change(s) on the Epoch sub-state based on information from the candidate block 183 // (under construction). Information that potentially changes the state (compared to the parent block's state): 184 // - Service Events sealed in the candidate block 185 // - the candidate block's view (already provided at construction time) 186 // 187 // CAUTION: EvolveState MUST be called for all candidate blocks, even if `sealedServiceEvents` is empty! 188 // This is because also the absence of expected service events by a certain view can also result in the 189 // Epoch state changing. (For example, not having received the EpochCommit event for the next epoch, but 190 // approaching the end of the current epoch.) 191 // 192 // The block's payload might contain epoch preparation service events for the next epoch. In this case, 193 // we need to update the tentative protocol state. We need to validate whether all information is available 194 // in the protocol state to go to the next epoch when needed. In cases where there is a bug in the smart 195 // contract, it could be that this happens too late, and we should trigger epoch fallback mode. 196 // No errors are expected during normal operations. 197 func (e *EpochStateMachine) EvolveState(sealedServiceEvents []flow.ServiceEvent) error { 198 parentProtocolState := e.activeStateMachine.ParentState() 199 200 // perform protocol state transition to next epoch if next epoch is committed, and we are at first block of epoch 201 // TODO: The current implementation has edge cases for future light clients and can potentially drive consensus 202 // into an irreconcilable state (not sure). See for details https://github.com/onflow/flow-go/issues/5631 203 // These edge cases are very unlikely, so this is an acceptable implementation in the short - mid term. 204 // However, this code will likely need to be changed when working on EFM recovery. 205 phase := parentProtocolState.EpochPhase() 206 if phase == flow.EpochPhaseCommitted { 207 activeSetup := parentProtocolState.CurrentEpochSetup 208 if e.activeStateMachine.View() > activeSetup.FinalView { 209 err := e.activeStateMachine.TransitionToNextEpoch() 210 if err != nil { 211 return fmt.Errorf("could not transition protocol state to next epoch: %w", err) 212 } 213 } 214 } 215 216 dbUpdates, err := e.applyServiceEventsFromOrderedResults(sealedServiceEvents) 217 if err != nil { 218 if protocol.IsInvalidServiceEventError(err) { 219 dbUpdates, err = e.transitionToEpochFallbackMode(sealedServiceEvents) 220 if err != nil { 221 return irrecoverable.NewExceptionf("could not transition to epoch fallback mode: %w", err) 222 } 223 } else { 224 return irrecoverable.NewExceptionf("could not apply service events from ordered results: %w", err) 225 } 226 } 227 e.pendingDbUpdates.AddIndexingOps(dbUpdates.Pending()) 228 return nil 229 } 230 231 // View returns the view associated with this state machine. 232 // The view of the state machine equals the view of the block carrying the respective updates. 233 func (e *EpochStateMachine) View() uint64 { 234 return e.activeStateMachine.View() 235 } 236 237 // ParentState returns parent state associated with this state machine. 238 func (e *EpochStateMachine) ParentState() protocol.KVStoreReader { 239 return e.parentState 240 } 241 242 // applyServiceEventsFromOrderedResults applies the service events contained within the list of results 243 // to the pending state tracked by `stateMutator`. 244 // Each result corresponds to one seal that was included in the payload of the block being processed by this `stateMutator`. 245 // Results must be ordered by block height. 246 // Expected errors during normal operations: 247 // - `protocol.InvalidServiceEventError` if any service event is invalid or is not a valid state transition for the current protocol state 248 func (e *EpochStateMachine) applyServiceEventsFromOrderedResults(orderedUpdates []flow.ServiceEvent) (*transaction.DeferredBlockPersist, error) { 249 dbUpdates := transaction.NewDeferredBlockPersist() 250 for _, event := range orderedUpdates { 251 switch ev := event.Event.(type) { 252 case *flow.EpochSetup: 253 processed, err := e.activeStateMachine.ProcessEpochSetup(ev) 254 if err != nil { 255 return nil, fmt.Errorf("could not process epoch setup event: %w", err) 256 } 257 if processed { 258 dbUpdates.AddDbOp(e.setups.StoreTx(ev)) // we'll insert the setup event when we insert the block 259 } 260 261 case *flow.EpochCommit: 262 processed, err := e.activeStateMachine.ProcessEpochCommit(ev) 263 if err != nil { 264 return nil, fmt.Errorf("could not process epoch commit event: %w", err) 265 } 266 if processed { 267 dbUpdates.AddDbOp(e.commits.StoreTx(ev)) // we'll insert the commit event when we insert the block 268 } 269 default: 270 continue 271 } 272 } 273 return dbUpdates, nil 274 } 275 276 // transitionToEpochFallbackMode transitions the protocol state to Epoch Fallback Mode [EFM]. 277 // This is implemented by switching to a different state machine implementation, which ignores all service events and epoch transitions. 278 // At the moment, this is a one-way transition: once we enter EFM, the only way to return to normal is with a spork. 279 func (e *EpochStateMachine) transitionToEpochFallbackMode(orderedUpdates []flow.ServiceEvent) (*transaction.DeferredBlockPersist, error) { 280 var err error 281 e.activeStateMachine, err = e.epochFallbackStateMachineFactory() 282 if err != nil { 283 return nil, fmt.Errorf("could not create epoch fallback state machine: %w", err) 284 } 285 dbUpdates, err := e.applyServiceEventsFromOrderedResults(orderedUpdates) 286 if err != nil { 287 return nil, irrecoverable.NewExceptionf("could not apply service events after transition to epoch fallback mode: %w", err) 288 } 289 return dbUpdates, nil 290 } 291 292 // epochFallbackTriggeredByIncorporatingCandidate checks whether incorporating the input block B 293 // would trigger epoch fallback mode [EFM] along the current fork. We trigger epoch fallback mode 294 // when: 295 // 1. The next epoch has not been committed as of B (EpochPhase ≠ flow.EpochPhaseCommitted) AND 296 // 2. B is the first incorporated block with view greater than or equal to the epoch commitment 297 // deadline for the current epoch 298 // 299 // In protocol terms, condition 1 means that an EpochCommit service event for the upcoming epoch has 300 // not yet been sealed as of block B. Formally, a service event S is considered sealed as of block B if: 301 // - S was emitted during execution of some block A, s.t. A is an ancestor of B. 302 // - The seal for block A was included in some block C, s.t C is an ancestor of B. 303 // 304 // For further details see `params.EpochCommitSafetyThreshold()`. 305 func epochFallbackTriggeredByIncorporatingCandidate(candidateView uint64, params protocol.GlobalParams, parentState *flow.RichProtocolStateEntry) bool { 306 if parentState.EpochPhase() == flow.EpochPhaseCommitted { // Requirement 1 307 return false 308 } 309 return candidateView+params.EpochCommitSafetyThreshold() >= parentState.CurrentEpochSetup.FinalView // Requirement 2 310 }