Gloas ptc duties beacon node response (#8415)

Co-Authored-By: shane-moore <skm1790@gmail.com>

Co-Authored-By: Eitan Seri-Levi <eserilev@ucsc.edu>

Co-Authored-By: Eitan Seri-Levi <eserilev@gmail.com>

Co-Authored-By: dapplion <35266934+dapplion@users.noreply.github.com>
This commit is contained in:
Shane K Moore
2026-04-26 08:25:00 -07:00
committed by GitHub
parent 276c4d5ff3
commit fae7941b2d
6 changed files with 411 additions and 4 deletions

View File

@@ -19,6 +19,7 @@ mod metrics;
mod peer;
mod produce_block;
mod proposer_duties;
mod ptc_duties;
mod publish_attestations;
mod publish_blocks;
mod standard_block_rewards;
@@ -2560,6 +2561,14 @@ pub fn serve<T: BeaconChainTypes>(
task_spawner_filter.clone(),
);
// POST validator/duties/ptc/{epoch}
let post_validator_duties_ptc = post_validator_duties_ptc(
eth_v1.clone(),
chain_filter.clone(),
not_while_syncing_filter.clone(),
task_spawner_filter.clone(),
);
// POST validator/duties/sync/{epoch}
let post_validator_duties_sync = post_validator_duties_sync(
eth_v1.clone(),
@@ -3410,6 +3419,7 @@ pub fn serve<T: BeaconChainTypes>(
.uor(post_beacon_rewards_attestations)
.uor(post_beacon_rewards_sync_committee)
.uor(post_validator_duties_attester)
.uor(post_validator_duties_ptc)
.uor(post_validator_duties_sync)
.uor(post_validator_aggregate_and_proofs)
.uor(post_validator_contribution_and_proofs)

View File

@@ -0,0 +1,182 @@
//! Contains the handler for the `POST validator/duties/ptc/{epoch}` endpoint.
use crate::state_id::StateId;
use beacon_chain::{BeaconChain, BeaconChainError, BeaconChainTypes};
use eth2::types::{self as api_types, PtcDuty};
use slot_clock::SlotClock;
use state_processing::state_advance::partial_state_advance;
use types::{BeaconState, ChainSpec, Epoch, EthSpec, Hash256};
type ApiDuties = api_types::DutiesResponse<Vec<PtcDuty>>;
pub fn ptc_duties<T: BeaconChainTypes>(
request_epoch: Epoch,
request_indices: &[u64],
chain: &BeaconChain<T>,
) -> Result<ApiDuties, warp::reject::Rejection> {
let current_epoch = chain
.slot_clock
.now_or_genesis()
.map(|slot| slot.epoch(T::EthSpec::slots_per_epoch()))
.ok_or(BeaconChainError::UnableToReadSlot)
.map_err(warp_utils::reject::unhandled_error)?;
let tolerant_current_epoch = if chain.slot_clock.is_prior_to_genesis().unwrap_or(true) {
current_epoch
} else {
chain
.slot_clock
.now_with_future_tolerance(chain.spec.maximum_gossip_clock_disparity())
.ok_or_else(|| {
warp_utils::reject::custom_server_error("unable to read slot clock".into())
})?
.epoch(T::EthSpec::slots_per_epoch())
};
let is_within_clock_tolerance = request_epoch == current_epoch
|| request_epoch == current_epoch + 1
|| request_epoch == tolerant_current_epoch + 1;
if is_within_clock_tolerance {
let head_epoch = chain
.canonical_head
.cached_head()
.snapshot
.beacon_state
.current_epoch();
let head_can_serve_request = request_epoch == head_epoch || request_epoch == head_epoch + 1;
if head_can_serve_request {
compute_ptc_duties_from_cached_head(request_epoch, request_indices, chain)
} else {
compute_ptc_duties_from_state(request_epoch, request_indices, chain)
}
} else if request_epoch > current_epoch + 1 {
Err(warp_utils::reject::custom_bad_request(format!(
"request epoch {} is more than one epoch past the current epoch {}",
request_epoch, current_epoch
)))
} else {
compute_ptc_duties_from_state(request_epoch, request_indices, chain)
}
}
fn compute_ptc_duties_from_cached_head<T: BeaconChainTypes>(
request_epoch: Epoch,
request_indices: &[u64],
chain: &BeaconChain<T>,
) -> Result<ApiDuties, warp::reject::Rejection> {
let (cached_head, execution_status) = chain
.canonical_head
.head_and_execution_status()
.map_err(warp_utils::reject::unhandled_error)?;
let state = &cached_head.snapshot.beacon_state;
let head_block_root = cached_head.head_block_root();
let (duties, dependent_root) = chain
.compute_ptc_duties(state, request_epoch, request_indices, head_block_root)
.map_err(warp_utils::reject::unhandled_error)?;
convert_to_api_response(
duties,
dependent_root,
execution_status.is_optimistic_or_invalid(),
)
}
fn compute_ptc_duties_from_state<T: BeaconChainTypes>(
request_epoch: Epoch,
request_indices: &[u64],
chain: &BeaconChain<T>,
) -> Result<ApiDuties, warp::reject::Rejection> {
let state_opt = {
let (cached_head, execution_status) = chain
.canonical_head
.head_and_execution_status()
.map_err(warp_utils::reject::unhandled_error)?;
let head = &cached_head.snapshot;
if head.beacon_state.current_epoch() <= request_epoch {
Some((
head.beacon_state_root(),
head.beacon_state.clone(),
execution_status.is_optimistic_or_invalid(),
))
} else {
None
}
};
let (state, execution_optimistic) =
if let Some((state_root, mut state, execution_optimistic)) = state_opt {
ensure_state_knows_ptc_duties_for_epoch(
&mut state,
state_root,
request_epoch,
&chain.spec,
)?;
(state, execution_optimistic)
} else {
let (state, execution_optimistic, _finalized) =
StateId::from_slot(request_epoch.start_slot(T::EthSpec::slots_per_epoch()))
.state(chain)?;
(state, execution_optimistic)
};
if !(state.current_epoch() == request_epoch || state.current_epoch() + 1 == request_epoch) {
return Err(warp_utils::reject::custom_server_error(format!(
"state epoch {} not suitable for request epoch {}",
state.current_epoch(),
request_epoch
)));
}
let (duties, dependent_root) = chain
.compute_ptc_duties(
&state,
request_epoch,
request_indices,
chain.genesis_block_root,
)
.map_err(warp_utils::reject::unhandled_error)?;
convert_to_api_response(duties, dependent_root, execution_optimistic)
}
fn ensure_state_knows_ptc_duties_for_epoch<E: EthSpec>(
state: &mut BeaconState<E>,
state_root: Hash256,
target_epoch: Epoch,
spec: &ChainSpec,
) -> Result<(), warp::reject::Rejection> {
if state.current_epoch() > target_epoch {
return Err(warp_utils::reject::custom_server_error(format!(
"state epoch {} is later than target epoch {}",
state.current_epoch(),
target_epoch
)));
} else if state.current_epoch() + 1 < target_epoch {
let target_slot = target_epoch
.saturating_sub(1_u64)
.start_slot(E::slots_per_epoch());
partial_state_advance(state, Some(state_root), target_slot, spec)
.map_err(BeaconChainError::from)
.map_err(warp_utils::reject::unhandled_error)?;
}
Ok(())
}
fn convert_to_api_response(
duties: Vec<Option<PtcDuty>>,
dependent_root: Hash256,
execution_optimistic: bool,
) -> Result<ApiDuties, warp::reject::Rejection> {
Ok(api_types::DutiesResponse {
dependent_root,
execution_optimistic: Some(execution_optimistic),
data: duties.into_iter().flatten().collect(),
})
}

View File

@@ -7,7 +7,7 @@ use crate::utils::{
ResponseFilter, TaskSpawnerFilter, ValidatorSubscriptionTxFilter, publish_network_message,
};
use crate::version::{V1, V2, V3, unsupported_version_rejection};
use crate::{StateId, attester_duties, proposer_duties, sync_committees};
use crate::{StateId, attester_duties, proposer_duties, ptc_duties, sync_committees};
use beacon_chain::attestation_verification::VerifiedAttestation;
use beacon_chain::{AttestationError, BeaconChain, BeaconChainError, BeaconChainTypes};
use bls::PublicKeyBytes;
@@ -168,6 +168,42 @@ pub fn post_validator_duties_attester<T: BeaconChainTypes>(
.boxed()
}
// POST validator/duties/ptc/{epoch}
pub fn post_validator_duties_ptc<T: BeaconChainTypes>(
eth_v1: EthV1Filter,
chain_filter: ChainFilter<T>,
not_while_syncing_filter: NotWhileSyncingFilter,
task_spawner_filter: TaskSpawnerFilter<T>,
) -> ResponseFilter {
eth_v1
.and(warp::path("validator"))
.and(warp::path("duties"))
.and(warp::path("ptc"))
.and(warp::path::param::<Epoch>().or_else(|_| async {
Err(warp_utils::reject::custom_bad_request(
"Invalid epoch".to_string(),
))
}))
.and(warp::path::end())
.and(not_while_syncing_filter.clone())
.and(warp_utils::json::json())
.and(task_spawner_filter.clone())
.and(chain_filter.clone())
.then(
|epoch: Epoch,
not_synced_filter: Result<(), Rejection>,
indices: ValidatorIndexData,
task_spawner: TaskSpawner<T::EthSpec>,
chain: Arc<BeaconChain<T>>| {
task_spawner.blocking_json_task(Priority::P0, move || {
not_synced_filter?;
ptc_duties::ptc_duties(epoch, &indices.0, &chain)
})
},
)
.boxed()
}
// GET validator/aggregate_attestation?attestation_data_root,slot
pub fn get_validator_aggregate_attestation<T: BeaconChainTypes>(
any_version: AnyVersionFilter,