referrerpolicy=no-referrer-when-downgrade

snowbridge_pallet_ethereum_client/
lib.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
3//! Ethereum Beacon Client
4//!
5//! A light client that verifies consensus updates signed by the sync committee of the beacon chain.
6//!
7//! # Extrinsics
8//!
9//! ## Governance
10//!
11//! * [`Call::force_checkpoint`]: Set the initial trusted consensus checkpoint.
12//! * [`Call::set_operating_mode`]: Set the operating mode of the pallet. Can be used to disable
13//!   processing of consensus updates.
14//!
15//! ## Consensus Updates
16//!
17//! * [`Call::submit`]: Submit a finalized beacon header with an optional sync committee update
18#![cfg_attr(not(feature = "std"), no_std)]
19
20pub mod config;
21pub mod functions;
22pub mod impls;
23pub mod types;
24pub mod weights;
25
26#[cfg(any(test, feature = "fuzzing"))]
27pub mod mock;
28
29#[cfg(test)]
30mod tests;
31
32#[cfg(feature = "runtime-benchmarks")]
33mod benchmarking;
34
35use frame_support::{
36	dispatch::{DispatchResult, PostDispatchInfo},
37	pallet_prelude::OptionQuery,
38	traits::Get,
39	transactional,
40};
41use frame_system::ensure_signed;
42use snowbridge_beacon_primitives::{
43	fast_aggregate_verify,
44	merkle_proof::{generalized_index_length, subtree_index},
45	verify_merkle_branch, BeaconHeader, BlsError, CommitmentError, CommitmentScheme,
46	CompactBeaconState, ExecutionCommitment, ForkData, ForkVersion, ForkVersions,
47	PublicKeyPrepared, SigningData, VersionedExecutionPayloadHeader,
48};
49use snowbridge_core::{BasicOperatingMode, RingBufferMap};
50use sp_core::H256;
51use sp_std::prelude::*;
52pub use weights::WeightInfo;
53
54use functions::{
55	compute_epoch, compute_period, decompress_sync_committee_bits, sync_committee_sum,
56};
57use types::{CheckpointUpdate, FinalizedBeaconStateBuffer, SyncCommitteePrepared, Update};
58
59pub use pallet::*;
60
61pub use config::SLOTS_PER_HISTORICAL_ROOT;
62
63pub const LOG_TARGET: &str = "ethereum-client";
64
65#[frame_support::pallet]
66pub mod pallet {
67	use super::*;
68
69	use frame_support::pallet_prelude::*;
70	use frame_system::pallet_prelude::*;
71
72	#[derive(scale_info::TypeInfo, codec::Encode, codec::Decode, codec::MaxEncodedLen)]
73	#[codec(mel_bound(T: Config))]
74	#[scale_info(skip_type_params(T))]
75	pub struct MaxFinalizedHeadersToKeep<T: Config>(PhantomData<T>);
76	impl<T: Config> Get<u32> for MaxFinalizedHeadersToKeep<T> {
77		fn get() -> u32 {
78			const MAX_REDUNDANCY: u32 = 20;
79			config::EPOCHS_PER_SYNC_COMMITTEE_PERIOD as u32 * MAX_REDUNDANCY
80		}
81	}
82
83	#[pallet::pallet]
84	pub struct Pallet<T>(_);
85
86	#[pallet::config]
87	pub trait Config: frame_system::Config {
88		#[allow(deprecated)]
89		type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
90		#[pallet::constant]
91		type ForkVersions: Get<ForkVersions>;
92		/// Minimum gap between finalized headers for an update to be free.
93		#[pallet::constant]
94		type FreeHeadersInterval: Get<u32>;
95		type WeightInfo: WeightInfo;
96	}
97
98	#[pallet::event]
99	#[pallet::generate_deposit(pub(super) fn deposit_event)]
100	pub enum Event<T: Config> {
101		BeaconHeaderImported {
102			block_hash: H256,
103			slot: u64,
104		},
105		SyncCommitteeUpdated {
106			period: u64,
107		},
108		/// Set OperatingMode
109		OperatingModeChanged {
110			mode: BasicOperatingMode,
111		},
112	}
113
114	#[pallet::error]
115	pub enum Error<T> {
116		SkippedSyncCommitteePeriod,
117		SyncCommitteeUpdateRequired,
118		/// Attested header is older than latest finalized header.
119		IrrelevantUpdate,
120		NotBootstrapped,
121		SyncCommitteeParticipantsNotSupermajority,
122		InvalidHeaderMerkleProof,
123		InvalidSyncCommitteeMerkleProof,
124		InvalidExecutionHeaderProof,
125		InvalidAncestryMerkleProof,
126		InvalidBlockRootsRootMerkleProof,
127		/// The gap between the finalized headers is larger than the sync committee period,
128		/// rendering execution headers unprovable using ancestry proofs (blocks root size is
129		/// the same as the sync committee period slots).
130		InvalidFinalizedHeaderGap,
131		HeaderNotFinalized,
132		BlockBodyHashTreeRootFailed,
133		HeaderHashTreeRootFailed,
134		SyncCommitteeHashTreeRootFailed,
135		SigningRootHashTreeRootFailed,
136		ForkDataHashTreeRootFailed,
137		ExpectedFinalizedHeaderNotStored,
138		BLSPreparePublicKeysFailed,
139		BLSVerificationFailed(BlsError),
140		InvalidUpdateSlot,
141		/// The given update is not in the expected period, or the given next sync committee does
142		/// not match the next sync committee in storage.
143		InvalidSyncCommitteeUpdate,
144		ExecutionHeaderTooFarBehind,
145		ExecutionHeaderSkippedBlock,
146		Halted,
147		/// The submitted Gloas execution header is not one canonical RLP header.
148		MalformedExecutionHeader,
149		/// The proof's commitment scheme does not match the fork era of the beacon header's slot.
150		ExecutionHeaderEraMismatch,
151	}
152
153	/// Latest imported checkpoint root
154	#[pallet::storage]
155	#[pallet::getter(fn initial_checkpoint_root)]
156	pub type InitialCheckpointRoot<T: Config> = StorageValue<_, H256, ValueQuery>;
157
158	/// Latest imported finalized block root
159	#[pallet::storage]
160	#[pallet::getter(fn latest_finalized_block_root)]
161	pub type LatestFinalizedBlockRoot<T: Config> = StorageValue<_, H256, ValueQuery>;
162
163	/// Beacon state by finalized block root
164	#[pallet::storage]
165	#[pallet::getter(fn finalized_beacon_state)]
166	pub type FinalizedBeaconState<T: Config> =
167		StorageMap<_, Identity, H256, CompactBeaconState, OptionQuery>;
168
169	/// Finalized Headers: Current position in ring buffer
170	#[pallet::storage]
171	pub type FinalizedBeaconStateIndex<T: Config> = StorageValue<_, u32, ValueQuery>;
172
173	/// Finalized Headers: Mapping of ring buffer index to a pruning candidate
174	#[pallet::storage]
175	pub type FinalizedBeaconStateMapping<T: Config> =
176		StorageMap<_, Identity, u32, H256, ValueQuery>;
177
178	#[pallet::storage]
179	#[pallet::getter(fn validators_root)]
180	pub type ValidatorsRoot<T: Config> = StorageValue<_, H256, ValueQuery>;
181
182	/// Sync committee for current period
183	#[pallet::storage]
184	pub type CurrentSyncCommittee<T: Config> = StorageValue<_, SyncCommitteePrepared, ValueQuery>;
185
186	/// Sync committee for next period
187	#[pallet::storage]
188	pub type NextSyncCommittee<T: Config> = StorageValue<_, SyncCommitteePrepared, ValueQuery>;
189
190	/// The last period where the next sync committee was updated for free.
191	#[pallet::storage]
192	pub type LatestSyncCommitteeUpdatePeriod<T: Config> = StorageValue<_, u64, ValueQuery>;
193
194	/// The current operating mode of the pallet.
195	#[pallet::storage]
196	#[pallet::getter(fn operating_mode)]
197	pub type OperatingMode<T: Config> = StorageValue<_, BasicOperatingMode, ValueQuery>;
198
199	#[pallet::call]
200	impl<T: Config> Pallet<T> {
201		#[pallet::call_index(0)]
202		#[pallet::weight(T::WeightInfo::force_checkpoint())]
203		#[transactional]
204		/// Used for pallet initialization and light client resetting. Needs to be called by
205		/// the root origin.
206		pub fn force_checkpoint(
207			origin: OriginFor<T>,
208			update: Box<CheckpointUpdate>,
209		) -> DispatchResult {
210			ensure_root(origin)?;
211			Self::process_checkpoint_update(&update)?;
212			Ok(())
213		}
214
215		#[pallet::call_index(1)]
216		#[pallet::weight({
217			match update.next_sync_committee_update {
218				None => T::WeightInfo::submit(),
219				Some(_) => T::WeightInfo::submit_with_sync_committee(),
220			}
221		})]
222		#[transactional]
223		/// Submits a new finalized beacon header update. The update may contain the next
224		/// sync committee.
225		pub fn submit(origin: OriginFor<T>, update: Box<Update>) -> DispatchResultWithPostInfo {
226			ensure_signed(origin)?;
227			ensure!(!Self::operating_mode().is_halted(), Error::<T>::Halted);
228			Self::process_update(&update)
229		}
230
231		/// Halt or resume all pallet operations. May only be called by root.
232		#[pallet::call_index(3)]
233		#[pallet::weight((T::DbWeight::get().reads_writes(1, 1), DispatchClass::Operational))]
234		pub fn set_operating_mode(
235			origin: OriginFor<T>,
236			mode: BasicOperatingMode,
237		) -> DispatchResult {
238			ensure_root(origin)?;
239			OperatingMode::<T>::set(mode);
240			Self::deposit_event(Event::OperatingModeChanged { mode });
241			Ok(())
242		}
243	}
244
245	impl<T: Config> Pallet<T> {
246		/// Forces a finalized beacon header checkpoint update. The current sync committee,
247		/// with a header attesting to the current sync committee, should be provided.
248		/// An `block_roots` proof should also be provided. This is used for ancestry proofs
249		/// for execution header updates.
250		pub(crate) fn process_checkpoint_update(update: &CheckpointUpdate) -> DispatchResult {
251			let sync_committee_root = update
252				.current_sync_committee
253				.hash_tree_root()
254				.map_err(|_| Error::<T>::SyncCommitteeHashTreeRootFailed)?;
255
256			let fork_versions = T::ForkVersions::get();
257			let sync_committee_gindex = Self::current_sync_committee_gindex_at_slot(
258				update.header.slot,
259				fork_versions.clone(),
260			);
261			// Verifies the sync committee in the Beacon state.
262			ensure!(
263				verify_merkle_branch(
264					sync_committee_root,
265					&update.current_sync_committee_branch,
266					subtree_index(sync_committee_gindex),
267					generalized_index_length(sync_committee_gindex),
268					update.header.state_root
269				),
270				Error::<T>::InvalidSyncCommitteeMerkleProof
271			);
272
273			let header_root: H256 = update
274				.header
275				.hash_tree_root()
276				.map_err(|_| Error::<T>::HeaderHashTreeRootFailed)?;
277
278			// This is used for ancestry proofs in ExecutionHeader updates. This verifies the
279			// BeaconState: the beacon state root is the tree root; the `block_roots` hash is the
280			// tree leaf.
281			let block_roots_gindex =
282				Self::block_roots_gindex_at_slot(update.header.slot, fork_versions);
283			ensure!(
284				verify_merkle_branch(
285					update.block_roots_root,
286					&update.block_roots_branch,
287					subtree_index(block_roots_gindex),
288					generalized_index_length(block_roots_gindex),
289					update.header.state_root
290				),
291				Error::<T>::InvalidBlockRootsRootMerkleProof
292			);
293
294			let sync_committee_prepared: SyncCommitteePrepared = (&update.current_sync_committee)
295				.try_into()
296				.map_err(|_| <Error<T>>::BLSPreparePublicKeysFailed)?;
297			<CurrentSyncCommittee<T>>::set(sync_committee_prepared);
298			<NextSyncCommittee<T>>::kill();
299			InitialCheckpointRoot::<T>::set(header_root);
300
301			Self::store_validators_root(update.validators_root);
302			Self::store_finalized_header(update.header, update.block_roots_root)?;
303
304			Ok(())
305		}
306
307		pub(crate) fn process_update(update: &Update) -> DispatchResultWithPostInfo {
308			Self::verify_update(update)?;
309			Self::apply_update(update)
310		}
311
312		/// References and strictly follows <https://github.com/ethereum/consensus-specs/blob/ec42646/specs/altair/light-client/sync-protocol.md#validate_light_client_update>
313		/// Verifies that provided next sync committee is valid through a series of checks
314		/// (including checking that a sync committee period isn't skipped and that the header is
315		/// signed by the current sync committee.
316		fn verify_update(update: &Update) -> DispatchResult {
317			// Verify sync committee has sufficient participants.
318			let participation =
319				decompress_sync_committee_bits(update.sync_aggregate.sync_committee_bits);
320			Self::sync_committee_participation_is_supermajority(&participation)?;
321
322			// Verify update does not skip a sync committee period.
323			ensure!(
324				update.signature_slot > update.attested_header.slot &&
325					update.attested_header.slot >= update.finalized_header.slot,
326				Error::<T>::InvalidUpdateSlot
327			);
328			// Retrieve latest finalized state.
329			let latest_finalized_state =
330				FinalizedBeaconState::<T>::get(LatestFinalizedBlockRoot::<T>::get())
331					.ok_or(Error::<T>::NotBootstrapped)?;
332			let store_period = compute_period(latest_finalized_state.slot);
333			let signature_period = compute_period(update.signature_slot);
334			if <NextSyncCommittee<T>>::exists() {
335				ensure!(
336					(store_period..=store_period + 1).contains(&signature_period),
337					Error::<T>::SkippedSyncCommitteePeriod
338				)
339			} else {
340				ensure!(signature_period == store_period, Error::<T>::SkippedSyncCommitteePeriod)
341			}
342
343			// Verify update is relevant.
344			let update_attested_period = compute_period(update.attested_header.slot);
345			let update_finalized_period = compute_period(update.finalized_header.slot);
346			let update_has_next_sync_committee = !<NextSyncCommittee<T>>::exists() &&
347				(update.next_sync_committee_update.is_some() &&
348					update_attested_period == store_period);
349			ensure!(
350				update.attested_header.slot > latest_finalized_state.slot ||
351					update_has_next_sync_committee,
352				Error::<T>::IrrelevantUpdate
353			);
354
355			// Verify the finalized header gap between the current finalized header and new imported
356			// header is not larger than the sync committee period, otherwise we cannot do
357			// ancestry proofs for execution headers in the gap.
358			ensure!(
359				latest_finalized_state
360					.slot
361					.saturating_add(config::SLOTS_PER_HISTORICAL_ROOT as u64) >=
362					update.finalized_header.slot,
363				Error::<T>::InvalidFinalizedHeaderGap
364			);
365
366			let fork_versions = T::ForkVersions::get();
367			let finalized_root_gindex = Self::finalized_root_gindex_at_slot(
368				update.attested_header.slot,
369				fork_versions.clone(),
370			);
371			// Verify that the `finality_branch`, if present, confirms `finalized_header` to match
372			// the finalized checkpoint root saved in the state of `attested_header`.
373			let finalized_block_root: H256 = update
374				.finalized_header
375				.hash_tree_root()
376				.map_err(|_| Error::<T>::HeaderHashTreeRootFailed)?;
377			ensure!(
378				verify_merkle_branch(
379					finalized_block_root,
380					&update.finality_branch,
381					subtree_index(finalized_root_gindex),
382					generalized_index_length(finalized_root_gindex),
383					update.attested_header.state_root
384				),
385				Error::<T>::InvalidHeaderMerkleProof
386			);
387
388			// Though following check does not belong to ALC spec we verify block_roots_root to
389			// match the finalized checkpoint root saved in the state of `finalized_header` so to
390			// cache it for later use in `verify_ancestry_proof`.
391			let block_roots_gindex = Self::block_roots_gindex_at_slot(
392				update.finalized_header.slot,
393				fork_versions.clone(),
394			);
395			ensure!(
396				verify_merkle_branch(
397					update.block_roots_root,
398					&update.block_roots_branch,
399					subtree_index(block_roots_gindex),
400					generalized_index_length(block_roots_gindex),
401					update.finalized_header.state_root
402				),
403				Error::<T>::InvalidBlockRootsRootMerkleProof
404			);
405
406			// Verify that the `next_sync_committee`, if present, actually is the next sync
407			// committee saved in the state of the `attested_header`.
408			if let Some(next_sync_committee_update) = &update.next_sync_committee_update {
409				let sync_committee_root = next_sync_committee_update
410					.next_sync_committee
411					.hash_tree_root()
412					.map_err(|_| Error::<T>::SyncCommitteeHashTreeRootFailed)?;
413				if update_attested_period == store_period && <NextSyncCommittee<T>>::exists() {
414					let next_committee_root = <NextSyncCommittee<T>>::get().root;
415					ensure!(
416						sync_committee_root == next_committee_root,
417						Error::<T>::InvalidSyncCommitteeUpdate
418					);
419				}
420				let next_sync_committee_gindex = Self::next_sync_committee_gindex_at_slot(
421					update.attested_header.slot,
422					fork_versions,
423				);
424				ensure!(
425					verify_merkle_branch(
426						sync_committee_root,
427						&next_sync_committee_update.next_sync_committee_branch,
428						subtree_index(next_sync_committee_gindex),
429						generalized_index_length(next_sync_committee_gindex),
430						update.attested_header.state_root
431					),
432					Error::<T>::InvalidSyncCommitteeMerkleProof
433				);
434			} else {
435				ensure!(
436					update_finalized_period == store_period,
437					Error::<T>::SyncCommitteeUpdateRequired
438				);
439			}
440
441			// Verify sync committee aggregate signature.
442			let sync_committee = if signature_period == store_period {
443				<CurrentSyncCommittee<T>>::get()
444			} else {
445				<NextSyncCommittee<T>>::get()
446			};
447			let absent_pubkeys =
448				Self::find_pubkeys(&participation, (*sync_committee.pubkeys).as_ref(), false);
449			let signing_root = Self::signing_root(
450				&update.attested_header,
451				Self::validators_root(),
452				update.signature_slot,
453			)?;
454			// Improvement here per <https://eth2book.info/capella/part2/building_blocks/signatures/#sync-aggregates>
455			// suggested start from the full set aggregate_pubkey then subtracting the absolute
456			// minority that did not participate.
457			fast_aggregate_verify(
458				&sync_committee.aggregate_pubkey,
459				&absent_pubkeys,
460				signing_root,
461				&update.sync_aggregate.sync_committee_signature,
462			)
463			.map_err(|e| Error::<T>::BLSVerificationFailed(e))?;
464
465			Ok(())
466		}
467
468		/// Reference and strictly follows <https://github.com/ethereum/consensus-specs/blob/ec42646/specs/altair/light-client/sync-protocol.md#apply_light_client_update>
469		/// Applies a finalized beacon header update to the beacon client. If a next sync committee
470		/// is present in the update, verify the sync committee by converting it to a
471		/// SyncCommitteePrepared type. Stores the provided finalized header. Updates are free
472		/// if the certain conditions specified in `check_refundable` are met.
473		fn apply_update(update: &Update) -> DispatchResultWithPostInfo {
474			let latest_finalized_state =
475				FinalizedBeaconState::<T>::get(LatestFinalizedBlockRoot::<T>::get())
476					.ok_or(Error::<T>::NotBootstrapped)?;
477
478			let pays_fee = Self::check_refundable(update, latest_finalized_state.slot);
479			let actual_weight = match update.next_sync_committee_update {
480				None => T::WeightInfo::submit(),
481				Some(_) => T::WeightInfo::submit_with_sync_committee(),
482			};
483
484			if let Some(next_sync_committee_update) = &update.next_sync_committee_update {
485				let store_period = compute_period(latest_finalized_state.slot);
486				let update_finalized_period = compute_period(update.finalized_header.slot);
487				let sync_committee_prepared: SyncCommitteePrepared = (&next_sync_committee_update
488					.next_sync_committee)
489					.try_into()
490					.map_err(|_| <Error<T>>::BLSPreparePublicKeysFailed)?;
491
492				if !<NextSyncCommittee<T>>::exists() {
493					ensure!(
494						update_finalized_period == store_period,
495						<Error<T>>::InvalidSyncCommitteeUpdate
496					);
497					<NextSyncCommittee<T>>::set(sync_committee_prepared);
498				} else if update_finalized_period == store_period + 1 {
499					<CurrentSyncCommittee<T>>::set(<NextSyncCommittee<T>>::get());
500					<NextSyncCommittee<T>>::set(sync_committee_prepared);
501				}
502				tracing::info!(
503					target: LOG_TARGET,
504					period=%update_finalized_period,
505					"💫 SyncCommitteeUpdated."
506				);
507				<LatestSyncCommitteeUpdatePeriod<T>>::set(update_finalized_period);
508				Self::deposit_event(Event::SyncCommitteeUpdated {
509					period: update_finalized_period,
510				});
511			};
512
513			if update.finalized_header.slot > latest_finalized_state.slot {
514				Self::store_finalized_header(update.finalized_header, update.block_roots_root)?;
515			}
516
517			Ok(PostDispatchInfo { actual_weight: Some(actual_weight), pays_fee })
518		}
519
520		/// Computes the signing root for a given beacon header and domain. The hash tree root
521		/// of the beacon header is computed, and then the combination of the beacon header hash
522		/// and the domain makes up the signing root.
523		pub(super) fn compute_signing_root(
524			beacon_header: &BeaconHeader,
525			domain: H256,
526		) -> Result<H256, DispatchError> {
527			let beacon_header_root = beacon_header
528				.hash_tree_root()
529				.map_err(|_| Error::<T>::HeaderHashTreeRootFailed)?;
530
531			let hash_root = SigningData { object_root: beacon_header_root, domain }
532				.hash_tree_root()
533				.map_err(|_| Error::<T>::SigningRootHashTreeRootFailed)?;
534
535			Ok(hash_root)
536		}
537
538		/// Stores a compacted (slot and block roots root (hash of the `block_roots` beacon state
539		/// field, used for ancestry proof)) beacon state in a ring buffer map, with the header root
540		/// as map key.
541		pub fn store_finalized_header(
542			header: BeaconHeader,
543			block_roots_root: H256,
544		) -> DispatchResult {
545			let slot = header.slot;
546
547			let header_root: H256 =
548				header.hash_tree_root().map_err(|_| Error::<T>::HeaderHashTreeRootFailed)?;
549
550			<FinalizedBeaconStateBuffer<T>>::insert(
551				header_root,
552				CompactBeaconState { slot: header.slot, block_roots_root },
553			);
554			<LatestFinalizedBlockRoot<T>>::set(header_root);
555
556			tracing::info!(
557				target: LOG_TARGET,
558				root=%header_root,
559				%slot,
560				"💫 Updated latest finalized block."
561			);
562
563			Self::deposit_event(Event::BeaconHeaderImported { block_hash: header_root, slot });
564
565			Ok(())
566		}
567
568		/// Stores the validators root in storage. Validators root is the hash tree root of all the
569		/// validators at genesis and is used to used to identify the chain that we are on
570		/// (used in conjunction with the fork version).
571		/// <https://eth2book.info/capella/part3/containers/state/#genesis_validators_root>
572		fn store_validators_root(validators_root: H256) {
573			<ValidatorsRoot<T>>::set(validators_root);
574		}
575
576		/// Returns the domain for the domain_type and fork_version. The domain is used to
577		/// distinguish between the different players in the chain (see DomainTypes
578		/// <https://eth2book.info/capella/part3/config/constants/#domain-types>) and to ensure we are
579		/// addressing the correct chain.
580		/// <https://eth2book.info/capella/part3/helper/misc/#compute_domain>
581		pub(super) fn compute_domain(
582			domain_type: Vec<u8>,
583			fork_version: ForkVersion,
584			genesis_validators_root: H256,
585		) -> Result<H256, DispatchError> {
586			let fork_data_root =
587				Self::compute_fork_data_root(fork_version, genesis_validators_root)?;
588
589			let mut domain = [0u8; 32];
590			domain[0..4].copy_from_slice(&(domain_type));
591			domain[4..32].copy_from_slice(&(fork_data_root.0[..28]));
592
593			Ok(domain.into())
594		}
595
596		/// Computes the fork data root. The fork data root is a merkleization of the current
597		/// fork version and the genesis validators root.
598		fn compute_fork_data_root(
599			current_version: ForkVersion,
600			genesis_validators_root: H256,
601		) -> Result<H256, DispatchError> {
602			let hash_root = ForkData {
603				current_version,
604				genesis_validators_root: genesis_validators_root.into(),
605			}
606			.hash_tree_root()
607			.map_err(|_| Error::<T>::ForkDataHashTreeRootFailed)?;
608
609			Ok(hash_root)
610		}
611
612		/// Checks that the sync committee bits (the votes of the sync committee members,
613		/// represented by bits 0 and 1) is more than a supermajority (2/3 of the votes are
614		/// positive).
615		pub(super) fn sync_committee_participation_is_supermajority(
616			sync_committee_bits: &[u8],
617		) -> DispatchResult {
618			let sync_committee_sum = sync_committee_sum(sync_committee_bits);
619			ensure!(
620				((sync_committee_sum * 3) as usize) >= sync_committee_bits.len() * 2,
621				Error::<T>::SyncCommitteeParticipantsNotSupermajority
622			);
623
624			Ok(())
625		}
626
627		/// Returns the fork version based on the current epoch. The hard fork versions
628		/// are defined in pallet config.
629		pub(super) fn compute_fork_version(epoch: u64) -> ForkVersion {
630			Self::select_fork_version(&T::ForkVersions::get(), epoch)
631		}
632
633		/// Returns the fork version based on the current epoch.
634		pub(super) fn select_fork_version(fork_versions: &ForkVersions, epoch: u64) -> ForkVersion {
635			if epoch >= fork_versions.gloas.epoch {
636				return fork_versions.gloas.version;
637			}
638			if epoch >= fork_versions.fulu.epoch {
639				return fork_versions.fulu.version;
640			}
641			if epoch >= fork_versions.electra.epoch {
642				return fork_versions.electra.version;
643			}
644			if epoch >= fork_versions.deneb.epoch {
645				return fork_versions.deneb.version;
646			}
647			if epoch >= fork_versions.capella.epoch {
648				return fork_versions.capella.version;
649			}
650			if epoch >= fork_versions.bellatrix.epoch {
651				return fork_versions.bellatrix.version;
652			}
653			if epoch >= fork_versions.altair.epoch {
654				return fork_versions.altair.version;
655			}
656			fork_versions.genesis.version
657		}
658
659		/// Returns a vector of public keys that participated in the sync committee block signage.
660		/// Sync committee bits is an array of 0s and 1s, 0 meaning the corresponding sync committee
661		/// member did not participate in the vote, 1 meaning they participated.
662		/// This method can find the absent or participating members, based on the participant
663		/// parameter. participant = false will return absent participants, participant = true will
664		/// return participating members.
665		pub fn find_pubkeys(
666			sync_committee_bits: &[u8],
667			sync_committee_pubkeys: &[PublicKeyPrepared],
668			participant: bool,
669		) -> Vec<PublicKeyPrepared> {
670			let mut pubkeys: Vec<PublicKeyPrepared> = Vec::new();
671			for (bit, pubkey) in sync_committee_bits.iter().zip(sync_committee_pubkeys.iter()) {
672				if *bit == u8::from(participant) {
673					pubkeys.push(pubkey.clone());
674				}
675			}
676			pubkeys
677		}
678
679		/// Calculates signing root for BeaconHeader. The signing root is used for the message
680		/// value in BLS signature verification.
681		pub fn signing_root(
682			header: &BeaconHeader,
683			validators_root: H256,
684			signature_slot: u64,
685		) -> Result<H256, DispatchError> {
686			// Per Ethereum Altair light-client spec, fork version is derived from signature_slot -
687			// 1. See: https://github.com/ethereum/consensus-specs/blob/ec42646/specs/altair/light-client/sync-protocol.md#validate_light_client_update
688			// In validate_light_client_update: fork_version_slot = max(signature_slot, 1) - 1
689			let fork_version = Self::compute_fork_version(compute_epoch(
690				signature_slot.saturating_sub(1),
691				config::SLOTS_PER_EPOCH as u64,
692			));
693			let domain_type = config::DOMAIN_SYNC_COMMITTEE.to_vec();
694			// Domains are used for seeds, for signatures, and for selecting aggregators.
695			let domain = Self::compute_domain(domain_type, fork_version, validators_root)?;
696			// Hash tree root of SigningData - object root + domain
697			let signing_root = Self::compute_signing_root(header, domain)?;
698			Ok(signing_root)
699		}
700
701		/// Updates are free if the update is successful and the interval between the latest
702		/// finalized header in storage and the newly imported header is large enough. All
703		/// successful sync committee updates are free.
704		pub(super) fn check_refundable(update: &Update, latest_slot: u64) -> Pays {
705			// If the sync committee was successfully updated, the update may be free.
706			let update_period = compute_period(update.finalized_header.slot);
707			let latest_free_update_period = LatestSyncCommitteeUpdatePeriod::<T>::get();
708			// If the next sync committee is not known and this update sets it, the update is free.
709			// If the sync committee update is in a period that we have not received an update for,
710			// the update is free.
711			let refundable =
712				!<NextSyncCommittee<T>>::exists() || update_period > latest_free_update_period;
713			if update.next_sync_committee_update.is_some() && refundable {
714				return Pays::No;
715			}
716
717			// If the latest finalized header is larger than the minimum slot interval, the header
718			// import transaction is free.
719			if update.finalized_header.slot >=
720				latest_slot.saturating_add(T::FreeHeadersInterval::get() as u64)
721			{
722				return Pays::No;
723			}
724
725			Pays::Yes
726		}
727
728		pub fn finalized_root_gindex_at_slot(slot: u64, fork_versions: ForkVersions) -> usize {
729			let epoch = compute_epoch(slot, config::SLOTS_PER_EPOCH as u64);
730
731			if epoch >= fork_versions.gloas.epoch {
732				return config::gloas::FINALIZED_ROOT_INDEX;
733			}
734
735			if epoch >= fork_versions.electra.epoch {
736				return config::electra::FINALIZED_ROOT_INDEX;
737			}
738
739			config::altair::FINALIZED_ROOT_INDEX
740		}
741
742		pub fn current_sync_committee_gindex_at_slot(
743			slot: u64,
744			fork_versions: ForkVersions,
745		) -> usize {
746			let epoch = compute_epoch(slot, config::SLOTS_PER_EPOCH as u64);
747
748			if epoch >= fork_versions.gloas.epoch {
749				return config::gloas::CURRENT_SYNC_COMMITTEE_INDEX;
750			}
751
752			if epoch >= fork_versions.electra.epoch {
753				return config::electra::CURRENT_SYNC_COMMITTEE_INDEX;
754			}
755
756			config::altair::CURRENT_SYNC_COMMITTEE_INDEX
757		}
758
759		pub fn next_sync_committee_gindex_at_slot(slot: u64, fork_versions: ForkVersions) -> usize {
760			let epoch = compute_epoch(slot, config::SLOTS_PER_EPOCH as u64);
761
762			if epoch >= fork_versions.gloas.epoch {
763				return config::gloas::NEXT_SYNC_COMMITTEE_INDEX;
764			}
765
766			if epoch >= fork_versions.electra.epoch {
767				return config::electra::NEXT_SYNC_COMMITTEE_INDEX;
768			}
769
770			config::altair::NEXT_SYNC_COMMITTEE_INDEX
771		}
772
773		pub fn block_roots_gindex_at_slot(slot: u64, fork_versions: ForkVersions) -> usize {
774			let epoch = compute_epoch(slot, config::SLOTS_PER_EPOCH as u64);
775
776			if epoch >= fork_versions.gloas.epoch {
777				return config::gloas::BLOCK_ROOTS_INDEX;
778			}
779
780			if epoch >= fork_versions.electra.epoch {
781				return config::electra::BLOCK_ROOTS_INDEX;
782			}
783
784			config::altair::BLOCK_ROOTS_INDEX
785		}
786
787		/// The commitment scheme a proof at `slot` must use.
788		pub fn commitment_scheme_at_slot(
789			slot: u64,
790			fork_versions: ForkVersions,
791		) -> CommitmentScheme {
792			let epoch = compute_epoch(slot, config::SLOTS_PER_EPOCH as u64);
793
794			if epoch >= fork_versions.gloas.epoch {
795				return CommitmentScheme::BlockHash;
796			}
797
798			CommitmentScheme::PayloadHeaderRoot
799		}
800
801		/// Generalized index of the execution commitment in `BeaconBlockBody` at `slot`.
802		pub fn execution_commitment_gindex_at_slot(
803			slot: u64,
804			fork_versions: ForkVersions,
805		) -> usize {
806			let epoch = compute_epoch(slot, config::SLOTS_PER_EPOCH as u64);
807
808			if epoch >= fork_versions.gloas.epoch {
809				return config::gloas::EXECUTION_BLOCK_HASH_INDEX;
810			}
811
812			config::altair::EXECUTION_HEADER_INDEX
813		}
814
815		/// The commitment and gindex a proof at `slot` must prove, both chosen by the slot.
816		pub fn execution_commitment_at_slot(
817			header: &VersionedExecutionPayloadHeader,
818			slot: u64,
819			fork_versions: ForkVersions,
820		) -> Result<(ExecutionCommitment, usize), CommitmentError> {
821			let scheme = Self::commitment_scheme_at_slot(slot, fork_versions.clone());
822			let gindex = Self::execution_commitment_gindex_at_slot(slot, fork_versions);
823
824			Ok((header.commitment(scheme)?, gindex))
825		}
826	}
827}