Gossip recently computed light client data (#7023)

This commit is contained in:
Eitan Seri-Levi
2025-07-08 10:07:10 +03:00
committed by GitHub
parent 56485cc986
commit bd8a2a8ffb
9 changed files with 307 additions and 21 deletions

View File

@@ -3,7 +3,7 @@ use derivative::Derivative;
use slot_clock::SlotClock;
use std::time::Duration;
use strum::AsRefStr;
use types::LightClientFinalityUpdate;
use types::{Hash256, LightClientFinalityUpdate, Slot};
/// Returned when a light client finality update was not successfully verified. It might not have been verified for
/// two reasons:
@@ -21,12 +21,37 @@ pub enum Error {
///
/// Assuming the local clock is correct, the peer has sent an invalid message.
TooEarly,
/// Light client finality update message does not match the locally constructed one.
InvalidLightClientFinalityUpdate,
/// Light client finalized update message does not match the locally constructed one, it has a
/// different signature slot.
MismatchedSignatureSlot { local: Slot, observed: Slot },
/// Light client finalized update message does not match the locally constructed one, it has a
/// different finalized block header for the same signature slot.
MismatchedFinalizedHeader {
local_finalized_header_root: Hash256,
observed_finalized_header_root: Hash256,
signature_slot: Slot,
},
/// Light client finalized update message does not match the locally constructed one, it has a
/// different attested block header for the same signature slot and finalized header.
MismatchedAttestedHeader {
local_attested_header_root: Hash256,
observed_attested_header_root: Hash256,
finalized_header_root: Hash256,
signature_slot: Slot,
},
/// Light client finalized update message does not match the locally constructed one, it has a
/// different proof or sync aggregate for the same slot, attested header and finalized header.
MismatchedProofOrSyncAggregate {
attested_header_root: Hash256,
finalized_header_root: Hash256,
signature_slot: Slot,
},
/// Signature slot start time is none.
SigSlotStartIsNone,
/// Failed to construct a LightClientFinalityUpdate from state.
FailedConstructingUpdate,
/// Silently ignore this light client finality update
Ignore,
}
/// Wraps a `LightClientFinalityUpdate` that has been verified for propagation on the gossip network.
@@ -48,7 +73,7 @@ impl<T: BeaconChainTypes> VerifiedLightClientFinalityUpdate<T> {
// verify that enough time has passed for the block to have been propagated
let start_time = chain
.slot_clock
.start_of(*rcv_finality_update.signature_slot())
.start_of(rcv_finality_update.signature_slot())
.ok_or(Error::SigSlotStartIsNone)?;
let one_third_slot_duration = Duration::new(chain.spec.seconds_per_slot / 3, 0);
if seen_timestamp + chain.spec.maximum_gossip_clock_disparity()
@@ -57,16 +82,76 @@ impl<T: BeaconChainTypes> VerifiedLightClientFinalityUpdate<T> {
return Err(Error::TooEarly);
}
if let Some(latest_broadcasted_finality_update) = chain
.light_client_server_cache
.get_latest_broadcasted_finality_update()
{
// Ignore the incoming finality update if we've already broadcasted it
if latest_broadcasted_finality_update == rcv_finality_update {
return Err(Error::Ignore);
}
// Ignore the incoming finality update if the latest broadcasted attested header slot
// is greater than the incoming attested header slot.
if latest_broadcasted_finality_update.get_attested_header_slot()
> rcv_finality_update.get_attested_header_slot()
{
return Err(Error::Ignore);
}
}
let latest_finality_update = chain
.light_client_server_cache
.get_latest_finality_update()
.ok_or(Error::FailedConstructingUpdate)?;
// verify that the gossiped finality update is the same as the locally constructed one.
if latest_finality_update != rcv_finality_update {
return Err(Error::InvalidLightClientFinalityUpdate);
// Ignore the incoming finality update if the latest constructed attested header slot
// is greater than the incoming attested header slot.
if latest_finality_update.get_attested_header_slot()
> rcv_finality_update.get_attested_header_slot()
{
return Err(Error::Ignore);
}
// Verify that the gossiped finality update is the same as the locally constructed one.
if latest_finality_update != rcv_finality_update {
let signature_slot = latest_finality_update.signature_slot();
if signature_slot != rcv_finality_update.signature_slot() {
return Err(Error::MismatchedSignatureSlot {
local: signature_slot,
observed: rcv_finality_update.signature_slot(),
});
}
let local_finalized_header_root = latest_finality_update.get_finalized_header_root();
let observed_finalized_header_root = rcv_finality_update.get_finalized_header_root();
if local_finalized_header_root != observed_finalized_header_root {
return Err(Error::MismatchedFinalizedHeader {
local_finalized_header_root,
observed_finalized_header_root,
signature_slot,
});
}
let local_attested_header_root = latest_finality_update.get_attested_header_root();
let observed_attested_header_root = rcv_finality_update.get_attested_header_root();
if local_attested_header_root != observed_attested_header_root {
return Err(Error::MismatchedAttestedHeader {
local_attested_header_root,
observed_attested_header_root,
finalized_header_root: local_finalized_header_root,
signature_slot,
});
}
return Err(Error::MismatchedProofOrSyncAggregate {
attested_header_root: local_attested_header_root,
finalized_header_root: local_finalized_header_root,
signature_slot,
});
}
chain
.light_client_server_cache
.set_latest_broadcasted_finality_update(rcv_finality_update.clone());
Ok(Self {
light_client_finality_update: rcv_finality_update,
seen_timestamp,

View File

@@ -4,7 +4,7 @@ use eth2::types::Hash256;
use slot_clock::SlotClock;
use std::time::Duration;
use strum::AsRefStr;
use types::LightClientOptimisticUpdate;
use types::{LightClientOptimisticUpdate, Slot};
/// Returned when a light client optimistic update was not successfully verified. It might not have been verified for
/// two reasons:
@@ -22,14 +22,30 @@ pub enum Error {
///
/// Assuming the local clock is correct, the peer has sent an invalid message.
TooEarly,
/// Light client optimistic update message does not match the locally constructed one.
InvalidLightClientOptimisticUpdate,
/// Light client optimistic update message does not match the locally constructed one, it has a
/// different signature slot.
MismatchedSignatureSlot { local: Slot, observed: Slot },
/// Light client optimistic update message does not match the locally constructed one, it has a
/// different block header at the same slot.
MismatchedAttestedHeader {
local_attested_header_root: Hash256,
observed_attested_header_root: Hash256,
signature_slot: Slot,
},
/// Light client optimistic update message does not match the locally constructed one, it has a
/// different sync aggregate for the same slot and attested header.
MismatchedSyncAggregate {
attested_header_root: Hash256,
signature_slot: Slot,
},
/// Signature slot start time is none.
SigSlotStartIsNone,
/// Failed to construct a LightClientOptimisticUpdate from state.
FailedConstructingUpdate,
/// Unknown block with parent root.
UnknownBlockParentRoot(Hash256),
/// Silently ignore this light client optimistic update
Ignore,
}
/// Wraps a `LightClientOptimisticUpdate` that has been verified for propagation on the gossip network.
@@ -52,7 +68,7 @@ impl<T: BeaconChainTypes> VerifiedLightClientOptimisticUpdate<T> {
// verify that enough time has passed for the block to have been propagated
let start_time = chain
.slot_clock
.start_of(*rcv_optimistic_update.signature_slot())
.start_of(rcv_optimistic_update.signature_slot())
.ok_or(Error::SigSlotStartIsNone)?;
let one_third_slot_duration = Duration::new(chain.spec.seconds_per_slot / 3, 0);
if seen_timestamp + chain.spec.maximum_gossip_clock_disparity()
@@ -61,6 +77,22 @@ impl<T: BeaconChainTypes> VerifiedLightClientOptimisticUpdate<T> {
return Err(Error::TooEarly);
}
if let Some(latest_broadcasted_optimistic_update) = chain
.light_client_server_cache
.get_latest_broadcasted_optimistic_update()
{
// Ignore the incoming optimistic update if we've already broadcasted it
if latest_broadcasted_optimistic_update == rcv_optimistic_update {
return Err(Error::Ignore);
}
// Ignore the incoming optimistic update if the latest broadcasted slot
// is greater than the incoming slot.
if latest_broadcasted_optimistic_update.get_slot() > rcv_optimistic_update.get_slot() {
return Err(Error::Ignore);
}
}
let head = chain.canonical_head.cached_head();
let head_block = &head.snapshot.beacon_block;
// check if we can process the optimistic update immediately
@@ -76,11 +108,40 @@ impl<T: BeaconChainTypes> VerifiedLightClientOptimisticUpdate<T> {
.get_latest_optimistic_update()
.ok_or(Error::FailedConstructingUpdate)?;
// verify that the gossiped optimistic update is the same as the locally constructed one.
if latest_optimistic_update != rcv_optimistic_update {
return Err(Error::InvalidLightClientOptimisticUpdate);
// Ignore the incoming optimistic update if the latest constructed slot
// is greater than the incoming slot.
if latest_optimistic_update.get_slot() > rcv_optimistic_update.get_slot() {
return Err(Error::Ignore);
}
// Verify that the gossiped optimistic update is the same as the locally constructed one.
if latest_optimistic_update != rcv_optimistic_update {
let signature_slot = latest_optimistic_update.signature_slot();
if signature_slot != rcv_optimistic_update.signature_slot() {
return Err(Error::MismatchedSignatureSlot {
local: signature_slot,
observed: rcv_optimistic_update.signature_slot(),
});
}
let local_attested_header_root = latest_optimistic_update.get_canonical_root();
let observed_attested_header_root = rcv_optimistic_update.get_canonical_root();
if local_attested_header_root != observed_attested_header_root {
return Err(Error::MismatchedAttestedHeader {
local_attested_header_root,
observed_attested_header_root,
signature_slot,
});
}
return Err(Error::MismatchedSyncAggregate {
attested_header_root: local_attested_header_root,
signature_slot,
});
}
chain
.light_client_server_cache
.set_latest_broadcasted_optimistic_update(rcv_optimistic_update.clone());
let parent_root = rcv_optimistic_update.get_parent_root();
Ok(Self {
light_client_optimistic_update: rcv_optimistic_update,

View File

@@ -40,6 +40,10 @@ pub struct LightClientServerCache<T: BeaconChainTypes> {
latest_written_current_sync_committee: RwLock<Option<Arc<SyncCommittee<T::EthSpec>>>>,
/// Caches state proofs by block root
prev_block_cache: Mutex<lru::LruCache<Hash256, LightClientCachedData<T::EthSpec>>>,
/// Tracks the latest broadcasted finality update
latest_broadcasted_finality_update: RwLock<Option<LightClientFinalityUpdate<T::EthSpec>>>,
/// Tracks the latest broadcasted optimistic update
latest_broadcasted_optimistic_update: RwLock<Option<LightClientOptimisticUpdate<T::EthSpec>>>,
}
impl<T: BeaconChainTypes> LightClientServerCache<T> {
@@ -49,6 +53,8 @@ impl<T: BeaconChainTypes> LightClientServerCache<T> {
latest_optimistic_update: None.into(),
latest_light_client_update: None.into(),
latest_written_current_sync_committee: None.into(),
latest_broadcasted_finality_update: None.into(),
latest_broadcasted_optimistic_update: None.into(),
prev_block_cache: lru::LruCache::new(PREV_BLOCK_CACHE_SIZE).into(),
}
}
@@ -334,10 +340,89 @@ impl<T: BeaconChainTypes> LightClientServerCache<T> {
Ok(new_value)
}
/// Checks if we've already broadcasted the latest finality update.
/// If we haven't, update the `latest_broadcasted_finality_update` cache
/// and return the latest finality update for broadcasting, else return `None`.
pub fn should_broadcast_latest_finality_update(
&self,
) -> Option<LightClientFinalityUpdate<T::EthSpec>> {
if let Some(latest_finality_update) = self.get_latest_finality_update() {
let latest_broadcasted_finality_update = self.get_latest_broadcasted_finality_update();
match latest_broadcasted_finality_update {
Some(latest_broadcasted_finality_update) => {
if latest_broadcasted_finality_update != latest_finality_update {
self.set_latest_broadcasted_finality_update(latest_finality_update.clone());
return Some(latest_finality_update);
}
}
None => {
self.set_latest_broadcasted_finality_update(latest_finality_update.clone());
return Some(latest_finality_update);
}
}
}
None
}
pub fn get_latest_finality_update(&self) -> Option<LightClientFinalityUpdate<T::EthSpec>> {
self.latest_finality_update.read().clone()
}
pub fn get_latest_broadcasted_optimistic_update(
&self,
) -> Option<LightClientOptimisticUpdate<T::EthSpec>> {
self.latest_broadcasted_optimistic_update.read().clone()
}
pub fn get_latest_broadcasted_finality_update(
&self,
) -> Option<LightClientFinalityUpdate<T::EthSpec>> {
self.latest_broadcasted_finality_update.read().clone()
}
pub fn set_latest_broadcasted_optimistic_update(
&self,
optimistic_update: LightClientOptimisticUpdate<T::EthSpec>,
) {
*self.latest_broadcasted_optimistic_update.write() = Some(optimistic_update.clone());
}
pub fn set_latest_broadcasted_finality_update(
&self,
finality_update: LightClientFinalityUpdate<T::EthSpec>,
) {
*self.latest_broadcasted_finality_update.write() = Some(finality_update.clone());
}
/// Checks if we've already broadcasted the latest optimistic update.
/// If we haven't, update the `latest_broadcasted_optimistic_update` cache
/// and return the latest optimistic update for broadcasting, else return `None`.
pub fn should_broadcast_latest_optimistic_update(
&self,
) -> Option<LightClientOptimisticUpdate<T::EthSpec>> {
if let Some(latest_optimistic_update) = self.get_latest_optimistic_update() {
let latest_broadcasted_optimistic_update =
self.get_latest_broadcasted_optimistic_update();
match latest_broadcasted_optimistic_update {
Some(latest_broadcasted_optimistic_update) => {
if latest_broadcasted_optimistic_update != latest_optimistic_update {
self.set_latest_broadcasted_optimistic_update(
latest_optimistic_update.clone(),
);
return Some(latest_optimistic_update);
}
}
None => {
self.set_latest_broadcasted_optimistic_update(latest_optimistic_update.clone());
return Some(latest_optimistic_update);
}
}
}
None
}
pub fn get_latest_optimistic_update(&self) -> Option<LightClientOptimisticUpdate<T::EthSpec>> {
self.latest_optimistic_update.read().clone()
}

View File

@@ -2600,7 +2600,7 @@ pub fn serve<T: BeaconChainTypes>(
let fork_name = chain
.spec
.fork_name_at_slot::<T::EthSpec>(*update.signature_slot());
.fork_name_at_slot::<T::EthSpec>(update.signature_slot());
match accept_header {
Some(api_types::Accept::Ssz) => Response::builder()
.status(200)

View File

@@ -320,6 +320,38 @@ pub fn process_signed_contribution_and_proofs<T: BeaconChainTypes>(
let seen_timestamp = timestamp_now();
if let Some(latest_optimistic_update) = chain
.light_client_server_cache
.should_broadcast_latest_optimistic_update()
{
let _ = publish_pubsub_message(
&network_tx,
PubsubMessage::LightClientOptimisticUpdate(Box::new(latest_optimistic_update)),
)
.inspect_err(|e| {
error!(
error = ?e,
"Unable to broadcast latest light client optimistic update"
);
});
};
if let Some(latest_finality_update) = chain
.light_client_server_cache
.should_broadcast_latest_finality_update()
{
let _ = publish_pubsub_message(
&network_tx,
PubsubMessage::LightClientFinalityUpdate(Box::new(latest_finality_update)),
)
.inspect_err(|e| {
error!(
error = ?e,
"Unable to broadcast latest light client finality update"
);
});
};
// Verify contributions & broadcast to the network.
for (index, contribution) in signed_contribution_and_proofs.into_iter().enumerate() {
let aggregator_index = contribution.message.aggregator_index;

View File

@@ -1967,7 +1967,10 @@ impl<T: BeaconChainTypes> NetworkBeaconProcessor<T> {
Err(e) => {
metrics::register_finality_update_error(&e);
match e {
LightClientFinalityUpdateError::InvalidLightClientFinalityUpdate => {
LightClientFinalityUpdateError::MismatchedSignatureSlot { .. }
| LightClientFinalityUpdateError::MismatchedAttestedHeader { .. }
| LightClientFinalityUpdateError::MismatchedFinalizedHeader { .. }
| LightClientFinalityUpdateError::MismatchedProofOrSyncAggregate { .. } => {
debug!(
%peer_id,
error = ?e,
@@ -1999,6 +2002,7 @@ impl<T: BeaconChainTypes> NetworkBeaconProcessor<T> {
error = ?e,
"Light client error constructing finality update"
),
LightClientFinalityUpdateError::Ignore => {}
}
self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Ignore);
}
@@ -2080,7 +2084,9 @@ impl<T: BeaconChainTypes> NetworkBeaconProcessor<T> {
}
return;
}
LightClientOptimisticUpdateError::InvalidLightClientOptimisticUpdate => {
LightClientOptimisticUpdateError::MismatchedSignatureSlot { .. }
| LightClientOptimisticUpdateError::MismatchedAttestedHeader { .. }
| LightClientOptimisticUpdateError::MismatchedSyncAggregate { .. } => {
metrics::register_optimistic_update_error(&e);
debug!(
@@ -2119,6 +2125,7 @@ impl<T: BeaconChainTypes> NetworkBeaconProcessor<T> {
"Light client error constructing optimistic update"
)
}
LightClientOptimisticUpdateError::Ignore => {}
}
self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Ignore);
}

View File

@@ -79,6 +79,7 @@ pub struct LightClientFinalityUpdate<E: EthSpec> {
/// current sync aggregate
pub sync_aggregate: SyncAggregate<E>,
/// Slot of the sync aggregated signature
#[superstruct(getter(copy))]
pub signature_slot: Slot,
}
@@ -179,6 +180,20 @@ impl<E: EthSpec> LightClientFinalityUpdate<E> {
})
}
pub fn get_attested_header_root<'a>(&'a self) -> Hash256 {
map_light_client_finality_update_ref!(&'a _, self.to_ref(), |inner, cons| {
cons(inner);
inner.attested_header.beacon.canonical_root()
})
}
pub fn get_finalized_header_root<'a>(&'a self) -> Hash256 {
map_light_client_finality_update_ref!(&'a _, self.to_ref(), |inner, cons| {
cons(inner);
inner.finalized_header.beacon.canonical_root()
})
}
pub fn from_ssz_bytes(bytes: &[u8], fork_name: ForkName) -> Result<Self, ssz::DecodeError> {
let finality_update = match fork_name {
ForkName::Altair | ForkName::Bellatrix => {
@@ -227,7 +242,7 @@ impl<E: EthSpec> LightClientFinalityUpdate<E> {
if attested_slot > prev_slot {
true
} else {
attested_slot == prev_slot && signature_slot > *self.signature_slot()
attested_slot == prev_slot && signature_slot > self.signature_slot()
}
}
}

View File

@@ -60,6 +60,7 @@ pub struct LightClientOptimisticUpdate<E: EthSpec> {
/// current sync aggregate
pub sync_aggregate: SyncAggregate<E>,
/// Slot of the sync aggregated signature
#[superstruct(getter(copy))]
pub signature_slot: Slot,
}
@@ -200,7 +201,7 @@ impl<E: EthSpec> LightClientOptimisticUpdate<E> {
if attested_slot > prev_slot {
true
} else {
attested_slot == prev_slot && signature_slot > *self.signature_slot()
attested_slot == prev_slot && signature_slot > self.signature_slot()
}
}
}

View File

@@ -303,7 +303,7 @@ pub(crate) async fn verify_light_client_updates<E: EthSpec>(
}
// Verify light client optimistic update. `signature_slot_distance` should be 1 in the ideal scenario.
let signature_slot = *client
let signature_slot = client
.get_beacon_light_client_optimistic_update::<E>()
.await
.map_err(|e| format!("Error while getting light client updates: {:?}", e))?
@@ -332,7 +332,7 @@ pub(crate) async fn verify_light_client_updates<E: EthSpec>(
}
continue;
}
let signature_slot = *client
let signature_slot = client
.get_beacon_light_client_finality_update::<E>()
.await
.map_err(|e| format!("Error while getting light client updates: {:?}", e))?