diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index d6993c8fcbc..e088ee08205 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -2571,7 +2571,7 @@ pub async fn serve( task_spawner_filter.clone(), ); - // GET validator/execution_payload_envelopes/{slot}/{builder_index} + // GET validator/execution_payload_envelopes/{slot}/{beacon_block_root} let get_validator_execution_payload_envelopes = get_validator_execution_payload_envelopes( eth_v1.clone(), chain_filter.clone(), diff --git a/beacon_node/http_api/src/validator/execution_payload_envelopes.rs b/beacon_node/http_api/src/validator/execution_payload_envelopes.rs index e5c9cbe0db1..e038bbb8f0f 100644 --- a/beacon_node/http_api/src/validator/execution_payload_envelopes.rs +++ b/beacon_node/http_api/src/validator/execution_payload_envelopes.rs @@ -8,10 +8,10 @@ use eth2::types::Accept; use ssz::Encode; use std::sync::Arc; use tracing::debug; -use types::Slot; +use types::{Hash256, Slot}; use warp::{Filter, Rejection, http::response::Builder, reply::Reply}; -// GET validator/execution_payload_envelopes/{slot} +// GET validator/execution_payload_envelopes/{slot}/{beacon_block_root} pub fn get_validator_execution_payload_envelopes( eth_v1: EthV1Filter, chain_filter: ChainFilter, @@ -26,6 +26,11 @@ pub fn get_validator_execution_payload_envelopes( "Invalid slot".to_string(), )) })) + .and(warp::path::param::().or_else(|_| async { + Err(warp_utils::reject::custom_bad_request( + "Invalid beacon_block_root".to_string(), + )) + })) .and(warp::path::end()) .and(warp::header::optional::("accept")) .and(not_while_syncing_filter) @@ -33,24 +38,26 @@ pub fn get_validator_execution_payload_envelopes( .and(chain_filter) .then( |slot: Slot, + beacon_block_root: Hash256, accept_header: Option, not_synced_filter: Result<(), Rejection>, task_spawner: TaskSpawner, chain: Arc>| { task_spawner.spawn_async_with_rejection(Priority::P0, async move { - debug!(?slot, "Execution payload envelope request from HTTP API"); + debug!(?slot, ?beacon_block_root, "Execution payload envelope request from HTTP API"); not_synced_filter?; - // Get the envelope from the pending cache (local building only) + // Get the envelope from the pending cache (local building only). let envelope = chain .pending_payload_envelopes .read() .get(slot) + .filter(|envelope| envelope.beacon_block_root == beacon_block_root) .cloned() .ok_or_else(|| { warp_utils::reject::custom_not_found(format!( - "Execution payload envelope not available for slot {slot}" + "Execution payload envelope not available for slot {slot} and root {beacon_block_root:?}" )) })?; diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index bd2a6f17559..b5d4b65c35b 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -4441,13 +4441,24 @@ impl ApiTester { let envelope = self .client - .get_validator_execution_payload_envelopes::(slot) + .get_validator_execution_payload_envelopes::(slot, block.tree_hash_root()) .await .unwrap() .data; self.assert_envelope_fields(&envelope, block.tree_hash_root(), slot); + // A request for a root this node did not build must miss (reorg-resistance). + assert!( + self.client + .get_validator_execution_payload_envelopes::( + slot, + Hash256::repeat_byte(0xff) + ) + .await + .is_err() + ); + let signed_block = block.sign(&sk, &fork, genesis_validators_root, &self.chain.spec); let signed_block_request = PublishBlockRequest::try_from(Arc::new(signed_block.clone())).unwrap(); @@ -4503,7 +4514,7 @@ impl ApiTester { let envelope = self .client - .get_validator_execution_payload_envelopes_ssz::(slot) + .get_validator_execution_payload_envelopes_ssz::(slot, block.tree_hash_root()) .await .unwrap(); @@ -4963,7 +4974,7 @@ impl ApiTester { // Retrieve and publish the envelope. let envelope = self .client - .get_validator_execution_payload_envelopes::(slot) + .get_validator_execution_payload_envelopes::(slot, block_root) .await .unwrap() .data; diff --git a/common/eth2/src/lib.rs b/common/eth2/src/lib.rs index 24bb873992a..2154c524441 100644 --- a/common/eth2/src/lib.rs +++ b/common/eth2/src/lib.rs @@ -2761,10 +2761,11 @@ impl BeaconNodeHttpClient { opt_response.ok_or(Error::StatusCode(StatusCode::NOT_FOUND)) } - /// `GET v1/validator/execution_payload_envelopes/{slot}` + /// `GET v1/validator/execution_payload_envelopes/{slot}/{beacon_block_root}` pub async fn get_validator_execution_payload_envelopes( &self, slot: Slot, + beacon_block_root: Hash256, ) -> Result>, Error> { let mut path = self.eth_path(V1)?; @@ -2772,15 +2773,17 @@ impl BeaconNodeHttpClient { .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("validator") .push("execution_payload_envelopes") - .push(&slot.to_string()); + .push(&slot.to_string()) + .push(&beacon_block_root.to_string()); self.get(path).await } - /// `GET v1/validator/execution_payload_envelopes/{slot}` in SSZ format + /// `GET v1/validator/execution_payload_envelopes/{slot}/{beacon_block_root}` in SSZ format pub async fn get_validator_execution_payload_envelopes_ssz( &self, slot: Slot, + beacon_block_root: Hash256, ) -> Result, Error> { let mut path = self.eth_path(V1)?; @@ -2788,7 +2791,8 @@ impl BeaconNodeHttpClient { .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("validator") .push("execution_payload_envelopes") - .push(&slot.to_string()); + .push(&slot.to_string()) + .push(&beacon_block_root.to_string()); let opt_response = self .get_bytes_opt_accept_header(path, Accept::Ssz, self.timeouts.get_validator_block) diff --git a/validator_client/validator_services/src/block_service.rs b/validator_client/validator_services/src/block_service.rs index 06fd14360a1..c953c581603 100644 --- a/validator_client/validator_services/src/block_service.rs +++ b/validator_client/validator_services/src/block_service.rs @@ -14,7 +14,7 @@ use std::time::Duration; use task_executor::TaskExecutor; use tokio::sync::mpsc; use tracing::{Instrument, debug, error, info, info_span, instrument, trace, warn}; -use types::{BlockType, ChainSpec, EthSpec, Graffiti, Slot}; +use types::{BlockType, ChainSpec, EthSpec, Graffiti, Hash256, Slot}; use validator_store::{Error as ValidatorStoreError, SignedBlock, UnsignedBlock, ValidatorStore}; #[derive(Debug)] @@ -611,6 +611,11 @@ impl BlockService { )); } + // Capture before `sign_and_publish_block` moves `unsigned_block`. + let produced_block_root = fork_name + .gloas_enabled() + .then(|| unsigned_block.block_root()); + self_ref .sign_and_publish_block( &proposer_fallback, @@ -624,11 +629,12 @@ impl BlockService { // TODO(gloas) we only need to fetch, sign and publish the envelope in the local building case. // Right now we always default to local building. Once we implement trustless/trusted builder logic // we should check the bid for index == BUILDER_INDEX_SELF_BUILD - if fork_name.gloas_enabled() { + if let Some(beacon_block_root) = produced_block_root { self_ref .fetch_sign_and_publish_payload_envelope( &proposer_fallback, slot, + beacon_block_root, &validator_pubkey, ) .await?; @@ -649,17 +655,21 @@ impl BlockService { &self, _proposer_fallback: &ProposerFallback, slot: Slot, + beacon_block_root: Hash256, validator_pubkey: &PublicKeyBytes, ) -> Result<(), BlockError> { - info!(slot = slot.as_u64(), "Fetching execution payload envelope"); + info!( + slot = slot.as_u64(), + %beacon_block_root, + "Fetching execution payload envelope" + ); // Fetch the envelope from the beacon node. - // TODO(gloas): Use proposer_fallback once multi-BN is supported. let envelope = self .beacon_nodes .first_success(|beacon_node| async move { beacon_node - .get_validator_execution_payload_envelopes_ssz::(slot) + .get_validator_execution_payload_envelopes_ssz::(slot, beacon_block_root) .await .map_err(|e| { BlockError::Recoverable(format!( diff --git a/validator_client/validator_store/src/lib.rs b/validator_client/validator_store/src/lib.rs index d40c7994f11..8c35f5ac0c9 100644 --- a/validator_client/validator_store/src/lib.rs +++ b/validator_client/validator_store/src/lib.rs @@ -232,6 +232,20 @@ pub enum UnsignedBlock { Blinded(BlindedBeaconBlock), } +impl UnsignedBlock { + /// The canonical root of this beacon block (identical for the full and blinded forms). + /// + /// This is the root of the block *this node produced*, which the local beacon node keys its + /// pending payload-envelope cache by. It is not necessarily the block ultimately signed or + /// published (e.g. under distributed validators). + pub fn block_root(&self) -> Hash256 { + match self { + UnsignedBlock::Full(block) => block.block().canonical_root(), + UnsignedBlock::Blinded(block) => block.canonical_root(), + } + } +} + #[derive(Clone, Debug, PartialEq)] pub enum SignedBlock { Full(PublishBlockRequest),