diff --git a/beacon_node/builder_client/src/lib.rs b/beacon_node/builder_client/src/lib.rs index d193eaf1d80..c1066042854 100644 --- a/beacon_node/builder_client/src/lib.rs +++ b/beacon_node/builder_client/src/lib.rs @@ -293,7 +293,7 @@ impl BuilderHttpClient { } /// `POST /eth/v1/builder/blinded_blocks` with SSZ serialized request body - pub async fn post_builder_blinded_blocks_ssz( + pub async fn post_builder_blinded_blocks_v1_ssz( &self, blinded_block: &SignedBlindedBeaconBlock, ) -> Result, Error> { @@ -340,8 +340,58 @@ impl BuilderHttpClient { .map_err(Error::InvalidSsz) } + /// `POST /eth/v2/builder/blinded_blocks` with SSZ serialized request body + pub async fn post_builder_blinded_blocks_v2_ssz( + &self, + blinded_block: &SignedBlindedBeaconBlock, + ) -> Result<(), Error> { + let mut path = self.server.full.clone(); + + let body = blinded_block.as_ssz_bytes(); + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("eth") + .push("v2") + .push("builder") + .push("blinded_blocks"); + + let mut headers = HeaderMap::new(); + headers.insert( + CONSENSUS_VERSION_HEADER, + HeaderValue::from_str(&blinded_block.fork_name_unchecked().to_string()) + .map_err(|e| Error::InvalidHeaders(format!("{}", e)))?, + ); + headers.insert( + CONTENT_TYPE_HEADER, + HeaderValue::from_str(SSZ_CONTENT_TYPE_HEADER) + .map_err(|e| Error::InvalidHeaders(format!("{}", e)))?, + ); + headers.insert( + ACCEPT, + HeaderValue::from_str(PREFERENCE_ACCEPT_VALUE) + .map_err(|e| Error::InvalidHeaders(format!("{}", e)))?, + ); + + let result = self + .post_ssz_with_raw_response( + path, + body, + headers, + Some(self.timeouts.post_blinded_blocks), + ) + .await?; + + if result.status() == StatusCode::ACCEPTED { + Ok(()) + } else { + // ACCEPTED is the only valid status code response + Err(Error::StatusCode(result.status())) + } + } + /// `POST /eth/v1/builder/blinded_blocks` - pub async fn post_builder_blinded_blocks( + pub async fn post_builder_blinded_blocks_v1( &self, blinded_block: &SignedBlindedBeaconBlock, ) -> Result>, Error> { @@ -383,6 +433,54 @@ impl BuilderHttpClient { .await?) } + /// `POST /eth/v2/builder/blinded_blocks` + pub async fn post_builder_blinded_blocks_v2( + &self, + blinded_block: &SignedBlindedBeaconBlock, + ) -> Result<(), Error> { + let mut path = self.server.full.clone(); + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("eth") + .push("v2") + .push("builder") + .push("blinded_blocks"); + + let mut headers = HeaderMap::new(); + headers.insert( + CONSENSUS_VERSION_HEADER, + HeaderValue::from_str(&blinded_block.fork_name_unchecked().to_string()) + .map_err(|e| Error::InvalidHeaders(format!("{}", e)))?, + ); + headers.insert( + CONTENT_TYPE_HEADER, + HeaderValue::from_str(JSON_CONTENT_TYPE_HEADER) + .map_err(|e| Error::InvalidHeaders(format!("{}", e)))?, + ); + headers.insert( + ACCEPT, + HeaderValue::from_str(JSON_ACCEPT_VALUE) + .map_err(|e| Error::InvalidHeaders(format!("{}", e)))?, + ); + + let result = self + .post_with_raw_response( + path, + &blinded_block, + headers, + Some(self.timeouts.post_blinded_blocks), + ) + .await?; + + if result.status() == StatusCode::ACCEPTED { + Ok(()) + } else { + // ACCEPTED is the only valid status code response + Err(Error::StatusCode(result.status())) + } + } + /// `GET /eth/v1/builder/header` pub async fn get_builder_header( &self, diff --git a/beacon_node/execution_layer/src/lib.rs b/beacon_node/execution_layer/src/lib.rs index cf751138d63..bfde2be17b0 100644 --- a/beacon_node/execution_layer/src/lib.rs +++ b/beacon_node/execution_layer/src/lib.rs @@ -411,6 +411,11 @@ pub enum FailedCondition { EpochsSinceFinalization, } +pub enum SubmitBlindedBlockResponse { + V1(Box>), + V2, +} + type PayloadContentsRefTuple<'a, E> = (ExecutionPayloadRef<'a, E>, Option<&'a BlobsBundle>); struct Inner { @@ -1893,9 +1898,25 @@ impl ExecutionLayer { &self, block_root: Hash256, block: &SignedBlindedBeaconBlock, - ) -> Result, Error> { + spec: &ChainSpec, + ) -> Result, Error> { debug!(?block_root, "Sending block to builder"); + if spec.is_fulu_scheduled() { + self.post_builder_blinded_blocks_v2(block_root, block) + .await + .map(|()| SubmitBlindedBlockResponse::V2) + } else { + self.post_builder_blinded_blocks_v1(block_root, block) + .await + .map(|full_payload| SubmitBlindedBlockResponse::V1(Box::new(full_payload))) + } + } + async fn post_builder_blinded_blocks_v1( + &self, + block_root: Hash256, + block: &SignedBlindedBeaconBlock, + ) -> Result, Error> { if let Some(builder) = self.builder() { let (payload_result, duration) = timed_future(metrics::POST_BLINDED_PAYLOAD_BUILDER, async { @@ -1903,16 +1924,16 @@ impl ExecutionLayer { debug!( ?block_root, ssz = ssz_enabled, - "Calling submit_blinded_block on builder" + "Calling submit_blinded_block v1 on builder" ); if ssz_enabled { builder - .post_builder_blinded_blocks_ssz(block) + .post_builder_blinded_blocks_v1_ssz(block) .await .map_err(Error::Builder) } else { builder - .post_builder_blinded_blocks(block) + .post_builder_blinded_blocks_v1(block) .await .map_err(Error::Builder) .map(|d| d.data) @@ -1961,6 +1982,66 @@ impl ExecutionLayer { Err(Error::NoPayloadBuilder) } } + + async fn post_builder_blinded_blocks_v2( + &self, + block_root: Hash256, + block: &SignedBlindedBeaconBlock, + ) -> Result<(), Error> { + if let Some(builder) = self.builder() { + let (result, duration) = timed_future(metrics::POST_BLINDED_PAYLOAD_BUILDER, async { + let ssz_enabled = builder.is_ssz_available(); + debug!( + ?block_root, + ssz = ssz_enabled, + "Calling submit_blinded_block v2 on builder" + ); + if ssz_enabled { + builder + .post_builder_blinded_blocks_v2_ssz(block) + .await + .map_err(Error::Builder) + } else { + builder + .post_builder_blinded_blocks_v2(block) + .await + .map_err(Error::Builder) + } + }) + .await; + + match result { + Ok(()) => { + metrics::inc_counter_vec( + &metrics::EXECUTION_LAYER_BUILDER_REVEAL_PAYLOAD_OUTCOME, + &[metrics::SUCCESS], + ); + info!( + relay_response_ms = duration.as_millis(), + ?block_root, + "Successfully submitted blinded block to the builder" + ) + } + Err(e) => { + metrics::inc_counter_vec( + &metrics::EXECUTION_LAYER_BUILDER_REVEAL_PAYLOAD_OUTCOME, + &[metrics::FAILURE], + ); + error!( + info = "this may result in a missed block proposal", + error = ?e, + relay_response_ms = duration.as_millis(), + ?block_root, + "Failed to submit blinded block to the builder" + ) + } + } + + Ok(()) + } else { + Err(Error::NoPayloadBuilder) + } + } } #[derive(AsRefStr)] diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index 83422090caa..c66ddacdaf6 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -2763,7 +2763,7 @@ pub fn serve( move |task_spawner: TaskSpawner, chain: Arc>| { task_spawner.blocking_json_task(Priority::P0, move || { let config_and_preset = - ConfigAndPreset::from_chain_spec::(&chain.spec, None); + ConfigAndPreset::from_chain_spec::(&chain.spec); Ok(api_types::GenericResponse::from(config_and_preset)) }) }, diff --git a/beacon_node/http_api/src/publish_blocks.rs b/beacon_node/http_api/src/publish_blocks.rs index 5d581859ae9..c1b86416b15 100644 --- a/beacon_node/http_api/src/publish_blocks.rs +++ b/beacon_node/http_api/src/publish_blocks.rs @@ -13,7 +13,7 @@ use eth2::types::{ BlobsBundle, BroadcastValidation, ErrorMessage, ExecutionPayloadAndBlobs, FullPayloadContents, PublishBlockRequest, SignedBlockContents, }; -use execution_layer::ProvenancedPayload; +use execution_layer::{ProvenancedPayload, SubmitBlindedBlockResponse}; use futures::TryFutureExt; use lighthouse_network::{NetworkGlobals, PubsubMessage}; use network::NetworkMessage; @@ -636,27 +636,37 @@ pub async fn publish_blinded_block( network_globals: Arc>, ) -> Result { let block_root = blinded_block.canonical_root(); - let full_block = reconstruct_block(chain.clone(), block_root, blinded_block).await?; - publish_block::( - Some(block_root), - full_block, - chain, - network_tx, - validation_level, - duplicate_status_code, - network_globals, - ) - .await + let full_block_opt = reconstruct_block(chain.clone(), block_root, blinded_block).await?; + + if let Some(full_block) = full_block_opt { + publish_block::( + Some(block_root), + full_block, + chain, + network_tx, + validation_level, + duplicate_status_code, + network_globals, + ) + .await + } else { + // From the fulu fork, builders are responsible for publishing and + // will no longer return the full payload and blobs. + Ok(warp::reply().into_response()) + } } /// Deconstruct the given blinded block, and construct a full block. This attempts to use the /// execution layer's payload cache, and if that misses, attempts a blind block proposal to retrieve /// the full payload. +/// +/// From the Fulu fork, external builders no longer return the full payload and blobs, and this +/// function will always return `Ok(None)` on successful submission of blinded block. pub async fn reconstruct_block( chain: Arc>, block_root: Hash256, block: Arc>, -) -> Result>>, Rejection> { +) -> Result>>>, Rejection> { let full_payload_opt = if let Ok(payload_header) = block.message().body().execution_payload() { let el = chain.execution_layer.as_ref().ok_or_else(|| { warp_utils::reject::custom_server_error("Missing execution layer".to_string()) @@ -696,17 +706,24 @@ pub async fn reconstruct_block( "builder", ); - let full_payload = el - .propose_blinded_beacon_block(block_root, &block) + match el + .propose_blinded_beacon_block(block_root, &block, &chain.spec) .await .map_err(|e| { warp_utils::reject::custom_server_error(format!( "Blind block proposal failed: {:?}", e )) - })?; - info!(block_hash = ?full_payload.block_hash(), "Successfully published a block to the builder network"); - ProvenancedPayload::Builder(full_payload) + })? { + SubmitBlindedBlockResponse::V1(full_payload) => { + info!(block_root = ?block_root, "Successfully published a block to the builder network"); + ProvenancedPayload::Builder(*full_payload) + } + SubmitBlindedBlockResponse::V2 => { + info!(block_root = ?block_root, "Successfully published a block to the builder network"); + return Ok(None); + } + } }; Some(full_payload_contents) @@ -734,6 +751,7 @@ pub async fn reconstruct_block( .map(|(block, blobs)| ProvenancedBlock::builder(block, blobs)) } } + .map(Some) .map_err(|e| { warp_utils::reject::custom_server_error(format!("Unable to add payload to block: {e:?}")) }) diff --git a/beacon_node/http_api/tests/broadcast_validation_tests.rs b/beacon_node/http_api/tests/broadcast_validation_tests.rs index 27831b3a232..95c21d8fe2f 100644 --- a/beacon_node/http_api/tests/broadcast_validation_tests.rs +++ b/beacon_node/http_api/tests/broadcast_validation_tests.rs @@ -1275,14 +1275,17 @@ pub async fn blinded_equivocation_consensus_late_equivocation() { Arc::new(block_a), ) .await - .unwrap(); + .expect("failed to reconstruct block") + .expect("block expected"); + let unblinded_block_b = reconstruct_block( tester.harness.chain.clone(), block_b.canonical_root(), block_b.clone(), ) .await - .unwrap(); + .expect("failed to reconstruct block") + .expect("block expected"); let inner_block_a = match unblinded_block_a { ProvenancedBlock::Local(a, _, _) => a, diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index ecd20f3f79c..5ac8cd91864 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -2616,13 +2616,19 @@ impl ApiTester { } pub async fn test_get_config_spec(self) -> Self { - let result = self - .client - .get_config_spec::() - .await - .map(|res| ConfigAndPreset::Fulu(res.data)) - .unwrap(); - let expected = ConfigAndPreset::from_chain_spec::(&self.chain.spec, None); + let result = if self.chain.spec.is_fulu_scheduled() { + self.client + .get_config_spec::() + .await + .map(|res| ConfigAndPreset::Fulu(res.data)) + } else { + self.client + .get_config_spec::() + .await + .map(|res| ConfigAndPreset::Electra(res.data)) + } + .unwrap(); + let expected = ConfigAndPreset::from_chain_spec::(&self.chain.spec); assert_eq!(result, expected); diff --git a/consensus/types/src/config_and_preset.rs b/consensus/types/src/config_and_preset.rs index a613db78f0b..cf5cff8ea67 100644 --- a/consensus/types/src/config_and_preset.rs +++ b/consensus/types/src/config_and_preset.rs @@ -1,6 +1,6 @@ use crate::{ consts::altair, consts::deneb, AltairPreset, BasePreset, BellatrixPreset, CapellaPreset, - ChainSpec, Config, DenebPreset, ElectraPreset, EthSpec, ForkName, FuluPreset, + ChainSpec, Config, DenebPreset, ElectraPreset, EthSpec, FuluPreset, }; use maplit::hashmap; use serde::{Deserialize, Serialize}; @@ -43,7 +43,7 @@ pub struct ConfigAndPreset { } impl ConfigAndPreset { - pub fn from_chain_spec(spec: &ChainSpec, fork_name: Option) -> Self { + pub fn from_chain_spec(spec: &ChainSpec) -> Self { let mut config = Config::from_chain_spec::(spec); let base_preset = BasePreset::from_chain_spec::(spec); let altair_preset = AltairPreset::from_chain_spec::(spec); @@ -52,15 +52,7 @@ impl ConfigAndPreset { let deneb_preset = DenebPreset::from_chain_spec::(spec); let extra_fields = get_extra_fields(spec); - // Remove blob schedule for backwards-compatibility. - if spec.fulu_fork_epoch.is_none() { - config.blob_schedule.set_skip_serializing(); - } - - if spec.fulu_fork_epoch.is_some() - || fork_name.is_none() - || fork_name == Some(ForkName::Fulu) - { + if spec.is_fulu_scheduled() { let electra_preset = ElectraPreset::from_chain_spec::(spec); let fulu_preset = FuluPreset::from_chain_spec::(spec); @@ -75,10 +67,10 @@ impl ConfigAndPreset { fulu_preset, extra_fields, }) - } else if spec.electra_fork_epoch.is_some() - || fork_name.is_none() - || fork_name == Some(ForkName::Electra) - { + } else { + // Remove blob schedule for backwards-compatibility. + config.blob_schedule.set_skip_serializing(); + let electra_preset = ElectraPreset::from_chain_spec::(spec); ConfigAndPreset::Electra(ConfigAndPresetElectra { @@ -91,16 +83,6 @@ impl ConfigAndPreset { electra_preset, extra_fields, }) - } else { - ConfigAndPreset::Deneb(ConfigAndPresetDeneb { - config, - base_preset, - altair_preset, - bellatrix_preset, - capella_preset, - deneb_preset, - extra_fields, - }) } } } @@ -159,8 +141,7 @@ mod test { let mut mainnet_spec = ChainSpec::mainnet(); // setting fulu_fork_epoch because we are roundtripping a fulu config mainnet_spec.fulu_fork_epoch = Some(Epoch::new(42)); - let mut yamlconfig = - ConfigAndPreset::from_chain_spec::(&mainnet_spec, None); + let mut yamlconfig = ConfigAndPreset::from_chain_spec::(&mainnet_spec); let (k1, v1) = ("SAMPLE_HARDFORK_KEY1", "123456789"); let (k2, v2) = ("SAMPLE_HARDFORK_KEY2", "987654321"); let (k3, v3) = ("SAMPLE_HARDFORK_KEY3", 32); diff --git a/testing/validator_test_rig/src/mock_beacon_node.rs b/testing/validator_test_rig/src/mock_beacon_node.rs index 7a902709138..ff1e772d544 100644 --- a/testing/validator_test_rig/src/mock_beacon_node.rs +++ b/testing/validator_test_rig/src/mock_beacon_node.rs @@ -41,7 +41,7 @@ impl MockBeaconNode { pub fn mock_config_spec(&mut self, spec: &ChainSpec) { let path_pattern = Regex::new(r"^/eth/v1/config/spec$").unwrap(); - let config_and_preset = ConfigAndPreset::from_chain_spec::(spec, None); + let config_and_preset = ConfigAndPreset::from_chain_spec::(spec); let data = GenericResponse::from(config_and_preset); self.server .mock("GET", Matcher::Regex(path_pattern.to_string())) diff --git a/validator_client/http_api/src/lib.rs b/validator_client/http_api/src/lib.rs index d5de24229c4..02a677212cb 100644 --- a/validator_client/http_api/src/lib.rs +++ b/validator_client/http_api/src/lib.rs @@ -315,7 +315,7 @@ pub fn serve( .and(spec_filter.clone()) .then(|spec: Arc<_>| { blocking_json_task(move || { - let config = ConfigAndPreset::from_chain_spec::(&spec, None); + let config = ConfigAndPreset::from_chain_spec::(&spec); Ok(api_types::GenericResponse::from(config)) }) }); diff --git a/validator_client/http_api/src/test_utils.rs b/validator_client/http_api/src/test_utils.rs index feb71c3a467..53bcf7baebb 100644 --- a/validator_client/http_api/src/test_utils.rs +++ b/validator_client/http_api/src/test_utils.rs @@ -260,7 +260,7 @@ impl ApiTester { .await .map(|res| ConfigAndPreset::Fulu(res.data)) .unwrap(); - let expected = ConfigAndPreset::from_chain_spec::(&E::default_spec(), None); + let expected = ConfigAndPreset::from_chain_spec::(&E::default_spec()); assert_eq!(result, expected); diff --git a/validator_client/http_api/src/tests.rs b/validator_client/http_api/src/tests.rs index 7d421cd7d58..b021186e77a 100644 --- a/validator_client/http_api/src/tests.rs +++ b/validator_client/http_api/src/tests.rs @@ -45,6 +45,7 @@ struct ApiTester { validator_store: Arc>, url: SensitiveUrl, slot_clock: TestingSlotClock, + spec: Arc, _validator_dir: TempDir, _secrets_dir: TempDir, _test_runtime: TestRuntime, @@ -117,7 +118,7 @@ impl ApiTester { validator_store: Some(validator_store.clone()), graffiti_file: None, graffiti_flag: Some(Graffiti::default()), - spec: E::default_spec().into(), + spec: spec.clone(), config: HttpConfig { enabled: true, listen_addr: IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), @@ -152,6 +153,7 @@ impl ApiTester { validator_store, url, slot_clock, + spec, _validator_dir: validator_dir, _secrets_dir: secrets_dir, _test_runtime: test_runtime, @@ -206,13 +208,19 @@ impl ApiTester { } pub async fn test_get_lighthouse_spec(self) -> Self { - let result = self - .client - .get_lighthouse_spec::() - .await - .map(|res| ConfigAndPreset::Fulu(res.data)) - .unwrap(); - let expected = ConfigAndPreset::from_chain_spec::(&E::default_spec(), None); + let result = if self.spec.is_fulu_scheduled() { + self.client + .get_lighthouse_spec::() + .await + .map(|res| ConfigAndPreset::Fulu(res.data)) + } else { + self.client + .get_lighthouse_spec::() + .await + .map(|res| ConfigAndPreset::Electra(res.data)) + } + .unwrap(); + let expected = ConfigAndPreset::from_chain_spec::(&self.spec); assert_eq!(result, expected);