// 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::{Deserializer, Deserialize}; pub use pallet::*; use frame_support::{ pallet_prelude::*, traits::{ tokens::{ fungible::{Inspect, Mutate}, Preservation, Precision, Fortitude, }, EstimateNextSessionRotation, ValidatorSet, ValidatorSetWithIdentification, OneSessionHandler, Get, }, PalletId, BoundedSlice, BoundedVec, WeakBoundedVec, }; use frame_system::{ offchain::{SendTransactionTypes, SubmitTransaction}, pallet_prelude::*, }; use sp_core::{H160, H256}; use sp_runtime::{ traits::{ TrailingZeroInput, Saturating, BlockNumberProvider, Convert, AccountIdConversion, }, offchain as rt_offchain, offchain::{ HttpError, storage::StorageValueRef, storage_lock::{BlockAndTime, StorageLock}, }, RuntimeAppPublic, Perbill, RuntimeDebug, SaturatedConversion, }; use sp_std::{ vec::Vec, prelude::*, collections::btree_map::BTreeMap, }; use sp_staking::{ offence::{Kind, Offence, ReportOffence}, SessionIndex, }; use sp_io::hashing::keccak_256; use ghost_networks::{ NetworkData, NetworkDataBasicHandler, NetworkDataInspectHandler, NetworkDataMutateHandler, }; pub mod weights; pub use crate::weights::WeightInfo; mod tests; mod mock; mod benchmarking; 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! { /// A staking keypair sr25519 at its crypto. pub type AuthorityPair = app_sr25519::Pair; } /// A staking signature using sr25519 as its crypto. pub type AuthoritySignature = app_sr25519::Signature; /// A staking identifier using sr25519 as its crypto. pub type AuthorityId = app_sr25519::Public; } const LOG_TARGET: &str = "runtime::ghost-slow-clap"; const DB_PREFIX: &[u8] = b"slow_clap::"; const FETCH_TIMEOUT_PERIOD: u64 = 3_000; const LOCK_BLOCK_EXPIRATION: u32 = 5; const LOCK_TIMEOUT_EXPIRATION: u64 = FETCH_TIMEOUT_PERIOD + 1_000; const PERCENT_DIVISOR: u32 = 4; const COMPANIONS_LIMIT: usize = 20; pub type CompanionId = u128; pub type AuthIndex = u32; #[derive(RuntimeDebug, Clone, PartialEq, Deserialize, Encode, Decode)] struct EvmResponse { #[serde(default)] id: Option, #[serde(default, deserialize_with = "de_string_to_bytes")] jsonrpc: Option>, #[serde(default, deserialize_with = "de_string_to_bytes")] error: Option>, #[serde(default)] result: Option, } #[derive(RuntimeDebug, Clone, PartialEq, Deserialize, Encode, Decode)] #[serde(untagged)] enum EvmResponseType { #[serde(deserialize_with = "de_string_to_u64_pure")] BlockNumber(u64), TransactionLogs(Vec), } #[derive(RuntimeDebug, Clone, Eq, PartialEq, Ord, PartialOrd, Deserialize, Encode, Decode)] #[serde(rename_all = "camelCase")] struct Log { #[serde(default, deserialize_with = "de_string_to_h256")] transaction_hash: Option, #[serde(default, deserialize_with = "de_string_to_u64")] block_number: Option, #[serde(default, deserialize_with = "de_string_to_vec_of_bytes")] topics: Vec>, #[serde(default, deserialize_with = "de_string_to_bytes")] address: Option>, #[serde(default, deserialize_with = "de_string_to_btree_map")] data: BTreeMap, removed: bool, } pub fn de_string_to_bytes<'de, D>(de: D) -> Result>, D::Error> where D: Deserializer<'de> { let s: &str = Deserialize::deserialize(de)?; Ok(Some(s.as_bytes().to_vec())) } pub fn de_string_to_u64<'de, D>(de: D) -> Result, D::Error> where D: Deserializer<'de> { let s: &str = Deserialize::deserialize(de)?; let s = if s.starts_with("0x") { &s[2..] } else { &s }; Ok(u64::from_str_radix(s, 16).ok()) } pub fn de_string_to_u64_pure<'de, D>(de: D) -> Result where D: Deserializer<'de> { let s: &str = Deserialize::deserialize(de)?; let s = if s.starts_with("0x") { &s[2..] } else { &s }; Ok(u64::from_str_radix(s, 16).unwrap_or_default()) } pub fn de_string_to_h256<'de, D>(de: D) -> Result, D::Error> where D: Deserializer<'de> { let s: &str = Deserialize::deserialize(de)?; let start_index = if s.starts_with("0x") { 2 } else { 0 }; let h256: Vec<_> = (start_index..s.len()) .step_by(2) .map(|i| u8::from_str_radix(&s[i..i+2], 16).expect("valid u8 symbol; qed")) .collect(); Ok(Some(H256::from_slice(&h256))) } pub fn de_string_to_vec_of_bytes<'de, D>(de: D) -> Result>, D::Error> where D: Deserializer<'de> { let strings: Vec<&str> = Deserialize::deserialize(de)?; Ok(strings .iter() .map(|s| { let start_index = if s.starts_with("0x") { 2 } else { 0 }; (start_index..s.len()) .step_by(2) .map(|i| u8::from_str_radix(&s[i..i+2], 16).expect("valid u8 symbol; qed")) .collect::>() }) .collect::>>()) } pub fn de_string_to_btree_map<'de, D>(de: D) -> Result, D::Error> where D: Deserializer<'de> { let s: &str = Deserialize::deserialize(de)?; let start_index = if s.starts_with("0x") { 2 } else { 0 }; Ok(BTreeMap::from_iter((start_index..s.len()) .step_by(64) .map(|i| ( u128::from_str_radix(&s[i..i+32], 16).expect("valid u8 symbol; qed"), u128::from_str_radix(&s[i+32..i+64], 16).expect("valid u8 symbol; qed"), )))) } #[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: u64, pub removed: bool, pub network_id: NetworkId, pub receiver: AccountId, pub amount: Balance, pub companions: BTreeMap, } #[derive(Encode, Decode, Clone, PartialEq, RuntimeDebug, MaxEncodedLen, TypeInfo)] pub struct Companion { pub network_id: NetworkId, pub receiver: H160, pub amount: Balance, } #[derive(Encode, Decode, Clone, PartialEq, RuntimeDebug, MaxEncodedLen, TypeInfo)] pub struct SessionAuthorityInfo { pub actions: u32, pub disabled: bool, } #[cfg_attr(test, derive(PartialEq))] enum OffchainErr { FailedSigning, FailedToAcquireLock(NetworkId), SubmitTransaction, HttpJsonParsingError, HttpBytesParsingError, HttpRequestError(HttpError), RequestUncompleted, HttpResponseNotOk(u16), ErrorInEvmResponse, NoEvmLogsFound(NetworkId), OnlyPendingEvmLogs(NetworkId), } impl core::fmt::Debug for OffchainErr { fn fmt(&self, fmt: &mut core::fmt::Formatter) -> core::fmt::Result { match *self { OffchainErr::FailedSigning => write!(fmt, "Failed to sign clap."), OffchainErr::FailedToAcquireLock(ref network_id) => write!(fmt, "Failed to acquire lock for network #{:?}.", network_id), OffchainErr::SubmitTransaction => write!(fmt, "Failed to submit transaction."), OffchainErr::HttpJsonParsingError => write!(fmt, "Failed to parse evm response as JSON."), OffchainErr::HttpBytesParsingError => write!(fmt, "Failed to parse evm response as bytes."), OffchainErr::HttpRequestError(http_error) => match http_error { HttpError::DeadlineReached => write!(fmt, "Requested action couldn't been completed within a deadline."), HttpError::IoError => write!(fmt, "There was an IO error while processing the request."), HttpError::Invalid => write!(fmt, "The ID of the request is invalid in this context"), }, OffchainErr::RequestUncompleted => write!(fmt, "Failed to complete request."), OffchainErr::HttpResponseNotOk(code) => write!(fmt, "Http response returned code {:?}", code), OffchainErr::ErrorInEvmResponse => write!(fmt, "Error in evm reponse."), OffchainErr::NoEvmLogsFound(ref network_id) => write!(fmt, "No incoming evm logs for network #{:?}.", network_id), OffchainErr::OnlyPendingEvmLogs(ref network_id) => write!(fmt, "Only pending evm logs for network #{:?}.", network_id), } } } pub type NetworkIdOf = <::NetworkDataHandler as NetworkDataBasicHandler>::NetworkId; pub type BalanceOf = <::Currency as Inspect< ::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::*; /// The current storage version. const STORAGE_VERSION: StorageVersion = StorageVersion::new(1); #[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 NextSessionRotation: EstimateNextSessionRotation>; type ValidatorSet: ValidatorSetWithIdentification; type Currency: Inspect + Mutate; type NetworkDataHandler: NetworkDataBasicHandler + NetworkDataInspectHandler + NetworkDataMutateHandler; type BlockNumberProvider: BlockNumberProvider>; type ReportUnresponsiveness: ReportOffence< Self::AccountId, IdentificationTuple, ThrottlingOffence>, >; #[pallet::constant] type MaxAuthorities: Get; #[pallet::constant] type MaxAuthorityInfoInSession: Get; #[pallet::constant] type MaxNumberOfClaps: Get; #[pallet::constant] type ApplauseThreshold: Get; #[pallet::constant] type OffenceThreshold: Get; #[pallet::constant] type UnsignedPriority: Get; #[pallet::constant] type TreasuryPalletId: Get; type WeightInfo: WeightInfo; } #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event { AllGood, SomeTrottling { throttling: Vec> }, Clapped { authority_id: T::AuthorityId, network_id: NetworkIdOf, clap_hash: H256, transaction_hash: H256, receiver: T::AccountId, amount: BalanceOf, }, Applaused { network_id: NetworkIdOf, receiver: T::AccountId, received_amount: BalanceOf, number_of_companions: u32, }, CompanionCreated { companion_id: CompanionId, owner: T::AccountId, companion: Companion, BalanceOf>, }, CompanionReleased { network_id: NetworkIdOf, companion_id: CompanionId, who: T::AccountId, release_block: BlockNumberFor, }, CompanionKilled { network_id: NetworkIdOf, who: T::AccountId, companion_id: CompanionId, freed_balance: BalanceOf, }, } #[pallet::error] pub enum Error { NotAnAuthority, CurrentValidatorIsDisabled, CouldNotHashCompanions, AlreadyClapped, CompanionAlreadyRegistered, CompanionDetailsAlreadyRegistered, CompanionDetailsNotRegistered, CompanionAmountNotExistent, CompanionAmountUnderflow, NoMoreCompanions, NotValidCompanion, NonRegisteredNetwork, ReleaseTimeHasNotCome, } #[pallet::storage] #[pallet::getter(fn received_claps)] pub(super) type ReceivedClaps = StorageDoubleMap< _, Twox64Concat, H256, Twox64Concat, T::AuthorityId, bool, ValueQuery >; #[pallet::storage] #[pallet::getter(fn applauses_for_transaction)] pub(super) type ApplausesForTransaction = StorageMap< _, Twox64Concat, H256, bool, ValueQuery >; #[pallet::storage] #[pallet::getter(fn authorities_claps)] pub(super) type AuthoritiesClaps = StorageDoubleMap< _, Twox64Concat, T::AuthorityId, Twox64Concat, H256, bool, ValueQuery, >; #[pallet::storage] #[pallet::getter(fn authority_info_in_session)] pub(super) type AuthorityInfoInSession = StorageMap< _, Twox64Concat, SessionIndex, BoundedVec, ValueQuery, >; #[pallet::storage] #[pallet::getter(fn companions)] pub(super) type Companions = StorageDoubleMap< _, Twox64Concat, NetworkIdOf, Twox64Concat, CompanionId, T::AccountId, OptionQuery, >; #[pallet::storage] #[pallet::getter(fn companion_details)] pub(super) type CompanionDetails = StorageMap< _, Twox64Concat, CompanionId, Companion, BalanceOf>, OptionQuery, >; #[pallet::storage] #[pallet::getter(fn release_blocks)] pub(super) type ReleaseBlocks = StorageMap< _, Twox64Concat, CompanionId, BlockNumberFor, ValueQuery, >; #[pallet::storage] #[pallet::getter(fn current_companion_id)] pub(super) type CurrentCompanionId = StorageValue<_, CompanionId, ValueQuery>; #[pallet::storage] #[pallet::getter(fn keys)] pub(super) type Authorities = StorageValue<_, WeakBoundedVec, ValueQuery>; #[pallet::genesis_config] #[derive(frame_support::DefaultNoBound)] pub struct GenesisConfig { pub keys: Vec, } #[pallet::genesis_build] impl BuildGenesisConfig for GenesisConfig { fn build(&self) { Pallet::::initialize_authorities(&self.keys); Pallet::::restart_clap_round(1, self.keys.len()); } } #[pallet::call] impl Pallet { #[pallet::call_index(0)] #[pallet::weight(( T::WeightInfo::slow_clap( claps.len() as u32, claps.iter().fold(0u32, |acc, c| { acc + c.companions.len() as u32 }) ), frame_support::dispatch::Pays::No, ))] pub fn slow_clap( origin: OriginFor, claps: Vec, BalanceOf>>, // since signature verification is done in `validate_unsigned` // we can skip doing it here again. _signature: ::Signature, ) -> DispatchResult { ensure_none(origin)?; Self::clap_if_possible(claps)?; Ok(()) } #[pallet::call_index(1)] #[pallet::weight(T::WeightInfo::propose_companion())] pub fn propose_companion( origin: OriginFor, network_id: NetworkIdOf, companion: Companion, BalanceOf>, ) -> DispatchResult { let who = ensure_signed(origin)?; let companion_id = CurrentCompanionId::::get(); ensure!(companion.amount > T::Currency::minimum_balance(), Error::::CompanionAmountNotExistent); ensure!(T::NetworkDataHandler::get(&network_id).is_some(), Error::::NonRegisteredNetwork); T::Currency::burn_from( &who, companion.amount, Preservation::Expendable, Precision::Exact, Fortitude::Force, )?; Companions::::set(network_id, companion_id, Some(who.clone())); CompanionDetails::::set(companion_id, Some(companion.clone())); CurrentCompanionId::::set(companion_id .checked_add(1) .ok_or(Error::::NoMoreCompanions)?); Self::deposit_event(Event::::CompanionCreated { companion_id, owner: who, companion, }); Ok(()) } #[pallet::call_index(2)] #[pallet::weight(T::WeightInfo::release_companion())] pub fn release_companion( origin: OriginFor, network_id: NetworkIdOf, companion_id: CompanionId, ) -> DispatchResult { let who = ensure_signed(origin)?; let owner = Companions::::get(network_id, companion_id); let network = T::NetworkDataHandler::get(&network_id); ensure!(owner.is_some_and(|o| o == who), Error::::NotValidCompanion); ensure!(CompanionDetails::::get(companion_id).is_some(), Error::::CompanionDetailsNotRegistered); let offset = T::BlockNumberProvider::current_block_number() + network .ok_or(Error::::NonRegisteredNetwork)? .release_delay .unwrap_or_default() .saturated_into::>(); ReleaseBlocks::::set(companion_id, offset); Self::deposit_event(Event::::CompanionReleased { network_id, companion_id, who, release_block: offset, }); Ok(()) } #[pallet::call_index(3)] #[pallet::weight(T::WeightInfo::kill_companion())] pub fn kill_companion( origin: OriginFor, network_id: NetworkIdOf, companion_id: CompanionId, ) -> DispatchResult { let who = ensure_signed(origin)?; ensure!(ReleaseBlocks::::get(companion_id) <= T::BlockNumberProvider::current_block_number(), Error::::ReleaseTimeHasNotCome); ensure!(Companions::::get(network_id, companion_id).is_some_and(|o| o == who), Error::::NotValidCompanion); let companion = CompanionDetails::::get(companion_id) .ok_or(Error::::CompanionDetailsNotRegistered)?; Companions::::remove(network_id, companion_id); CompanionDetails::::remove(companion_id); T::Currency::mint_into(&who, companion.amount)?; Self::deposit_event(Event::::CompanionKilled { network_id, who, companion_id, freed_balance: companion.amount, }); Ok(()) } } #[pallet::hooks] impl Hooks> for Pallet { fn offchain_worker(now: BlockNumberFor) { // Only send messages of we are a potential validator. if sp_io::offchain::is_validator() { let networks_len = T::NetworkDataHandler::iter().count(); if networks_len == 0 { log::info!( target: LOG_TARGET, "👏 Skipping slow clap at {:?}: no evm networks", now, ); } else { for result in Self::start_slow_clapping(now, networks_len).into_iter().flatten() { if let Err(e) = result { log::debug!( target: LOG_TARGET, "👏 Skipping slow clap at {:?}: {:?}", now, e, ); } } } } else { log::info!( target: LOG_TARGET, "🤥 Not a validator, skipping slow clap at {:?}.", now, ); } } } #[pallet::validate_unsigned] impl ValidateUnsigned for Pallet { type Call = Call; fn validate_unsigned(_source: TransactionSource, call: &Self::Call) -> TransactionValidity { if let Call::slow_clap { claps, signature } = call { if claps.is_empty() || claps.len() > T::MaxNumberOfClaps::get() as usize { return InvalidTransaction::BadProof.into(); } if claps .iter() .fold(0usize, |max_len, c| { usize::max(max_len, c.companions.len()) }) > COMPANIONS_LIMIT { return InvalidTransaction::BadProof.into(); } let authority_index = match &claps[..] { [head, tail @ ..] => tail .iter() .all(|x| x.authority_index == head.authority_index) .then(|| head.authority_index), _ => None, }; if authority_index.is_none() { return InvalidTransaction::BadProof.into(); } let authority_index = authority_index .expect("authority index info already checked; qed"); let authorities = Authorities::::get(); let authority = match authorities.get(authority_index as usize) { Some(id) => id, None => return InvalidTransaction::BadProof.into(), }; let signature_valid = claps.using_encoded(|encoded_claps| { authority.verify(&encoded_claps, signature) }); if !signature_valid { return InvalidTransaction::BadProof.into(); } ValidTransaction::with_tag_prefix("SlowClap") .priority(T::UnsignedPriority::get()) .and_provides(authority) .longevity(LOCK_BLOCK_EXPIRATION.saturated_into::()) .propagate(true) .build() } else { InvalidTransaction::Call.into() } } } } impl Pallet { fn treasury_account_id() -> T::AccountId { T::TreasuryPalletId::get().into_account_truncating() } fn clap_if_possible( claps: Vec, BalanceOf>>, ) -> DispatchResult { let current_authorities = Authorities::::get(); for clap in claps.iter() { let Ok(mut clap_str) = serde_json::to_vec(&clap.companions) else { continue; }; clap_str.extend(clap.transaction_hash.to_fixed_bytes()); clap_str.extend(clap.network_id.encode()); let clap_hash = H256::from_slice(&keccak_256(&clap_str)[..]); let Some(public) = current_authorities.get(clap.authority_index as usize) else { continue; }; let mut claps_in_session = AuthorityInfoInSession::::get(&clap.session_index); if let Some(_) = claps_in_session.get(clap.authority_index as usize) { if claps_in_session[clap.authority_index as usize].disabled { continue; } let received_clap = ReceivedClaps::::get(&clap_hash, &public); match (received_clap, clap.removed) { (true, false) => ReceivedClaps::::set(&clap_hash, &public, false), (false, true) => ReceivedClaps::::set(&clap_hash, &public, true), _ => continue, } claps_in_session[clap.authority_index as usize].actions.saturating_inc(); AuthorityInfoInSession::::set(&clap.session_index, claps_in_session); Self::deposit_event(Event::::Clapped { authority_id: public.clone(), network_id: clap.network_id, clap_hash, transaction_hash: clap.transaction_hash, receiver: clap.receiver.clone(), amount: clap.amount, }); } Self::applause_if_possible(clap_hash, &clap, current_authorities.clone())?; } Ok(()) } fn applause_if_possible( clap_hash: H256, clap: &Clap, BalanceOf>, current_authorities: WeakBoundedVec, ) -> DispatchResult { if !ApplausesForTransaction::::get(&clap_hash) { let clappers_len = ReceivedClaps::::iter_prefix(&clap_hash) .filter(|(authority, yes_vote)| current_authorities.contains(authority) && *yes_vote) .count(); let enough_authorities = Perbill::from_rational( clappers_len as u32, current_authorities.len() as u32, ) > Perbill::from_percent(T::ApplauseThreshold::get()); if enough_authorities { Self::reward_companions( clap.network_id, clap.receiver.clone(), clap.amount, clap.companions.clone(), )?; } } Ok(()) } fn reward_companions( network_id: NetworkIdOf, receiver: T::AccountId, amount: BalanceOf, companions: BTreeMap>, ) -> DispatchResult { let mut final_bridge_amount = amount; let mut number_of_companions: u32 = 0; let existential_balance = T::Currency::minimum_balance(); for (companion_id, companion_amount) in companions.iter() { if final_bridge_amount < existential_balance { break; } number_of_companions.saturating_inc(); CompanionDetails::::mutate(companion_id, |maybe_stored_companion| { match maybe_stored_companion { Some(stored_companion) if (stored_companion.amount >= *companion_amount) => { stored_companion.amount = stored_companion .amount .saturating_sub(*companion_amount); if stored_companion.amount > existential_balance { *maybe_stored_companion = Some(stored_companion.clone()); } else { *maybe_stored_companion = None; } }, _ => { final_bridge_amount = final_bridge_amount .saturating_sub(*companion_amount); }, } }); } let incoming_fee = match T::NetworkDataHandler::get(&network_id) { Some(network_data) => network_data.incoming_fee, None => 0, }; let gatekeeper_fees = Perbill::from_parts(incoming_fee) .mul_floor(final_bridge_amount); let final_bridge_amount = final_bridge_amount .saturating_sub(gatekeeper_fees); if gatekeeper_fees >= existential_balance { T::Currency::mint_into( &Self::treasury_account_id(), gatekeeper_fees, )?; } if final_bridge_amount >= existential_balance { T::Currency::mint_into( &receiver, final_bridge_amount, )?; } Self::deposit_event(Event::::Applaused { network_id, receiver, received_amount: final_bridge_amount, number_of_companions, }); Ok(()) } fn create_key(first: Vec, second: Vec) -> Vec { let mut key = DB_PREFIX.to_vec(); key.extend(first); key.extend(second); key } fn start_slow_clapping( block_number: BlockNumberFor, networks_len: usize, ) -> OffchainResult>> { let session_index = T::ValidatorSet::session_index(); let index = block_number.into().as_u128() % networks_len as u128; let network_in_use = T::NetworkDataHandler::iter() .nth(index as usize) .expect("network should exist; qed"); let network_id = network_in_use.0; let gatekeeper = network_in_use.1.gatekeeper; let topic_name = network_in_use.1.topic_name; let key = Self::create_key(network_id.encode(), b"endpoint".to_vec()); let rpc_endpoint = match StorageValueRef::persistent(&key).get() { Ok(Some(endpoint)) => endpoint, _ => network_in_use.1.default_endpoint, }; let finality_delay = network_in_use.1.finality_delay.unwrap_or(1u64); Ok(Self::local_authorities().map(move |(authority_index, authority_key)| { Self::do_evm_claps_or_save_block( authority_index, authority_key, session_index, network_id, finality_delay, gatekeeper.clone(), topic_name.clone(), rpc_endpoint.clone(), ) })) } fn do_evm_claps_or_save_block( authority_index: AuthIndex, authority_key: T::AuthorityId, session_index: SessionIndex, network_id: NetworkIdOf, finality_delay: u64, gatekeeper: Vec, topic_name: Vec, rpc_endpoint: Vec, ) -> OffchainResult { let key = Self::create_key(network_id.encode(), b"lock".to_vec()); let mut network_lock = StorageLock::>::with_block_and_time_deadline( &key, LOCK_BLOCK_EXPIRATION, rt_offchain::Duration::from_millis(LOCK_TIMEOUT_EXPIRATION), ); network_lock .try_lock() .map_err(|_| OffchainErr::FailedToAcquireLock(network_id))? .forget(); let key = Self::create_key(network_id.encode(), b"block".to_vec()); let mut evm_block_info = StorageValueRef::persistent(&key); let evm_block: Option = match evm_block_info.get() { Ok(option_block) => option_block, _ => None, }; let evm_response = Self::fetch_and_parse( evm_block, finality_delay, gatekeeper, topic_name, rpc_endpoint )?; match evm_response { EvmResponseType::BlockNumber(evm_block) => { let deviation = Self::random_u64_deviation(finality_delay); let evm_start_block = evm_block.saturating_sub(deviation); evm_block_info.set(&evm_start_block); log::info!( target: LOG_TARGET, "🧐 New evm block #{:?} found for {:?} network", evm_start_block, network_id, ); }, EvmResponseType::TransactionLogs(evm_logs) => { if evm_logs.is_empty() { return Err(OffchainErr::NoEvmLogsFound(network_id).into()); } let evm_logs: Vec<_> = evm_logs .iter() .filter(|log| { log.block_number.is_some() && log.transaction_hash.is_some() && log.topics.len() == 3 && log.data.len() <= COMPANIONS_LIMIT }) .collect(); if evm_logs.is_empty() { return Err(OffchainErr::OnlyPendingEvmLogs(network_id)); } log::info!( target: LOG_TARGET, "🧐 {:?} evm logs found for network #{:?}", evm_logs.len(), network_id, ); let mut claps: Vec<_> = evm_logs .iter() .map(|log| Clap { authority_index, session_index, network_id, removed: log.removed, receiver: T::AccountId::decode(&mut &log.topics[1][0..32]) .expect("32 bytes always construct an AccountId32"), amount: u128::from_be_bytes(log.topics[2][16..32] .try_into() .expect("amount is valid hex; qed")) .saturated_into::>(), companions: log .data .clone() .into_iter() .map(|(key, value)| ( key.saturated_into::(), value.saturated_into::>(), )) .collect(), transaction_hash: log .transaction_hash .clone() .expect("tx hash exists; qed"), block_number: log .block_number .expect("block number exists; qed"), }) .collect(); claps.sort_by(|a, b| a.block_number.cmp(&b.block_number)); let maximum_claps = T::MaxNumberOfClaps::get() as usize; claps.truncate(maximum_claps); match claps.get(maximum_claps) { Some(last_clap) => { evm_block_info.set(&last_clap.block_number); log::info!( target: LOG_TARGET, "🧐 New evm block #{:?} found for {:?} network", last_clap.block_number, network_id, ); }, None => evm_block_info.clear(), } let signature = authority_key.sign(&claps.encode()) .ok_or(OffchainErr::FailedSigning)?; let call = Call::slow_clap { claps, signature }; SubmitTransaction::>::submit_unsigned_transaction(call.into()) .map_err(|_| OffchainErr::SubmitTransaction)?; } } Ok(()) } fn random_u64_deviation(original: u64) -> u64 { let seed = sp_io::offchain::random_seed(); let random = ::decode(&mut TrailingZeroInput::new(seed.as_ref())) .expect("input is padded with zeroes; qed"); let maximum = Perbill::one() .saturating_div( Perbill::from_parts(PERCENT_DIVISOR), sp_runtime::Rounding::Down, ) .deconstruct(); let random = Perbill::from_parts(random % maximum); original.saturating_add(random.mul_ceil(original)) } fn local_authorities() -> impl Iterator { let authorities = Authorities::::get(); let mut local_authorities = T::AuthorityId::all(); local_authorities.sort(); authorities.into_iter().enumerate().filter_map(move |(index, authority)| { local_authorities .binary_search(&authority) .ok() .map(|location| (index as u32, local_authorities[location].clone())) }) } fn fetch_from_remote( rpc_endpoint: Vec, request_body: Vec, ) -> OffchainResult> { let rpc_str = core::str::from_utf8(&rpc_endpoint) .expect("rpc endpoint valid str; qed"); let body_str = core::str::from_utf8(&request_body) .expect("request body valid str: qed"); let deadline = sp_io::offchain::timestamp() .add(rt_offchain::Duration::from_millis(FETCH_TIMEOUT_PERIOD)); let pending = rt_offchain::http::Request::post(&rpc_str, vec![body_str]) .add_header("ACCEPT", "APPLICATION/JSON") .add_header("CONTENT-TYPE", "APPLICATION/JSON") .deadline(deadline) .send() .map_err(|err| OffchainErr::HttpRequestError(err))?; let response = pending .try_wait(deadline) .map_err(|_| OffchainErr::RequestUncompleted)? .map_err(|_| OffchainErr::RequestUncompleted)?; if response.code != 200 { return Err(OffchainErr::HttpResponseNotOk(response.code)) } Ok(response.body().collect::>()) } fn prepare_request_body( maybe_block: Option, finality_delay: u64, gatekeeper: Vec, topic_name: Vec, ) -> Vec { match maybe_block { Some(to_block) => { let from_block: u64 = to_block.saturating_sub(finality_delay); let convert_number_to_hex_vector = |value: u64| -> Vec { let mut r = Vec::new(); let mut value = value; loop { r.push((value % 10 + 48) as u8); value /= 10; if value == 0 { break; } } r.reverse(); r }; let mut body = b"{\"id\":0,\"jsonrpc\":\"2.0\",\"method\":\"eth_getLogs\",\"params\":[{".to_vec(); body.extend(b"\"fromBlock\":".to_vec()); body.extend(convert_number_to_hex_vector(from_block)); body.extend(b",\"toBlock\":".to_vec()); body.extend(convert_number_to_hex_vector(to_block)); body.extend(b",\"address\":\"".to_vec()); body.extend(gatekeeper); body.extend(b"\",\"topics\":[\"".to_vec()); body.extend(topic_name); body.extend(b"\"]}]}".to_vec()); body }, None => b"{\"id\":0,\"jsonrpc\":\"2.0\",\"method\":\"eth_blockNumber\"}".to_vec() }.to_vec() } fn fetch_and_parse( evm_block: Option, finality_delay: u64, gatekeeper: Vec, topic_name: Vec, rpc_endpoint: Vec, ) -> OffchainResult { let request_body = Self::prepare_request_body( evm_block, finality_delay, gatekeeper, topic_name); let resp_bytes = Self::fetch_from_remote(rpc_endpoint, request_body)?; let resp_str = sp_std::str::from_utf8(&resp_bytes) .map_err(|_| OffchainErr::HttpBytesParsingError)?; let result: EvmResponse = serde_json::from_str(&resp_str) .map_err(|_| OffchainErr::HttpJsonParsingError)?; if result.error.is_some() { return Err(OffchainErr::ErrorInEvmResponse); } Ok(result.result.ok_or(OffchainErr::ErrorInEvmResponse)?) } fn is_good_actor( authority_index: usize, session_index: SessionIndex, average_claps: u32, ) -> bool { let claps_in_session = AuthorityInfoInSession::::get(session_index); match claps_in_session.get(authority_index) { Some(clap_in_session) => { let number_of_claps = &clap_in_session.actions; let authority_deviation = if *number_of_claps < average_claps { Perbill::from_rational(*number_of_claps, average_claps) } else { Perbill::from_rational(average_claps, *number_of_claps) }; authority_deviation < Perbill::from_percent(T::OffenceThreshold::get()) }, _ => false, } } fn initialize_authorities(authorities: &[T::AuthorityId]) { if !authorities.is_empty() { assert!(Authorities::::get().is_empty(), "Authorities are already initilized!"); let bounded_authorities = BoundedSlice::<'_, _, T::MaxAuthorities>::try_from(authorities) .expect("more than the maximum number of clappers"); Authorities::::put(bounded_authorities); } } fn restart_clap_round(session_index: SessionIndex, length: usize) { let empty_authority_info = SessionAuthorityInfo { actions: 0u32, disabled: false, }; let empty_authorities_vec = BoundedVec::::try_from( vec![empty_authority_info; length]) .expect("authorities length should be correct"); AuthorityInfoInSession::::set(session_index, empty_authorities_vec); } // #[cfg(test)] // fn set_authorities(authorities: Vec) { // let bounded_authorities = WeakBoundedVec::<_, T::MaxAuthorities>::try_from(authorities) // .expect("more than the maximum number of clappers"); // Authorities::::put(&bounded_authorities); // Self::restart_clap_round(T::ValidatorSet::session_index(), bounded_authorities.len()); // } } impl sp_runtime::BoundToRuntimeAppPublic for Pallet { type Public = T::AuthorityId; } impl BlockNumberProvider for Pallet { type BlockNumber = BlockNumberFor; fn current_block_number() -> Self::BlockNumber { T::BlockNumberProvider::current_block_number() } } impl OneSessionHandler for Pallet { type Key = T::AuthorityId; fn on_genesis_session<'a, I: 'a>(validators: I) where I: Iterator, { let authorities = validators.map(|x| x.1).collect::>(); Self::initialize_authorities(&authorities); Self::restart_clap_round(1, authorities.len()); } fn on_new_session<'a, I: 'a>(_changed: bool, validators: I, _queued_validators: I) where I: Iterator, { let authorities = validators.map(|x| x.1).collect::>(); let bounded_authorities = WeakBoundedVec::<_, T::MaxAuthorities>::force_from( authorities, Some( "Warning: The session has more authorities than expected. \ A runtime configuration adjustment may be needed.", ), ); let bounded_authorities_len = (&bounded_authorities).len(); Authorities::::put(bounded_authorities); Self::restart_clap_round(T::ValidatorSet::session_index(), bounded_authorities_len); } fn on_before_session_ending() { let session_index = T::ValidatorSet::session_index(); let validators = T::ValidatorSet::validators(); let authorities = Authorities::::get(); let claps_in_session = AuthorityInfoInSession::::get(session_index); let average_claps = claps_in_session .iter() .filter(|x| !x.disabled) .fold(0u32, |acc, x| acc + x.actions) .checked_div(claps_in_session.len() as u32) .unwrap_or_default(); let offenders = validators .into_iter() .enumerate() .filter(|(index, _)| !Self::is_good_actor(*index, session_index, average_claps)) .filter_map(|(_, id)| { >::IdentificationOf::convert( id.clone(), ).map(|full_id| (id, full_id)) }) .collect::>>(); if offenders.is_empty() { Self::deposit_event(Event::::AllGood); } else { Self::deposit_event(Event::::SomeTrottling { throttling: offenders.clone() }); let validator_set_count = authorities.len() as u32; let offence = ThrottlingOffence { session_index, validator_set_count, offenders }; if let Err(e) = T::ReportUnresponsiveness::report_offence(vec![], offence) { sp_runtime::print(e) } } } fn on_disabled(validator_index: u32) { let session_index = T::ValidatorSet::session_index(); let mut claps_in_session = AuthorityInfoInSession::::get(&session_index); if let Some(_) = claps_in_session.get(validator_index as usize) { claps_in_session[validator_index as usize].disabled = true; AuthorityInfoInSession::::set(&session_index, claps_in_session); } } } #[derive(RuntimeDebug, TypeInfo)] #[cfg_attr(feature = "std", derive(Clone, PartialEq, Eq))] pub struct ThrottlingOffence { /// Current session index in which we report the unresponsive validators. pub session_index: SessionIndex, /// The size pf the validator set in current session. pub validator_set_count: u32, /// Authorities that were unresponsive during the current session. pub offenders: Vec } impl Offence for ThrottlingOffence { const ID: Kind = *b"slow-clap:throtl"; type TimeSlot = SessionIndex; fn offenders(&self) -> Vec { self.offenders.clone() } fn session_index(&self) -> SessionIndex { self.session_index } fn validator_set_count(&self) -> u32 { self.validator_set_count } fn time_slot(&self) -> Self::TimeSlot { self.session_index } fn slash_fraction(&self, offenders_count: u32) -> Perbill { if let Some(threshold) = offenders_count.checked_sub(self.validator_set_count / 10 + 1) { let x = Perbill::from_rational(3 * threshold, self.validator_set_count); x.saturating_mul(Perbill::from_percent(7)) } else { Perbill::default() } } }