Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 5 additions & 10 deletions beacon_node/beacon_chain/src/beacon_chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7482,7 +7482,9 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
pe.missing_proofs()
.into_iter()
.filter_map(|mut info| {
info.root = self.store.get_block_root_by_request_root(&info.root)?;
let (block_root, slot) = self.store.get_block_root_by_request_root(&info.root)?;
info.root = block_root;
info.slot = slot;
Some(info)
})
.collect()
Expand Down Expand Up @@ -7604,7 +7606,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
let request_root = signed_proof.request_root();

// Look up the beacon block root from request root
let block_root = self
let (block_root, slot) = self
.store
.get_block_root_by_request_root(&request_root)
.ok_or_else(|| ExecutionProofError::UnknownRequestRoot(request_root))?;
Expand Down Expand Up @@ -7654,14 +7656,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
Err(e) => return Err(Error::ForkChoiceError(e)),
}

// Look up the slot so caller can update local execution proof status.
let slot = self
.store
.get_blinded_block(&block_root)
.ok()
.flatten()
.map(|b| b.slot());
return Ok((verification_result, slot.map(|s| (block_root, s))));
return Ok((verification_result, Some((block_root, slot))));
}

Ok((verification_result, None))
Expand Down
12 changes: 1 addition & 11 deletions beacon_node/beacon_chain/src/bellatrix_readiness.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//! transition.

use crate::{BeaconChain, BeaconChainError as Error, BeaconChainTypes};
use execution_layer::{BlockByNumberQuery, ForkchoiceState};
use execution_layer::BlockByNumberQuery;
use serde::{Deserialize, Serialize, Serializer};
use std::fmt;
use std::fmt::Write;
Expand Down Expand Up @@ -205,16 +205,6 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
.ok_or(Error::ExecutionLayerMissing)?;
let exec_block_hash = latest_execution_payload_header.block_hash();

if let Some(proof_engine) = execution_layer.proof_engine() {
proof_engine
.forkchoice_updated(ForkchoiceState {
head_block_hash: exec_block_hash,
safe_block_hash: exec_block_hash,
finalized_block_hash: exec_block_hash,
})
.await?;
}

// Use getBlockByNumber(0) to check that the block hash matches.
// At present, Geth does not respond to engine_getPayloadBodiesByRange before genesis.
if execution_layer.engine().is_some() {
Expand Down
39 changes: 31 additions & 8 deletions beacon_node/beacon_chain/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ use crate::{
BeaconChain, BeaconChainTypes, BeaconForkChoiceStore, BeaconSnapshot, ServerSentEventHandler,
};
use bls::Signature;
use execution_layer::ExecutionLayer;
use execution_layer::{ExecutionLayer, ForkchoiceState};
use fixed_bytes::FixedBytesExtended;
use fork_choice::{ForkChoice, ResetPayloadStatuses};
use futures::channel::mpsc::Sender;
Expand All @@ -42,7 +42,7 @@ use std::sync::Arc;
use std::time::Duration;
use store::{Error as StoreError, HotColdDB, ItemStore, KeyValueStoreOp};
use task_executor::{ShutdownReason, TaskExecutor};
use tracing::{debug, error, info};
use tracing::{debug, error, info, warn};
use types::data::CustodyIndex;
use types::{
BeaconBlock, BeaconState, BlobSidecarList, ChainSpec, ColumnIndex, DataColumnSidecarList,
Expand Down Expand Up @@ -917,6 +917,15 @@ where

let genesis_validators_root = head_snapshot.beacon_state.genesis_validators_root();
let genesis_time = head_snapshot.beacon_state.genesis_time();
let genesis_execution_block_hash = (head_snapshot.beacon_state.slot() == 0)
.then(|| {
head_snapshot
.beacon_state
.latest_execution_payload_header()
.ok()
.map(|header| header.block_hash())
})
.flatten();
let canonical_head = CanonicalHead::new(fork_choice, Arc::new(head_snapshot));
let shuffling_cache_size = self.chain_config.shuffling_cache_size;
let complete_blob_backfill = self.chain_config.complete_blob_backfill;
Expand Down Expand Up @@ -976,18 +985,32 @@ where
};
debug!(?custody_context, "Loaded persisted custody context");

// Restore ProofEngine state from disk if available.
// Restore ProofEngine state from disk if available, or seed from genesis on fresh start.
if let Some(proof_engine) = self
.execution_layer
.as_ref()
.and_then(|el| el.proof_engine())
&& let Some(store) = self.store
&& let Some(persisted) =
crate::BeaconChain::<Witness<TSlotClock, _, _, _>>::load_proof_engine_state(
store.clone(),
)
{
proof_engine.restore_from_persisted(persisted);
match crate::BeaconChain::<Witness<TSlotClock, _, _, _>>::load_proof_engine_state(
store.clone(),
) {
Some(persisted) => proof_engine.restore_from_persisted(persisted),
None if genesis_execution_block_hash.is_some() => {
proof_engine
.forkchoice_updated(ForkchoiceState::new_genesis(
genesis_execution_block_hash.expect("is Some"),
))
.map_err(|err| {
format!("failed to seed proof engine with genesis hash: {err:?}")
})?;
}
_ => {
warn!(
"No persisted ProofEngine state and head is not at genesis. ProofEngine may be out of sync until next fork choice update."
);
}
}
}

let beacon_chain = BeaconChain {
Expand Down
2 changes: 1 addition & 1 deletion beacon_node/beacon_chain/src/execution_payload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ async fn notify_new_payload<T: BeaconChainTypes>(
);
chain
.store
.put_request_root_mapping(new_payload_request_root, block_root);
.put_request_root_mapping(new_payload_request_root, block_root, block.slot());

match new_payload_response {
Ok(status) => match status {
Expand Down
2 changes: 1 addition & 1 deletion beacon_node/execution_layer/src/eip8025/proof_engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ impl HttpProofEngine {
}

/// Notify the proof engine of a forkchoice update.
pub async fn forkchoice_updated(
pub fn forkchoice_updated(
&self,
forkchoice_state: ForkchoiceState,
) -> Result<ForkchoiceUpdatedResponse, ProofEngineError> {
Expand Down
11 changes: 9 additions & 2 deletions beacon_node/execution_layer/src/eip8025/proof_node_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,17 +117,21 @@ enum ProofVerificationStatus {
pub struct HttpProofNodeClient {
client: Client,
url: SensitiveUrl,
timeout: Duration,
}

impl HttpProofNodeClient {
/// Create a new HTTP proof node client.
pub fn new(url: SensitiveUrl, timeout: Option<Duration>) -> Self {
let client = Client::builder()
.timeout(timeout.unwrap_or(PROOF_ENGINE_TIMEOUT))
.build()
.expect("Failed to build HTTP client");

Self { client, url }
Self {
client,
url,
timeout: timeout.unwrap_or(PROOF_ENGINE_TIMEOUT),
}
}

/// Build a URL from the base URL and a path.
Expand Down Expand Up @@ -164,6 +168,7 @@ impl ProofNodeClient for HttpProofNodeClient {
.query(&[(QUERY_PROOF_TYPES, &proof_types_csv)])
.header(HEADER_CONTENT_TYPE, HEADER_VALUE_SSZ)
.body(ssz_body)
.timeout(self.timeout)
.send()
.await?
.error_for_status()?
Expand Down Expand Up @@ -192,6 +197,7 @@ impl ProofNodeClient for HttpProofNodeClient {
])
.header(HEADER_CONTENT_TYPE, HEADER_VALUE_SSZ)
.body(proof_data.to_vec())
.timeout(self.timeout)
.send()
.await?
.error_for_status()?
Expand All @@ -212,6 +218,7 @@ impl ProofNodeClient for HttpProofNodeClient {
Ok(self
.client
.get(self.url(&format!("{PATH_PROOFS}/{root}/{proof_type_str}")))
.timeout(self.timeout)
.send()
.await?
.error_for_status()?
Expand Down
81 changes: 64 additions & 17 deletions beacon_node/execution_layer/src/eip8025/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,23 +51,46 @@ impl State {
Self::default()
}

/// Return all buffer entries that do not yet have sufficient proofs for promotion.
/// Return buffer entries that do not yet have sufficient proofs for promotion,
/// restricted to those on the ancestor path required to satisfy `latest_fcs`.
///
/// Only the `buffer` is scanned: by design, every entry in the buffer has not been
/// promoted to the tree, meaning it lacks sufficient proofs. Tree entries are already done.
/// If `latest_fcs` is unset there is no pending fork-choice update to satisfy, so
/// nothing is returned. Otherwise the buffer is walked backwards from
/// `latest_fcs.head_block_hash`; entries that lack sufficient proofs are collected
/// until a block is not found in the buffer (reached the tree or an unseen block).
pub fn missing_proofs(&self) -> Vec<MissingProofInfo> {
self.buffer
let Some(latest_fcs) = &self.latest_fcs else {
return vec![];
};

// Build block_hash → &PayloadRequest for O(1) lookup during the walk.
let buffer_by_block_hash: HashMap<ExecutionBlockHash, &PayloadRequest> = self
.buffer
.proofs
.iter()
.map(|(request_root, payload_request)| MissingProofInfo {
root: *request_root,
existing_proof_types: payload_request
.proofs
.iter()
.map(|p| p.message.proof_type)
.collect(),
})
.collect()
.values()
.map(|p| (p.metadata.block_hash, p))
.collect();

// Walk backwards from the FCS head through buffer entries, collecting
// those that still lack sufficient proofs. Stop when a block is not in
// the buffer (reached the tree or an unseen block).
let mut result = Vec::new();
let mut current = latest_fcs.head_block_hash;
loop {
let Some(req) = buffer_by_block_hash.get(&current) else {
break;
};
if req.proofs.len() < self.min_required_proofs {
result.push(MissingProofInfo {
root: req.metadata.request_root,
existing_proof_types: req.proofs.iter().map(|p| p.message.proof_type).collect(),
slot: Default::default(), // populated by BeaconChain::missing_execution_proofs()
});
}
current = req.metadata.parent_hash;
}

result
}

/// Check if the state contains any proofs associated with the given new payload request root.
Expand Down Expand Up @@ -121,6 +144,21 @@ impl State {
self.tree.current_canonical_head = finalized;

tracing::info!(target: "execution_layer", ?finalized, "Updated last_valid_fcs to finalized block (tree empty)");

// Check if any buffered requests can be promoted based on the new last_valid_fcs.
let mut promote_requests = Vec::new();
for request in self.buffer.proofs.keys() {
if self.can_promote(request)? {
promote_requests.push(*request);
}
}
// Promote any buffered requests that can now be associated with the tree state.
for request_root in promote_requests {
if let Some(latest_canonical_head) = self.promote_buffered_requests(request_root)? {
tracing::info!(target: "execution_layer", ?latest_canonical_head, "Updated canonical head after promoting buffered proofs");
}
}

return Ok(self.forkchoice_response_syncing());
}

Expand Down Expand Up @@ -653,11 +691,19 @@ pub mod test_utils {
pub fn create_signed_proof(
request_root: Hash256,
validator_index: u64,
) -> SignedExecutionProof {
create_signed_proof_with_type(request_root, validator_index, 1)
}

pub fn create_signed_proof_with_type(
request_root: Hash256,
validator_index: u64,
proof_type: u8,
) -> SignedExecutionProof {
SignedExecutionProof {
message: ExecutionProof {
proof_data: VariableList::new(vec![0xaa, 0xbb, 0xcc]).unwrap(),
proof_type: 1,
proof_type,
public_input: PublicInput {
new_payload_request_root: request_root,
},
Expand Down Expand Up @@ -936,12 +982,13 @@ pub mod test_utils {
let metadata =
create_request_metadata(request_root, block_hash, parent_hash, block_number);

// Generate proofs
// Generate proofs with distinct proof types to avoid deduplication.
let mut proofs = Vec::new();
for i in 0..proof_count {
proofs.push(create_signed_proof(
proofs.push(create_signed_proof_with_type(
request_root,
request_root.0[0] as u64 + i as u64,
(i as u8).wrapping_add(1), // types 1, 2, 3, ... (avoid 0)
));
}

Expand Down
4 changes: 3 additions & 1 deletion beacon_node/execution_layer/src/engine_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ pub use types::{
use types::{
ExecutionPayloadBellatrix, ExecutionPayloadCapella, ExecutionPayloadDeneb,
ExecutionPayloadElectra, ExecutionPayloadFulu, ExecutionPayloadGloas, ExecutionRequests,
KzgProofs,
KzgProofs, Slot,
};
use types::{GRAFFITI_BYTES_LEN, Graffiti};

Expand Down Expand Up @@ -269,6 +269,8 @@ pub struct MissingProofInfo {
pub root: Hash256,
/// Proof types already received for this request root (to avoid redundant requests).
pub existing_proof_types: Vec<u8>,
/// Beacon slot of the block whose proofs are missing.
pub slot: Slot,
}

#[derive(Clone, Debug, PartialEq)]
Expand Down
11 changes: 11 additions & 0 deletions beacon_node/execution_layer/src/engines.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,17 @@ pub struct ForkchoiceState {
pub finalized_block_hash: ExecutionBlockHash,
}

impl ForkchoiceState {
/// Creates a `ForkchoiceState` with all block hashes set to the genesis hash.
pub fn new_genesis(genesis_hash: ExecutionBlockHash) -> Self {
Self {
head_block_hash: genesis_hash,
safe_block_hash: genesis_hash,
finalized_block_hash: genesis_hash,
}
}
}

#[derive(Hash, PartialEq, std::cmp::Eq)]
struct PayloadIdCacheKey {
pub head_block_hash: ExecutionBlockHash,
Expand Down
2 changes: 1 addition & 1 deletion beacon_node/execution_layer/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1657,7 +1657,7 @@ impl<E: EthSpec> ExecutionLayer<E> {
};

let proof_engine_result = if let Some(proof_engine) = self.proof_engine() {
match proof_engine.forkchoice_updated(forkchoice_state).await {
match proof_engine.forkchoice_updated(forkchoice_state) {
Ok(response) => Some(Ok(response)),
Err(e) => {
debug!(error = ?e, "Proof engine forkchoice_updated error (non-fatal)");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ use types::{
#[derive(SszEncode, SszDecode, TreeHashDerive)]
#[ssz(enum_behaviour = "transparent")]
#[tree_hash(enum_behaviour = "transparent")]
pub(crate) struct OwnedNewPayloadRequest<E: EthSpec> {
pub struct OwnedNewPayloadRequest<E: EthSpec> {
#[superstruct(
only(Bellatrix),
partial_getter(rename = "execution_payload_bellatrix")
Expand Down
Loading
Loading