referrerpolicy=no-referrer-when-downgrade

pallet_session/
disabling.rs

1// This file is part of Substrate.
2
3// Copyright (C) Parity Technologies (UK) Ltd.
4// SPDX-License-Identifier: Apache-2.0
5
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10// 	http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18use crate::*;
19use frame_support::defensive;
20/// Controls validator disabling
21pub trait DisablingStrategy<T: Config> {
22	/// Make a disabling decision. Returning a [`DisablingDecision`]
23	fn decision(
24		offender_stash: &T::ValidatorId,
25		offender_slash_severity: OffenceSeverity,
26		currently_disabled: &Vec<(u32, OffenceSeverity)>,
27	) -> DisablingDecision;
28}
29
30/// Helper struct representing a decision coming from a given [`DisablingStrategy`] implementing
31/// `decision`
32///
33/// `disable` is the index of the validator to disable,
34/// `reenable` is the index of the validator to re-enable.
35#[derive(Debug)]
36pub struct DisablingDecision {
37	pub disable: Option<u32>,
38	pub reenable: Option<u32>,
39}
40
41impl<T: Config> DisablingStrategy<T> for () {
42	fn decision(
43		_offender_stash: &T::ValidatorId,
44		_offender_slash_severity: OffenceSeverity,
45		_currently_disabled: &Vec<(u32, OffenceSeverity)>,
46	) -> DisablingDecision {
47		DisablingDecision { disable: None, reenable: None }
48	}
49}
50/// Calculate the disabling limit based on the number of validators and the disabling limit factor.
51///
52/// This is a sensible default implementation for the disabling limit factor for most disabling
53/// strategies.
54///
55/// Disabling limit factor n=2 -> 1/n = 1/2 = 50% of validators can be disabled
56fn factor_based_disable_limit(validators_len: usize, disabling_limit_factor: usize) -> usize {
57	validators_len
58		.saturating_sub(1)
59		.checked_div(disabling_limit_factor)
60		.unwrap_or_else(|| {
61			defensive!("DISABLING_LIMIT_FACTOR should not be 0");
62			0
63		})
64}
65
66/// Implementation of [`DisablingStrategy`] using factor_based_disable_limit which disables
67/// validators from the active set up to a threshold. `DISABLING_LIMIT_FACTOR` is the factor of the
68/// maximum disabled validators in the active set. E.g. setting this value to `3` means no more than
69/// 1/3 of the validators in the active set can be disabled in an era.
70///
71/// By default a factor of 3 is used which is the byzantine threshold.
72pub struct UpToLimitDisablingStrategy<const DISABLING_LIMIT_FACTOR: usize = 3>;
73
74impl<const DISABLING_LIMIT_FACTOR: usize> UpToLimitDisablingStrategy<DISABLING_LIMIT_FACTOR> {
75	/// Disabling limit calculated from the total number of validators in the active set. When
76	/// reached no more validators will be disabled.
77	pub fn disable_limit(validators_len: usize) -> usize {
78		factor_based_disable_limit(validators_len, DISABLING_LIMIT_FACTOR)
79	}
80}
81
82impl<T: Config, const DISABLING_LIMIT_FACTOR: usize> DisablingStrategy<T>
83	for UpToLimitDisablingStrategy<DISABLING_LIMIT_FACTOR>
84{
85	fn decision(
86		offender_stash: &T::ValidatorId,
87		_offender_slash_severity: OffenceSeverity,
88		currently_disabled: &Vec<(u32, OffenceSeverity)>,
89	) -> DisablingDecision {
90		let active_set = Validators::<T>::get();
91
92		// We don't disable more than the limit
93		if currently_disabled.len() >= Self::disable_limit(active_set.len()) {
94			log!(
95				debug,
96				"Won't disable: reached disabling limit {:?}",
97				Self::disable_limit(active_set.len())
98			);
99			return DisablingDecision { disable: None, reenable: None }
100		}
101
102		let offender_idx = if let Some(idx) = active_set.iter().position(|i| i == offender_stash) {
103			idx as u32
104		} else {
105			log!(debug, "Won't disable: offender not in active set",);
106			return DisablingDecision { disable: None, reenable: None }
107		};
108
109		log!(debug, "Will disable {:?}", offender_idx);
110
111		DisablingDecision { disable: Some(offender_idx), reenable: None }
112	}
113}
114
115/// Implementation of [`DisablingStrategy`] which disables validators from the active set up to a
116/// limit (factor_based_disable_limit) and if the limit is reached and the new offender is higher
117/// (bigger punishment/severity) then it re-enables the lowest offender to free up space for the new
118/// offender.
119///
120/// This strategy is not based on cumulative severity of offences but only on the severity of the
121/// highest offence. Offender first committing a 25% offence and then a 50% offence will be treated
122/// the same as an offender committing 50% offence.
123///
124/// An extension of [`UpToLimitDisablingStrategy`].
125pub struct UpToLimitWithReEnablingDisablingStrategy<const DISABLING_LIMIT_FACTOR: usize = 3>;
126
127impl<const DISABLING_LIMIT_FACTOR: usize>
128	UpToLimitWithReEnablingDisablingStrategy<DISABLING_LIMIT_FACTOR>
129{
130	/// Disabling limit calculated from the total number of validators in the active set. When
131	/// reached re-enabling logic might kick in.
132	pub fn disable_limit(validators_len: usize) -> usize {
133		factor_based_disable_limit(validators_len, DISABLING_LIMIT_FACTOR)
134	}
135}
136
137impl<T: Config, const DISABLING_LIMIT_FACTOR: usize> DisablingStrategy<T>
138	for UpToLimitWithReEnablingDisablingStrategy<DISABLING_LIMIT_FACTOR>
139{
140	fn decision(
141		offender_stash: &T::ValidatorId,
142		offender_slash_severity: OffenceSeverity,
143		currently_disabled: &Vec<(u32, OffenceSeverity)>,
144	) -> DisablingDecision {
145		let active_set = Validators::<T>::get();
146
147		// We don't disable validators that are not in the active set
148		let offender_idx = if let Some(idx) = active_set.iter().position(|i| i == offender_stash) {
149			idx as u32
150		} else {
151			log!(debug, "Won't disable: offender not in active set",);
152			return DisablingDecision { disable: None, reenable: None }
153		};
154
155		// Check if offender is already disabled
156		if let Some((_, old_severity)) =
157			currently_disabled.iter().find(|(idx, _)| *idx == offender_idx)
158		{
159			if offender_slash_severity > *old_severity {
160				log!(debug, "Offender already disabled but with lower severity, will disable again to refresh severity of {:?}", offender_idx);
161				return DisablingDecision { disable: Some(offender_idx), reenable: None };
162			} else {
163				log!(debug, "Offender already disabled with higher or equal severity");
164				return DisablingDecision { disable: None, reenable: None };
165			}
166		}
167
168		// We don't disable more than the limit (but we can re-enable a smaller offender to make
169		// space)
170		if currently_disabled.len() >= Self::disable_limit(active_set.len()) {
171			log!(
172				debug,
173				"Reached disabling limit {:?}, checking for re-enabling",
174				Self::disable_limit(active_set.len())
175			);
176
177			// Find the smallest offender to re-enable that is not higher than
178			// offender_slash_severity
179			if let Some((smallest_idx, _)) = currently_disabled
180				.iter()
181				.filter(|(_, severity)| *severity <= offender_slash_severity)
182				.min_by_key(|(_, severity)| *severity)
183			{
184				log!(debug, "Will disable {:?} and re-enable {:?}", offender_idx, smallest_idx);
185				return DisablingDecision {
186					disable: Some(offender_idx),
187					reenable: Some(*smallest_idx),
188				}
189			} else {
190				log!(debug, "No smaller offender found to re-enable");
191				return DisablingDecision { disable: None, reenable: None }
192			}
193		} else {
194			// If we are not at the limit, just disable the new offender and dont re-enable anyone
195			log!(debug, "Will disable {:?}", offender_idx);
196			return DisablingDecision { disable: Some(offender_idx), reenable: None }
197		}
198	}
199}