Skip to content
Open
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
31 changes: 29 additions & 2 deletions beacon_node/http_api/src/beacon/states.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -619,6 +619,33 @@ pub fn post_beacon_state_validators<T: BeaconChainTypes>(
.boxed()
}

// POST beacon/states/{state_id}/builders
pub fn post_beacon_state_builders<T: BeaconChainTypes>(
beacon_states_path: BeaconStatesPath<T>,
) -> 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<T::EthSpec>,
chain: Arc<BeaconChain<T>>,
query: BuildersRequestBody| {
task_spawner.blocking_json_task(Priority::P1, 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<T: BeaconChainTypes>(
beacon_states_path: BeaconStatesPath<T>,
Expand Down
70 changes: 70 additions & 0 deletions beacon_node/http_api/src/builders.rs
Original file line number Diff line number Diff line change
@@ -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<T: BeaconChainTypes>(
state_id: StateId,
chain: Arc<BeaconChain<T>>,
query_ids: &Option<Vec<BuilderId>>,
query_statuses: &Option<Vec<BuilderStatus>>,
) -> Result<ExecutionOptimisticFinalizedResponse<Vec<BuilderData>>, 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<HashSet<&BuilderId>> = query_ids
.as_ref()
.filter(|ids| !ids.is_empty())
.map(HashSet::from_iter);
let statuses_filter_set: Option<HashSet<&BuilderStatus>> = 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 {
execution_optimistic: Some(execution_optimistic),
finalized: Some(finalized),
data,
})
}
5 changes: 5 additions & 0 deletions beacon_node/http_api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ mod beacon;
mod block_id;
mod build_block_contents;
mod builder_states;
mod builders;
mod caches;
mod custody;
mod database;
Expand Down Expand Up @@ -626,6 +627,9 @@ pub async fn serve<T: BeaconChainTypes>(
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());
Expand Down Expand Up @@ -3477,6 +3481,7 @@ pub async fn serve<T: BeaconChainTypes>(
.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)
Expand Down
137 changes: 134 additions & 3 deletions beacon_node/http_api/tests/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,11 @@ 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,
attestation::AttestationBase,
consts::gloas::{BUILDER_INDEX_SELF_BUILD, PAYLOAD_BUILDER_VERSION},
};

type E = MainnetEthSpec;
Expand Down Expand Up @@ -101,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();
Expand Down Expand Up @@ -1178,6 +1184,107 @@ impl ApiTester {
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!(
finalized_epoch > 0,
"precondition: finalized epoch permits an active builder"
);

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 = 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();

BuilderStateTestFixture {
state_id: CoreStateId::Root(state_root),
pending_id,
}
}

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(fixture.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<_>>(),
vec![
(0, BuilderStatus::Active),
(1, BuilderStatus::Pending),
(2, BuilderStatus::Exited),
]
);

let empty_filters = self
.client
.post_beacon_states_builders(fixture.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(
fixture.state_id,
Some(vec![BuilderId::Index(0), BuilderId::Index(u64::MAX)]),
Some(vec![BuilderStatus::Active]),
)
.await
.unwrap()
.unwrap();
assert_eq!(filtered_by_index.data, vec![all_builders.data[0].clone()]);

let filtered_by_pubkey = self
.client
.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()]);

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
Expand Down Expand Up @@ -8454,6 +8561,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_filters()
.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()
Expand Down
23 changes: 23 additions & 0 deletions common/eth2/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -744,6 +744,29 @@ impl BeaconNodeHttpClient {
self.post_with_opt_response(path, &request).await
}

/// `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<Vec<BuilderId>>,
statuses: Option<Vec<BuilderStatus>>,
) -> Result<Option<ExecutionOptimisticFinalizedResponse<Vec<BuilderData>>>, Error> {
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
}

/// `GET beacon/states/{state_id}/committees?slot,index,epoch`
///
/// Returns `Ok(None)` on a 404 error.
Expand Down
Loading
Loading