From 7e2df6b602a1c3593605eb21cd4761aebbdb90e7 Mon Sep 17 00:00:00 2001 From: chonghe <44791194+chong-he@users.noreply.github.com> Date: Tue, 20 May 2025 15:18:29 +0800 Subject: [PATCH] Empty list `[]` to return all validators balances (#7474) The endpoint `/eth/v1/beacon/states/head/validator_balances` returns an empty data when the data field is `[]`. According to the beacon API spec, it should return the balances of all validators: Reference: https://ethereum.github.io/beacon-APIs/#/Beacon/postStateValidatorBalances `If the supplied list is empty (i.e. the body is []) or no body is supplied then balances will be returned for all validators.` This PR changes so that: `curl -X 'POST' 'http://localhost:5052/eth/v1/beacon/states/head/validator_balances' -d '[]' | jq` returns balances of all validators. --- beacon_node/http_api/src/lib.rs | 2 +- beacon_node/http_api/src/validators.rs | 9 ++++++-- beacon_node/http_api/tests/tests.rs | 32 ++++++++++++++++++-------- common/eth2/src/types.rs | 2 +- common/warp_utils/src/json.rs | 24 +++++++++++++++++++ 5 files changed, 56 insertions(+), 13 deletions(-) diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index ff3bfce19d..2eaa33a964 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -709,7 +709,7 @@ pub fn serve( .clone() .and(warp::path("validator_balances")) .and(warp::path::end()) - .and(warp_utils::json::json()) + .and(warp_utils::json::json_no_body()) .then( |state_id: StateId, task_spawner: TaskSpawner, diff --git a/beacon_node/http_api/src/validators.rs b/beacon_node/http_api/src/validators.rs index f3d78e6fcd..90ddd1ee8f 100644 --- a/beacon_node/http_api/src/validators.rs +++ b/beacon_node/http_api/src/validators.rs @@ -81,8 +81,13 @@ pub fn get_beacon_state_validator_balances( .map_state_and_execution_optimistic_and_finalized( &chain, |state, execution_optimistic, finalized| { - let ids_filter_set: Option> = - optional_ids.map(|f| HashSet::from_iter(f.iter())); + let ids_filter_set: Option> = match optional_ids { + // if optional_ids (the request data body) is [], returns a `None`, so that later when calling .is_none_or() will return True + // Hence, all validators will pass through .filter(), and balances of all validators are returned, in accordance to the spec + Some([]) => None, + Some(ids) => Some(HashSet::from_iter(ids.iter())), + None => None, + }; Ok(( state diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index 33ac8e413d..a5a21fd985 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -927,18 +927,32 @@ impl ApiTester { .map(|res| res.data); let expected = state_opt.map(|(state, _execution_optimistic, _finalized)| { - let mut validators = Vec::with_capacity(validator_indices.len()); + // If validator_indices is empty, return balances for all validators + if validator_indices.is_empty() { + state + .balances() + .iter() + .enumerate() + .map(|(index, balance)| ValidatorBalanceData { + index: index as u64, + balance: *balance, + }) + .collect() + } else { + // Same behaviour as before for the else branch + let mut validators = Vec::with_capacity(validator_indices.len()); - for i in validator_indices { - if i < state.balances().len() as u64 { - validators.push(ValidatorBalanceData { - index: i, - balance: *state.balances().get(i as usize).unwrap(), - }); + for i in validator_indices { + if i < state.balances().len() as u64 { + validators.push(ValidatorBalanceData { + index: i, + balance: *state.balances().get(i as usize).unwrap(), + }); + } } - } - validators + validators + } }); assert_eq!(result_index_ids, expected, "{:?}", state_id); diff --git a/common/eth2/src/types.rs b/common/eth2/src/types.rs index f895e5cb8f..b8c74d4dcd 100644 --- a/common/eth2/src/types.rs +++ b/common/eth2/src/types.rs @@ -688,7 +688,7 @@ pub struct ValidatorBalancesQuery { pub id: Option>, } -#[derive(Clone, Serialize, Deserialize)] +#[derive(Clone, Default, Serialize, Deserialize)] #[serde(transparent)] pub struct ValidatorBalancesRequestBody { pub ids: Vec, diff --git a/common/warp_utils/src/json.rs b/common/warp_utils/src/json.rs index 6ee5e77261..bc7d61557b 100644 --- a/common/warp_utils/src/json.rs +++ b/common/warp_utils/src/json.rs @@ -31,3 +31,27 @@ pub fn json() -> impl Filter( +) -> impl Filter + Copy { + warp::header::optional::(CONTENT_TYPE_HEADER) + .and(warp::body::bytes()) + .and_then(|header: Option, bytes: Bytes| async move { + if let Some(header) = header { + if header == SSZ_CONTENT_TYPE_HEADER { + return Err(reject::unsupported_media_type( + "The request's content-type is not supported".to_string(), + )); + } + } + + // Handle the case when the HTTP request has no body, i.e., without the -d header + if bytes.is_empty() { + return Ok(T::default()); + } + + Json::decode(bytes) + .map_err(|err| reject::custom_deserialize_error(format!("{:?}", err))) + }) +}