// 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}, EstimateNextSessionRotation, ValidatorSet, ValidatorSetWithIdentification, OneSessionHandler, Get, }, WeakBoundedVec, }; use frame_system::{ offchain::{SendTransactionTypes, SubmitTransaction}, pallet_prelude::*, }; use sp_core::H256; use sp_runtime::{ Perbill, RuntimeAppPublic, RuntimeDebug, SaturatedConversion, offchain::{ self as rt_offchain, HttpError, storage::{MutateStorageError, StorageRetrievalError, StorageValueRef}, storage_lock::{StorageLock, Time}, }, traits::{BlockNumberProvider, Convert, Saturating}, }; use sp_std::{ vec::Vec, prelude::*, collections::btree_map::BTreeMap, }; use sp_staking::{ offence::{Kind, Offence, ReportOffence}, SessionIndex, }; use ghost_networks::{ NetworkData, NetworkDataBasicHandler, NetworkDataInspectHandler, NetworkDataMutateHandler, NetworkType, }; 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! { 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 FETCH_TIMEOUT_PERIOD: u64 = 3_000; const LOCK_BLOCK_EXPIRATION: u64 = 10; const NUMBER_OF_TOPICS: usize = 3; 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, } impl Log { fn is_sufficient(&self) -> bool { self.transaction_hash.is_some() && self.block_number.is_some() && self.topics.len() == NUMBER_OF_TOPICS } } 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, } #[derive(Default, PartialEq, Encode, Decode, RuntimeDebug, TypeInfo)] pub struct SessionAuthorityInfo { pub claps: u32, pub disabled: bool, } #[cfg_attr(test, derive(PartialEq))] enum OffchainErr { FailedSigning, SubmitTransaction, HttpJsonParsingError, HttpBytesParsingError, HttpRequestError(HttpError), RequestUncompleted, HttpResponseNotOk(u16), ErrorInEvmResponse, NoStoredNetworks, NotValidator, StorageRetrievalError(NetworkId), ConcurrentModificationError(NetworkId), UtxoNotImplemented(NetworkId), UnknownNetworkType(NetworkId), OffchainTimeoutPeriod(NetworkId), TooManyRequests(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::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::ConcurrentModificationError(ref network_id) => write!(fmt, "The underlying DB failed to update due to a concurrent modification for network #{:?}.", network_id), 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::NotValidator => write!(fmt, "Not a validator for slow clap, `--validator` flag needed."), 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), OffchainErr::TooManyRequests(ref network_id) => write!(fmt, "Too many requests over RPC endpoint 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::*; 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 ApplauseThreshold: Get; #[pallet::constant] type OffenceThreshold: Get; #[pallet::constant] type UnsignedPriority: Get; #[pallet::constant] type HistoryDepth: Get; type WeightInfo: WeightInfo; } #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event { AuthoritiesEquilibrium, SomeAuthoritiesTrottling { throttling: Vec> }, Clapped { authority_id: AuthIndex, network_id: NetworkIdOf, transaction_hash: H256, receiver: T::AccountId, amount: BalanceOf, }, Applaused { network_id: NetworkIdOf, receiver: T::AccountId, received_amount: BalanceOf, }, } #[pallet::error] pub enum Error { NotEnoughClaps, NotAnAuthority, CurrentValidatorIsDisabled, AlreadyClapped, UnregisteredClapRemove, TooMuchAuthorities, CouldNotAccumulateCommission, CouldNotAccumulateIncomingImbalance, CouldNotIncreaseGatekeeperAmount, } #[pallet::storage] #[pallet::getter(fn received_claps)] pub(super) type ReceivedClaps = StorageNMap< _, ( NMapKey, NMapKey, NMapKey, ), BoundedBTreeSet, ValueQuery >; #[pallet::storage] #[pallet::getter(fn applauses_for_transaction)] pub(super) type ApplausesForTransaction = StorageNMap< _, ( NMapKey, NMapKey, NMapKey, ), bool, ValueQuery >; #[pallet::storage] #[pallet::getter(fn claps_in_session)] pub(super) type ClapsInSession = StorageMap< _, Twox64Concat, SessionIndex, BTreeMap, ValueQuery, >; #[pallet::storage] #[pallet::getter(fn authorities)] pub(super) type Authorities = StorageMap< _, Twox64Concat, SessionIndex, WeakBoundedVec, 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::self_applause())] pub fn self_applause( origin: OriginFor, network_id: NetworkIdOf, session_index: SessionIndex, transaction_hash: H256, receiver: T::AccountId, amount: BalanceOf, ) -> DispatchResult { let _ = ensure_signed(origin)?; Self::applause_if_posible( network_id, session_index, transaction_hash, receiver, amount, ) } } #[pallet::hooks] impl Hooks> for Pallet { fn offchain_worker(now: BlockNumberFor) { match Self::start_slow_clapping(now) { Ok(iter) => for result in iter.into_iter() { if let Err(e) = result { log::info!( target: LOG_TARGET, "👏 Skipping slow clap at {:?}: {:?}", now, e, ) } }, Err(e) => log::info!( target: LOG_TARGET, "👏 Could not start slow clap at {:?}: {:?}", now, e, ), } } } #[pallet::validate_unsigned] impl ValidateUnsigned for Pallet { type Call = Call; fn validate_unsigned(_source: TransactionSource, call: &Self::Call) -> TransactionValidity { if let Call::slow_clap { clap, signature } = call { let authorities = Authorities::::get(&clap.session_index); let authority = match authorities.get(clap.authority_index as usize) { Some(authority) => authority, None => return InvalidTransaction::BadSigner.into(), }; let signature_valid = clap.using_encoded(|encoded_clap| { authority.verify(&encoded_clap, signature) }); if !signature_valid { return InvalidTransaction::BadProof.into(); } ValidTransaction::with_tag_prefix("SlowClap") .priority(T::UnsignedPriority::get()) .and_provides(signature) .longevity(LOCK_BLOCK_EXPIRATION) .propagate(true) .build() } else { 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( receiver: &T::AccountId, amount: &BalanceOf, network_id: &NetworkIdOf, ) -> H256 { let mut clap_args_str = receiver.encode(); clap_args_str.extend(&amount.encode()); clap_args_str.extend(&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 try_slow_clap(clap: &Clap, BalanceOf>) -> DispatchResult { let authorities = Authorities::::get(&clap.session_index); ensure!(authorities.get(clap.authority_index as usize).is_some(), Error::::NotAnAuthority); let clap_unique_hash = Self::generate_unique_hash(&clap.receiver, &clap.amount, &clap.network_id); let received_claps_key = (clap.session_index, &clap.transaction_hash, &clap_unique_hash); let number_of_received_claps = ReceivedClaps::::try_mutate(&received_claps_key, |tree_of_claps| { let number_of_claps = tree_of_claps.len(); match (tree_of_claps.contains(&clap.authority_index), clap.removed) { (true, true) => tree_of_claps .remove(&clap.authority_index) .then(|| number_of_claps.saturating_sub(1)) .ok_or(Error::::UnregisteredClapRemove), (true, false) => Err(Error::::AlreadyClapped), (false, true) => Err(Error::::UnregisteredClapRemove), (false, false) => tree_of_claps .try_insert(clap.authority_index) .map(|_| number_of_claps.saturating_add(1)) .map_err(|_| Error::::TooMuchAuthorities), } })?; ClapsInSession::::try_mutate(&clap.session_index, |claps_details| { if claps_details.get(&clap.authority_index).map(|x| x.disabled).unwrap_or_default() { return Err(Error::::CurrentValidatorIsDisabled); } (*claps_details) .entry(clap.authority_index) .and_modify(|individual| (*individual).claps.saturating_inc()) .or_insert(SessionAuthorityInfo { claps: 1u32, disabled: false }); Ok(()) })?; Self::deposit_event(Event::::Clapped { authority_id: clap.authority_index, network_id: clap.network_id, transaction_hash: clap.transaction_hash, receiver: clap.receiver.clone(), amount: clap.amount, }); let enough_authorities = Perbill::from_rational( number_of_received_claps as u32, authorities.len() as u32, ) > Perbill::from_percent(T::ApplauseThreshold::get()); if enough_authorities { let _ = Self::try_applause(&clap, &received_claps_key) .inspect_err(|error_msg| log::info!( target: LOG_TARGET, "👏 Could not applause because of: {:?}", error_msg, )); } Ok(()) } fn try_applause( clap: &Clap, BalanceOf>, received_claps_key: &(SessionIndex, &H256, &H256), ) -> DispatchResult { ApplausesForTransaction::::try_mutate(received_claps_key, |is_applaused| { if *is_applaused || 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)?; if final_amount > T::Currency::minimum_balance() { T::Currency::mint_into( &clap.receiver, final_amount )?; } *is_applaused = true; Self::deposit_event(Event::::Applaused { network_id: clap.network_id, receiver: clap.receiver.clone(), received_amount: final_amount, }); Ok(()) }) } fn applause_if_posible( network_id: NetworkIdOf, session_index: SessionIndex, transaction_hash: H256, receiver: T::AccountId, amount: BalanceOf, ) -> DispatchResult{ let clap_unique_hash = Self::generate_unique_hash(&receiver, &amount, &network_id); let received_claps_key = (session_index, &transaction_hash, &clap_unique_hash); let clap = Clap { authority_index: Default::default(), block_number: Default::default(), removed: false, session_index, network_id, receiver, amount, transaction_hash, }; let enough_authorities = Perbill::from_rational( ReceivedClaps::::get(&received_claps_key).len() as u32, Authorities::::get(session_index).len() as u32, ) > Perbill::from_percent(T::ApplauseThreshold::get()); ensure!(enough_authorities, Error::::NotEnoughClaps); Self::try_applause(&clap, &received_claps_key)?; Ok(()) } fn start_slow_clapping( block_number: BlockNumberFor ) -> OffchainResult>> { sp_io::offchain::is_validator() .then(|| ()) .ok_or(OffchainErr::NotValidator)?; 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 last_timestamp_key = Self::create_storage_key(b"last-timestamp-", &network_id_encoded); 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(networks_len as u64 * FETCH_TIMEOUT_PERIOD); let mut network_lock = StorageLock::