-
Notifications
You must be signed in to change notification settings - Fork 212
fix(node/engine): audit engine for bugs. rename forkchoice task -> synchronize task. move block building logic to build task #2524
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,7 +1,7 @@ | ||||||||||
| //! Contains error types for the [crate::ForkchoiceTask]. | ||||||||||
| //! Contains error types for the [crate::SynchronizeTask]. | ||||||||||
|
|
||||||||||
| use crate::{ | ||||||||||
| EngineTaskError, ForkchoiceTaskError, InsertTaskError, | ||||||||||
| EngineTaskError, InsertTaskError, SynchronizeTaskError, | ||||||||||
| task_queue::tasks::task::EngineTaskErrorSeverity, | ||||||||||
| }; | ||||||||||
| use alloy_rpc_types_engine::PayloadStatusEnum; | ||||||||||
|
|
@@ -11,33 +11,44 @@ use op_alloy_rpc_types_engine::OpExecutionPayloadEnvelope; | |||||||||
| use thiserror::Error; | ||||||||||
| use tokio::sync::mpsc; | ||||||||||
|
|
||||||||||
| /// An error that occurs when running the [crate::ForkchoiceTask]. | ||||||||||
| /// An error that occurs when building a payload in the engine. | ||||||||||
| #[derive(Debug, Error)] | ||||||||||
| pub enum BuildTaskError { | ||||||||||
| /// The forkchoice update is not needed. | ||||||||||
| #[error("No forkchoice update needed")] | ||||||||||
| NoForkchoiceUpdateNeeded, | ||||||||||
| pub enum EngineBuildError { | ||||||||||
| /// The finalized head is ahead of the unsafe head. | ||||||||||
| #[error("Finalized head is ahead of unsafe head")] | ||||||||||
| FinalizedAheadOfUnsafe(u64, u64), | ||||||||||
| /// The forkchoice update call to the engine api failed. | ||||||||||
| #[error("Failed to build payload attributes in the engine. Forkchoice RPC error: {0}")] | ||||||||||
| AttributesInsertionFailed(#[from] RpcError<TransportErrorKind>), | ||||||||||
| /// The inserted payload is invalid. | ||||||||||
| #[error("The inserted payload is invalid: {0}")] | ||||||||||
| InvalidPayload(String), | ||||||||||
| /// The inserted payload status is unexpected. | ||||||||||
| #[error("The inserted payload status is unexpected: {0}")] | ||||||||||
| UnexpectedPayloadStatus(PayloadStatusEnum), | ||||||||||
| /// The payload ID is missing. | ||||||||||
| #[error("The inserted payload ID is missing")] | ||||||||||
| MissingPayloadId, | ||||||||||
| /// The engine is syncing. | ||||||||||
| #[error("Attempting to update forkchoice state while EL syncing")] | ||||||||||
| #[error("The engine is syncing")] | ||||||||||
| EngineSyncing, | ||||||||||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Those variants are the same as before #2388 https://github.com/op-rs/kona/pull/2388/files |
||||||||||
| /// Missing payload ID. | ||||||||||
| #[error("Missing payload ID")] | ||||||||||
| MissingPayloadId, | ||||||||||
| } | ||||||||||
|
|
||||||||||
| /// An error that occurs when running the [crate::SynchronizeTask]. | ||||||||||
| #[derive(Debug, Error)] | ||||||||||
| pub enum BuildTaskError { | ||||||||||
| /// An error occurred when building the payload attributes in the engine. | ||||||||||
| #[error("An error occurred when building the payload attributes to the engine.")] | ||||||||||
| EngineBuildError(EngineBuildError), | ||||||||||
| /// The initial forkchoice update call to the engine api failed. | ||||||||||
| #[error(transparent)] | ||||||||||
| ForkchoiceUpdateFailed(#[from] ForkchoiceTaskError), | ||||||||||
| ForkchoiceUpdateFailed(#[from] SynchronizeTaskError), | ||||||||||
| /// Impossible to insert the payload into the engine. | ||||||||||
| #[error(transparent)] | ||||||||||
| PayloadInsertionFailed(#[from] InsertTaskError), | ||||||||||
| /// Unexpected payload status | ||||||||||
| #[error("Unexpected payload status: {0}")] | ||||||||||
| UnexpectedPayloadStatus(PayloadStatusEnum), | ||||||||||
| /// The get payload call to the engine api failed. | ||||||||||
| #[error(transparent)] | ||||||||||
| GetPayloadFailed(RpcError<TransportErrorKind>), | ||||||||||
| /// The new payload call to the engine api failed. | ||||||||||
| #[error(transparent)] | ||||||||||
| NewPayloadFailed(RpcError<TransportErrorKind>), | ||||||||||
| /// A deposit-only payload failed to import. | ||||||||||
| #[error("Deposit-only payload failed to import")] | ||||||||||
| DepositOnlyPayloadFailed, | ||||||||||
|
|
@@ -64,13 +75,26 @@ impl EngineTaskError for BuildTaskError { | |||||||||
| match self { | ||||||||||
| Self::ForkchoiceUpdateFailed(inner) => inner.severity(), | ||||||||||
| Self::PayloadInsertionFailed(inner) => inner.severity(), | ||||||||||
| Self::NoForkchoiceUpdateNeeded => EngineTaskErrorSeverity::Temporary, | ||||||||||
| Self::EngineSyncing => EngineTaskErrorSeverity::Temporary, | ||||||||||
| Self::EngineBuildError(EngineBuildError::FinalizedAheadOfUnsafe(_, _)) => { | ||||||||||
| EngineTaskErrorSeverity::Critical | ||||||||||
| } | ||||||||||
| Self::EngineBuildError(EngineBuildError::AttributesInsertionFailed(_)) => { | ||||||||||
| EngineTaskErrorSeverity::Temporary | ||||||||||
| } | ||||||||||
| Self::EngineBuildError(EngineBuildError::InvalidPayload(_)) => { | ||||||||||
| EngineTaskErrorSeverity::Temporary | ||||||||||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This also should probably be
Suggested change
|
||||||||||
| } | ||||||||||
| Self::EngineBuildError(EngineBuildError::UnexpectedPayloadStatus(_)) => { | ||||||||||
| EngineTaskErrorSeverity::Temporary | ||||||||||
| } | ||||||||||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To check: I think that this should instead be
Suggested change
The |
||||||||||
| Self::EngineBuildError(EngineBuildError::MissingPayloadId) => { | ||||||||||
| EngineTaskErrorSeverity::Temporary | ||||||||||
| } | ||||||||||
| Self::EngineBuildError(EngineBuildError::EngineSyncing) => { | ||||||||||
| EngineTaskErrorSeverity::Temporary | ||||||||||
| } | ||||||||||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note here that:
|
||||||||||
| Self::GetPayloadFailed(_) => EngineTaskErrorSeverity::Temporary, | ||||||||||
| Self::NewPayloadFailed(_) => EngineTaskErrorSeverity::Temporary, | ||||||||||
| Self::HoloceneInvalidFlush => EngineTaskErrorSeverity::Flush, | ||||||||||
| Self::MissingPayloadId => EngineTaskErrorSeverity::Critical, | ||||||||||
| Self::UnexpectedPayloadStatus(_) => EngineTaskErrorSeverity::Critical, | ||||||||||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Some of these variants were either unused or outdated |
||||||||||
| Self::DepositOnlyPayloadReattemptFailed => EngineTaskErrorSeverity::Critical, | ||||||||||
| Self::DepositOnlyPayloadFailed => EngineTaskErrorSeverity::Critical, | ||||||||||
| Self::FromBlock(_) => EngineTaskErrorSeverity::Critical, | ||||||||||
|
|
||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,12 +1,13 @@ | ||||||||||||||||||||
| //! A task for building a new block and importing it. | ||||||||||||||||||||
| use super::BuildTaskError; | ||||||||||||||||||||
| use crate::{ | ||||||||||||||||||||
| EngineClient, EngineGetPayloadVersion, EngineState, EngineTaskExt, ForkchoiceTask, | ||||||||||||||||||||
| ForkchoiceTaskError, InsertTask, | ||||||||||||||||||||
| EngineClient, EngineForkchoiceVersion, EngineGetPayloadVersion, EngineState, EngineTaskExt, | ||||||||||||||||||||
| InsertTask, | ||||||||||||||||||||
| InsertTaskError::{self}, | ||||||||||||||||||||
| state::EngineSyncStateUpdate, | ||||||||||||||||||||
| task_queue::tasks::build::error::EngineBuildError, | ||||||||||||||||||||
| }; | ||||||||||||||||||||
| use alloy_rpc_types_engine::{ExecutionPayload, PayloadId}; | ||||||||||||||||||||
| use alloy_rpc_types_engine::{ExecutionPayload, PayloadId, PayloadStatusEnum}; | ||||||||||||||||||||
| use async_trait::async_trait; | ||||||||||||||||||||
| use kona_genesis::RollupConfig; | ||||||||||||||||||||
| use kona_protocol::{L2BlockInfo, OpAttributesWithParent}; | ||||||||||||||||||||
|
|
@@ -43,6 +44,112 @@ impl BuildTask { | |||||||||||||||||||
| Self { engine, cfg, attributes, is_attributes_derived, payload_tx } | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| /// Starts the block building process by sending an initial `engine_forkchoiceUpdate` call with | ||||||||||||||||||||
| /// the payload attributes to build. | ||||||||||||||||||||
| /// | ||||||||||||||||||||
| /// ## Observed [PayloadStatusEnum] Variants | ||||||||||||||||||||
| /// The `engine_forkchoiceUpdate` payload statuses that this function observes are below. Any | ||||||||||||||||||||
| /// other [PayloadStatusEnum] variant is considered a failure. | ||||||||||||||||||||
| /// | ||||||||||||||||||||
| /// ### Success (`VALID`) | ||||||||||||||||||||
| /// If the build is successful, the [PayloadId] is returned for sealing and the external | ||||||||||||||||||||
| /// actor is notified of the successful forkchoice update. | ||||||||||||||||||||
| /// | ||||||||||||||||||||
| /// ### Failure (`INVALID`) | ||||||||||||||||||||
| /// If the forkchoice update fails, the external actor is notified of the failure. | ||||||||||||||||||||
| /// | ||||||||||||||||||||
| /// ### Syncing (`SYNCING`) | ||||||||||||||||||||
| /// If the EL is syncing, the payload attributes are buffered and the function returns early. | ||||||||||||||||||||
| /// This is a temporary state, and the function should be called again later. | ||||||||||||||||||||
| async fn start_build( | ||||||||||||||||||||
| &self, | ||||||||||||||||||||
| state: &EngineState, | ||||||||||||||||||||
| engine_client: &EngineClient, | ||||||||||||||||||||
| attributes_envelope: OpAttributesWithParent, | ||||||||||||||||||||
| ) -> Result<PayloadId, BuildTaskError> { | ||||||||||||||||||||
| debug!( | ||||||||||||||||||||
| target: "engine_builder", | ||||||||||||||||||||
| txs = attributes_envelope.inner.transactions.as_ref().map_or(0, |txs| txs.len()), | ||||||||||||||||||||
| "Starting new build job" | ||||||||||||||||||||
| ); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // Sanity check if the head is behind the finalized head. If it is, this is a critical | ||||||||||||||||||||
| // error. | ||||||||||||||||||||
| if state.sync_state.unsafe_head().block_info.number < | ||||||||||||||||||||
| state.sync_state.finalized_head().block_info.number | ||||||||||||||||||||
| { | ||||||||||||||||||||
| return Err(BuildTaskError::EngineBuildError(EngineBuildError::FinalizedAheadOfUnsafe( | ||||||||||||||||||||
| state.sync_state.unsafe_head().block_info.number, | ||||||||||||||||||||
| state.sync_state.finalized_head().block_info.number, | ||||||||||||||||||||
| ))); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // When inserting a payload, we advertise the parent's unsafe head as the current unsafe | ||||||||||||||||||||
| // head to build on top of. | ||||||||||||||||||||
| let new_forkchoice = state | ||||||||||||||||||||
| .sync_state | ||||||||||||||||||||
| .apply_update(EngineSyncStateUpdate { | ||||||||||||||||||||
| unsafe_head: Some(attributes_envelope.parent), | ||||||||||||||||||||
| ..Default::default() | ||||||||||||||||||||
| }) | ||||||||||||||||||||
| .create_forkchoice_state(); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| let forkchoice_version = EngineForkchoiceVersion::from_cfg( | ||||||||||||||||||||
| &self.cfg, | ||||||||||||||||||||
| attributes_envelope.inner.payload_attributes.timestamp, | ||||||||||||||||||||
| ); | ||||||||||||||||||||
| let update = match forkchoice_version { | ||||||||||||||||||||
| EngineForkchoiceVersion::V3 => { | ||||||||||||||||||||
| engine_client | ||||||||||||||||||||
| .fork_choice_updated_v3(new_forkchoice, Some(attributes_envelope.inner)) | ||||||||||||||||||||
| .await | ||||||||||||||||||||
| } | ||||||||||||||||||||
| EngineForkchoiceVersion::V2 => { | ||||||||||||||||||||
| engine_client | ||||||||||||||||||||
| .fork_choice_updated_v2(new_forkchoice, Some(attributes_envelope.inner)) | ||||||||||||||||||||
| .await | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
| .map_err(|e| { | ||||||||||||||||||||
| error!(target: "engine_builder", "Forkchoice update failed: {}", e); | ||||||||||||||||||||
| BuildTaskError::EngineBuildError(EngineBuildError::AttributesInsertionFailed(e)) | ||||||||||||||||||||
| })?; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| match update.payload_status.status { | ||||||||||||||||||||
| PayloadStatusEnum::Valid => { | ||||||||||||||||||||
| debug!( | ||||||||||||||||||||
| target: "engine_builder", | ||||||||||||||||||||
| unsafe_hash = new_forkchoice.head_block_hash.to_string(), | ||||||||||||||||||||
| safe_hash = new_forkchoice.safe_block_hash.to_string(), | ||||||||||||||||||||
| finalized_hash = new_forkchoice.finalized_block_hash.to_string(), | ||||||||||||||||||||
| "Forkchoice update with attributes successful" | ||||||||||||||||||||
| ); | ||||||||||||||||||||
| } | ||||||||||||||||||||
| PayloadStatusEnum::Invalid { validation_error } => { | ||||||||||||||||||||
| error!(target: "engine_builder", "Forkchoice update failed: {}", validation_error); | ||||||||||||||||||||
| return Err(BuildTaskError::EngineBuildError(EngineBuildError::InvalidPayload( | ||||||||||||||||||||
| validation_error, | ||||||||||||||||||||
| ))); | ||||||||||||||||||||
| } | ||||||||||||||||||||
| PayloadStatusEnum::Syncing => { | ||||||||||||||||||||
| warn!(target: "engine_builder", "Forkchoice update failed temporarily: EL is syncing"); | ||||||||||||||||||||
| return Err(BuildTaskError::EngineBuildError(EngineBuildError::EngineSyncing)); | ||||||||||||||||||||
| } | ||||||||||||||||||||
| s => { | ||||||||||||||||||||
| // Other codes are never returned by `engine_forkchoiceUpdate` | ||||||||||||||||||||
| return Err(BuildTaskError::EngineBuildError( | ||||||||||||||||||||
| EngineBuildError::UnexpectedPayloadStatus(s), | ||||||||||||||||||||
| )); | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // Fetch the payload ID from the FCU. If no payload ID was returned, something went wrong - | ||||||||||||||||||||
| // the block building job on the EL should have been initiated. | ||||||||||||||||||||
| update | ||||||||||||||||||||
| .payload_id | ||||||||||||||||||||
| .ok_or(BuildTaskError::EngineBuildError(EngineBuildError::MissingPayloadId)) | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||||||||||||||||
|
|
||||||||||||||||||||
| /// Fetches the execution payload from the EL. | ||||||||||||||||||||
| /// | ||||||||||||||||||||
| /// ## Engine Method Selection | ||||||||||||||||||||
|
|
@@ -129,18 +236,8 @@ impl EngineTaskExt for BuildTask { | |||||||||||||||||||
| // Start the build by sending an FCU call with the current forkchoice and the input | ||||||||||||||||||||
| // payload attributes. | ||||||||||||||||||||
| let fcu_start_time = Instant::now(); | ||||||||||||||||||||
| let payload_id = ForkchoiceTask::new( | ||||||||||||||||||||
| self.engine.clone(), | ||||||||||||||||||||
| self.cfg.clone(), | ||||||||||||||||||||
| EngineSyncStateUpdate { | ||||||||||||||||||||
| unsafe_head: Some(self.attributes.parent), | ||||||||||||||||||||
| ..Default::default() | ||||||||||||||||||||
| }, | ||||||||||||||||||||
| Some(self.attributes.clone()), | ||||||||||||||||||||
| ) | ||||||||||||||||||||
| .execute(state) | ||||||||||||||||||||
| .await? | ||||||||||||||||||||
| .ok_or(BuildTaskError::MissingPayloadId)?; | ||||||||||||||||||||
| let payload_id = self.start_build(state, &self.engine, self.attributes.clone()).await?; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| let fcu_duration = fcu_start_time.elapsed(); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // Fetch the payload just inserted from the EL and import it into the engine. | ||||||||||||||||||||
|
|
@@ -166,18 +263,17 @@ impl EngineTaskExt for BuildTask { | |||||||||||||||||||
| .execute(state) | ||||||||||||||||||||
| .await | ||||||||||||||||||||
| { | ||||||||||||||||||||
| Err(InsertTaskError::ForkchoiceUpdateFailed( | ||||||||||||||||||||
| ForkchoiceTaskError::InvalidPayloadStatus(e), | ||||||||||||||||||||
| )) if self.attributes.is_deposits_only() => { | ||||||||||||||||||||
| Err(InsertTaskError::UnexpectedPayloadStatus(e)) | ||||||||||||||||||||
| if self.attributes.is_deposits_only() => | ||||||||||||||||||||
| { | ||||||||||||||||||||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This was a bug in the refactor. The error we should match on should be raised when we first try to call kona/crates/node/engine/src/task_queue/tasks/insert/task.rs Lines 112 to 120 in 572de05
InsertTask)
|
||||||||||||||||||||
| error!(target: "engine_builder", error = ?e, "Critical: Deposit-only payload import failed"); | ||||||||||||||||||||
| return Err(BuildTaskError::DepositOnlyPayloadFailed) | ||||||||||||||||||||
| } | ||||||||||||||||||||
| // HOLOCENE: Re-attempt payload import with deposits only | ||||||||||||||||||||
| Err(InsertTaskError::ForkchoiceUpdateFailed( | ||||||||||||||||||||
| ForkchoiceTaskError::InvalidPayloadStatus(e), | ||||||||||||||||||||
| )) if self | ||||||||||||||||||||
| .cfg | ||||||||||||||||||||
| .is_holocene_active(self.attributes.inner().payload_attributes.timestamp) => | ||||||||||||||||||||
| Err(InsertTaskError::UnexpectedPayloadStatus(e)) | ||||||||||||||||||||
|
Comment on lines
+266
to
+273
|
||||||||||||||||||||
| if self | ||||||||||||||||||||
| .cfg | ||||||||||||||||||||
| .is_holocene_active(self.attributes.inner().payload_attributes.timestamp) => | ||||||||||||||||||||
| { | ||||||||||||||||||||
| warn!(target: "engine_builder", error = ?e, "Re-attempting payload import with deposits only."); | ||||||||||||||||||||
| // HOLOCENE: Re-attempt payload import with deposits only | ||||||||||||||||||||
|
|
||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Motivation, the synchronize task can only fail if:
find_starting_forkchoiceis correctValidorEngineSyncingstatusIf we don't have a
whileloop here:invalid forkchoice state