fix(rpc-engine-api): enforce FCU SYNCING precedence over V3 payload attr (#22682)

Signed-off-by: Delweng <delweng@gmail.com>
This commit is contained in:
Delweng
2026-03-03 15:20:13 +08:00
committed by GitHub
parent 2d27a96d9a
commit 757d9c1c92
2 changed files with 119 additions and 13 deletions

View File

@@ -20,9 +20,7 @@ engine-withdrawals: [ ]
engine-api: [ ]
# no fix due to https://github.com/paradigmxyz/reth/issues/8732
engine-cancun:
- Invalid PayloadAttributes, Missing BeaconRoot, Syncing=True (Cancun) (reth)
engine-cancun: [ ]
sync: [ ]

View File

@@ -759,17 +759,11 @@ where
// MUST NOT begin a payload build process. In such an event, the forkchoiceState
// update MUST NOT be rolled back.
//
// NOTE: This will also apply to the validation result for the cancun or
// shanghai-specific fields provided in the payload attributes.
//
// To do this, we set the payload attrs to `None` if attribute validation failed, but
// we still apply the forkchoice update.
// NOTE: This also applies to cancun/shanghai-specific payload attributes.
if let Err(err) = attr_validation_res {
let fcu_res =
self.inner.beacon_consensus.fork_choice_updated(state, None, version).await?;
// TODO: decide if we want this branch - the FCU INVALID response might be more
// useful than the payload attributes INVALID response
if fcu_res.is_invalid() {
if fcu_res.is_invalid() || fcu_res.payload_status.is_syncing() {
return Ok(fcu_res)
}
return Err(err.into())
@@ -1344,10 +1338,13 @@ struct EngineApiInner<Provider, PayloadT: PayloadTypes, Pool, Validator, ChainSp
#[cfg(test)]
mod tests {
use super::*;
use alloy_rpc_types_engine::{ClientCode, ClientVersionV1};
use alloy_primitives::{Address, B256};
use alloy_rpc_types_engine::{
ClientCode, ClientVersionV1, PayloadAttributes, PayloadStatusEnum,
};
use assert_matches::assert_matches;
use reth_chainspec::{ChainSpec, ChainSpecBuilder, MAINNET};
use reth_engine_primitives::BeaconEngineMessage;
use reth_engine_primitives::{BeaconEngineMessage, OnForkChoiceUpdated};
use reth_ethereum_engine_primitives::EthEngineTypes;
use reth_ethereum_primitives::Block;
use reth_network_api::{
@@ -1505,6 +1502,117 @@ mod tests {
assert_matches!(res, Ok(None));
}
#[tokio::test]
async fn fcu_v3_syncing_precedes_invalid_payload_attributes_validation() {
let (mut handle, api) = setup_engine_api();
let state = ForkchoiceState {
head_block_hash: B256::from([0x11; 32]),
safe_block_hash: B256::ZERO,
finalized_block_hash: B256::ZERO,
};
let payload_attributes = PayloadAttributes {
timestamp: 1,
prev_randao: B256::ZERO,
suggested_fee_recipient: Address::ZERO,
withdrawals: Some(vec![]),
// Invalid for V3/Cancun, but should be ignored if forkchoice is SYNCING.
parent_beacon_block_root: None,
};
let api_task = tokio::spawn(async move {
api.fork_choice_updated_v3(state, Some(payload_attributes)).await
});
let request =
tokio::time::timeout(std::time::Duration::from_secs(1), handle.from_api.recv())
.await
.expect("timed out waiting for forkchoiceUpdated request")
.expect("expected forkchoiceUpdated request");
let response_tx = match request {
BeaconEngineMessage::ForkchoiceUpdated { payload_attrs, tx, .. } => {
assert!(
payload_attrs.is_none(),
"FCU for syncing state should be evaluated before payload attributes"
);
tx
}
other => panic!("unexpected engine message: {other:?}"),
};
response_tx.send(Ok(OnForkChoiceUpdated::syncing())).expect("send syncing response");
let response = api_task
.await
.expect("api task should not panic")
.expect("forkchoiceUpdatedV3 should return a syncing response");
assert!(response.payload_status.is_syncing());
assert!(response.payload_id.is_none());
}
#[tokio::test]
async fn fcu_v3_valid_forkchoice_missing_beacon_root_returns_invalid_attributes() {
let (mut handle, api) = setup_engine_api();
let state = ForkchoiceState {
head_block_hash: B256::from([0x22; 32]),
safe_block_hash: B256::ZERO,
finalized_block_hash: B256::ZERO,
};
let payload_attributes = PayloadAttributes {
timestamp: 1,
prev_randao: B256::ZERO,
suggested_fee_recipient: Address::ZERO,
withdrawals: Some(vec![]),
parent_beacon_block_root: None,
};
let api_task = tokio::spawn(async move {
api.fork_choice_updated_v3(state, Some(payload_attributes)).await
});
let request =
tokio::time::timeout(std::time::Duration::from_secs(1), handle.from_api.recv())
.await
.expect("timed out waiting for forkchoiceUpdated request")
.expect("expected forkchoiceUpdated request");
let response_tx = match request {
BeaconEngineMessage::ForkchoiceUpdated { payload_attrs, tx, .. } => {
assert!(
payload_attrs.is_none(),
"when attrs are invalid, API should first evaluate forkchoice without attrs"
);
tx
}
other => panic!("unexpected engine message: {other:?}"),
};
response_tx
.send(Ok(OnForkChoiceUpdated::valid(PayloadStatus::from_status(
PayloadStatusEnum::Valid,
))))
.expect("send valid response");
let response = api_task.await.expect("api task should not panic");
assert_matches!(
response,
Err(EngineApiError::EngineObjectValidationError(
reth_payload_primitives::EngineObjectValidationError::PayloadAttributes(_)
))
);
match tokio::time::timeout(std::time::Duration::from_millis(100), handle.from_api.recv())
.await
{
Err(_) | Ok(None) => {}
Ok(Some(BeaconEngineMessage::ForkchoiceUpdated { .. })) => {
panic!("no second forkchoiceUpdated call should be sent when attrs are invalid")
}
Ok(Some(other)) => panic!("unexpected engine message: {other:?}"),
}
}
// tests covering `engine_getPayloadBodiesByRange` and `engine_getPayloadBodiesByHash`
mod get_payload_bodies {
use super::*;