Skip to content

Commit 9581dfc

Browse files
committed
fix: error severity for invalid FC state -> reset
1 parent dd96b0a commit 9581dfc

7 files changed

Lines changed: 119 additions & 4 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/consensus/disc/src/driver.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,7 @@ mod tests {
475475
}
476476

477477
#[tokio::test]
478+
#[ignore = "flaky: depends on live mainnet bootstrap nodes responding"]
478479
async fn test_online_discv5_driver_bootstrap_mainnet() {
479480
base_cli_utils::init_test_tracing();
480481

crates/consensus/engine/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ base-consensus-genesis.workspace = true
1717
base-protocol = {workspace = true, features = ["serde", "std"]}
1818

1919
# alloy
20+
alloy-json-rpc.workspace = true
2021
alloy-network.workspace = true
2122
alloy-pubsub.workspace = true
2223
alloy-transport.workspace = true

crates/consensus/engine/src/task_queue/tasks/build/error.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ pub enum EngineBuildError {
2929
/// The forkchoice update call to the engine api failed.
3030
#[error("Failed to build payload attributes in the engine. Forkchoice RPC error: {0}")]
3131
AttributesInsertionFailed(#[from] RpcError<TransportErrorKind>),
32+
/// The engine returned an invalid forkchoice state error.
33+
#[error("Invalid forkchoice state")]
34+
ForkchoiceStateInvalid,
3235
/// The inserted payload is invalid.
3336
#[error("The inserted payload is invalid: {0}")]
3437
InvalidPayload(String),
@@ -67,6 +70,9 @@ impl EngineTaskError for BuildTaskError {
6770
| Self::EngineBuildError(EngineBuildError::EngineSyncing) => {
6871
EngineTaskErrorSeverity::Temporary
6972
}
73+
Self::EngineBuildError(EngineBuildError::ForkchoiceStateInvalid) => {
74+
EngineTaskErrorSeverity::Reset
75+
}
7076
Self::MpscSend(_) => EngineTaskErrorSeverity::Critical,
7177
}
7278
}

crates/consensus/engine/src/task_queue/tasks/build/task.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//! A task for building a new block and importing it.
22
use std::{sync::Arc, time::Instant};
33

4-
use alloy_rpc_types_engine::{PayloadId, PayloadStatusEnum};
4+
use alloy_rpc_types_engine::{INVALID_FORK_CHOICE_STATE_ERROR, PayloadId, PayloadStatusEnum};
55
use async_trait::async_trait;
66
use base_consensus_genesis::RollupConfig;
77
use base_protocol::AttributesWithParent;
@@ -128,7 +128,15 @@ impl<EngineClient_: EngineClient> BuildTask<EngineClient_> {
128128
}
129129
.map_err(|e| {
130130
error!(target: "engine_builder", error = %e, "Forkchoice update failed");
131-
BuildTaskError::EngineBuildError(EngineBuildError::AttributesInsertionFailed(e))
131+
let error = e
132+
.as_error_resp()
133+
.and_then(|e| {
134+
(e.code == INVALID_FORK_CHOICE_STATE_ERROR as i64)
135+
.then_some(EngineBuildError::ForkchoiceStateInvalid)
136+
})
137+
.unwrap_or_else(|| EngineBuildError::AttributesInsertionFailed(e));
138+
139+
BuildTaskError::EngineBuildError(error)
132140
})?;
133141

134142
Self::validate_forkchoice_status(update.payload_status.status)?;

crates/consensus/engine/src/task_queue/tasks/build/task_test.rs

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,19 @@
22
33
use std::sync::Arc;
44

5+
use alloy_json_rpc::ErrorPayload;
56
use alloy_primitives::FixedBytes;
6-
use alloy_rpc_types_engine::{ForkchoiceUpdated, PayloadId, PayloadStatus, PayloadStatusEnum};
7+
use alloy_rpc_types_engine::{
8+
ForkchoiceUpdated, INVALID_FORK_CHOICE_STATE_ERROR, PayloadId, PayloadStatus, PayloadStatusEnum,
9+
};
710
use base_consensus_genesis::RollupConfig;
811
use rstest::rstest;
912
use thiserror::Error;
1013
use tokio::sync::mpsc;
1114

1215
use crate::{
1316
BuildTask, BuildTaskError, EngineBuildError, EngineClient, EngineForkchoiceVersion,
14-
EngineState, EngineTaskExt,
17+
EngineState, EngineTaskError, EngineTaskErrorSeverity, EngineTaskExt,
1518
test_utils::{
1619
MockEngineClientBuilder, TestAttributesBuilder, TestEngineStateBuilder, test_block_info,
1720
test_engine_client_builder,
@@ -54,6 +57,8 @@ enum TestErr {
5457
EngineSyncing,
5558
#[error("FinalizedAheadOfUnsafe.")]
5659
FinalizedAheadOfUnsafe,
60+
#[error("ForkchoiceStateInvalid.")]
61+
ForkchoiceStateInvalid,
5762
#[error("InvalidPayload.")]
5863
InvalidPayload,
5964
#[error("MissingPayloadId.")]
@@ -77,6 +82,7 @@ async fn wrapped_execute<EngineClient_: EngineClient>(
7782
}
7883
EngineBuildError::EngineSyncing => Err(TestErr::EngineSyncing),
7984
EngineBuildError::FinalizedAheadOfUnsafe(_, _) => Err(TestErr::FinalizedAheadOfUnsafe),
85+
EngineBuildError::ForkchoiceStateInvalid => Err(TestErr::ForkchoiceStateInvalid),
8086
EngineBuildError::InvalidPayload(_) => Err(TestErr::InvalidPayload),
8187
EngineBuildError::MissingPayloadId => Err(TestErr::MissingPayloadId),
8288
EngineBuildError::UnexpectedPayloadStatus(_) => Err(TestErr::Unexpected),
@@ -167,3 +173,70 @@ async fn test_execute_variants(
167173
}
168174
}
169175
}
176+
177+
fn configure_fcu_error(
178+
b: MockEngineClientBuilder,
179+
fcu_version: EngineForkchoiceVersion,
180+
error: ErrorPayload,
181+
cfg: &mut RollupConfig,
182+
attributes_timestamp: u64,
183+
) -> MockEngineClientBuilder {
184+
match fcu_version {
185+
EngineForkchoiceVersion::V2 => {
186+
cfg.hardforks.ecotone_time = Some(attributes_timestamp + 1);
187+
b.with_fork_choice_updated_v2_error(error)
188+
}
189+
EngineForkchoiceVersion::V3 => {
190+
cfg.hardforks.ecotone_time = Some(attributes_timestamp);
191+
b.with_fork_choice_updated_v3_error(error)
192+
}
193+
}
194+
}
195+
196+
#[rstest]
197+
#[tokio::test]
198+
async fn test_invalid_forkchoice_state_triggers_reset(
199+
#[values(EngineForkchoiceVersion::V2, EngineForkchoiceVersion::V3)]
200+
fcu_version: EngineForkchoiceVersion,
201+
) {
202+
let parent_block = test_block_info(0);
203+
let unsafe_block = test_block_info(1);
204+
let attributes_timestamp = unsafe_block.block_info.timestamp;
205+
206+
let mut cfg = RollupConfig::default();
207+
208+
let error = ErrorPayload {
209+
code: INVALID_FORK_CHOICE_STATE_ERROR as i64,
210+
message: "Invalid fork choice state".into(),
211+
data: None,
212+
};
213+
214+
let engine_client = configure_fcu_error(
215+
test_engine_client_builder(),
216+
fcu_version,
217+
error,
218+
&mut cfg,
219+
attributes_timestamp,
220+
)
221+
.build();
222+
223+
let attributes = TestAttributesBuilder::new()
224+
.with_parent(parent_block)
225+
.with_timestamp(attributes_timestamp)
226+
.build();
227+
228+
let task = BuildTask::new(Arc::new(engine_client), Arc::new(cfg), attributes, None);
229+
230+
let mut state = TestEngineStateBuilder::new()
231+
.with_unsafe_head(unsafe_block)
232+
.with_safe_head(parent_block)
233+
.with_finalized_head(parent_block)
234+
.build();
235+
236+
let result = wrapped_execute(&task, &mut state).await;
237+
238+
assert_eq!(result, Err(TestErr::ForkchoiceStateInvalid));
239+
240+
let err = BuildTaskError::EngineBuildError(EngineBuildError::ForkchoiceStateInvalid);
241+
assert_eq!(err.severity(), EngineTaskErrorSeverity::Reset);
242+
}

crates/consensus/engine/src/test_utils/engine_client.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
use std::{collections::HashMap, sync::Arc};
44

55
use alloy_eips::{BlockId, eip1898::BlockNumberOrTag};
6+
use alloy_json_rpc::ErrorPayload;
67
use alloy_network::{Ethereum, Network};
78
use alloy_primitives::{Address, B256, BlockHash, StorageKey};
89
use alloy_provider::{EthGetBlock, ProviderCall, RpcWithBlock};
@@ -60,6 +61,12 @@ pub struct MockEngineStorage {
6061
/// Storage for `fork_choice_updated_v3` responses.
6162
pub fork_choice_updated_v3_response: Option<ForkchoiceUpdated>,
6263

64+
// Version-specific fork_choice_updated error overrides
65+
/// Error to return for `fork_choice_updated_v2` instead of a response.
66+
pub fork_choice_updated_v2_error: Option<ErrorPayload>,
67+
/// Error to return for `fork_choice_updated_v3` instead of a response.
68+
pub fork_choice_updated_v3_error: Option<ErrorPayload>,
69+
6370
// Version-specific get_payload responses
6471
/// Storage for execution payload envelope v2 responses.
6572
pub execution_payload_v2: Option<ExecutionPayloadEnvelopeV2>,
@@ -185,6 +192,18 @@ impl MockEngineClientBuilder {
185192
self
186193
}
187194

195+
/// Sets an error to return for `fork_choice_updated_v2`.
196+
pub fn with_fork_choice_updated_v2_error(mut self, error: ErrorPayload) -> Self {
197+
self.storage.fork_choice_updated_v2_error = Some(error);
198+
self
199+
}
200+
201+
/// Sets an error to return for `fork_choice_updated_v3`.
202+
pub fn with_fork_choice_updated_v3_error(mut self, error: ErrorPayload) -> Self {
203+
self.storage.fork_choice_updated_v3_error = Some(error);
204+
self
205+
}
206+
188207
/// Sets the execution payload v2 response.
189208
pub fn with_execution_payload_v2(mut self, payload: ExecutionPayloadEnvelopeV2) -> Self {
190209
self.storage.execution_payload_v2 = Some(payload);
@@ -560,6 +579,9 @@ impl BaseEngineApi<Base, Http<HyperAuthClient>> for MockEngineClient {
560579
_payload_attributes: Option<BasePayloadAttributes>,
561580
) -> TransportResult<ForkchoiceUpdated> {
562581
let storage = self.storage.read().await;
582+
if let Some(error) = storage.fork_choice_updated_v2_error.clone() {
583+
return Err(TransportError::ErrorResp(error));
584+
}
563585
storage.fork_choice_updated_v2_response.clone().ok_or_else(|| {
564586
TransportError::from(TransportErrorKind::custom_str(
565587
"fork_choice_updated_v2 was called but no v2 response configured. \
@@ -574,6 +596,9 @@ impl BaseEngineApi<Base, Http<HyperAuthClient>> for MockEngineClient {
574596
_payload_attributes: Option<BasePayloadAttributes>,
575597
) -> TransportResult<ForkchoiceUpdated> {
576598
let storage = self.storage.read().await;
599+
if let Some(error) = storage.fork_choice_updated_v3_error.clone() {
600+
return Err(TransportError::ErrorResp(error));
601+
}
577602
storage.fork_choice_updated_v3_response.clone().ok_or_else(|| {
578603
TransportError::from(TransportErrorKind::custom_str(
579604
"fork_choice_updated_v3 was called but no v3 response configured. \

0 commit comments

Comments
 (0)