Skip to content
Merged
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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions beacon_node/http_api/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ sensitive_url = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
slot_clock = { workspace = true }
ssz_types = { workspace = true }
state_processing = { workspace = true }
store = { workspace = true }
sysinfo = { workspace = true }
Expand Down
85 changes: 85 additions & 0 deletions beacon_node/http_api/src/beacon/states.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use eth2::types::{
ValidatorIdentitiesRequestBody, ValidatorIndexData, ValidatorsRequestBody,
};
use ssz::Encode;
use ssz_types::FixedVector;
use std::sync::Arc;
use types::{
AttestationShufflingId, BeaconStateError, CommitteeCache, EthSpec, RelativeEpoch,
Expand Down Expand Up @@ -378,6 +379,90 @@ pub fn get_beacon_state_sync_committees<T: BeaconChainTypes>(
.boxed()
}

// `GET /eth/v1/beacon/states/{state_id}/ptc`
pub fn get_beacon_state_ptc<T: BeaconChainTypes>(
beacon_states_path: BeaconStatesPath<T>,
) -> ResponseFilter {
beacon_states_path
.clone()
.and(warp::path("ptc"))
.and(warp::query::<eth2::types::PtcQuery>())
.and(warp::path::end())
.and(warp::header::optional::<api_types::Accept>("accept"))
.then(
|state_id: StateId,
task_spawner: TaskSpawner<T::EthSpec>,
chain: Arc<BeaconChain<T>>,
query: eth2::types::PtcQuery,
accept_header: Option<api_types::Accept>| {
task_spawner.blocking_response_task(Priority::P1, move || {
let (data, execution_optimistic, finalized) = state_id
.map_state_and_execution_optimistic_and_finalized(
&chain,
|state, execution_optimistic, finalized| {
let slot = query.slot.unwrap_or(state.slot());
let ptc =
state.get_ptc(slot, &chain.spec).map_err(|e| match e {
BeaconStateError::IncorrectStateVariant => {
warp_utils::reject::custom_bad_request(format!(
"state at slot {} is pre-Gloas",
state.slot()
))
}
BeaconStateError::SlotOutOfBounds => {
warp_utils::reject::custom_bad_request(format!(
"slot {} is pre-Gloas or outside the PTC window \
of the state at epoch {}",
slot,
state.current_epoch()
))
}
e => warp_utils::reject::beacon_state_error(e),
})?;

let validators =
FixedVector::new(ptc.0.iter().map(|&i| i as u64).collect())
.map_err(|e| {
warp_utils::reject::custom_server_error(format!(
"invalid PTC size: {:?}",
e
))
})?;

Ok((
eth2::types::PtcData::<T::EthSpec> { slot, validators },
execution_optimistic,
finalized,
))
},
)?;

match accept_header {
Some(api_types::Accept::Ssz) => Builder::new()
.status(200)
.body(data.as_ssz_bytes())
.map(add_ssz_content_type_header)
.map_err(|e| {
warp_utils::reject::custom_server_error(format!(
"failed to create response: {}",
e
))
}),
_ => Ok(warp::reply::json(
&eth2::types::GenericResponse::from(data)
.add_execution_optimistic_finalized(
execution_optimistic,
finalized,
),
)
.into_response()),
}
})
},
)
.boxed()
}

// GET beacon/states/{state_id}/committees?slot,index,epoch
pub fn get_beacon_state_committees<T: BeaconChainTypes>(
beacon_states_path: BeaconStatesPath<T>,
Expand Down
4 changes: 4 additions & 0 deletions beacon_node/http_api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -668,6 +668,9 @@ pub async fn serve<T: BeaconChainTypes>(
let get_beacon_state_proposer_lookahead =
states::get_beacon_state_proposer_lookahead(beacon_states_path.clone());

// `GET /beacon/states/{state_id}/ptc`
let get_beacon_state_ptc = states::get_beacon_state_ptc(beacon_states_path.clone());

// GET beacon/headers
//
// Note: this endpoint only returns information about blocks in the canonical chain. Given that
Expand Down Expand Up @@ -3426,6 +3429,7 @@ pub async fn serve<T: BeaconChainTypes>(
.uor(get_beacon_state_pending_partial_withdrawals)
.uor(get_beacon_state_pending_consolidations)
.uor(get_beacon_state_proposer_lookahead)
.uor(get_beacon_state_ptc)
.uor(get_beacon_headers)
.uor(get_beacon_headers_block_id)
.uor(get_beacon_block)
Expand Down
96 changes: 96 additions & 0 deletions beacon_node/http_api/tests/fork_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,102 @@ async fn ptc_duties_across_fork() {
assert_eq!(ptc_duty_slots(&post_fork_response.data), expected_duties);
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn ptc_state_endpoint_bad_requests_across_fork() {
let validator_count = E::sync_committee_size();
let fork_epoch = Epoch::new(2);
let spec = gloas_spec(fork_epoch);
let tester = InteractiveTester::<E>::new(Some(spec.clone()), validator_count).await;
let harness = &tester.harness;
let client = &tester.client;

let all_validators = harness.get_all_validators();
let fork_slot = fork_epoch.start_slot(E::slots_per_epoch());

// Pre-Gloas head state: rejected for its own slot and for a post-fork slot.
let genesis_state = harness.get_current_state();
let (_, pre_fork_state) = harness
.add_attested_block_at_slot(fork_slot - 1, genesis_state, &all_validators)
.await
.unwrap();
assert_eq!(
client
.get_beacon_states_ptc::<E>(StateId::Head, None)
.await
.unwrap_err()
.status()
.unwrap(),
400
);
assert_eq!(
client
.get_beacon_states_ptc::<E>(StateId::Head, Some(fork_slot))
.await
.unwrap_err()
.status()
.unwrap(),
400
);

// Head state at the first Gloas slot: its PTC window starts in the pre-Gloas epoch.
let (_, mut state) = harness
.add_attested_block_at_slot(fork_slot, pre_fork_state, &all_validators)
.await
.unwrap();
assert!(
client
.get_beacon_states_ptc::<E>(StateId::Head, Some(fork_slot))
.await
.unwrap()
.is_some()
);
// Pre-Gloas slot within the window range of a Gloas state.
assert_eq!(
client
.get_beacon_states_ptc::<E>(StateId::Head, Some(fork_slot - 1))
.await
.unwrap_err()
.status()
.unwrap(),
400
);

// Advance the head to epoch `fork_epoch + 2` so that the fork epoch falls below the window.
let head_epoch = fork_epoch + 2;
for slot in (fork_slot.as_u64() + 1..=head_epoch.start_slot(E::slots_per_epoch()).as_u64())
.map(Slot::new)
{
(_, state) = harness
.add_attested_block_at_slot(slot, state, &all_validators)
.await
.unwrap();
}

// Post-Gloas slot below the window: epoch `current_epoch - 2`.
assert_eq!(
client
.get_beacon_states_ptc::<E>(StateId::Head, Some(fork_slot))
.await
.unwrap_err()
.status()
.unwrap(),
400
);

// Slot above the window: epoch `current_epoch + MIN_SEED_LOOKAHEAD + 1`.
let above_window_slot =
(head_epoch + spec.min_seed_lookahead + 1).start_slot(E::slots_per_epoch());
assert_eq!(
client
.get_beacon_states_ptc::<E>(StateId::Head, Some(above_window_slot))
.await
.unwrap_err()
.status()
.unwrap(),
400
);
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn attestations_across_fork_with_skip_slots() {
let validator_count = E::sync_committee_size();
Expand Down
143 changes: 143 additions & 0 deletions beacon_node/http_api/tests/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1807,6 +1807,131 @@ impl ApiTester {
self
}

pub async fn test_beacon_states_ptc(self) -> Self {
for state_id in self.interesting_state_ids() {
let state_opt = state_id
.state(&self.chain)
.ok()
.map(|(state, _execution_optimistic, _finalized)| state);

let result = self
.client
.get_beacon_states_ptc::<E>(state_id.0, None)
.await
.unwrap()
.map(|res| res.data);

if result.is_none() && state_opt.is_none() {
continue;
}

let state = state_opt.as_ref().expect("result should be none");

// Without a `slot` query the committee for the state's own slot is returned.
let expected_ptc = state.get_ptc(state.slot(), &self.chain.spec).unwrap();
let result = result.unwrap();
assert_eq!(result.slot, state.slot(), "{}", state_id);
assert_eq!(
result
.validators
.iter()
.map(|&i| i as usize)
.collect::<Vec<_>>(),
expected_ptc.0.to_vec(),
"{}",
state_id
);

// A slot in the next epoch is within the state's PTC window.
let next_epoch_slot = state.slot() + E::slots_per_epoch();
let result = self
.client
.get_beacon_states_ptc::<E>(state_id.0, Some(next_epoch_slot))
.await
.unwrap()
.unwrap()
.data;
let expected_ptc = state.get_ptc(next_epoch_slot, &self.chain.spec).unwrap();
assert_eq!(result.slot, next_epoch_slot, "{}", state_id);
assert_eq!(
result
.validators
.iter()
.map(|&i| i as usize)
.collect::<Vec<_>>(),
expected_ptc.0.to_vec(),
"{}",
state_id
);
}

self
}
Comment thread
hopinheimer marked this conversation as resolved.

pub async fn test_beacon_states_ptc_ssz(self) -> Self {
for state_id in self.interesting_state_ids() {
let state_opt = state_id
.state(&self.chain)
.ok()
.map(|(state, _execution_optimistic, _finalized)| state);

let result = match self
.client
.get_beacon_states_ptc_ssz(state_id.0, None)
.await
{
Ok(response) => response,
Err(e) => panic!("query failed incorrectly: {e:?}"),
};

if result.is_none() && state_opt.is_none() {
continue;
}

let state = state_opt.as_ref().expect("result should be none");

// Without a `slot` query the committee for the state's own slot is returned.
let expected_ptc = state.get_ptc(state.slot(), &self.chain.spec).unwrap();
let decoded =
PtcData::<E>::from_ssz_bytes(&result.unwrap()).expect("should decode SSZ PTC");
assert_eq!(decoded.slot, state.slot(), "{}", state_id);
assert_eq!(
decoded
.validators
.iter()
.map(|&i| i as usize)
.collect::<Vec<_>>(),
expected_ptc.0.to_vec(),
"{}",
state_id
);

// A slot in the next epoch is within the state's PTC window.
let next_epoch_slot = state.slot() + E::slots_per_epoch();
let ssz_bytes = self
.client
.get_beacon_states_ptc_ssz(state_id.0, Some(next_epoch_slot))
.await
.unwrap()
.expect("response should exist");
let decoded = PtcData::<E>::from_ssz_bytes(&ssz_bytes).expect("should decode SSZ PTC");
let expected_ptc = state.get_ptc(next_epoch_slot, &self.chain.spec).unwrap();
assert_eq!(decoded.slot, next_epoch_slot, "{}", state_id);
assert_eq!(
decoded
.validators
.iter()
.map(|&i| i as usize)
.collect::<Vec<_>>(),
expected_ptc.0.to_vec(),
"{}",
state_id
);
}

self
}

pub async fn test_beacon_headers_all_slots(self) -> Self {
for slot in 0..CHAIN_LENGTH {
let slot = Slot::from(slot);
Expand Down Expand Up @@ -9841,6 +9966,24 @@ async fn beacon_get_state_info_fulu() {
.await;
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn beacon_get_state_info_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_states_ptc()
.await
.test_beacon_states_ptc_ssz()
.await;
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn beacon_state_builders_gloas() {
let mut config = ApiTesterConfig::default();
Expand Down
Loading
Loading