Skip to content

Commit 382322e

Browse files
committed
Implement expected withdrawals endpoint (#4390)
## Issue Addressed [#4029](#4029) ## Proposed Changes implement expected_withdrawals HTTP API per the spec ethereum/beacon-APIs#304 ## Additional Info
1 parent f92b856 commit 382322e

File tree

5 files changed

+252
-0
lines changed

5 files changed

+252
-0
lines changed
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
use crate::StateId;
2+
use beacon_chain::{BeaconChain, BeaconChainTypes};
3+
use safe_arith::SafeArith;
4+
use state_processing::per_block_processing::get_expected_withdrawals;
5+
use state_processing::state_advance::partial_state_advance;
6+
use std::sync::Arc;
7+
use types::{BeaconState, EthSpec, ForkName, Slot, Withdrawals};
8+
9+
const MAX_EPOCH_LOOKAHEAD: u64 = 2;
10+
11+
/// Get the withdrawals computed from the specified state, that will be included in the block
12+
/// that gets built on the specified state.
13+
pub fn get_next_withdrawals<T: BeaconChainTypes>(
14+
chain: &Arc<BeaconChain<T>>,
15+
mut state: BeaconState<T::EthSpec>,
16+
state_id: StateId,
17+
proposal_slot: Slot,
18+
) -> Result<Withdrawals<T::EthSpec>, warp::Rejection> {
19+
get_next_withdrawals_sanity_checks(chain, &state, proposal_slot)?;
20+
21+
// advance the state to the epoch of the proposal slot.
22+
let proposal_epoch = proposal_slot.epoch(T::EthSpec::slots_per_epoch());
23+
let (state_root, _, _) = state_id.root(chain)?;
24+
if proposal_epoch != state.current_epoch() {
25+
if let Err(e) =
26+
partial_state_advance(&mut state, Some(state_root), proposal_slot, &chain.spec)
27+
{
28+
return Err(warp_utils::reject::custom_server_error(format!(
29+
"failed to advance to the epoch of the proposal slot: {:?}",
30+
e
31+
)));
32+
}
33+
}
34+
35+
match get_expected_withdrawals(&state, &chain.spec) {
36+
Ok(withdrawals) => Ok(withdrawals),
37+
Err(e) => Err(warp_utils::reject::custom_server_error(format!(
38+
"failed to get expected withdrawal: {:?}",
39+
e
40+
))),
41+
}
42+
}
43+
44+
fn get_next_withdrawals_sanity_checks<T: BeaconChainTypes>(
45+
chain: &BeaconChain<T>,
46+
state: &BeaconState<T::EthSpec>,
47+
proposal_slot: Slot,
48+
) -> Result<(), warp::Rejection> {
49+
if proposal_slot <= state.slot() {
50+
return Err(warp_utils::reject::custom_bad_request(
51+
"proposal slot must be greater than the pre-state slot".to_string(),
52+
));
53+
}
54+
55+
let fork = chain.spec.fork_name_at_slot::<T::EthSpec>(proposal_slot);
56+
if let ForkName::Base | ForkName::Altair | ForkName::Merge = fork {
57+
return Err(warp_utils::reject::custom_bad_request(
58+
"the specified state is a pre-capella state.".to_string(),
59+
));
60+
}
61+
62+
let look_ahead_limit = MAX_EPOCH_LOOKAHEAD
63+
.safe_mul(T::EthSpec::slots_per_epoch())
64+
.map_err(warp_utils::reject::arith_error)?;
65+
if proposal_slot >= state.slot() + look_ahead_limit {
66+
return Err(warp_utils::reject::custom_bad_request(format!(
67+
"proposal slot is greater than or equal to the look ahead limit: {look_ahead_limit}"
68+
)));
69+
}
70+
71+
Ok(())
72+
}

beacon_node/http_api/src/lib.rs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ mod attester_duties;
1010
mod block_id;
1111
mod block_packing_efficiency;
1212
mod block_rewards;
13+
mod builder_states;
1314
mod database;
1415
mod metrics;
1516
mod proposer_duties;
@@ -32,6 +33,7 @@ use beacon_chain::{
3233
};
3334
use beacon_processor::BeaconProcessorSend;
3435
pub use block_id::BlockId;
36+
use builder_states::get_next_withdrawals;
3537
use bytes::Bytes;
3638
use directory::DEFAULT_ROOT_DIR;
3739
use eth2::types::{
@@ -2291,6 +2293,60 @@ pub fn serve<T: BeaconChainTypes>(
22912293
},
22922294
);
22932295

2296+
/*
2297+
* builder/states
2298+
*/
2299+
2300+
let builder_states_path = eth_v1
2301+
.and(warp::path("builder"))
2302+
.and(warp::path("states"))
2303+
.and(chain_filter.clone());
2304+
2305+
// GET builder/states/{state_id}/expected_withdrawals
2306+
let get_expected_withdrawals = builder_states_path
2307+
.clone()
2308+
.and(task_spawner_filter.clone())
2309+
.and(warp::path::param::<StateId>())
2310+
.and(warp::path("expected_withdrawals"))
2311+
.and(warp::query::<api_types::ExpectedWithdrawalsQuery>())
2312+
.and(warp::path::end())
2313+
.and(warp::header::optional::<api_types::Accept>("accept"))
2314+
.then(
2315+
|chain: Arc<BeaconChain<T>>,
2316+
task_spawner: TaskSpawner<T::EthSpec>,
2317+
state_id: StateId,
2318+
query: api_types::ExpectedWithdrawalsQuery,
2319+
accept_header: Option<api_types::Accept>| {
2320+
task_spawner.blocking_response_task(Priority::P1, move || {
2321+
let (state, execution_optimistic, finalized) = state_id.state(&chain)?;
2322+
let proposal_slot = query.proposal_slot.unwrap_or(state.slot() + 1);
2323+
let withdrawals =
2324+
get_next_withdrawals::<T>(&chain, state, state_id, proposal_slot)?;
2325+
2326+
match accept_header {
2327+
Some(api_types::Accept::Ssz) => Response::builder()
2328+
.status(200)
2329+
.header("Content-Type", "application/octet-stream")
2330+
.body(withdrawals.as_ssz_bytes().into())
2331+
.map_err(|e| {
2332+
warp_utils::reject::custom_server_error(format!(
2333+
"failed to create response: {}",
2334+
e
2335+
))
2336+
}),
2337+
_ => Ok(warp::reply::json(
2338+
&api_types::ExecutionOptimisticFinalizedResponse {
2339+
data: withdrawals,
2340+
execution_optimistic: Some(execution_optimistic),
2341+
finalized: Some(finalized),
2342+
},
2343+
)
2344+
.into_response()),
2345+
}
2346+
})
2347+
},
2348+
);
2349+
22942350
/*
22952351
* beacon/rewards
22962352
*/
@@ -4503,6 +4559,7 @@ pub fn serve<T: BeaconChainTypes>(
45034559
.uor(get_lighthouse_block_packing_efficiency)
45044560
.uor(get_lighthouse_merge_readiness)
45054561
.uor(get_events)
4562+
.uor(get_expected_withdrawals)
45064563
.uor(lighthouse_log_events.boxed())
45074564
.recover(warp_utils::reject::handle_rejection),
45084565
)

beacon_node/http_api/tests/tests.rs

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ use sensitive_url::SensitiveUrl;
2828
use slot_clock::SlotClock;
2929
use state_processing::per_block_processing::get_expected_withdrawals;
3030
use state_processing::per_slot_processing;
31+
use state_processing::state_advance::partial_state_advance;
3132
use std::convert::TryInto;
3233
use std::sync::Arc;
3334
use tokio::time::Duration;
@@ -4341,6 +4342,72 @@ impl ApiTester {
43414342
self
43424343
}
43434344

4345+
pub async fn test_get_expected_withdrawals_invalid_state(self) -> Self {
4346+
let state_id = CoreStateId::Root(Hash256::zero());
4347+
4348+
let result = self.client.get_expected_withdrawals(&state_id).await;
4349+
4350+
match result {
4351+
Err(e) => {
4352+
assert_eq!(e.status().unwrap(), 404);
4353+
}
4354+
_ => panic!("query did not fail correctly"),
4355+
}
4356+
4357+
self
4358+
}
4359+
4360+
pub async fn test_get_expected_withdrawals_capella(self) -> Self {
4361+
let slot = self.chain.slot().unwrap();
4362+
let state_id = CoreStateId::Slot(slot);
4363+
4364+
// calculate the expected withdrawals
4365+
let (mut state, _, _) = StateId(state_id).state(&self.chain).unwrap();
4366+
let proposal_slot = state.slot() + 1;
4367+
let proposal_epoch = proposal_slot.epoch(E::slots_per_epoch());
4368+
let (state_root, _, _) = StateId(state_id).root(&self.chain).unwrap();
4369+
if proposal_epoch != state.current_epoch() {
4370+
let _ = partial_state_advance(
4371+
&mut state,
4372+
Some(state_root),
4373+
proposal_slot,
4374+
&self.chain.spec,
4375+
);
4376+
}
4377+
let expected_withdrawals = get_expected_withdrawals(&state, &self.chain.spec).unwrap();
4378+
4379+
// fetch expected withdrawals from the client
4380+
let result = self.client.get_expected_withdrawals(&state_id).await;
4381+
match result {
4382+
Ok(withdrawal_response) => {
4383+
assert_eq!(withdrawal_response.execution_optimistic, Some(false));
4384+
assert_eq!(withdrawal_response.finalized, Some(false));
4385+
assert_eq!(withdrawal_response.data, expected_withdrawals.to_vec());
4386+
}
4387+
Err(e) => {
4388+
println!("{:?}", e);
4389+
panic!("query failed incorrectly");
4390+
}
4391+
}
4392+
4393+
self
4394+
}
4395+
4396+
pub async fn test_get_expected_withdrawals_pre_capella(self) -> Self {
4397+
let state_id = CoreStateId::Head;
4398+
4399+
let result = self.client.get_expected_withdrawals(&state_id).await;
4400+
4401+
match result {
4402+
Err(e) => {
4403+
assert_eq!(e.status().unwrap(), 400);
4404+
}
4405+
_ => panic!("query did not fail correctly"),
4406+
}
4407+
4408+
self
4409+
}
4410+
43444411
pub async fn test_get_events_altair(self) -> Self {
43454412
let topics = vec![EventTopic::ContributionAndProof];
43464413
let mut events_future = self
@@ -5123,3 +5190,37 @@ async fn optimistic_responses() {
51235190
.test_check_optimistic_responses()
51245191
.await;
51255192
}
5193+
5194+
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
5195+
async fn expected_withdrawals_invalid_pre_capella() {
5196+
let mut config = ApiTesterConfig::default();
5197+
config.spec.altair_fork_epoch = Some(Epoch::new(0));
5198+
ApiTester::new_from_config(config)
5199+
.await
5200+
.test_get_expected_withdrawals_pre_capella()
5201+
.await;
5202+
}
5203+
5204+
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
5205+
async fn expected_withdrawals_invalid_state() {
5206+
let mut config = ApiTesterConfig::default();
5207+
config.spec.altair_fork_epoch = Some(Epoch::new(0));
5208+
config.spec.bellatrix_fork_epoch = Some(Epoch::new(0));
5209+
config.spec.capella_fork_epoch = Some(Epoch::new(0));
5210+
ApiTester::new_from_config(config)
5211+
.await
5212+
.test_get_expected_withdrawals_invalid_state()
5213+
.await;
5214+
}
5215+
5216+
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
5217+
async fn expected_withdrawals_valid_capella() {
5218+
let mut config = ApiTesterConfig::default();
5219+
config.spec.altair_fork_epoch = Some(Epoch::new(0));
5220+
config.spec.bellatrix_fork_epoch = Some(Epoch::new(0));
5221+
config.spec.capella_fork_epoch = Some(Epoch::new(0));
5222+
ApiTester::new_from_config(config)
5223+
.await
5224+
.test_get_expected_withdrawals_capella()
5225+
.await;
5226+
}

common/eth2/src/lib.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1261,6 +1261,23 @@ impl BeaconNodeHttpClient {
12611261
Ok(())
12621262
}
12631263

1264+
// GET builder/states/{state_id}/expected_withdrawals
1265+
pub async fn get_expected_withdrawals(
1266+
&self,
1267+
state_id: &StateId,
1268+
) -> Result<ExecutionOptimisticFinalizedResponse<Vec<Withdrawal>>, Error> {
1269+
let mut path = self.eth_path(V1)?;
1270+
1271+
path.path_segments_mut()
1272+
.map_err(|()| Error::InvalidUrl(self.server.clone()))?
1273+
.push("builder")
1274+
.push("states")
1275+
.push(&state_id.to_string())
1276+
.push("expected_withdrawals");
1277+
1278+
self.get(path).await
1279+
}
1280+
12641281
/// `POST validator/contribution_and_proofs`
12651282
pub async fn post_validator_contribution_and_proofs<T: EthSpec>(
12661283
&self,

common/eth2/src/types.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -581,6 +581,11 @@ pub struct SyncingData {
581581
pub sync_distance: Slot,
582582
}
583583

584+
#[derive(Serialize, Deserialize)]
585+
pub struct ExpectedWithdrawalsQuery {
586+
pub proposal_slot: Option<Slot>,
587+
}
588+
584589
#[derive(Clone, PartialEq, Debug, Deserialize)]
585590
#[serde(try_from = "String", bound = "T: FromStr")]
586591
pub struct QueryVec<T: FromStr> {

0 commit comments

Comments
 (0)