Skip to content

Commit a94b12b

Browse files
authored
Persist light client bootstrap (#5915)
* persist light client updates * update beacon chain to serve light client updates * resolve todos * cache best update * extend cache parts * is better light client update * resolve merge conflict * initial api changes * add lc update db column * fmt * added tests * add sim * Merge branch 'unstable' of https://github.com/sigp/lighthouse into persist-light-client-updates * fix some weird issues with the simulator * tests * Merge branch 'unstable' of https://github.com/sigp/lighthouse into persist-light-client-updates * test changes * merge conflict * testing * started work on ef tests and some code clean up * update tests * linting * noop pre altair, were still failing on electra though * allow for zeroed light client header * Merge branch 'unstable' of https://github.com/sigp/lighthouse into persist-light-client-updates * merge unstable * remove unwraps * remove unwraps * fetch bootstrap without always querying for state * storing bootstrap parts in db * mroe code cleanup * test * prune sync committee branches from dropped chains * Update light_client_update.rs * merge unstable * move functionality to helper methods * refactor is best update fn * refactor is best update fn * improve organization of light client server cache logic * fork diget calc, and only spawn as many blcoks as we need for the lc update test * resovle merge conflict * add electra bootstrap logic, add logic to cache current sync committee * add latest sync committe branch cache * fetch lc update from the cache if it exists * fmt * Fix beacon_chain tests * Add debug code to update ranking_order ef test * Fix compare code * merge conflicts * merge conflict * add better error messaging * resolve merge conflicts * remove lc update from basicsim * rename sync comittte variable and fix persist condition * refactor get_light_client_update logic * add better comments, return helpful error messages over http and rpc * pruning canonical non checkpoint slots * fix test * rerun test * update pruning logic, add tests * fix tests * fix imports * fmt * refactor db code * Refactor db method * Refactor db method * add additional comments * Merge branch 'unstable' of https://github.com/sigp/lighthouse into persist-light-client-bootstrap * fix merge * linting * merge conflict * prevent overflow * enable lc server for http api tests * fix tests * remove prints * remove warning * revert change
1 parent 51091a4 commit a94b12b

File tree

19 files changed

+733
-238
lines changed

19 files changed

+733
-238
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

beacon_node/beacon_chain/src/beacon_chain.rs

Lines changed: 11 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -6987,32 +6987,18 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
69876987
&self,
69886988
block_root: &Hash256,
69896989
) -> Result<Option<(LightClientBootstrap<T::EthSpec>, ForkName)>, Error> {
6990-
let Some(block) = self.get_blinded_block(block_root)? else {
6991-
return Ok(None);
6992-
};
6993-
6994-
let (state_root, slot) = (block.state_root(), block.slot());
6995-
6996-
let Some(mut state) = self.get_state(&state_root, Some(slot))? else {
6997-
return Ok(None);
6998-
};
6990+
let head_state = &self.head().snapshot.beacon_state;
6991+
let finalized_period = head_state
6992+
.finalized_checkpoint()
6993+
.epoch
6994+
.sync_committee_period(&self.spec)?;
69996995

7000-
let fork_name = state
7001-
.fork_name(&self.spec)
7002-
.map_err(Error::InconsistentFork)?;
7003-
7004-
match fork_name {
7005-
ForkName::Altair
7006-
| ForkName::Bellatrix
7007-
| ForkName::Capella
7008-
| ForkName::Deneb
7009-
| ForkName::Electra => {
7010-
LightClientBootstrap::from_beacon_state(&mut state, &block, &self.spec)
7011-
.map(|bootstrap| Some((bootstrap, fork_name)))
7012-
.map_err(Error::LightClientError)
7013-
}
7014-
ForkName::Base => Err(Error::UnsupportedFork),
7015-
}
6996+
self.light_client_server_cache.get_light_client_bootstrap(
6997+
&self.store,
6998+
block_root,
6999+
finalized_period,
7000+
&self.spec,
7001+
)
70167002
}
70177003

70187004
pub fn metrics(&self) -> BeaconChainMetrics {

beacon_node/beacon_chain/src/errors.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,8 @@ pub enum BeaconChainError {
216216
UnableToPublish,
217217
UnableToBuildColumnSidecar(String),
218218
AvailabilityCheckError(AvailabilityCheckError),
219-
LightClientError(LightClientError),
219+
LightClientUpdateError(LightClientUpdateError),
220+
LightClientBootstrapError(String),
220221
UnsupportedFork,
221222
MilhouseError(MilhouseError),
222223
EmptyRpcCustodyColumns,
@@ -250,7 +251,7 @@ easy_from_to!(BlockReplayError, BeaconChainError);
250251
easy_from_to!(InconsistentFork, BeaconChainError);
251252
easy_from_to!(AvailabilityCheckError, BeaconChainError);
252253
easy_from_to!(EpochCacheError, BeaconChainError);
253-
easy_from_to!(LightClientError, BeaconChainError);
254+
easy_from_to!(LightClientUpdateError, BeaconChainError);
254255
easy_from_to!(MilhouseError, BeaconChainError);
255256
easy_from_to!(AttestationError, BeaconChainError);
256257

beacon_node/beacon_chain/src/light_client_server_cache.rs

Lines changed: 124 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,25 @@
11
use crate::errors::BeaconChainError;
22
use crate::{metrics, BeaconChainTypes, BeaconStore};
3+
use eth2::types::light_client_update::CurrentSyncCommitteeProofLen;
34
use parking_lot::{Mutex, RwLock};
45
use safe_arith::SafeArith;
56
use slog::{debug, Logger};
67
use ssz::Decode;
7-
use ssz::Encode;
88
use ssz_types::FixedVector;
99
use std::num::NonZeroUsize;
1010
use std::sync::Arc;
1111
use store::DBColumn;
1212
use store::KeyValueStore;
13+
use tree_hash::TreeHash;
1314
use types::light_client_update::{
14-
FinalizedRootProofLen, NextSyncCommitteeProofLen, FINALIZED_ROOT_INDEX,
15-
NEXT_SYNC_COMMITTEE_INDEX,
15+
FinalizedRootProofLen, NextSyncCommitteeProofLen, CURRENT_SYNC_COMMITTEE_INDEX,
16+
FINALIZED_ROOT_INDEX, NEXT_SYNC_COMMITTEE_INDEX,
1617
};
1718
use types::non_zero_usize::new_non_zero_usize;
1819
use types::{
19-
BeaconBlockRef, BeaconState, ChainSpec, EthSpec, ForkName, Hash256, LightClientFinalityUpdate,
20-
LightClientOptimisticUpdate, LightClientUpdate, Slot, SyncAggregate, SyncCommittee,
20+
BeaconBlockRef, BeaconState, ChainSpec, Checkpoint, EthSpec, ForkName, Hash256,
21+
LightClientBootstrap, LightClientFinalityUpdate, LightClientOptimisticUpdate,
22+
LightClientUpdate, Slot, SyncAggregate, SyncCommittee,
2123
};
2224

2325
/// A prev block cache miss requires to re-generate the state of the post-parent block. Items in the
@@ -28,7 +30,6 @@ const PREV_BLOCK_CACHE_SIZE: NonZeroUsize = new_non_zero_usize(32);
2830
/// This cache computes light client messages ahead of time, required to satisfy p2p and API
2931
/// requests. These messages include proofs on historical states, so on-demand computation is
3032
/// expensive.
31-
///
3233
pub struct LightClientServerCache<T: BeaconChainTypes> {
3334
/// Tracks a single global latest finality update out of all imported blocks.
3435
///
@@ -41,6 +42,8 @@ pub struct LightClientServerCache<T: BeaconChainTypes> {
4142
latest_optimistic_update: RwLock<Option<LightClientOptimisticUpdate<T::EthSpec>>>,
4243
/// Caches the most recent light client update
4344
latest_light_client_update: RwLock<Option<LightClientUpdate<T::EthSpec>>>,
45+
/// Caches the current sync committee,
46+
latest_written_current_sync_committee: RwLock<Option<Arc<SyncCommittee<T::EthSpec>>>>,
4447
/// Caches state proofs by block root
4548
prev_block_cache: Mutex<lru::LruCache<Hash256, LightClientCachedData<T::EthSpec>>>,
4649
}
@@ -51,6 +54,7 @@ impl<T: BeaconChainTypes> LightClientServerCache<T> {
5154
latest_finality_update: None.into(),
5255
latest_optimistic_update: None.into(),
5356
latest_light_client_update: None.into(),
57+
latest_written_current_sync_committee: None.into(),
5458
prev_block_cache: lru::LruCache::new(PREV_BLOCK_CACHE_SIZE).into(),
5559
}
5660
}
@@ -96,6 +100,10 @@ impl<T: BeaconChainTypes> LightClientServerCache<T> {
96100
let signature_slot = block_slot;
97101
let attested_block_root = block_parent_root;
98102

103+
let sync_period = block_slot
104+
.epoch(T::EthSpec::slots_per_epoch())
105+
.sync_committee_period(chain_spec)?;
106+
99107
let attested_block = store.get_blinded_block(attested_block_root)?.ok_or(
100108
BeaconChainError::DBInconsistent(format!(
101109
"Block not available {:?}",
@@ -110,6 +118,18 @@ impl<T: BeaconChainTypes> LightClientServerCache<T> {
110118
attested_block.slot(),
111119
)?;
112120

121+
let finalized_period = cached_parts
122+
.finalized_checkpoint
123+
.epoch
124+
.sync_committee_period(chain_spec)?;
125+
126+
store.store_sync_committee_branch(
127+
attested_block.message().tree_hash_root(),
128+
&cached_parts.current_sync_committee_branch,
129+
)?;
130+
131+
self.store_current_sync_committee(&store, &cached_parts, sync_period, finalized_period)?;
132+
113133
let attested_slot = attested_block.slot();
114134

115135
let maybe_finalized_block = store.get_blinded_block(&cached_parts.finalized_block_root)?;
@@ -178,57 +198,57 @@ impl<T: BeaconChainTypes> LightClientServerCache<T> {
178198

179199
// Spec: Full nodes SHOULD provide the best derivable LightClientUpdate (according to is_better_update)
180200
// for each sync committee period
181-
let prev_light_client_update = match &self.latest_light_client_update.read().clone() {
182-
Some(prev_light_client_update) => Some(prev_light_client_update.clone()),
183-
None => self.get_light_client_update(&store, sync_period, chain_spec)?,
184-
};
201+
let prev_light_client_update =
202+
self.get_light_client_update(&store, sync_period, chain_spec)?;
185203

186204
let should_persist_light_client_update =
187205
if let Some(prev_light_client_update) = prev_light_client_update {
188-
let prev_sync_period = prev_light_client_update
189-
.signature_slot()
190-
.epoch(T::EthSpec::slots_per_epoch())
191-
.sync_committee_period(chain_spec)?;
192-
193-
if sync_period != prev_sync_period {
194-
true
195-
} else {
196-
prev_light_client_update
197-
.is_better_light_client_update(&new_light_client_update, chain_spec)?
198-
}
206+
prev_light_client_update
207+
.is_better_light_client_update(&new_light_client_update, chain_spec)?
199208
} else {
200209
true
201210
};
202211

203212
if should_persist_light_client_update {
204-
self.store_light_client_update(&store, sync_period, &new_light_client_update)?;
213+
store.store_light_client_update(sync_period, &new_light_client_update)?;
214+
*self.latest_light_client_update.write() = Some(new_light_client_update);
205215
}
206216

207217
Ok(())
208218
}
209219

210-
fn store_light_client_update(
220+
fn store_current_sync_committee(
211221
&self,
212222
store: &BeaconStore<T>,
223+
cached_parts: &LightClientCachedData<T::EthSpec>,
213224
sync_committee_period: u64,
214-
light_client_update: &LightClientUpdate<T::EthSpec>,
225+
finalized_period: u64,
215226
) -> Result<(), BeaconChainError> {
216-
let column = DBColumn::LightClientUpdate;
217-
218-
store.hot_db.put_bytes(
219-
column.into(),
220-
&sync_committee_period.to_le_bytes(),
221-
&light_client_update.as_ssz_bytes(),
222-
)?;
227+
if let Some(latest_sync_committee) =
228+
self.latest_written_current_sync_committee.read().clone()
229+
{
230+
if latest_sync_committee == cached_parts.current_sync_committee {
231+
return Ok(());
232+
}
233+
};
223234

224-
*self.latest_light_client_update.write() = Some(light_client_update.clone());
235+
if finalized_period + 1 >= sync_committee_period {
236+
store.store_sync_committee(
237+
sync_committee_period,
238+
&cached_parts.current_sync_committee,
239+
)?;
240+
*self.latest_written_current_sync_committee.write() =
241+
Some(cached_parts.current_sync_committee.clone());
242+
}
225243

226244
Ok(())
227245
}
228246

229-
// Used to fetch the most recently persisted "best" light client update.
230-
// Should not be used outside the light client server, as it also caches the fetched
231-
// light client update.
247+
/// Used to fetch the most recently persisted light client update for the given `sync_committee_period`.
248+
/// It first checks the `latest_light_client_update` cache before querying the db.
249+
///
250+
/// Note: Should not be used outside the light client server, as it also caches the fetched
251+
/// light client update.
232252
fn get_light_client_update(
233253
&self,
234254
store: &BeaconStore<T>,
@@ -245,21 +265,7 @@ impl<T: BeaconChainTypes> LightClientServerCache<T> {
245265
}
246266
}
247267

248-
let column = DBColumn::LightClientUpdate;
249-
let res = store
250-
.hot_db
251-
.get_bytes(column.into(), &sync_committee_period.to_le_bytes())?;
252-
253-
if let Some(light_client_update_bytes) = res {
254-
let epoch = sync_committee_period
255-
.safe_mul(chain_spec.epochs_per_sync_committee_period.into())?;
256-
257-
let fork_name = chain_spec.fork_name_at_epoch(epoch.into());
258-
259-
let light_client_update =
260-
LightClientUpdate::from_ssz_bytes(&light_client_update_bytes, &fork_name)
261-
.map_err(store::errors::Error::SszDecodeError)?;
262-
268+
if let Some(light_client_update) = store.get_light_client_update(sync_committee_period)? {
263269
*self.latest_light_client_update.write() = Some(light_client_update.clone());
264270
return Ok(Some(light_client_update));
265271
}
@@ -340,6 +346,65 @@ impl<T: BeaconChainTypes> LightClientServerCache<T> {
340346
pub fn get_latest_optimistic_update(&self) -> Option<LightClientOptimisticUpdate<T::EthSpec>> {
341347
self.latest_optimistic_update.read().clone()
342348
}
349+
350+
/// Fetches a light client bootstrap for a given finalized checkpoint `block_root`. We eagerly persist
351+
/// `sync_committee_branch and `sync_committee` to allow for a more efficient bootstrap construction.
352+
///
353+
/// Note: It should be the case that a `sync_committee_branch` and `sync_committee` exist in the db
354+
/// for a finalized checkpoint block root. However, we currently have no backfill mechanism for these values.
355+
/// Therefore, `sync_committee_branch` and `sync_committee` are only persisted while a node is synced.
356+
#[allow(clippy::type_complexity)]
357+
pub fn get_light_client_bootstrap(
358+
&self,
359+
store: &BeaconStore<T>,
360+
block_root: &Hash256,
361+
finalized_period: u64,
362+
chain_spec: &ChainSpec,
363+
) -> Result<Option<(LightClientBootstrap<T::EthSpec>, ForkName)>, BeaconChainError> {
364+
let Some(block) = store.get_blinded_block(block_root)? else {
365+
return Err(BeaconChainError::LightClientBootstrapError(format!(
366+
"Block root {block_root} not found"
367+
)));
368+
};
369+
370+
let (_, slot) = (block.state_root(), block.slot());
371+
372+
let fork_name = chain_spec.fork_name_at_slot::<T::EthSpec>(slot);
373+
374+
let sync_committee_period = block
375+
.slot()
376+
.epoch(T::EthSpec::slots_per_epoch())
377+
.sync_committee_period(chain_spec)?;
378+
379+
let Some(current_sync_committee_branch) = store.get_sync_committee_branch(block_root)?
380+
else {
381+
return Err(BeaconChainError::LightClientBootstrapError(format!(
382+
"Sync committee branch for block root {:?} not found",
383+
block_root
384+
)));
385+
};
386+
387+
if sync_committee_period > finalized_period {
388+
return Err(BeaconChainError::LightClientBootstrapError(
389+
format!("The blocks sync committee period {sync_committee_period} is greater than the current finalized period {finalized_period}"),
390+
));
391+
}
392+
393+
let Some(current_sync_committee) = store.get_sync_committee(sync_committee_period)? else {
394+
return Err(BeaconChainError::LightClientBootstrapError(format!(
395+
"Sync committee for period {sync_committee_period} not found"
396+
)));
397+
};
398+
399+
let light_client_bootstrap = LightClientBootstrap::new(
400+
&block,
401+
Arc::new(current_sync_committee),
402+
current_sync_committee_branch,
403+
chain_spec,
404+
)?;
405+
406+
Ok(Some((light_client_bootstrap, fork_name)))
407+
}
343408
}
344409

345410
impl<T: BeaconChainTypes> Default for LightClientServerCache<T> {
@@ -350,23 +415,32 @@ impl<T: BeaconChainTypes> Default for LightClientServerCache<T> {
350415

351416
type FinalityBranch = FixedVector<Hash256, FinalizedRootProofLen>;
352417
type NextSyncCommitteeBranch = FixedVector<Hash256, NextSyncCommitteeProofLen>;
418+
type CurrentSyncCommitteeBranch = FixedVector<Hash256, CurrentSyncCommitteeProofLen>;
353419

354420
#[derive(Clone)]
355421
struct LightClientCachedData<E: EthSpec> {
422+
finalized_checkpoint: Checkpoint,
356423
finality_branch: FinalityBranch,
357424
next_sync_committee_branch: NextSyncCommitteeBranch,
425+
current_sync_committee_branch: CurrentSyncCommitteeBranch,
358426
next_sync_committee: Arc<SyncCommittee<E>>,
427+
current_sync_committee: Arc<SyncCommittee<E>>,
359428
finalized_block_root: Hash256,
360429
}
361430

362431
impl<E: EthSpec> LightClientCachedData<E> {
363432
fn from_state(state: &mut BeaconState<E>) -> Result<Self, BeaconChainError> {
364433
Ok(Self {
434+
finalized_checkpoint: state.finalized_checkpoint(),
365435
finality_branch: state.compute_merkle_proof(FINALIZED_ROOT_INDEX)?.into(),
366436
next_sync_committee: state.next_sync_committee()?.clone(),
437+
current_sync_committee: state.current_sync_committee()?.clone(),
367438
next_sync_committee_branch: state
368439
.compute_merkle_proof(NEXT_SYNC_COMMITTEE_INDEX)?
369440
.into(),
441+
current_sync_committee_branch: state
442+
.compute_merkle_proof(CURRENT_SYNC_COMMITTEE_INDEX)?
443+
.into(),
370444
finalized_block_root: state.finalized_checkpoint().root,
371445
})
372446
}

beacon_node/beacon_chain/src/migrate.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -676,6 +676,7 @@ impl<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> BackgroundMigrator<E, Ho
676676
StoreOp::DeleteBlock(block_root),
677677
StoreOp::DeleteExecutionPayload(block_root),
678678
StoreOp::DeleteBlobs(block_root),
679+
StoreOp::DeleteSyncCommitteeBranch(block_root),
679680
]
680681
})
681682
.chain(

0 commit comments

Comments
 (0)