From 10a9d4a2c3909f7e910160df71ba8327642a79e9 Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Thu, 23 Jul 2026 13:23:07 +0000 Subject: [PATCH 1/4] Implement Gloas builder state endpoint --- beacon_node/http_api/src/beacon/states.rs | 36 +++- beacon_node/http_api/src/builders.rs | 70 ++++++++ beacon_node/http_api/src/lib.rs | 5 + beacon_node/http_api/tests/tests.rs | 208 +++++++++++++++++++++- common/eth2/src/lib.rs | 28 +++ common/eth2/src/types.rs | 88 +++++++++ 6 files changed, 432 insertions(+), 3 deletions(-) create mode 100644 beacon_node/http_api/src/builders.rs diff --git a/beacon_node/http_api/src/beacon/states.rs b/beacon_node/http_api/src/beacon/states.rs index 0aabf0ccfe2..f9a70a3748f 100644 --- a/beacon_node/http_api/src/beacon/states.rs +++ b/beacon_node/http_api/src/beacon/states.rs @@ -9,8 +9,8 @@ use crate::version::{ }; use beacon_chain::{BeaconChain, BeaconChainError, BeaconChainTypes, WhenSlotSkipped}; use eth2::types::{ - self as api_types, ValidatorBalancesRequestBody, ValidatorId, ValidatorIdentitiesRequestBody, - ValidatorIndexData, ValidatorsRequestBody, + self as api_types, BuildersRequestBody, ValidatorBalancesRequestBody, ValidatorId, + ValidatorIdentitiesRequestBody, ValidatorIndexData, ValidatorsRequestBody, }; use ssz::Encode; use std::sync::Arc; @@ -619,6 +619,38 @@ pub fn post_beacon_state_validators( .boxed() } +// POST beacon/states/{state_id}/builders +pub fn post_beacon_state_builders( + beacon_states_path: BeaconStatesPath, +) -> ResponseFilter { + beacon_states_path + .clone() + .and(warp::path("builders")) + .and(warp::path::end()) + .and(warp_utils::json::json_no_body()) + .then( + |state_id: StateId, + task_spawner: TaskSpawner, + chain: Arc>, + query: BuildersRequestBody| { + let priority = if let StateId(eth2::types::StateId::Head) = state_id { + Priority::P0 + } else { + Priority::P1 + }; + task_spawner.blocking_json_task(priority, move || { + crate::builders::get_beacon_state_builders( + state_id, + chain, + &query.ids, + &query.statuses, + ) + }) + }, + ) + .boxed() +} + // GET beacon/states/{state_id}/validators?id,status pub fn get_beacon_state_validators( beacon_states_path: BeaconStatesPath, diff --git a/beacon_node/http_api/src/builders.rs b/beacon_node/http_api/src/builders.rs new file mode 100644 index 00000000000..5a70737f95c --- /dev/null +++ b/beacon_node/http_api/src/builders.rs @@ -0,0 +1,70 @@ +use crate::state_id::StateId; +use beacon_chain::{BeaconChain, BeaconChainTypes}; +use eth2::types::{BuilderData, BuilderId, BuilderStatus, ExecutionOptimisticFinalizedResponse}; +use std::{collections::HashSet, sync::Arc}; + +pub fn get_beacon_state_builders( + state_id: StateId, + chain: Arc>, + query_ids: &Option>, + query_statuses: &Option>, +) -> Result>, warp::Rejection> { + let (data, execution_optimistic, finalized) = state_id + .map_state_and_execution_optimistic_and_finalized( + &chain, + |state, execution_optimistic, finalized| { + let builders = state.builders().map_err(|_| { + warp_utils::reject::custom_bad_request( + "Builders are not available for pre-Gloas states".to_string(), + ) + })?; + let finalized_epoch = state.finalized_checkpoint().epoch; + let far_future_epoch = chain.spec.far_future_epoch; + + let ids_filter_set: Option> = query_ids + .as_ref() + .filter(|ids| !ids.is_empty()) + .map(HashSet::from_iter); + let statuses_filter_set: Option> = query_statuses + .as_ref() + .filter(|statuses| !statuses.is_empty()) + .map(HashSet::from_iter); + + Ok(( + builders + .iter() + .enumerate() + .filter(|(index, builder)| { + ids_filter_set.as_ref().is_none_or(|ids| { + ids.contains(&BuilderId::PublicKey(builder.pubkey)) + || ids.contains(&BuilderId::Index(*index as u64)) + }) + }) + .filter_map(|(index, builder)| { + let status = BuilderStatus::from_builder( + builder, + finalized_epoch, + far_future_epoch, + ); + statuses_filter_set + .as_ref() + .is_none_or(|statuses| statuses.contains(&status)) + .then(|| BuilderData { + index: index as u64, + status, + builder: builder.clone(), + }) + }) + .collect(), + execution_optimistic, + finalized, + )) + }, + )?; + + Ok(ExecutionOptimisticFinalizedResponse { + data, + execution_optimistic: Some(execution_optimistic), + finalized: Some(finalized), + }) +} diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index e088ee08205..ebbf201805a 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -12,6 +12,7 @@ mod beacon; mod block_id; mod build_block_contents; mod builder_states; +mod builders; mod caches; mod custody; mod database; @@ -626,6 +627,9 @@ pub async fn serve( let post_beacon_state_validators = states::post_beacon_state_validators(beacon_states_path.clone()); + // POST beacon/states/{state_id}/builders + let post_beacon_state_builders = states::post_beacon_state_builders(beacon_states_path.clone()); + // GET beacon/states/{state_id}/validators/{validator_id} let get_beacon_state_validators_id = states::get_beacon_state_validators_id(beacon_states_path.clone()); @@ -3477,6 +3481,7 @@ pub async fn serve( .uor(post_beacon_execution_payload_envelopes) .uor(post_beacon_execution_payload_bids) .uor(post_beacon_state_validators) + .uor(post_beacon_state_builders) .uor(post_beacon_state_validator_balances) .uor(post_beacon_state_validator_identities) .uor(post_beacon_rewards_attestations) diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index 9b4dfbba873..a243ce2c03b 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -52,7 +52,8 @@ use types::{ Address, Domain, EthSpec, ExecutionBlockHash, ExecutionPayloadBid, Hash256, MainnetEthSpec, ProposerPreferences, RelativeEpoch, SelectionProof, SignedExecutionPayloadBid, SignedExecutionPayloadEnvelope, SignedProposerPreferences, SignedRoot, SingleAttestation, Slot, - attestation::AttestationBase, consts::gloas::BUILDER_INDEX_SELF_BUILD, + attestation::AttestationBase, + consts::gloas::{BUILDER_INDEX_SELF_BUILD, PAYLOAD_BUILDER_VERSION}, }; type E = MainnetEthSpec; @@ -1178,6 +1179,187 @@ impl ApiTester { self } + pub async fn test_beacon_state_builders(self) -> Self { + let mut state = self.chain.head_snapshot().beacon_state.clone(); + let finalized_epoch = state.finalized_checkpoint().epoch; + assert!( + finalized_epoch > 0, + "precondition: finalized epoch permits an active builder" + ); + + let builder_withdrawal_credentials = |address: Address| { + let mut credentials = [0; 32]; + credentials[0] = self.chain.spec.builder_withdrawal_prefix_byte; + credentials[12..].copy_from_slice(address.as_slice()); + Hash256::from_slice(&credentials) + }; + + let active_index = state + .add_builder_to_registry( + self.validator_keypairs()[0].pk.clone().into(), + PAYLOAD_BUILDER_VERSION, + builder_withdrawal_credentials(Address::repeat_byte(1)), + self.chain.spec.min_deposit_amount, + Slot::new(0), + &self.chain.spec, + ) + .unwrap(); + let pending_index = state + .add_builder_to_registry( + self.validator_keypairs()[1].pk.clone().into(), + PAYLOAD_BUILDER_VERSION, + builder_withdrawal_credentials(Address::repeat_byte(2)), + self.chain.spec.min_deposit_amount, + finalized_epoch.start_slot(E::slots_per_epoch()), + &self.chain.spec, + ) + .unwrap(); + let exited_index = state + .add_builder_to_registry( + self.validator_keypairs()[2].pk.clone().into(), + PAYLOAD_BUILDER_VERSION, + builder_withdrawal_credentials(Address::repeat_byte(3)), + self.chain.spec.min_deposit_amount, + Slot::new(0), + &self.chain.spec, + ) + .unwrap(); + state + .builders_mut() + .unwrap() + .get_mut(exited_index as usize) + .unwrap() + .withdrawable_epoch = state.current_epoch(); + + let state_root = state.update_tree_hash_cache().unwrap(); + self.chain.store.put_state(&state_root, &state).unwrap(); + let state_id = CoreStateId::Root(state_root); + + let all_builders = self + .client + .post_beacon_states_builders(state_id, None, None) + .await + .unwrap() + .unwrap(); + assert_eq!(all_builders.execution_optimistic, Some(false)); + assert_eq!(all_builders.finalized, Some(false)); + assert_eq!( + all_builders + .data + .iter() + .map(|builder| (builder.index, builder.status)) + .collect::>(), + vec![ + (active_index, BuilderStatus::Active), + (pending_index, BuilderStatus::Pending), + (exited_index, BuilderStatus::Exited), + ] + ); + + let empty_filters = self + .client + .post_beacon_states_builders(state_id, Some(vec![]), Some(vec![])) + .await + .unwrap() + .unwrap(); + assert_eq!(empty_filters, all_builders); + + let filtered_by_index = self + .client + .post_beacon_states_builders( + state_id, + Some(vec![ + BuilderId::Index(active_index), + BuilderId::Index(u64::MAX), + ]), + Some(vec![BuilderStatus::Active]), + ) + .await + .unwrap() + .unwrap(); + assert_eq!(filtered_by_index.data, vec![all_builders.data[0].clone()]); + + let pending_pubkey = state + .builders() + .unwrap() + .get(pending_index as usize) + .unwrap() + .pubkey; + let filtered_by_pubkey = self + .client + .post_beacon_states_builders( + state_id, + Some(vec![BuilderId::PublicKey(pending_pubkey)]), + None, + ) + .await + .unwrap() + .unwrap(); + assert_eq!(filtered_by_pubkey.data, vec![all_builders.data[1].clone()]); + + let path = self + .client + .post_beacon_states_builders_path(state_id) + .unwrap(); + let no_body_response = reqwest::Client::new() + .post(path.clone()) + .send() + .await + .unwrap(); + assert_eq!(no_body_response.status(), StatusCode::OK); + let no_body_builders = no_body_response + .json::>>() + .await + .unwrap(); + assert_eq!(no_body_builders, all_builders); + + for invalid_body in [ + serde_json::json!({"ids": ["invalid"]}), + serde_json::json!({"statuses": ["unknown"]}), + serde_json::json!({"unknown": []}), + ] { + let invalid_response = reqwest::Client::new() + .post(path.clone()) + .json(&invalid_body) + .send() + .await + .unwrap(); + assert_eq!(invalid_response.status(), StatusCode::BAD_REQUEST); + } + + let malformed_response = reqwest::Client::new() + .post(path) + .header(CONTENT_TYPE_HEADER, JSON_CONTENT_TYPE_HEADER) + .body("{") + .send() + .await + .unwrap(); + assert_eq!(malformed_response.status(), StatusCode::BAD_REQUEST); + + let missing_state_path = self + .client + .post_beacon_states_builders_path(CoreStateId::Root(Hash256::zero())) + .unwrap(); + let missing_state_response = reqwest::Client::new() + .post(missing_state_path) + .send() + .await + .unwrap(); + assert_eq!(missing_state_response.status(), StatusCode::NOT_FOUND); + + self + } + + pub async fn test_beacon_state_builders_pre_gloas(self) -> Self { + let error = self + .client + .post_beacon_states_builders(CoreStateId::Head, None, None) + .await + .unwrap_err(); + assert_eq!(error.status(), Some(StatusCode::BAD_REQUEST)); + self + } + pub async fn test_beacon_states_validator_id(self) -> Self { for state_id in self.interesting_state_ids() { let state_opt = state_id @@ -8454,6 +8636,30 @@ async fn beacon_get_state_info_fulu() { .await; } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn beacon_state_builders_gloas() { + let mut config = ApiTesterConfig::default(); + config.spec.altair_fork_epoch = Some(Epoch::new(0)); + config.spec.bellatrix_fork_epoch = Some(Epoch::new(0)); + config.spec.capella_fork_epoch = Some(Epoch::new(0)); + config.spec.deneb_fork_epoch = Some(Epoch::new(0)); + config.spec.electra_fork_epoch = Some(Epoch::new(0)); + config.spec.fulu_fork_epoch = Some(Epoch::new(0)); + config.spec.gloas_fork_epoch = Some(Epoch::new(0)); + ApiTester::new_from_config(config) + .await + .test_beacon_state_builders() + .await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn beacon_state_builders_pre_gloas() { + ApiTester::new() + .await + .test_beacon_state_builders_pre_gloas() + .await; +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn beacon_get_blocks() { ApiTester::new() diff --git a/common/eth2/src/lib.rs b/common/eth2/src/lib.rs index 3bda02935fb..55eebe247e0 100644 --- a/common/eth2/src/lib.rs +++ b/common/eth2/src/lib.rs @@ -744,6 +744,34 @@ impl BeaconNodeHttpClient { self.post_with_opt_response(path, &request).await } + pub fn post_beacon_states_builders_path(&self, state_id: StateId) -> Result { + let mut path = self.eth_path(V1)?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("beacon") + .push("states") + .push(&state_id.to_string()) + .push("builders"); + + Ok(path) + } + + /// `POST beacon/states/{state_id}/builders` + /// + /// Returns `Ok(None)` on a 404 error. + pub async fn post_beacon_states_builders( + &self, + state_id: StateId, + ids: Option>, + statuses: Option>, + ) -> Result>>, Error> { + let path = self.post_beacon_states_builders_path(state_id)?; + let request = BuildersRequestBody { ids, statuses }; + + self.post_with_opt_response(path, &request).await + } + /// `GET beacon/states/{state_id}/committees?slot,index,epoch` /// /// Returns `Ok(None)` on a 404 error. diff --git a/common/eth2/src/types.rs b/common/eth2/src/types.rs index d69e99293a9..fe0da82b48c 100644 --- a/common/eth2/src/types.rs +++ b/common/eth2/src/types.rs @@ -300,6 +300,53 @@ impl From for String { } } +#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] +#[serde(into = "String")] +#[serde(try_from = "std::borrow::Cow")] +pub enum BuilderId { + PublicKey(PublicKeyBytes), + Index(u64), +} + +impl TryFrom> for BuilderId { + type Error = String; + + fn try_from(s: std::borrow::Cow) -> Result { + Self::from_str(&s) + } +} + +impl FromStr for BuilderId { + type Err = String; + + fn from_str(s: &str) -> Result { + if s.starts_with("0x") { + PublicKeyBytes::from_str(s) + .map(BuilderId::PublicKey) + .map_err(|e| format!("{} cannot be parsed as a public key: {}", s, e)) + } else { + u64::from_str(s) + .map(BuilderId::Index) + .map_err(|e| format!("{} cannot be parsed as a builder index: {}", s, e)) + } + } +} + +impl fmt::Display for BuilderId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + BuilderId::PublicKey(pubkey) => write!(f, "{:?}", pubkey), + BuilderId::Index(index) => write!(f, "{}", index), + } + } +} + +impl From for String { + fn from(id: BuilderId) -> String { + id.to_string() + } +} + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct ValidatorData { #[serde(with = "serde_utils::quoted_u64")] @@ -326,6 +373,38 @@ pub struct ValidatorIdentityData { pub activation_epoch: Epoch, } +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct BuilderData { + #[serde(with = "serde_utils::quoted_u64")] + pub index: u64, + pub status: BuilderStatus, + pub builder: Builder, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum BuilderStatus { + Pending, + Active, + Exited, +} + +impl BuilderStatus { + pub fn from_builder( + builder: &Builder, + finalized_epoch: Epoch, + far_future_epoch: Epoch, + ) -> Self { + if builder.withdrawable_epoch != far_future_epoch { + Self::Exited + } else if builder.deposit_epoch < finalized_epoch { + Self::Active + } else { + Self::Pending + } + } +} + // Implemented according to what is described here: // // https://hackmd.io/ofFJ5gOmQpu1jjHilHbdQQ @@ -492,6 +571,15 @@ pub struct ValidatorsRequestBody { pub statuses: Option>, } +#[derive(Debug, Default, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct BuildersRequestBody { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub ids: Option>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub statuses: Option>, +} + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct CommitteeData { #[serde(with = "serde_utils::quoted_u64")] From ee4e27101f1bf1b265b317b9b581f46514347d00 Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Thu, 23 Jul 2026 13:53:17 +0000 Subject: [PATCH 2/4] Refine builder endpoint scheduling and tests --- beacon_node/http_api/src/beacon/states.rs | 7 +- beacon_node/http_api/tests/tests.rs | 186 +++++++++------------- common/eth2/src/lib.rs | 23 ++- 3 files changed, 86 insertions(+), 130 deletions(-) diff --git a/beacon_node/http_api/src/beacon/states.rs b/beacon_node/http_api/src/beacon/states.rs index f9a70a3748f..2aa549afab9 100644 --- a/beacon_node/http_api/src/beacon/states.rs +++ b/beacon_node/http_api/src/beacon/states.rs @@ -633,12 +633,7 @@ pub fn post_beacon_state_builders( task_spawner: TaskSpawner, chain: Arc>, query: BuildersRequestBody| { - let priority = if let StateId(eth2::types::StateId::Head) = state_id { - Priority::P0 - } else { - Priority::P1 - }; - task_spawner.blocking_json_task(priority, move || { + task_spawner.blocking_json_task(Priority::P1, move || { crate::builders::get_beacon_state_builders( state_id, chain, diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index a243ce2c03b..2573840df1f 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -36,7 +36,7 @@ use network::NetworkReceivers; use network_utils::enr_ext::EnrExt; use operation_pool::attestation_storage::CheckpointKey; use proto_array::{ExecutionStatus, core::ProtoNode}; -use reqwest::{RequestBuilder, Response, StatusCode}; +use reqwest::{RequestBuilder, Response, StatusCode, Url}; use sensitive_url::SensitiveUrl; use slot_clock::SlotClock; use ssz::{BitList, Decode}; @@ -49,8 +49,8 @@ use tokio::time::Duration; use tree_hash::TreeHash; use types::ApplicationDomain; use types::{ - Address, Domain, EthSpec, ExecutionBlockHash, ExecutionPayloadBid, Hash256, MainnetEthSpec, - ProposerPreferences, RelativeEpoch, SelectionProof, SignedExecutionPayloadBid, + Address, Builder, Domain, EthSpec, ExecutionBlockHash, ExecutionPayloadBid, Hash256, + MainnetEthSpec, ProposerPreferences, RelativeEpoch, SelectionProof, SignedExecutionPayloadBid, SignedExecutionPayloadEnvelope, SignedProposerPreferences, SignedRoot, SingleAttestation, Slot, attestation::AttestationBase, consts::gloas::{BUILDER_INDEX_SELF_BUILD, PAYLOAD_BUILDER_VERSION}, @@ -102,6 +102,11 @@ struct ApiTesterConfig { node_custody_type: NodeCustodyType, } +struct BuilderStateTestFixture { + state_id: CoreStateId, + pending_id: BuilderId, +} + impl Default for ApiTesterConfig { fn default() -> Self { let mut spec = E::default_spec(); @@ -1179,7 +1184,7 @@ impl ApiTester { self } - pub async fn test_beacon_state_builders(self) -> Self { + fn builder_state_test_fixture(&self) -> BuilderStateTestFixture { let mut state = self.chain.head_snapshot().beacon_state.clone(); let finalized_epoch = state.finalized_checkpoint().epoch; assert!( @@ -1187,57 +1192,54 @@ impl ApiTester { "precondition: finalized epoch permits an active builder" ); - let builder_withdrawal_credentials = |address: Address| { - let mut credentials = [0; 32]; - credentials[0] = self.chain.spec.builder_withdrawal_prefix_byte; - credentials[12..].copy_from_slice(address.as_slice()); - Hash256::from_slice(&credentials) - }; + let builder = + |keypair_index: usize, deposit_epoch: Epoch, withdrawable_epoch: Epoch| Builder { + pubkey: self.validator_keypairs()[keypair_index].pk.clone().into(), + version: PAYLOAD_BUILDER_VERSION, + execution_address: Address::repeat_byte(keypair_index as u8 + 1), + balance: self.chain.spec.min_deposit_amount, + deposit_epoch, + withdrawable_epoch, + }; - let active_index = state - .add_builder_to_registry( - self.validator_keypairs()[0].pk.clone().into(), - PAYLOAD_BUILDER_VERSION, - builder_withdrawal_credentials(Address::repeat_byte(1)), - self.chain.spec.min_deposit_amount, - Slot::new(0), - &self.chain.spec, - ) - .unwrap(); - let pending_index = state - .add_builder_to_registry( - self.validator_keypairs()[1].pk.clone().into(), - PAYLOAD_BUILDER_VERSION, - builder_withdrawal_credentials(Address::repeat_byte(2)), - self.chain.spec.min_deposit_amount, - finalized_epoch.start_slot(E::slots_per_epoch()), - &self.chain.spec, - ) - .unwrap(); - let exited_index = state - .add_builder_to_registry( - self.validator_keypairs()[2].pk.clone().into(), - PAYLOAD_BUILDER_VERSION, - builder_withdrawal_credentials(Address::repeat_byte(3)), - self.chain.spec.min_deposit_amount, - Slot::new(0), - &self.chain.spec, - ) - .unwrap(); - state - .builders_mut() - .unwrap() - .get_mut(exited_index as usize) - .unwrap() - .withdrawable_epoch = state.current_epoch(); + let active = builder(0, Epoch::new(0), self.chain.spec.far_future_epoch); + let pending = builder(1, finalized_epoch, self.chain.spec.far_future_epoch); + let pending_id = BuilderId::PublicKey(pending.pubkey); + let exited = builder(2, Epoch::new(0), state.current_epoch()); + let builders = state.builders_mut().unwrap(); + assert!(builders.is_empty()); + builders.push(active).unwrap(); + builders.push(pending).unwrap(); + builders.push(exited).unwrap(); let state_root = state.update_tree_hash_cache().unwrap(); self.chain.store.put_state(&state_root, &state).unwrap(); - let state_id = CoreStateId::Root(state_root); + BuilderStateTestFixture { + state_id: CoreStateId::Root(state_root), + pending_id, + } + } + + fn beacon_state_builders_url(&self, state_id: CoreStateId) -> Url { + let mut path = self.client.server().expose_full().clone(); + path.path_segments_mut() + .unwrap() + .pop_if_empty() + .push("eth") + .push("v1") + .push("beacon") + .push("states") + .push(&state_id.to_string()) + .push("builders"); + path + } + + pub async fn test_beacon_state_builders_filters(self) -> Self { + let fixture = self.builder_state_test_fixture(); let all_builders = self .client - .post_beacon_states_builders(state_id, None, None) + .post_beacon_states_builders(fixture.state_id, None, None) .await .unwrap() .unwrap(); @@ -1250,15 +1252,15 @@ impl ApiTester { .map(|builder| (builder.index, builder.status)) .collect::>(), vec![ - (active_index, BuilderStatus::Active), - (pending_index, BuilderStatus::Pending), - (exited_index, BuilderStatus::Exited), + (0, BuilderStatus::Active), + (1, BuilderStatus::Pending), + (2, BuilderStatus::Exited), ] ); let empty_filters = self .client - .post_beacon_states_builders(state_id, Some(vec![]), Some(vec![])) + .post_beacon_states_builders(fixture.state_id, Some(vec![]), Some(vec![])) .await .unwrap() .unwrap(); @@ -1267,11 +1269,8 @@ impl ApiTester { let filtered_by_index = self .client .post_beacon_states_builders( - state_id, - Some(vec![ - BuilderId::Index(active_index), - BuilderId::Index(u64::MAX), - ]), + fixture.state_id, + Some(vec![BuilderId::Index(0), BuilderId::Index(u64::MAX)]), Some(vec![BuilderStatus::Active]), ) .await @@ -1279,73 +1278,36 @@ impl ApiTester { .unwrap(); assert_eq!(filtered_by_index.data, vec![all_builders.data[0].clone()]); - let pending_pubkey = state - .builders() - .unwrap() - .get(pending_index as usize) - .unwrap() - .pubkey; let filtered_by_pubkey = self .client - .post_beacon_states_builders( - state_id, - Some(vec![BuilderId::PublicKey(pending_pubkey)]), - None, - ) + .post_beacon_states_builders(fixture.state_id, Some(vec![fixture.pending_id]), None) .await .unwrap() .unwrap(); assert_eq!(filtered_by_pubkey.data, vec![all_builders.data[1].clone()]); - let path = self - .client - .post_beacon_states_builders_path(state_id) - .unwrap(); - let no_body_response = reqwest::Client::new() - .post(path.clone()) - .send() - .await - .unwrap(); - assert_eq!(no_body_response.status(), StatusCode::OK); - let no_body_builders = no_body_response - .json::>>() - .await - .unwrap(); - assert_eq!(no_body_builders, all_builders); - - for invalid_body in [ - serde_json::json!({"ids": ["invalid"]}), - serde_json::json!({"statuses": ["unknown"]}), - serde_json::json!({"unknown": []}), - ] { - let invalid_response = reqwest::Client::new() - .post(path.clone()) - .json(&invalid_body) - .send() - .await - .unwrap(); - assert_eq!(invalid_response.status(), StatusCode::BAD_REQUEST); - } + self + } - let malformed_response = reqwest::Client::new() - .post(path) - .header(CONTENT_TYPE_HEADER, JSON_CONTENT_TYPE_HEADER) - .body("{") + pub async fn test_beacon_state_builders_no_body(self) -> Self { + let response = reqwest::Client::new() + .post(self.beacon_state_builders_url(CoreStateId::Head)) .send() .await .unwrap(); - assert_eq!(malformed_response.status(), StatusCode::BAD_REQUEST); + assert_eq!(response.status(), StatusCode::OK); - let missing_state_path = self - .client - .post_beacon_states_builders_path(CoreStateId::Root(Hash256::zero())) - .unwrap(); - let missing_state_response = reqwest::Client::new() - .post(missing_state_path) + self + } + + pub async fn test_beacon_state_builders_invalid_id(self) -> Self { + let response = reqwest::Client::new() + .post(self.beacon_state_builders_url(CoreStateId::Head)) + .json(&serde_json::json!({"ids": ["invalid"]})) .send() .await .unwrap(); - assert_eq!(missing_state_response.status(), StatusCode::NOT_FOUND); + assert_eq!(response.status(), StatusCode::BAD_REQUEST); self } @@ -8648,7 +8610,11 @@ async fn beacon_state_builders_gloas() { config.spec.gloas_fork_epoch = Some(Epoch::new(0)); ApiTester::new_from_config(config) .await - .test_beacon_state_builders() + .test_beacon_state_builders_filters() + .await + .test_beacon_state_builders_no_body() + .await + .test_beacon_state_builders_invalid_id() .await; } diff --git a/common/eth2/src/lib.rs b/common/eth2/src/lib.rs index 55eebe247e0..4502e23c615 100644 --- a/common/eth2/src/lib.rs +++ b/common/eth2/src/lib.rs @@ -744,19 +744,6 @@ impl BeaconNodeHttpClient { self.post_with_opt_response(path, &request).await } - pub fn post_beacon_states_builders_path(&self, state_id: StateId) -> Result { - let mut path = self.eth_path(V1)?; - - path.path_segments_mut() - .map_err(|()| Error::InvalidUrl(self.server.clone()))? - .push("beacon") - .push("states") - .push(&state_id.to_string()) - .push("builders"); - - Ok(path) - } - /// `POST beacon/states/{state_id}/builders` /// /// Returns `Ok(None)` on a 404 error. @@ -766,7 +753,15 @@ impl BeaconNodeHttpClient { ids: Option>, statuses: Option>, ) -> Result>>, Error> { - let path = self.post_beacon_states_builders_path(state_id)?; + let mut path = self.eth_path(V1)?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("beacon") + .push("states") + .push(&state_id.to_string()) + .push("builders"); + let request = BuildersRequestBody { ids, statuses }; self.post_with_opt_response(path, &request).await From 89b14289925b031ec35902bfba2cfac13deeeb91 Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Thu, 23 Jul 2026 14:02:08 +0000 Subject: [PATCH 3/4] Trim builder endpoint tests --- beacon_node/http_api/tests/tests.rs | 43 +---------------------------- 1 file changed, 1 insertion(+), 42 deletions(-) diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index 2573840df1f..04b10bd6e9d 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -36,7 +36,7 @@ use network::NetworkReceivers; use network_utils::enr_ext::EnrExt; use operation_pool::attestation_storage::CheckpointKey; use proto_array::{ExecutionStatus, core::ProtoNode}; -use reqwest::{RequestBuilder, Response, StatusCode, Url}; +use reqwest::{RequestBuilder, Response, StatusCode}; use sensitive_url::SensitiveUrl; use slot_clock::SlotClock; use ssz::{BitList, Decode}; @@ -1221,20 +1221,6 @@ impl ApiTester { } } - fn beacon_state_builders_url(&self, state_id: CoreStateId) -> Url { - let mut path = self.client.server().expose_full().clone(); - path.path_segments_mut() - .unwrap() - .pop_if_empty() - .push("eth") - .push("v1") - .push("beacon") - .push("states") - .push(&state_id.to_string()) - .push("builders"); - path - } - pub async fn test_beacon_state_builders_filters(self) -> Self { let fixture = self.builder_state_test_fixture(); let all_builders = self @@ -1289,29 +1275,6 @@ impl ApiTester { self } - pub async fn test_beacon_state_builders_no_body(self) -> Self { - let response = reqwest::Client::new() - .post(self.beacon_state_builders_url(CoreStateId::Head)) - .send() - .await - .unwrap(); - assert_eq!(response.status(), StatusCode::OK); - - self - } - - pub async fn test_beacon_state_builders_invalid_id(self) -> Self { - let response = reqwest::Client::new() - .post(self.beacon_state_builders_url(CoreStateId::Head)) - .json(&serde_json::json!({"ids": ["invalid"]})) - .send() - .await - .unwrap(); - assert_eq!(response.status(), StatusCode::BAD_REQUEST); - - self - } - pub async fn test_beacon_state_builders_pre_gloas(self) -> Self { let error = self .client @@ -8611,10 +8574,6 @@ async fn beacon_state_builders_gloas() { ApiTester::new_from_config(config) .await .test_beacon_state_builders_filters() - .await - .test_beacon_state_builders_no_body() - .await - .test_beacon_state_builders_invalid_id() .await; } From 41d63325e1ec51ecbbf3b3703178d1f819a78bc4 Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Wed, 29 Jul 2026 13:21:08 +1000 Subject: [PATCH 4/4] Address code style fixes Co-authored-by: chonghe <44791194+chong-he@users.noreply.github.com> --- beacon_node/http_api/src/builders.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beacon_node/http_api/src/builders.rs b/beacon_node/http_api/src/builders.rs index 5a70737f95c..8e26d1f5ce0 100644 --- a/beacon_node/http_api/src/builders.rs +++ b/beacon_node/http_api/src/builders.rs @@ -63,8 +63,8 @@ pub fn get_beacon_state_builders( )?; Ok(ExecutionOptimisticFinalizedResponse { - data, execution_optimistic: Some(execution_optimistic), finalized: Some(finalized), + data, }) }