Ensure PTC votes accurately reflect data availability (#9412)

Co-Authored-By: Eitan Seri-Levi <eserilev@ucsc.edu>
This commit is contained in:
Eitan Seri-Levi
2026-06-04 17:01:20 -07:00
committed by GitHub
parent eeae8514b1
commit da42d37456
3 changed files with 149 additions and 2 deletions

View File

@@ -2197,8 +2197,11 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
slot_start.is_some_and(|start| observed.saturating_sub(start) < payload_due) slot_start.is_some_and(|start| observed.saturating_sub(start) < payload_due)
}); });
// TODO(EIP-7732): Check blob data availability. For now, default to true. // A payload is only imported into fork choice if its data was available.
let blob_data_available = true; let blob_data_available = self
.canonical_head
.fork_choice_read_lock()
.is_payload_received(&beacon_block_root);
Ok(PayloadAttestationData { Ok(PayloadAttestationData {
beacon_block_root, beacon_block_root,

View File

@@ -8,6 +8,7 @@ use beacon_chain::test_utils::{
use beacon_chain::validator_monitor::UNAGGREGATED_ATTESTATION_LAG_SLOTS; use beacon_chain::validator_monitor::UNAGGREGATED_ATTESTATION_LAG_SLOTS;
use beacon_chain::{StateSkipConfig, WhenSlotSkipped, metrics}; use beacon_chain::{StateSkipConfig, WhenSlotSkipped, metrics};
use bls::{AggregateSignature, Keypair}; use bls::{AggregateSignature, Keypair};
use slot_clock::SlotClock;
use std::sync::{Arc, LazyLock}; use std::sync::{Arc, LazyLock};
use tree_hash::TreeHash; use tree_hash::TreeHash;
use types::{Attestation, EthSpec, MainnetEthSpec, RelativeEpoch, Slot}; use types::{Attestation, EthSpec, MainnetEthSpec, RelativeEpoch, Slot};
@@ -448,3 +449,69 @@ async fn gloas_attestation_index_payload_absent() {
"gloas attestation to prior slot without payload should have index=0 (payload_absent)" "gloas attestation to prior slot without payload should have index=0 (payload_absent)"
); );
} }
/// Verify that `produce_payload_attestation_data` reports `payload_present = true` but
/// `blob_data_available = false` when the envelope was observed on but not imported
/// because its data was unavailable.
///
/// Setup: build a chain through slot 2, then at slot 3 import only the beacon block (no
/// envelope) and mark the envelope as observed on time.
#[tokio::test]
async fn gloas_payload_attestation_seen_but_data_unavailable() {
if fork_name_from_env().is_some_and(|f| !f.gloas_enabled()) {
return;
}
let harness = BeaconChainHarness::builder(MainnetEthSpec)
.default_spec()
.keypairs(KEYPAIRS[..].to_vec())
.fresh_ephemeral_store()
.mock_execution_layer()
.build();
let chain = &harness.chain;
harness.advance_slot();
harness
.extend_chain(
2,
BlockStrategy::OnCanonicalHead,
AttestationStrategy::AllValidators,
)
.await;
// Slot 3: import the beacon block but withhold its envelope.
harness.advance_slot();
let state = harness.get_current_state();
let (block_contents, _envelope, _new_state) =
harness.make_block_with_envelope(state, Slot::new(3)).await;
let block_root = block_contents.0.canonical_root();
harness
.process_block(Slot::new(3), block_root, block_contents)
.await
.expect("block should import without envelope");
assert_eq!(chain.head_snapshot().beacon_block.slot(), Slot::new(3));
// Mark the envelope as observed at the start of the slot, before its deadline.
let slot_start = chain.slot_clock.start_of(Slot::new(3)).unwrap();
chain.envelope_times_cache.write().set_time_observed(
block_root,
Slot::new(3),
slot_start,
None,
);
let pa_data = chain
.produce_payload_attestation_data(Slot::new(3))
.expect("should produce payload attestation data");
assert!(
pa_data.payload_present,
"envelope observed before the deadline should vote payload_present=true"
);
assert!(
!pa_data.blob_data_available,
"unimported envelope data should vote blob_data_available=false"
);
}

View File

@@ -4970,6 +4970,10 @@ impl ApiTester {
"payload attestation should report payload_present=true after publishing \ "payload attestation should report payload_present=true after publishing \
the envelope via the HTTP API (slot {slot})" the envelope via the HTTP API (slot {slot})"
); );
assert!(
pa_data.blob_data_available,
"blob_data_available should be true once the envelope is imported (slot {slot})"
);
self.chain.slot_clock.set_slot(slot.as_u64() + 1); self.chain.slot_clock.set_slot(slot.as_u64() + 1);
} }
@@ -4977,6 +4981,71 @@ impl ApiTester {
self self
} }
/// When a payload hasn't been seen, the payload attestation data
/// must report `payload_present = false` and `blob_data_available = false`.
pub async fn test_payload_attestation_unavailable_without_envelope(self) -> Self {
if !self.chain.spec.is_gloas_scheduled() {
return self;
}
let fork = self.chain.canonical_head.cached_head().head_fork();
let genesis_validators_root = self.chain.genesis_validators_root;
for _ in 0..E::slots_per_epoch() * 3 {
let slot = self.chain.slot().unwrap();
let epoch = self.chain.epoch().unwrap();
let fork_name = self.chain.spec.fork_name_at_slot::<E>(slot);
if !fork_name.gloas_enabled() {
self.chain.slot_clock.set_slot(slot.as_u64() + 1);
continue;
}
let (sk, randao_reveal) = self
.proposer_setup(slot, epoch, &fork, genesis_validators_root)
.await;
// Produce and publish a block, but withhold its envelope.
let (response, _metadata) = self
.client
.get_validator_blocks_v4::<E>(slot, &randao_reveal, None, None, None, None)
.await
.unwrap();
let block = response.data;
let block_root = block.tree_hash_root();
let signed_block = block.sign(&sk, &fork, genesis_validators_root, &self.chain.spec);
let signed_block_request =
PublishBlockRequest::try_from(Arc::new(signed_block)).unwrap();
self.client
.post_beacon_blocks_v2(&signed_block_request, None)
.await
.unwrap();
let pa_data = self
.client
.get_validator_payload_attestation_data(slot)
.await
.unwrap()
.expect("expected payload attestation data for slot with block")
.into_data();
assert_eq!(pa_data.beacon_block_root, block_root);
assert!(
!pa_data.payload_present,
"payload_present should be false when the envelope is withheld (slot {slot})"
);
assert!(
!pa_data.blob_data_available,
"blob_data_available should be false when the envelope is not imported (slot {slot})"
);
return self;
}
self
}
pub async fn test_get_validator_payload_attestation_data_pre_gloas(self) -> Self { pub async fn test_get_validator_payload_attestation_data_pre_gloas(self) -> Self {
let slot = self.chain.slot().unwrap(); let slot = self.chain.slot().unwrap();
@@ -8703,6 +8772,14 @@ async fn payload_attestation_present_after_envelope_publish() {
.await; .await;
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn payload_attestation_unavailable_without_envelope() {
ApiTester::new_with_hard_forks()
.await
.test_payload_attestation_unavailable_without_envelope()
.await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn post_beacon_pool_payload_attestations_valid() { async fn post_beacon_pool_payload_attestations_valid() {
if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) {