diff --git a/Cargo.lock b/Cargo.lock index 8054c812f17..1d243d95544 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8262,6 +8262,7 @@ dependencies = [ "lighthouse_metrics", "lru", "parking_lot 0.12.3", + "safe_arith", "serde", "slog", "sloggers", diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 322a2caa673..bf660c9eaf9 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -6987,32 +6987,18 @@ impl BeaconChain { &self, block_root: &Hash256, ) -> Result, ForkName)>, Error> { - let Some(block) = self.get_blinded_block(block_root)? else { - return Ok(None); - }; - - let (state_root, slot) = (block.state_root(), block.slot()); - - let Some(mut state) = self.get_state(&state_root, Some(slot))? else { - return Ok(None); - }; + let head_state = &self.head().snapshot.beacon_state; + let finalized_period = head_state + .finalized_checkpoint() + .epoch + .sync_committee_period(&self.spec)?; - let fork_name = state - .fork_name(&self.spec) - .map_err(Error::InconsistentFork)?; - - match fork_name { - ForkName::Altair - | ForkName::Bellatrix - | ForkName::Capella - | ForkName::Deneb - | ForkName::Electra => { - LightClientBootstrap::from_beacon_state(&mut state, &block, &self.spec) - .map(|bootstrap| Some((bootstrap, fork_name))) - .map_err(Error::LightClientError) - } - ForkName::Base => Err(Error::UnsupportedFork), - } + self.light_client_server_cache.get_light_client_bootstrap( + &self.store, + block_root, + finalized_period, + &self.spec, + ) } pub fn metrics(&self) -> BeaconChainMetrics { diff --git a/beacon_node/beacon_chain/src/errors.rs b/beacon_node/beacon_chain/src/errors.rs index 4db3f0ebb41..994ac79af7e 100644 --- a/beacon_node/beacon_chain/src/errors.rs +++ b/beacon_node/beacon_chain/src/errors.rs @@ -216,7 +216,8 @@ pub enum BeaconChainError { UnableToPublish, UnableToBuildColumnSidecar(String), AvailabilityCheckError(AvailabilityCheckError), - LightClientError(LightClientError), + LightClientUpdateError(LightClientUpdateError), + LightClientBootstrapError(String), UnsupportedFork, MilhouseError(MilhouseError), EmptyRpcCustodyColumns, @@ -250,7 +251,7 @@ easy_from_to!(BlockReplayError, BeaconChainError); easy_from_to!(InconsistentFork, BeaconChainError); easy_from_to!(AvailabilityCheckError, BeaconChainError); easy_from_to!(EpochCacheError, BeaconChainError); -easy_from_to!(LightClientError, BeaconChainError); +easy_from_to!(LightClientUpdateError, BeaconChainError); easy_from_to!(MilhouseError, BeaconChainError); easy_from_to!(AttestationError, BeaconChainError); diff --git a/beacon_node/beacon_chain/src/light_client_server_cache.rs b/beacon_node/beacon_chain/src/light_client_server_cache.rs index efc746675dc..ca015d0365a 100644 --- a/beacon_node/beacon_chain/src/light_client_server_cache.rs +++ b/beacon_node/beacon_chain/src/light_client_server_cache.rs @@ -1,23 +1,25 @@ use crate::errors::BeaconChainError; use crate::{metrics, BeaconChainTypes, BeaconStore}; +use eth2::types::light_client_update::CurrentSyncCommitteeProofLen; use parking_lot::{Mutex, RwLock}; use safe_arith::SafeArith; use slog::{debug, Logger}; use ssz::Decode; -use ssz::Encode; use ssz_types::FixedVector; use std::num::NonZeroUsize; use std::sync::Arc; use store::DBColumn; use store::KeyValueStore; +use tree_hash::TreeHash; use types::light_client_update::{ - FinalizedRootProofLen, NextSyncCommitteeProofLen, FINALIZED_ROOT_INDEX, - NEXT_SYNC_COMMITTEE_INDEX, + FinalizedRootProofLen, NextSyncCommitteeProofLen, CURRENT_SYNC_COMMITTEE_INDEX, + FINALIZED_ROOT_INDEX, NEXT_SYNC_COMMITTEE_INDEX, }; use types::non_zero_usize::new_non_zero_usize; use types::{ - BeaconBlockRef, BeaconState, ChainSpec, EthSpec, ForkName, Hash256, LightClientFinalityUpdate, - LightClientOptimisticUpdate, LightClientUpdate, Slot, SyncAggregate, SyncCommittee, + BeaconBlockRef, BeaconState, ChainSpec, Checkpoint, EthSpec, ForkName, Hash256, + LightClientBootstrap, LightClientFinalityUpdate, LightClientOptimisticUpdate, + LightClientUpdate, Slot, SyncAggregate, SyncCommittee, }; /// 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); /// This cache computes light client messages ahead of time, required to satisfy p2p and API /// requests. These messages include proofs on historical states, so on-demand computation is /// expensive. -/// pub struct LightClientServerCache { /// Tracks a single global latest finality update out of all imported blocks. /// @@ -41,6 +42,8 @@ pub struct LightClientServerCache { latest_optimistic_update: RwLock>>, /// Caches the most recent light client update latest_light_client_update: RwLock>>, + /// Caches the current sync committee, + latest_written_current_sync_committee: RwLock>>>, /// Caches state proofs by block root prev_block_cache: Mutex>>, } @@ -51,6 +54,7 @@ impl LightClientServerCache { latest_finality_update: None.into(), latest_optimistic_update: None.into(), latest_light_client_update: None.into(), + latest_written_current_sync_committee: None.into(), prev_block_cache: lru::LruCache::new(PREV_BLOCK_CACHE_SIZE).into(), } } @@ -96,6 +100,10 @@ impl LightClientServerCache { let signature_slot = block_slot; let attested_block_root = block_parent_root; + let sync_period = block_slot + .epoch(T::EthSpec::slots_per_epoch()) + .sync_committee_period(chain_spec)?; + let attested_block = store.get_blinded_block(attested_block_root)?.ok_or( BeaconChainError::DBInconsistent(format!( "Block not available {:?}", @@ -110,6 +118,18 @@ impl LightClientServerCache { attested_block.slot(), )?; + let finalized_period = cached_parts + .finalized_checkpoint + .epoch + .sync_committee_period(chain_spec)?; + + store.store_sync_committee_branch( + attested_block.message().tree_hash_root(), + &cached_parts.current_sync_committee_branch, + )?; + + self.store_current_sync_committee(&store, &cached_parts, sync_period, finalized_period)?; + let attested_slot = attested_block.slot(); let maybe_finalized_block = store.get_blinded_block(&cached_parts.finalized_block_root)?; @@ -178,57 +198,57 @@ impl LightClientServerCache { // Spec: Full nodes SHOULD provide the best derivable LightClientUpdate (according to is_better_update) // for each sync committee period - let prev_light_client_update = match &self.latest_light_client_update.read().clone() { - Some(prev_light_client_update) => Some(prev_light_client_update.clone()), - None => self.get_light_client_update(&store, sync_period, chain_spec)?, - }; + let prev_light_client_update = + self.get_light_client_update(&store, sync_period, chain_spec)?; let should_persist_light_client_update = if let Some(prev_light_client_update) = prev_light_client_update { - let prev_sync_period = prev_light_client_update - .signature_slot() - .epoch(T::EthSpec::slots_per_epoch()) - .sync_committee_period(chain_spec)?; - - if sync_period != prev_sync_period { - true - } else { - prev_light_client_update - .is_better_light_client_update(&new_light_client_update, chain_spec)? - } + prev_light_client_update + .is_better_light_client_update(&new_light_client_update, chain_spec)? } else { true }; if should_persist_light_client_update { - self.store_light_client_update(&store, sync_period, &new_light_client_update)?; + store.store_light_client_update(sync_period, &new_light_client_update)?; + *self.latest_light_client_update.write() = Some(new_light_client_update); } Ok(()) } - fn store_light_client_update( + fn store_current_sync_committee( &self, store: &BeaconStore, + cached_parts: &LightClientCachedData, sync_committee_period: u64, - light_client_update: &LightClientUpdate, + finalized_period: u64, ) -> Result<(), BeaconChainError> { - let column = DBColumn::LightClientUpdate; - - store.hot_db.put_bytes( - column.into(), - &sync_committee_period.to_le_bytes(), - &light_client_update.as_ssz_bytes(), - )?; + if let Some(latest_sync_committee) = + self.latest_written_current_sync_committee.read().clone() + { + if latest_sync_committee == cached_parts.current_sync_committee { + return Ok(()); + } + }; - *self.latest_light_client_update.write() = Some(light_client_update.clone()); + if finalized_period + 1 >= sync_committee_period { + store.store_sync_committee( + sync_committee_period, + &cached_parts.current_sync_committee, + )?; + *self.latest_written_current_sync_committee.write() = + Some(cached_parts.current_sync_committee.clone()); + } Ok(()) } - // Used to fetch the most recently persisted "best" light client update. - // Should not be used outside the light client server, as it also caches the fetched - // light client update. + /// Used to fetch the most recently persisted light client update for the given `sync_committee_period`. + /// It first checks the `latest_light_client_update` cache before querying the db. + /// + /// Note: Should not be used outside the light client server, as it also caches the fetched + /// light client update. fn get_light_client_update( &self, store: &BeaconStore, @@ -245,21 +265,7 @@ impl LightClientServerCache { } } - let column = DBColumn::LightClientUpdate; - let res = store - .hot_db - .get_bytes(column.into(), &sync_committee_period.to_le_bytes())?; - - if let Some(light_client_update_bytes) = res { - let epoch = sync_committee_period - .safe_mul(chain_spec.epochs_per_sync_committee_period.into())?; - - let fork_name = chain_spec.fork_name_at_epoch(epoch.into()); - - let light_client_update = - LightClientUpdate::from_ssz_bytes(&light_client_update_bytes, &fork_name) - .map_err(store::errors::Error::SszDecodeError)?; - + if let Some(light_client_update) = store.get_light_client_update(sync_committee_period)? { *self.latest_light_client_update.write() = Some(light_client_update.clone()); return Ok(Some(light_client_update)); } @@ -340,6 +346,65 @@ impl LightClientServerCache { pub fn get_latest_optimistic_update(&self) -> Option> { self.latest_optimistic_update.read().clone() } + + /// Fetches a light client bootstrap for a given finalized checkpoint `block_root`. We eagerly persist + /// `sync_committee_branch and `sync_committee` to allow for a more efficient bootstrap construction. + /// + /// Note: It should be the case that a `sync_committee_branch` and `sync_committee` exist in the db + /// for a finalized checkpoint block root. However, we currently have no backfill mechanism for these values. + /// Therefore, `sync_committee_branch` and `sync_committee` are only persisted while a node is synced. + #[allow(clippy::type_complexity)] + pub fn get_light_client_bootstrap( + &self, + store: &BeaconStore, + block_root: &Hash256, + finalized_period: u64, + chain_spec: &ChainSpec, + ) -> Result, ForkName)>, BeaconChainError> { + let Some(block) = store.get_blinded_block(block_root)? else { + return Err(BeaconChainError::LightClientBootstrapError(format!( + "Block root {block_root} not found" + ))); + }; + + let (_, slot) = (block.state_root(), block.slot()); + + let fork_name = chain_spec.fork_name_at_slot::(slot); + + let sync_committee_period = block + .slot() + .epoch(T::EthSpec::slots_per_epoch()) + .sync_committee_period(chain_spec)?; + + let Some(current_sync_committee_branch) = store.get_sync_committee_branch(block_root)? + else { + return Err(BeaconChainError::LightClientBootstrapError(format!( + "Sync committee branch for block root {:?} not found", + block_root + ))); + }; + + if sync_committee_period > finalized_period { + return Err(BeaconChainError::LightClientBootstrapError( + format!("The blocks sync committee period {sync_committee_period} is greater than the current finalized period {finalized_period}"), + )); + } + + let Some(current_sync_committee) = store.get_sync_committee(sync_committee_period)? else { + return Err(BeaconChainError::LightClientBootstrapError(format!( + "Sync committee for period {sync_committee_period} not found" + ))); + }; + + let light_client_bootstrap = LightClientBootstrap::new( + &block, + Arc::new(current_sync_committee), + current_sync_committee_branch, + chain_spec, + )?; + + Ok(Some((light_client_bootstrap, fork_name))) + } } impl Default for LightClientServerCache { @@ -350,23 +415,32 @@ impl Default for LightClientServerCache { type FinalityBranch = FixedVector; type NextSyncCommitteeBranch = FixedVector; +type CurrentSyncCommitteeBranch = FixedVector; #[derive(Clone)] struct LightClientCachedData { + finalized_checkpoint: Checkpoint, finality_branch: FinalityBranch, next_sync_committee_branch: NextSyncCommitteeBranch, + current_sync_committee_branch: CurrentSyncCommitteeBranch, next_sync_committee: Arc>, + current_sync_committee: Arc>, finalized_block_root: Hash256, } impl LightClientCachedData { fn from_state(state: &mut BeaconState) -> Result { Ok(Self { + finalized_checkpoint: state.finalized_checkpoint(), finality_branch: state.compute_merkle_proof(FINALIZED_ROOT_INDEX)?.into(), next_sync_committee: state.next_sync_committee()?.clone(), + current_sync_committee: state.current_sync_committee()?.clone(), next_sync_committee_branch: state .compute_merkle_proof(NEXT_SYNC_COMMITTEE_INDEX)? .into(), + current_sync_committee_branch: state + .compute_merkle_proof(CURRENT_SYNC_COMMITTEE_INDEX)? + .into(), finalized_block_root: state.finalized_checkpoint().root, }) } diff --git a/beacon_node/beacon_chain/src/migrate.rs b/beacon_node/beacon_chain/src/migrate.rs index fcb8fb1c897..f83df7b4468 100644 --- a/beacon_node/beacon_chain/src/migrate.rs +++ b/beacon_node/beacon_chain/src/migrate.rs @@ -676,6 +676,7 @@ impl, Cold: ItemStore> BackgroundMigrator, + slot: Slot, + block_root: Hash256, + ) { + let fork_name = state.fork_name(&self.spec).unwrap(); + if !fork_name.altair_enabled() { + return; + } + + let log = self.logger(); + let contributions = + self.make_sync_contributions(state, block_root, slot, RelativeSyncCommittee::Current); + + for (_, contribution_and_proof) in contributions { + let Some(contribution_and_proof) = contribution_and_proof else { + continue; + }; + let contribution = contribution_and_proof.message.contribution; + self.chain + .op_pool + .insert_sync_contribution(contribution.clone()) + .unwrap(); + self.chain + .op_pool + .insert_sync_contribution(contribution) + .unwrap(); + } + + let Some(sync_aggregate) = self.chain.op_pool.get_sync_aggregate(state).unwrap() else { + return; + }; + + let _ = self + .chain + .light_client_server_cache + .recompute_and_cache_updates( + self.chain.store.clone(), + slot, + &block_root, + &sync_aggregate, + log, + &self.spec, + ); + } + + pub async fn add_attested_blocks_at_slots_with_lc_data( + &self, + mut state: BeaconState, + state_root: Hash256, + slots: &[Slot], + validators: &[usize], + mut latest_block_hash: Option, + sync_committee_strategy: SyncCommitteeStrategy, + ) -> AddBlocksResult { + assert!( + slots.windows(2).all(|w| w[0] <= w[1]), + "Slots have to be sorted" + ); // slice.is_sorted() isn't stabilized at the moment of writing this + let mut block_hash_from_slot: HashMap = HashMap::new(); + let mut state_hash_from_slot: HashMap = HashMap::new(); + for slot in slots { + let (block_hash, new_state) = self + .add_attested_block_at_slot_with_sync( + *slot, + state, + state_root, + validators, + sync_committee_strategy, + ) + .await + .unwrap(); + + state = new_state; + + self.update_light_client_server_cache(&state, *slot, block_hash.into()); + + block_hash_from_slot.insert(*slot, block_hash); + state_hash_from_slot.insert(*slot, state.canonical_root().unwrap().into()); + latest_block_hash = Some(block_hash); + } + ( + block_hash_from_slot, + state_hash_from_slot, + latest_block_hash.unwrap(), + state, + ) + } + async fn add_attested_blocks_at_slots_given_lbh( &self, mut state: BeaconState, @@ -2250,7 +2346,9 @@ where ) .await .unwrap(); + state = new_state; + block_hash_from_slot.insert(*slot, block_hash); state_hash_from_slot.insert(*slot, state.canonical_root().unwrap().into()); latest_block_hash = Some(block_hash); @@ -2459,6 +2557,23 @@ where block_strategy, attestation_strategy, SyncCommitteeStrategy::NoValidators, + LightClientStrategy::Disabled, + ) + .await + } + + pub async fn extend_chain_with_light_client_data( + &self, + num_blocks: usize, + block_strategy: BlockStrategy, + attestation_strategy: AttestationStrategy, + ) -> Hash256 { + self.extend_chain_with_sync( + num_blocks, + block_strategy, + attestation_strategy, + SyncCommitteeStrategy::NoValidators, + LightClientStrategy::Enabled, ) .await } @@ -2469,6 +2584,7 @@ where block_strategy: BlockStrategy, attestation_strategy: AttestationStrategy, sync_committee_strategy: SyncCommitteeStrategy, + light_client_strategy: LightClientStrategy, ) -> Hash256 { let (mut state, slots) = match block_strategy { BlockStrategy::OnCanonicalHead => { @@ -2500,15 +2616,30 @@ where }; let state_root = state.update_tree_hash_cache().unwrap(); - let (_, _, last_produced_block_hash, _) = self - .add_attested_blocks_at_slots_with_sync( - state, - state_root, - &slots, - &validators, - sync_committee_strategy, - ) - .await; + let (_, _, last_produced_block_hash, _) = match light_client_strategy { + LightClientStrategy::Enabled => { + self.add_attested_blocks_at_slots_with_lc_data( + state, + state_root, + &slots, + &validators, + None, + sync_committee_strategy, + ) + .await + } + LightClientStrategy::Disabled => { + self.add_attested_blocks_at_slots_with_sync( + state, + state_root, + &slots, + &validators, + sync_committee_strategy, + ) + .await + } + }; + last_produced_block_hash.into() } diff --git a/beacon_node/beacon_chain/tests/store_tests.rs b/beacon_node/beacon_chain/tests/store_tests.rs index 95bf7f1ce84..1b1e5ea5149 100644 --- a/beacon_node/beacon_chain/tests/store_tests.rs +++ b/beacon_node/beacon_chain/tests/store_tests.rs @@ -5,7 +5,7 @@ use beacon_chain::block_verification_types::RpcBlock; use beacon_chain::builder::BeaconChainBuilder; use beacon_chain::data_availability_checker::AvailableBlock; use beacon_chain::schema_change::migrate_schema; -use beacon_chain::test_utils::RelativeSyncCommittee; +use beacon_chain::test_utils::SyncCommitteeStrategy; use beacon_chain::test_utils::{ mock_execution_layer_from_parts, test_spec, AttestationStrategy, BeaconChainHarness, BlockStrategy, DiskHarnessType, KZG, @@ -104,6 +104,142 @@ fn get_harness_generic( harness } +#[tokio::test] +async fn light_client_bootstrap_test() { + let spec = test_spec::(); + let Some(_) = spec.altair_fork_epoch else { + // No-op prior to Altair. + return; + }; + + let checkpoint_slot = Slot::new(E::slots_per_epoch() * 6); + let db_path = tempdir().unwrap(); + let log = test_logger(); + + let seconds_per_slot = spec.seconds_per_slot; + let store = get_store_generic( + &db_path, + StoreConfig { + slots_per_restore_point: 2 * E::slots_per_epoch(), + ..Default::default() + }, + test_spec::(), + ); + let harness = get_harness(store.clone(), LOW_VALIDATOR_COUNT); + let all_validators = (0..LOW_VALIDATOR_COUNT).collect::>(); + let num_initial_slots = E::slots_per_epoch() * 7; + let slots: Vec = (1..num_initial_slots).map(Slot::new).collect(); + + let (genesis_state, genesis_state_root) = harness.get_current_state_and_root(); + harness + .add_attested_blocks_at_slots_with_lc_data( + genesis_state.clone(), + genesis_state_root, + &slots, + &all_validators, + None, + SyncCommitteeStrategy::NoValidators, + ) + .await; + + let wss_block_root = harness + .chain + .block_root_at_slot(checkpoint_slot, WhenSlotSkipped::Prev) + .unwrap() + .unwrap(); + let wss_state_root = harness + .chain + .state_root_at_slot(checkpoint_slot) + .unwrap() + .unwrap(); + let wss_block = harness + .chain + .store + .get_full_block(&wss_block_root) + .unwrap() + .unwrap(); + let wss_blobs_opt = harness.chain.store.get_blobs(&wss_block_root).unwrap(); + let wss_state = store + .get_state(&wss_state_root, Some(checkpoint_slot)) + .unwrap() + .unwrap(); + + let kzg = spec.deneb_fork_epoch.map(|_| KZG.clone()); + + let mock = + mock_execution_layer_from_parts(&harness.spec, harness.runtime.task_executor.clone()); + + // Initialise a new beacon chain from the finalized checkpoint. + // The slot clock must be set to a time ahead of the checkpoint state. + let slot_clock = TestingSlotClock::new( + Slot::new(0), + Duration::from_secs(harness.chain.genesis_time), + Duration::from_secs(seconds_per_slot), + ); + slot_clock.set_slot(harness.get_current_slot().as_u64()); + + let (shutdown_tx, _shutdown_rx) = futures::channel::mpsc::channel(1); + + let beacon_chain = BeaconChainBuilder::>::new(MinimalEthSpec) + .store(store.clone()) + .custom_spec(test_spec::()) + .task_executor(harness.chain.task_executor.clone()) + .logger(log.clone()) + .weak_subjectivity_state( + wss_state, + wss_block.clone(), + wss_blobs_opt.clone(), + genesis_state, + ) + .unwrap() + .store_migrator_config(MigratorConfig::default().blocking()) + .dummy_eth1_backend() + .expect("should build dummy backend") + .slot_clock(slot_clock) + .shutdown_sender(shutdown_tx) + .chain_config(ChainConfig::default()) + .event_handler(Some(ServerSentEventHandler::new_with_capacity( + log.clone(), + 1, + ))) + .execution_layer(Some(mock.el)) + .kzg(kzg) + .build() + .expect("should build"); + + let current_state = harness.get_current_state(); + + if ForkName::Electra == current_state.fork_name_unchecked() { + // TODO(electra) fix beacon state `compute_merkle_proof` + return; + } + + let finalized_checkpoint = beacon_chain + .canonical_head + .cached_head() + .finalized_checkpoint(); + + let block_root = finalized_checkpoint.root; + + let (lc_bootstrap, _) = harness + .chain + .get_light_client_bootstrap(&block_root) + .unwrap() + .unwrap(); + + let bootstrap_slot = match lc_bootstrap { + LightClientBootstrap::Altair(lc_bootstrap) => lc_bootstrap.header.beacon.slot, + LightClientBootstrap::Capella(lc_bootstrap) => lc_bootstrap.header.beacon.slot, + LightClientBootstrap::Deneb(lc_bootstrap) => lc_bootstrap.header.beacon.slot, + LightClientBootstrap::Electra(lc_bootstrap) => lc_bootstrap.header.beacon.slot, + }; + + assert_eq!( + bootstrap_slot.epoch(E::slots_per_epoch()), + finalized_checkpoint.epoch + ); +} + #[tokio::test] async fn light_client_updates_test() { let spec = test_spec::(); @@ -170,7 +306,7 @@ async fn light_client_updates_test() { harness.advance_slot(); harness - .extend_chain( + .extend_chain_with_light_client_data( num_final_blocks as usize, BlockStrategy::OnCanonicalHead, AttestationStrategy::AllValidators, @@ -224,53 +360,6 @@ async fn light_client_updates_test() { return; } - let block_root = *current_state - .get_block_root(current_state.slot() - Slot::new(1)) - .unwrap(); - - let contributions = harness.make_sync_contributions( - ¤t_state, - block_root, - current_state.slot() - Slot::new(1), - RelativeSyncCommittee::Current, - ); - - // generate sync aggregates - for (_, contribution_and_proof) in contributions { - let contribution = contribution_and_proof - .expect("contribution exists for committee") - .message - .contribution; - beacon_chain - .op_pool - .insert_sync_contribution(contribution.clone()) - .unwrap(); - beacon_chain - .op_pool - .insert_sync_contribution(contribution) - .unwrap(); - } - - // check that we can fetch the newly generated sync aggregate - let sync_aggregate = beacon_chain - .op_pool - .get_sync_aggregate(¤t_state) - .unwrap() - .unwrap(); - - // cache light client data - beacon_chain - .light_client_server_cache - .recompute_and_cache_updates( - store.clone(), - current_state.slot() - Slot::new(1), - &block_root, - &sync_aggregate, - &log, - &spec, - ) - .unwrap(); - // calculate the sync period from the previous slot let sync_period = (current_state.slot() - Slot::new(1)) .epoch(E::slots_per_epoch()) @@ -291,61 +380,13 @@ async fn light_client_updates_test() { } harness - .extend_chain( + .extend_chain_with_light_client_data( num_final_blocks as usize, BlockStrategy::OnCanonicalHead, AttestationStrategy::AllValidators, ) .await; - let current_state = harness.get_current_state(); - - let block_root = *current_state - .get_block_root(current_state.slot() - Slot::new(1)) - .unwrap(); - - let contributions = harness.make_sync_contributions( - ¤t_state, - block_root, - current_state.slot() - Slot::new(1), - RelativeSyncCommittee::Current, - ); - - // generate new sync aggregates from this new state - for (_, contribution_and_proof) in contributions { - let contribution = contribution_and_proof - .expect("contribution exists for committee") - .message - .contribution; - beacon_chain - .op_pool - .insert_sync_contribution(contribution.clone()) - .unwrap(); - beacon_chain - .op_pool - .insert_sync_contribution(contribution) - .unwrap(); - } - - let sync_aggregate = beacon_chain - .op_pool - .get_sync_aggregate(¤t_state) - .unwrap() - .unwrap(); - - // cache new light client data - beacon_chain - .light_client_server_cache - .recompute_and_cache_updates( - store.clone(), - current_state.slot() - Slot::new(1), - &block_root, - &sync_aggregate, - &log, - &spec, - ) - .unwrap(); - // we should now have two light client updates in the db let lc_updates = beacon_chain .get_light_client_updates(sync_period, 100) diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index 22e9931043e..998114f565e 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -31,7 +31,7 @@ mod validator_inclusion; mod validators; mod version; -use crate::light_client::get_light_client_updates; +use crate::light_client::{get_light_client_bootstrap, get_light_client_updates}; use crate::produce_block::{produce_blinded_block_v2, produce_block_v2, produce_block_v3}; use crate::version::fork_versioned_response; use beacon_chain::{ @@ -2411,40 +2411,7 @@ pub fn serve( block_root: Hash256, accept_header: Option| { task_spawner.blocking_response_task(Priority::P1, move || { - let (bootstrap, fork_name) = match chain.get_light_client_bootstrap(&block_root) - { - Ok(Some(res)) => res, - Ok(None) => { - return Err(warp_utils::reject::custom_not_found( - "Light client bootstrap unavailable".to_string(), - )); - } - Err(e) => { - return Err(warp_utils::reject::custom_server_error(format!( - "Unable to obtain LightClientBootstrap instance: {e:?}" - ))); - } - }; - - match accept_header { - Some(api_types::Accept::Ssz) => Response::builder() - .status(200) - .body(bootstrap.as_ssz_bytes().into()) - .map(|res: Response| add_ssz_content_type_header(res)) - .map_err(|e| { - warp_utils::reject::custom_server_error(format!( - "failed to create response: {}", - e - )) - }), - _ => Ok(warp::reply::json(&ForkVersionedResponse { - version: Some(fork_name), - metadata: EmptyMetadata {}, - data: bootstrap, - }) - .into_response()), - } - .map(|resp| add_consensus_version_header(resp, fork_name)) + get_light_client_bootstrap::(chain, &block_root, accept_header) }) }, ); diff --git a/beacon_node/http_api/src/light_client.rs b/beacon_node/http_api/src/light_client.rs index a6543114b85..ac8c08581ca 100644 --- a/beacon_node/http_api/src/light_client.rs +++ b/beacon_node/http_api/src/light_client.rs @@ -1,18 +1,20 @@ -use beacon_chain::{BeaconChain, BeaconChainTypes}; +use crate::version::{ + add_consensus_version_header, add_ssz_content_type_header, fork_versioned_response, V1, +}; +use beacon_chain::{BeaconChain, BeaconChainError, BeaconChainTypes}; use eth2::types::{ self as api_types, ChainSpec, ForkVersionedResponse, LightClientUpdate, LightClientUpdateResponseChunk, LightClientUpdateSszResponse, LightClientUpdatesQuery, }; use ssz::Encode; use std::sync::Arc; +use types::{ForkName, Hash256, LightClientBootstrap}; use warp::{ hyper::{Body, Response}, reply::Reply, Rejection, }; -use crate::version::{add_ssz_content_type_header, fork_versioned_response, V1}; - const MAX_REQUEST_LIGHT_CLIENT_UPDATES: u64 = 128; pub fn get_light_client_updates( @@ -62,6 +64,45 @@ pub fn get_light_client_updates( } } +pub fn get_light_client_bootstrap( + chain: Arc>, + block_root: &Hash256, + accept_header: Option, +) -> Result, Rejection> { + let (light_client_bootstrap, fork_name) = chain + .get_light_client_bootstrap(block_root) + .map_err(|err| { + let error_message = if let BeaconChainError::LightClientBootstrapError(err) = err { + println!("{:?}", err); + err + } else { + "No LightClientBootstrap found".to_string() + }; + warp_utils::reject::custom_not_found(error_message) + })? + .ok_or(warp_utils::reject::custom_not_found( + "No LightClientBootstrap found".to_string(), + ))?; + + match accept_header { + Some(api_types::Accept::Ssz) => Response::builder() + .status(200) + .body(light_client_bootstrap.as_ssz_bytes().into()) + .map(|res: Response| add_consensus_version_header(res, fork_name)) + .map(|res: Response| add_ssz_content_type_header(res)) + .map_err(|e| { + warp_utils::reject::custom_server_error(format!("failed to create response: {}", e)) + }), + _ => { + let fork_versioned_response = map_light_client_bootstrap_to_json_response::( + fork_name, + light_client_bootstrap, + )?; + Ok(warp::reply::json(&fork_versioned_response).into_response()) + } + } +} + pub fn validate_light_client_updates_request( chain: &BeaconChain, query: &LightClientUpdatesQuery, @@ -131,6 +172,13 @@ fn map_light_client_update_to_ssz_chunk( } } +fn map_light_client_bootstrap_to_json_response( + fork_name: ForkName, + light_client_bootstrap: LightClientBootstrap, +) -> Result>, Rejection> { + fork_versioned_response(V1, fork_name, light_client_bootstrap) +} + fn map_light_client_update_to_json_response( chain: &BeaconChain, light_client_update: LightClientUpdate, diff --git a/beacon_node/http_api/tests/interactive_tests.rs b/beacon_node/http_api/tests/interactive_tests.rs index 9ff411cf1c9..5034492e250 100644 --- a/beacon_node/http_api/tests/interactive_tests.rs +++ b/beacon_node/http_api/tests/interactive_tests.rs @@ -1,7 +1,7 @@ //! Generic tests that make use of the (newer) `InteractiveApiTester` use beacon_chain::{ chain_config::{DisallowedReOrgOffsets, ReOrgThreshold}, - test_utils::{AttestationStrategy, BlockStrategy, SyncCommitteeStrategy}, + test_utils::{AttestationStrategy, BlockStrategy, LightClientStrategy, SyncCommitteeStrategy}, ChainConfig, }; use beacon_processor::work_reprocessing_queue::ReprocessQueueMessage; @@ -88,6 +88,7 @@ async fn state_by_root_pruned_from_fork_choice() { BlockStrategy::OnCanonicalHead, AttestationStrategy::AllValidators, SyncCommitteeStrategy::NoValidators, + LightClientStrategy::Disabled, ) .await; @@ -469,6 +470,7 @@ pub async fn proposer_boost_re_org_test( BlockStrategy::OnCanonicalHead, AttestationStrategy::AllValidators, SyncCommitteeStrategy::AllValidators, + LightClientStrategy::Disabled, ) .await; diff --git a/beacon_node/http_api/tests/status_tests.rs b/beacon_node/http_api/tests/status_tests.rs index 8f962995300..01731530d36 100644 --- a/beacon_node/http_api/tests/status_tests.rs +++ b/beacon_node/http_api/tests/status_tests.rs @@ -1,6 +1,6 @@ //! Tests related to the beacon node's sync status use beacon_chain::{ - test_utils::{AttestationStrategy, BlockStrategy, SyncCommitteeStrategy}, + test_utils::{AttestationStrategy, BlockStrategy, LightClientStrategy, SyncCommitteeStrategy}, BlockError, }; use eth2::StatusCode; @@ -37,6 +37,7 @@ async fn post_merge_tester(chain_depth: u64, validator_count: u64) -> Interactiv BlockStrategy::OnCanonicalHead, AttestationStrategy::AllValidators, SyncCommitteeStrategy::AllValidators, + LightClientStrategy::Disabled, ) .await; tester diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index 6e6f72b6c08..19a01a91c50 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -153,7 +153,7 @@ impl ApiTester { if !SKIPPED_SLOTS.contains(&slot) { harness - .extend_chain( + .extend_chain_with_light_client_data( 1, BlockStrategy::OnCanonicalHead, AttestationStrategy::AllValidators, @@ -1926,6 +1926,7 @@ impl ApiTester { ) .unwrap(); + assert_eq!(1, expected.len()); assert_eq!(result.clone().unwrap().len(), expected.len()); self } @@ -1933,19 +1934,26 @@ impl ApiTester { pub async fn test_get_beacon_light_client_bootstrap(self) -> Self { let block_id = BlockId(CoreBlockId::Finalized); let (block_root, _, _) = block_id.root(&self.chain).unwrap(); - let (block, _, _) = block_id.full_block(&self.chain).await.unwrap(); let result = match self .client .get_light_client_bootstrap::(block_root) .await { - Ok(result) => result.unwrap().data, + Ok(result) => result, Err(e) => panic!("query failed incorrectly: {e:?}"), }; - let expected = block.slot(); - assert_eq!(result.get_slot(), expected); + assert!(result.is_some()); + + let expected = self + .chain + .light_client_server_cache + .get_light_client_bootstrap(&self.chain.store, &block_root, 1u64, &self.chain.spec); + + assert!(expected.is_ok()); + + assert_eq!(result.unwrap().data, expected.unwrap().unwrap().0); self } diff --git a/beacon_node/network/src/network_beacon_processor/rpc_methods.rs b/beacon_node/network/src/network_beacon_processor/rpc_methods.rs index dde6f2e3130..0c98f5c17e5 100644 --- a/beacon_node/network/src/network_beacon_processor/rpc_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/rpc_methods.rs @@ -394,7 +394,7 @@ impl NetworkBeaconProcessor { Ok(Some((bootstrap, _))) => Ok(Arc::new(bootstrap)), Ok(None) => Err(( RPCResponseErrorCode::ResourceUnavailable, - "Bootstrap not available", + "Bootstrap not available".to_string(), )), Err(e) => { error!(self.log, "Error getting LightClientBootstrap instance"; @@ -404,7 +404,7 @@ impl NetworkBeaconProcessor { ); Err(( RPCResponseErrorCode::ResourceUnavailable, - "Bootstrap not available", + format!("{:?}", e), )) } }, @@ -429,7 +429,7 @@ impl NetworkBeaconProcessor { Some(update) => Ok(Arc::new(update)), None => Err(( RPCResponseErrorCode::ResourceUnavailable, - "Latest optimistic update not available", + "Latest optimistic update not available".to_string(), )), }, Response::LightClientOptimisticUpdate, @@ -453,7 +453,7 @@ impl NetworkBeaconProcessor { Some(update) => Ok(Arc::new(update)), None => Err(( RPCResponseErrorCode::ResourceUnavailable, - "Latest finality update not available", + "Latest finality update not available".to_string(), )), }, Response::LightClientFinalityUpdate, @@ -1081,7 +1081,7 @@ impl NetworkBeaconProcessor { &self, peer_id: PeerId, request_id: PeerRequestId, - result: Result, + result: Result, into_response: F, ) { match result { @@ -1096,7 +1096,7 @@ impl NetworkBeaconProcessor { }); } Err((error_code, reason)) => { - self.send_error_response(peer_id, error_code, reason.into(), request_id); + self.send_error_response(peer_id, error_code, reason, request_id); } } } diff --git a/beacon_node/store/Cargo.toml b/beacon_node/store/Cargo.toml index b26eb2bb91b..cdb18b3b9cb 100644 --- a/beacon_node/store/Cargo.toml +++ b/beacon_node/store/Cargo.toml @@ -16,6 +16,7 @@ itertools = { workspace = true } ethereum_ssz = { workspace = true } ethereum_ssz_derive = { workspace = true } types = { workspace = true } +safe_arith = { workspace = true } state_processing = { workspace = true } slog = { workspace = true } serde = { workspace = true } diff --git a/beacon_node/store/src/errors.rs b/beacon_node/store/src/errors.rs index e3b2d327b0a..c543a9c4e4a 100644 --- a/beacon_node/store/src/errors.rs +++ b/beacon_node/store/src/errors.rs @@ -59,6 +59,7 @@ pub enum Error { state_root: Hash256, slot: Slot, }, + ArithError(safe_arith::ArithError), } pub trait HandleUnavailable { @@ -129,6 +130,12 @@ impl From for Error { } } +impl From for Error { + fn from(e: safe_arith::ArithError) -> Error { + Error::ArithError(e) + } +} + #[derive(Debug)] pub struct DBError { pub message: String, diff --git a/beacon_node/store/src/hot_cold_store.rs b/beacon_node/store/src/hot_cold_store.rs index a53b697ea77..bd87cdcfee6 100644 --- a/beacon_node/store/src/hot_cold_store.rs +++ b/beacon_node/store/src/hot_cold_store.rs @@ -27,6 +27,7 @@ use itertools::process_results; use leveldb::iterator::LevelDBIterator; use lru::LruCache; use parking_lot::{Mutex, RwLock}; +use safe_arith::SafeArith; use serde::{Deserialize, Serialize}; use slog::{debug, error, info, trace, warn, Logger}; use ssz::{Decode, Encode}; @@ -36,13 +37,14 @@ use state_processing::{ SlotProcessingError, }; use std::cmp::min; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::marker::PhantomData; use std::num::NonZeroUsize; use std::path::Path; use std::sync::Arc; use std::time::Duration; use types::data_column_sidecar::{ColumnIndex, DataColumnSidecar, DataColumnSidecarList}; +use types::light_client_update::CurrentSyncCommitteeProofLen; use types::*; /// On-disk database that stores finalized states efficiently. @@ -634,6 +636,143 @@ impl, Cold: ItemStore> HotColdDB .map(|payload| payload.is_some()) } + /// Get the sync committee branch for the given block root + /// Note: we only persist sync committee branches for checkpoint slots + pub fn get_sync_committee_branch( + &self, + block_root: &Hash256, + ) -> Result>, Error> { + let column = DBColumn::SyncCommitteeBranch; + + if let Some(bytes) = self + .hot_db + .get_bytes(column.into(), &block_root.as_ssz_bytes())? + { + let sync_committee_branch: FixedVector = + FixedVector::from_ssz_bytes(&bytes)?; + return Ok(Some(sync_committee_branch)); + } + + Ok(None) + } + + /// Fetch sync committee by sync committee period + pub fn get_sync_committee( + &self, + sync_committee_period: u64, + ) -> Result>, Error> { + let column = DBColumn::SyncCommittee; + + if let Some(bytes) = self + .hot_db + .get_bytes(column.into(), &sync_committee_period.as_ssz_bytes())? + { + let sync_committee: SyncCommittee = SyncCommittee::from_ssz_bytes(&bytes)?; + return Ok(Some(sync_committee)); + } + + Ok(None) + } + + pub fn store_sync_committee_branch( + &self, + block_root: Hash256, + sync_committee_branch: &FixedVector, + ) -> Result<(), Error> { + let column = DBColumn::SyncCommitteeBranch; + self.hot_db.put_bytes( + column.into(), + &block_root.as_ssz_bytes(), + &sync_committee_branch.as_ssz_bytes(), + )?; + Ok(()) + } + + pub fn store_sync_committee( + &self, + sync_committee_period: u64, + sync_committee: &SyncCommittee, + ) -> Result<(), Error> { + let column = DBColumn::SyncCommittee; + self.hot_db.put_bytes( + column.into(), + &sync_committee_period.to_le_bytes(), + &sync_committee.as_ssz_bytes(), + )?; + + Ok(()) + } + + pub fn get_light_client_update( + &self, + sync_committee_period: u64, + ) -> Result>, Error> { + let column = DBColumn::LightClientUpdate; + let res = self + .hot_db + .get_bytes(column.into(), &sync_committee_period.to_le_bytes())?; + + if let Some(light_client_update_bytes) = res { + let epoch = sync_committee_period + .safe_mul(self.spec.epochs_per_sync_committee_period.into())?; + + let fork_name = self.spec.fork_name_at_epoch(epoch.into()); + + let light_client_update = + LightClientUpdate::from_ssz_bytes(&light_client_update_bytes, &fork_name)?; + + return Ok(Some(light_client_update)); + } + + Ok(None) + } + + pub fn get_light_client_updates( + &self, + start_period: u64, + count: u64, + ) -> Result>, Error> { + let column = DBColumn::LightClientUpdate; + let mut light_client_updates = vec![]; + for res in self + .hot_db + .iter_column_from::>(column, &start_period.to_le_bytes()) + { + let (sync_committee_bytes, light_client_update_bytes) = res?; + let sync_committee_period = u64::from_ssz_bytes(&sync_committee_bytes)?; + let epoch = sync_committee_period + .safe_mul(self.spec.epochs_per_sync_committee_period.into())?; + + let fork_name = self.spec.fork_name_at_epoch(epoch.into()); + + let light_client_update = + LightClientUpdate::from_ssz_bytes(&light_client_update_bytes, &fork_name)?; + + light_client_updates.push(light_client_update); + + if sync_committee_period >= start_period + count { + break; + } + } + Ok(light_client_updates) + } + + pub fn store_light_client_update( + &self, + sync_committee_period: u64, + light_client_update: &LightClientUpdate, + ) -> Result<(), Error> { + let column = DBColumn::LightClientUpdate; + + self.hot_db.put_bytes( + column.into(), + &sync_committee_period.to_le_bytes(), + &light_client_update.as_ssz_bytes(), + )?; + + Ok(()) + } + /// Check if the blobs for a block exists on disk. pub fn blobs_exist(&self, block_root: &Hash256) -> Result { self.blobs_db @@ -1037,6 +1176,14 @@ impl, Cold: ItemStore> HotColdDB key_value_batch.push(KeyValueStoreOp::DeleteKey(key)); } + StoreOp::DeleteSyncCommitteeBranch(block_root) => { + let key = get_key_for_col( + DBColumn::SyncCommitteeBranch.into(), + block_root.as_slice(), + ); + key_value_batch.push(KeyValueStoreOp::DeleteKey(key)); + } + StoreOp::KeyValueOp(kv_op) => { key_value_batch.push(kv_op); } @@ -1182,6 +1329,8 @@ impl, Cold: ItemStore> HotColdDB StoreOp::DeleteExecutionPayload(_) => (), + StoreOp::DeleteSyncCommitteeBranch(_) => (), + StoreOp::KeyValueOp(_) => (), } } @@ -2816,12 +2965,16 @@ pub fn migrate_database, Cold: ItemStore>( .into()); } + // finalized_state.slot() must be at an epoch boundary + // else we may introduce bugs to the migration/pruning logic if finalized_state.slot() % E::slots_per_epoch() != 0 { return Err(HotColdDBError::FreezeSlotUnaligned(finalized_state.slot()).into()); } let mut hot_db_ops = vec![]; let mut cold_db_ops = vec![]; + let mut epoch_boundary_blocks = HashSet::new(); + let mut non_checkpoint_block_roots = HashSet::new(); // Chunk writer for the linear block roots in the freezer DB. // Start at the new upper limit because we iterate backwards. @@ -2849,6 +3002,22 @@ pub fn migrate_database, Cold: ItemStore>( hot_db_ops.push(StoreOp::DeleteExecutionPayload(block_root)); } + // At a missed slot, `state_root_iter` will return the block root + // from the previous non-missed slot. This ensures that the block root at an + // epoch boundary is always a checkpoint block root. We keep track of block roots + // at epoch boundaries by storing them in the `epoch_boundary_blocks` hash set. + // We then ensure that block roots at the epoch boundary aren't included in the + // `non_checkpoint_block_roots` hash set. + if slot % E::slots_per_epoch() == 0 { + epoch_boundary_blocks.insert(block_root); + } else { + non_checkpoint_block_roots.insert(block_root); + } + + if epoch_boundary_blocks.contains(&block_root) { + non_checkpoint_block_roots.remove(&block_root); + } + // Delete the old summary, and the full state if we lie on an epoch boundary. hot_db_ops.push(StoreOp::DeleteState(state_root, Some(slot))); @@ -2888,6 +3057,19 @@ pub fn migrate_database, Cold: ItemStore>( } } + // Prune sync committee branch data for all non checkpoint block roots. + // Note that `non_checkpoint_block_roots` should only contain non checkpoint block roots + // as long as `finalized_state.slot()` is at an epoch boundary. If this were not the case + // we risk the chance of pruning a `sync_committee_branch` for a checkpoint block root. + // E.g. if `current_split_slot` = (Epoch A slot 0) and `finalized_state.slot()` = (Epoch C slot 31) + // and (Epoch D slot 0) is a skipped slot, we will have pruned a `sync_committee_branch` + // for a checkpoint block root. + non_checkpoint_block_roots + .into_iter() + .for_each(|block_root| { + hot_db_ops.push(StoreOp::DeleteSyncCommitteeBranch(block_root)); + }); + // Finish writing the block roots and commit the remaining cold DB ops. block_root_writer.write(&mut cold_db_ops)?; store.cold_db.do_atomically(cold_db_ops)?; @@ -2904,7 +3086,6 @@ pub fn migrate_database, Cold: ItemStore>( // Flush to disk all the states that have just been migrated to the cold store. store.cold_db.sync()?; - { let mut split_guard = store.split.write(); let latest_split_slot = split_guard.slot; diff --git a/beacon_node/store/src/lib.rs b/beacon_node/store/src/lib.rs index 60dddeb1760..1d02bfbb3cc 100644 --- a/beacon_node/store/src/lib.rs +++ b/beacon_node/store/src/lib.rs @@ -241,6 +241,7 @@ pub enum StoreOp<'a, E: EthSpec> { DeleteDataColumns(Hash256, Vec), DeleteState(Hash256, Option), DeleteExecutionPayload(Hash256), + DeleteSyncCommitteeBranch(Hash256), KeyValueOp(KeyValueStoreOp), } @@ -303,6 +304,12 @@ pub enum DBColumn { /// For persisting eagerly computed light client data #[strum(serialize = "lcu")] LightClientUpdate, + /// For helping persist eagerly computed light client bootstrap data + #[strum(serialize = "scb")] + SyncCommitteeBranch, + /// For helping persist eagerly computed light client bootstrap data + #[strum(serialize = "scm")] + SyncCommittee, } /// A block from the database, which might have an execution payload or not. @@ -346,6 +353,8 @@ impl DBColumn { | Self::BeaconHistoricalRoots | Self::BeaconHistoricalSummaries | Self::BeaconRandaoMixes + | Self::SyncCommittee + | Self::SyncCommitteeBranch | Self::LightClientUpdate => 8, Self::BeaconDataColumn => DATA_COLUMN_DB_KEY_SIZE, } diff --git a/consensus/types/src/lib.rs b/consensus/types/src/lib.rs index afc64e86a82..281a84d8592 100644 --- a/consensus/types/src/lib.rs +++ b/consensus/types/src/lib.rs @@ -197,7 +197,7 @@ pub use crate::light_client_optimistic_update::{ LightClientOptimisticUpdateElectra, }; pub use crate::light_client_update::{ - Error as LightClientError, LightClientUpdate, LightClientUpdateAltair, + Error as LightClientUpdateError, LightClientUpdate, LightClientUpdateAltair, LightClientUpdateCapella, LightClientUpdateDeneb, LightClientUpdateElectra, }; pub use crate::participation_flags::ParticipationFlags; diff --git a/consensus/types/src/light_client_bootstrap.rs b/consensus/types/src/light_client_bootstrap.rs index f06a94adce9..7c716e6bb2d 100644 --- a/consensus/types/src/light_client_bootstrap.rs +++ b/consensus/types/src/light_client_bootstrap.rs @@ -112,6 +112,42 @@ impl LightClientBootstrap { fixed_len + LightClientHeader::::ssz_max_var_len_for_fork(fork_name) } + pub fn new( + block: &SignedBlindedBeaconBlock, + current_sync_committee: Arc>, + current_sync_committee_branch: FixedVector, + chain_spec: &ChainSpec, + ) -> Result { + let light_client_bootstrap = match block + .fork_name(chain_spec) + .map_err(|_| Error::InconsistentFork)? + { + ForkName::Base => return Err(Error::AltairForkNotActive), + ForkName::Altair | ForkName::Bellatrix => Self::Altair(LightClientBootstrapAltair { + header: LightClientHeaderAltair::block_to_light_client_header(block)?, + current_sync_committee, + current_sync_committee_branch, + }), + ForkName::Capella => Self::Capella(LightClientBootstrapCapella { + header: LightClientHeaderCapella::block_to_light_client_header(block)?, + current_sync_committee, + current_sync_committee_branch, + }), + ForkName::Deneb => Self::Deneb(LightClientBootstrapDeneb { + header: LightClientHeaderDeneb::block_to_light_client_header(block)?, + current_sync_committee, + current_sync_committee_branch, + }), + ForkName::Electra => Self::Electra(LightClientBootstrapElectra { + header: LightClientHeaderElectra::block_to_light_client_header(block)?, + current_sync_committee, + current_sync_committee_branch, + }), + }; + + Ok(light_client_bootstrap) + } + pub fn from_beacon_state( beacon_state: &mut BeaconState, block: &SignedBlindedBeaconBlock,