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
39 changes: 39 additions & 0 deletions beacon_node/beacon_chain/src/beacon_chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2064,6 +2064,45 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
)?)
}

/// Produce a `PayloadAttestationData` for a PTC validator to sign.
///
/// This is used by PTC (Payload Timeliness Committee) validators to attest to the
/// presence/absence of an execution payload and blobs for a given slot.
pub fn produce_payload_attestation_data(
&self,
request_slot: Slot,
) -> Result<PayloadAttestationData, Error> {
let _timer = metrics::start_timer(&metrics::PAYLOAD_ATTESTATION_PRODUCTION_SECONDS);

// Payload attestations are only valid for the current slot
let current_slot = self.slot()?;
if request_slot != current_slot {
return Err(Error::InvalidSlot(request_slot));
}

// Check if we've seen a block for this slot from the canonical head
let head = self.head_snapshot();
if head.beacon_block.slot() != request_slot {
return Err(Error::NoBlockForSlot(request_slot));
}

let beacon_block_root = head.beacon_block_root;

// TODO(EIP-7732): Check if we've seen a SignedExecutionPayloadEnvelope
// referencing this block root. For now, default to false.
let payload_present = false;

// TODO(EIP-7732): Check blob data availability. For now, default to false.
let blob_data_available = false;

Ok(PayloadAttestationData {
beacon_block_root,
slot: head.beacon_block.slot(),
payload_present,
blob_data_available,
})
}

/// Performs the same validation as `Self::verify_unaggregated_attestation_for_gossip`, but for
/// multiple attestations using batch BLS verification. Batch verification can provide
/// significant CPU-time savings compared to individual verification.
Expand Down
1 change: 1 addition & 0 deletions beacon_node/beacon_chain/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ pub enum BeaconChainError {
},
SlotClockDidNotStart,
NoStateForSlot(Slot),
NoBlockForSlot(Slot),
BeaconStateError(BeaconStateError),
EpochCacheError(EpochCacheError),
DBInconsistent(String),
Expand Down
11 changes: 11 additions & 0 deletions beacon_node/beacon_chain/src/metrics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,17 @@ pub static ATTESTATION_PRODUCTION_CACHE_PRIME_SECONDS: LazyLock<Result<Histogram
)
});

/*
* Payload Attestation Production
*/
pub static PAYLOAD_ATTESTATION_PRODUCTION_SECONDS: LazyLock<Result<Histogram>> =
LazyLock::new(|| {
try_create_histogram(
"beacon_payload_attestation_production_seconds",
"Full runtime of payload attestation production",
)
});

/*
* Fork Choice
*/
Expand Down
40 changes: 40 additions & 0 deletions beacon_node/http_api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3517,6 +3517,45 @@ pub fn serve<T: BeaconChainTypes>(
},
);

// GET validator/payload_attestation_data/{slot}
let get_validator_payload_attestation_data = eth_v1
.and(warp::path("validator"))
.and(warp::path("payload_attestation_data"))
.and(warp::path::param::<Slot>().or_else(|_| async {
Err(warp_utils::reject::custom_bad_request(
"Invalid slot".to_string(),
))
}))
.and(warp::path::end())
.and(not_while_syncing_filter.clone())
.and(task_spawner_filter.clone())
.and(chain_filter.clone())
.then(
|slot: Slot,
not_synced_filter: Result<(), Rejection>,
task_spawner: TaskSpawner<T::EthSpec>,
chain: Arc<BeaconChain<T>>| {
task_spawner.blocking_response_task(Priority::P0, move || {
not_synced_filter?;

let fork_name = chain.spec.fork_name_at_slot::<T::EthSpec>(slot);

let payload_attestation_data = chain
.produce_payload_attestation_data(slot)
.map_err(warp_utils::reject::unhandled_error)?;

Ok(add_consensus_version_header(
warp::reply::json(&beacon_response(
ResponseIncludesVersion::Yes(fork_name),
payload_attestation_data,
))
.into_response(),
fork_name,
))
})
},
);

// GET validator/aggregate_attestation?attestation_data_root,slot
let get_validator_aggregate_attestation = any_version
.and(warp::path("validator"))
Expand Down Expand Up @@ -4948,6 +4987,7 @@ pub fn serve<T: BeaconChainTypes>(
.uor(get_validator_blocks)
.uor(get_validator_blinded_blocks)
.uor(get_validator_attestation_data)
.uor(get_validator_payload_attestation_data)
.uor(get_validator_aggregate_attestation)
.uor(get_validator_sync_committee_contribution)
.uor(get_lighthouse_health)
Expand Down
69 changes: 69 additions & 0 deletions beacon_node/http_api/tests/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4060,6 +4060,54 @@ impl ApiTester {
self
}

pub async fn test_get_validator_payload_attestation_data(self) -> Self {
let slot = self.chain.slot().unwrap();
let fork_name = self.chain.spec.fork_name_at_slot::<E>(slot);

// Payload attestation data is only available in Gloas fork.
if !fork_name.gloas_enabled() {
return self;
}

let result = self
.client
.get_validator_payload_attestation_data(slot)
.await
.unwrap()
.into_data();

let expected = self.chain.produce_payload_attestation_data(slot).unwrap();

assert_eq!(result.beacon_block_root, expected.beacon_block_root);
assert_eq!(result.slot, expected.slot);
assert_eq!(result.payload_present, expected.payload_present);
assert_eq!(result.blob_data_available, expected.blob_data_available);

self
}

pub async fn test_get_validator_payload_attestation_data_pre_gloas(self) -> Self {
let slot = self.chain.slot().unwrap();
let fork_name = self.chain.spec.fork_name_at_slot::<E>(slot);

// This test is for pre-Gloas forks
if fork_name.gloas_enabled() {
return self;
}

// The endpoint should return a 400 error for pre-Gloas forks
match self
.client
.get_validator_payload_attestation_data(slot)
.await
{
Ok(result) => panic!("query for pre-Gloas slot should fail, got: {result:?}"),
Err(e) => assert_eq!(e.status().unwrap(), 400),
}

self
}

#[allow(clippy::await_holding_lock)] // This is a test, so it should be fine.
pub async fn test_get_validator_aggregate_attestation_v1(self) -> Self {
let attestation = self
Expand Down Expand Up @@ -7464,6 +7512,27 @@ async fn get_validator_attestation_data_with_skip_slots() {
.await;
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn get_validator_payload_attestation_data() {
// TODO(EIP-7732): Remove this conditional once gloas block production is implemented
if fork_name_from_env().map_or(false, |f| f.gloas_enabled()) {
return;
}

ApiTester::new_with_hard_forks()
.await
.test_get_validator_payload_attestation_data()
.await;
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn get_validator_payload_attestation_data_pre_gloas() {
ApiTester::new()
.await
.test_get_validator_payload_attestation_data_pre_gloas()
.await;
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn get_validator_aggregate_attestation_v1() {
ApiTester::new()
Expand Down
25 changes: 24 additions & 1 deletion common/eth2/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ pub mod types;
pub use self::error::{Error, ok_or_error, success_or_error};
use self::mixin::{RequestAccept, ResponseOptional};
use self::types::*;
use ::types::beacon_response::ExecutionOptimisticFinalizedBeaconResponse;
use ::types::PayloadAttestationData;
use ::types::beacon_response::{ExecutionOptimisticFinalizedBeaconResponse, ForkVersionedResponse};
use educe::Educe;
use futures::Stream;
use futures_util::StreamExt;
Expand Down Expand Up @@ -66,6 +67,7 @@ const HTTP_GET_BEACON_BLOCK_SSZ_TIMEOUT_QUOTIENT: u32 = 4;
const HTTP_GET_DEBUG_BEACON_STATE_QUOTIENT: u32 = 4;
const HTTP_GET_DEPOSIT_SNAPSHOT_QUOTIENT: u32 = 4;
const HTTP_GET_VALIDATOR_BLOCK_TIMEOUT_QUOTIENT: u32 = 4;
const HTTP_PAYLOAD_ATTESTATION_TIMEOUT_QUOTIENT: u32 = 4;
const HTTP_DEFAULT_TIMEOUT_QUOTIENT: u32 = 4;

/// A struct to define a variety of different timeouts for different validator tasks to ensure
Expand All @@ -86,6 +88,7 @@ pub struct Timeouts {
pub get_debug_beacon_states: Duration,
pub get_deposit_snapshot: Duration,
pub get_validator_block: Duration,
pub payload_attestation: Duration,
pub default: Duration,
}

Expand All @@ -106,6 +109,7 @@ impl Timeouts {
get_debug_beacon_states: timeout,
get_deposit_snapshot: timeout,
get_validator_block: timeout,
payload_attestation: timeout,
default: timeout,
}
}
Expand All @@ -128,6 +132,7 @@ impl Timeouts {
get_debug_beacon_states: base_timeout / HTTP_GET_DEBUG_BEACON_STATE_QUOTIENT,
get_deposit_snapshot: base_timeout / HTTP_GET_DEPOSIT_SNAPSHOT_QUOTIENT,
get_validator_block: base_timeout / HTTP_GET_VALIDATOR_BLOCK_TIMEOUT_QUOTIENT,
payload_attestation: base_timeout / HTTP_PAYLOAD_ATTESTATION_TIMEOUT_QUOTIENT,
default: base_timeout / HTTP_DEFAULT_TIMEOUT_QUOTIENT,
}
}
Expand Down Expand Up @@ -2528,6 +2533,24 @@ impl BeaconNodeHttpClient {
self.get_with_timeout(path, self.timeouts.attestation).await
}

/// `GET validator/payload_attestation_data/{slot}`
pub async fn get_validator_payload_attestation_data(
&self,
slot: Slot,
) -> Result<BeaconResponse<PayloadAttestationData>, Error> {
let mut path = self.eth_path(V1)?;

path.path_segments_mut()
.map_err(|()| Error::InvalidUrl(self.server.clone()))?
.push("validator")
.push("payload_attestation_data")
.push(&slot.to_string());

self.get_with_timeout(path, self.timeouts.payload_attestation)
.await
.map(BeaconResponse::ForkVersioned)
}

/// `GET v1/validator/aggregate_attestation?slot,attestation_data_root`
pub async fn get_validator_aggregate_attestation_v1<E: EthSpec>(
&self,
Expand Down