github.com/ethereum-optimism/optimism@v1.7.2/op-node/rollup/derive/engine_controller.go (about)

     1  package derive
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"time"
     8  
     9  	"github.com/ethereum-optimism/optimism/op-node/rollup"
    10  	"github.com/ethereum-optimism/optimism/op-node/rollup/async"
    11  	"github.com/ethereum-optimism/optimism/op-node/rollup/conductor"
    12  	"github.com/ethereum-optimism/optimism/op-node/rollup/sync"
    13  	"github.com/ethereum-optimism/optimism/op-service/clock"
    14  	"github.com/ethereum-optimism/optimism/op-service/eth"
    15  	"github.com/ethereum/go-ethereum"
    16  	"github.com/ethereum/go-ethereum/common"
    17  	"github.com/ethereum/go-ethereum/log"
    18  )
    19  
    20  type syncStatusEnum int
    21  
    22  const (
    23  	syncStatusCL syncStatusEnum = iota
    24  	// We transition between the 4 EL states linearly. We spend the majority of the time in the second & fourth.
    25  	// We only want to EL sync if there is no finalized block & once we finish EL sync we need to mark the last block
    26  	// as finalized so we can switch to consolidation
    27  	// TODO(protocol-quest/91): We can restart EL sync & still consolidate if there finalized blocks on the execution client if the
    28  	// execution client is running in archive mode. In some cases we may want to switch back from CL to EL sync, but that is complicated.
    29  	syncStatusWillStartEL               // First if we are directed to EL sync, check that nothing has been finalized yet
    30  	syncStatusStartedEL                 // Perform our EL sync
    31  	syncStatusFinishedELButNotFinalized // EL sync is done, but we need to mark the final sync block as finalized
    32  	syncStatusFinishedEL                // EL sync is done & we should be performing consolidation
    33  )
    34  
    35  var errNoFCUNeeded = errors.New("no FCU call was needed")
    36  
    37  var _ EngineControl = (*EngineController)(nil)
    38  var _ LocalEngineControl = (*EngineController)(nil)
    39  
    40  type ExecEngine interface {
    41  	GetPayload(ctx context.Context, payloadInfo eth.PayloadInfo) (*eth.ExecutionPayloadEnvelope, error)
    42  	ForkchoiceUpdate(ctx context.Context, state *eth.ForkchoiceState, attr *eth.PayloadAttributes) (*eth.ForkchoiceUpdatedResult, error)
    43  	NewPayload(ctx context.Context, payload *eth.ExecutionPayload, parentBeaconBlockRoot *common.Hash) (*eth.PayloadStatusV1, error)
    44  	L2BlockRefByLabel(ctx context.Context, label eth.BlockLabel) (eth.L2BlockRef, error)
    45  }
    46  
    47  type EngineController struct {
    48  	engine     ExecEngine // Underlying execution engine RPC
    49  	log        log.Logger
    50  	metrics    Metrics
    51  	syncMode   sync.Mode
    52  	syncStatus syncStatusEnum
    53  	rollupCfg  *rollup.Config
    54  	elStart    time.Time
    55  	clock      clock.Clock
    56  
    57  	// Block Head State
    58  	unsafeHead       eth.L2BlockRef
    59  	pendingSafeHead  eth.L2BlockRef // L2 block processed from the middle of a span batch, but not marked as the safe block yet.
    60  	safeHead         eth.L2BlockRef
    61  	finalizedHead    eth.L2BlockRef
    62  	backupUnsafeHead eth.L2BlockRef
    63  	needFCUCall      bool
    64  	// Track when the rollup node changes the forkchoice to restore previous
    65  	// known unsafe chain. e.g. Unsafe Reorg caused by Invalid span batch.
    66  	// This update does not retry except engine returns non-input error
    67  	// because engine may forgot backupUnsafeHead or backupUnsafeHead is not part
    68  	// of the chain.
    69  	needFCUCallForBackupUnsafeReorg bool
    70  
    71  	// Building State
    72  	buildingOnto eth.L2BlockRef
    73  	buildingInfo eth.PayloadInfo
    74  	buildingSafe bool
    75  	safeAttrs    *AttributesWithParent
    76  }
    77  
    78  func NewEngineController(engine ExecEngine, log log.Logger, metrics Metrics, rollupCfg *rollup.Config, syncMode sync.Mode) *EngineController {
    79  	syncStatus := syncStatusCL
    80  	if syncMode == sync.ELSync {
    81  		syncStatus = syncStatusWillStartEL
    82  	}
    83  
    84  	return &EngineController{
    85  		engine:     engine,
    86  		log:        log,
    87  		metrics:    metrics,
    88  		rollupCfg:  rollupCfg,
    89  		syncMode:   syncMode,
    90  		syncStatus: syncStatus,
    91  		clock:      clock.SystemClock,
    92  	}
    93  }
    94  
    95  // State Getters
    96  
    97  func (e *EngineController) UnsafeL2Head() eth.L2BlockRef {
    98  	return e.unsafeHead
    99  }
   100  
   101  func (e *EngineController) PendingSafeL2Head() eth.L2BlockRef {
   102  	return e.pendingSafeHead
   103  }
   104  
   105  func (e *EngineController) SafeL2Head() eth.L2BlockRef {
   106  	return e.safeHead
   107  }
   108  
   109  func (e *EngineController) Finalized() eth.L2BlockRef {
   110  	return e.finalizedHead
   111  }
   112  
   113  func (e *EngineController) BackupUnsafeL2Head() eth.L2BlockRef {
   114  	return e.backupUnsafeHead
   115  }
   116  
   117  func (e *EngineController) BuildingPayload() (eth.L2BlockRef, eth.PayloadID, bool) {
   118  	return e.buildingOnto, e.buildingInfo.ID, e.buildingSafe
   119  }
   120  
   121  func (e *EngineController) IsEngineSyncing() bool {
   122  	return e.syncStatus == syncStatusWillStartEL || e.syncStatus == syncStatusStartedEL || e.syncStatus == syncStatusFinishedELButNotFinalized
   123  }
   124  
   125  // Setters
   126  
   127  // SetFinalizedHead implements LocalEngineControl.
   128  func (e *EngineController) SetFinalizedHead(r eth.L2BlockRef) {
   129  	e.metrics.RecordL2Ref("l2_finalized", r)
   130  	e.finalizedHead = r
   131  	e.needFCUCall = true
   132  }
   133  
   134  // SetPendingSafeL2Head implements LocalEngineControl.
   135  func (e *EngineController) SetPendingSafeL2Head(r eth.L2BlockRef) {
   136  	e.metrics.RecordL2Ref("l2_pending_safe", r)
   137  	e.pendingSafeHead = r
   138  }
   139  
   140  // SetSafeHead implements LocalEngineControl.
   141  func (e *EngineController) SetSafeHead(r eth.L2BlockRef) {
   142  	e.metrics.RecordL2Ref("l2_safe", r)
   143  	e.safeHead = r
   144  	e.needFCUCall = true
   145  }
   146  
   147  // SetUnsafeHead implements LocalEngineControl.
   148  func (e *EngineController) SetUnsafeHead(r eth.L2BlockRef) {
   149  	e.metrics.RecordL2Ref("l2_unsafe", r)
   150  	e.unsafeHead = r
   151  	e.needFCUCall = true
   152  }
   153  
   154  // SetBackupUnsafeL2Head implements LocalEngineControl.
   155  func (e *EngineController) SetBackupUnsafeL2Head(r eth.L2BlockRef, triggerReorg bool) {
   156  	e.metrics.RecordL2Ref("l2_backup_unsafe", r)
   157  	e.backupUnsafeHead = r
   158  	e.needFCUCallForBackupUnsafeReorg = triggerReorg
   159  }
   160  
   161  // Engine Methods
   162  
   163  func (e *EngineController) StartPayload(ctx context.Context, parent eth.L2BlockRef, attrs *AttributesWithParent, updateSafe bool) (errType BlockInsertionErrType, err error) {
   164  	if e.IsEngineSyncing() {
   165  		return BlockInsertTemporaryErr, fmt.Errorf("engine is in progess of p2p sync")
   166  	}
   167  	if e.buildingInfo != (eth.PayloadInfo{}) {
   168  		e.log.Warn("did not finish previous block building, starting new building now", "prev_onto", e.buildingOnto, "prev_payload_id", e.buildingInfo.ID, "new_onto", parent)
   169  		// TODO(8841): maybe worth it to force-cancel the old payload ID here.
   170  	}
   171  	fc := eth.ForkchoiceState{
   172  		HeadBlockHash:      parent.Hash,
   173  		SafeBlockHash:      e.safeHead.Hash,
   174  		FinalizedBlockHash: e.finalizedHead.Hash,
   175  	}
   176  
   177  	id, errTyp, err := startPayload(ctx, e.engine, fc, attrs.attributes)
   178  	if err != nil {
   179  		return errTyp, err
   180  	}
   181  
   182  	e.buildingInfo = eth.PayloadInfo{ID: id, Timestamp: uint64(attrs.attributes.Timestamp)}
   183  	e.buildingSafe = updateSafe
   184  	e.buildingOnto = parent
   185  	if updateSafe {
   186  		e.safeAttrs = attrs
   187  	}
   188  
   189  	return BlockInsertOK, nil
   190  }
   191  
   192  func (e *EngineController) ConfirmPayload(ctx context.Context, agossip async.AsyncGossiper, sequencerConductor conductor.SequencerConductor) (out *eth.ExecutionPayloadEnvelope, errTyp BlockInsertionErrType, err error) {
   193  	// don't create a BlockInsertPrestateErr if we have a cached gossip payload
   194  	if e.buildingInfo == (eth.PayloadInfo{}) && agossip.Get() == nil {
   195  		return nil, BlockInsertPrestateErr, fmt.Errorf("cannot complete payload building: not currently building a payload")
   196  	}
   197  	if p := agossip.Get(); p != nil && e.buildingOnto == (eth.L2BlockRef{}) {
   198  		e.log.Warn("Found reusable payload from async gossiper, and no block was being built. Reusing payload.",
   199  			"hash", p.ExecutionPayload.BlockHash,
   200  			"number", uint64(p.ExecutionPayload.BlockNumber),
   201  			"parent", p.ExecutionPayload.ParentHash)
   202  	} else if e.buildingOnto.Hash != e.unsafeHead.Hash { // E.g. when safe-attributes consolidation fails, it will drop the existing work.
   203  		e.log.Warn("engine is building block that reorgs previous unsafe head", "onto", e.buildingOnto, "unsafe", e.unsafeHead)
   204  	}
   205  	fc := eth.ForkchoiceState{
   206  		HeadBlockHash:      common.Hash{}, // gets overridden
   207  		SafeBlockHash:      e.safeHead.Hash,
   208  		FinalizedBlockHash: e.finalizedHead.Hash,
   209  	}
   210  	// Update the safe head if the payload is built with the last attributes in the batch.
   211  	updateSafe := e.buildingSafe && e.safeAttrs != nil && e.safeAttrs.isLastInSpan
   212  	envelope, errTyp, err := confirmPayload(ctx, e.log, e.engine, fc, e.buildingInfo, updateSafe, agossip, sequencerConductor)
   213  	if err != nil {
   214  		return nil, errTyp, fmt.Errorf("failed to complete building on top of L2 chain %s, id: %s, error (%d): %w", e.buildingOnto, e.buildingInfo.ID, errTyp, err)
   215  	}
   216  	ref, err := PayloadToBlockRef(e.rollupCfg, envelope.ExecutionPayload)
   217  	if err != nil {
   218  		return nil, BlockInsertPayloadErr, NewResetError(fmt.Errorf("failed to decode L2 block ref from payload: %w", err))
   219  	}
   220  	// Backup unsafeHead when new block is not built on original unsafe head.
   221  	if e.unsafeHead.Number >= ref.Number {
   222  		e.SetBackupUnsafeL2Head(e.unsafeHead, false)
   223  	}
   224  	e.unsafeHead = ref
   225  
   226  	e.metrics.RecordL2Ref("l2_unsafe", ref)
   227  	if e.buildingSafe {
   228  		e.metrics.RecordL2Ref("l2_pending_safe", ref)
   229  		e.pendingSafeHead = ref
   230  		if updateSafe {
   231  			e.safeHead = ref
   232  			e.metrics.RecordL2Ref("l2_safe", ref)
   233  			// Remove backupUnsafeHead because this backup will be never used after consolidation.
   234  			e.SetBackupUnsafeL2Head(eth.L2BlockRef{}, false)
   235  		}
   236  	}
   237  
   238  	e.resetBuildingState()
   239  	return envelope, BlockInsertOK, nil
   240  }
   241  
   242  func (e *EngineController) CancelPayload(ctx context.Context, force bool) error {
   243  	if e.buildingInfo == (eth.PayloadInfo{}) { // only cancel if there is something to cancel.
   244  		return nil
   245  	}
   246  	// the building job gets wrapped up as soon as the payload is retrieved, there's no explicit cancel in the Engine API
   247  	e.log.Error("cancelling old block sealing job", "payload", e.buildingInfo.ID)
   248  	_, err := e.engine.GetPayload(ctx, e.buildingInfo)
   249  	if err != nil {
   250  		e.log.Error("failed to cancel block building job", "payload", e.buildingInfo.ID, "err", err)
   251  		if !force {
   252  			return err
   253  		}
   254  	}
   255  	e.resetBuildingState()
   256  	return nil
   257  }
   258  
   259  func (e *EngineController) resetBuildingState() {
   260  	e.buildingInfo = eth.PayloadInfo{}
   261  	e.buildingOnto = eth.L2BlockRef{}
   262  	e.buildingSafe = false
   263  	e.safeAttrs = nil
   264  }
   265  
   266  // Misc Setters only used by the engine queue
   267  
   268  // checkNewPayloadStatus checks returned status of engine_newPayloadV1 request for next unsafe payload.
   269  // It returns true if the status is acceptable.
   270  func (e *EngineController) checkNewPayloadStatus(status eth.ExecutePayloadStatus) bool {
   271  	if e.syncMode == sync.ELSync {
   272  		if status == eth.ExecutionValid && e.syncStatus == syncStatusStartedEL {
   273  			e.syncStatus = syncStatusFinishedELButNotFinalized
   274  		}
   275  		// Allow SYNCING and ACCEPTED if engine EL sync is enabled
   276  		return status == eth.ExecutionValid || status == eth.ExecutionSyncing || status == eth.ExecutionAccepted
   277  	}
   278  	return status == eth.ExecutionValid
   279  }
   280  
   281  // checkForkchoiceUpdatedStatus checks returned status of engine_forkchoiceUpdatedV1 request for next unsafe payload.
   282  // It returns true if the status is acceptable.
   283  func (e *EngineController) checkForkchoiceUpdatedStatus(status eth.ExecutePayloadStatus) bool {
   284  	if e.syncMode == sync.ELSync {
   285  		if status == eth.ExecutionValid && e.syncStatus == syncStatusStartedEL {
   286  			e.syncStatus = syncStatusFinishedELButNotFinalized
   287  		}
   288  		// Allow SYNCING if engine P2P sync is enabled
   289  		return status == eth.ExecutionValid || status == eth.ExecutionSyncing
   290  	}
   291  	return status == eth.ExecutionValid
   292  }
   293  
   294  // TryUpdateEngine attempts to update the engine with the current forkchoice state of the rollup node,
   295  // this is a no-op if the nodes already agree on the forkchoice state.
   296  func (e *EngineController) TryUpdateEngine(ctx context.Context) error {
   297  	if !e.needFCUCall {
   298  		return errNoFCUNeeded
   299  	}
   300  	if e.IsEngineSyncing() {
   301  		e.log.Warn("Attempting to update forkchoice state while EL syncing")
   302  	}
   303  	fc := eth.ForkchoiceState{
   304  		HeadBlockHash:      e.unsafeHead.Hash,
   305  		SafeBlockHash:      e.safeHead.Hash,
   306  		FinalizedBlockHash: e.finalizedHead.Hash,
   307  	}
   308  	_, err := e.engine.ForkchoiceUpdate(ctx, &fc, nil)
   309  	if err != nil {
   310  		var inputErr eth.InputError
   311  		if errors.As(err, &inputErr) {
   312  			switch inputErr.Code {
   313  			case eth.InvalidForkchoiceState:
   314  				return NewResetError(fmt.Errorf("forkchoice update was inconsistent with engine, need reset to resolve: %w", inputErr.Unwrap()))
   315  			default:
   316  				return NewTemporaryError(fmt.Errorf("unexpected error code in forkchoice-updated response: %w", err))
   317  			}
   318  		} else {
   319  			return NewTemporaryError(fmt.Errorf("failed to sync forkchoice with engine: %w", err))
   320  		}
   321  	}
   322  	e.needFCUCall = false
   323  	return nil
   324  }
   325  
   326  func (e *EngineController) InsertUnsafePayload(ctx context.Context, envelope *eth.ExecutionPayloadEnvelope, ref eth.L2BlockRef) error {
   327  	// Check if there is a finalized head once when doing EL sync. If so, transition to CL sync
   328  	if e.syncStatus == syncStatusWillStartEL {
   329  		b, err := e.engine.L2BlockRefByLabel(ctx, eth.Finalized)
   330  		isTransitionBlock := e.rollupCfg.Genesis.L2.Number != 0 && b.Hash == e.rollupCfg.Genesis.L2.Hash
   331  		if errors.Is(err, ethereum.NotFound) || isTransitionBlock {
   332  			e.syncStatus = syncStatusStartedEL
   333  			e.log.Info("Starting EL sync")
   334  			e.elStart = e.clock.Now()
   335  		} else if err == nil {
   336  			e.syncStatus = syncStatusFinishedEL
   337  			e.log.Info("Skipping EL sync and going straight to CL sync because there is a finalized block", "id", b.ID())
   338  			return nil
   339  		} else {
   340  			return NewTemporaryError(fmt.Errorf("failed to fetch finalized head: %w", err))
   341  		}
   342  	}
   343  	// Insert the payload & then call FCU
   344  	status, err := e.engine.NewPayload(ctx, envelope.ExecutionPayload, envelope.ParentBeaconBlockRoot)
   345  	if err != nil {
   346  		return NewTemporaryError(fmt.Errorf("failed to update insert payload: %w", err))
   347  	}
   348  	if !e.checkNewPayloadStatus(status.Status) {
   349  		payload := envelope.ExecutionPayload
   350  		return NewTemporaryError(fmt.Errorf("cannot process unsafe payload: new - %v; parent: %v; err: %w",
   351  			payload.ID(), payload.ParentID(), eth.NewPayloadErr(payload, status)))
   352  	}
   353  
   354  	// Mark the new payload as valid
   355  	fc := eth.ForkchoiceState{
   356  		HeadBlockHash:      envelope.ExecutionPayload.BlockHash,
   357  		SafeBlockHash:      e.safeHead.Hash,
   358  		FinalizedBlockHash: e.finalizedHead.Hash,
   359  	}
   360  	if e.syncStatus == syncStatusFinishedELButNotFinalized {
   361  		fc.SafeBlockHash = envelope.ExecutionPayload.BlockHash
   362  		fc.FinalizedBlockHash = envelope.ExecutionPayload.BlockHash
   363  		e.SetSafeHead(ref)
   364  		e.SetFinalizedHead(ref)
   365  	}
   366  	fcRes, err := e.engine.ForkchoiceUpdate(ctx, &fc, nil)
   367  	if err != nil {
   368  		var inputErr eth.InputError
   369  		if errors.As(err, &inputErr) {
   370  			switch inputErr.Code {
   371  			case eth.InvalidForkchoiceState:
   372  				return NewResetError(fmt.Errorf("pre-unsafe-block forkchoice update was inconsistent with engine, need reset to resolve: %w", inputErr.Unwrap()))
   373  			default:
   374  				return NewTemporaryError(fmt.Errorf("unexpected error code in forkchoice-updated response: %w", err))
   375  			}
   376  		} else {
   377  			return NewTemporaryError(fmt.Errorf("failed to update forkchoice to prepare for new unsafe payload: %w", err))
   378  		}
   379  	}
   380  	if !e.checkForkchoiceUpdatedStatus(fcRes.PayloadStatus.Status) {
   381  		payload := envelope.ExecutionPayload
   382  		return NewTemporaryError(fmt.Errorf("cannot prepare unsafe chain for new payload: new - %v; parent: %v; err: %w",
   383  			payload.ID(), payload.ParentID(), eth.ForkchoiceUpdateErr(fcRes.PayloadStatus)))
   384  	}
   385  	e.SetUnsafeHead(ref)
   386  	e.needFCUCall = false
   387  
   388  	if e.syncStatus == syncStatusFinishedELButNotFinalized {
   389  		e.log.Info("Finished EL sync", "sync_duration", e.clock.Since(e.elStart), "finalized_block", ref.ID().String())
   390  		e.syncStatus = syncStatusFinishedEL
   391  	}
   392  
   393  	return nil
   394  }
   395  
   396  // shouldTryBackupUnsafeReorg checks reorging(restoring) unsafe head to backupUnsafeHead is needed.
   397  // Returns boolean which decides to trigger FCU.
   398  func (e *EngineController) shouldTryBackupUnsafeReorg() bool {
   399  	if !e.needFCUCallForBackupUnsafeReorg {
   400  		return false
   401  	}
   402  	// This method must be never called when EL sync. If EL sync is in progress, early return.
   403  	if e.IsEngineSyncing() {
   404  		e.log.Warn("Attempting to unsafe reorg using backupUnsafe while EL syncing")
   405  		return false
   406  	}
   407  	if e.BackupUnsafeL2Head() == (eth.L2BlockRef{}) { // sanity check backupUnsafeHead is there
   408  		e.log.Warn("Attempting to unsafe reorg using backupUnsafe even though it is empty")
   409  		e.SetBackupUnsafeL2Head(eth.L2BlockRef{}, false)
   410  		return false
   411  	}
   412  	return true
   413  }
   414  
   415  // TryBackupUnsafeReorg attempts to reorg(restore) unsafe head to backupUnsafeHead.
   416  // If succeeds, update current forkchoice state to the rollup node.
   417  func (e *EngineController) TryBackupUnsafeReorg(ctx context.Context) (bool, error) {
   418  	if !e.shouldTryBackupUnsafeReorg() {
   419  		// Do not need to perform FCU.
   420  		return false, nil
   421  	}
   422  	// Only try FCU once because execution engine may forgot backupUnsafeHead
   423  	// or backupUnsafeHead is not part of the chain.
   424  	// Exception: Retry when forkChoiceUpdate returns non-input error.
   425  	e.needFCUCallForBackupUnsafeReorg = false
   426  	// Reorg unsafe chain. Safe/Finalized chain will not be updated.
   427  	e.log.Warn("trying to restore unsafe head", "backupUnsafe", e.backupUnsafeHead.ID(), "unsafe", e.unsafeHead.ID())
   428  	fc := eth.ForkchoiceState{
   429  		HeadBlockHash:      e.backupUnsafeHead.Hash,
   430  		SafeBlockHash:      e.safeHead.Hash,
   431  		FinalizedBlockHash: e.finalizedHead.Hash,
   432  	}
   433  	fcRes, err := e.engine.ForkchoiceUpdate(ctx, &fc, nil)
   434  	if err != nil {
   435  		var inputErr eth.InputError
   436  		if errors.As(err, &inputErr) {
   437  			e.SetBackupUnsafeL2Head(eth.L2BlockRef{}, false)
   438  			switch inputErr.Code {
   439  			case eth.InvalidForkchoiceState:
   440  				return true, NewResetError(fmt.Errorf("forkchoice update was inconsistent with engine, need reset to resolve: %w", inputErr.Unwrap()))
   441  			default:
   442  				return true, NewTemporaryError(fmt.Errorf("unexpected error code in forkchoice-updated response: %w", err))
   443  			}
   444  		} else {
   445  			// Retry when forkChoiceUpdate returns non-input error.
   446  			// Do not reset backupUnsafeHead because it will be used again.
   447  			e.needFCUCallForBackupUnsafeReorg = true
   448  			return true, NewTemporaryError(fmt.Errorf("failed to sync forkchoice with engine: %w", err))
   449  		}
   450  	}
   451  	if fcRes.PayloadStatus.Status == eth.ExecutionValid {
   452  		// Execution engine accepted the reorg.
   453  		e.log.Info("successfully reorged unsafe head using backupUnsafe", "unsafe", e.backupUnsafeHead.ID())
   454  		e.SetUnsafeHead(e.BackupUnsafeL2Head())
   455  		e.SetBackupUnsafeL2Head(eth.L2BlockRef{}, false)
   456  		return true, nil
   457  	}
   458  	e.SetBackupUnsafeL2Head(eth.L2BlockRef{}, false)
   459  	// Execution engine could not reorg back to previous unsafe head.
   460  	return true, NewTemporaryError(fmt.Errorf("cannot restore unsafe chain using backupUnsafe: err: %w",
   461  		eth.ForkchoiceUpdateErr(fcRes.PayloadStatus)))
   462  }
   463  
   464  // ResetBuildingState implements LocalEngineControl.
   465  func (e *EngineController) ResetBuildingState() {
   466  	e.resetBuildingState()
   467  }