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  }