Re-issue ForkchoiceUpdate based on updated PayloadStatus (#9102)

Co-Authored-By: hopinheimer <knmanas6@gmail.com>

Co-Authored-By: Michael Sproul <michael@sigmaprime.io>

Co-Authored-By: Michael Sproul <michaelsproul@users.noreply.github.com>
This commit is contained in:
hopinheimer
2026-04-25 04:04:09 -04:00
committed by GitHub
parent 8a384ff445
commit df764ffa9a
14 changed files with 808 additions and 41 deletions

View File

@@ -1,7 +1,7 @@
use super::*;
use alloy_rlp::RlpEncodable;
use serde::{Deserialize, Serialize};
use ssz::{Decode, TryFromIter};
use ssz::{Decode, Encode, TryFromIter};
use ssz_types::{FixedVector, VariableList, typenum::Unsigned};
use strum::EnumString;
use superstruct::superstruct;
@@ -481,6 +481,34 @@ pub enum RequestsError {
#[serde(transparent)]
pub struct JsonExecutionRequests(pub Vec<String>);
impl<E: EthSpec> From<ExecutionRequests<E>> for JsonExecutionRequests {
fn from(requests: ExecutionRequests<E>) -> Self {
let mut result = Vec::new();
if !requests.deposits.is_empty() {
result.push(format!(
"0x{:02x}{}",
RequestType::Deposit.to_u8(),
hex::encode(requests.deposits.as_ssz_bytes())
));
}
if !requests.withdrawals.is_empty() {
result.push(format!(
"0x{:02x}{}",
RequestType::Withdrawal.to_u8(),
hex::encode(requests.withdrawals.as_ssz_bytes())
));
}
if !requests.consolidations.is_empty() {
result.push(format!(
"0x{:02x}{}",
RequestType::Consolidation.to_u8(),
hex::encode(requests.consolidations.as_ssz_bytes())
));
}
JsonExecutionRequests(result)
}
}
impl<E: EthSpec> TryFrom<JsonExecutionRequests> for ExecutionRequests<E> {
type Error = RequestsError;

View File

@@ -403,6 +403,7 @@ impl ProposerPreparationDataEntry {
pub struct ProposerKey {
slot: Slot,
head_block_root: Hash256,
head_payload_status: fork_choice::PayloadStatus,
}
#[derive(PartialEq, Clone)]
@@ -1461,12 +1462,14 @@ impl<E: EthSpec> ExecutionLayer<E> {
&self,
slot: Slot,
head_block_root: Hash256,
head_payload_status: fork_choice::PayloadStatus,
validator_index: u64,
payload_attributes: PayloadAttributes,
) -> bool {
let proposers_key = ProposerKey {
slot,
head_block_root,
head_payload_status,
};
let existing = self.proposers().write().await.insert(
@@ -1485,16 +1488,18 @@ impl<E: EthSpec> ExecutionLayer<E> {
}
/// If there has been a proposer registered via `Self::insert_proposer` with a matching `slot`
/// `head_block_root`, then return the appropriate `PayloadAttributes` for inclusion in
/// `forkchoiceUpdated` calls.
/// `head_block_root`, and `head_payload_status` then return the appropriate `PayloadAttributes`
/// for inclusion in `forkchoiceUpdated` calls.
pub async fn payload_attributes(
&self,
current_slot: Slot,
head_block_root: Hash256,
head_payload_status: fork_choice::PayloadStatus,
) -> Option<PayloadAttributes> {
let proposers_key = ProposerKey {
slot: current_slot,
head_block_root,
head_payload_status,
};
let proposer = self.proposers().read().await.get(&proposers_key).cloned()?;
@@ -1518,6 +1523,7 @@ impl<E: EthSpec> ExecutionLayer<E> {
finalized_block_hash: ExecutionBlockHash,
current_slot: Slot,
head_block_root: Hash256,
head_payload_status: fork_choice::PayloadStatus,
) -> Result<PayloadStatus, Error> {
let _timer = metrics::start_timer_vec(
&metrics::EXECUTION_LAYER_REQUEST_TIMES,
@@ -1534,7 +1540,9 @@ impl<E: EthSpec> ExecutionLayer<E> {
);
let next_slot = current_slot + 1;
let payload_attributes = self.payload_attributes(next_slot, head_block_root).await;
let payload_attributes = self
.payload_attributes(next_slot, head_block_root, head_payload_status)
.await;
// Compute the "lookahead", the time between when the payload will be produced and now.
if let Some(ref payload_attributes) = payload_attributes

View File

@@ -26,8 +26,8 @@ use tree_hash_derive::TreeHash;
use types::{
Blob, ChainSpec, EthSpec, ExecutionBlockHash, ExecutionPayload, ExecutionPayloadBellatrix,
ExecutionPayloadCapella, ExecutionPayloadDeneb, ExecutionPayloadElectra, ExecutionPayloadFulu,
ExecutionPayloadGloas, ExecutionPayloadHeader, ForkName, Hash256, KzgProofs, Transaction,
Transactions, Uint256,
ExecutionPayloadGloas, ExecutionPayloadHeader, ExecutionRequests, ForkName, Hash256, KzgProofs,
Transaction, Transactions, Uint256,
};
const TEST_BLOB_BUNDLE: &[u8] = include_bytes!("fixtures/mainnet/test_blobs_bundle.ssz");
@@ -161,6 +161,14 @@ pub struct ExecutionBlockGenerator<E: EthSpec> {
pub blobs_bundles: HashMap<PayloadId, BlobsBundle<E>>,
pub kzg: Option<Arc<Kzg>>,
rng: Arc<Mutex<StdRng>>,
/*
* Execution requests (electra+)
*/
/// Per-payload execution requests returned by `getPayload`.
execution_requests: HashMap<PayloadId, ExecutionRequests<E>>,
/// If set, the next call to `build_new_execution_payload` will associate these
/// execution requests with the generated payload ID.
next_execution_requests: Option<ExecutionRequests<E>>,
}
fn make_rng() -> Arc<Mutex<StdRng>> {
@@ -199,6 +207,8 @@ impl<E: EthSpec> ExecutionBlockGenerator<E> {
blobs_bundles: <_>::default(),
kzg,
rng: make_rng(),
execution_requests: <_>::default(),
next_execution_requests: None,
};
generator.insert_pow_block(0).unwrap();
@@ -458,6 +468,15 @@ impl<E: EthSpec> ExecutionBlockGenerator<E> {
self.blobs_bundles.get(id).cloned()
}
pub fn get_execution_requests(&self, id: &PayloadId) -> Option<ExecutionRequests<E>> {
self.execution_requests.get(id).cloned()
}
/// Set execution requests to be returned alongside the next generated payload.
pub fn set_next_execution_requests(&mut self, requests: ExecutionRequests<E>) {
self.next_execution_requests = Some(requests);
}
/// Look up a blob and proof by versioned hash across all stored bundles.
pub fn get_blob_and_proof(&self, versioned_hash: &Hash256) -> Option<BlobAndProof<E>> {
self.blobs_bundles
@@ -763,6 +782,11 @@ impl<E: EthSpec> ExecutionBlockGenerator<E> {
},
};
// Store execution requests for this payload if configured.
if let Some(requests) = self.next_execution_requests.take() {
self.execution_requests.insert(id, requests);
}
let fork_name = execution_payload.fork_name();
if fork_name.deneb_enabled() {
// get random number between 0 and 1 blobs by default

View File

@@ -295,6 +295,10 @@ pub async fn handle_rpc<E: EthSpec>(
})?;
let maybe_blobs = ctx.execution_block_generator.write().get_blobs_bundle(&id);
let maybe_execution_requests = ctx
.execution_block_generator
.read()
.get_execution_requests(&id);
// validate method called correctly according to shanghai fork time
if ctx
@@ -432,8 +436,10 @@ pub async fn handle_rpc<E: EthSpec>(
))?
.into(),
should_override_builder: false,
// TODO(electra): add EL requests in mock el
execution_requests: Default::default(),
execution_requests: maybe_execution_requests
.clone()
.unwrap_or_default()
.into(),
})
.unwrap()
}
@@ -453,7 +459,10 @@ pub async fn handle_rpc<E: EthSpec>(
))?
.into(),
should_override_builder: false,
execution_requests: Default::default(),
execution_requests: maybe_execution_requests
.clone()
.unwrap_or_default()
.into(),
})
.unwrap()
}
@@ -473,7 +482,9 @@ pub async fn handle_rpc<E: EthSpec>(
))?
.into(),
should_override_builder: false,
execution_requests: Default::default(),
execution_requests: maybe_execution_requests
.unwrap_or_default()
.into(),
})
.unwrap()
}

View File

@@ -800,6 +800,10 @@ impl<E: EthSpec> MockBuilder<E> {
let head_block_root = head_block_root.unwrap_or(head.canonical_root());
// TODO(gloas): Currently the tests are pre-Gloas and we are not considering
// other payload statuses. This codepath may not be relevant for Gloas.
let head_payload_status = fork_choice::PayloadStatus::Pending;
let head_execution_payload = head
.message()
.body()
@@ -934,7 +938,13 @@ impl<E: EthSpec> MockBuilder<E> {
);
self.el
.insert_proposer(slot, head_block_root, val_index, payload_attributes.clone())
.insert_proposer(
slot,
head_block_root,
head_payload_status,
val_index,
payload_attributes.clone(),
)
.await;
let forkchoice_update_params = ForkchoiceUpdateParameters {
@@ -952,6 +962,7 @@ impl<E: EthSpec> MockBuilder<E> {
finalized_execution_hash,
slot - 1,
head_block_root,
head_payload_status,
)
.await
.map_err(|e| format!("fcu call failed : {:?}", e))?;

View File

@@ -90,6 +90,8 @@ impl<E: EthSpec> MockExecutionLayer<E> {
let timestamp = block_number;
let prev_randao = Hash256::from_low_u64_be(block_number);
let head_block_root = Hash256::repeat_byte(42);
// TODO(gloas): allow statuses other than Pending?
let head_payload_status = fork_choice::PayloadStatus::Pending;
let forkchoice_update_params = ForkchoiceUpdateParameters {
head_root: head_block_root,
head_hash: Some(parent_hash),
@@ -109,7 +111,13 @@ impl<E: EthSpec> MockExecutionLayer<E> {
let slot = Slot::new(0);
let validator_index = 0;
self.el
.insert_proposer(slot, head_block_root, validator_index, payload_attributes)
.insert_proposer(
slot,
head_block_root,
head_payload_status,
validator_index,
payload_attributes,
)
.await;
self.el
@@ -119,6 +127,7 @@ impl<E: EthSpec> MockExecutionLayer<E> {
ExecutionBlockHash::zero(),
slot,
head_block_root,
head_payload_status,
)
.await
.unwrap();
@@ -280,6 +289,7 @@ impl<E: EthSpec> MockExecutionLayer<E> {
// Use junk values for slot/head-root to ensure there is no payload supplied.
let slot = Slot::new(0);
let head_block_root = Hash256::repeat_byte(13);
// TODO(gloas): reconsider the state_payload_status
self.el
.notify_forkchoice_updated(
block_hash,
@@ -287,6 +297,7 @@ impl<E: EthSpec> MockExecutionLayer<E> {
ExecutionBlockHash::zero(),
slot,
head_block_root,
fork_choice::PayloadStatus::Pending,
)
.await
.unwrap();