Skip to content
lib.rs 48.7 KiB
Newer Older
/// Filter historical offences out and only allow those from the bonding period.
pub struct FilterHistoricalOffences<T, R> {
	_inner: sp_std::marker::PhantomData<(T, R)>,
}
impl<T, Reporter, Offender, R, O> ReportOffence<Reporter, Offender, O>
	for FilterHistoricalOffences<Pallet<T>, R>
where
	T: Config,
	R: ReportOffence<Reporter, Offender, O>,
	O: Offence<Offender>,
{
	fn report_offence(reporters: Vec<Reporter>, offence: O) -> Result<(), OffenceError> {
		// Disallow any slashing from before the current bonding period.
		let offence_session = offence.session_index();
		let bonded_eras = BondedEras::<T>::get();
		if bonded_eras.first().filter(|(_, start)| offence_session >= *start).is_some() {
			R::report_offence(reporters, offence)
		} else {
			<Pallet<T>>::deposit_event(Event::<T>::OldSlashingReportDiscarded {
				session_index: offence_session,
			});
	fn is_known_offence(offenders: &[Offender], time_slot: &O::TimeSlot) -> bool {
		R::is_known_offence(offenders, time_slot)
/// Wrapper struct for Era related information. It is not a pure encapsulation as these storage
/// items can be accessed directly but nevertheless, its recommended to use `EraInfo` where we
/// can and add more functions to it as needed.
pub struct EraInfo<T>(sp_std::marker::PhantomData<T>);
impl<T: Config> EraInfo<T> {
	/// Temporary function which looks at both (1) passed param `T::StakingLedger` for legacy
	/// non-paged rewards, and (2) `T::ClaimedRewards` for paged rewards. This function can be
	/// removed once `T::HistoryDepth` eras have passed and none of the older non-paged rewards
	/// are relevant/claimable.
	// Refer tracker issue for cleanup: #13034
	pub(crate) fn is_rewards_claimed_with_legacy_fallback(
		era: EraIndex,
		ledger: &StakingLedger<T>,
		validator: &T::AccountId,
		page: Page,
	) -> bool {
		ledger.legacy_claimed_rewards.binary_search(&era).is_ok() ||
			Self::is_rewards_claimed(era, validator, page)
	}

	/// Check if the rewards for the given era and page index have been claimed.
	///
	/// This is only used for paged rewards. Once older non-paged rewards are no longer
	/// relevant, `is_rewards_claimed_with_legacy_fallback` can be removed and this function can
	/// be made public.
	fn is_rewards_claimed(era: EraIndex, validator: &T::AccountId, page: Page) -> bool {
		ClaimedRewards::<T>::get(era, validator).contains(&page)
	}

	/// Get exposure for a validator at a given era and page.
	///
	/// This builds a paged exposure from `PagedExposureMetadata` and `ExposurePage` of the
	/// validator. For older non-paged exposure, it returns the clipped exposure directly.
		era: EraIndex,
		validator: &T::AccountId,
		page: Page,
	) -> Option<PagedExposure<T::AccountId, BalanceOf<T>>> {
		let overview = <ErasStakersOverview<T>>::get(&era, validator);

		// return clipped exposure if page zero and paged exposure does not exist
		// exists for backward compatibility and can be removed as part of #13034
		if overview.is_none() && page == 0 {
			return Some(PagedExposure::from_clipped(<ErasStakersClipped<T>>::get(era, validator)))
		}

		// no exposure for this validator
		if overview.is_none() {
			return None
		}

		let overview = overview.expect("checked above; qed");

		// validator stake is added only in page zero
		let validator_stake = if page == 0 { overview.own } else { Zero::zero() };

		// since overview is present, paged exposure will always be present except when a
		// validator has only own stake and no nominator stake.
		let exposure_page = <ErasStakersPaged<T>>::get((era, validator, page)).unwrap_or_default();

		// build the exposure
		Some(PagedExposure {
			exposure_metadata: PagedExposureMetadata { own: validator_stake, ..overview },
			exposure_page,
		})
	}

	/// Get full exposure of the validator at a given era.
		era: EraIndex,
		validator: &T::AccountId,
	) -> Exposure<T::AccountId, BalanceOf<T>> {
		let overview = <ErasStakersOverview<T>>::get(&era, validator);

		if overview.is_none() {
			return ErasStakers::<T>::get(era, validator)
		}

		let overview = overview.expect("checked above; qed");

		let mut others = Vec::with_capacity(overview.nominator_count as usize);
		for page in 0..overview.page_count {
			let nominators = <ErasStakersPaged<T>>::get((era, validator, page));
			others.append(&mut nominators.map(|n| n.others).defensive_unwrap_or_default());
		}

		Exposure { total: overview.total, own: overview.own, others }
	}

	/// Returns the number of pages of exposure a validator has for the given era.
	///
	/// For eras where paged exposure does not exist, this returns 1 to keep backward compatibility.
	pub(crate) fn get_page_count(era: EraIndex, validator: &T::AccountId) -> Page {
		<ErasStakersOverview<T>>::get(&era, validator)
			.map(|overview| {
				if overview.page_count == 0 && overview.own > Zero::zero() {
					// Even though there are no nominator pages, there is still validator's own
					// stake exposed which needs to be paid out in a page.
					1
				} else {
					overview.page_count
				}
			})
			// Always returns 1 page for older non-paged exposure.
			// FIXME: Can be cleaned up with issue #13034.
			.unwrap_or(1)
	}

	/// Returns the next page that can be claimed or `None` if nothing to claim.
	pub(crate) fn get_next_claimable_page(
		era: EraIndex,
		validator: &T::AccountId,
		ledger: &StakingLedger<T>,
	) -> Option<Page> {
		if Self::is_non_paged_exposure(era, validator) {
			return match ledger.legacy_claimed_rewards.binary_search(&era) {
				// already claimed
				Ok(_) => None,
				// Non-paged exposure is considered as a single page
				Err(_) => Some(0),
			}
		}

		// Find next claimable page of paged exposure.
		let page_count = Self::get_page_count(era, validator);
		let all_claimable_pages: Vec<Page> = (0..page_count).collect();
		let claimed_pages = ClaimedRewards::<T>::get(era, validator);

		all_claimable_pages.into_iter().find(|p| !claimed_pages.contains(p))
	}

	/// Checks if exposure is paged or not.
	fn is_non_paged_exposure(era: EraIndex, validator: &T::AccountId) -> bool {
		<ErasStakersClipped<T>>::contains_key(&era, validator)
	}

	/// Returns validator commission for this era and page.
	pub(crate) fn get_validator_commission(
		era: EraIndex,
		validator_stash: &T::AccountId,
	) -> Perbill {
		<ErasValidatorPrefs<T>>::get(&era, validator_stash).commission
	}

	/// Creates an entry to track validator reward has been claimed for a given era and page.
	/// Noop if already claimed.
	pub(crate) fn set_rewards_as_claimed(era: EraIndex, validator: &T::AccountId, page: Page) {
		let mut claimed_pages = ClaimedRewards::<T>::get(era, validator);

		// this should never be called if the reward has already been claimed
		if claimed_pages.contains(&page) {
			defensive!("Trying to set an already claimed reward");
			// nevertheless don't do anything since the page already exist in claimed rewards.
			return
		}

		// add page to claimed entries
		claimed_pages.push(page);
		ClaimedRewards::<T>::insert(era, validator, claimed_pages);
	}

	/// Store exposure for elected validators at start of an era.
		era: EraIndex,
		validator: &T::AccountId,
		exposure: Exposure<T::AccountId, BalanceOf<T>>,
	) {
		let page_size = T::MaxExposurePageSize::get().defensive_max(1);

		let nominator_count = exposure.others.len();
		// expected page count is the number of nominators divided by the page size, rounded up.
		let expected_page_count = nominator_count
			.defensive_saturating_add((page_size as usize).defensive_saturating_sub(1))
			.saturating_div(page_size as usize);

		let (exposure_metadata, exposure_pages) = exposure.into_pages(page_size);
		defensive_assert!(exposure_pages.len() == expected_page_count, "unexpected page count");

		<ErasStakersOverview<T>>::insert(era, &validator, &exposure_metadata);
		exposure_pages.iter().enumerate().for_each(|(page, paged_exposure)| {
			<ErasStakersPaged<T>>::insert((era, &validator, page as Page), &paged_exposure);
		});
	}

	/// Store total exposure for all the elected validators in the era.
	pub(crate) fn set_total_stake(era: EraIndex, total_stake: BalanceOf<T>) {
		<ErasTotalStake<T>>::insert(era, total_stake);
	}
}

/// Configurations of the benchmarking of the pallet.
pub trait BenchmarkingConfig {
	/// The maximum number of validators to use.
	type MaxValidators: Get<u32>;
	/// The maximum number of nominators to use.
	type MaxNominators: Get<u32>;
}

/// A mock benchmarking config for pallet-staking.
///
/// Should only be used for testing.
#[cfg(feature = "std")]
pub struct TestBenchmarkingConfig;

#[cfg(feature = "std")]
impl BenchmarkingConfig for TestBenchmarkingConfig {
	type MaxValidators = frame_support::traits::ConstU32<100>;
	type MaxNominators = frame_support::traits::ConstU32<100>;

/// Controls validator disabling
pub trait DisablingStrategy<T: Config> {
	/// Make a disabling decision. Returns the index of the validator to disable or `None` if no new
	/// validator should be disabled.
	fn decision(
		offender_stash: &T::AccountId,
		slash_era: EraIndex,
		currently_disabled: &Vec<u32>,
	) -> Option<u32>;
}

/// Implementation of [`DisablingStrategy`] which disables validators from the active set up to a
/// threshold. `DISABLING_LIMIT_FACTOR` is the factor of the maximum disabled validators in the
/// active set. E.g. setting this value to `3` means no more than 1/3 of the validators in the
/// active set can be disabled in an era.
/// By default a factor of 3 is used which is the byzantine threshold.
pub struct UpToLimitDisablingStrategy<const DISABLING_LIMIT_FACTOR: usize = 3>;

impl<const DISABLING_LIMIT_FACTOR: usize> UpToLimitDisablingStrategy<DISABLING_LIMIT_FACTOR> {
	/// Disabling limit calculated from the total number of validators in the active set. When
	/// reached no more validators will be disabled.
	pub fn disable_limit(validators_len: usize) -> usize {
		validators_len
			.saturating_sub(1)
			.checked_div(DISABLING_LIMIT_FACTOR)
			.unwrap_or_else(|| {
				defensive!("DISABLING_LIMIT_FACTOR should not be 0");
				0
			})
	}
}

impl<T: Config, const DISABLING_LIMIT_FACTOR: usize> DisablingStrategy<T>
	for UpToLimitDisablingStrategy<DISABLING_LIMIT_FACTOR>
{
	fn decision(
		offender_stash: &T::AccountId,
		slash_era: EraIndex,
		currently_disabled: &Vec<u32>,
	) -> Option<u32> {
		let active_set = T::SessionInterface::validators();

		// We don't disable more than the limit
		if currently_disabled.len() >= Self::disable_limit(active_set.len()) {
			log!(
				debug,
				"Won't disable: reached disabling limit {:?}",
				Self::disable_limit(active_set.len())
			);
			return None
		}

		// We don't disable for offences in previous eras
		if ActiveEra::<T>::get().map(|e| e.index).unwrap_or_default() > slash_era {
			log!(
				debug,
				"Won't disable: current_era {:?} > slash_era {:?}",
				Pallet::<T>::current_era().unwrap_or_default(),
				slash_era
			);
			return None
		}

		let offender_idx = if let Some(idx) = active_set.iter().position(|i| i == offender_stash) {
			idx as u32
		} else {
			log!(debug, "Won't disable: offender not in active set",);
			return None
		};

		log!(debug, "Will disable {:?}", offender_idx);

		Some(offender_idx)
	}
}