Compare commits
6 Commits
75268b4c0a
...
6fa5e5ed97
| Author | SHA1 | Date | |
|---|---|---|---|
| 6fa5e5ed97 | |||
| 58c5f1f33d | |||
| 55a77cd3d4 | |||
| 2cf4637d0c | |||
| 8123295f91 | |||
| 092679eb0c |
4
Cargo.lock
generated
4
Cargo.lock
generated
@ -1186,7 +1186,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "casper-runtime"
|
name = "casper-runtime"
|
||||||
version = "3.5.34"
|
version = "3.5.36"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"casper-runtime-constants",
|
"casper-runtime-constants",
|
||||||
"frame-benchmarking",
|
"frame-benchmarking",
|
||||||
@ -3836,7 +3836,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ghost-slow-clap"
|
name = "ghost-slow-clap"
|
||||||
version = "0.3.51"
|
version = "0.3.53"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"frame-benchmarking",
|
"frame-benchmarking",
|
||||||
"frame-support",
|
"frame-support",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "ghost-slow-clap"
|
name = "ghost-slow-clap"
|
||||||
version = "0.3.51"
|
version = "0.3.53"
|
||||||
description = "Applause protocol for the EVM bridge"
|
description = "Applause protocol for the EVM bridge"
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
authors.workspace = true
|
authors.workspace = true
|
||||||
|
|||||||
@ -9,8 +9,7 @@ use frame_support::{
|
|||||||
pallet_prelude::*,
|
pallet_prelude::*,
|
||||||
traits::{
|
traits::{
|
||||||
tokens::fungible::{Inspect, Mutate},
|
tokens::fungible::{Inspect, Mutate},
|
||||||
DisabledValidators, EstimateNextSessionRotation, Get, OneSessionHandler, ValidatorSet,
|
DisabledValidators, Get, OneSessionHandler, ValidatorSet, ValidatorSetWithIdentification,
|
||||||
ValidatorSetWithIdentification,
|
|
||||||
},
|
},
|
||||||
WeakBoundedVec,
|
WeakBoundedVec,
|
||||||
};
|
};
|
||||||
@ -35,11 +34,7 @@ use sp_staking::{
|
|||||||
offence::{Kind, Offence, ReportOffence},
|
offence::{Kind, Offence, ReportOffence},
|
||||||
SessionIndex,
|
SessionIndex,
|
||||||
};
|
};
|
||||||
use sp_std::{
|
use sp_std::{collections::btree_map::BTreeMap, prelude::*, vec::Vec};
|
||||||
collections::btree_map::BTreeMap,
|
|
||||||
prelude::*,
|
|
||||||
vec::Vec,
|
|
||||||
};
|
|
||||||
|
|
||||||
use ghost_networks::{
|
use ghost_networks::{
|
||||||
NetworkData, NetworkDataBasicHandler, NetworkDataInspectHandler, NetworkDataMutateHandler,
|
NetworkData, NetworkDataBasicHandler, NetworkDataInspectHandler, NetworkDataMutateHandler,
|
||||||
@ -118,7 +113,6 @@ enum OffchainErr<NetworkId> {
|
|||||||
UnknownNetworkType(NetworkId),
|
UnknownNetworkType(NetworkId),
|
||||||
OffchainTimeoutPeriod(NetworkId),
|
OffchainTimeoutPeriod(NetworkId),
|
||||||
TooManyRequests(NetworkId),
|
TooManyRequests(NetworkId),
|
||||||
AuthorityDisabled(AuthIndex),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<NetworkId: core::fmt::Debug> core::fmt::Debug for OffchainErr<NetworkId> {
|
impl<NetworkId: core::fmt::Debug> core::fmt::Debug for OffchainErr<NetworkId> {
|
||||||
@ -144,7 +138,6 @@ impl<NetworkId: core::fmt::Debug> core::fmt::Debug for OffchainErr<NetworkId> {
|
|||||||
OffchainErr::UnknownNetworkType(ref network_id) => write!(fmt, "Unknown type for network #{:?}.", network_id),
|
OffchainErr::UnknownNetworkType(ref network_id) => write!(fmt, "Unknown type for network #{:?}.", network_id),
|
||||||
OffchainErr::OffchainTimeoutPeriod(ref network_id) => write!(fmt, "Offchain request should be in-flight for network #{:?}.", network_id),
|
OffchainErr::OffchainTimeoutPeriod(ref network_id) => write!(fmt, "Offchain request should be in-flight for network #{:?}.", network_id),
|
||||||
OffchainErr::TooManyRequests(ref network_id) => write!(fmt, "Too many requests over RPC endpoint for network #{:?}.", network_id),
|
OffchainErr::TooManyRequests(ref network_id) => write!(fmt, "Too many requests over RPC endpoint for network #{:?}.", network_id),
|
||||||
OffchainErr::AuthorityDisabled(ref authority_index) => write!(fmt, "Authority index {:?} is disabled in current session.", authority_index),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -188,7 +181,6 @@ pub mod pallet {
|
|||||||
+ MaybeSerializeDeserialize
|
+ MaybeSerializeDeserialize
|
||||||
+ MaxEncodedLen;
|
+ MaxEncodedLen;
|
||||||
|
|
||||||
type NextSessionRotation: EstimateNextSessionRotation<BlockNumberFor<Self>>;
|
|
||||||
type ValidatorSet: ValidatorSetWithIdentification<Self::AccountId>;
|
type ValidatorSet: ValidatorSetWithIdentification<Self::AccountId>;
|
||||||
type Currency: Inspect<Self::AccountId> + Mutate<Self::AccountId>;
|
type Currency: Inspect<Self::AccountId> + Mutate<Self::AccountId>;
|
||||||
type NetworkDataHandler: NetworkDataBasicHandler
|
type NetworkDataHandler: NetworkDataBasicHandler
|
||||||
@ -217,12 +209,16 @@ pub mod pallet {
|
|||||||
#[pallet::constant]
|
#[pallet::constant]
|
||||||
type HistoryDepth: Get<SessionIndex>;
|
type HistoryDepth: Get<SessionIndex>;
|
||||||
|
|
||||||
|
#[pallet::constant]
|
||||||
|
type MinAuthoritiesNumber: Get<u32>;
|
||||||
|
|
||||||
type WeightInfo: WeightInfo;
|
type WeightInfo: WeightInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pallet::event]
|
#[pallet::event]
|
||||||
#[pallet::generate_deposit(pub(super) fn deposit_event)]
|
#[pallet::generate_deposit(pub(super) fn deposit_event)]
|
||||||
pub enum Event<T: Config> {
|
pub enum Event<T: Config> {
|
||||||
|
BlackSwan,
|
||||||
AuthoritiesEquilibrium,
|
AuthoritiesEquilibrium,
|
||||||
SomeAuthoritiesTrottling {
|
SomeAuthoritiesTrottling {
|
||||||
throttling: Vec<IdentificationTuple<T>>,
|
throttling: Vec<IdentificationTuple<T>>,
|
||||||
@ -630,7 +626,8 @@ impl<T: Config> Pallet<T> {
|
|||||||
let curr_received_claps_key = (curr_session_index, &transaction_hash, &clap_unique_hash);
|
let curr_received_claps_key = (curr_session_index, &transaction_hash, &clap_unique_hash);
|
||||||
|
|
||||||
let mut previous_claps = ClapsInSession::<T>::get(&prev_session_index);
|
let mut previous_claps = ClapsInSession::<T>::get(&prev_session_index);
|
||||||
let mut total_received_claps = ReceivedClaps::<T>::get(&prev_received_claps_key).into_inner();
|
let mut total_received_claps =
|
||||||
|
ReceivedClaps::<T>::get(&prev_received_claps_key).into_inner();
|
||||||
|
|
||||||
for (auth_index, info) in ClapsInSession::<T>::get(&curr_session_index).iter() {
|
for (auth_index, info) in ClapsInSession::<T>::get(&curr_session_index).iter() {
|
||||||
if !info.disabled {
|
if !info.disabled {
|
||||||
@ -658,14 +655,9 @@ impl<T: Config> Pallet<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let disabled_authorities = previous_claps
|
let disabled_authorities = previous_claps.values().filter(|info| info.disabled).count();
|
||||||
.values()
|
|
||||||
.filter(|info| info.disabled)
|
|
||||||
.count();
|
|
||||||
|
|
||||||
let active_authorities = prev_authorities
|
let active_authorities = prev_authorities.len().saturating_sub(disabled_authorities);
|
||||||
.len()
|
|
||||||
.saturating_sub(disabled_authorities);
|
|
||||||
|
|
||||||
let clap = Clap {
|
let clap = Clap {
|
||||||
authority_index: Default::default(),
|
authority_index: Default::default(),
|
||||||
@ -678,10 +670,9 @@ impl<T: Config> Pallet<T> {
|
|||||||
amount,
|
amount,
|
||||||
};
|
};
|
||||||
|
|
||||||
let enough_authorities = Perbill::from_rational(
|
let enough_authorities =
|
||||||
total_received_claps.len() as u32,
|
Perbill::from_rational(total_received_claps.len() as u32, active_authorities as u32)
|
||||||
active_authorities as u32,
|
> Perbill::from_percent(T::ApplauseThreshold::get());
|
||||||
) > Perbill::from_percent(T::ApplauseThreshold::get());
|
|
||||||
|
|
||||||
ensure!(enough_authorities, Error::<T>::NotEnoughClaps);
|
ensure!(enough_authorities, Error::<T>::NotEnoughClaps);
|
||||||
Self::try_applause(&clap, &prev_received_claps_key)?;
|
Self::try_applause(&clap, &prev_received_claps_key)?;
|
||||||
@ -768,14 +759,6 @@ impl<T: Config> Pallet<T> {
|
|||||||
network_id: NetworkIdOf<T>,
|
network_id: NetworkIdOf<T>,
|
||||||
network_data: &NetworkData,
|
network_data: &NetworkData,
|
||||||
) -> OffchainResult<T, ()> {
|
) -> OffchainResult<T, ()> {
|
||||||
if ClapsInSession::<T>::get(&session_index)
|
|
||||||
.get(&authority_index)
|
|
||||||
.map(|info| info.disabled)
|
|
||||||
.unwrap_or_default()
|
|
||||||
{
|
|
||||||
return Err(OffchainErr::AuthorityDisabled(authority_index));
|
|
||||||
}
|
|
||||||
|
|
||||||
let network_id_encoded = network_id.encode();
|
let network_id_encoded = network_id.encode();
|
||||||
|
|
||||||
let block_number_key = Self::create_storage_key(b"block-", &network_id_encoded);
|
let block_number_key = Self::create_storage_key(b"block-", &network_id_encoded);
|
||||||
@ -914,6 +897,20 @@ impl<T: Config> Pallet<T> {
|
|||||||
Ok(Some(new_evm_block))
|
Ok(Some(new_evm_block))
|
||||||
}
|
}
|
||||||
EvmResponseType::TransactionLogs(evm_logs) => {
|
EvmResponseType::TransactionLogs(evm_logs) => {
|
||||||
|
if ClapsInSession::<T>::get(&session_index)
|
||||||
|
.get(&authority_index)
|
||||||
|
.map(|info| info.disabled)
|
||||||
|
.unwrap_or_default()
|
||||||
|
{
|
||||||
|
log::info!(
|
||||||
|
target: LOG_TARGET,
|
||||||
|
"🧐 Authority #{:?} disabled in session {:?}; no claps available",
|
||||||
|
authority_index,
|
||||||
|
session_index
|
||||||
|
);
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
let claps: Vec<_> = evm_logs
|
let claps: Vec<_> = evm_logs
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|log| {
|
.filter_map(|log| {
|
||||||
@ -1221,8 +1218,21 @@ impl<T: Config> OneSessionHandler<T::AccountId> for Pallet<T> {
|
|||||||
})
|
})
|
||||||
.collect::<Vec<IdentificationTuple<T>>>();
|
.collect::<Vec<IdentificationTuple<T>>>();
|
||||||
|
|
||||||
if offenders.is_empty() {
|
let disabled_validators = T::DisabledValidators::disabled_validators()
|
||||||
|
.into_iter()
|
||||||
|
.count();
|
||||||
|
|
||||||
|
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::<T>::AuthoritiesEquilibrium);
|
Self::deposit_event(Event::<T>::AuthoritiesEquilibrium);
|
||||||
|
} else if authorities_left < T::MinAuthoritiesNumber::get() {
|
||||||
|
Self::deposit_event(Event::<T>::BlackSwan);
|
||||||
} else {
|
} else {
|
||||||
Self::deposit_event(Event::<T>::SomeAuthoritiesTrottling {
|
Self::deposit_event(Event::<T>::SomeAuthoritiesTrottling {
|
||||||
throttling: offenders.clone(),
|
throttling: offenders.clone(),
|
||||||
|
|||||||
@ -3,7 +3,6 @@
|
|||||||
use frame_support::{
|
use frame_support::{
|
||||||
derive_impl, parameter_types,
|
derive_impl, parameter_types,
|
||||||
traits::{ConstU32, ConstU64},
|
traits::{ConstU32, ConstU64},
|
||||||
weights::Weight,
|
|
||||||
};
|
};
|
||||||
use frame_system::EnsureRoot;
|
use frame_system::EnsureRoot;
|
||||||
use pallet_session::historical as pallet_session_historical;
|
use pallet_session::historical as pallet_session_historical;
|
||||||
@ -140,27 +139,6 @@ parameter_types! {
|
|||||||
pub static MockAverageSessionLength: Option<u64> = None;
|
pub static MockAverageSessionLength: Option<u64> = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct TestNextSessionRotation;
|
|
||||||
impl frame_support::traits::EstimateNextSessionRotation<u64> for TestNextSessionRotation {
|
|
||||||
fn average_session_length() -> u64 {
|
|
||||||
let mock = MockAverageSessionLength::mutate(|p| p.take());
|
|
||||||
mock.unwrap_or(pallet_session::PeriodicSessions::<Period, Offset>::average_session_length())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn estimate_current_session_progress(now: u64) -> (Option<Permill>, Weight) {
|
|
||||||
let (estimate, weight) =
|
|
||||||
pallet_session::PeriodicSessions::<Period, Offset>::estimate_current_session_progress(
|
|
||||||
now,
|
|
||||||
);
|
|
||||||
let mock = MockCurrentSessionProgress::mutate(|p| p.take());
|
|
||||||
(mock.unwrap_or(estimate), weight)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn estimate_next_session_rotation(now: u64) -> (Option<u64>, Weight) {
|
|
||||||
pallet_session::PeriodicSessions::<Period, Offset>::estimate_next_session_rotation(now)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ghost_networks::Config for Runtime {
|
impl ghost_networks::Config for Runtime {
|
||||||
type RuntimeEvent = RuntimeEvent;
|
type RuntimeEvent = RuntimeEvent;
|
||||||
type Currency = Balances;
|
type Currency = Balances;
|
||||||
@ -200,7 +178,6 @@ impl Config for Runtime {
|
|||||||
type RuntimeEvent = RuntimeEvent;
|
type RuntimeEvent = RuntimeEvent;
|
||||||
type AuthorityId = UintAuthorityId;
|
type AuthorityId = UintAuthorityId;
|
||||||
|
|
||||||
type NextSessionRotation = TestNextSessionRotation;
|
|
||||||
type ValidatorSet = Historical;
|
type ValidatorSet = Historical;
|
||||||
type Currency = Balances;
|
type Currency = Balances;
|
||||||
type NetworkDataHandler = Networks;
|
type NetworkDataHandler = Networks;
|
||||||
@ -210,9 +187,10 @@ impl Config for Runtime {
|
|||||||
|
|
||||||
type MaxAuthorities = ConstU32<5>;
|
type MaxAuthorities = ConstU32<5>;
|
||||||
type ApplauseThreshold = ConstU32<50>;
|
type ApplauseThreshold = ConstU32<50>;
|
||||||
type OffenceThreshold = ConstU32<75>;
|
type OffenceThreshold = ConstU32<0>;
|
||||||
type UnsignedPriority = ConstU64<{ 1 << 20 }>;
|
type UnsignedPriority = ConstU64<{ 1 << 20 }>;
|
||||||
type HistoryDepth = HistoryDepth;
|
type HistoryDepth = HistoryDepth;
|
||||||
|
type MinAuthoritiesNumber = ConstU32<2>;
|
||||||
|
|
||||||
type WeightInfo = ();
|
type WeightInfo = ();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -979,7 +979,8 @@ fn should_self_applause_after_diabled() {
|
|||||||
let curr_session_index = Session::session_index();
|
let curr_session_index = Session::session_index();
|
||||||
|
|
||||||
pallet::ClapsInSession::<Runtime>::mutate(&session_index, |claps| {
|
pallet::ClapsInSession::<Runtime>::mutate(&session_index, |claps| {
|
||||||
claps.entry(1 as AuthIndex)
|
claps
|
||||||
|
.entry(1 as AuthIndex)
|
||||||
.and_modify(|individual| (*individual).disabled = true)
|
.and_modify(|individual| (*individual).disabled = true)
|
||||||
.or_insert(SessionAuthorityInfo {
|
.or_insert(SessionAuthorityInfo {
|
||||||
claps: 0u32,
|
claps: 0u32,
|
||||||
@ -988,7 +989,8 @@ fn should_self_applause_after_diabled() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
pallet::ClapsInSession::<Runtime>::mutate(&curr_session_index, |claps| {
|
pallet::ClapsInSession::<Runtime>::mutate(&curr_session_index, |claps| {
|
||||||
claps.entry(2 as AuthIndex)
|
claps
|
||||||
|
.entry(2 as AuthIndex)
|
||||||
.and_modify(|individual| (*individual).disabled = true)
|
.and_modify(|individual| (*individual).disabled = true)
|
||||||
.or_insert(SessionAuthorityInfo {
|
.or_insert(SessionAuthorityInfo {
|
||||||
claps: 0u32,
|
claps: 0u32,
|
||||||
@ -1207,6 +1209,22 @@ fn should_not_fail_on_sub_existential_balance() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
fn advance_session_and_get_index() -> u32 {
|
fn advance_session_and_get_index() -> u32 {
|
||||||
advance_session();
|
advance_session();
|
||||||
assert_eq!(Session::validators(), Vec::<u64>::new());
|
assert_eq!(Session::validators(), Vec::<u64>::new());
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "casper-runtime"
|
name = "casper-runtime"
|
||||||
version = "3.5.34"
|
version = "3.5.36"
|
||||||
build = "build.rs"
|
build = "build.rs"
|
||||||
description = "Runtime of the Casper Network"
|
description = "Runtime of the Casper Network"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
|
|||||||
@ -117,8 +117,8 @@ pub const VERSION: RuntimeVersion = RuntimeVersion {
|
|||||||
spec_name: create_runtime_str!("casper"),
|
spec_name: create_runtime_str!("casper"),
|
||||||
impl_name: create_runtime_str!("casper-svengali"),
|
impl_name: create_runtime_str!("casper-svengali"),
|
||||||
authoring_version: 0,
|
authoring_version: 0,
|
||||||
spec_version: 4,
|
spec_version: 6,
|
||||||
impl_version: 2,
|
impl_version: 4,
|
||||||
apis: RUNTIME_API_VERSIONS,
|
apis: RUNTIME_API_VERSIONS,
|
||||||
transaction_version: 1,
|
transaction_version: 1,
|
||||||
state_version: 1,
|
state_version: 1,
|
||||||
@ -1062,6 +1062,8 @@ parameter_types! {
|
|||||||
pub const ApplauseThreshold: u32 = 66;
|
pub const ApplauseThreshold: u32 = 66;
|
||||||
// will be used in `Perbill::from_percent()`
|
// will be used in `Perbill::from_percent()`
|
||||||
pub const OffenceThreshold: u32 = 5;
|
pub const OffenceThreshold: u32 = 5;
|
||||||
|
// 4 validators should be functional if 1 is offline
|
||||||
|
pub const MinAuthoritiesNumber: u32 = 5;
|
||||||
pub const SlowClapUnsignedPriority: TransactionPriority = TransactionPriority::MAX;
|
pub const SlowClapUnsignedPriority: TransactionPriority = TransactionPriority::MAX;
|
||||||
pub const SlowClapHistoryDepth: sp_staking::SessionIndex =
|
pub const SlowClapHistoryDepth: sp_staking::SessionIndex =
|
||||||
StakingHistoryDepth::get() * SessionsPerEra::get();
|
StakingHistoryDepth::get() * SessionsPerEra::get();
|
||||||
@ -1071,7 +1073,6 @@ impl ghost_slow_clap::Config for Runtime {
|
|||||||
type RuntimeEvent = RuntimeEvent;
|
type RuntimeEvent = RuntimeEvent;
|
||||||
type AuthorityId = SlowClapId;
|
type AuthorityId = SlowClapId;
|
||||||
|
|
||||||
type NextSessionRotation = Babe;
|
|
||||||
type ValidatorSet = Historical;
|
type ValidatorSet = Historical;
|
||||||
type Currency = Balances;
|
type Currency = Balances;
|
||||||
type NetworkDataHandler = GhostNetworks;
|
type NetworkDataHandler = GhostNetworks;
|
||||||
@ -1084,6 +1085,7 @@ impl ghost_slow_clap::Config for Runtime {
|
|||||||
type OffenceThreshold = OffenceThreshold;
|
type OffenceThreshold = OffenceThreshold;
|
||||||
type UnsignedPriority = SlowClapUnsignedPriority;
|
type UnsignedPriority = SlowClapUnsignedPriority;
|
||||||
type HistoryDepth = SlowClapHistoryDepth;
|
type HistoryDepth = SlowClapHistoryDepth;
|
||||||
|
type MinAuthoritiesNumber = MinAuthoritiesNumber;
|
||||||
|
|
||||||
type WeightInfo = weights::ghost_slow_clap::WeightInfo<Runtime>;
|
type WeightInfo = weights::ghost_slow_clap::WeightInfo<Runtime>;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user