// Ensure we're `no_std` when compiling for Wasm. #![cfg_attr(not(feature = "std"), no_std)] use core::usize; 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, storage::StorageValueRef, storage_lock::{StorageLock, Time}, HttpError, }, traits::{BlockNumberProvider, Convert, Saturating, TrailingZeroInput}, 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 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 COMMITMENT_DELAY_MILLIS: u64 = 600_000; const ONE_HOUR_MILLIS: u64 = 3_600_000; pub type AuthIndex = u32; #[derive( RuntimeDebug, Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Encode, Decode, TypeInfo, MaxEncodedLen, )] pub struct CommitmentDetails { pub last_stored_block: u64, pub last_updated: u64, pub commits: u64, } #[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 commitment: CommitmentDetails, } #[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, } #[derive(Clone, PartialEq, Encode, Decode, RuntimeDebug, TypeInfo)] pub struct ApplauseDetail { pub network_id: NetworkId, pub authorities: BitMap, pub clapped_amount: Balance, pub block_number: u64, pub finalized: bool, } impl ApplauseDetail { pub fn new(network_id: NetworkId, block_number: u64, 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: u64, } #[cfg_attr(test, derive(PartialEq))] enum OffchainErr { HttpJsonParsingError, HttpBytesParsingError, HttpRequestError(HttpError), RequestUncompleted, HttpResponseNotOk(u16), ErrorInEvmResponse, NoStoredNetworks, 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::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::StorageRetrievalError(ref network_id) => write!( fmt, "Storage value found for network #{:?} but it's undecodable.", network_id ), 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::NoStoredNetworks => { write!(fmt, "No networks stored for the offchain slow claps.") } 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(2); #[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; 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: u64, }, BlockCommited { authority_id: AuthIndex, network_id: NetworkIdOf, }, } #[pallet::error] pub enum Error { NotEnoughClaps, AlreadyClapped, UnregistedNetwork, UnregisteredClapRemove, TooMuchAuthorities, CouldNotAccumulateCommission, CouldNotAccumulateIncomingImbalance, CouldNotIncreaseGatekeeperAmount, NonExistentAuthorityIndex, TimeWentBackwards, DisabledAuthority, ExecutedBlockIsHigher, } #[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, u64, 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 offchain_worker(now: BlockNumberFor) { if let Err(e) = Self::start_slow_clapping(now) { log::info!( target: LOG_TARGET, "👻 Skipping slow clap at {:?}: {:?}", 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.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 u64_to_hexadecimal_bytes(value: u64) -> 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::::get(&prev_session_index, &clap_unique_hash).is_some() { 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::get(&network_id).is_some(), Error::::UnregistedNetwork ); let (session_index, clap_unique_hash) = Self::mended_session_index(&clap); let authorities = Authorities::::get(&session_index); let authorities_length = authorities.len(); let is_disabled = DisabledAuthorityIndexes::::get(&session_index) .map(|bitmap| bitmap.exists(&clap.authority_index)) .unwrap_or(true); ensure!(!is_disabled, Error::::DisabledAuthority); let applause_threshold = Perbill::from_parts(T::ApplauseThreshold::get()); let threshold_amount = applause_threshold.mul_floor(TotalExposure::::get()); let maybe_account_id = T::ExposureListener::get_account_by_index(clap.authority_index as usize); ensure!( maybe_account_id.is_some(), Error::::NonExistentAuthorityIndex ); let account_id = maybe_account_id.unwrap(); let new_clapped_amount = T::ExposureListener::get_validator_exposure(&account_id); let mut applause_details = match ApplauseDetails::::take(&session_index, &clap_unique_hash) { Some(applause_details) => applause_details, None => { ensure!( LatestExecutedBlock::::get(&network_id) <= clap.block_number, Error::::ExecutedBlockIsHigher, ); 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 { applause_details.finalized = true; if let Err(e) = Self::try_applause(&clap) { sp_runtime::print(e); } } ApplauseDetails::::insert(&session_index, &clap_unique_hash, applause_details); Ok(()) } fn try_applause(clap: &Clap, BalanceOf>) -> DispatchResult { if T::NetworkDataHandler::is_nullification_period() { return Ok(()); } 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; ensure!( T::NetworkDataHandler::get(&network_id).is_some(), Error::::UnregistedNetwork ); let current_commits = BlockCommitments::::try_mutate( &network_id, |current_commitments| -> Result { let mut new_commitment_details = new_commitment.commitment; let (current_commits, current_last_updated) = current_commitments .get(&authority_index) .map(|details| (details.commits + 1, details.last_updated)) .unwrap_or((1, 0)); ensure!( new_commitment_details.last_updated > current_last_updated, Error::::TimeWentBackwards ); new_commitment_details.commits = current_commits; current_commitments.insert(authority_index, new_commitment_details); Ok(current_commits) }, )?; Self::deposit_event(Event::::BlockCommited { network_id, authority_id: authority_index, }); let session_index = T::ValidatorSet::session_index(); let validators = Validators::::get(&session_index); let block_commitments = BlockCommitments::::get(&network_id); let disabled_bitmap = DisabledAuthorityIndexes::::get(&session_index) .unwrap_or(BitMap::new(validators.len() as u32)); let max_block_deviation = T::NetworkDataHandler::get(&network_id) .map(|network| { ONE_HOUR_MILLIS .saturating_mul(6) .saturating_div(network.avg_block_speed) }) .unwrap_or_default(); if current_commits > 2 && validators.len() > 0 { let offence_bitmap = Self::capture_deviation_in_commitments_and_remove( &disabled_bitmap, &block_commitments, &validators, max_block_deviation, ); let offence_type = OffenceType::CommitmentOffence; Self::try_offend_validators( &session_index, &validators, offence_bitmap, disabled_bitmap, offence_type, ); } Ok(()) } fn start_slow_clapping(block_number: BlockNumberFor) -> OffchainResult { let session_index = T::ValidatorSet::session_index(); let networks_len = T::NetworkDataHandler::iter().count(); let network_in_use = T::NetworkDataHandler::iter() .nth( block_number .into() .as_usize() .checked_rem(networks_len) .unwrap_or_default(), ) .ok_or(OffchainErr::NoStoredNetworks)?; let network_id_encoded = network_in_use.0.encode(); let rate_limit_delay_key = Self::create_storage_key(b"rate-limit-", &network_id_encoded); let rate_limit_delay = Self::read_persistent_offchain_storage( &rate_limit_delay_key, network_in_use.1.rate_limit_delay, ); let network_lock_key = Self::create_storage_key(b"network-lock-", &network_id_encoded); let block_until = rt_offchain::Duration::from_millis(rate_limit_delay.max(MIN_LOCK_GUARD_PERIOD)); let mut network_lock = StorageLock::