Skip to content

Commit

Permalink
Added lightclient server side containers (#3655)
Browse files Browse the repository at this point in the history
## Issue Addressed

This PR partially addresses #3651

## Proposed Changes
This PR adds the following containers types from [the lightclient specs](https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/light-client/sync-protocol.md): `LightClientUpdate`, `LightClientFinalityUpdate`, `LightClientOptimisticUpdate` and `LightClientBootstrap`. It also implements the creation of each updates as delined by this [document](https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/light-client/full-node.md).

## Additional Info

Here is a brief description of what each of these container signify:

`LightClientUpdate`: This container is only provided by server (full node) to lightclients when catching up new sync committees beetwen periods and we want possibly one lightclient update ready for each post-altair period the lighthouse node go over. it is needed in the resp/req in method `light_client_update_by_range`.

`LightClientFinalityUpdate/LightClientFinalityUpdate`: Lighthouse will need only the latest of each of this kind of updates, so no need to store them in the database, we can just store the latest one of each one in memory and then just supply them via gossip or respreq, only the latest ones are served by a full node. finality updates marks the transition to a new finalized header, while optimistic updates signify new non-finalized header which are imported optimistically.

`LightClientBootstrap`: This object is retrieved by lightclients during the bootstrap process after a finalized checkpoint is retrieved, ideally we want to store a LightClientBootstrap for each finalized root and then serve each of them by finalized root in respreq protocol id `light_client_bootstrap`.

Little digression to how we implement the creation of each updates: the creation of a optimistic/finality update is just a version of the lightclient_update creation mechanism with less fields being set, there is underlying concept of inheritance, if you look at the specs it becomes very obvious that a lightclient update is just an extension of a finality update and a finality update an extension to an optimistic update.

## Extra note

`LightClientStore` is not implemented as it is only useful as internal storage design for the lightclient side.
  • Loading branch information
Giulio2002 committed Oct 28, 2022
1 parent 6d5a2b5 commit f2f920d
Show file tree
Hide file tree
Showing 5 changed files with 358 additions and 0 deletions.
3 changes: 3 additions & 0 deletions consensus/types/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ pub mod free_attestation;
pub mod graffiti;
pub mod historical_batch;
pub mod indexed_attestation;
pub mod light_client_bootstrap;
pub mod light_client_optimistic_update;
pub mod light_client_update;
pub mod pending_attestation;
pub mod proposer_preparation_data;
pub mod proposer_slashing;
Expand Down
45 changes: 45 additions & 0 deletions consensus/types/src/light_client_bootstrap.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
use super::{BeaconBlockHeader, BeaconState, EthSpec, FixedVector, Hash256, SyncCommittee};
use crate::{light_client_update::*, test_utils::TestRandom};
use serde_derive::{Deserialize, Serialize};
use ssz_derive::{Decode, Encode};
use std::sync::Arc;
use test_random_derive::TestRandom;
use tree_hash::TreeHash;

/// A LightClientBootstrap is the initializer we send over to lightclient nodes
/// that are trying to generate their basic storage when booting up.
#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Encode, Decode, TestRandom)]
#[serde(bound = "T: EthSpec")]
pub struct LightClientBootstrap<T: EthSpec> {
/// Requested beacon block header.
pub header: BeaconBlockHeader,
/// The `SyncCommittee` used in the requested period.
pub current_sync_committee: Arc<SyncCommittee<T>>,
/// Merkle proof for sync committee
pub current_sync_committee_branch: FixedVector<Hash256, CurrentSyncCommitteeProofLen>,
}

impl<T: EthSpec> LightClientBootstrap<T> {
pub fn from_beacon_state(beacon_state: BeaconState<T>) -> Result<Self, Error> {
let mut header = beacon_state.latest_block_header().clone();
header.state_root = beacon_state.tree_hash_root();
Ok(LightClientBootstrap {
header,
current_sync_committee: beacon_state.current_sync_committee()?.clone(),
/// TODO(Giulio2002): Generate Merkle Proof, this is just empty hashes
current_sync_committee_branch: FixedVector::new(vec![
Hash256::zero();
CURRENT_SYNC_COMMITTEE_PROOF_LEN
])?,
})
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::MainnetEthSpec;

ssz_tests!(LightClientBootstrap<MainnetEthSpec>);
}
80 changes: 80 additions & 0 deletions consensus/types/src/light_client_finality_update.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
use super::{BeaconBlockHeader, EthSpec, FixedVector, Hash256, Slot, SyncAggregate, SyncCommittee};
use crate::{light_client_update::*, test_utils::TestRandom, BeaconBlock, BeaconState, ChainSpec};
use safe_arith::ArithError;
use serde_derive::{Deserialize, Serialize};
use ssz_derive::{Decode, Encode};
use ssz_types::typenum::{U5, U6};
use std::sync::Arc;
use test_random_derive::TestRandom;
use tree_hash::TreeHash;

/// A LightClientFinalityUpdate is the update lightclient request or received by a gossip that
/// signal a new finalized beacon block header for the light client sync protocol.
#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Encode, Decode, TestRandom)]
#[serde(bound = "T: EthSpec")]
pub struct LightClientFinalityUpdate<T: EthSpec> {
/// The last `BeaconBlockHeader` from the last attested block by the sync committee.
pub attested_header: BeaconBlockHeader,
/// The last `BeaconBlockHeader` from the last attested finalized block (end of epoch).
pub finalized_header: BeaconBlockHeader,
/// Merkle proof attesting finalized header.
pub finality_branch: FixedVector<Hash256, FinalizedRootProofLen>,
/// current sync aggreggate
pub sync_aggregate: SyncAggregate<T>,
/// Slot of the sync aggregated singature
pub signature_slot: Slot,
}

impl<T: EthSpec> LightClientFinalityUpdate<T> {
pub fn new(
chain_spec: ChainSpec,
beacon_state: BeaconState<T>,
block: BeaconBlock<T>,
attested_state: BeaconState<T>,
finalized_block: BeaconBlock<T>,
) -> Result<Self, Error> {
let altair_fork_epoch = chain_spec
.altair_fork_epoch
.ok_or(Error::AltairForkNotActive)?;
if attested_state.slot().epoch(T::slots_per_epoch()) < altair_fork_epoch {
return Err(Error::AltairForkNotActive);
}

let sync_aggregate = block.body().sync_aggregate()?;
if sync_aggregate.num_set_bits() < chain_spec.min_sync_committee_participants as usize {
return Err(Error::NotEnoughSyncCommitteeParticipants);
}

// Compute and validate attested header.
let mut attested_header = attested_state.latest_block_header().clone();
attested_header.state_root = attested_state.tree_hash_root();
// Build finalized header from finalized block
let finalized_header = BeaconBlockHeader {
slot: finalized_block.slot(),
proposer_index: finalized_block.proposer_index(),
parent_root: finalized_block.parent_root(),
state_root: finalized_block.state_root(),
body_root: finalized_block.body_root(),
};
if finalized_header.tree_hash_root() != beacon_state.finalized_checkpoint().root {
return Err(Error::InvalidFinalizedBlock);
}
// TODO(Giulio2002): compute proper merkle proofs.
Ok(Self {
attested_header: attested_header,
finalized_header: finalized_header,
finality_branch: FixedVector::new(vec![Hash256::zero(); FINALIZED_ROOT_PROOF_LEN])?,
sync_aggregate: sync_aggregate.clone(),
signature_slot: block.slot(),
})
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::MainnetEthSpec;

ssz_tests!(LightClientFinalityUpdate<MainnetEthSpec>);
}
59 changes: 59 additions & 0 deletions consensus/types/src/light_client_optimistic_update.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
use super::{BeaconBlockHeader, EthSpec, Slot, SyncAggregate};
use crate::{
light_client_update::Error, test_utils::TestRandom, BeaconBlock, BeaconState, ChainSpec,
};
use serde_derive::{Deserialize, Serialize};
use ssz_derive::{Decode, Encode};
use test_random_derive::TestRandom;
use tree_hash::TreeHash;

/// A LightClientOptimisticUpdate is the update we send on each slot,
/// it is based off the current unfinalized epoch is verified only against BLS signature.
#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Encode, Decode, TestRandom)]
#[serde(bound = "T: EthSpec")]
pub struct LightClientOptimisticUpdate<T: EthSpec> {
/// The last `BeaconBlockHeader` from the last attested block by the sync committee.
pub attested_header: BeaconBlockHeader,
/// current sync aggreggate
pub sync_aggregate: SyncAggregate<T>,
/// Slot of the sync aggregated singature
pub signature_slot: Slot,
}

impl<T: EthSpec> LightClientOptimisticUpdate<T> {
pub fn new(
chain_spec: ChainSpec,
block: BeaconBlock<T>,
attested_state: BeaconState<T>,
) -> Result<Self, Error> {
let altair_fork_epoch = chain_spec
.altair_fork_epoch
.ok_or(Error::AltairForkNotActive)?;
if attested_state.slot().epoch(T::slots_per_epoch()) < altair_fork_epoch {
return Err(Error::AltairForkNotActive);
}

let sync_aggregate = block.body().sync_aggregate()?;
if sync_aggregate.num_set_bits() < chain_spec.min_sync_committee_participants as usize {
return Err(Error::NotEnoughSyncCommitteeParticipants);
}

// Compute and validate attested header.
let mut attested_header = attested_state.latest_block_header().clone();
attested_header.state_root = attested_state.tree_hash_root();
Ok(Self {
attested_header,
sync_aggregate: sync_aggregate.clone(),
signature_slot: block.slot(),
})
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::MainnetEthSpec;

ssz_tests!(LightClientOptimisticUpdate<MainnetEthSpec>);
}
171 changes: 171 additions & 0 deletions consensus/types/src/light_client_update.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
use super::{BeaconBlockHeader, EthSpec, FixedVector, Hash256, Slot, SyncAggregate, SyncCommittee};
use crate::{beacon_state, test_utils::TestRandom, BeaconBlock, BeaconState, ChainSpec};
use safe_arith::ArithError;
use serde_derive::{Deserialize, Serialize};
use ssz_derive::{Decode, Encode};
use ssz_types::typenum::{U5, U6};
use std::sync::Arc;
use test_random_derive::TestRandom;
use tree_hash::TreeHash;

pub const FINALIZED_ROOT_INDEX: usize = 105;
pub const CURRENT_SYNC_COMMITTEE_INDEX: usize = 54;
pub const NEXT_SYNC_COMMITTEE_INDEX: usize = 55;

pub type FinalizedRootProofLen = U6;
pub type CurrentSyncCommitteeProofLen = U5;
pub type NextSyncCommitteeProofLen = U5;

pub const FINALIZED_ROOT_PROOF_LEN: usize = 6;
pub const CURRENT_SYNC_COMMITTEE_PROOF_LEN: usize = 5;
pub const NEXT_SYNC_COMMITTEE_PROOF_LEN: usize = 5;

#[derive(Debug, PartialEq, Clone)]
pub enum Error {
SszTypesError(ssz_types::Error),
BeaconStateError(beacon_state::Error),
ArithError(ArithError),
AltairForkNotActive,
NotEnoughSyncCommitteeParticipants,
MismatchingPeriods,
InvalidFinalizedBlock,
}

impl From<ssz_types::Error> for Error {
fn from(e: ssz_types::Error) -> Error {
Error::SszTypesError(e)
}
}

impl From<beacon_state::Error> for Error {
fn from(e: beacon_state::Error) -> Error {
Error::BeaconStateError(e)
}
}

impl From<ArithError> for Error {
fn from(e: ArithError) -> Error {
Error::ArithError(e)
}
}

/// A LightClientUpdate is the update we request solely to either complete the bootstraping process,
/// or to sync up to the last committee period, we need to have one ready for each ALTAIR period
/// we go over, note: there is no need to keep all of the updates from [ALTAIR_PERIOD, CURRENT_PERIOD].
#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Encode, Decode, TestRandom)]
#[serde(bound = "T: EthSpec")]
pub struct LightClientUpdate<T: EthSpec> {
/// The last `BeaconBlockHeader` from the last attested block by the sync committee.
pub attested_header: BeaconBlockHeader,
/// The `SyncCommittee` used in the next period.
pub next_sync_committee: Arc<SyncCommittee<T>>,
/// Merkle proof for next sync committee
pub next_sync_committee_branch: FixedVector<Hash256, NextSyncCommitteeProofLen>,
/// The last `BeaconBlockHeader` from the last attested finalized block (end of epoch).
pub finalized_header: BeaconBlockHeader,
/// Merkle proof attesting finalized header.
pub finality_branch: FixedVector<Hash256, FinalizedRootProofLen>,
/// current sync aggreggate
pub sync_aggregate: SyncAggregate<T>,
/// Slot of the sync aggregated singature
pub signature_slot: Slot,
}

impl<T: EthSpec> LightClientUpdate<T> {
pub fn new(
chain_spec: ChainSpec,
beacon_state: BeaconState<T>,
block: BeaconBlock<T>,
attested_state: BeaconState<T>,
finalized_block: BeaconBlock<T>,
) -> Result<Self, Error> {
let altair_fork_epoch = chain_spec
.altair_fork_epoch
.ok_or(Error::AltairForkNotActive)?;
if attested_state.slot().epoch(T::slots_per_epoch()) < altair_fork_epoch {
return Err(Error::AltairForkNotActive);
}

let sync_aggregate = block.body().sync_aggregate()?;
if sync_aggregate.num_set_bits() < chain_spec.min_sync_committee_participants as usize {
return Err(Error::NotEnoughSyncCommitteeParticipants);
}

let signature_period = block.epoch().sync_committee_period(&chain_spec)?;
// Compute and validate attested header.
let mut attested_header = attested_state.latest_block_header().clone();
attested_header.state_root = attested_state.tree_hash_root();
let attested_period = attested_header
.slot
.epoch(T::slots_per_epoch())
.sync_committee_period(&chain_spec)?;
if attested_period != signature_period {
return Err(Error::MismatchingPeriods);
}
// Build finalized header from finalized block
let finalized_header = BeaconBlockHeader {
slot: finalized_block.slot(),
proposer_index: finalized_block.proposer_index(),
parent_root: finalized_block.parent_root(),
state_root: finalized_block.state_root(),
body_root: finalized_block.body_root(),
};
if finalized_header.tree_hash_root() != beacon_state.finalized_checkpoint().root {
return Err(Error::InvalidFinalizedBlock);
}
// TODO(Giulio2002): compute proper merkle proofs.
Ok(Self {
attested_header,
next_sync_committee: attested_state.next_sync_committee()?.clone(),
next_sync_committee_branch: FixedVector::new(vec![
Hash256::zero();
NEXT_SYNC_COMMITTEE_PROOF_LEN
])?,
finalized_header,
finality_branch: FixedVector::new(vec![Hash256::zero(); FINALIZED_ROOT_PROOF_LEN])?,
sync_aggregate: sync_aggregate.clone(),
signature_slot: block.slot(),
})
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::MainnetEthSpec;
use ssz_types::typenum::Unsigned;

ssz_tests!(LightClientUpdate<MainnetEthSpec>);

#[test]
fn finalized_root_params() {
assert!(2usize.pow(FINALIZED_ROOT_PROOF_LEN as u32) <= FINALIZED_ROOT_INDEX);
assert!(2usize.pow(FINALIZED_ROOT_PROOF_LEN as u32 + 1) > FINALIZED_ROOT_INDEX);
assert_eq!(FinalizedRootProofLen::to_usize(), FINALIZED_ROOT_PROOF_LEN);
}

#[test]
fn current_sync_committee_params() {
assert!(
2usize.pow(CURRENT_SYNC_COMMITTEE_PROOF_LEN as u32) <= CURRENT_SYNC_COMMITTEE_INDEX
);
assert!(
2usize.pow(CURRENT_SYNC_COMMITTEE_PROOF_LEN as u32 + 1) > CURRENT_SYNC_COMMITTEE_INDEX
);
assert_eq!(
CurrentSyncCommitteeProofLen::to_usize(),
CURRENT_SYNC_COMMITTEE_PROOF_LEN
);
}

#[test]
fn next_sync_committee_params() {
assert!(2usize.pow(NEXT_SYNC_COMMITTEE_PROOF_LEN as u32) <= NEXT_SYNC_COMMITTEE_INDEX);
assert!(2usize.pow(NEXT_SYNC_COMMITTEE_PROOF_LEN as u32 + 1) > NEXT_SYNC_COMMITTEE_INDEX);
assert_eq!(
NextSyncCommitteeProofLen::to_usize(),
NEXT_SYNC_COMMITTEE_PROOF_LEN
);
}
}

0 comments on commit f2f920d

Please sign in to comment.