diff --git a/pallets/slow-clap/Cargo.toml b/pallets/slow-clap/Cargo.toml index cd8c3dd..f41da26 100644 --- a/pallets/slow-clap/Cargo.toml +++ b/pallets/slow-clap/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ghost-slow-clap" -version = "0.3.57" +version = "0.4.0" description = "Applause protocol for the EVM bridge" license.workspace = true authors.workspace = true @@ -28,6 +28,7 @@ sp-io = { workspace = true } sp-std = { workspace = true } ghost-networks = { workspace = true } +ghost-traits = { workspace = true } [dev-dependencies] pallet-balances = { workspace = true } diff --git a/pallets/slow-clap/src/benchmarking.rs b/pallets/slow-clap/src/benchmarking.rs index 960db1e..f664c58 100644 --- a/pallets/slow-clap/src/benchmarking.rs +++ b/pallets/slow-clap/src/benchmarking.rs @@ -3,7 +3,6 @@ use super::*; use frame_benchmarking::v1::*; -use frame_support::traits::fungible::Inspect; use frame_system::RawOrigin; pub fn create_account() -> T::AccountId { @@ -12,16 +11,35 @@ pub fn create_account() -> T::AccountId { .expect("32 bytes always construct an AccountId32") } +pub fn prepare_evm_network(network_id: NetworkIdOf) { + let network_data = NetworkData { + chain_name: "Ethereum".into(), + default_endpoints: vec![b"https://other.endpoint.network.com".to_vec()], + finality_delay: 69, + avg_block_speed: 69, + rate_limit_delay: 69, + block_distance: 69, + network_type: ghost_networks::NetworkType::Evm, + gatekeeper: b"0x4d224452801ACEd8B2F0aebE155379bb5D594381".to_vec(), + topic_name: b"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef".to_vec(), + incoming_fee: 0, + outgoing_fee: 0, + }; + + let _ = T::NetworkDataHandler::register(network_id, network_data.clone()); +} + benchmarks! { slow_clap { + let network_id = NetworkIdOf::::default(); + prepare_evm_network::(network_id); + let minimum_balance = <::Currency>::minimum_balance(); let receiver = create_account::(); let amount = minimum_balance + minimum_balance; - let network_id = NetworkIdOf::::default(); let session_index = T::ValidatorSet::session_index(); let transaction_hash = H256::repeat_byte(1u8); - let args_hash = Pallet::::generate_unique_hash(&receiver, &amount, &network_id); let authorities = vec![T::AuthorityId::generate_pair(None)]; let bounded_authorities = WeakBoundedVec::<_, T::MaxAuthorities>::try_from(authorities.clone()) @@ -39,6 +57,7 @@ benchmarks! { receiver: receiver.clone(), amount, }; + let args_hash = Pallet::::generate_unique_hash(&clap); let authority_id = authorities .get(authority_index as usize) @@ -46,16 +65,20 @@ benchmarks! { let signature = authority_id.sign(&clap.encode()) .ok_or("couldn't make signature")?; + let empty_bitmap = BitMap::new(1); + DisabledAuthorityIndexes::::insert(session_index, empty_bitmap); + + assert_eq!(ApplauseDetails::::get(&session_index, &args_hash).is_none(), true); }: _(RawOrigin::None, clap, signature) verify { - let clap_key = (session_index, transaction_hash, args_hash); - assert_eq!(ReceivedClaps::::get(&clap_key).get(&authority_index).is_some(), true); + assert_eq!(ApplauseDetails::::get(&session_index, &args_hash).is_some(), true); assert_eq!(<::Currency>::total_balance(&receiver), amount); } commit_block { let session_index = T::ValidatorSet::session_index(); let network_id = NetworkIdOf::::default(); + prepare_evm_network::(network_id); let authorities = vec![T::AuthorityId::generate_pair(None)]; let bounded_authorities = WeakBoundedVec::<_, T::MaxAuthorities>::try_from(authorities.clone()) @@ -68,8 +91,8 @@ benchmarks! { authority_index, network_id, commitment: CommitmentDetails { - last_registered_block: 69, - last_seen_block: 420, + last_stored_block: 69, + commits: 420, last_updated: 1337, } }; @@ -86,8 +109,8 @@ benchmarks! { .get(&authority_index) .cloned() .unwrap_or_default(); - assert_eq!(stored_commitment.last_registered_block, 69); - assert_eq!(stored_commitment.last_seen_block, 420); + assert_eq!(stored_commitment.last_stored_block, 69); + assert_eq!(stored_commitment.commits, 1); assert_eq!(stored_commitment.last_updated, 1337); } diff --git a/pallets/slow-clap/src/evm_types.rs b/pallets/slow-clap/src/evm_types.rs index 330be9e..0115a4a 100644 --- a/pallets/slow-clap/src/evm_types.rs +++ b/pallets/slow-clap/src/evm_types.rs @@ -54,19 +54,14 @@ impl EvmResponseType { network_id: NetworkIdOf, ) -> BlockCommitment> { let last_updated = sp_io::offchain::timestamp().unix_millis(); - let current_block = match self { - EvmResponseType::BlockNumber(block_number) => *block_number, - EvmResponseType::TransactionLogs(_) => Default::default(), - }; - BlockCommitment { session_index, authority_index, network_id, commitment: CommitmentDetails { - last_registered_block: from_block, - last_seen_block: current_block, + last_stored_block: from_block, last_updated, + commits: 420, }, } } diff --git a/pallets/slow-clap/src/lib.rs b/pallets/slow-clap/src/lib.rs index d88a2e6..0196b4c 100644 --- a/pallets/slow-clap/src/lib.rs +++ b/pallets/slow-clap/src/lib.rs @@ -1,6 +1,8 @@ // 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}; @@ -28,7 +30,7 @@ use sp_runtime::{ storage_lock::{StorageLock, Time}, HttpError, }, - traits::{AtLeast32BitUnsigned, BlockNumberProvider, Convert, Saturating, TrailingZeroInput}, + traits::{BlockNumberProvider, Convert, Saturating, TrailingZeroInput}, Perbill, RuntimeAppPublic, RuntimeDebug, }; use sp_staking::{ @@ -37,6 +39,7 @@ use sp_staking::{ }; use sp_std::{collections::btree_map::BTreeMap, prelude::*, vec::Vec}; +use ghost_traits::exposure::ExposureListener; use ghost_networks::{ NetworkData, NetworkDataBasicHandler, NetworkDataInspectHandler, NetworkDataMutateHandler, NetworkType, @@ -50,8 +53,10 @@ mod tests; mod deserialisations; mod evm_types; +mod bitmap; use evm_types::{EvmResponse, EvmResponseType}; +use bitmap::{Bucket, BitMap}; pub mod sr25519 { mod app_sr25519 { @@ -71,10 +76,12 @@ pub mod sr25519 { 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 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; @@ -93,9 +100,9 @@ pub type AuthIndex = u32; MaxEncodedLen, )] pub struct CommitmentDetails { - pub last_registered_block: u64, - pub last_seen_block: u64, + pub last_stored_block: u64, pub last_updated: u64, + pub commits: u64, } #[derive( @@ -122,10 +129,33 @@ pub struct Clap { 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 SessionAuthorityInfo { - pub claps: u32, - pub disabled: bool, +pub struct PreparedApplause { + pub network_id: NetworkId, + pub receiver: AccountId, + pub amount: Balance, + pub block_number: u64, } #[cfg_attr(test, derive(PartialEq))] @@ -218,12 +248,6 @@ pub type IdentificationTuple = ( type OffchainResult = Result>>; -pub trait ApplauseListener { - fn get_current_era() -> EraIndex; - fn get_threshold_amount(era: EraIndex) -> Balance; - fn get_validator_total_exposure(era: EraIndex, index: usize) -> Balance; -} - #[frame_support::pallet] pub mod pallet { use super::*; @@ -254,10 +278,10 @@ pub mod pallet { type ReportUnresponsiveness: ReportOffence< Self::AccountId, IdentificationTuple, - ThrottlingOffence>, + SlowClapOffence>, >; type DisabledValidators: DisabledValidators; - type ApplauseListener: ApplauseListener>; + type ExposureListener: ExposureListener, EraIndex, AuthIndex>; #[pallet::constant] type MaxAuthorities: Get; @@ -265,9 +289,6 @@ pub mod pallet { #[pallet::constant] type ApplauseThreshold: Get; - #[pallet::constant] - type OffenceThreshold: Get; - #[pallet::constant] type UnsignedPriority: Get; @@ -284,21 +305,27 @@ pub mod pallet { #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event { BlackSwan, - AuthoritiesEquilibrium, + AuthoritiesCommitmentEquilibrium, + AuthoritiesApplauseEquilibrium, + SomeAuthoritiesDelayed { + delayed: Vec>, + }, SomeAuthoritiesTrottling { throttling: Vec>, }, Clapped { authority_id: AuthIndex, network_id: NetworkIdOf, - transaction_hash: H256, + 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, @@ -310,6 +337,7 @@ pub mod pallet { pub enum Error { NotEnoughClaps, AlreadyClapped, + UnregistedNetwork, UnregisteredClapRemove, TooMuchAuthorities, CouldNotAccumulateCommission, @@ -317,18 +345,21 @@ pub mod pallet { CouldNotIncreaseGatekeeperAmount, NonExistentAuthorityIndex, TimeWentBackwards, + DisabledAuthority, + ExecutedBlockIsHigher, } #[pallet::storage] - #[pallet::getter(fn clapped_amount)] - pub(super) type ClappedAmount = StorageNMap< + #[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< _, - ( - NMapKey, - NMapKey, - NMapKey, - ), - BalanceOf, + Twox64Concat, + NetworkIdOf, + u64, ValueQuery, >; @@ -343,39 +374,25 @@ pub mod pallet { >; #[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< + #[pallet::getter(fn applause_details)] + pub(super) type ApplauseDetails = StorageDoubleMap< _, Twox64Concat, SessionIndex, - BTreeMap, - ValueQuery, + Twox64Concat, + H256, + ApplauseDetail, BalanceOf>, + OptionQuery + >; + + #[pallet::storage] + #[pallet::getter(fn disabled_authority_indexes)] + pub(super) type DisabledAuthorityIndexes = StorageMap< + _, + Twox64Concat, + SessionIndex, + BitMap, + OptionQuery, >; #[pallet::storage] @@ -395,7 +412,7 @@ pub mod pallet { Twox64Concat, SessionIndex, WeakBoundedVec, T::MaxAuthorities>, - OptionQuery, + ValueQuery, >; #[pallet::genesis_config] @@ -463,23 +480,17 @@ pub mod pallet { type Call = Call; fn validate_unsigned(_source: TransactionSource, call: &Self::Call) -> TransactionValidity { match call { - Call::commit_block { - block_commitment, - signature, - } => { - let authorities = Authorities::::get(&block_commitment.session_index); - let authority = match authorities.get(block_commitment.authority_index as usize) - { - Some(authority) => authority, + 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(), - }; - - let signature_valid = block_commitment.using_encoded(|encoded_commitment| { - authority.verify(&encoded_commitment, signature) - }); - - if !signature_valid { - return InvalidTransaction::BadProof.into(); } ValidTransaction::with_tag_prefix("SlowClap") @@ -491,25 +502,14 @@ pub mod pallet { } Call::slow_clap { clap, signature } => { let (session_index, _) = Self::mended_session_index(&clap); - let authorities = Authorities::::get(&session_index); - let authority = match authorities.get(clap.authority_index as usize) { - Some(authority) => authority, + + 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(), - }; - - if ClapsInSession::::get(&session_index) - .get(&clap.authority_index) - .map(|info| info.disabled) - .unwrap_or_default() - { - 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") @@ -544,14 +544,11 @@ impl Pallet { .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()); + 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)[..]) } @@ -584,158 +581,183 @@ impl Pallet { clap: &Clap, BalanceOf>, ) -> (SessionIndex, H256) { let prev_session_index = clap.session_index.saturating_sub(1); - let clap_unique_hash = - Self::generate_unique_hash(&clap.receiver, &clap.amount, &clap.network_id); + let clap_unique_hash = Self::generate_unique_hash(&clap); - let received_claps_key = ( - prev_session_index, - &clap.transaction_hash, - &clap_unique_hash, - ); - - let session_index = ReceivedClaps::::get(&received_claps_key) - .is_empty() - .then(|| clap.session_index) - .unwrap_or(prev_session_index); + 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 mut claps_in_session = ClapsInSession::::get(&session_index); + let authorities_length = Authorities::::get(&session_index).len(); - let received_claps_key = (session_index, &clap.transaction_hash, &clap_unique_hash); + let is_disabled = DisabledAuthorityIndexes::::get(&session_index) + .map(|bitmap| bitmap.exists(&clap.authority_index)) + .unwrap_or(true); - 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), + ensure!(!is_disabled, Error::::DisabledAuthority); + + let applause_threshold = Perbill::from_parts(T::ApplauseThreshold::get()); + let threshold_amount = applause_threshold.mul_floor(TotalExposure::::get()); + let new_clapped_amount = T::ExposureListener::get_validator_exposure( + clap.authority_index, + ); + + 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) } - })?; + }; - claps_in_session - .entry(clap.authority_index) - .and_modify(|individual| individual.claps.saturating_inc()) - .or_insert(SessionAuthorityInfo { - claps: 1u32, - disabled: false, - }); + 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) + }; - ClapsInSession::::insert(&session_index, claps_in_session); + applause_details.clapped_amount = total_clapped; Self::deposit_event(Event::::Clapped { + network_id, authority_id: clap.authority_index, - network_id: clap.network_id, - transaction_hash: clap.transaction_hash, + clap_unique_hash, receiver: clap.receiver.clone(), amount: clap.amount, + removed: clap.removed, }); - let enough_authorities = - Self::validator_clap_by_amount(clap.authority_index, &received_claps_key); + let is_enough = applause_details.authorities + .count_ones() + .gt(&(authorities_length as u32 / 2)); - 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, - ) - }); + 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 validator_clap_by_amount( - authority_index: AuthIndex, - received_claps_key: &(SessionIndex, &H256, &H256), - ) -> bool { - let era = T::ApplauseListener::get_current_era(); - let threshold_amount = T::ApplauseListener::get_threshold_amount(era); - let new_clapped_amount = - T::ApplauseListener::get_validator_total_exposure(era, authority_index as usize); + fn try_applause(clap: &Clap, BalanceOf>) -> DispatchResult { + if T::NetworkDataHandler::is_nullification_period() { + return Ok(()); + } - let total_clapped = ClappedAmount::::mutate(received_claps_key, |clapped_amount| { - let total_clapped = clapped_amount.saturating_add(new_clapped_amount); - *clapped_amount = total_clapped; - total_clapped + 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 }); - total_clapped >= threshold_amount - } - - 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)?; - - let _ = T::Currency::deposit_creating(&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(()) - }) + Ok(()) } fn try_commit_block(new_commitment: &BlockCommitment>) -> DispatchResult { - BlockCommitments::::try_mutate(&new_commitment.network_id, |current_commitments| { - let authority_index = new_commitment.authority_index; - let new_commitment_details = new_commitment.commitment; + 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_last_updated = current_commitments + 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.last_updated) - .unwrap_or_default(); + .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); - Self::deposit_event(Event::::BlockCommited { - network_id: new_commitment.network_id, - authority_id: authority_index, - }); + Ok(current_commits) + })?; - Ok(()) - }) + 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 > 1 { + 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 { @@ -988,68 +1010,14 @@ impl Pallet { .ok_or(OffchainErr::ErrorInEvmResponse)?) } - fn calculate_median_claps( - actual_claps_in_session: &BTreeMap, - authorities_len: usize, - ) -> u32 { - let mut claps_in_session = (0..authorities_len) - .filter_map(|authority_index| { - let clap_info = actual_claps_in_session - .get(&(authority_index as AuthIndex)) - .copied() - .unwrap_or_default(); - (!clap_info.disabled).then(|| clap_info.claps) - }) - .collect::>(); - - if claps_in_session.is_empty() { - return 0; - } - - claps_in_session.sort(); - let number_of_claps = claps_in_session.len(); - - if number_of_claps % 2 == 0 { - let mid_left = claps_in_session[number_of_claps / 2 - 1]; - let mid_right = claps_in_session[number_of_claps / 2]; - (mid_left + mid_right) / 2 - } else { - claps_in_session[number_of_claps / 2] - } - } - - fn is_good_actor( - authority_index: usize, - median_claps: u32, - claps_in_session: &BTreeMap, - ) -> bool { - if median_claps == 0 { - return true; - } - - let number_of_claps = claps_in_session - .get(&(authority_index as AuthIndex)) - .copied() - .map(|info| match info.disabled { - true => median_claps, - false => info.claps, - }) - .unwrap_or_default(); - - let authority_deviation = if number_of_claps < median_claps { - Perbill::from_rational(median_claps - number_of_claps, median_claps) - } else { - Perbill::from_rational(number_of_claps - median_claps, median_claps) - }; - authority_deviation < Perbill::from_percent(T::OffenceThreshold::get()) - } - fn initialize_authorities(authorities: Vec) { let session_index = T::ValidatorSet::session_index(); assert!( Authorities::::get(&session_index).is_empty(), "Authorities are already initilized!" ); + + let authorities_len = authorities.len(); let bounded_authorities = WeakBoundedVec::<_, T::MaxAuthorities>::try_from(authorities) .expect("more than the maximum number of authorities"); @@ -1064,35 +1032,181 @@ impl Pallet { Validators::::insert(&session_index, bounded_validators); Authorities::::set(&session_index, bounded_authorities); - let mut disabled_validators: BTreeMap = Default::default(); - for disabled_index in T::DisabledValidators::disabled_validators().iter() { - let _ = disabled_validators.insert( - *disabled_index, - SessionAuthorityInfo { - claps: 0u32, - disabled: true, - }, - ); + let mut disabled_bitmap = BitMap::new(authorities_len as AuthIndex); + for disabled_index in T::DisabledValidators::disabled_validators() { + disabled_bitmap.set(disabled_index); } - ClapsInSession::::set(&session_index, disabled_validators); + DisabledAuthorityIndexes::::insert(session_index, disabled_bitmap); } fn clear_history(target_session_index: &SessionIndex) { - ClapsInSession::::remove(target_session_index); Authorities::::remove(target_session_index); Validators::::remove(target_session_index); - let mut cursor = ReceivedClaps::::clear_prefix((target_session_index,), u32::MAX, None); - debug_assert!(cursor.maybe_cursor.is_none()); - cursor = - ApplausesForTransaction::::clear_prefix((target_session_index,), u32::MAX, None); + DisabledAuthorityIndexes::::remove(target_session_index); + + let cursor = ApplauseDetails::::clear_prefix(target_session_index, u32::MAX, None); debug_assert!(cursor.maybe_cursor.is_none()); } - #[cfg(test)] - fn set_test_authorities(session_index: SessionIndex, authorities: Vec) { - let bounded_authorities = WeakBoundedVec::<_, T::MaxAuthorities>::try_from(authorities) - .expect("more than the maximum number of authorities"); - Authorities::::set(session_index, bounded_authorities); + fn calculate_weighted_median(values: &mut Vec<(AuthIndex, u64)>) -> u64 { + values.sort_by_key(|data| data.1); + + let mid = values.len() / 2; + if values.len() % 2 == 1 { + values[mid].1 + } else { + values[mid - 1].1 + } + } + + fn apply_median_deviation( + bitmap: &mut BitMap, + disabled: &BitMap, + values: &Vec<(AuthIndex, u64)>, + median: u64, + max_deviation: u64, + ) { + values.iter().for_each(|(authority_index, value)| { + if !disabled.exists(authority_index) && value.abs_diff(median) > max_deviation { + bitmap.set(*authority_index); + } + }) + } + + fn capture_deviation_in_commitments_and_remove( + disabled_bitmap: &BitMap, + block_commitments: &BTreeMap, + validators: &WeakBoundedVec, T::MaxAuthorities>, + max_block_deviation: u64, + ) -> BitMap { + let validators_len = validators.len() as u32; + + let mut delayed = BitMap::new(validators_len); + let mut stored_blocks: Vec<(AuthIndex, u64)> = Vec::new(); + let mut time_updates: Vec<(AuthIndex, u64)> = 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)); + time_updates.push((authority_index, data.last_updated)); + } + + let stored_block_median = Self::calculate_weighted_median(&mut stored_blocks); + let time_update_median = Self::calculate_weighted_median(&mut time_updates); + + Self::apply_median_deviation(&mut delayed, disabled_bitmap, &stored_blocks, stored_block_median, max_block_deviation); + Self::apply_median_deviation(&mut delayed, disabled_bitmap, &time_updates, time_update_median, ONE_HOUR_MILLIS); + + delayed + } + + fn get_cumulative_missing_clapped_amount( + disabled_bitmap: &BitMap, + session_index: &SessionIndex, + missed_validators: &mut BitMap, + ) -> Perbill { + let total_exposure = TotalExposure::::get(); + + let bucket_size = BitMap::size_of_bucket(); + let mut missed_percent = Perbill::default(); + + for applause_details in ApplauseDetails::::iter_prefix_values(session_index) { + let max_value = applause_details.authorities.max_value(); + let clapped_amount = applause_details.clapped_amount; + let is_finalized = applause_details.finalized as Bucket; + + for (bucket_id, bucket) in applause_details.authorities.iter_buckets() { + let bits_in_bucket = 1 << bucket_size; + let bucket_shift = bucket_id << bucket_size; + + let disabled_bucket = disabled_bitmap.get_bucket(bucket_id); + let missed_authorities = bucket ^ (Bucket::MAX * is_finalized); + let missed_authorities = missed_authorities & !disabled_bucket; + + for bit_position in 0..bits_in_bucket { + if (missed_authorities >> bit_position) & 1 == 0 { + continue; + } + + let position = bucket_shift + bit_position; + if position > max_value { + continue; + } + + missed_validators.set(position.into()); + missed_percent = missed_percent + .saturating_add(Perbill::from_rational( + total_exposure.saturating_sub(clapped_amount), + total_exposure, + )); + } + } + } + + missed_percent + } + + fn try_offend_validators( + session_index: &SessionIndex, + validators: &WeakBoundedVec, T::MaxAuthorities>, + offence_bitmap: BitMap, + disabled_bitmap: BitMap, + offence_type: OffenceType, + ) { + let validator_set_count = validators.len() as u32; + + let offenders = validators + .into_iter() + .enumerate() + .filter_map(|(index, id)| { + (offence_bitmap.exists(&(index as AuthIndex))).then(|| { + >::IdentificationOf::convert( + id.clone(), + ).map(|full_id| (id.clone(), full_id)) + }) + .flatten() + }) + .collect::>>(); + + let not_enough_validators_left = validator_set_count + .saturating_sub(disabled_bitmap + .bitor(offence_bitmap) + .count_ones() + ).lt(&T::MinAuthoritiesNumber::get()); + + if not_enough_validators_left && offenders.len() > 0 { + Self::deposit_event(Event::::BlackSwan); + return; + } + + if offenders.len() == 0 { + let equilibrium_event = match offence_type { + OffenceType::CommitmentOffence => Event::::AuthoritiesCommitmentEquilibrium, + OffenceType::ThrottlingOffence(_) => Event::::AuthoritiesApplauseEquilibrium, + }; + Self::deposit_event(equilibrium_event); + return; + } + + let offence_event = match offence_type { + OffenceType::CommitmentOffence => + Event::::SomeAuthoritiesDelayed { delayed: offenders.clone() }, + OffenceType::ThrottlingOffence(_) => + Event::::SomeAuthoritiesTrottling { throttling: offenders.clone() }, + }; + + Self::deposit_event(offence_event); + + let offence = SlowClapOffence { + validator_set_count, + session_index: *session_index, + offenders, + offence_type, + }; + + if let Err(e) = T::ReportUnresponsiveness::report_offence(vec![], offence) { + sp_runtime::print(e); + } } } @@ -1126,86 +1240,73 @@ impl OneSessionHandler for Pallet { BlockCommitments::::remove(network_id); } + let current_era = T::ExposureListener::get_current_era(); + let total_exposure = T::ExposureListener::get_total_exposure(current_era); + + TotalExposure::::set(total_exposure); + let authorities = validators.map(|x| x.1).collect::>(); Self::initialize_authorities(authorities); } fn on_before_session_ending() { let session_index = T::ValidatorSet::session_index().saturating_sub(1); - let validators = Validators::::get(&session_index).unwrap_or_default(); - let authorities_len = Authorities::::get(&session_index).len(); - let claps_in_session = ClapsInSession::::get(&session_index); - let median_claps = Self::calculate_median_claps(&claps_in_session, authorities_len); + let validators = Validators::::get(&session_index); + let validators_len = validators.len() as u32; + let disabled_bitmap = DisabledAuthorityIndexes::::get(&session_index) + .unwrap_or(BitMap::new(validators_len)); - let offenders = validators - .into_iter() - .enumerate() - .filter_map(|(index, id)| { - (!Self::is_good_actor(index, median_claps, &claps_in_session)).then(|| { - >::IdentificationOf::convert( - id.clone(), - ).map(|full_id| (id, full_id)) - }) - .flatten() - }) - .collect::>>(); + let mut offence_bitmap = BitMap::new(validators_len); - let disabled_validators = T::DisabledValidators::disabled_validators() - .into_iter() - .count(); + let missed_percent = Self::get_cumulative_missing_clapped_amount( + &disabled_bitmap, + &session_index, + &mut offence_bitmap, + ); - let offenders_length = offenders.len(); - let authorities_left: u32 = authorities_len - .saturating_sub(disabled_validators) - .saturating_sub(offenders_length) - .try_into() - .unwrap_or_default(); - - if offenders_length == 0 { - Self::deposit_event(Event::::AuthoritiesEquilibrium); - } else if authorities_left < T::MinAuthoritiesNumber::get() { - Self::deposit_event(Event::::BlackSwan); - } else { - Self::deposit_event(Event::::SomeAuthoritiesTrottling { - 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) - } - } + let offence_type = OffenceType::ThrottlingOffence(missed_percent); + Self::try_offend_validators( + &session_index, + &validators, + offence_bitmap, + disabled_bitmap, + offence_type, + ); } fn on_disabled(validator_index: u32) { let session_index = T::ValidatorSet::session_index(); - ClapsInSession::::mutate(&session_index, |claps_details| { - (*claps_details) - .entry(validator_index as AuthIndex) - .and_modify(|individual| (*individual).disabled = true) - .or_insert(SessionAuthorityInfo { - claps: 0u32, - disabled: true, - }); + let length = Authorities::::get(&session_index).len(); + DisabledAuthorityIndexes::::mutate(&session_index, |maybe_bitmap| { + if let Some(bitmap) = maybe_bitmap { + bitmap.set(validator_index); + } else { + let mut new_bitmap = BitMap::new(length as u32); + new_bitmap.set(validator_index); + *maybe_bitmap = Some(new_bitmap); + } }); } } #[derive(RuntimeDebug, TypeInfo)] #[cfg_attr(feature = "std", derive(Clone, PartialEq, Eq))] -pub struct ThrottlingOffence { - pub session_index: SessionIndex, - pub validator_set_count: u32, - pub offenders: Vec, +pub enum OffenceType { + CommitmentOffence, + ThrottlingOffence(Perbill), } -impl Offence for ThrottlingOffence { +#[derive(RuntimeDebug, TypeInfo)] +#[cfg_attr(feature = "std", derive(Clone, PartialEq, Eq))] +pub struct SlowClapOffence { + session_index: SessionIndex, + validator_set_count: u32, + offenders: Vec, + offence_type: OffenceType, +} + +impl Offence for SlowClapOffence { const ID: Kind = *b"slow-clap:throtl"; type TimeSlot = SessionIndex; @@ -1226,11 +1327,15 @@ impl Offence for ThrottlingOffence { } 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() + match self.offence_type { + OffenceType::ThrottlingOffence(missed_percent) => missed_percent + .saturating_mul(Perbill::from_rational(1, offenders_count)), + OffenceType::CommitmentOffence => offenders_count + .checked_sub(self.validator_set_count / 10 + 1) + .map(|threshold| Perbill::from_rational(3 * threshold, self.validator_set_count) + .saturating_mul(Perbill::from_percent(7)) + ) + .unwrap_or_default(), } } } diff --git a/pallets/slow-clap/src/mock.rs b/pallets/slow-clap/src/mock.rs index 1fcb9a4..0eeab0c 100644 --- a/pallets/slow-clap/src/mock.rs +++ b/pallets/slow-clap/src/mock.rs @@ -19,8 +19,8 @@ use sp_staking::{ use sp_runtime::BuildStorage; -use crate as slow_clap; -use crate::{ApplauseListener, Config, EraIndex}; +use crate::{self as slow_clap, AuthIndex}; +use crate::{ExposureListener, Config, EraIndex}; type Block = frame_system::mocking::MockBlock; @@ -36,17 +36,13 @@ frame_support::construct_runtime!( ); parameter_types! { - pub static Validators: Option> = Some(vec![ - 1, - 2, - 3, - ]); + pub static FixedValidators: Vec = vec![0, 1, 2, 3]; } pub struct TestSessionManager; impl pallet_session::SessionManager for TestSessionManager { fn new_session(_new_index: SessionIndex) -> Option> { - Validators::mutate(|l| l.take()) + Some(FixedValidators::get()) } fn end_session(_: SessionIndex) {} fn start_session(_: SessionIndex) {} @@ -54,17 +50,14 @@ impl pallet_session::SessionManager for TestSessionManager { impl pallet_session::historical::SessionManager for TestSessionManager { fn new_session(_new_index: SessionIndex) -> Option> { - Validators::mutate(|l| { - l.take() - .map(|validators| validators.iter().map(|v| (*v, *v)).collect()) - }) + Some(FixedValidators::get().iter().map(|l| (*l, *l)).collect()) } fn end_session(_: SessionIndex) {} fn start_session(_: SessionIndex) {} } type IdentificationTuple = (u64, u64); -type Offence = crate::ThrottlingOffence; +type Offence = crate::SlowClapOffence; parameter_types! { pub static Offences: Vec<(Vec, Offence)> = vec![]; @@ -90,7 +83,7 @@ pub fn new_test_ext() -> sp_io::TestExternalities { let mut result = sp_io::TestExternalities::new(t); result.execute_with(|| { - for i in 1..=3 { + for i in 0..=3 { System::inc_providers(&i); assert_eq!( Session::set_keys(RuntimeOrigin::signed(i), i.into(), vec![],), @@ -176,8 +169,8 @@ impl pallet_balances::Config for Runtime { type Balance = u64; -pub struct TestSomeCoolTrait; -impl ApplauseListener for TestSomeCoolTrait +pub struct TestExposureListener; +impl ExposureListener for TestExposureListener where Balance: AtLeast32BitUnsigned + From, { @@ -185,15 +178,16 @@ where 1 } - fn get_threshold_amount(_era: EraIndex) -> Balance { - 666_666_667u64.into() + fn get_total_exposure(_era: EraIndex) -> Balance { + 1_000_000_000u64.into() } - fn get_validator_total_exposure(_era: EraIndex, index: usize) -> Balance { + fn get_validator_exposure(index: AuthIndex) -> Balance { match index { - 0 => 500_000_000u64, - 1 => 300_000_000u64, - 2 => 200_000_000u64, + 0 => 250_000_000u64, + 1 => 200_000_000u64, + 2 => 250_000_000u64, + 3 => 300_000_000u64, _ => 0, } .into() @@ -210,14 +204,13 @@ impl Config for Runtime { type BlockNumberProvider = System; type ReportUnresponsiveness = OffenceHandler; type DisabledValidators = Session; - type ApplauseListener = TestSomeCoolTrait; + type ExposureListener = TestExposureListener; type MaxAuthorities = ConstU32<5>; - type ApplauseThreshold = ConstU32<50>; - type OffenceThreshold = ConstU32<0>; + type ApplauseThreshold = ConstU32<500_000_000>; type UnsignedPriority = ConstU64<{ 1 << 20 }>; type HistoryDepth = HistoryDepth; - type MinAuthoritiesNumber = ConstU32<2>; + type MinAuthoritiesNumber = ConstU32<1>; type WeightInfo = (); } @@ -236,32 +229,7 @@ pub fn advance_session() { let now = System::block_number().max(1); System::set_block_number(now + 1); Session::rotate_session(); - let session_index = Session::current_index(); - - let authorities = Session::validators() - .into_iter() - .map(UintAuthorityId) - .collect(); - - SlowClap::set_test_authorities(session_index, authorities); - assert_eq!(session_index, (now / Period::get()) as u32); -} - -pub fn advance_session_with_authority(authority: u64) { - let now = System::block_number().max(1); - System::set_block_number(now + 1); - Session::rotate_session(); let session_index = Session::current_index(); - - SlowClap::set_test_authorities( - session_index, - vec![ - UintAuthorityId::from(authority), - UintAuthorityId::from(69), - UintAuthorityId::from(420), - UintAuthorityId::from(1337), - ], - ); assert_eq!(session_index, (now / Period::get()) as u32); } diff --git a/pallets/slow-clap/src/tests.rs b/pallets/slow-clap/src/tests.rs index 2a46775..b697e63 100644 --- a/pallets/slow-clap/src/tests.rs +++ b/pallets/slow-clap/src/tests.rs @@ -1,5 +1,7 @@ #![cfg(test)] +use std::time::{SystemTime, UNIX_EPOCH}; + use super::*; use crate::mock::*; @@ -9,294 +11,125 @@ use sp_core::offchain::{ testing::{TestOffchainExt, TestTransactionPoolExt}, OffchainDbExt, OffchainWorkerExt, TransactionPoolExt, }; -use sp_runtime::{testing::UintAuthorityId, DispatchError}; +use sp_runtime::testing::UintAuthorityId; use ghost_networks::BridgedInflationCurve; use pallet_staking::EraPayout; -const MAX_DEVIATION_DEPTH: u64 = 10; - -fn prepare_evm_network( - maybe_network_id: Option, - maybe_incoming_commission: Option, -) -> NetworkData { - let network_data = NetworkData { - chain_name: "Ethereum".into(), - default_endpoints: get_rpc_endpoints(), - finality_delay: 69, - rate_limit_delay: 69, - block_distance: 69, - network_type: ghost_networks::NetworkType::Evm, - gatekeeper: get_gatekeeper(), - topic_name: get_topic_name(), - incoming_fee: maybe_incoming_commission.unwrap_or_default(), - outgoing_fee: 0, - }; - - assert_ok!(Networks::register_network( - RuntimeOrigin::root(), - maybe_network_id.unwrap_or(1u32), - network_data.clone() - )); - - network_data -} - -fn do_clap_from_first_authority( - session_index: u32, - network_id: u32, - authority: u64, -) -> dispatch::DispatchResult { - let (transaction_hash, receiver, amount) = get_mocked_metadata(); - let clap = Clap { - block_number: 420, - removed: false, - transaction_hash, - session_index, - authority_index: 0, - network_id, - receiver, - amount, - }; - let authority = UintAuthorityId::from(authority); - let signature = authority.sign(&clap.encode()).unwrap(); - - SlowClap::pre_dispatch(&crate::Call::slow_clap { - clap: clap.clone(), - signature: signature.clone(), - }) - .map_err(|e| <&'static str>::from(e))?; - - SlowClap::slow_clap(RuntimeOrigin::none(), clap, signature) -} - -fn do_block_commitment( - session_index: u32, - network_id: u32, - authority_index: u32, -) -> dispatch::DispatchResult { - let last_updated = 1337; - - let block_commitment = BlockCommitment { - session_index, - authority_index, - network_id, - commitment: CommitmentDetails { - last_registered_block: 69, - last_seen_block: 420, - last_updated, - }, - }; - let authority = UintAuthorityId::from((authority_index + 1) as u64); - let signature = authority.sign(&block_commitment.encode()).unwrap(); - - SlowClap::pre_dispatch(&crate::Call::commit_block { - block_commitment: block_commitment.clone(), - signature: signature.clone(), - }) - .map_err(|e| <&'static str>::from(e))?; - - SlowClap::commit_block(RuntimeOrigin::none(), block_commitment, signature) -} - -fn do_clap_from( - session_index: u32, - network_id: u32, - authority_index: u32, - transaction_removed: bool, -) -> dispatch::DispatchResult { - let (transaction_hash, receiver, amount) = get_mocked_metadata(); - let clap = Clap { - block_number: 420, - removed: transaction_removed, - transaction_hash, - session_index, - authority_index, - network_id, - receiver, - amount, - }; - let authority = UintAuthorityId::from((authority_index + 1) as u64); - let signature = authority.sign(&clap.encode()).unwrap(); - - SlowClap::pre_dispatch(&crate::Call::slow_clap { - clap: clap.clone(), - signature: signature.clone(), - }) - .map_err(|e| <&'static str>::from(e))?; - - SlowClap::slow_clap(RuntimeOrigin::none(), clap, signature) -} - #[test] fn should_calculate_throttling_slash_function_correctly() { - let dummy_offence = ThrottlingOffence { + let dummy_offence = SlowClapOffence { + session_index: 0, + validator_set_count: 69, + offenders: vec![()], + offence_type: OffenceType::ThrottlingOffence(Perbill::from_percent(50)), + }; + + assert_eq!(dummy_offence.slash_fraction(1), Perbill::from_percent(50)); + assert_eq!(dummy_offence.slash_fraction(5), Perbill::from_percent(10)); + assert_eq!(dummy_offence.slash_fraction(10), Perbill::from_percent(5)); + + let dummy_offence = SlowClapOffence { session_index: 0, validator_set_count: 50, offenders: vec![()], + offence_type: OffenceType::CommitmentOffence, }; assert_eq!(dummy_offence.slash_fraction(1), Perbill::zero()); assert_eq!(dummy_offence.slash_fraction(5), Perbill::zero()); - assert_eq!( - dummy_offence.slash_fraction(7), - Perbill::from_parts(4200000) - ); - assert_eq!( - dummy_offence.slash_fraction(17), - Perbill::from_parts(46200000) - ); + assert_eq!(dummy_offence.slash_fraction(7), Perbill::from_parts(4_200_000)); + assert_eq!(dummy_offence.slash_fraction(17), Perbill::from_parts(46_200_000)); } #[test] -fn should_calculate_median_correctly() { - new_test_ext().execute_with(|| { - let data = BTreeMap::from([ - ( - 0u32, - SessionAuthorityInfo { - claps: 0, - disabled: true, - }, - ), - ( - 3u32, - SessionAuthorityInfo { - claps: 1, - disabled: false, - }, - ), - ]); - assert_eq!(SlowClap::calculate_median_claps(&data, 4), 0); +fn bitmap_operations_correct() { + let max_value = 440; + let sixty_nine = 69; + let four_twenty = 420; + let mut bitmap = BitMap::new(max_value); + assert_eq!(bitmap.max_value(), max_value); - let data = BTreeMap::from([ - ( - 0u32, - SessionAuthorityInfo { - claps: 0, - disabled: false, - }, - ), - ( - 1u32, - SessionAuthorityInfo { - claps: 69, - disabled: false, - }, - ), - ( - 2u32, - SessionAuthorityInfo { - claps: 69, - disabled: false, - }, - ), - ( - 3u32, - SessionAuthorityInfo { - claps: 420, - disabled: false, - }, - ), - ]); - assert_eq!(SlowClap::calculate_median_claps(&data, 4), 69); + assert!(!bitmap.exists(&sixty_nine)); + bitmap.set(sixty_nine); + assert!(bitmap.exists(&sixty_nine)); - let data = BTreeMap::from([ - ( - 0u32, - SessionAuthorityInfo { - claps: 31, - disabled: false, - }, - ), - ( - 1u32, - SessionAuthorityInfo { - claps: 420, - disabled: true, - }, - ), - ( - 2u32, - SessionAuthorityInfo { - claps: 69, - disabled: false, - }, - ), - ( - 3u32, - SessionAuthorityInfo { - claps: 156, - disabled: true, - }, - ), - ]); - assert_eq!(SlowClap::calculate_median_claps(&data, 4), 50); + assert!(!bitmap.exists(&four_twenty)); + bitmap.set(four_twenty); + assert!(bitmap.exists(&four_twenty)); - let data = BTreeMap::from([ - ( - 0u32, - SessionAuthorityInfo { - claps: 0, - disabled: true, - }, - ), - ( - 1u32, - SessionAuthorityInfo { - claps: 420, - disabled: false, - }, - ), - ( - 2u32, - SessionAuthorityInfo { - claps: 0, - disabled: true, - }, - ), - ( - 3u32, - SessionAuthorityInfo { - claps: 0, - disabled: false, - }, - ), - ]); - assert_eq!(SlowClap::calculate_median_claps(&data, 4), 210); + let bitmap_cloned = bitmap.clone(); + let mut bucket_iter = bitmap_cloned.iter_buckets(); + let empty_result = 0; + assert_eq!(bucket_iter.next(), Some((&0u32, &empty_result))); + // index = 69 / 64 = 1; bit_pos = 69 % 64 = 5 + let first_result = 1 << 5; + assert_eq!(bucket_iter.next(), Some((&1u32, &first_result))); + assert_eq!(bucket_iter.next(), Some((&2u32, &empty_result))); + assert_eq!(bucket_iter.next(), Some((&3u32, &empty_result))); + assert_eq!(bucket_iter.next(), Some((&4u32, &empty_result))); + assert_eq!(bucket_iter.next(), Some((&5u32, &empty_result))); + // index = 420 / 64 = 6; bit_pos = 420 % 64 = 36 + let second_result = 1 << 36; + assert_eq!(bucket_iter.next(), Some((&6u32, &second_result))); + assert_eq!(bucket_iter.next(), None); - let data = BTreeMap::from([ - ( - 0u32, - SessionAuthorityInfo { - claps: 0, - disabled: false, - }, - ), - ( - 1u32, - SessionAuthorityInfo { - claps: 420, - disabled: true, - }, - ), - ( - 2u32, - SessionAuthorityInfo { - claps: 69, - disabled: true, - }, - ), - ( - 3u32, - SessionAuthorityInfo { - claps: 0, - disabled: false, - }, - ), - ]); - assert_eq!(SlowClap::calculate_median_claps(&data, 4), 0); - }); + assert_eq!(bitmap.get_bucket(&0), empty_result); + assert_eq!(bitmap.get_bucket(&1), first_result); + assert_eq!(bitmap.get_bucket(&6), second_result); + + bitmap.unset(sixty_nine); + bitmap.unset(four_twenty); + + assert!(!bitmap.exists(&sixty_nine)); + assert!(!bitmap.exists(&four_twenty)); + + assert_eq!(BitMap::size_of_bucket(), 6); // for u64 + + let mut bitmap1 = BitMap::new(max_value); + let mut bitmap2 = BitMap::new(max_value); + assert_eq!(bitmap1.max_value(), max_value); + assert_eq!(bitmap2.max_value(), max_value); + + bitmap1.set(1); + bitmap1.set(12); + bitmap1.set(44); + bitmap1.set(80); + + bitmap2.set(1); + bitmap2.set(2); + bitmap2.set(15); + bitmap2.set(36); + bitmap2.set(130); + + let ones = bitmap1.clone().bitor(bitmap2.clone()).count_ones(); + assert_eq!(ones, 8); + + bitmap2.set(80); + bitmap2.set(420); + + let ones = bitmap1.clone().bitor(bitmap2.clone()).count_ones(); + assert_eq!(ones, 9); + + bitmap1.set(15); + bitmap1.set(69); + bitmap1.set(33); + bitmap2.set(15); + bitmap2.set(69); + bitmap2.set(33); + + let ones = bitmap1.clone().bitor(bitmap2.clone()).count_ones(); + assert_eq!(ones, 11); +} + +#[test] +fn should_correctly_adjust_to_block() { + assert_eq!(SlowClap::adjust_to_block(420, 69, 1337), 420); + assert_eq!(SlowClap::adjust_to_block(420, 1337, 69), 420); + assert_eq!(SlowClap::adjust_to_block(1337, 420, 69), 489); + assert_eq!(SlowClap::adjust_to_block(1337, 69, 420), 489); + assert_eq!(SlowClap::adjust_to_block(69, 1337, 420), 69); + assert_eq!(SlowClap::adjust_to_block(69, 420, 1337), 69); } #[test] @@ -333,16 +166,6 @@ fn request_body_is_correct_for_get_logs() { }); } -#[test] -fn should_correctly_adjust_to_block() { - assert_eq!(SlowClap::adjust_to_block(420, 69, 1337), 420); - assert_eq!(SlowClap::adjust_to_block(420, 1337, 69), 420); - assert_eq!(SlowClap::adjust_to_block(1337, 420, 69), 489); - assert_eq!(SlowClap::adjust_to_block(1337, 69, 420), 489); - assert_eq!(SlowClap::adjust_to_block(69, 1337, 420), 69); - assert_eq!(SlowClap::adjust_to_block(69, 420, 1337), 69); -} - #[test] fn should_make_http_call_for_block_number() { let (offchain, state) = TestOffchainExt::new(); @@ -429,9 +252,8 @@ fn should_make_http_call_and_parse_logs() { let from_block: u64 = 20335770; let to_block: u64 = 20335858; - let network_id: u32 = 1; - let network_data = prepare_evm_network(Some(network_id), None); + let network_data = prepare_evm_network(None, None); let request_body = SlowClap::prepare_request_body_for_latest_transfers( from_block, to_block, @@ -449,50 +271,182 @@ fn should_make_http_call_and_parse_logs() { } #[test] -fn should_clear_sesions_based_on_history_depth() { - let (network_id, transaction_hash, unique_transaction_hash) = - generate_unique_hash(None, None, None, None); +fn should_emit_black_swan_if_not_enough_authorities_left() { + let (network_id, _, _) = generate_unique_hash(None, None, None, None, None); new_test_ext().execute_with(|| { let session_index = advance_session_and_get_index(); - let history_depth = HistoryDepth::get(); - let storage_key = (session_index, transaction_hash, unique_transaction_hash); + prepare_evm_network(None, None); - assert_claps_info_correct(&storage_key, &session_index, 0); - assert_eq!( - pallet::ApplausesForTransaction::::get(&storage_key), - false - ); + assert_ok!(do_clap_from(session_index, network_id, 0, false)); + assert_ok!(do_clap_from(session_index, network_id, 1, false)); + Session::disable_index(2); + Session::disable_index(3); + + advance_session(); + advance_session(); + + System::assert_has_event(RuntimeEvent::SlowClap(crate::Event::BlackSwan)); + }); +} + +#[test] +fn should_emit_offence_on_missed_clap() { + let (network_id, _, _) = generate_unique_hash(None, None, None, None, None); + + new_test_ext().execute_with(|| { + let session_index = advance_session_and_get_index(); + prepare_evm_network(None, None); + + assert_ok!(do_clap_from(session_index, network_id, 1, false)); + assert_ok!(do_clap_from(session_index, network_id, 2, false)); + assert_ok!(do_clap_from(session_index, network_id, 3, false)); + + advance_session(); + advance_session(); + + System::assert_has_event(RuntimeEvent::SlowClap( + crate::Event::SomeAuthoritiesTrottling { + throttling: vec![(0, 0)] + } + )); + }); +} + +#[test] +fn should_emit_offence_on_fake_clap() { + let (network_id, _, _) = generate_unique_hash(None, None, None, None, None); + + new_test_ext().execute_with(|| { + let session_index = advance_session_and_get_index(); + prepare_evm_network(None, None); + + assert_ok!(do_clap_from(session_index, network_id, 0, false)); + assert_ok!(do_clap_from(session_index, network_id, 1, false)); + + advance_session(); + advance_session(); + + System::assert_has_event(RuntimeEvent::SlowClap( + crate::Event::SomeAuthoritiesTrottling { + throttling: vec![(0, 0), (1, 1)] + } + )); + }); +} + +#[test] +fn should_not_emit_offence_if_already_disabled() { + let (network_id, _, _) = generate_unique_hash(None, None, None, None, None); + + new_test_ext().execute_with(|| { + let session_index = advance_session_and_get_index(); + prepare_evm_network(None, None); + + assert_ok!(do_clap_from(session_index, network_id, 0, false)); + assert_ok!(do_clap_from(session_index, network_id, 1, false)); + assert_eq!(Session::disable_index(0), true); + + advance_session(); + advance_session(); + + System::assert_has_event(RuntimeEvent::SlowClap( + crate::Event::SomeAuthoritiesTrottling { + throttling: vec![(1, 1)] + } + )); + }); +} + +#[test] +fn should_emit_authorities_equilibrium_if_no_deviation() { + let (network_id, _, _) = generate_unique_hash(None, None, None, None, None); + + new_test_ext().execute_with(|| { + let session_index = advance_session_and_get_index(); + prepare_evm_network(None, None); assert_ok!(do_clap_from(session_index, network_id, 0, false)); assert_ok!(do_clap_from(session_index, network_id, 1, false)); assert_ok!(do_clap_from(session_index, network_id, 2, false)); + assert_ok!(do_clap_from(session_index, network_id, 3, false)); - assert_claps_info_correct(&storage_key, &session_index, 3); - assert_eq!( - pallet::ApplausesForTransaction::::get(&storage_key), - true - ); + advance_session(); + advance_session(); + + let session_index = Session::session_index(); + + for index in 0..10 { + let transaction_hash = H256::repeat_byte(index as u8); + let receiver: u64 = index + 1; + let amount: u64 = 1_000_000_000_000 * receiver; + + for authority_index in 0..=3 { + let clap = Clap { + block_number: 1337u64, + removed: false, + transaction_hash, + authority_index, + session_index, + network_id, + receiver, + amount, + }; + + let unique_hash = SlowClap::generate_unique_hash(&clap); + let authority = UintAuthorityId::from(index as u64); + let signature = authority.sign(&clap.encode()).unwrap(); + + assert_ok!(SlowClap::slow_clap(RuntimeOrigin::none(), clap, signature)); + assert_clapped(&session_index, &unique_hash, authority_index, true); + } + } + + advance_session(); + advance_session(); + + let binding = System::events(); + let number_of_events = binding.iter() + .filter(|x| { + x.event == RuntimeEvent::SlowClap(crate::Event::AuthoritiesApplauseEquilibrium) + }) + .count(); + + assert_eq!(number_of_events, 6); + }); +} + +#[test] +fn should_clear_sesions_based_on_history_depth() { + let (network_id, _, unique_hash) = generate_unique_hash(None, None, None, None, None); + + new_test_ext().execute_with(|| { + let history_depth = HistoryDepth::get(); + let session_index = advance_session_and_get_index(); + prepare_evm_network(None, None); + + assert_eq!(ApplauseDetails::::get(&session_index, &unique_hash), None); + assert_ok!(do_clap_from(session_index, network_id, 0, false)); + assert_ok!(do_clap_from(session_index, network_id, 1, false)); + assert_ok!(do_clap_from(session_index, network_id, 2, false)); + assert_ok!(do_clap_from(session_index, network_id, 3, false)); + assert_eq!(ApplauseDetails::::get(&session_index, &unique_hash).is_some(), true); for _ in 0..history_depth { advance_session(); } - assert_claps_info_correct(&storage_key, &session_index, 0); - assert_eq!( - pallet::ApplausesForTransaction::::get(&storage_key), - false - ); + assert_eq!(ApplauseDetails::::get(&session_index, &unique_hash), None); }); } #[test] fn should_collect_commission_accordingly() { - let (network_id, _, _) = generate_unique_hash(None, None, None, None); - let (_, _, amount) = get_mocked_metadata(); + let (network_id, _, _) = generate_unique_hash(None, None, None, None, None); + let (_, _, amount, _) = get_mocked_metadata(); new_test_ext().execute_with(|| { - let _ = prepare_evm_network(Some(network_id), Some(500_000_000)); + let _ = prepare_evm_network(None, Some(500_000_000)); let session_index = advance_session_and_get_index(); assert_eq!(Networks::accumulated_commission(), 0); @@ -505,11 +459,11 @@ fn should_collect_commission_accordingly() { #[test] fn should_increase_gatkeeper_amount_accordingly() { - let (network_id, _, _) = generate_unique_hash(None, None, None, None); - let (_, _, amount) = get_mocked_metadata(); + let (network_id, _, _) = generate_unique_hash(None, None, None, None, None); + let (_, _, amount, _) = get_mocked_metadata(); new_test_ext().execute_with(|| { - let _ = prepare_evm_network(Some(network_id), Some(500_000_000)); + let _ = prepare_evm_network(None, Some(500_000_000)); let session_index = advance_session_and_get_index(); assert_eq!(Networks::gatekeeper_amount(network_id), 0); @@ -531,251 +485,129 @@ fn should_increase_gatkeeper_amount_accordingly() { #[test] fn should_mark_clapped_transaction_when_clap_received() { - let (network_id, transaction_hash, unique_transaction_hash) = - generate_unique_hash(None, None, None, None); + let (network_id, _, unique_hash) = generate_unique_hash(None, None, None, None, None); new_test_ext().execute_with(|| { let session_index = advance_session_and_get_index(); - let storage_key = (session_index, transaction_hash, unique_transaction_hash); + prepare_evm_network(None, None); - assert_claps_info_correct(&storage_key, &session_index, 0); assert_ok!(do_clap_from(session_index, network_id, 0, false)); - assert_claps_info_correct(&storage_key, &session_index, 1); + assert_clapped(&session_index, &unique_hash, 0, true); assert_ok!(do_clap_from(session_index, network_id, 1, false)); - assert_claps_info_correct(&storage_key, &session_index, 2); + assert_clapped(&session_index, &unique_hash, 1, true); assert_ok!(do_clap_from(session_index, network_id, 2, false)); - assert_claps_info_correct(&storage_key, &session_index, 3); + assert_clapped(&session_index, &unique_hash, 2, true); + assert_ok!(do_clap_from(session_index, network_id, 3, false)); + assert_clapped(&session_index, &unique_hash, 3, true); }); } #[test] fn should_applause_and_take_next_claps() { - let (network_id, transaction_hash, unique_transaction_hash) = - generate_unique_hash(None, None, None, None); - let (_, receiver, amount) = get_mocked_metadata(); + let (network_id, _, unique_hash) = generate_unique_hash(None, None, None, None, None); + let (_, receiver, amount, _) = get_mocked_metadata(); new_test_ext().execute_with(|| { + let mut clapped_amount = Default::default(); let session_index = advance_session_and_get_index(); - let storage_key = (session_index, transaction_hash, unique_transaction_hash); + prepare_evm_network(None, None); - assert_eq!( - pallet::ApplausesForTransaction::::get(&storage_key), - false - ); assert_eq!(Balances::total_balance(&receiver), 0); - assert_ok!(do_clap_from(session_index, network_id, 0, false)); - assert_eq!( - pallet::ApplausesForTransaction::::get(&storage_key), - false - ); - assert_eq!(Balances::total_balance(&receiver), 0); - assert_ok!(do_clap_from(session_index, network_id, 1, false)); - assert_eq!( - pallet::ApplausesForTransaction::::get(&storage_key), - true - ); - assert_eq!(Balances::total_balance(&receiver), amount); - assert_ok!(do_clap_from(session_index, network_id, 2, false)); - assert_eq!( - pallet::ApplausesForTransaction::::get(&storage_key), - true - ); + for i in 0..=3 { + assert_ok!(do_clap_from(session_index, network_id, i, false)); + clapped_amount = TestExposureListener::get_validator_exposure(i) + .saturating_add(clapped_amount); + assert_clapped_amount(&session_index, &unique_hash, clapped_amount); + } + assert_applaused(&session_index, &unique_hash); assert_eq!(Balances::total_balance(&receiver), amount); }); } #[test] fn should_throw_error_on_clap_duplication() { - let (network_id, transaction_hash, unique_transaction_hash) = - generate_unique_hash(None, None, None, None); + let (network_id, _, unique_hash) = generate_unique_hash(None, None, None, None, None); new_test_ext().execute_with(|| { let session_index = advance_session_and_get_index(); - let storage_key = (session_index, transaction_hash, unique_transaction_hash); + prepare_evm_network(None, None); - assert_claps_info_correct(&storage_key, &session_index, 0); assert_ok!(do_clap_from(session_index, network_id, 0, false)); - assert_claps_info_correct(&storage_key, &session_index, 1); + assert_clapped(&session_index, &unique_hash, 0, true); assert_err!( do_clap_from(session_index, network_id, 0, false), Error::::AlreadyClapped ); - assert_claps_info_correct(&storage_key, &session_index, 1); + assert_clapped(&session_index, &unique_hash, 0, true); }); } #[test] -fn should_throw_error_on_removal_of_unregistered_clap() { - let (network_id, transaction_hash, unique_transaction_hash) = - generate_unique_hash(None, None, None, None); +fn should_remove_clap_by_authority() { + let (network_id, _, unique_hash) = generate_unique_hash(None, None, None, None, None); new_test_ext().execute_with(|| { let session_index = advance_session_and_get_index(); - let storage_key = (session_index, transaction_hash, unique_transaction_hash); + prepare_evm_network(None, None); + + assert_ok!(do_clap_from(session_index, network_id, 0, false)); + assert_ok!(do_clap_from(session_index, network_id, 1, false)); + assert_ok!(do_clap_from(session_index, network_id, 1, true)); + + assert_clapped(&session_index, &unique_hash, 0, true); + assert_clapped(&session_index, &unique_hash, 1, false); + }); +} + +#[test] +fn should_throw_error_if_executed_block_number_is_higher() { + let (network_id, block_number, unique_hash) = generate_unique_hash(None, None, None, None, None); + + new_test_ext().execute_with(|| { + let session_index = advance_session_and_get_index(); + prepare_evm_network(None, None); + + assert_eq!(ApplauseDetails::::get(session_index, unique_hash), None); + assert_ok!(do_clap_from(session_index, network_id, 0, false)); + assert_ok!(do_clap_from(session_index, network_id, 1, false)); + assert_ok!(do_clap_from(session_index, network_id, 2, false)); + assert_ok!(do_clap_from(session_index, network_id, 3, false)); + + assert_clapped(&session_index, &unique_hash, 0, true); + assert_applaused(&session_index, &unique_hash); - assert_claps_info_correct(&storage_key, &session_index, 0); assert_err!( - do_clap_from(session_index, network_id, 0, true), - Error::::UnregisteredClapRemove + do_clap_from_at_block(session_index, network_id, 0, block_number - 1), + Error::::ExecutedBlockIsHigher, ); - assert_claps_info_correct(&storage_key, &session_index, 0); - }); -} - -#[test] -fn should_throw_error_if_session_index_is_not_current() { - let (network_id, transaction_hash, unique_transaction_hash) = - generate_unique_hash(None, None, None, None); - - let bad_signer = 777; - let bad_signer_id = 5; - - new_test_ext().execute_with(|| { - let mut session_and_indexes = Vec::new(); - - for deviation in 1..MAX_DEVIATION_DEPTH { - advance_session_with_authority(deviation); - session_and_indexes.push((deviation, Session::current_index())); - } - - for chunk in session_and_indexes.chunks(3).into_iter() { - let authority_curr = chunk[1].0; - let authority_prev = chunk[0].0; - let authority_next = chunk[2].0; - - let session_index_curr = chunk[1].1; - let session_index_prev = chunk[0].1; - let session_index_next = chunk[2].1; - - let storage_key_curr = ( - session_index_curr, - transaction_hash, - unique_transaction_hash, - ); - let storage_key_prev = ( - session_index_prev, - transaction_hash, - unique_transaction_hash, - ); - let storage_key_next = ( - session_index_next, - transaction_hash, - unique_transaction_hash, - ); - - assert_claps_info_correct(&storage_key_curr, &session_index_curr, 0); - assert_claps_info_correct(&storage_key_prev, &session_index_prev, 0); - assert_claps_info_correct(&storage_key_next, &session_index_next, 0); - - assert_invalid_signing_address(session_index_curr, network_id, bad_signer_id); - assert_invalid_signing_address(session_index_prev, network_id, bad_signer_id); - assert_invalid_signing_address(session_index_prev, network_id, bad_signer_id); - - assert_transaction_has_bad_signature(session_index_curr, network_id, bad_signer); - assert_transaction_has_bad_signature(session_index_prev, network_id, bad_signer); - assert_transaction_has_bad_signature(session_index_prev, network_id, bad_signer); - - assert_transaction_has_bad_signature(session_index_curr, network_id, authority_prev); - assert_transaction_has_bad_signature(session_index_curr, network_id, authority_next); - - assert_transaction_has_bad_signature(session_index_prev, network_id, authority_curr); - assert_transaction_has_bad_signature(session_index_prev, network_id, authority_next); - - assert_transaction_has_bad_signature(session_index_next, network_id, authority_prev); - assert_transaction_has_bad_signature(session_index_next, network_id, authority_curr); - - assert_claps_info_correct(&storage_key_curr, &session_index_curr, 0); - assert_claps_info_correct(&storage_key_prev, &session_index_prev, 0); - assert_claps_info_correct(&storage_key_next, &session_index_next, 0); - - assert_ok!(do_clap_from_first_authority( - session_index_curr, - network_id, - authority_curr - )); - assert_ok!(do_clap_from_first_authority( - session_index_prev, - network_id, - authority_prev - )); - assert_err!( - do_clap_from_first_authority(session_index_next, network_id, authority_next), - DispatchError::Other("Transaction has a bad signature") - ); - - assert_claps_info_correct(&storage_key_curr, &session_index_curr, 1); - assert_claps_info_correct(&storage_key_prev, &session_index_prev, 1); - assert_claps_info_correct(&storage_key_next, &session_index_next, 0); - } - }); -} - -#[test] -fn should_throw_error_if_signer_has_incorrect_index() { - let (network_id, transaction_hash, unique_transaction_hash) = - generate_unique_hash(None, None, None, None); - - new_test_ext().execute_with(|| { - let session_index = advance_session_and_get_index(); - let storage_key = (session_index, transaction_hash, unique_transaction_hash); - - assert_claps_info_correct(&storage_key, &session_index, 0); - assert_invalid_signing_address(session_index, network_id, 69); - assert_transaction_has_bad_signature(session_index, network_id, 69); - assert_invalid_signing_address(session_index, network_id, 420); - assert_transaction_has_bad_signature(session_index, network_id, 420); - assert_invalid_signing_address(session_index, network_id, 1337); - assert_transaction_has_bad_signature(session_index, network_id, 1337); - assert_claps_info_correct(&storage_key, &session_index, 0); }); } #[test] fn should_throw_error_if_validator_disabled_and_ignore_later() { - let (network_id, transaction_hash, unique_transaction_hash) = - generate_unique_hash(None, None, None, None); + let (network_id, _, unique_hash) = generate_unique_hash(None, None, None, None, None); new_test_ext().execute_with(|| { let session_index = advance_session_and_get_index(); - let storage_key = (session_index, transaction_hash, unique_transaction_hash); + prepare_evm_network(None, None); - assert_claps_info_correct(&storage_key, &session_index, 0); + assert_eq!(ApplauseDetails::::get(&session_index, &unique_hash), None); assert_eq!(Session::disable_index(0), true); assert_err!( do_clap_from(session_index, network_id, 0, false), - DispatchError::Other("Invalid signing address") + Error::::DisabledAuthority, ); + assert_eq!(ApplauseDetails::::get(&session_index, &unique_hash), None); - assert_eq!(pallet::ReceivedClaps::::get(&storage_key).len(), 0); - assert_eq!( - pallet::ClapsInSession::::get(&session_index).len(), - 1 - ); - assert_eq!( - pallet::ClapsInSession::::get(&session_index) - .into_iter() - .filter(|(_, v)| !v.disabled) - .collect::>() - .len(), - 0 - ); + advance_session(); + let session_index = session_index + 1; - assert_ok!(do_clap_from(session_index, network_id, 1, false)); - assert_eq!(Session::disable_index(1), true); - - assert_eq!(pallet::ReceivedClaps::::get(&storage_key).len(), 1); - assert_eq!( - pallet::ClapsInSession::::get(&session_index).len(), - 2 - ); - assert_eq!( - pallet::ClapsInSession::::get(&session_index) - .into_iter() - .filter(|(_, v)| !v.disabled) - .collect::>() - .len(), - 0 + assert_eq!(Session::disable_index(0), true); + assert_err!( + do_clap_from(session_index, network_id, 0, false), + Error::::DisabledAuthority, ); + assert_eq!(ApplauseDetails::::get(&session_index, &unique_hash), None); }); } @@ -784,49 +616,47 @@ fn should_clap_without_applause_on_gatekeeper_amount_overflow() { let big_amount: u64 = u64::MAX; let first_receiver: u64 = 1337; let second_receiver: u64 = 420; - let (network_id, transaction_hash, unique_transaction_hash) = - generate_unique_hash(None, None, Some(first_receiver), Some(big_amount)); + + let (network_id, block_number, unique_hash) = + generate_unique_hash(None, None, Some(first_receiver), Some(big_amount), None); new_test_ext().execute_with(|| { let session_index = advance_session_and_get_index(); - let storage_key_first = (session_index, transaction_hash, unique_transaction_hash); + prepare_evm_network(None, None); - for authority_index in 0..=2 { + for authority_index in 0..=3 { let clap = Clap { - block_number: 420, removed: false, - transaction_hash, + block_number, + transaction_hash: H256::repeat_byte(1u8), session_index, authority_index, network_id, receiver: first_receiver, amount: big_amount, }; - let authority = UintAuthorityId::from((authority_index + 1) as u64); + let authority = UintAuthorityId::from(authority_index as u64); let signature = authority.sign(&clap.encode()).unwrap(); assert_ok!(SlowClap::slow_clap(RuntimeOrigin::none(), clap, signature)); + assert_clapped(&session_index, &unique_hash, authority_index, true); } - assert_claps_info_correct(&storage_key_first, &session_index, 3); - assert_eq!( - pallet::ApplausesForTransaction::::get(&storage_key_first), - true - ); + assert_applaused(&session_index, &unique_hash); assert_eq!(Balances::total_balance(&first_receiver), big_amount); assert_eq!(Balances::total_balance(&second_receiver), 0); - for authority_index in 0..=2 { + for authority_index in 0..=3 { let clap = Clap { - block_number: 420, + block_number, removed: false, transaction_hash: H256::repeat_byte(3u8), session_index, authority_index, network_id, receiver: second_receiver, - amount: 420, + amount: second_receiver, }; - let authority = UintAuthorityId::from((authority_index + 1) as u64); + let authority = UintAuthorityId::from(authority_index as u64); let signature = authority.sign(&clap.encode()).unwrap(); assert_ok!(SlowClap::slow_clap(RuntimeOrigin::none(), clap, signature)); } @@ -840,88 +670,25 @@ fn should_clap_without_applause_on_gatekeeper_amount_overflow() { }); } -#[test] -fn should_clap_without_applause_on_commission_overflow() { - let big_amount: u64 = u64::MAX; - let first_receiver: u64 = 1337; - let second_receiver: u64 = 420; - let network_id_other: u32 = 69; - let (network_id, transaction_hash, unique_transaction_hash) = - generate_unique_hash(None, None, Some(first_receiver), Some(big_amount)); - - new_test_ext().execute_with(|| { - let _ = prepare_evm_network(Some(network_id), Some(0)); - let _ = prepare_evm_network(Some(network_id_other), Some(0)); - let session_index = advance_session_and_get_index(); - let storage_key_first = (session_index, transaction_hash, unique_transaction_hash); - - for authority_index in 0..=2 { - let clap = Clap { - block_number: 420, - removed: false, - transaction_hash, - session_index, - authority_index, - network_id, - receiver: first_receiver, - amount: big_amount, - }; - let authority = UintAuthorityId::from((authority_index + 1) as u64); - let signature = authority.sign(&clap.encode()).unwrap(); - assert_ok!(SlowClap::slow_clap(RuntimeOrigin::none(), clap, signature)); - } - - assert_claps_info_correct(&storage_key_first, &session_index, 3); - assert_eq!( - pallet::ApplausesForTransaction::::get(&storage_key_first), - true - ); - assert_eq!(Balances::total_balance(&first_receiver), big_amount); - assert_eq!(Balances::total_balance(&second_receiver), 0); - - for authority_index in 0..=2 { - let clap = Clap { - block_number: 420, - removed: false, - transaction_hash: H256::repeat_byte(3u8), - session_index, - authority_index, - network_id: network_id_other, - receiver: 420, - amount: big_amount, - }; - let authority = UintAuthorityId::from((authority_index + 1) as u64); - let signature = authority.sign(&clap.encode()).unwrap(); - assert_ok!(SlowClap::slow_clap(RuntimeOrigin::none(), clap, signature)); - } - - assert_eq!(Balances::total_balance(&first_receiver), big_amount); - assert_eq!(Balances::total_balance(&second_receiver), 0); - - assert_eq!(Networks::gatekeeper_amount(network_id), big_amount); - assert_eq!(Networks::gatekeeper_amount(network_id_other), big_amount); - assert_eq!(Networks::bridged_imbalance().bridged_in, big_amount); - assert_eq!(Networks::bridged_imbalance().bridged_out, 0); - }); -} - #[test] fn should_nullify_commission_on_finalize() { let total_staked = 69_000_000; let total_issuance = 100_000_000; - let (network_id, _, _) = generate_unique_hash(None, None, None, None); - let (_, _, amount) = get_mocked_metadata(); + let (network_id, _, unique_hash) = generate_unique_hash(None, None, None, None, None); + let (_, _, amount, _) = get_mocked_metadata(); new_test_ext().execute_with(|| { - let _ = prepare_evm_network(Some(network_id), Some(1_000_000_000)); + let _ = prepare_evm_network(None, Some(1_000_000_000)); let session_index = advance_session_and_get_index(); assert_eq!(Networks::accumulated_commission(), 0); assert_ok!(do_clap_from(session_index, network_id, 0, false)); assert_ok!(do_clap_from(session_index, network_id, 1, false)); + assert_ok!(do_clap_from(session_index, network_id, 2, false)); assert_eq!(Networks::accumulated_commission(), amount); assert_eq!(Networks::is_nullification_period(), false); + assert_applaused(&session_index, &unique_hash); assert_eq!( BridgedInflationCurve::::era_payout( @@ -936,7 +703,7 @@ fn should_nullify_commission_on_finalize() { assert_eq!(Networks::is_nullification_period(), false); assert_eq!(Networks::accumulated_commission(), 0); - assert_ok!(do_clap_from(session_index, network_id, 2, false)); + assert_ok!(do_clap_from(session_index, network_id, 3, false)); assert_eq!(Networks::accumulated_commission(), 0); }); } @@ -947,11 +714,11 @@ fn should_avoid_applause_during_nullification_period() { let total_staked = 69_000_000; let total_issuance = 100_000_000; - let (network_id, _, _) = generate_unique_hash(None, None, None, None); - let (_, receiver, amount) = get_mocked_metadata(); + let (network_id, _, unique_hash) = generate_unique_hash(None, None, None, None, None); + let (_, receiver, amount, _) = get_mocked_metadata(); new_test_ext().execute_with(|| { - let _ = prepare_evm_network(Some(network_id), Some(0)); + let _ = prepare_evm_network(None, None); let session_index = advance_session_and_get_index(); assert_eq!(Networks::is_nullification_period(), false); @@ -968,112 +735,127 @@ fn should_avoid_applause_during_nullification_period() { assert_ok!(do_clap_from(session_index, network_id, 0, false)); assert_ok!(do_clap_from(session_index, network_id, 1, false)); assert_eq!(Balances::total_balance(&receiver), 0); + assert_clapped(&session_index, &unique_hash, 0, true); + assert_clapped(&session_index, &unique_hash, 1, true); + assert_clapped(&session_index, &unique_hash, 2, false); + assert_clapped(&session_index, &unique_hash, 3, false); Networks::on_finalize(System::block_number()); assert_eq!(Networks::is_nullification_period(), false); assert_ok!(do_clap_from(session_index, network_id, 2, false)); + assert_ok!(do_clap_from(session_index, network_id, 3, false)); assert_eq!(Balances::total_balance(&receiver), amount); + + assert_applaused(&session_index, &unique_hash); + assert_clapped(&session_index, &unique_hash, 0, true); + assert_clapped(&session_index, &unique_hash, 1, true); + assert_clapped(&session_index, &unique_hash, 2, true); + assert_clapped(&session_index, &unique_hash, 3, true); }); } #[test] fn should_avoid_session_overlap_on_mended_session_index() { - let (network_id, transaction_hash, unique_transaction_hash) = - generate_unique_hash(None, None, None, None); - let (_, receiver, amount) = get_mocked_metadata(); + let (network_id, _, unique_hash) = generate_unique_hash(None, None, None, None, None); + let (_, receiver, amount, _) = get_mocked_metadata(); new_test_ext().execute_with(|| { - let _ = prepare_evm_network(Some(network_id), Some(0)); + let _ = prepare_evm_network(None, None); let session_index = advance_session_and_get_index(); - let storage_key = (session_index, transaction_hash, unique_transaction_hash); assert_ok!(do_clap_from(session_index, network_id, 0, false)); + assert_ok!(do_clap_from(session_index, network_id, 1, false)); + + assert_clapped(&session_index, &unique_hash, 0, true); + assert_clapped(&session_index, &unique_hash, 1, true); + assert_clapped(&session_index, &unique_hash, 2, false); + assert_clapped(&session_index, &unique_hash, 3, false); + + advance_session(); + let new_session_index = session_index + 1; + + assert_ok!(do_clap_from(new_session_index, network_id, 2, false)); + assert_eq!(Balances::total_balance(&receiver), amount); + assert_eq!(ApplauseDetails::::get(new_session_index, unique_hash), None); + + assert_applaused(&session_index, &unique_hash); + assert_clapped(&session_index, &unique_hash, 0, true); + assert_clapped(&session_index, &unique_hash, 1, true); + assert_clapped(&session_index, &unique_hash, 2, true); + assert_clapped(&session_index, &unique_hash, 3, false); + advance_session(); - assert_eq!( - pallet::ApplausesForTransaction::::get(&storage_key), - false - ); - assert_eq!(Balances::total_balance(&receiver), 0u64); + let new_session_index = new_session_index + 1; - assert_ok!(do_clap_from(session_index, network_id, 1, false)); - assert_ok!(do_clap_from(session_index, network_id, 2, false)); - - assert_eq!( - pallet::ApplausesForTransaction::::get(&storage_key), - true - ); - assert_eq!(Balances::total_balance(&receiver), amount); + assert_ok!(do_clap_from(new_session_index, network_id, 3, false)); + assert_clapped(&new_session_index, &unique_hash, 0, false); + assert_clapped(&new_session_index, &unique_hash, 1, false); + assert_clapped(&new_session_index, &unique_hash, 2, false); + assert_clapped(&new_session_index, &unique_hash, 3, true); }); } #[test] fn should_emit_event_on_each_clap_and_on_applause() { - let (network_id, transaction_hash, unique_transaction_hash) = - generate_unique_hash(None, None, None, None); - let (_, receiver, amount) = get_mocked_metadata(); + let (network_id, block_number, unique_hash) = + generate_unique_hash(None, None, None, None, None); + let (_, receiver, amount, _) = get_mocked_metadata(); new_test_ext().execute_with(|| { let commission = Perbill::from_parts(100_000_000); // 10% let commission_amount = commission.mul_ceil(amount); let amount_after_commission = amount.saturating_sub(commission_amount); - let _ = prepare_evm_network(Some(network_id), Some(100_000_000)); + let _ = prepare_evm_network(None, Some(100_000_000)); let session_index = advance_session_and_get_index(); - let storage_key = (session_index, transaction_hash, unique_transaction_hash); - assert_eq!( - pallet::ApplausesForTransaction::::get(&storage_key), - false - ); - assert_claps_info_correct(&storage_key, &session_index, 0); - assert_ok!(do_clap_from(session_index, network_id, 0, false)); - System::assert_last_event(RuntimeEvent::SlowClap(crate::Event::Clapped { - receiver: receiver.clone(), - authority_id: 0, - network_id, - transaction_hash, - amount, - })); - - assert_eq!( - pallet::ApplausesForTransaction::::get(&storage_key), - false - ); - assert_claps_info_correct(&storage_key, &session_index, 1); - assert_ok!(do_clap_from(session_index, network_id, 1, false)); - let binding = System::events(); - let last_two_events = binding.iter().rev().take(5).collect::>(); - assert_eq!( - last_two_events[0].event, - RuntimeEvent::SlowClap(crate::Event::Applaused { - network_id, + assert_eq!(ApplauseDetails::::get(&session_index, &unique_hash), None); + for index in 0..2 { + assert_ok!(do_clap_from(session_index, network_id, index, false)); + System::assert_last_event(RuntimeEvent::SlowClap(crate::Event::Clapped { + clap_unique_hash: unique_hash, receiver: receiver.clone(), + authority_id: index, + removed: false, + network_id, + amount, + })); + } + + assert_ok!(do_clap_from(session_index, network_id, 2, false)); + let binding = System::events(); + let last_five_events = binding.iter().rev().take(5).collect::>(); + + assert_eq!( + last_five_events[0].event, + RuntimeEvent::SlowClap(crate::Event::Applaused { received_amount: amount_after_commission, + receiver: receiver.clone(), + block_number, + network_id, }) ); assert_eq!( - last_two_events[4].event, + last_five_events[4].event, RuntimeEvent::SlowClap(crate::Event::Clapped { receiver: receiver.clone(), - authority_id: 1, + clap_unique_hash: unique_hash, + authority_id: 2, + removed: false, network_id, - transaction_hash, amount, }) ); - assert_eq!( - pallet::ApplausesForTransaction::::get(&storage_key), - true - ); - assert_claps_info_correct(&storage_key, &session_index, 2); - assert_ok!(do_clap_from(session_index, network_id, 2, false)); + assert_applaused(&session_index, &unique_hash); + assert_ok!(do_clap_from(session_index, network_id, 3, false)); System::assert_last_event(RuntimeEvent::SlowClap(crate::Event::Clapped { receiver: receiver.clone(), - authority_id: 2, + clap_unique_hash: unique_hash, + authority_id: 3, + removed: false, network_id, - transaction_hash, amount, })); }); @@ -1081,24 +863,19 @@ fn should_emit_event_on_each_clap_and_on_applause() { #[test] fn should_not_fail_on_sub_existential_balance() { - let (network_id, transaction_hash, unique_transaction_hash) = - generate_unique_hash(None, None, None, None); - let (_, receiver, amount) = get_mocked_metadata(); + let (network_id, _, unique_hash) = generate_unique_hash(None, None, None, None, None); + let (_, receiver, amount, _) = get_mocked_metadata(); new_test_ext().execute_with(|| { - let _ = prepare_evm_network(Some(network_id), Some(1_000_000_000)); // 100% + let _ = prepare_evm_network(None, Some(1_000_000_000)); // 100% let session_index = advance_session_and_get_index(); - let received_claps_key = (session_index, transaction_hash, unique_transaction_hash); assert_eq!(Networks::accumulated_commission(), 0); assert_eq!(Networks::gatekeeper_amount(network_id), 0); assert_eq!(Networks::bridged_imbalance().bridged_in, 0); assert_eq!(Networks::bridged_imbalance().bridged_out, 0); assert_eq!(Balances::total_balance(&receiver), 0); - assert_eq!( - SlowClap::applauses_for_transaction(&received_claps_key), - false - ); + assert_eq!(ApplauseDetails::::get(session_index, unique_hash), None); assert_ok!(do_clap_from(session_index, network_id, 0, false)); assert_ok!(do_clap_from(session_index, network_id, 1, false)); @@ -1109,111 +886,380 @@ fn should_not_fail_on_sub_existential_balance() { assert_eq!(Networks::bridged_imbalance().bridged_in, 0); assert_eq!(Networks::bridged_imbalance().bridged_out, 0); assert_eq!(Balances::total_balance(&receiver), 0); - assert_eq!( - SlowClap::applauses_for_transaction(&received_claps_key), - true - ); - }); -} - -#[test] -fn should_emit_black_swan_if_not_enough_authorities_left() { - let (network_id, _, _) = generate_unique_hash(None, None, None, None); - - new_test_ext().execute_with(|| { - let session_index = advance_session_and_get_index(); - assert_ok!(do_clap_from(session_index, network_id, 0, false)); - Session::disable_index(1); - Session::disable_index(2); - - advance_session(); - advance_session(); - System::assert_has_event(RuntimeEvent::SlowClap(crate::Event::BlackSwan)); + assert_applaused(&session_index, &unique_hash); }); } #[test] fn should_register_block_commitments() { - let (network_id, _, _) = generate_unique_hash(None, None, None, None); + let (network_id, _, _) = generate_unique_hash(None, None, None, None, None); new_test_ext().execute_with(|| { let session_index = advance_session_and_get_index(); + prepare_evm_network(None, None); - for commitment_details in SlowClap::block_commitments(network_id).values() { - assert_eq!(commitment_details.last_registered_block, 0); - assert_eq!(commitment_details.last_seen_block, 0); + for commitment_details in BlockCommitments::::get(network_id).values() { + assert_eq!(commitment_details.last_stored_block, 0); assert_eq!(commitment_details.last_updated, 0); + assert_eq!(commitment_details.commits, 0); } - assert_ok!(do_block_commitment(session_index, network_id, 0)); - assert_ok!(do_block_commitment(session_index, network_id, 1)); - assert_ok!(do_block_commitment(session_index, network_id, 2)); + let mut block_commitment = CommitmentDetails { + last_stored_block: 69, + commits: 420, + last_updated: 1337, + }; + for i in 0..=3 { + assert_ok!(do_block_commitment(session_index, network_id, i, &block_commitment)); + } - assert_err!( - do_block_commitment(session_index, network_id, 3), - DispatchError::Other("Invalid signing address"), - ); + block_commitment.last_updated = 1000; + for i in 0..=3 { + assert_err!( + do_block_commitment(session_index, network_id, i, &block_commitment), + Error::::TimeWentBackwards, + ); + } - assert_err!( - do_block_commitment(session_index, network_id, 1), - Error::::TimeWentBackwards, - ); - - for commitment_details in SlowClap::block_commitments(network_id).values() { - assert_eq!(commitment_details.last_registered_block, 69); - assert_eq!(commitment_details.last_seen_block, 420); + for commitment_details in BlockCommitments::::get(network_id).values() { + assert_eq!(commitment_details.last_stored_block, 69); assert_eq!(commitment_details.last_updated, 1337); + assert_eq!(commitment_details.commits, 1); } advance_session(); - for commitment_details in SlowClap::block_commitments(network_id).values() { - assert_eq!(commitment_details.last_registered_block, 0); - assert_eq!(commitment_details.last_seen_block, 0); + for commitment_details in BlockCommitments::::get(network_id).values() { + assert_eq!(commitment_details.last_stored_block, 0); assert_eq!(commitment_details.last_updated, 0); + assert_eq!(commitment_details.commits, 0); } }); } #[test] -fn should_accumulate_clapped_amount() { - let (network_id, transaction_hash, unique_transaction_hash) = - generate_unique_hash(None, None, None, None); +fn should_disable_on_commitment_inactivity() { + let (network_id, _, _) = generate_unique_hash(None, None, None, None, None); new_test_ext().execute_with(|| { let session_index = advance_session_and_get_index(); - let storage_key = (session_index, transaction_hash, unique_transaction_hash); + prepare_evm_network(None, None); - let era = ::ApplauseListener::get_current_era(); - let threshold_amount = ::ApplauseListener::get_threshold_amount(era); + for commitment_details in BlockCommitments::::get(network_id).values() { + assert_eq!(commitment_details.last_stored_block, 0); + assert_eq!(commitment_details.last_updated, 0); + assert_eq!(commitment_details.commits, 0); + } - assert_eq!( - ClappedAmount::::get(&storage_key) < threshold_amount, - true - ); - assert_ok!(do_clap_from(session_index, network_id, 2, false)); - assert_eq!( - ClappedAmount::::get(&storage_key) < threshold_amount, - true - ); - assert_ok!(do_clap_from(session_index, network_id, 1, false)); - assert_eq!( - ClappedAmount::::get(&storage_key) < threshold_amount, - true - ); - assert_ok!(do_clap_from(session_index, network_id, 0, false)); - assert_eq!( - ClappedAmount::::get(&storage_key) > threshold_amount, - true - ); + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_millis() as u64; + + let mut block_commitment = CommitmentDetails { + last_stored_block: 69, + commits: 420, + last_updated: timestamp, + }; + + // two block commitments + for extra_time in 0..=2 { + block_commitment.last_updated += extra_time; + for i in 0..3 { + assert_ok!(do_block_commitment(session_index, network_id, i, &block_commitment)); + } + } + + System::assert_has_event(RuntimeEvent::SlowClap( + crate::Event::SomeAuthoritiesDelayed { + delayed: vec![(3, 3)] + } + )); + + for commitment_details in BlockCommitments::::get(network_id).values().take(2) { + assert_eq!(commitment_details.commits, 3); + } }); } +#[test] +fn should_disable_on_commitment_block_deviation() { + let (network_id, _, _) = generate_unique_hash(None, None, None, None, None); + + new_test_ext().execute_with(|| { + let session_index = advance_session_and_get_index(); + prepare_evm_network(None, None); + + for commitment_details in BlockCommitments::::get(network_id).values() { + assert_eq!(commitment_details.last_stored_block, 0); + assert_eq!(commitment_details.last_updated, 0); + } + + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_millis() as u64; + + let mut block_commitment = CommitmentDetails { + last_stored_block: 9_500_000, + commits: 420, + last_updated: timestamp, + }; + + let mut bad_block_commitment = CommitmentDetails { + last_stored_block: 9_100_000, + commits: 420, + last_updated: timestamp, + }; + + // two block commitments + for extra_time in 0..=2 { + block_commitment.last_updated += extra_time; + bad_block_commitment.last_updated += extra_time; + for i in 0..3 { + assert_ok!(do_block_commitment(session_index, network_id, i, &block_commitment)); + } + assert_ok!(do_block_commitment(session_index, network_id, 3, &bad_block_commitment)); + } + + System::assert_has_event(RuntimeEvent::SlowClap( + crate::Event::SomeAuthoritiesDelayed { + delayed: vec![(3, 3)] + } + )); + }); +} + +#[test] +fn should_not_offend_disabled_authorities() { + let (network_id, _, _) = generate_unique_hash(None, None, None, None, None); + + new_test_ext().execute_with(|| { + let session_index = advance_session_and_get_index(); + prepare_evm_network(None, None); + + assert_ok!(do_clap_from(session_index, network_id, 0, false)); + assert_ok!(do_clap_from(session_index, network_id, 1, false)); + assert_ok!(do_clap_from(session_index, network_id, 2, false)); + + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_millis() as u64; + + let mut block_commitment = CommitmentDetails { + last_stored_block: 9_500_000, + commits: 420, + last_updated: timestamp, + }; + + Session::disable_index(3); + + // two block commitments + for extra_time in 0..=2 { + block_commitment.last_updated += extra_time; + for i in 0..3 { + assert_ok!(do_block_commitment(session_index, network_id, i, &block_commitment)); + } + } + + System::assert_has_event(RuntimeEvent::SlowClap( + crate::Event::AuthoritiesCommitmentEquilibrium, + )); + }); +} + +#[test] +fn should_not_slash_by_applause_if_disabled_by_commitment() { + let (network_id, _, _) = generate_unique_hash(None, None, None, None, None); + + new_test_ext().execute_with(|| { + let session_index = advance_session_and_get_index(); + prepare_evm_network(None, None); + + for commitment_details in BlockCommitments::::get(network_id).values() { + assert_eq!(commitment_details.last_stored_block, 0); + assert_eq!(commitment_details.last_updated, 0); + } + + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_millis() as u64; + + let mut block_commitment = CommitmentDetails { + last_stored_block: 9_500_000, + commits: 420, + last_updated: timestamp, + }; + + Session::disable_index(3); + + // two block commitments + for extra_time in 0..=2 { + block_commitment.last_updated += extra_time; + for i in 0..3 { + assert_ok!(do_block_commitment(session_index, network_id, i, &block_commitment)); + } + } + + System::assert_has_event(RuntimeEvent::SlowClap( + crate::Event::AuthoritiesCommitmentEquilibrium, + )); + }); +} + +fn assert_clapped_amount( + session_index: &SessionIndex, + unique_hash: &H256, + clapped_amount: BalanceOf, +) { + let maybe_details = ApplauseDetails::::get(session_index, unique_hash); + assert!(maybe_details.is_some()); + let actual_clapped_amount = maybe_details + .map(|details| details.clapped_amount) + .unwrap_or_default(); + assert_eq!(actual_clapped_amount, clapped_amount); +} + +fn assert_applaused(session_index: &SessionIndex, unique_hash: &H256) { + let maybe_details = ApplauseDetails::::get(session_index, unique_hash); + assert!(maybe_details.is_some()); + let is_applaused = maybe_details + .map(|details| details.finalized) + .unwrap_or_default(); + assert!(is_applaused); +} + +fn prepare_evm_network( + maybe_network_id: Option, + maybe_incoming_commission: Option, +) -> NetworkData { + let network_data = NetworkData { + chain_name: "Ethereum".into(), + default_endpoints: get_rpc_endpoints(), + finality_delay: 69, + avg_block_speed: 69, + rate_limit_delay: 69, + block_distance: 69, + network_type: ghost_networks::NetworkType::Evm, + gatekeeper: get_gatekeeper(), + topic_name: get_topic_name(), + incoming_fee: maybe_incoming_commission.unwrap_or_default(), + outgoing_fee: 0, + }; + + assert_ok!(Networks::register_network( + RuntimeOrigin::root(), + maybe_network_id.unwrap_or(1u32), + network_data.clone() + )); + + network_data +} + +fn do_block_commitment( + session_index: u32, + network_id: u32, + authority_index: u32, + commitment: &CommitmentDetails, +) -> dispatch::DispatchResult { + let block_commitment = BlockCommitment { + session_index, + authority_index, + network_id, + commitment: *commitment, + }; + let authority = UintAuthorityId::from(authority_index as u64); + let signature = authority.sign(&block_commitment.encode()).unwrap(); + + SlowClap::pre_dispatch(&crate::Call::commit_block { + block_commitment: block_commitment.clone(), + signature: signature.clone(), + }) + .map_err(|e| <&'static str>::from(e))?; + + SlowClap::commit_block(RuntimeOrigin::none(), block_commitment, signature) +} + +fn do_clap_from( + session_index: u32, + network_id: u32, + authority_index: u32, + transaction_removed: bool, +) -> dispatch::DispatchResult { + let (transaction_hash, receiver, amount, block_number) = get_mocked_metadata(); + let clap = Clap { + block_number, + removed: transaction_removed, + transaction_hash, + session_index, + authority_index, + network_id, + receiver, + amount, + }; + let authority = UintAuthorityId::from(authority_index as u64); + let signature = authority.sign(&clap.encode()).unwrap(); + + SlowClap::pre_dispatch(&crate::Call::slow_clap { + clap: clap.clone(), + signature: signature.clone(), + }) + .map_err(|e| <&'static str>::from(e))?; + + SlowClap::slow_clap(RuntimeOrigin::none(), clap, signature) +} + +fn do_clap_from_at_block( + session_index: u32, + network_id: u32, + authority_index: u32, + block_number: u64, +) -> dispatch::DispatchResult { + let transaction_hash = H256::repeat_byte(96u8); + let receiver: u64 = 1337; + let amount: u64 = 420_000_000_000_000; + + let clap = Clap { + block_number, + removed: false, + transaction_hash, + session_index, + authority_index, + network_id, + receiver, + amount, + }; + let authority = UintAuthorityId::from(authority_index as u64); + let signature = authority.sign(&clap.encode()).unwrap(); + + SlowClap::pre_dispatch(&crate::Call::slow_clap { + clap: clap.clone(), + signature: signature.clone(), + }) + .map_err(|e| <&'static str>::from(e))?; + + SlowClap::slow_clap(RuntimeOrigin::none(), clap, signature) +} + +fn assert_clapped( + session_index: &SessionIndex, + unique_hash: &H256, + authority_index: u32, + is_clapped: bool, +) { + let maybe_details = ApplauseDetails::::get(session_index, unique_hash); + assert!(maybe_details.is_some()); + let bitmap = maybe_details.map(|details| details.authorities).unwrap(); + assert_eq!(bitmap.exists(&authority_index), is_clapped); +} + fn advance_session_and_get_index() -> u32 { advance_session(); assert_eq!(Session::validators(), Vec::::new()); - advance_session(); + advance_session(); Session::session_index() } @@ -1222,42 +1268,32 @@ fn generate_unique_hash( maybe_transaction_hash: Option, maybe_receiver: Option, maybe_amount: Option, -) -> (u32, H256, H256) { + maybe_block_number: Option, +) -> (NetworkIdOf, u64, H256) { let network_id = maybe_network_id.unwrap_or(1); - let (transaction_hash, receiver, amount) = get_mocked_metadata(); + + let (transaction_hash, receiver, amount, block_number) = + get_mocked_metadata(); let transaction_hash = maybe_transaction_hash.unwrap_or(transaction_hash); let receiver = maybe_receiver.unwrap_or(receiver); let amount = maybe_amount.unwrap_or(amount); + let block_number = maybe_block_number.unwrap_or(block_number); - let unique_transaction_hash = SlowClap::generate_unique_hash(&receiver, &amount, &network_id); + let clap = Clap { + session_index: 0, + authority_index: 0, + removed: false, + block_number, + transaction_hash, + network_id, + receiver, + amount, + }; - (network_id, transaction_hash, unique_transaction_hash) -} + let unique_hash = SlowClap::generate_unique_hash(&clap); -fn assert_claps_info_correct(storage_key: &(u32, H256, H256), session_index: &u32, index: usize) { - assert_eq!( - pallet::ReceivedClaps::::get(storage_key).len(), - index - ); - assert_eq!( - pallet::ClapsInSession::::get(session_index).len(), - index - ); -} - -fn assert_transaction_has_bad_signature(session_index: u32, network_id: u32, authority: u64) { - assert_err!( - do_clap_from_first_authority(session_index, network_id, authority), - DispatchError::Other("Transaction has a bad signature") - ); -} - -fn assert_invalid_signing_address(session_index: u32, network_id: u32, authority_index: u32) { - assert_err!( - do_clap_from(session_index, network_id, authority_index, false), - DispatchError::Other("Invalid signing address") - ); + (network_id, block_number, unique_hash) } fn get_rpc_endpoint() -> Vec { @@ -1279,12 +1315,13 @@ fn get_topic_name() -> Vec { b"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef".to_vec() } -fn get_mocked_metadata() -> (H256, u64, u64) { +fn get_mocked_metadata() -> (H256, u64, u64, u64) { let transaction_hash = H256::repeat_byte(69u8); let receiver: u64 = 1337; let amount: u64 = 420_000_000_000_000; + let block_number: u64 = 555; - (transaction_hash, receiver, amount) + (transaction_hash, receiver, amount, block_number) } fn evm_block_response(state: &mut testing::OffchainState) {