// Ensure we're `no_std` when compiling for Wasm. #![cfg_attr(not(feature = "std"), no_std)] use codec::{Decode, Encode, MaxEncodedLen}; use scale_info::TypeInfo; use serde::{Deserialize, Deserializer}; use frame_support::{ pallet_prelude::*, traits::{ Currency, DisabledValidators, Get, OneSessionHandler, ValidatorSet, ValidatorSetWithIdentification, }, WeakBoundedVec, }; use frame_system::{ offchain::{SendTransactionTypes, SubmitTransaction}, pallet_prelude::*, }; pub use pallet::*; use sp_core::H256; use sp_runtime::{ offchain::{ self as rt_offchain, http::PendingRequest, storage::StorageValueRef, storage_lock::{StorageLock, Time}, }, traits::{AtLeast32BitUnsigned, BlockNumberProvider, Convert, Saturating, UniqueSaturatedInto}, Perbill, RuntimeAppPublic, RuntimeDebug, }; use sp_staking::{ offence::{Kind, Offence, ReportOffence}, SessionIndex, }; use sp_std::{collections::btree_map::BTreeMap, prelude::*, vec::Vec}; use ghost_networks::{ NetworkData, NetworkDataBasicHandler, NetworkDataInspectHandler, NetworkDataMutateHandler, NetworkType, }; use ghost_traits::exposure::ExposureListener; pub mod migrations; pub mod weights; pub use crate::weights::WeightInfo; mod benchmarking; mod mock; mod tests; mod bitmap; mod deserialisations; mod evm_types; use bitmap::{BitMap, Bucket}; use evm_types::{EvmResponse, EvmResponseType}; pub mod sr25519 { mod app_sr25519 { use sp_application_crypto::{app_crypto, sr25519, KeyTypeId}; const SLOW_CLAP: KeyTypeId = KeyTypeId(*b"slow"); app_crypto!(sr25519, SLOW_CLAP); } sp_application_crypto::with_pair! { pub type AuthorityPair = app_sr25519::Pair; } pub type AuthoritySignature = app_sr25519::Signature; pub type AuthorityId = app_sr25519::Public; } const LOG_TARGET: &str = "runtime::ghost-slow-clap"; const DB_PREFIX: &[u8] = b"slow_clap::"; const MIN_LOCK_GUARD_PERIOD: u64 = 15_000; const FETCH_TIMEOUT_PERIOD: u64 = 3_000; const LOCK_BLOCK_EXPIRATION: u64 = 20; const ONE_HOUR_MILLIS: u64 = 3_600_000; const BLOCK_CHECK_CYCLES: u64 = 8; const BLOCK_COMMITMENT_DELAY: u64 = 100; pub type AuthIndex = u32; pub type ExternalBlockNumber = u64; #[derive( RuntimeDebug, Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Encode, Decode, TypeInfo, MaxEncodedLen, )] pub struct CommitmentDetails { pub last_stored_block: ExternalBlockNumber, pub last_updated: BlockNumberFor, pub commits: u8, } #[derive( RuntimeDebug, Clone, Eq, PartialEq, Ord, PartialOrd, Encode, Decode, TypeInfo, MaxEncodedLen, )] pub struct BlockCommitment { pub session_index: SessionIndex, pub authority_index: AuthIndex, pub network_id: NetworkId, pub last_stored_block: ExternalBlockNumber, } #[derive( RuntimeDebug, Clone, Eq, PartialEq, Ord, PartialOrd, Encode, Decode, TypeInfo, MaxEncodedLen, )] pub struct Clap { pub session_index: SessionIndex, pub authority_index: AuthIndex, pub transaction_hash: H256, pub block_number: ExternalBlockNumber, pub removed: bool, pub network_id: NetworkId, pub receiver: AccountId, pub amount: Balance, } #[derive(Clone, PartialEq, Encode, Decode, RuntimeDebug, TypeInfo)] pub struct ApplauseDetail { pub network_id: NetworkId, pub authorities: BitMap, pub clapped_amount: Balance, pub block_number: ExternalBlockNumber, pub finalized: bool, } impl ApplauseDetail { pub fn new( network_id: NetworkId, block_number: ExternalBlockNumber, max_authorities: usize, ) -> Self { ApplauseDetail { network_id, block_number, finalized: false, clapped_amount: Default::default(), authorities: BitMap::new(max_authorities as AuthIndex), } } } #[derive(Default, Clone, Copy, PartialEq, Encode, Decode, RuntimeDebug, TypeInfo)] pub struct PreparedApplause { pub network_id: NetworkId, pub receiver: AccountId, pub amount: Balance, pub block_number: ExternalBlockNumber, } #[cfg_attr(test, derive(PartialEq))] enum OffchainErr { HttpJsonParsingError, HttpBytesParsingError, ErrorInEvmResponse, NoStoredNetworks, NoRequestsSent, EmptyResponses, NotValidator, DifferentEvmResponseTypes, MissingBlockNumber(u32, u32), ContradictoryTransactionLogs(u32, u32), ContradictoryBlockMedian( ExternalBlockNumber, ExternalBlockNumber, ExternalBlockNumber, ), UnparsableRequestBody(Vec), NoEndpointAvailable(NetworkId), StorageRetrievalError(NetworkId), UtxoNotImplemented(NetworkId), UnknownNetworkType(NetworkId), OffchainTimeoutPeriod(NetworkId), } impl core::fmt::Debug for OffchainErr { fn fmt(&self, fmt: &mut core::fmt::Formatter) -> core::fmt::Result { match *self { OffchainErr::HttpJsonParsingError => { write!(fmt, "Failed to parse evm response as JSON.") } OffchainErr::HttpBytesParsingError => { write!(fmt, "Failed to parse evm response as bytes.") } OffchainErr::StorageRetrievalError(ref network_id) => write!( fmt, "Storage value found for network #{:?} but it's undecodable.", network_id ), OffchainErr::ErrorInEvmResponse => write!(fmt, "Error in evm reponse."), OffchainErr::NoStoredNetworks => { write!(fmt, "No networks stored for the offchain slow claps.") } OffchainErr::NoRequestsSent => write!( fmt, "Could not send a request to any available RPC ednpoint." ), OffchainErr::EmptyResponses => { write!(fmt, "No responses to be used by the offchain worker.") } OffchainErr::NotValidator => write!(fmt, "Not a validator to broadcast slow claps"), OffchainErr::DifferentEvmResponseTypes => write!( fmt, "Different endpoints returned conflicting response types." ), OffchainErr::MissingBlockNumber(ref index, ref length) => write!( fmt, "Could not get block response at index {index} where total length is {length}.", ), OffchainErr::ContradictoryBlockMedian(ref mid, ref prev, ref distance) => write!( fmt, "Contradictory block median: values are {prev} {mid} while max distance is {distance}.", ), OffchainErr::ContradictoryTransactionLogs(ref count, ref number) => write!( fmt, "Contradictory tx logs: {number} event sequences from {count} endpoints.", ), OffchainErr::UnparsableRequestBody(ref bytes) => write!( fmt, "Could not get valid utf8 request body from bytes: {:?}.", bytes ), OffchainErr::NoEndpointAvailable(ref network_id) => write!( fmt, "No RPC endpoint available for network #{:?}.", network_id ), OffchainErr::UtxoNotImplemented(ref network_id) => write!( fmt, "Network #{:?} is marked as UTXO, which is not implemented yet.", network_id ), OffchainErr::UnknownNetworkType(ref network_id) => { write!(fmt, "Unknown type for network #{:?}.", network_id) } OffchainErr::OffchainTimeoutPeriod(ref network_id) => write!( fmt, "Offchain request should be in-flight for network #{:?}.", network_id ), } } } pub type NetworkIdOf = <::NetworkDataHandler as NetworkDataBasicHandler>::NetworkId; pub type BalanceOf = <::Currency as Currency<::AccountId>>::Balance; pub type ValidatorId = <::ValidatorSet as ValidatorSet< ::AccountId, >>::ValidatorId; pub type IdentificationTuple = ( ValidatorId, <::ValidatorSet as ValidatorSetWithIdentification< ::AccountId, >>::Identification, ); type OffchainResult = Result>>; #[frame_support::pallet] pub mod pallet { use super::*; const STORAGE_VERSION: StorageVersion = StorageVersion::new(3); #[pallet::pallet] #[pallet::storage_version(STORAGE_VERSION)] #[pallet::without_storage_info] pub struct Pallet(_); #[pallet::config] pub trait Config: SendTransactionTypes> + frame_system::Config { type RuntimeEvent: From> + IsType<::RuntimeEvent>; type AuthorityId: Member + Parameter + RuntimeAppPublic + Ord + MaybeSerializeDeserialize + MaxEncodedLen; type ValidatorSet: ValidatorSetWithIdentification; type Currency: Currency; type NetworkDataHandler: NetworkDataBasicHandler + NetworkDataInspectHandler + NetworkDataMutateHandler>; type BlockNumberProvider: BlockNumberProvider>; type ReportUnresponsiveness: ReportOffence< Self::AccountId, IdentificationTuple, SlowClapOffence>, >; type DisabledValidators: DisabledValidators; type ExposureListener: ExposureListener, Self::AccountId>; #[pallet::constant] type MaxAuthorities: Get; #[pallet::constant] type ApplauseThreshold: Get; #[pallet::constant] type UnsignedPriority: Get; #[pallet::constant] type HistoryDepth: Get; #[pallet::constant] type MinAuthoritiesNumber: Get; #[pallet::constant] type EpochDuration: Get; type WeightInfo: WeightInfo; } #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event { BlackSwan, AuthoritiesCommitmentEquilibrium, AuthoritiesApplauseEquilibrium, SomeAuthoritiesDelayed { delayed: Vec>, }, SomeAuthoritiesTrottling { throttling: Vec>, }, Clapped { authority_id: AuthIndex, network_id: NetworkIdOf, clap_unique_hash: H256, receiver: T::AccountId, amount: BalanceOf, removed: bool, }, Applaused { network_id: NetworkIdOf, receiver: T::AccountId, received_amount: BalanceOf, block_number: ExternalBlockNumber, }, BlockCommited { authority_id: AuthIndex, network_id: NetworkIdOf, }, BlockCommitmentsCheck { network_id: NetworkIdOf, block_number: BlockNumberFor, }, } #[pallet::error] pub enum Error { NotEnoughClaps, AlreadyClapped, UnregistedNetwork, UnregisteredClapRemove, TooMuchAuthorities, CouldNotAccumulateCommission, CouldNotAccumulateIncomingImbalance, CouldNotIncreaseGatekeeperAmount, NonExistentAuthorityIndex, TimeWentBackwards, InnerTimeWentBackwards, DisabledAuthority, ExecutedBlockIsHigher, CommitInWrongSession, } #[pallet::storage] #[pallet::getter(fn total_exposure)] pub(super) type TotalExposure = StorageValue<_, BalanceOf, ValueQuery>; #[pallet::storage] #[pallet::getter(fn latest_executed_block)] pub(super) type LatestExecutedBlock = StorageMap<_, Twox64Concat, NetworkIdOf, ExternalBlockNumber, ValueQuery>; #[pallet::storage] #[pallet::getter(fn block_commitments)] pub(super) type BlockCommitments = StorageMap< _, Twox64Concat, NetworkIdOf, BTreeMap>>, ValueQuery, >; #[pallet::storage] #[pallet::getter(fn applause_details)] pub(super) type ApplauseDetails = StorageDoubleMap< _, Twox64Concat, SessionIndex, Twox64Concat, H256, ApplauseDetail, BalanceOf>, OptionQuery, >; #[pallet::storage] #[pallet::getter(fn disabled_authority_indexes)] pub(super) type DisabledAuthorityIndexes = StorageMap<_, Twox64Concat, SessionIndex, BitMap, OptionQuery>; #[pallet::storage] #[pallet::getter(fn authorities)] pub(super) type Authorities = StorageMap< _, Twox64Concat, SessionIndex, WeakBoundedVec, ValueQuery, >; #[pallet::storage] #[pallet::getter(fn validators)] pub(super) type Validators = StorageMap< _, Twox64Concat, SessionIndex, WeakBoundedVec, T::MaxAuthorities>, ValueQuery, >; #[pallet::genesis_config] #[derive(frame_support::DefaultNoBound)] pub struct GenesisConfig { pub authorities: Vec, } #[pallet::genesis_build] impl BuildGenesisConfig for GenesisConfig { fn build(&self) { if !self.authorities.is_empty() { Pallet::::initialize_authorities(self.authorities.clone()); } } } #[pallet::call] impl Pallet { #[pallet::call_index(0)] #[pallet::weight((T::WeightInfo::slow_clap(), DispatchClass::Normal, Pays::No))] pub fn slow_clap( origin: OriginFor, clap: Clap, BalanceOf>, // since signature verification is done in `validate_unsigned` // we can skip doing it here again. _signature: ::Signature, ) -> DispatchResult { ensure_none(origin)?; Self::try_slow_clap(&clap)?; Ok(()) } #[pallet::call_index(1)] #[pallet::weight((T::WeightInfo::commit_block(), DispatchClass::Normal, Pays::No))] pub fn commit_block( origin: OriginFor, block_commitment: BlockCommitment>, // since signature verification is done in `validate_unsigned` // we can skip doing it here again. _signature: ::Signature, ) -> DispatchResult { ensure_none(origin)?; Self::try_commit_block(&block_commitment)?; Ok(()) } } #[pallet::hooks] impl Hooks> for Pallet { fn on_initialize(current_block: BlockNumberFor) -> Weight { let mut weight = T::DbWeight::get().reads(1); let networks_count = T::NetworkDataHandler::count(); let current_block_number: ExternalBlockNumber = current_block.unique_saturated_into(); let check_block_interval = T::EpochDuration::get().saturating_div(BLOCK_CHECK_CYCLES); if check_block_interval == 0 { return weight; } let cycle_offset = current_block_number % check_block_interval; let block_in_epoch = current_block_number % T::EpochDuration::get(); if cycle_offset >= networks_count.into() || block_in_epoch < check_block_interval { return weight; } let converted_block: usize = current_block_number.unique_saturated_into(); if let Some((network_id, network_data)) = T::NetworkDataHandler::network_for_block(converted_block) { weight.saturating_accrue(T::DbWeight::get().reads_writes(3, 1)); let session_index = T::ValidatorSet::session_index(); let block_commitments = BlockCommitments::::get(&network_id); let validators = Validators::::get(&session_index); let validators_len = validators.len() as u32; if validators_len == 0 { return weight; } let disabled_bitmap = DisabledAuthorityIndexes::::get(&session_index) .unwrap_or(BitMap::new(validators.len() as u32)); let max_external_block_deviation = ONE_HOUR_MILLIS .saturating_mul(6) .saturating_div(network_data.avg_block_speed); let max_internal_block_deviation = check_block_interval.unique_saturated_into(); let mut stored_blocks: Vec<(AuthIndex, ExternalBlockNumber)> = Vec::new(); let mut block_updates: Vec<(AuthIndex, BlockNumberFor)> = Vec::new(); for authority_index in 0..validators_len { let data = block_commitments .get(&authority_index) .copied() .unwrap_or_default(); stored_blocks.push((authority_index, data.last_stored_block)); block_updates.push((authority_index, data.last_updated)); } let stored_blocks_offence_bitmap = Self::capture_deviation_in_commitments( &disabled_bitmap, &mut stored_blocks, validators_len, max_external_block_deviation, ); let block_updates_offence_bitmap = Self::capture_deviation_in_commitments( &disabled_bitmap, &mut block_updates, validators_len, max_internal_block_deviation, ); let offence_bitmap = stored_blocks_offence_bitmap.bitor(block_updates_offence_bitmap); let offence_type = OffenceType::CommitmentOffence; let extra_weight = Self::try_offend_validators( &session_index, &validators, offence_bitmap, disabled_bitmap, offence_type, ); Self::deposit_event(Event::::BlockCommitmentsCheck { block_number: current_block, network_id, }); weight.saturating_accrue(extra_weight); } weight } fn offchain_worker(now: BlockNumberFor) { match Self::start_slow_clapping(now) { Ok(_) => { log::info!(target: LOG_TARGET, "👻 Slow Clap #{:?} finished gracefully", now) } Err(e) => log::info!(target: LOG_TARGET, "👻 Slow Clap #{:?} failed: {:?}", now, e), } } } #[pallet::validate_unsigned] impl ValidateUnsigned for Pallet { type Call = Call; fn validate_unsigned(_source: TransactionSource, call: &Self::Call) -> TransactionValidity { match call { Call::commit_block { block_commitment, signature, } => { let session_index = block_commitment.session_index; let authority_index = block_commitment.authority_index; match Authorities::::get(&session_index).get(authority_index as usize) { Some(authority) => { if !block_commitment .using_encoded(|encoded| authority.verify(&encoded, signature)) { return InvalidTransaction::BadProof.into(); } } None => return InvalidTransaction::BadSigner.into(), } ValidTransaction::with_tag_prefix("SlowClap") .priority(T::UnsignedPriority::get()) .and_provides(block_commitment.encode()) .longevity(LOCK_BLOCK_EXPIRATION) .propagate(true) .build() } Call::slow_clap { clap, signature } => { let (session_index, _) = Self::mended_session_index(&clap); match Authorities::::get(&session_index).get(clap.authority_index as usize) { Some(authority) => { if !clap.using_encoded(|encoded| authority.verify(&encoded, signature)) { return InvalidTransaction::BadProof.into(); } } None => return InvalidTransaction::BadSigner.into(), } ValidTransaction::with_tag_prefix("SlowClap") .priority(T::UnsignedPriority::get()) .and_provides(signature) .longevity(LOCK_BLOCK_EXPIRATION) .propagate(true) .build() } _ => InvalidTransaction::Call.into(), } } } } impl Pallet { fn create_storage_key(first: &[u8], second: &[u8]) -> Vec { let mut key = DB_PREFIX.to_vec(); key.extend(first); key.extend(second); key } fn read_persistent_offchain_storage( storage_key: &[u8], default_value: R, ) -> R { StorageValueRef::persistent(&storage_key) .get::() .ok() .flatten() .unwrap_or(default_value) } fn generate_unique_hash(clap: &Clap, BalanceOf>) -> H256 { let mut clap_args_str = clap.receiver.encode(); clap_args_str.extend(&clap.amount.encode()); clap_args_str.extend(&clap.block_number.encode()); clap_args_str.extend(&clap.network_id.encode()); H256::from_slice(&sp_io::hashing::keccak_256(&clap_args_str)[..]) } fn block_number_to_hexadecimal_bytes(value: ExternalBlockNumber) -> Vec { let mut hex_str = Vec::new(); hex_str.push(b'0'); hex_str.push(b'x'); if value == 0 { hex_str.push(b'0'); return hex_str; } for i in (0..16).rev() { let nibble = (value >> (i * 4)) & 0xF; if nibble != 0 || hex_str.len() > 2 { hex_str.push(match nibble { 0..=9 => b'0' + nibble as u8, 10..=15 => b'a' + (nibble - 10) as u8, _ => unreachable!(), }); } } hex_str } fn mended_session_index( clap: &Clap, BalanceOf>, ) -> (SessionIndex, H256) { let prev_session_index = clap.session_index.saturating_sub(1); let clap_unique_hash = Self::generate_unique_hash(&clap); let session_index = if ApplauseDetails::::contains_key(&prev_session_index, &clap_unique_hash) { prev_session_index } else { clap.session_index }; (session_index, clap_unique_hash) } fn try_slow_clap(clap: &Clap, BalanceOf>) -> DispatchResult { let network_id = clap.network_id; ensure!( T::NetworkDataHandler::contains_key(&network_id), Error::::UnregistedNetwork ); ensure!( LatestExecutedBlock::::get(&network_id) <= clap.block_number, Error::::ExecutedBlockIsHigher, ); let (session_index, clap_unique_hash) = Self::mended_session_index(&clap); let authorities = Authorities::::get(&session_index); let authorities_length = authorities.len(); let not_disabled = DisabledAuthorityIndexes::::get(&session_index) .map(|bitmap| !bitmap.exists(&clap.authority_index)) .unwrap_or_default(); ensure!(not_disabled, Error::::DisabledAuthority); let applause_threshold = Perbill::from_parts(T::ApplauseThreshold::get()); let threshold_amount = applause_threshold.mul_floor(TotalExposure::::get()); let account_id = T::ExposureListener::get_account_by_index(clap.authority_index as usize) .ok_or(Error::::NonExistentAuthorityIndex)?; let new_clapped_amount = T::ExposureListener::get_validator_exposure(&account_id); let mut applause_details = ApplauseDetails::::try_get(&session_index, &clap_unique_hash).unwrap_or( ApplauseDetail::new(network_id, clap.block_number, authorities_length), ); let total_clapped = if clap.removed { ensure!( applause_details.authorities.exists(&clap.authority_index), Error::::UnregisteredClapRemove, ); applause_details.authorities.unset(clap.authority_index); applause_details .clapped_amount .saturating_sub(new_clapped_amount) } else { ensure!( !applause_details.authorities.exists(&clap.authority_index), Error::::AlreadyClapped, ); applause_details.authorities.set(clap.authority_index); applause_details .clapped_amount .saturating_add(new_clapped_amount) }; applause_details.clapped_amount = total_clapped; Self::deposit_event(Event::::Clapped { network_id, authority_id: clap.authority_index, clap_unique_hash, receiver: clap.receiver.clone(), amount: clap.amount, removed: clap.removed, }); let is_enough = applause_details .authorities .count_ones() .gt(&(authorities_length as u32 / 2)); if total_clapped > threshold_amount && is_enough && !applause_details.finalized { match Self::try_applause(&clap) { Ok(_) => applause_details.finalized = true, Err(e) => sp_runtime::print(e), } } ApplauseDetails::::insert(&session_index, &clap_unique_hash, applause_details); Ok(()) } fn try_applause(clap: &Clap, BalanceOf>) -> DispatchResult { let commission = T::NetworkDataHandler::get(&clap.network_id) .map(|network_data| Perbill::from_parts(network_data.incoming_fee)) .unwrap_or_default() .mul_ceil(clap.amount); let final_amount = clap.amount.saturating_sub(commission); let _ = T::NetworkDataHandler::increase_gatekeeper_amount(&clap.network_id, &clap.amount) .map_err(|_| Error::::CouldNotIncreaseGatekeeperAmount)?; let _ = T::NetworkDataHandler::accumulate_incoming_imbalance(&final_amount) .map_err(|_| Error::::CouldNotAccumulateIncomingImbalance)?; let _ = T::NetworkDataHandler::accumulate_commission(&commission) .map_err(|_| Error::::CouldNotAccumulateCommission)?; let _ = T::Currency::deposit_creating(&clap.receiver, final_amount); LatestExecutedBlock::::set(clap.network_id, clap.block_number); Self::deposit_event(Event::::Applaused { network_id: clap.network_id, receiver: clap.receiver.clone(), received_amount: final_amount, block_number: clap.block_number, }); Ok(()) } fn try_commit_block(new_commitment: &BlockCommitment>) -> DispatchResult { let authority_index = new_commitment.authority_index; let network_id = new_commitment.network_id; let session_index = T::ValidatorSet::session_index(); let latest_executed_block = LatestExecutedBlock::::get(&network_id); ensure!( T::NetworkDataHandler::contains_key(&network_id), Error::::UnregistedNetwork ); ensure!( new_commitment.session_index == session_index, Error::::CommitInWrongSession, ); BlockCommitments::::try_mutate(&network_id, |current_commitments| -> DispatchResult { let current_block_number = Self::current_block_number(); let (current_commits, current_last_updated) = current_commitments .get(&authority_index) .map(|details| { ( details.commits.saturating_add(1), details .last_updated .saturating_add(BLOCK_COMMITMENT_DELAY.unique_saturated_into()), ) }) .unwrap_or((1, Default::default())); ensure!( current_block_number >= current_last_updated, Error::::TimeWentBackwards, ); ensure!( new_commitment.last_stored_block > latest_executed_block, Error::::InnerTimeWentBackwards, ); let new_commitment_details = CommitmentDetails { last_stored_block: new_commitment.last_stored_block, last_updated: Self::current_block_number(), commits: current_commits, }; current_commitments.insert(authority_index, new_commitment_details); Ok(()) })?; Self::deposit_event(Event::::BlockCommited { network_id, authority_id: authority_index, }); Ok(()) } fn start_slow_clapping(block_number: BlockNumberFor) -> OffchainResult { let converted_block: usize = block_number.unique_saturated_into(); let session_index = T::ValidatorSet::session_index(); let network_in_use = T::NetworkDataHandler::network_for_block(converted_block) .ok_or(OffchainErr::NoStoredNetworks)?; log::info!( target: LOG_TARGET, "👻 Slow Clap #{:?} started for network #{:?}", block_number, network_in_use.0, ); let network_id_encoded = network_in_use.0.encode(); let network_lock_key = Self::create_storage_key(b"network-lock-", &network_id_encoded); let lock_until = rt_offchain::Duration::from_millis(MIN_LOCK_GUARD_PERIOD); let mut network_lock = StorageLock::