Single attestation "Full" implementation (#7444)

#6970


  This allows for us to receive `SingleAttestation` over gossip and process it without converting. There is still a conversion to `Attestation` as a final step in the attestation verification process, but by then the `SingleAttestation` is fully verified.

I've also fully removed the `submitPoolAttestationsV1` endpoint as its been deprecated

I've also pre-emptively deprecated supporting `Attestation` in `submitPoolAttestationsV2` endpoint. See here for more info: https://github.com/ethereum/beacon-APIs/pull/531

I tried to the minimize the diff here by only making the "required" changes. There are some unnecessary complexities with the way we manage the different attestation verification wrapper types. We could probably consolidate this to one wrapper type and refactor this even further. We could leave that to a separate PR if we feel like cleaning things up in the future.

Note that I've also updated the test harness to always submit `SingleAttestation` regardless of fork variant. I don't see a problem in that approach and it allows us to delete more code :)
This commit is contained in:
Eitan Seri-Levi
2025-06-17 12:01:26 +03:00
committed by GitHub
parent 3d2d65bf8d
commit 6786b9d12a
24 changed files with 777 additions and 981 deletions

View File

@@ -45,7 +45,6 @@ pub use block_id::BlockId;
use builder_states::get_next_withdrawals;
use bytes::Bytes;
use directory::DEFAULT_ROOT_DIR;
use either::Either;
use eth2::types::{
self as api_types, BroadcastValidation, ContextDeserialize, EndpointVersion, ForkChoice,
ForkChoiceNode, LightClientUpdatesQuery, PublishBlockRequest, StateId as CoreStateId,
@@ -64,7 +63,6 @@ pub use publish_blocks::{
publish_blinded_block, publish_block, reconstruct_block, ProvenancedBlock,
};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use slot_clock::SlotClock;
use ssz::Encode;
pub use state_id::StateId;
@@ -87,13 +85,13 @@ use tokio_stream::{
StreamExt,
};
use tracing::{debug, error, info, warn};
use types::AttestationData;
use types::{
Attestation, AttestationShufflingId, AttesterSlashing, BeaconStateError, ChainSpec, Checkpoint,
CommitteeCache, ConfigAndPreset, Epoch, EthSpec, ForkName, Hash256, ProposerPreparationData,
ProposerSlashing, RelativeEpoch, SignedAggregateAndProof, SignedBlindedBeaconBlock,
SignedBlsToExecutionChange, SignedContributionAndProof, SignedValidatorRegistrationData,
SignedVoluntaryExit, Slot, SyncCommitteeMessage, SyncContributionData,
Attestation, AttestationData, AttestationShufflingId, AttesterSlashing, BeaconStateError,
ChainSpec, Checkpoint, CommitteeCache, ConfigAndPreset, Epoch, EthSpec, ForkName, Hash256,
ProposerPreparationData, ProposerSlashing, RelativeEpoch, SignedAggregateAndProof,
SignedBlindedBeaconBlock, SignedBlsToExecutionChange, SignedContributionAndProof,
SignedValidatorRegistrationData, SignedVoluntaryExit, SingleAttestation, Slot,
SyncCommitteeMessage, SyncContributionData,
};
use validator::pubkey_to_validator_index;
use version::{
@@ -1981,68 +1979,21 @@ pub fn serve<T: BeaconChainTypes>(
.and(task_spawner_filter.clone())
.and(chain_filter.clone());
let post_beacon_pool_attestations_v1 = beacon_pool_path
.clone()
.and(warp::path("attestations"))
.and(warp::path::end())
.and(warp_utils::json::json())
.and(network_tx_filter.clone())
.and(reprocess_send_filter.clone())
.then(
|task_spawner: TaskSpawner<T::EthSpec>,
chain: Arc<BeaconChain<T>>,
attestations: Vec<Attestation<T::EthSpec>>,
network_tx: UnboundedSender<NetworkMessage<T::EthSpec>>,
reprocess_tx: Option<Sender<ReprocessQueueMessage>>| async move {
let attestations = attestations.into_iter().map(Either::Left).collect();
let result = crate::publish_attestations::publish_attestations(
task_spawner,
chain,
attestations,
network_tx,
reprocess_tx,
)
.await
.map(|()| warp::reply::json(&()));
convert_rejection(result).await
},
);
let post_beacon_pool_attestations_v2 = beacon_pool_path_v2
.clone()
.and(warp::path("attestations"))
.and(warp::path::end())
.and(warp_utils::json::json::<Value>())
.and(warp_utils::json::json::<Vec<SingleAttestation>>())
.and(optional_consensus_version_header_filter)
.and(network_tx_filter.clone())
.and(reprocess_send_filter.clone())
.then(
|task_spawner: TaskSpawner<T::EthSpec>,
chain: Arc<BeaconChain<T>>,
payload: Value,
fork_name: Option<ForkName>,
attestations: Vec<SingleAttestation>,
_fork_name: Option<ForkName>,
network_tx: UnboundedSender<NetworkMessage<T::EthSpec>>,
reprocess_tx: Option<Sender<ReprocessQueueMessage>>| async move {
let attestations =
match crate::publish_attestations::deserialize_attestation_payload::<T>(
payload, fork_name,
) {
Ok(attestations) => attestations,
Err(err) => {
warn!(
error = ?err,
"Unable to deserialize attestation POST request"
);
return warp::reply::with_status(
warp::reply::json(
&"Unable to deserialize request body".to_string(),
),
eth2::StatusCode::BAD_REQUEST,
)
.into_response();
}
};
let result = crate::publish_attestations::publish_attestations(
task_spawner,
chain,
@@ -5058,7 +5009,6 @@ pub fn serve<T: BeaconChainTypes>(
.uor(post_beacon_blinded_blocks)
.uor(post_beacon_blocks_v2)
.uor(post_beacon_blinded_blocks_v2)
.uor(post_beacon_pool_attestations_v1)
.uor(post_beacon_pool_attestations_v2)
.uor(post_beacon_pool_attester_slashings)
.uor(post_beacon_pool_proposer_slashings)

View File

@@ -36,16 +36,13 @@
//! attestations and there's no immediate cause for concern.
use crate::task_spawner::{Priority, TaskSpawner};
use beacon_chain::{
single_attestation::single_attestation_to_attestation, validator_monitor::timestamp_now,
AttestationError, BeaconChain, BeaconChainError, BeaconChainTypes,
validator_monitor::timestamp_now, AttestationError, BeaconChain, BeaconChainError,
BeaconChainTypes,
};
use beacon_processor::work_reprocessing_queue::{QueuedUnaggregate, ReprocessQueueMessage};
use either::Either;
use eth2::types::Failure;
use lighthouse_network::PubsubMessage;
use network::NetworkMessage;
use serde_json::Value;
use std::borrow::Cow;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::{
@@ -53,7 +50,7 @@ use tokio::sync::{
oneshot,
};
use tracing::{debug, error, warn};
use types::{Attestation, EthSpec, ForkName, SingleAttestation};
use types::SingleAttestation;
// Error variants are only used in `Debug` and considered `dead_code` by the compiler.
#[derive(Debug)]
@@ -65,8 +62,6 @@ pub enum Error {
ReprocessDisabled,
ReprocessFull,
ReprocessTimeout,
InvalidJson(#[allow(dead_code)] serde_json::Error),
FailedConversion(#[allow(dead_code)] Box<BeaconChainError>),
}
enum PublishAttestationResult {
@@ -76,66 +71,24 @@ enum PublishAttestationResult {
Failure(Error),
}
#[allow(clippy::type_complexity)]
pub fn deserialize_attestation_payload<T: BeaconChainTypes>(
payload: Value,
fork_name: Option<ForkName>,
) -> Result<Vec<Either<Attestation<T::EthSpec>, SingleAttestation>>, Error> {
if fork_name.is_some_and(|fork_name| fork_name.electra_enabled()) || fork_name.is_none() {
if fork_name.is_none() {
warn!("No Consensus Version header specified.");
}
Ok(serde_json::from_value::<Vec<SingleAttestation>>(payload)
.map_err(Error::InvalidJson)?
.into_iter()
.map(Either::Right)
.collect())
} else {
Ok(
serde_json::from_value::<Vec<Attestation<T::EthSpec>>>(payload)
.map_err(Error::InvalidJson)?
.into_iter()
.map(Either::Left)
.collect(),
)
}
}
fn verify_and_publish_attestation<T: BeaconChainTypes>(
chain: &Arc<BeaconChain<T>>,
either_attestation: &Either<Attestation<T::EthSpec>, SingleAttestation>,
attestation: &SingleAttestation,
seen_timestamp: Duration,
network_tx: &UnboundedSender<NetworkMessage<T::EthSpec>>,
) -> Result<(), Error> {
let attestation = convert_to_attestation(chain, either_attestation)?;
let verified_attestation = chain
.verify_unaggregated_attestation_for_gossip(&attestation, None)
.verify_unaggregated_attestation_for_gossip(attestation, None)
.map_err(Error::Validation)?;
match either_attestation {
Either::Left(attestation) => {
// Publish.
network_tx
.send(NetworkMessage::Publish {
messages: vec![PubsubMessage::Attestation(Box::new((
verified_attestation.subnet_id(),
attestation.clone(),
)))],
})
.map_err(|_| Error::Publication)?;
}
Either::Right(single_attestation) => {
network_tx
.send(NetworkMessage::Publish {
messages: vec![PubsubMessage::SingleAttestation(Box::new((
verified_attestation.subnet_id(),
single_attestation.clone(),
)))],
})
.map_err(|_| Error::Publication)?;
}
}
network_tx
.send(NetworkMessage::Publish {
messages: vec![PubsubMessage::Attestation(Box::new((
verified_attestation.subnet_id(),
attestation.clone(),
)))],
})
.map_err(|_| Error::Publication)?;
// Notify the validator monitor.
chain
@@ -172,57 +125,10 @@ fn verify_and_publish_attestation<T: BeaconChainTypes>(
}
}
fn convert_to_attestation<'a, T: BeaconChainTypes>(
chain: &Arc<BeaconChain<T>>,
attestation: &'a Either<Attestation<T::EthSpec>, SingleAttestation>,
) -> Result<Cow<'a, Attestation<T::EthSpec>>, Error> {
match attestation {
Either::Left(a) => Ok(Cow::Borrowed(a)),
Either::Right(single_attestation) => {
let conversion_result = chain.with_committee_cache(
single_attestation.data.target.root,
single_attestation
.data
.slot
.epoch(T::EthSpec::slots_per_epoch()),
|committee_cache, _| {
let Some(committee) = committee_cache.get_beacon_committee(
single_attestation.data.slot,
single_attestation.committee_index,
) else {
return Ok(Err(AttestationError::NoCommitteeForSlotAndIndex {
slot: single_attestation.data.slot,
index: single_attestation.committee_index,
}));
};
Ok(single_attestation_to_attestation::<T::EthSpec>(
single_attestation,
committee.committee,
)
.map(Cow::Owned))
},
);
match conversion_result {
Ok(Ok(attestation)) => Ok(attestation),
Ok(Err(e)) => Err(Error::Validation(e)),
// Map the error returned by `with_committee_cache` for unknown blocks into the
// `UnknownHeadBlock` error that is gracefully handled.
Err(BeaconChainError::MissingBeaconBlock(beacon_block_root)) => {
Err(Error::Validation(AttestationError::UnknownHeadBlock {
beacon_block_root,
}))
}
Err(e) => Err(Error::FailedConversion(Box::new(e))),
}
}
}
}
pub async fn publish_attestations<T: BeaconChainTypes>(
task_spawner: TaskSpawner<T::EthSpec>,
chain: Arc<BeaconChain<T>>,
attestations: Vec<Either<Attestation<T::EthSpec>, SingleAttestation>>,
attestations: Vec<SingleAttestation>,
network_tx: UnboundedSender<NetworkMessage<T::EthSpec>>,
reprocess_send: Option<Sender<ReprocessQueueMessage>>,
) -> Result<(), warp::Rejection> {
@@ -230,10 +136,7 @@ pub async fn publish_attestations<T: BeaconChainTypes>(
// move the `attestations` vec into the blocking task, so this small overhead is unavoidable.
let attestation_metadata = attestations
.iter()
.map(|att| match att {
Either::Left(att) => (att.data().slot, att.committee_index()),
Either::Right(att) => (att.data.slot, Some(att.committee_index)),
})
.map(|att| (att.data.slot, Some(att.committee_index)))
.collect::<Vec<_>>();
// Gossip validate and publish attestations that can be immediately processed.