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 }