From 913232ae00b4954baf538a67a6b022a947579bfb Mon Sep 17 00:00:00 2001 From: Tsvetomir Dimitrov Date: Fri, 20 Oct 2023 16:13:29 +0300 Subject: [PATCH 01/67] Handling of disabled validators in backing subsystem (#1259) Handles validator disabling in the backing subsystem. The PR introduces two changes: 1. Don't import statements from disabled validators. 2. Don't validate and second if the local validator is disabled. The purpose of this change is first to ignore statements from disabled validators on the node side. This is an optimisation as these statements are supposed to be cleared in the runtime too (still not implemented at the moment). Additionally if the local node is a validator which is disabled - don't process any new candidates. --------- Co-authored-by: ordian Co-authored-by: ordian --- polkadot/node/core/backing/src/lib.rs | 87 ++++- polkadot/node/core/backing/src/tests/mod.rs | 327 +++++++++++++++++- .../src/tests/prospective_parachains.rs | 20 ++ polkadot/node/core/provisioner/src/lib.rs | 59 +--- polkadot/node/subsystem-util/src/lib.rs | 80 ++++- polkadot/node/subsystem-util/src/vstaging.rs | 56 +++ 6 files changed, 556 insertions(+), 73 deletions(-) create mode 100644 polkadot/node/subsystem-util/src/vstaging.rs diff --git a/polkadot/node/core/backing/src/lib.rs b/polkadot/node/core/backing/src/lib.rs index 97efb3ba8089..9fc03f2efdd5 100644 --- a/polkadot/node/core/backing/src/lib.rs +++ b/polkadot/node/core/backing/src/lib.rs @@ -118,6 +118,7 @@ use statement_table::{ }, Config as TableConfig, Context as TableContextTrait, Table, }; +use util::vstaging::get_disabled_validators_with_fallback; mod error; @@ -383,6 +384,21 @@ struct TableContext { validator: Option, groups: HashMap>, validators: Vec, + disabled_validators: Vec, +} + +impl TableContext { + // Returns `true` if the provided `ValidatorIndex` is in the disabled validators list + pub fn validator_is_disabled(&self, validator_idx: &ValidatorIndex) -> bool { + self.disabled_validators + .iter() + .any(|disabled_val_idx| *disabled_val_idx == *validator_idx) + } + + // Returns `true` if the local validator is in the disabled validators list + pub fn local_validator_is_disabled(&self) -> Option { + self.validator.as_ref().map(|v| v.disabled()) + } } impl TableContextTrait for TableContext { @@ -1010,21 +1026,33 @@ async fn construct_per_relay_parent_state( let minimum_backing_votes = try_runtime_api!(request_min_backing_votes(parent, session_index, ctx.sender()).await); + // TODO: https://github.com/paritytech/polkadot-sdk/issues/1940 + // Once runtime ver `DISABLED_VALIDATORS_RUNTIME_REQUIREMENT` is released remove this call to + // `get_disabled_validators_with_fallback`, add `request_disabled_validators` call to the + // `try_join!` above and use `try_runtime_api!` to get `disabled_validators` + let disabled_validators = get_disabled_validators_with_fallback(ctx.sender(), parent) + .await + .map_err(Error::UtilError)?; + let signing_context = SigningContext { parent_hash: parent, session_index }; - let validator = - match Validator::construct(&validators, signing_context.clone(), keystore.clone()) { - Ok(v) => Some(v), - Err(util::Error::NotAValidator) => None, - Err(e) => { - gum::warn!( - target: LOG_TARGET, - err = ?e, - "Cannot participate in candidate backing", - ); + let validator = match Validator::construct( + &validators, + &disabled_validators, + signing_context.clone(), + keystore.clone(), + ) { + Ok(v) => Some(v), + Err(util::Error::NotAValidator) => None, + Err(e) => { + gum::warn!( + target: LOG_TARGET, + err = ?e, + "Cannot participate in candidate backing", + ); - return Ok(None) - }, - }; + return Ok(None) + }, + }; let mut groups = HashMap::new(); let n_cores = cores.len(); @@ -1054,7 +1082,7 @@ async fn construct_per_relay_parent_state( } } - let table_context = TableContext { groups, validators, validator }; + let table_context = TableContext { validator, groups, validators, disabled_validators }; let table_config = TableConfig { allow_multiple_seconded: match mode { ProspectiveParachainsMode::Enabled { .. } => true, @@ -1728,6 +1756,19 @@ async fn kick_off_validation_work( background_validation_tx: &mpsc::Sender<(Hash, ValidatedCandidateCommand)>, attesting: AttestingData, ) -> Result<(), Error> { + // Do nothing if the local validator is disabled or not a validator at all + match rp_state.table_context.local_validator_is_disabled() { + Some(true) => { + gum::info!(target: LOG_TARGET, "We are disabled - don't kick off validation"); + return Ok(()) + }, + Some(false) => {}, // we are not disabled - move on + None => { + gum::debug!(target: LOG_TARGET, "We are not a validator - don't kick off validation"); + return Ok(()) + }, + } + let candidate_hash = attesting.candidate.hash(); if rp_state.issued_statements.contains(&candidate_hash) { return Ok(()) @@ -1785,6 +1826,16 @@ async fn maybe_validate_and_import( }, }; + // Don't import statement if the sender is disabled + if rp_state.table_context.validator_is_disabled(&statement.validator_index()) { + gum::debug!( + target: LOG_TARGET, + sender_validator_idx = ?statement.validator_index(), + "Not importing statement because the sender is disabled" + ); + return Ok(()) + } + let res = import_statement(ctx, rp_state, &mut state.per_candidate, &statement).await; // if we get an Error::RejectedByProspectiveParachains, @@ -1946,6 +1997,13 @@ async fn handle_second_message( Some(r) => r, }; + // Just return if the local validator is disabled. If we are here the local node should be a + // validator but defensively use `unwrap_or(false)` to continue processing in this case. + if rp_state.table_context.local_validator_is_disabled().unwrap_or(false) { + gum::warn!(target: LOG_TARGET, "Local validator is disabled. Don't validate and second"); + return Ok(()) + } + // Sanity check that candidate is from our assignment. if Some(candidate.descriptor().para_id) != rp_state.assignment { gum::debug!( @@ -1992,6 +2050,7 @@ async fn handle_statement_message( ) -> Result<(), Error> { let _timer = metrics.time_process_statement(); + // Validator disabling is handled in `maybe_validate_and_import` match maybe_validate_and_import(ctx, state, relay_parent, statement).await { Err(Error::ValidationFailed(_)) => Ok(()), Err(e) => Err(e), diff --git a/polkadot/node/core/backing/src/tests/mod.rs b/polkadot/node/core/backing/src/tests/mod.rs index 35d17f3d905e..26b0c679325b 100644 --- a/polkadot/node/core/backing/src/tests/mod.rs +++ b/polkadot/node/core/backing/src/tests/mod.rs @@ -41,7 +41,7 @@ use sp_keyring::Sr25519Keyring; use sp_keystore::Keystore; use sp_tracing as _; use statement_table::v2::Misbehavior; -use std::collections::HashMap; +use std::{collections::HashMap, time::Duration}; mod prospective_parachains; @@ -77,6 +77,7 @@ struct TestState { signing_context: SigningContext, relay_parent: Hash, minimum_backing_votes: u32, + disabled_validators: Vec, } impl TestState { @@ -148,6 +149,7 @@ impl Default for TestState { signing_context, relay_parent, minimum_backing_votes: LEGACY_MIN_BACKING_VOTES, + disabled_validators: Vec::new(), } } } @@ -293,6 +295,26 @@ async fn test_startup(virtual_overseer: &mut VirtualOverseer, test_state: &TestS tx.send(Ok(test_state.minimum_backing_votes)).unwrap(); } ); + + // Check that subsystem job issues a request for the runtime version. + assert_matches!( + virtual_overseer.recv().await, + AllMessages::RuntimeApi( + RuntimeApiMessage::Request(parent, RuntimeApiRequest::Version(tx)) + ) if parent == test_state.relay_parent => { + tx.send(Ok(RuntimeApiRequest::DISABLED_VALIDATORS_RUNTIME_REQUIREMENT)).unwrap(); + } + ); + + // Check that subsystem job issues a request for the disabled validators. + assert_matches!( + virtual_overseer.recv().await, + AllMessages::RuntimeApi( + RuntimeApiMessage::Request(parent, RuntimeApiRequest::DisabledValidators(tx)) + ) if parent == test_state.relay_parent => { + tx.send(Ok(test_state.disabled_validators.clone())).unwrap(); + } + ); } async fn assert_validation_requests( @@ -1420,6 +1442,7 @@ fn candidate_backing_reorders_votes() { let table_context = TableContext { validator: None, + disabled_validators: Vec::new(), groups: validator_groups, validators: validator_public.clone(), }; @@ -1958,3 +1981,305 @@ fn new_leaf_view_doesnt_clobber_old() { virtual_overseer }); } + +// Test that a disabled local validator doesn't do any work on `CandidateBackingMessage::Second` +#[test] +fn disabled_validator_doesnt_distribute_statement_on_receiving_second() { + let mut test_state = TestState::default(); + test_state.disabled_validators.push(ValidatorIndex(0)); + + test_harness(test_state.keystore.clone(), |mut virtual_overseer| async move { + test_startup(&mut virtual_overseer, &test_state).await; + + let pov = PoV { block_data: BlockData(vec![42, 43, 44]) }; + let pvd = dummy_pvd(); + let validation_code = ValidationCode(vec![1, 2, 3]); + + let expected_head_data = test_state.head_data.get(&test_state.chain_ids[0]).unwrap(); + + let pov_hash = pov.hash(); + let candidate = TestCandidateBuilder { + para_id: test_state.chain_ids[0], + relay_parent: test_state.relay_parent, + pov_hash, + head_data: expected_head_data.clone(), + erasure_root: make_erasure_root(&test_state, pov.clone(), pvd.clone()), + persisted_validation_data_hash: pvd.hash(), + validation_code: validation_code.0.clone(), + } + .build(); + + let second = CandidateBackingMessage::Second( + test_state.relay_parent, + candidate.to_plain(), + pvd.clone(), + pov.clone(), + ); + + virtual_overseer.send(FromOrchestra::Communication { msg: second }).await; + + // Ensure backing subsystem is not doing any work + assert_matches!(virtual_overseer.recv().timeout(Duration::from_secs(1)).await, None); + + virtual_overseer + .send(FromOrchestra::Signal(OverseerSignal::ActiveLeaves( + ActiveLeavesUpdate::stop_work(test_state.relay_parent), + ))) + .await; + virtual_overseer + }); +} + +// Test that a disabled local validator doesn't do any work on `CandidateBackingMessage::Statement` +#[test] +fn disabled_validator_doesnt_distribute_statement_on_receiving_statement() { + let mut test_state = TestState::default(); + test_state.disabled_validators.push(ValidatorIndex(0)); + + test_harness(test_state.keystore.clone(), |mut virtual_overseer| async move { + test_startup(&mut virtual_overseer, &test_state).await; + + let pov = PoV { block_data: BlockData(vec![42, 43, 44]) }; + let pvd = dummy_pvd(); + let validation_code = ValidationCode(vec![1, 2, 3]); + + let expected_head_data = test_state.head_data.get(&test_state.chain_ids[0]).unwrap(); + + let pov_hash = pov.hash(); + let candidate = TestCandidateBuilder { + para_id: test_state.chain_ids[0], + relay_parent: test_state.relay_parent, + pov_hash, + head_data: expected_head_data.clone(), + erasure_root: make_erasure_root(&test_state, pov.clone(), pvd.clone()), + persisted_validation_data_hash: pvd.hash(), + validation_code: validation_code.0.clone(), + } + .build(); + + let public2 = Keystore::sr25519_generate_new( + &*test_state.keystore, + ValidatorId::ID, + Some(&test_state.validators[2].to_seed()), + ) + .expect("Insert key into keystore"); + + let signed = SignedFullStatementWithPVD::sign( + &test_state.keystore, + StatementWithPVD::Seconded(candidate.clone(), pvd.clone()), + &test_state.signing_context, + ValidatorIndex(2), + &public2.into(), + ) + .ok() + .flatten() + .expect("should be signed"); + + let statement = CandidateBackingMessage::Statement(test_state.relay_parent, signed.clone()); + + virtual_overseer.send(FromOrchestra::Communication { msg: statement }).await; + + // Ensure backing subsystem is not doing any work + assert_matches!(virtual_overseer.recv().timeout(Duration::from_secs(1)).await, None); + + virtual_overseer + .send(FromOrchestra::Signal(OverseerSignal::ActiveLeaves( + ActiveLeavesUpdate::stop_work(test_state.relay_parent), + ))) + .await; + virtual_overseer + }); +} + +// Test that a validator doesn't do any work on receiving a `CandidateBackingMessage::Statement` +// from a disabled validator +#[test] +fn validator_ignores_statements_from_disabled_validators() { + let mut test_state = TestState::default(); + test_state.disabled_validators.push(ValidatorIndex(2)); + + test_harness(test_state.keystore.clone(), |mut virtual_overseer| async move { + test_startup(&mut virtual_overseer, &test_state).await; + + let pov = PoV { block_data: BlockData(vec![42, 43, 44]) }; + let pvd = dummy_pvd(); + let validation_code = ValidationCode(vec![1, 2, 3]); + + let expected_head_data = test_state.head_data.get(&test_state.chain_ids[0]).unwrap(); + + let pov_hash = pov.hash(); + let candidate = TestCandidateBuilder { + para_id: test_state.chain_ids[0], + relay_parent: test_state.relay_parent, + pov_hash, + head_data: expected_head_data.clone(), + erasure_root: make_erasure_root(&test_state, pov.clone(), pvd.clone()), + persisted_validation_data_hash: pvd.hash(), + validation_code: validation_code.0.clone(), + } + .build(); + let candidate_commitments_hash = candidate.commitments.hash(); + + let public2 = Keystore::sr25519_generate_new( + &*test_state.keystore, + ValidatorId::ID, + Some(&test_state.validators[2].to_seed()), + ) + .expect("Insert key into keystore"); + + let signed_2 = SignedFullStatementWithPVD::sign( + &test_state.keystore, + StatementWithPVD::Seconded(candidate.clone(), pvd.clone()), + &test_state.signing_context, + ValidatorIndex(2), + &public2.into(), + ) + .ok() + .flatten() + .expect("should be signed"); + + let statement_2 = + CandidateBackingMessage::Statement(test_state.relay_parent, signed_2.clone()); + + virtual_overseer.send(FromOrchestra::Communication { msg: statement_2 }).await; + + // Ensure backing subsystem is not doing any work + assert_matches!(virtual_overseer.recv().timeout(Duration::from_secs(1)).await, None); + + // Now send a statement from a honest validator and make sure it gets processed + let public3 = Keystore::sr25519_generate_new( + &*test_state.keystore, + ValidatorId::ID, + Some(&test_state.validators[3].to_seed()), + ) + .expect("Insert key into keystore"); + + let signed_3 = SignedFullStatementWithPVD::sign( + &test_state.keystore, + StatementWithPVD::Seconded(candidate.clone(), pvd.clone()), + &test_state.signing_context, + ValidatorIndex(3), + &public3.into(), + ) + .ok() + .flatten() + .expect("should be signed"); + + let statement_3 = + CandidateBackingMessage::Statement(test_state.relay_parent, signed_3.clone()); + + virtual_overseer.send(FromOrchestra::Communication { msg: statement_3 }).await; + + assert_matches!( + virtual_overseer.recv().await, + AllMessages::RuntimeApi( + RuntimeApiMessage::Request(_, RuntimeApiRequest::ValidationCodeByHash(hash, tx)) + ) if hash == validation_code.hash() => { + tx.send(Ok(Some(validation_code.clone()))).unwrap(); + } + ); + + assert_matches!( + virtual_overseer.recv().await, + AllMessages::RuntimeApi( + RuntimeApiMessage::Request(_, RuntimeApiRequest::SessionIndexForChild(tx)) + ) => { + tx.send(Ok(1u32.into())).unwrap(); + } + ); + + assert_matches!( + virtual_overseer.recv().await, + AllMessages::RuntimeApi( + RuntimeApiMessage::Request(_, RuntimeApiRequest::SessionExecutorParams(sess_idx, tx)) + ) if sess_idx == 1 => { + tx.send(Ok(Some(ExecutorParams::default()))).unwrap(); + } + ); + + // Sending a `Statement::Seconded` for our assignment will start + // validation process. The first thing requested is the PoV. + assert_matches!( + virtual_overseer.recv().await, + AllMessages::AvailabilityDistribution( + AvailabilityDistributionMessage::FetchPoV { + relay_parent, + tx, + .. + } + ) if relay_parent == test_state.relay_parent => { + tx.send(pov.clone()).unwrap(); + } + ); + + // The next step is the actual request to Validation subsystem + // to validate the `Seconded` candidate. + assert_matches!( + virtual_overseer.recv().await, + AllMessages::CandidateValidation( + CandidateValidationMessage::ValidateFromExhaustive( + _pvd, + _validation_code, + candidate_receipt, + _pov, + _, + timeout, + tx, + ), + ) if _pvd == pvd && + _validation_code == validation_code && + *_pov == pov && &candidate_receipt.descriptor == candidate.descriptor() && + timeout == PvfExecTimeoutKind::Backing && + candidate_commitments_hash == candidate_receipt.commitments_hash => + { + tx.send(Ok( + ValidationResult::Valid(CandidateCommitments { + head_data: expected_head_data.clone(), + upward_messages: Default::default(), + horizontal_messages: Default::default(), + new_validation_code: None, + processed_downward_messages: 0, + hrmp_watermark: 0, + }, test_state.validation_data.clone()), + )).unwrap(); + } + ); + + assert_matches!( + virtual_overseer.recv().await, + AllMessages::AvailabilityStore( + AvailabilityStoreMessage::StoreAvailableData { candidate_hash, tx, .. } + ) if candidate_hash == candidate.hash() => { + tx.send(Ok(())).unwrap(); + } + ); + + assert_matches!( + virtual_overseer.recv().await, + AllMessages::StatementDistribution( + StatementDistributionMessage::Share(hash, _stmt) + ) => { + assert_eq!(test_state.relay_parent, hash); + } + ); + + assert_matches!( + virtual_overseer.recv().await, + AllMessages::Provisioner( + ProvisionerMessage::ProvisionableData( + _, + ProvisionableData::BackedCandidate(candidate_receipt) + ) + ) => { + assert_eq!(candidate_receipt, candidate.to_plain()); + } + ); + + virtual_overseer + .send(FromOrchestra::Signal(OverseerSignal::ActiveLeaves( + ActiveLeavesUpdate::stop_work(test_state.relay_parent), + ))) + .await; + virtual_overseer + }); +} diff --git a/polkadot/node/core/backing/src/tests/prospective_parachains.rs b/polkadot/node/core/backing/src/tests/prospective_parachains.rs index b79515ed37a6..661b74c2f969 100644 --- a/polkadot/node/core/backing/src/tests/prospective_parachains.rs +++ b/polkadot/node/core/backing/src/tests/prospective_parachains.rs @@ -195,6 +195,26 @@ async fn activate_leaf( tx.send(Ok(test_state.minimum_backing_votes)).unwrap(); } ); + + // Check that subsystem job issues a request for the runtime version. + assert_matches!( + virtual_overseer.recv().await, + AllMessages::RuntimeApi( + RuntimeApiMessage::Request(parent, RuntimeApiRequest::Version(tx)) + ) if parent == hash => { + tx.send(Ok(RuntimeApiRequest::DISABLED_VALIDATORS_RUNTIME_REQUIREMENT)).unwrap(); + } + ); + + // Check that the subsystem job issues a request for the disabled validators. + assert_matches!( + virtual_overseer.recv().await, + AllMessages::RuntimeApi( + RuntimeApiMessage::Request(parent, RuntimeApiRequest::DisabledValidators(tx)) + ) if parent == hash => { + tx.send(Ok(Vec::new())).unwrap(); + } + ); } } diff --git a/polkadot/node/core/provisioner/src/lib.rs b/polkadot/node/core/provisioner/src/lib.rs index f81e5550b15d..a8475cffa199 100644 --- a/polkadot/node/core/provisioner/src/lib.rs +++ b/polkadot/node/core/provisioner/src/lib.rs @@ -29,13 +29,13 @@ use polkadot_node_subsystem::{ jaeger, messages::{ CandidateBackingMessage, ChainApiMessage, ProspectiveParachainsMessage, ProvisionableData, - ProvisionerInherentData, ProvisionerMessage, RuntimeApiMessage, RuntimeApiRequest, + ProvisionerInherentData, ProvisionerMessage, RuntimeApiRequest, }, overseer, ActivatedLeaf, ActiveLeavesUpdate, FromOrchestra, LeafStatus, OverseerSignal, - PerLeafSpan, RuntimeApiError, SpawnedSubsystem, SubsystemError, + PerLeafSpan, SpawnedSubsystem, SubsystemError, }; use polkadot_node_subsystem_util::{ - request_availability_cores, request_persisted_validation_data, + has_required_runtime, request_availability_cores, request_persisted_validation_data, runtime::{prospective_parachains_mode, ProspectiveParachainsMode}, TimeoutExt, }; @@ -862,56 +862,3 @@ fn bitfields_indicate_availability( 3 * availability.count_ones() >= 2 * availability.len() } - -// If we have to be absolutely precise here, this method gets the version of the `ParachainHost` -// api. For brevity we'll just call it 'runtime version'. -async fn has_required_runtime( - sender: &mut impl overseer::ProvisionerSenderTrait, - relay_parent: Hash, - required_runtime_version: u32, -) -> bool { - gum::trace!(target: LOG_TARGET, ?relay_parent, "Fetching ParachainHost runtime api version"); - - let (tx, rx) = oneshot::channel(); - sender - .send_message(RuntimeApiMessage::Request(relay_parent, RuntimeApiRequest::Version(tx))) - .await; - - match rx.await { - Result::Ok(Ok(runtime_version)) => { - gum::trace!( - target: LOG_TARGET, - ?relay_parent, - ?runtime_version, - ?required_runtime_version, - "Fetched ParachainHost runtime api version" - ); - runtime_version >= required_runtime_version - }, - Result::Ok(Err(RuntimeApiError::Execution { source: error, .. })) => { - gum::trace!( - target: LOG_TARGET, - ?relay_parent, - ?error, - "Execution error while fetching ParachainHost runtime api version" - ); - false - }, - Result::Ok(Err(RuntimeApiError::NotSupported { .. })) => { - gum::trace!( - target: LOG_TARGET, - ?relay_parent, - "NotSupported error while fetching ParachainHost runtime api version" - ); - false - }, - Result::Err(_) => { - gum::trace!( - target: LOG_TARGET, - ?relay_parent, - "Cancelled error while fetching ParachainHost runtime api version" - ); - false - }, - } -} diff --git a/polkadot/node/subsystem-util/src/lib.rs b/polkadot/node/subsystem-util/src/lib.rs index a5f3e9d4a0c0..1fe52767df61 100644 --- a/polkadot/node/subsystem-util/src/lib.rs +++ b/polkadot/node/subsystem-util/src/lib.rs @@ -55,6 +55,7 @@ use sp_core::ByteArray; use sp_keystore::{Error as KeystoreError, KeystorePtr}; use std::time::Duration; use thiserror::Error; +use vstaging::get_disabled_validators_with_fallback; pub use metered; pub use polkadot_node_network_protocol::MIN_GOSSIP_PEERS; @@ -79,6 +80,9 @@ pub mod inclusion_emulator; /// Convenient and efficient runtime info access. pub mod runtime; +/// Helpers for working with unreleased runtime calls +pub mod vstaging; + /// Nested message sending /// /// Useful for having mostly synchronous code, with submodules spawning short lived asynchronous @@ -92,6 +96,8 @@ mod determine_new_blocks; #[cfg(test)] mod tests; +const LOG_TARGET: &'static str = "parachain::subsystem-util"; + /// Duration a job will wait after sending a stop signal before hard-aborting. pub const JOB_GRACEFUL_STOP_DURATION: Duration = Duration::from_secs(1); /// Capacity of channels to and from individual jobs @@ -157,6 +163,62 @@ where rx } +/// Verifies if `ParachainHost` runtime api is at least at version `required_runtime_version`. This +/// method is used to determine if a given runtime call is supported by the runtime. +pub async fn has_required_runtime( + sender: &mut Sender, + relay_parent: Hash, + required_runtime_version: u32, +) -> bool +where + Sender: SubsystemSender, +{ + gum::trace!(target: LOG_TARGET, ?relay_parent, "Fetching ParachainHost runtime api version"); + + let (tx, rx) = oneshot::channel(); + sender + .send_message(RuntimeApiMessage::Request(relay_parent, RuntimeApiRequest::Version(tx))) + .await; + + match rx.await { + Result::Ok(Ok(runtime_version)) => { + gum::trace!( + target: LOG_TARGET, + ?relay_parent, + ?runtime_version, + ?required_runtime_version, + "Fetched ParachainHost runtime api version" + ); + runtime_version >= required_runtime_version + }, + Result::Ok(Err(RuntimeApiError::Execution { source: error, .. })) => { + gum::trace!( + target: LOG_TARGET, + ?relay_parent, + ?error, + "Execution error while fetching ParachainHost runtime api version" + ); + false + }, + Result::Ok(Err(RuntimeApiError::NotSupported { .. })) => { + gum::trace!( + target: LOG_TARGET, + ?relay_parent, + "NotSupported error while fetching ParachainHost runtime api version" + ); + false + }, + Result::Err(_) => { + gum::trace!( + target: LOG_TARGET, + ?relay_parent, + "Cancelled error while fetching ParachainHost runtime api version" + ); + false + }, + } +} + /// Construct specialized request functions for the runtime. /// /// These would otherwise get pretty repetitive. @@ -378,6 +440,7 @@ pub struct Validator { signing_context: SigningContext, key: ValidatorId, index: ValidatorIndex, + disabled: bool, } impl Validator { @@ -399,7 +462,12 @@ impl Validator { let validators = validators?; - Self::construct(&validators, signing_context, keystore) + // TODO: https://github.com/paritytech/polkadot-sdk/issues/1940 + // When `DisabledValidators` is released remove this and add a + // `request_disabled_validators` call here + let disabled_validators = get_disabled_validators_with_fallback(sender, parent).await?; + + Self::construct(&validators, &disabled_validators, signing_context, keystore) } /// Construct a validator instance without performing runtime fetches. @@ -407,13 +475,16 @@ impl Validator { /// This can be useful if external code also needs the same data. pub fn construct( validators: &[ValidatorId], + disabled_validators: &[ValidatorIndex], signing_context: SigningContext, keystore: KeystorePtr, ) -> Result { let (key, index) = signing_key_and_index(validators, &keystore).ok_or(Error::NotAValidator)?; - Ok(Validator { signing_context, key, index }) + let disabled = disabled_validators.iter().any(|d: &ValidatorIndex| *d == index); + + Ok(Validator { signing_context, key, index, disabled }) } /// Get this validator's id. @@ -426,6 +497,11 @@ impl Validator { self.index } + /// Get the enabled/disabled state of this validator + pub fn disabled(&self) -> bool { + self.disabled + } + /// Get the current signing context. pub fn signing_context(&self) -> &SigningContext { &self.signing_context diff --git a/polkadot/node/subsystem-util/src/vstaging.rs b/polkadot/node/subsystem-util/src/vstaging.rs new file mode 100644 index 000000000000..3efd3b61f93c --- /dev/null +++ b/polkadot/node/subsystem-util/src/vstaging.rs @@ -0,0 +1,56 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// This file is part of Polkadot. + +// Polkadot is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Polkadot is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Polkadot. If not, see . + +//! Contains helpers for staging runtime calls. +//! +//! This module is intended to contain common boiler plate code handling unreleased runtime API +//! calls. + +use polkadot_node_subsystem_types::messages::{RuntimeApiMessage, RuntimeApiRequest}; +use polkadot_overseer::SubsystemSender; +use polkadot_primitives::{Hash, ValidatorIndex}; + +use crate::{has_required_runtime, request_disabled_validators, Error}; + +const LOG_TARGET: &'static str = "parachain::subsystem-util-vstaging"; + +// TODO: https://github.com/paritytech/polkadot-sdk/issues/1940 +/// Returns disabled validators list if the runtime supports it. Otherwise logs a debug messages and +/// returns an empty vec. +/// Once runtime ver `DISABLED_VALIDATORS_RUNTIME_REQUIREMENT` is released remove this function and +/// replace all usages with `request_disabled_validators` +pub async fn get_disabled_validators_with_fallback>( + sender: &mut Sender, + relay_parent: Hash, +) -> Result, Error> { + let disabled_validators = if has_required_runtime( + sender, + relay_parent, + RuntimeApiRequest::DISABLED_VALIDATORS_RUNTIME_REQUIREMENT, + ) + .await + { + request_disabled_validators(relay_parent, sender) + .await + .await + .map_err(Error::Oneshot)?? + } else { + gum::debug!(target: LOG_TARGET, "Runtime doesn't support `DisabledValidators` - continuing with an empty disabled validators set"); + vec![] + }; + + Ok(disabled_validators) +} From bbb6631641f9adba30c0ee6f4d11023a424dd362 Mon Sep 17 00:00:00 2001 From: Tsvetomir Dimitrov Date: Wed, 8 Nov 2023 15:54:52 +0200 Subject: [PATCH 02/67] Any offender is disabled for a whole era - wip --- substrate/frame/offences/src/lib.rs | 1 - substrate/frame/offences/src/migration.rs | 9 +---- substrate/frame/offences/src/mock.rs | 1 - substrate/frame/staking/src/mock.rs | 8 ++-- substrate/frame/staking/src/pallet/impls.rs | 10 ++--- substrate/frame/staking/src/pallet/mod.rs | 3 +- substrate/frame/staking/src/slashing.rs | 43 +++++++-------------- substrate/frame/staking/src/tests.rs | 41 +++++--------------- substrate/primitives/staking/src/offence.rs | 5 +-- 9 files changed, 36 insertions(+), 85 deletions(-) diff --git a/substrate/frame/offences/src/lib.rs b/substrate/frame/offences/src/lib.rs index 1c7ffeca7198..a328b2fee4e2 100644 --- a/substrate/frame/offences/src/lib.rs +++ b/substrate/frame/offences/src/lib.rs @@ -132,7 +132,6 @@ where &concurrent_offenders, &slash_perbill, offence.session_index(), - offence.disable_strategy(), ); // Deposit the event. diff --git a/substrate/frame/offences/src/migration.rs b/substrate/frame/offences/src/migration.rs index 3b5cf3ce9269..199f47491369 100644 --- a/substrate/frame/offences/src/migration.rs +++ b/substrate/frame/offences/src/migration.rs @@ -23,7 +23,7 @@ use frame_support::{ weights::Weight, Twox64Concat, }; -use sp_staking::offence::{DisableStrategy, OnOffenceHandler}; +use sp_staking::offence::OnOffenceHandler; use sp_std::vec::Vec; #[cfg(feature = "try-runtime")] @@ -106,12 +106,7 @@ pub fn remove_deferred_storage() -> Weight { let deferred = >::take(); log::info!(target: LOG_TARGET, "have {} deferred offences, applying.", deferred.len()); for (offences, perbill, session) in deferred.iter() { - let consumed = T::OnOffenceHandler::on_offence( - offences, - perbill, - *session, - DisableStrategy::WhenSlashed, - ); + let consumed = T::OnOffenceHandler::on_offence(offences, perbill, *session); weight = weight.saturating_add(consumed); } diff --git a/substrate/frame/offences/src/mock.rs b/substrate/frame/offences/src/mock.rs index 990ceae5ac01..2b80668b0d49 100644 --- a/substrate/frame/offences/src/mock.rs +++ b/substrate/frame/offences/src/mock.rs @@ -51,7 +51,6 @@ impl offence::OnOffenceHandler _offenders: &[OffenceDetails], slash_fraction: &[Perbill], _offence_session: SessionIndex, - _disable_strategy: DisableStrategy, ) -> Weight { OnOffencePerbill::mutate(|f| { *f = slash_fraction.to_vec(); diff --git a/substrate/frame/staking/src/mock.rs b/substrate/frame/staking/src/mock.rs index d2afd8f26e24..c23b1caf3bad 100644 --- a/substrate/frame/staking/src/mock.rs +++ b/substrate/frame/staking/src/mock.rs @@ -40,7 +40,7 @@ use sp_runtime::{ BuildStorage, }; use sp_staking::{ - offence::{DisableStrategy, OffenceDetails, OnOffenceHandler}, + offence::{OffenceDetails, OnOffenceHandler}, OnStakingUpdate, }; @@ -723,12 +723,11 @@ pub(crate) fn on_offence_in_era( >], slash_fraction: &[Perbill], era: EraIndex, - disable_strategy: DisableStrategy, ) { let bonded_eras = crate::BondedEras::::get(); for &(bonded_era, start_session) in bonded_eras.iter() { if bonded_era == era { - let _ = Staking::on_offence(offenders, slash_fraction, start_session, disable_strategy); + let _ = Staking::on_offence(offenders, slash_fraction, start_session); return } else if bonded_era > era { break @@ -740,7 +739,6 @@ pub(crate) fn on_offence_in_era( offenders, slash_fraction, Staking::eras_start_session_index(era).unwrap(), - disable_strategy, ); } else { panic!("cannot slash in era {}", era); @@ -755,7 +753,7 @@ pub(crate) fn on_offence_now( slash_fraction: &[Perbill], ) { let now = Staking::active_era().unwrap().index; - on_offence_in_era(offenders, slash_fraction, now, DisableStrategy::WhenSlashed) + on_offence_in_era(offenders, slash_fraction, now) } pub(crate) fn add_slash(who: &AccountId) { diff --git a/substrate/frame/staking/src/pallet/impls.rs b/substrate/frame/staking/src/pallet/impls.rs index 9c36c94b87b4..d0e509b0da0c 100644 --- a/substrate/frame/staking/src/pallet/impls.rs +++ b/substrate/frame/staking/src/pallet/impls.rs @@ -40,7 +40,7 @@ use sp_runtime::{ }; use sp_staking::{ currency_to_vote::CurrencyToVote, - offence::{DisableStrategy, OffenceDetails, OnOffenceHandler}, + offence::{OffenceDetails, OnOffenceHandler}, EraIndex, Page, SessionIndex, Stake, StakingAccount::{self, Controller, Stash}, StakingInterface, @@ -427,10 +427,8 @@ impl Pallet { } // disable all offending validators that have been disabled for the whole era - for (index, disabled) in >::get() { - if disabled { - T::SessionInterface::disable_validator(index); - } + for index in >::get() { + T::SessionInterface::disable_validator(index); } } @@ -1354,7 +1352,6 @@ where >], slash_fraction: &[Perbill], slash_session: SessionIndex, - disable_strategy: DisableStrategy, ) -> Weight { let reward_proportion = SlashRewardFraction::::get(); let mut consumed_weight = Weight::from_parts(0, 0); @@ -1419,7 +1416,6 @@ where window_start, now: active_era, reward_proportion, - disable_strategy, }); Self::deposit_event(Event::::SlashReported { diff --git a/substrate/frame/staking/src/pallet/mod.rs b/substrate/frame/staking/src/pallet/mod.rs index 18ad3e4a6cf1..667b3859a023 100644 --- a/substrate/frame/staking/src/pallet/mod.rs +++ b/substrate/frame/staking/src/pallet/mod.rs @@ -645,10 +645,11 @@ pub mod pallet { /// `OffendingValidatorsThreshold` is reached. The vec is always kept sorted so that we can find /// whether a given validator has previously offended using binary search. It gets cleared when /// the era ends. + // TODO: Fix the comment above #[pallet::storage] #[pallet::unbounded] #[pallet::getter(fn offending_validators)] - pub type OffendingValidators = StorageValue<_, Vec<(u32, bool)>, ValueQuery>; + pub type OffendingValidators = StorageValue<_, Vec, ValueQuery>; /// The threshold for when users can start calling `chill_other` for other validators / /// nominators. The threshold is compared to the actual number of validators / nominators diff --git a/substrate/frame/staking/src/slashing.rs b/substrate/frame/staking/src/slashing.rs index 0d84d503733e..35b35217e49e 100644 --- a/substrate/frame/staking/src/slashing.rs +++ b/substrate/frame/staking/src/slashing.rs @@ -64,7 +64,7 @@ use sp_runtime::{ traits::{Saturating, Zero}, DispatchResult, RuntimeDebug, }; -use sp_staking::{offence::DisableStrategy, EraIndex}; +use sp_staking::EraIndex; use sp_std::vec::Vec; /// The proportion of the slashing reward to be paid out on the first slashing detection. @@ -215,8 +215,6 @@ pub(crate) struct SlashParams<'a, T: 'a + Config> { /// The maximum percentage of a slash that ever gets paid out. /// This is f_inf in the paper. pub(crate) reward_proportion: Perbill, - /// When to disable offenders. - pub(crate) disable_strategy: DisableStrategy, } /// Computes a slash of a validator and nominators. It returns an unapplied @@ -285,8 +283,7 @@ pub(crate) fn compute_slash( } } - let disable_when_slashed = params.disable_strategy != DisableStrategy::Never; - add_offending_validator::(params.stash, disable_when_slashed); + add_offending_validator::(params.stash); let mut nominators_slashed = Vec::new(); reward_payout += slash_nominators::(params.clone(), prior_slash_p, &mut nominators_slashed); @@ -319,14 +316,13 @@ fn kick_out_if_recent(params: SlashParams) { >::chill_stash(params.stash); } - let disable_without_slash = params.disable_strategy == DisableStrategy::Always; - add_offending_validator::(params.stash, disable_without_slash); + add_offending_validator::(params.stash); } /// Add the given validator to the offenders list and optionally disable it. /// If after adding the validator `OffendingValidatorsThreshold` is reached /// a new era will be forced. -fn add_offending_validator(stash: &T::AccountId, disable: bool) { +fn add_offending_validator(stash: &T::AccountId) { OffendingValidators::::mutate(|offending| { let validators = T::SessionInterface::validators(); let validator_index = match validators.iter().position(|i| i == stash) { @@ -336,31 +332,20 @@ fn add_offending_validator(stash: &T::AccountId, disable: bool) { let validator_index_u32 = validator_index as u32; - match offending.binary_search_by_key(&validator_index_u32, |(index, _)| *index) { + if let Err(index) = offending.binary_search_by_key(&validator_index_u32, |index| *index) { // this is a new offending validator - Err(index) => { - offending.insert(index, (validator_index_u32, disable)); + offending.insert(index, validator_index_u32); - let offending_threshold = - T::OffendingValidatorsThreshold::get() * validators.len() as u32; + let offending_threshold = + T::OffendingValidatorsThreshold::get() * validators.len() as u32; - if offending.len() >= offending_threshold as usize { - // force a new era, to select a new validator set - >::ensure_new_era() - } + // TODO - don't do this + if offending.len() >= offending_threshold as usize { + // force a new era, to select a new validator set + >::ensure_new_era() + } - if disable { - T::SessionInterface::disable_validator(validator_index_u32); - } - }, - Ok(index) => { - if disable && !offending[index].1 { - // the validator had previously offended without being disabled, - // let's make sure we disable it now - offending[index].1 = true; - T::SessionInterface::disable_validator(validator_index_u32); - } - }, + T::SessionInterface::disable_validator(validator_index_u32); } }); } diff --git a/substrate/frame/staking/src/tests.rs b/substrate/frame/staking/src/tests.rs index ee6f67adf14c..add212e7279b 100644 --- a/substrate/frame/staking/src/tests.rs +++ b/substrate/frame/staking/src/tests.rs @@ -38,7 +38,7 @@ use sp_runtime::{ Perbill, Percent, Perquintill, Rounding, TokenError, }; use sp_staking::{ - offence::{DisableStrategy, OffenceDetails, OnOffenceHandler}, + offence::{OffenceDetails, OnOffenceHandler}, SessionIndex, }; use sp_std::prelude::*; @@ -2429,7 +2429,6 @@ fn slash_in_old_span_does_not_deselect() { }], &[Perbill::from_percent(0)], 1, - DisableStrategy::WhenSlashed, ); // the validator doesn't get chilled again @@ -2446,7 +2445,6 @@ fn slash_in_old_span_does_not_deselect() { // NOTE: A 100% slash here would clean up the account, causing de-registration. &[Perbill::from_percent(95)], 1, - DisableStrategy::WhenSlashed, ); // the validator doesn't get chilled again @@ -2746,7 +2744,6 @@ fn slashing_nominators_by_span_max() { }], &[Perbill::from_percent(10)], 2, - DisableStrategy::WhenSlashed, ); assert_eq!(Balances::free_balance(11), 900); @@ -2773,7 +2770,6 @@ fn slashing_nominators_by_span_max() { }], &[Perbill::from_percent(30)], 3, - DisableStrategy::WhenSlashed, ); // 11 was not further slashed, but 21 and 101 were. @@ -2795,7 +2791,6 @@ fn slashing_nominators_by_span_max() { }], &[Perbill::from_percent(20)], 2, - DisableStrategy::WhenSlashed, ); // 11 was further slashed, but 21 and 101 were not. @@ -2942,7 +2937,6 @@ fn retroactive_deferred_slashes_two_eras_before() { &[OffenceDetails { offender: (11, exposure_11_at_era1), reporters: vec![] }], &[Perbill::from_percent(10)], 1, // should be deferred for two full eras, and applied at the beginning of era 4. - DisableStrategy::Never, ); mock::start_active_era(4); @@ -2980,7 +2974,6 @@ fn retroactive_deferred_slashes_one_before() { &[OffenceDetails { offender: (11, exposure_11_at_era1), reporters: vec![] }], &[Perbill::from_percent(10)], 2, // should be deferred for two full eras, and applied at the beginning of era 5. - DisableStrategy::Never, ); mock::start_active_era(4); @@ -3110,7 +3103,6 @@ fn remove_deferred() { &[OffenceDetails { offender: (11, exposure.clone()), reporters: vec![] }], &[Perbill::from_percent(15)], 1, - DisableStrategy::WhenSlashed, ); // fails if empty @@ -3290,7 +3282,7 @@ fn slash_kicks_validators_not_nominators_and_disables_nominator_for_kicked_valid } #[test] -fn non_slashable_offence_doesnt_disable_validator() { +fn non_slashable_offence_disables_validator() { ExtBuilder::default().build_and_execute(|| { mock::start_active_era(1); assert_eq_uvec!(Session::validators(), vec![11, 21]); @@ -3339,8 +3331,8 @@ fn non_slashable_offence_doesnt_disable_validator() { ] ); - // the offence for validator 10 wasn't slashable so it wasn't disabled - assert!(!is_disabled(11)); + // the offence for validator 10 wasn't slashable but it should have been disabled + assert!(is_disabled(11)); // whereas validator 20 gets disabled assert!(is_disabled(21)); }); @@ -3357,23 +3349,21 @@ fn slashing_independent_of_disabling_validator() { let now = Staking::active_era().unwrap().index; - // offence with no slash associated, BUT disabling + // offence with no slash associated on_offence_in_era( &[OffenceDetails { offender: (11, exposure_11.clone()), reporters: vec![] }], &[Perbill::zero()], now, - DisableStrategy::Always, ); // nomination remains untouched. assert_eq!(Staking::nominators(101).unwrap().targets, vec![11, 21]); - // offence that slashes 25% of the bond, BUT not disabling + // offence that slashes 25% of the bond on_offence_in_era( &[OffenceDetails { offender: (21, exposure_21.clone()), reporters: vec![] }], &[Perbill::from_percent(25)], now, - DisableStrategy::Never, ); // nomination remains untouched. @@ -3402,10 +3392,9 @@ fn slashing_independent_of_disabling_validator() { ] ); - // the offence for validator 10 was explicitly disabled + // both validators should be disabled assert!(is_disabled(11)); - // whereas validator 21 is explicitly not disabled - assert!(!is_disabled(21)); + assert!(is_disabled(21)); }); } @@ -3466,11 +3455,6 @@ fn disabled_validators_are_kept_disabled_for_whole_era() { let exposure_11 = Staking::eras_stakers(Staking::active_era().unwrap().index, &11); let exposure_21 = Staking::eras_stakers(Staking::active_era().unwrap().index, &21); - on_offence_now( - &[OffenceDetails { offender: (11, exposure_11.clone()), reporters: vec![] }], - &[Perbill::zero()], - ); - on_offence_now( &[OffenceDetails { offender: (21, exposure_21.clone()), reporters: vec![] }], &[Perbill::from_percent(25)], @@ -3479,18 +3463,15 @@ fn disabled_validators_are_kept_disabled_for_whole_era() { // nominations are not updated. assert_eq!(Staking::nominators(101).unwrap().targets, vec![11, 21]); - // validator 11 should not be disabled since the offence wasn't slashable - assert!(!is_disabled(11)); // validator 21 gets disabled since it got slashed assert!(is_disabled(21)); advance_session(); // disabled validators should carry-on through all sessions in the era - assert!(!is_disabled(11)); assert!(is_disabled(21)); - // validator 11 should now get disabled + // validator 11 commits an offence on_offence_now( &[OffenceDetails { offender: (11, exposure_11.clone()), reporters: vec![] }], &[Perbill::from_percent(25)], @@ -4623,7 +4604,7 @@ fn offences_weight_calculated_correctly() { let zero_offence_weight = ::DbWeight::get().reads_writes(4, 1); assert_eq!( - Staking::on_offence(&[], &[Perbill::from_percent(50)], 0, DisableStrategy::WhenSlashed), + Staking::on_offence(&[], &[Perbill::from_percent(50)], 0), zero_offence_weight ); @@ -4648,7 +4629,6 @@ fn offences_weight_calculated_correctly() { &offenders, &[Perbill::from_percent(50)], 0, - DisableStrategy::WhenSlashed ), n_offence_unapplied_weight ); @@ -4678,7 +4658,6 @@ fn offences_weight_calculated_correctly() { &one_offender, &[Perbill::from_percent(50)], 0, - DisableStrategy::WhenSlashed{} ), one_offence_unapplied_weight ); diff --git a/substrate/primitives/staking/src/offence.rs b/substrate/primitives/staking/src/offence.rs index 8013166374e0..0875e181ee17 100644 --- a/substrate/primitives/staking/src/offence.rs +++ b/substrate/primitives/staking/src/offence.rs @@ -177,15 +177,15 @@ pub trait OnOffenceHandler { /// /// The `session` parameter is the session index of the offence. /// - /// The `disable_strategy` parameter decides if the offenders need to be disabled immediately. + /// NOTE: All offenders are disabled immediately. /// /// The receiver might decide to not accept this offence. In this case, the call site is /// responsible for queuing the report and re-submitting again. + // TODO: check if the comment above is correct fn on_offence( offenders: &[OffenceDetails], slash_fraction: &[Perbill], session: SessionIndex, - disable_strategy: DisableStrategy, ) -> Res; } @@ -194,7 +194,6 @@ impl OnOffenceHandler _offenders: &[OffenceDetails], _slash_fraction: &[Perbill], _session: SessionIndex, - _disable_strategy: DisableStrategy, ) -> Res { Default::default() } From c80ae21d379abb70fd8799ccae64799df853c601 Mon Sep 17 00:00:00 2001 From: Tsvetomir Dimitrov Date: Thu, 9 Nov 2023 10:23:34 +0200 Subject: [PATCH 03/67] Fix `on_offence` compilation errors --- .../election-provider-multi-phase/test-staking-e2e/src/mock.rs | 1 - substrate/frame/offences/src/mock.rs | 2 +- substrate/frame/root-offences/src/lib.rs | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/mock.rs b/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/mock.rs index 751ffc07aa5d..623f881d13f7 100644 --- a/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/mock.rs +++ b/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/mock.rs @@ -800,7 +800,6 @@ pub(crate) fn on_offence_now( offenders, slash_fraction, Staking::eras_start_session_index(now).unwrap(), - DisableStrategy::WhenSlashed, ); } diff --git a/substrate/frame/offences/src/mock.rs b/substrate/frame/offences/src/mock.rs index 2b80668b0d49..cc501bc1ca3d 100644 --- a/substrate/frame/offences/src/mock.rs +++ b/substrate/frame/offences/src/mock.rs @@ -33,7 +33,7 @@ use sp_runtime::{ BuildStorage, Perbill, }; use sp_staking::{ - offence::{self, DisableStrategy, Kind, OffenceDetails}, + offence::{self, Kind, OffenceDetails}, SessionIndex, }; diff --git a/substrate/frame/root-offences/src/lib.rs b/substrate/frame/root-offences/src/lib.rs index e6bb5bb18819..8815ecbeeecf 100644 --- a/substrate/frame/root-offences/src/lib.rs +++ b/substrate/frame/root-offences/src/lib.rs @@ -125,7 +125,7 @@ pub mod pallet { T::AccountId, IdentificationTuple, Weight, - >>::on_offence(&offenders, &slash_fraction, session_index, DisableStrategy::WhenSlashed); + >>::on_offence(&offenders, &slash_fraction, session_index); } } } From 8f5d2b648cdfe7d4de0e7c5a2edf91beffda70d6 Mon Sep 17 00:00:00 2001 From: Tsvetomir Dimitrov Date: Thu, 9 Nov 2023 12:02:57 +0200 Subject: [PATCH 04/67] Remove `DisableStrategy` --- .../parachains/src/disputes/slashing.rs | 11 +------- .../test-staking-e2e/src/mock.rs | 2 +- substrate/frame/im-online/src/lib.rs | 6 +--- substrate/frame/im-online/src/tests.rs | 3 -- substrate/frame/root-offences/src/lib.rs | 2 +- substrate/primitives/staking/src/offence.rs | 28 ------------------- 6 files changed, 4 insertions(+), 48 deletions(-) diff --git a/polkadot/runtime/parachains/src/disputes/slashing.rs b/polkadot/runtime/parachains/src/disputes/slashing.rs index 9b2b7a48dc8b..898dcd75bfd2 100644 --- a/polkadot/runtime/parachains/src/disputes/slashing.rs +++ b/polkadot/runtime/parachains/src/disputes/slashing.rs @@ -64,7 +64,7 @@ use sp_runtime::{ KeyTypeId, Perbill, }; use sp_session::{GetSessionNumber, GetValidatorCount}; -use sp_staking::offence::{DisableStrategy, Kind, Offence, OffenceError, ReportOffence}; +use sp_staking::offence::{Kind, Offence, OffenceError, ReportOffence}; use sp_std::{ collections::{btree_map::Entry, btree_set::BTreeSet}, prelude::*, @@ -134,15 +134,6 @@ where self.time_slot.clone() } - fn disable_strategy(&self) -> DisableStrategy { - match self.kind { - SlashingOffenceKind::ForInvalid => DisableStrategy::Always, - // in the future we might change it based on number of disputes initiated: - // - SlashingOffenceKind::AgainstValid => DisableStrategy::Never, - } - } - fn slash_fraction(&self, _offenders: u32) -> Perbill { self.slash_fraction } diff --git a/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/mock.rs b/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/mock.rs index 623f881d13f7..ca7fa74f435b 100644 --- a/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/mock.rs +++ b/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/mock.rs @@ -35,7 +35,7 @@ use sp_runtime::{ transaction_validity, BuildStorage, PerU16, Perbill, Percent, }; use sp_staking::{ - offence::{DisableStrategy, OffenceDetails, OnOffenceHandler}, + offence::{OffenceDetails, OnOffenceHandler}, EraIndex, SessionIndex, }; use sp_std::prelude::*; diff --git a/substrate/frame/im-online/src/lib.rs b/substrate/frame/im-online/src/lib.rs index 1de89dd00c81..72d90f2fcf84 100644 --- a/substrate/frame/im-online/src/lib.rs +++ b/substrate/frame/im-online/src/lib.rs @@ -104,7 +104,7 @@ use sp_runtime::{ PerThing, Perbill, Permill, RuntimeDebug, SaturatedConversion, }; use sp_staking::{ - offence::{DisableStrategy, Kind, Offence, ReportOffence}, + offence::{Kind, Offence, ReportOffence}, SessionIndex, }; use sp_std::prelude::*; @@ -851,10 +851,6 @@ impl Offence for UnresponsivenessOffence { self.session_index } - fn disable_strategy(&self) -> DisableStrategy { - DisableStrategy::Never - } - fn slash_fraction(&self, offenders: u32) -> Perbill { // the formula is min((3 * (k - (n / 10 + 1))) / n, 1) * 0.07 // basically, 10% can be offline with no slash, but after that, it linearly climbs up to 7% diff --git a/substrate/frame/im-online/src/tests.rs b/substrate/frame/im-online/src/tests.rs index 79036760c2d4..c2346f7a2c58 100644 --- a/substrate/frame/im-online/src/tests.rs +++ b/substrate/frame/im-online/src/tests.rs @@ -53,9 +53,6 @@ fn test_unresponsiveness_slash_fraction() { dummy_offence.slash_fraction(17), Perbill::from_parts(46200000), // 4.62% ); - - // Offline offences should never lead to being disabled. - assert_eq!(dummy_offence.disable_strategy(), DisableStrategy::Never); } #[test] diff --git a/substrate/frame/root-offences/src/lib.rs b/substrate/frame/root-offences/src/lib.rs index 8815ecbeeecf..39c260976a82 100644 --- a/substrate/frame/root-offences/src/lib.rs +++ b/substrate/frame/root-offences/src/lib.rs @@ -30,7 +30,7 @@ mod tests; use pallet_session::historical::IdentificationTuple; use pallet_staking::{BalanceOf, Exposure, ExposureOf, Pallet as Staking}; use sp_runtime::Perbill; -use sp_staking::offence::{DisableStrategy, OnOffenceHandler}; +use sp_staking::offence::OnOffenceHandler; pub use pallet::*; diff --git a/substrate/primitives/staking/src/offence.rs b/substrate/primitives/staking/src/offence.rs index 0875e181ee17..18d126c56dc2 100644 --- a/substrate/primitives/staking/src/offence.rs +++ b/substrate/primitives/staking/src/offence.rs @@ -37,29 +37,6 @@ pub type Kind = [u8; 16]; /// so that we can slash it accordingly. pub type OffenceCount = u32; -/// In case of an offence, which conditions get an offending validator disabled. -#[derive( - Clone, - Copy, - PartialEq, - Eq, - Hash, - PartialOrd, - Ord, - Encode, - Decode, - sp_runtime::RuntimeDebug, - scale_info::TypeInfo, -)] -pub enum DisableStrategy { - /// Independently of slashing, this offence will not disable the offender. - Never, - /// Only disable the offender if it is also slashed. - WhenSlashed, - /// Independently of slashing, this offence will always disable the offender. - Always, -} - /// A trait implemented by an offence report. /// /// This trait assumes that the offence is legitimate and was validated already. @@ -102,11 +79,6 @@ pub trait Offence { /// number. Note that for GRANDPA the round number is reset each epoch. fn time_slot(&self) -> Self::TimeSlot; - /// In which cases this offence needs to disable offenders until the next era starts. - fn disable_strategy(&self) -> DisableStrategy { - DisableStrategy::WhenSlashed - } - /// A slash fraction of the total exposure that should be slashed for this /// particular offence for the `offenders_count` that happened at a singular `TimeSlot`. /// From bff0240d28ac8e5f91e837d389546883108fe747 Mon Sep 17 00:00:00 2001 From: Tsvetomir Dimitrov Date: Thu, 9 Nov 2023 19:09:14 +0200 Subject: [PATCH 05/67] Fix a compilation error after merge master: `CandidateValidationMessage::ValidateFromExhaustive` now has got named fields --- polkadot/node/core/backing/src/tests/mod.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/polkadot/node/core/backing/src/tests/mod.rs b/polkadot/node/core/backing/src/tests/mod.rs index a1cc8dc73ad5..1703934a1ce0 100644 --- a/polkadot/node/core/backing/src/tests/mod.rs +++ b/polkadot/node/core/backing/src/tests/mod.rs @@ -2216,15 +2216,15 @@ fn validator_ignores_statements_from_disabled_validators() { assert_matches!( virtual_overseer.recv().await, AllMessages::CandidateValidation( - CandidateValidationMessage::ValidateFromExhaustive( - _pvd, - _validation_code, + CandidateValidationMessage::ValidateFromExhaustive{ + validation_data: _pvd, + validation_code: _validation_code, candidate_receipt, - _pov, - _, - timeout, - tx, - ), + pov: _pov, + executor_params: _, + exec_timeout_kind: timeout, + response_sender: tx, + }, ) if _pvd == pvd && _validation_code == validation_code && *_pov == pov && &candidate_receipt.descriptor == candidate.descriptor() && From 95aa673416916d89130af56514f698171a6041e1 Mon Sep 17 00:00:00 2001 From: Tsvetomir Dimitrov Date: Fri, 10 Nov 2023 09:51:42 +0200 Subject: [PATCH 06/67] Update a comment --- substrate/frame/staking/src/pallet/mod.rs | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/substrate/frame/staking/src/pallet/mod.rs b/substrate/frame/staking/src/pallet/mod.rs index 667b3859a023..ba346ce361ee 100644 --- a/substrate/frame/staking/src/pallet/mod.rs +++ b/substrate/frame/staking/src/pallet/mod.rs @@ -636,16 +636,12 @@ pub mod pallet { #[pallet::getter(fn current_planned_session)] pub type CurrentPlannedSession = StorageValue<_, SessionIndex, ValueQuery>; - /// Indices of validators that have offended in the active era and whether they are currently - /// disabled. + /// Indices of validators that have offended in the active era. The offenders are disabled for a + /// whole era. For this reason they are kept here - only staking pallet knows about eras. To + /// avoid breaking consensus no more than a `byzantine_threshold` are disabled at a given era. /// - /// This value should be a superset of disabled validators since not all offences lead to the - /// validator being disabled (if there was no slash). This is needed to track the percentage of - /// validators that have offended in the current era, ensuring a new era is forced if - /// `OffendingValidatorsThreshold` is reached. The vec is always kept sorted so that we can find - /// whether a given validator has previously offended using binary search. It gets cleared when - /// the era ends. - // TODO: Fix the comment above + /// The vec is always kept sorted so that we can find whether a given validator has previously + /// offended using binary search. #[pallet::storage] #[pallet::unbounded] #[pallet::getter(fn offending_validators)] From 873e072cf7b718d256b8ccc2fa3590e5455b9037 Mon Sep 17 00:00:00 2001 From: Tsvetomir Dimitrov Date: Tue, 14 Nov 2023 10:01:40 +0200 Subject: [PATCH 07/67] Update comments --- substrate/frame/staking/src/pallet/mod.rs | 4 ++-- substrate/primitives/staking/src/offence.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/substrate/frame/staking/src/pallet/mod.rs b/substrate/frame/staking/src/pallet/mod.rs index ba346ce361ee..eaa292e5ccfb 100644 --- a/substrate/frame/staking/src/pallet/mod.rs +++ b/substrate/frame/staking/src/pallet/mod.rs @@ -637,8 +637,8 @@ pub mod pallet { pub type CurrentPlannedSession = StorageValue<_, SessionIndex, ValueQuery>; /// Indices of validators that have offended in the active era. The offenders are disabled for a - /// whole era. For this reason they are kept here - only staking pallet knows about eras. To - /// avoid breaking consensus no more than a `byzantine_threshold` are disabled at a given era. + /// whole era. For this reason they are kept here - only staking pallet knows about eras. No + /// more than `byzantine_threshold` validators are disabled at a given era. /// /// The vec is always kept sorted so that we can find whether a given validator has previously /// offended using binary search. diff --git a/substrate/primitives/staking/src/offence.rs b/substrate/primitives/staking/src/offence.rs index 18d126c56dc2..52e20b720ada 100644 --- a/substrate/primitives/staking/src/offence.rs +++ b/substrate/primitives/staking/src/offence.rs @@ -149,11 +149,11 @@ pub trait OnOffenceHandler { /// /// The `session` parameter is the session index of the offence. /// - /// NOTE: All offenders are disabled immediately. + /// NOTE: All offenders are disabled immediately unless there are already `byzantine threshold` + /// offenders. In this case they are slashed but not disabled. /// /// The receiver might decide to not accept this offence. In this case, the call site is /// responsible for queuing the report and re-submitting again. - // TODO: check if the comment above is correct fn on_offence( offenders: &[OffenceDetails], slash_fraction: &[Perbill], From 129a98615088535f4e68f24366e6a41adbba84c4 Mon Sep 17 00:00:00 2001 From: Tsvetomir Dimitrov Date: Mon, 13 Nov 2023 15:57:14 +0200 Subject: [PATCH 08/67] Replace `OffendingValidatorsThreshold` with `disabling_threshold`. Don't disable if disabled validators will go beyond the byzantine threshold --- polkadot/runtime/test-runtime/src/lib.rs | 2 -- polkadot/runtime/westend/src/lib.rs | 2 -- substrate/bin/node/runtime/src/lib.rs | 2 -- substrate/frame/babe/src/mock.rs | 2 -- substrate/frame/beefy/src/mock.rs | 2 -- .../test-staking-e2e/src/mock.rs | 6 +----- substrate/frame/fast-unstake/src/mock.rs | 1 - substrate/frame/grandpa/src/mock.rs | 2 -- .../nomination-pools/test-staking/src/mock.rs | 1 - substrate/frame/root-offences/src/mock.rs | 2 -- substrate/frame/staking/src/lib.rs | 2 +- substrate/frame/staking/src/mock.rs | 2 -- substrate/frame/staking/src/pallet/impls.rs | 8 ------- substrate/frame/staking/src/pallet/mod.rs | 10 +++++---- substrate/frame/staking/src/slashing.rs | 21 ++++++++----------- 15 files changed, 17 insertions(+), 48 deletions(-) diff --git a/polkadot/runtime/test-runtime/src/lib.rs b/polkadot/runtime/test-runtime/src/lib.rs index 596e65eca068..0d2160b6c0ad 100644 --- a/polkadot/runtime/test-runtime/src/lib.rs +++ b/polkadot/runtime/test-runtime/src/lib.rs @@ -320,7 +320,6 @@ parameter_types! { pub const RewardCurve: &'static PiecewiseLinear<'static> = &REWARD_CURVE; pub const MaxExposurePageSize: u32 = 64; pub const MaxNominators: u32 = 256; - pub storage OffendingValidatorsThreshold: Perbill = Perbill::from_percent(17); pub const MaxAuthorities: u32 = 100_000; pub const OnChainMaxWinners: u32 = u32::MAX; // Unbounded number of election targets and voters. @@ -356,7 +355,6 @@ impl pallet_staking::Config for Runtime { type SessionInterface = Self; type EraPayout = pallet_staking::ConvertCurve; type MaxExposurePageSize = MaxExposurePageSize; - type OffendingValidatorsThreshold = OffendingValidatorsThreshold; type NextNewSession = Session; type ElectionProvider = onchain::OnChainExecution; type GenesisElectionProvider = onchain::OnChainExecution; diff --git a/polkadot/runtime/westend/src/lib.rs b/polkadot/runtime/westend/src/lib.rs index d80640c016f7..de9c6c2545d6 100644 --- a/polkadot/runtime/westend/src/lib.rs +++ b/polkadot/runtime/westend/src/lib.rs @@ -650,7 +650,6 @@ parameter_types! { // this is an unbounded number. We just set it to a reasonably high value, 1 full page // of nominators. pub const MaxNominators: u32 = 64; - pub const OffendingValidatorsThreshold: Perbill = Perbill::from_percent(17); pub const MaxNominations: u32 = ::LIMIT as u32; } @@ -670,7 +669,6 @@ impl pallet_staking::Config for Runtime { type SessionInterface = Self; type EraPayout = pallet_staking::ConvertCurve; type MaxExposurePageSize = MaxExposurePageSize; - type OffendingValidatorsThreshold = OffendingValidatorsThreshold; type NextNewSession = Session; type ElectionProvider = ElectionProviderMultiPhase; type GenesisElectionProvider = onchain::OnChainExecution; diff --git a/substrate/bin/node/runtime/src/lib.rs b/substrate/bin/node/runtime/src/lib.rs index e9adc48ff9cc..e923dd8747b6 100644 --- a/substrate/bin/node/runtime/src/lib.rs +++ b/substrate/bin/node/runtime/src/lib.rs @@ -633,7 +633,6 @@ parameter_types! { pub const SlashDeferDuration: sp_staking::EraIndex = 24 * 7; // 1/4 the bonding duration. pub const RewardCurve: &'static PiecewiseLinear<'static> = &REWARD_CURVE; pub const MaxNominators: u32 = 64; - pub const OffendingValidatorsThreshold: Perbill = Perbill::from_percent(17); pub OffchainRepeat: BlockNumber = 5; pub HistoryDepth: u32 = 84; } @@ -668,7 +667,6 @@ impl pallet_staking::Config for Runtime { type EraPayout = pallet_staking::ConvertCurve; type NextNewSession = Session; type MaxExposurePageSize = ConstU32<256>; - type OffendingValidatorsThreshold = OffendingValidatorsThreshold; type ElectionProvider = ElectionProviderMultiPhase; type GenesisElectionProvider = onchain::OnChainExecution; type VoterList = VoterList; diff --git a/substrate/frame/babe/src/mock.rs b/substrate/frame/babe/src/mock.rs index 0003c6f9f11a..ae7be6d1ead3 100644 --- a/substrate/frame/babe/src/mock.rs +++ b/substrate/frame/babe/src/mock.rs @@ -145,7 +145,6 @@ parameter_types! { pub const BondingDuration: EraIndex = 3; pub const SlashDeferDuration: EraIndex = 0; pub const RewardCurve: &'static PiecewiseLinear<'static> = &REWARD_CURVE; - pub const OffendingValidatorsThreshold: Perbill = Perbill::from_percent(16); pub static ElectionsBounds: ElectionBounds = ElectionBoundsBuilder::default().build(); } @@ -175,7 +174,6 @@ impl pallet_staking::Config for Test { type UnixTime = pallet_timestamp::Pallet; type EraPayout = pallet_staking::ConvertCurve; type MaxExposurePageSize = ConstU32<64>; - type OffendingValidatorsThreshold = OffendingValidatorsThreshold; type NextNewSession = Session; type ElectionProvider = onchain::OnChainExecution; type GenesisElectionProvider = Self::ElectionProvider; diff --git a/substrate/frame/beefy/src/mock.rs b/substrate/frame/beefy/src/mock.rs index 53d523cf724d..fa5c97b271a0 100644 --- a/substrate/frame/beefy/src/mock.rs +++ b/substrate/frame/beefy/src/mock.rs @@ -163,7 +163,6 @@ parameter_types! { pub const SessionsPerEra: SessionIndex = 3; pub const BondingDuration: EraIndex = 3; pub const RewardCurve: &'static PiecewiseLinear<'static> = &REWARD_CURVE; - pub const OffendingValidatorsThreshold: Perbill = Perbill::from_percent(17); pub static ElectionsBoundsOnChain: ElectionBounds = ElectionBoundsBuilder::default().build(); } @@ -193,7 +192,6 @@ impl pallet_staking::Config for Test { type UnixTime = pallet_timestamp::Pallet; type EraPayout = pallet_staking::ConvertCurve; type MaxExposurePageSize = ConstU32<64>; - type OffendingValidatorsThreshold = OffendingValidatorsThreshold; type NextNewSession = Session; type ElectionProvider = onchain::OnChainExecution; type GenesisElectionProvider = Self::ElectionProvider; diff --git a/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/mock.rs b/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/mock.rs index ca7fa74f435b..68a8449a887e 100644 --- a/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/mock.rs +++ b/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/mock.rs @@ -237,7 +237,6 @@ parameter_types! { pub const SessionsPerEra: sp_staking::SessionIndex = 2; pub const BondingDuration: sp_staking::EraIndex = 28; pub const SlashDeferDuration: sp_staking::EraIndex = 7; // 1/4 the bonding duration. - pub const OffendingValidatorsThreshold: Perbill = Perbill::from_percent(40); pub HistoryDepth: u32 = 84; } @@ -269,7 +268,6 @@ impl pallet_staking::Config for Runtime { type EraPayout = (); type NextNewSession = Session; type MaxExposurePageSize = ConstU32<256>; - type OffendingValidatorsThreshold = OffendingValidatorsThreshold; type ElectionProvider = ElectionProviderMultiPhase; type GenesisElectionProvider = onchain::OnChainExecution; type VoterList = BagsList; @@ -817,9 +815,7 @@ pub(crate) fn add_slash(who: &AccountId) { // Slashes enough validators to cross the `Staking::OffendingValidatorsThreshold`. pub(crate) fn slash_through_offending_threshold() { let validators = Session::validators(); - let mut remaining_slashes = - ::OffendingValidatorsThreshold::get() * - validators.len() as u32; + let mut remaining_slashes = pallet_staking::disabling_threshold(validators.len()); for v in validators.into_iter() { if remaining_slashes != 0 { diff --git a/substrate/frame/fast-unstake/src/mock.rs b/substrate/frame/fast-unstake/src/mock.rs index df133bdfd47f..48baa574e73f 100644 --- a/substrate/frame/fast-unstake/src/mock.rs +++ b/substrate/frame/fast-unstake/src/mock.rs @@ -135,7 +135,6 @@ impl pallet_staking::Config for Runtime { type NextNewSession = (); type HistoryDepth = ConstU32<84>; type MaxExposurePageSize = ConstU32<64>; - type OffendingValidatorsThreshold = (); type ElectionProvider = MockElection; type GenesisElectionProvider = Self::ElectionProvider; type VoterList = pallet_staking::UseNominatorsAndValidatorsMap; diff --git a/substrate/frame/grandpa/src/mock.rs b/substrate/frame/grandpa/src/mock.rs index 9afcec1c797a..d88e98c7eb54 100644 --- a/substrate/frame/grandpa/src/mock.rs +++ b/substrate/frame/grandpa/src/mock.rs @@ -167,7 +167,6 @@ parameter_types! { pub const SessionsPerEra: SessionIndex = 3; pub const BondingDuration: EraIndex = 3; pub const RewardCurve: &'static PiecewiseLinear<'static> = &REWARD_CURVE; - pub const OffendingValidatorsThreshold: Perbill = Perbill::from_percent(17); pub static ElectionsBoundsOnChain: ElectionBounds = ElectionBoundsBuilder::default().build(); } @@ -197,7 +196,6 @@ impl pallet_staking::Config for Test { type UnixTime = pallet_timestamp::Pallet; type EraPayout = pallet_staking::ConvertCurve; type MaxExposurePageSize = ConstU32<64>; - type OffendingValidatorsThreshold = OffendingValidatorsThreshold; type NextNewSession = Session; type ElectionProvider = onchain::OnChainExecution; type GenesisElectionProvider = Self::ElectionProvider; diff --git a/substrate/frame/nomination-pools/test-staking/src/mock.rs b/substrate/frame/nomination-pools/test-staking/src/mock.rs index 0db24e9c2441..3cf9bff56cec 100644 --- a/substrate/frame/nomination-pools/test-staking/src/mock.rs +++ b/substrate/frame/nomination-pools/test-staking/src/mock.rs @@ -125,7 +125,6 @@ impl pallet_staking::Config for Runtime { type EraPayout = pallet_staking::ConvertCurve; type NextNewSession = (); type MaxExposurePageSize = ConstU32<64>; - type OffendingValidatorsThreshold = (); type ElectionProvider = frame_election_provider_support::NoElection<(AccountId, BlockNumber, Staking, ())>; type GenesisElectionProvider = Self::ElectionProvider; diff --git a/substrate/frame/root-offences/src/mock.rs b/substrate/frame/root-offences/src/mock.rs index 82da429e00a5..77111e2c6e9e 100644 --- a/substrate/frame/root-offences/src/mock.rs +++ b/substrate/frame/root-offences/src/mock.rs @@ -160,7 +160,6 @@ parameter_types! { pub static SlashDeferDuration: EraIndex = 0; pub const BondingDuration: EraIndex = 3; pub static LedgerSlashPerEra: (BalanceOf, BTreeMap>) = (Zero::zero(), BTreeMap::new()); - pub const OffendingValidatorsThreshold: Perbill = Perbill::from_percent(75); } impl pallet_staking::Config for Test { @@ -180,7 +179,6 @@ impl pallet_staking::Config for Test { type EraPayout = pallet_staking::ConvertCurve; type NextNewSession = Session; type MaxExposurePageSize = ConstU32<64>; - type OffendingValidatorsThreshold = OffendingValidatorsThreshold; type ElectionProvider = onchain::OnChainExecution; type GenesisElectionProvider = Self::ElectionProvider; type TargetList = pallet_staking::UseValidatorsMap; diff --git a/substrate/frame/staking/src/lib.rs b/substrate/frame/staking/src/lib.rs index 9e4697e845b6..aa25fe1af0de 100644 --- a/substrate/frame/staking/src/lib.rs +++ b/substrate/frame/staking/src/lib.rs @@ -329,7 +329,7 @@ pub use sp_staking::{Exposure, IndividualExposure, StakerStatus}; use sp_std::{collections::btree_map::BTreeMap, prelude::*}; pub use weights::WeightInfo; -pub use pallet::{pallet::*, UseNominatorsAndValidatorsMap, UseValidatorsMap}; +pub use pallet::{disabling_threshold, pallet::*, UseNominatorsAndValidatorsMap, UseValidatorsMap}; pub(crate) const STAKING_ID: LockIdentifier = *b"staking "; pub(crate) const LOG_TARGET: &str = "runtime::staking"; diff --git a/substrate/frame/staking/src/mock.rs b/substrate/frame/staking/src/mock.rs index c23b1caf3bad..944bcb97df18 100644 --- a/substrate/frame/staking/src/mock.rs +++ b/substrate/frame/staking/src/mock.rs @@ -212,7 +212,6 @@ pallet_staking_reward_curve::build! { parameter_types! { pub const BondingDuration: EraIndex = 3; pub const RewardCurve: &'static PiecewiseLinear<'static> = &I_NPOS; - pub const OffendingValidatorsThreshold: Perbill = Perbill::from_percent(75); } parameter_types! { @@ -306,7 +305,6 @@ impl crate::pallet::pallet::Config for Test { type EraPayout = ConvertCurve; type NextNewSession = Session; type MaxExposurePageSize = MaxExposurePageSize; - type OffendingValidatorsThreshold = OffendingValidatorsThreshold; type ElectionProvider = onchain::OnChainExecution; type GenesisElectionProvider = Self::ElectionProvider; // NOTE: consider a macro and use `UseNominatorsAndValidatorsMap` as well. diff --git a/substrate/frame/staking/src/pallet/impls.rs b/substrate/frame/staking/src/pallet/impls.rs index d0e509b0da0c..d15e0fb6bb3f 100644 --- a/substrate/frame/staking/src/pallet/impls.rs +++ b/substrate/frame/staking/src/pallet/impls.rs @@ -778,14 +778,6 @@ impl Pallet { Self::deposit_event(Event::::ForceEra { mode }); } - /// Ensures that at the end of the current session there will be a new era. - pub(crate) fn ensure_new_era() { - match ForceEra::::get() { - Forcing::ForceAlways | Forcing::ForceNew => (), - _ => Self::set_force_era(Forcing::ForceNew), - } - } - #[cfg(feature = "runtime-benchmarks")] pub fn add_era_stakers( current_era: EraIndex, diff --git a/substrate/frame/staking/src/pallet/mod.rs b/substrate/frame/staking/src/pallet/mod.rs index eaa292e5ccfb..b45fad096836 100644 --- a/substrate/frame/staking/src/pallet/mod.rs +++ b/substrate/frame/staking/src/pallet/mod.rs @@ -217,10 +217,6 @@ pub mod pallet { #[pallet::constant] type MaxExposurePageSize: Get; - /// The fraction of the validator set that is safe to be offending. - /// After the threshold is reached a new era will be forced. - type OffendingValidatorsThreshold: Get; - /// Something that provides a best-effort sorted list of voters aka electing nominators, /// used for NPoS election. /// @@ -1876,3 +1872,9 @@ pub mod pallet { fn is_sorted_and_unique(list: &[u32]) -> bool { list.windows(2).all(|w| w[0] < w[1]) } + +/// Disabling threshold calculated from the total number of validators in the active set. When +/// reached no more validators will be disabled. +pub fn disabling_threshold(validators_len: usize) -> usize { + validators_len.saturating_sub(1) / 3 +} diff --git a/substrate/frame/staking/src/slashing.rs b/substrate/frame/staking/src/slashing.rs index 35b35217e49e..1504a838b86e 100644 --- a/substrate/frame/staking/src/slashing.rs +++ b/substrate/frame/staking/src/slashing.rs @@ -50,14 +50,14 @@ //! Based on research at use crate::{ - BalanceOf, Config, Error, Exposure, NegativeImbalanceOf, NominatorSlashInEra, - OffendingValidators, Pallet, Perbill, SessionInterface, SpanSlash, UnappliedSlash, - ValidatorSlashInEra, + pallet::disabling_threshold, BalanceOf, Config, Error, Exposure, NegativeImbalanceOf, + NominatorSlashInEra, OffendingValidators, Pallet, Perbill, SessionInterface, SpanSlash, + UnappliedSlash, ValidatorSlashInEra, }; use codec::{Decode, Encode, MaxEncodedLen}; use frame_support::{ ensure, - traits::{Currency, Defensive, Get, Imbalance, OnUnbalanced}, + traits::{Currency, Defensive, Imbalance, OnUnbalanced}, }; use scale_info::TypeInfo; use sp_runtime::{ @@ -334,17 +334,14 @@ fn add_offending_validator(stash: &T::AccountId) { if let Err(index) = offending.binary_search_by_key(&validator_index_u32, |index| *index) { // this is a new offending validator - offending.insert(index, validator_index_u32); - - let offending_threshold = - T::OffendingValidatorsThreshold::get() * validators.len() as u32; - // TODO - don't do this - if offending.len() >= offending_threshold as usize { - // force a new era, to select a new validator set - >::ensure_new_era() + // we don't want to disable too many validators otherwise we will break consensus + if offending.len() + 1 > disabling_threshold(validators.len()) as usize { + return } + // Add the validator to `OffendingValidators` and disable it + offending.insert(index, validator_index_u32); T::SessionInterface::disable_validator(validator_index_u32); } }); From ce1539c3ed9b84f641b77e22f61dce8381641091 Mon Sep 17 00:00:00 2001 From: Tsvetomir Dimitrov Date: Mon, 13 Nov 2023 15:58:26 +0200 Subject: [PATCH 09/67] Make tests pass --- substrate/frame/staking/src/mock.rs | 8 +- substrate/frame/staking/src/tests.rs | 434 ++++++++++++++------------- 2 files changed, 232 insertions(+), 210 deletions(-) diff --git a/substrate/frame/staking/src/mock.rs b/substrate/frame/staking/src/mock.rs index 944bcb97df18..eece56af7025 100644 --- a/substrate/frame/staking/src/mock.rs +++ b/substrate/frame/staking/src/mock.rs @@ -471,6 +471,8 @@ impl ExtBuilder { (31, self.balance_factor * 2000), (41, self.balance_factor * 2000), (51, self.balance_factor * 2000), + (201, self.balance_factor * 2000), + (202, self.balance_factor * 2000), // optional nominator (100, self.balance_factor * 2000), (101, self.balance_factor * 2000), @@ -498,8 +500,10 @@ impl ExtBuilder { (31, 31, self.balance_factor * 500, StakerStatus::::Validator), // an idle validator (41, 41, self.balance_factor * 1000, StakerStatus::::Idle), - ]; - // optionally add a nominator + (51, 51, self.balance_factor * 1000, StakerStatus::::Idle), + (201, 201, self.balance_factor * 1000, StakerStatus::::Idle), + (202, 202, self.balance_factor * 1000, StakerStatus::::Idle), + ]; // optionally add a nominator if self.nominate { stakers.push(( 101, diff --git a/substrate/frame/staking/src/tests.rs b/substrate/frame/staking/src/tests.rs index add212e7279b..41197bd4ef23 100644 --- a/substrate/frame/staking/src/tests.rs +++ b/substrate/frame/staking/src/tests.rs @@ -711,56 +711,65 @@ fn nominating_and_rewards_should_work() { #[test] fn nominators_also_get_slashed_pro_rata() { - ExtBuilder::default().build_and_execute(|| { - mock::start_active_era(1); - let slash_percent = Perbill::from_percent(5); - let initial_exposure = Staking::eras_stakers(active_era(), &11); - // 101 is a nominator for 11 - assert_eq!(initial_exposure.others.first().unwrap().who, 101); - - // staked values; - let nominator_stake = Staking::ledger(101.into()).unwrap().active; - let nominator_balance = balances(&101).0; - let validator_stake = Staking::ledger(11.into()).unwrap().active; - let validator_balance = balances(&11).0; - let exposed_stake = initial_exposure.total; - let exposed_validator = initial_exposure.own; - let exposed_nominator = initial_exposure.others.first().unwrap().value; - - // 11 goes offline - on_offence_now( - &[OffenceDetails { offender: (11, initial_exposure.clone()), reporters: vec![] }], - &[slash_percent], - ); + ExtBuilder::default() + .validator_count(4) + .set_status(41, StakerStatus::Validator) + .build_and_execute(|| { + mock::start_active_era(1); + let slash_percent = Perbill::from_percent(5); + let initial_exposure = Staking::eras_stakers(active_era(), &11); + // 101 is a nominator for 11 + assert_eq!(initial_exposure.others.first().unwrap().who, 101); + + // staked values; + let nominator_stake = Staking::ledger(101.into()).unwrap().active; + let nominator_balance = balances(&101).0; + let validator_stake = Staking::ledger(11.into()).unwrap().active; + let validator_balance = balances(&11).0; + let exposed_stake = initial_exposure.total; + let exposed_validator = initial_exposure.own; + let exposed_nominator = initial_exposure.others.first().unwrap().value; + + // 11 goes offline + on_offence_now( + &[OffenceDetails { offender: (11, initial_exposure.clone()), reporters: vec![] }], + &[slash_percent], + ); - // both stakes must have been decreased. - assert!(Staking::ledger(101.into()).unwrap().active < nominator_stake); - assert!(Staking::ledger(11.into()).unwrap().active < validator_stake); + // both stakes must have been decreased. + assert!(Staking::ledger(101.into()).unwrap().active < nominator_stake); + assert!(Staking::ledger(11.into()).unwrap().active < validator_stake); - let slash_amount = slash_percent * exposed_stake; - let validator_share = - Perbill::from_rational(exposed_validator, exposed_stake) * slash_amount; - let nominator_share = - Perbill::from_rational(exposed_nominator, exposed_stake) * slash_amount; + let slash_amount = slash_percent * exposed_stake; + let validator_share = + Perbill::from_rational(exposed_validator, exposed_stake) * slash_amount; + let nominator_share = + Perbill::from_rational(exposed_nominator, exposed_stake) * slash_amount; - // both slash amounts need to be positive for the test to make sense. - assert!(validator_share > 0); - assert!(nominator_share > 0); + // both slash amounts need to be positive for the test to make sense. + assert!(validator_share > 0); + assert!(nominator_share > 0); - // both stakes must have been decreased pro-rata. - assert_eq!(Staking::ledger(101.into()).unwrap().active, nominator_stake - nominator_share); - assert_eq!(Staking::ledger(11.into()).unwrap().active, validator_stake - validator_share); - assert_eq!( - balances(&101).0, // free balance - nominator_balance - nominator_share, - ); - assert_eq!( - balances(&11).0, // free balance - validator_balance - validator_share, - ); - // Because slashing happened. - assert!(is_disabled(11)); - }); + // both stakes must have been decreased pro-rata. + assert_eq!( + Staking::ledger(101.into()).unwrap().active, + nominator_stake - nominator_share + ); + assert_eq!( + Staking::ledger(11.into()).unwrap().active, + validator_stake - validator_share + ); + assert_eq!( + balances(&101).0, // free balance + nominator_balance - nominator_share, + ); + assert_eq!( + balances(&11).0, // free balance + validator_balance - validator_share, + ); + // Because slashing happened. + assert!(is_disabled(11)); + }); } #[test] @@ -2314,7 +2323,7 @@ fn era_is_always_same_length() { } #[test] -fn offence_forces_new_era() { +fn offence_doesnt_force_new_era() { ExtBuilder::default().build_and_execute(|| { on_offence_now( &[OffenceDetails { @@ -2324,7 +2333,7 @@ fn offence_forces_new_era() { &[Perbill::from_percent(5)], ); - assert_eq!(Staking::force_era(), Forcing::ForceNew); + assert_eq!(Staking::force_era(), Forcing::NotForcing); }); } @@ -2360,7 +2369,7 @@ fn offence_deselects_validator_even_when_slash_is_zero() { &[Perbill::from_percent(0)], ); - assert_eq!(Staking::force_era(), Forcing::ForceNew); + assert_eq!(Staking::force_era(), Forcing::NotForcing); assert!(!>::contains_key(11)); mock::start_active_era(1); @@ -2393,68 +2402,71 @@ fn slashing_performed_according_exposure() { #[test] fn slash_in_old_span_does_not_deselect() { - ExtBuilder::default().build_and_execute(|| { - mock::start_active_era(1); + ExtBuilder::default() + .validator_count(4) + .set_status(41, StakerStatus::Validator) + .build_and_execute(|| { + mock::start_active_era(1); - assert!(>::contains_key(11)); - assert!(Session::validators().contains(&11)); + assert!(>::contains_key(11)); + assert!(Session::validators().contains(&11)); - on_offence_now( - &[OffenceDetails { - offender: (11, Staking::eras_stakers(active_era(), &11)), - reporters: vec![], - }], - &[Perbill::from_percent(0)], - ); + on_offence_now( + &[OffenceDetails { + offender: (11, Staking::eras_stakers(active_era(), &11)), + reporters: vec![], + }], + &[Perbill::from_percent(0)], + ); - assert_eq!(Staking::force_era(), Forcing::ForceNew); - assert!(!>::contains_key(11)); + assert_eq!(Staking::force_era(), Forcing::NotForcing); + assert!(!>::contains_key(11)); - mock::start_active_era(2); + mock::start_active_era(2); - Staking::validate(RuntimeOrigin::signed(11), Default::default()).unwrap(); - assert_eq!(Staking::force_era(), Forcing::NotForcing); - assert!(>::contains_key(11)); - assert!(!Session::validators().contains(&11)); + Staking::validate(RuntimeOrigin::signed(11), Default::default()).unwrap(); + assert_eq!(Staking::force_era(), Forcing::NotForcing); + assert!(>::contains_key(11)); + assert!(!Session::validators().contains(&11)); - mock::start_active_era(3); + mock::start_active_era(3); - // this staker is in a new slashing span now, having re-registered after - // their prior slash. + // this staker is in a new slashing span now, having re-registered after + // their prior slash. - on_offence_in_era( - &[OffenceDetails { - offender: (11, Staking::eras_stakers(active_era(), &11)), - reporters: vec![], - }], - &[Perbill::from_percent(0)], - 1, - ); + on_offence_in_era( + &[OffenceDetails { + offender: (11, Staking::eras_stakers(active_era(), &11)), + reporters: vec![], + }], + &[Perbill::from_percent(0)], + 1, + ); - // the validator doesn't get chilled again - assert!(Validators::::iter().any(|(stash, _)| stash == 11)); + // the validator doesn't get chilled again + assert!(Validators::::iter().any(|(stash, _)| stash == 11)); - // but we are still forcing a new era - assert_eq!(Staking::force_era(), Forcing::ForceNew); + // but we are still forcing a new era + assert_eq!(Staking::force_era(), Forcing::NotForcing); - on_offence_in_era( - &[OffenceDetails { - offender: (11, Staking::eras_stakers(active_era(), &11)), - reporters: vec![], - }], - // NOTE: A 100% slash here would clean up the account, causing de-registration. - &[Perbill::from_percent(95)], - 1, - ); + on_offence_in_era( + &[OffenceDetails { + offender: (11, Staking::eras_stakers(active_era(), &11)), + reporters: vec![], + }], + // NOTE: A 100% slash here would clean up the account, causing de-registration. + &[Perbill::from_percent(95)], + 1, + ); - // the validator doesn't get chilled again - assert!(Validators::::iter().any(|(stash, _)| stash == 11)); + // the validator doesn't get chilled again + assert!(Validators::::iter().any(|(stash, _)| stash == 11)); - // but it's disabled - assert!(is_disabled(11)); - // and we are still forcing a new era - assert_eq!(Staking::force_era(), Forcing::ForceNew); - }); + // but it's disabled + assert!(is_disabled(11)); + // and we are still forcing a new era + assert_eq!(Staking::force_era(), Forcing::NotForcing); + }); } #[test] @@ -2582,7 +2594,7 @@ fn dont_slash_if_fraction_is_zero() { // The validator hasn't been slashed. The new era is not forced. assert_eq!(Balances::free_balance(11), 1000); - assert_eq!(Staking::force_era(), Forcing::ForceNew); + assert_eq!(Staking::force_era(), Forcing::NotForcing); }); } @@ -2603,7 +2615,7 @@ fn only_slash_for_max_in_era() { // The validator has been slashed and has been force-chilled. assert_eq!(Balances::free_balance(11), 500); - assert_eq!(Staking::force_era(), Forcing::ForceNew); + assert_eq!(Staking::force_era(), Forcing::NotForcing); on_offence_now( &[OffenceDetails { @@ -2908,10 +2920,8 @@ fn deferred_slashes_are_deferred() { staking_events_since_last_call().as_slice(), &[ Event::Chilled { stash: 11 }, - Event::ForceEra { mode: Forcing::ForceNew }, Event::SlashReported { validator: 11, slash_era: 1, .. }, Event::StakersElected, - Event::ForceEra { mode: Forcing::NotForcing }, .., Event::Slashed { staker: 11, amount: 100 }, Event::Slashed { staker: 101, amount: 12 } @@ -2945,7 +2955,6 @@ fn retroactive_deferred_slashes_two_eras_before() { staking_events_since_last_call().as_slice(), &[ Event::Chilled { stash: 11 }, - Event::ForceEra { mode: Forcing::ForceNew }, Event::SlashReported { validator: 11, slash_era: 1, .. }, .., Event::Slashed { staker: 11, amount: 100 }, @@ -3246,7 +3255,6 @@ fn slash_kicks_validators_not_nominators_and_disables_nominator_for_kicked_valid Event::StakersElected, Event::EraPaid { era_index: 0, validator_payout: 11075, remainder: 33225 }, Event::Chilled { stash: 11 }, - Event::ForceEra { mode: Forcing::ForceNew }, Event::SlashReported { validator: 11, fraction: Perbill::from_percent(10), @@ -3283,123 +3291,133 @@ fn slash_kicks_validators_not_nominators_and_disables_nominator_for_kicked_valid #[test] fn non_slashable_offence_disables_validator() { - ExtBuilder::default().build_and_execute(|| { - mock::start_active_era(1); - assert_eq_uvec!(Session::validators(), vec![11, 21]); + ExtBuilder::default() + .validator_count(7) + .set_status(41, StakerStatus::Validator) + .set_status(51, StakerStatus::Validator) + .set_status(201, StakerStatus::Validator) + .set_status(202, StakerStatus::Validator) + .build_and_execute(|| { + mock::start_active_era(1); + assert_eq_uvec!(Session::validators(), vec![11, 21, 31, 41, 51, 201, 202]); - let exposure_11 = Staking::eras_stakers(Staking::active_era().unwrap().index, &11); - let exposure_21 = Staking::eras_stakers(Staking::active_era().unwrap().index, &21); + let exposure_11 = Staking::eras_stakers(Staking::active_era().unwrap().index, &11); + let exposure_21 = Staking::eras_stakers(Staking::active_era().unwrap().index, &21); - // offence with no slash associated - on_offence_now( - &[OffenceDetails { offender: (11, exposure_11.clone()), reporters: vec![] }], - &[Perbill::zero()], - ); + // offence with no slash associated + on_offence_now( + &[OffenceDetails { offender: (11, exposure_11.clone()), reporters: vec![] }], + &[Perbill::zero()], + ); - // it does NOT affect the nominator. - assert_eq!(Staking::nominators(101).unwrap().targets, vec![11, 21]); + // it does NOT affect the nominator. + assert_eq!(Staking::nominators(101).unwrap().targets, vec![11, 21]); - // offence that slashes 25% of the bond - on_offence_now( - &[OffenceDetails { offender: (21, exposure_21.clone()), reporters: vec![] }], - &[Perbill::from_percent(25)], - ); + // offence that slashes 25% of the bond + on_offence_now( + &[OffenceDetails { offender: (21, exposure_21.clone()), reporters: vec![] }], + &[Perbill::from_percent(25)], + ); - // it DOES NOT affect the nominator. - assert_eq!(Staking::nominators(101).unwrap().targets, vec![11, 21]); + // it DOES NOT affect the nominator. + assert_eq!(Staking::nominators(101).unwrap().targets, vec![11, 21]); - assert_eq!( - staking_events_since_last_call(), - vec![ - Event::StakersElected, - Event::EraPaid { era_index: 0, validator_payout: 11075, remainder: 33225 }, - Event::Chilled { stash: 11 }, - Event::ForceEra { mode: Forcing::ForceNew }, - Event::SlashReported { - validator: 11, - fraction: Perbill::from_percent(0), - slash_era: 1 - }, - Event::Chilled { stash: 21 }, - Event::SlashReported { - validator: 21, - fraction: Perbill::from_percent(25), - slash_era: 1 - }, - Event::Slashed { staker: 21, amount: 250 }, - Event::Slashed { staker: 101, amount: 94 } - ] - ); + assert_eq!( + staking_events_since_last_call(), + vec![ + Event::StakersElected, + Event::EraPaid { era_index: 0, validator_payout: 11075, remainder: 33225 }, + Event::Chilled { stash: 11 }, + Event::SlashReported { + validator: 11, + fraction: Perbill::from_percent(0), + slash_era: 1 + }, + Event::Chilled { stash: 21 }, + Event::SlashReported { + validator: 21, + fraction: Perbill::from_percent(25), + slash_era: 1 + }, + Event::Slashed { staker: 21, amount: 250 }, + Event::Slashed { staker: 101, amount: 94 } + ] + ); - // the offence for validator 10 wasn't slashable but it should have been disabled - assert!(is_disabled(11)); - // whereas validator 20 gets disabled - assert!(is_disabled(21)); - }); + // the offence for validator 11 wasn't slashable but it is disabled + assert!(is_disabled(11)); + // validator 21 gets disabled too + assert!(is_disabled(21)); + }); } #[test] fn slashing_independent_of_disabling_validator() { - ExtBuilder::default().build_and_execute(|| { - mock::start_active_era(1); - assert_eq_uvec!(Session::validators(), vec![11, 21]); + ExtBuilder::default() + .validator_count(7) + .set_status(41, StakerStatus::Validator) + .set_status(51, StakerStatus::Validator) + .set_status(201, StakerStatus::Validator) + .set_status(202, StakerStatus::Validator) + .build_and_execute(|| { + mock::start_active_era(1); + assert_eq_uvec!(Session::validators(), vec![11, 21, 31, 41, 51, 201, 202]); - let exposure_11 = Staking::eras_stakers(Staking::active_era().unwrap().index, &11); - let exposure_21 = Staking::eras_stakers(Staking::active_era().unwrap().index, &21); + let exposure_11 = Staking::eras_stakers(Staking::active_era().unwrap().index, &11); + let exposure_21 = Staking::eras_stakers(Staking::active_era().unwrap().index, &21); - let now = Staking::active_era().unwrap().index; + let now = Staking::active_era().unwrap().index; - // offence with no slash associated - on_offence_in_era( - &[OffenceDetails { offender: (11, exposure_11.clone()), reporters: vec![] }], - &[Perbill::zero()], - now, - ); + // offence with no slash associated + on_offence_in_era( + &[OffenceDetails { offender: (11, exposure_11.clone()), reporters: vec![] }], + &[Perbill::zero()], + now, + ); - // nomination remains untouched. - assert_eq!(Staking::nominators(101).unwrap().targets, vec![11, 21]); + // nomination remains untouched. + assert_eq!(Staking::nominators(101).unwrap().targets, vec![11, 21]); - // offence that slashes 25% of the bond - on_offence_in_era( - &[OffenceDetails { offender: (21, exposure_21.clone()), reporters: vec![] }], - &[Perbill::from_percent(25)], - now, - ); + // offence that slashes 25% of the bond + on_offence_in_era( + &[OffenceDetails { offender: (21, exposure_21.clone()), reporters: vec![] }], + &[Perbill::from_percent(25)], + now, + ); - // nomination remains untouched. - assert_eq!(Staking::nominators(101).unwrap().targets, vec![11, 21]); + // nomination remains untouched. + assert_eq!(Staking::nominators(101).unwrap().targets, vec![11, 21]); - assert_eq!( - staking_events_since_last_call(), - vec![ - Event::StakersElected, - Event::EraPaid { era_index: 0, validator_payout: 11075, remainder: 33225 }, - Event::Chilled { stash: 11 }, - Event::ForceEra { mode: Forcing::ForceNew }, - Event::SlashReported { - validator: 11, - fraction: Perbill::from_percent(0), - slash_era: 1 - }, - Event::Chilled { stash: 21 }, - Event::SlashReported { - validator: 21, - fraction: Perbill::from_percent(25), - slash_era: 1 - }, - Event::Slashed { staker: 21, amount: 250 }, - Event::Slashed { staker: 101, amount: 94 } - ] - ); + assert_eq!( + staking_events_since_last_call(), + vec![ + Event::StakersElected, + Event::EraPaid { era_index: 0, validator_payout: 11075, remainder: 33225 }, + Event::Chilled { stash: 11 }, + Event::SlashReported { + validator: 11, + fraction: Perbill::from_percent(0), + slash_era: 1 + }, + Event::Chilled { stash: 21 }, + Event::SlashReported { + validator: 21, + fraction: Perbill::from_percent(25), + slash_era: 1 + }, + Event::Slashed { staker: 21, amount: 250 }, + Event::Slashed { staker: 101, amount: 94 } + ] + ); - // both validators should be disabled - assert!(is_disabled(11)); - assert!(is_disabled(21)); - }); + // both validators should be disabled + assert!(is_disabled(11)); + assert!(is_disabled(21)); + }); } #[test] -fn offence_threshold_triggers_new_era() { +fn offence_threshold_doesnt_trigger_new_era() { ExtBuilder::default() .validator_count(4) .set_status(41, StakerStatus::Validator) @@ -3407,13 +3425,10 @@ fn offence_threshold_triggers_new_era() { mock::start_active_era(1); assert_eq_uvec!(Session::validators(), vec![11, 21, 31, 41]); - assert_eq!( - ::OffendingValidatorsThreshold::get(), - Perbill::from_percent(75), - ); + assert_eq!(crate::disabling_threshold(Session::validators().len()), 1); - // we have 4 validators and an offending validator threshold of 75%, - // once the third validator commits an offence a new era should be forced + // we have 4 validators and an offending validator threshold of 1/3, + // even if the third validator commits an offence a new era should not be forced let exposure_11 = Staking::eras_stakers(Staking::active_era().unwrap().index, &11); let exposure_21 = Staking::eras_stakers(Staking::active_era().unwrap().index, &21); @@ -3438,18 +3453,21 @@ fn offence_threshold_triggers_new_era() { &[Perbill::zero()], ); - assert_eq!(ForceEra::::get(), Forcing::ForceNew); + assert_eq!(ForceEra::::get(), Forcing::NotForcing); }); } #[test] fn disabled_validators_are_kept_disabled_for_whole_era() { ExtBuilder::default() - .validator_count(4) + .validator_count(7) .set_status(41, StakerStatus::Validator) + .set_status(51, StakerStatus::Validator) + .set_status(201, StakerStatus::Validator) + .set_status(202, StakerStatus::Validator) .build_and_execute(|| { mock::start_active_era(1); - assert_eq_uvec!(Session::validators(), vec![11, 21, 31, 41]); + assert_eq_uvec!(Session::validators(), vec![11, 21, 31, 41, 51, 201, 202]); assert_eq!(::SessionsPerEra::get(), 3); let exposure_11 = Staking::eras_stakers(Staking::active_era().unwrap().index, &11); From aee9fe3eab3c323285e23c0216f4e6b508957b46 Mon Sep 17 00:00:00 2001 From: Tsvetomir Dimitrov Date: Wed, 15 Nov 2023 12:55:12 +0200 Subject: [PATCH 10/67] Update tests to cover the new disabling strategy --- substrate/frame/staking/src/tests.rs | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/substrate/frame/staking/src/tests.rs b/substrate/frame/staking/src/tests.rs index 41197bd4ef23..81ce61faac2a 100644 --- a/substrate/frame/staking/src/tests.rs +++ b/substrate/frame/staking/src/tests.rs @@ -3354,14 +3354,12 @@ fn non_slashable_offence_disables_validator() { #[test] fn slashing_independent_of_disabling_validator() { ExtBuilder::default() - .validator_count(7) + .validator_count(5) .set_status(41, StakerStatus::Validator) .set_status(51, StakerStatus::Validator) - .set_status(201, StakerStatus::Validator) - .set_status(202, StakerStatus::Validator) .build_and_execute(|| { mock::start_active_era(1); - assert_eq_uvec!(Session::validators(), vec![11, 21, 31, 41, 51, 201, 202]); + assert_eq_uvec!(Session::validators(), vec![11, 21, 31, 41, 51]); let exposure_11 = Staking::eras_stakers(Staking::active_era().unwrap().index, &11); let exposure_21 = Staking::eras_stakers(Staking::active_era().unwrap().index, &21); @@ -3410,9 +3408,10 @@ fn slashing_independent_of_disabling_validator() { ] ); - // both validators should be disabled + // first validator is disabled but not slashed assert!(is_disabled(11)); - assert!(is_disabled(21)); + // second validator is slashed but not disabled + assert!(!is_disabled(21)); }); } @@ -3439,6 +3438,9 @@ fn offence_threshold_doesnt_trigger_new_era() { &[Perbill::zero()], ); + // 11 should be disabled because the byzantine threshold is 1 + assert!(is_disabled(11)); + assert_eq!(ForceEra::::get(), Forcing::NotForcing); on_offence_now( @@ -3446,6 +3448,10 @@ fn offence_threshold_doesnt_trigger_new_era() { &[Perbill::zero()], ); + // 21 should not be disabled because the number of disabled validators will be above the + // byzantine threshold + assert!(!is_disabled(21)); + assert_eq!(ForceEra::::get(), Forcing::NotForcing); on_offence_now( @@ -3453,6 +3459,9 @@ fn offence_threshold_doesnt_trigger_new_era() { &[Perbill::zero()], ); + // same for 31 + assert!(!is_disabled(31)); + assert_eq!(ForceEra::::get(), Forcing::NotForcing); }); } From d23c53080ce5b76ae55c5a0dcf5a58e78e5110aa Mon Sep 17 00:00:00 2001 From: Tsvetomir Dimitrov Date: Wed, 15 Nov 2023 16:17:02 +0200 Subject: [PATCH 11/67] Storage migration for `OffendingValidators` --- polkadot/runtime/westend/src/lib.rs | 1 + substrate/frame/staking/src/migrations.rs | 59 +++++++++++++++++++++-- substrate/frame/staking/src/pallet/mod.rs | 2 +- 3 files changed, 57 insertions(+), 5 deletions(-) diff --git a/polkadot/runtime/westend/src/lib.rs b/polkadot/runtime/westend/src/lib.rs index de9c6c2545d6..910d8f823c86 100644 --- a/polkadot/runtime/westend/src/lib.rs +++ b/polkadot/runtime/westend/src/lib.rs @@ -1547,6 +1547,7 @@ pub mod migrations { pallet_im_online::migration::v1::Migration, parachains_configuration::migration::v7::MigrateToV7, pallet_staking::migrations::v14::MigrateToV14, + pallet_staking::migrations::v15::MigrateToV15, assigned_slots::migration::v1::MigrateToV1, parachains_scheduler::migration::v1::MigrateToV1, parachains_configuration::migration::v8::MigrateToV8, diff --git a/substrate/frame/staking/src/migrations.rs b/substrate/frame/staking/src/migrations.rs index 84b00254126f..fad2c6814bfa 100644 --- a/substrate/frame/staking/src/migrations.rs +++ b/substrate/frame/staking/src/migrations.rs @@ -59,11 +59,62 @@ impl Default for ObsoleteReleases { #[storage_alias] type StorageVersion = StorageValue, ObsoleteReleases, ValueQuery>; +/// Migrating `OffendingValidators` from `Vec<(u32, bool)>` to `Vec` +pub mod v15 { + use super::*; + + pub struct MigrateToV15(sp_std::marker::PhantomData); + impl OnRuntimeUpgrade for MigrateToV15 { + fn on_runtime_upgrade() -> Weight { + let current = Pallet::::current_storage_version(); + let on_chain = Pallet::::on_chain_storage_version(); + + if current == 15 && on_chain == 14 { + current.put::>(); + + let migrated = v14::V14OffendingValidators::::get() + .into_iter() + .map(|p| p.0) + .collect::>(); + OffendingValidators::::set(migrated); + + log!(info, "v15 applied successfully."); + T::DbWeight::get().reads_writes(1, 1) + } else { + log!(warn, "v15 not applied."); + T::DbWeight::get().reads(1) + } + } + + #[cfg(feature = "try-runtime")] + fn pre_upgrade() -> Result, TryRuntimeError> { + frame_support::ensure!( + Pallet::::on_chain_storage_version() == 14, + "Required v14 before upgrading to v15." + ); + + Ok(Default::default()) + } + + #[cfg(feature = "try-runtime")] + fn post_upgrade(_state: Vec) -> Result<(), TryRuntimeError> { + frame_support::ensure!( + Pallet::::on_chain_storage_version() == 15, + "v15 not applied" + ); + Ok(()) + } + } +} /// Migration of era exposure storage items to paged exposures. /// Changelog: [v14.](https://github.com/paritytech/substrate/blob/ankan/paged-rewards-rebased2/frame/staking/CHANGELOG.md#14) pub mod v14 { use super::*; + #[frame_support::storage_alias] + pub(crate) type V14OffendingValidators = + StorageValue, Vec<(u32, bool)>, ValueQuery>; + pub struct MigrateToV14(sp_std::marker::PhantomData); impl OnRuntimeUpgrade for MigrateToV14 { fn on_runtime_upgrade() -> Weight { @@ -73,10 +124,10 @@ pub mod v14 { if current == 14 && on_chain == 13 { current.put::>(); - log!(info, "v14 applied successfully."); + log!(info, "staking v14 applied successfully."); T::DbWeight::get().reads_writes(1, 1) } else { - log!(warn, "v14 not applied."); + log!(warn, "staking v14 not applied."); T::DbWeight::get().reads(1) } } @@ -85,7 +136,7 @@ pub mod v14 { fn pre_upgrade() -> Result, TryRuntimeError> { frame_support::ensure!( Pallet::::on_chain_storage_version() == 13, - "Required v13 before upgrading to v14." + "Required staking v13 before upgrading to v14." ); Ok(Default::default()) @@ -95,7 +146,7 @@ pub mod v14 { fn post_upgrade(_state: Vec) -> Result<(), TryRuntimeError> { frame_support::ensure!( Pallet::::on_chain_storage_version() == 14, - "v14 not applied" + "staking v14 not applied" ); Ok(()) } diff --git a/substrate/frame/staking/src/pallet/mod.rs b/substrate/frame/staking/src/pallet/mod.rs index b45fad096836..82895e7d4da6 100644 --- a/substrate/frame/staking/src/pallet/mod.rs +++ b/substrate/frame/staking/src/pallet/mod.rs @@ -67,7 +67,7 @@ pub mod pallet { use super::*; /// The current storage version. - const STORAGE_VERSION: StorageVersion = StorageVersion::new(14); + const STORAGE_VERSION: StorageVersion = StorageVersion::new(15); #[pallet::pallet] #[pallet::storage_version(STORAGE_VERSION)] From d2c58950bf23db4004ca4ca7d42f8e3244f08a2c Mon Sep 17 00:00:00 2001 From: Tsvetomir Dimitrov Date: Thu, 16 Nov 2023 10:47:43 +0200 Subject: [PATCH 12/67] Apply suggestions from code review Co-authored-by: ordian --- substrate/frame/staking/src/slashing.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/substrate/frame/staking/src/slashing.rs b/substrate/frame/staking/src/slashing.rs index 1504a838b86e..35299097dc46 100644 --- a/substrate/frame/staking/src/slashing.rs +++ b/substrate/frame/staking/src/slashing.rs @@ -336,7 +336,7 @@ fn add_offending_validator(stash: &T::AccountId) { // this is a new offending validator // we don't want to disable too many validators otherwise we will break consensus - if offending.len() + 1 > disabling_threshold(validators.len()) as usize { + if offending.len() >= disabling_threshold(validators.len()) as usize { return } From 06e79d9723d08d29391ae53dcd0fcda2327d4c66 Mon Sep 17 00:00:00 2001 From: Tsvetomir Dimitrov Date: Thu, 16 Nov 2023 10:45:35 +0200 Subject: [PATCH 13/67] Fix storage migration - use `VersionedMigration`, fix storage alias, rename `OffendingValidators` to `DisabledValidators` --- polkadot/runtime/westend/src/lib.rs | 2 +- substrate/frame/staking/src/migrations.rs | 61 +++++++-------------- substrate/frame/staking/src/pallet/impls.rs | 4 +- substrate/frame/staking/src/pallet/mod.rs | 4 +- substrate/frame/staking/src/slashing.rs | 8 +-- 5 files changed, 30 insertions(+), 49 deletions(-) diff --git a/polkadot/runtime/westend/src/lib.rs b/polkadot/runtime/westend/src/lib.rs index 910d8f823c86..39664673546b 100644 --- a/polkadot/runtime/westend/src/lib.rs +++ b/polkadot/runtime/westend/src/lib.rs @@ -1547,7 +1547,7 @@ pub mod migrations { pallet_im_online::migration::v1::Migration, parachains_configuration::migration::v7::MigrateToV7, pallet_staking::migrations::v14::MigrateToV14, - pallet_staking::migrations::v15::MigrateToV15, + pallet_staking::migrations::v15::MigrateV14ToV15, assigned_slots::migration::v1::MigrateToV1, parachains_scheduler::migration::v1::MigrateToV1, parachains_configuration::migration::v8::MigrateToV8, diff --git a/substrate/frame/staking/src/migrations.rs b/substrate/frame/staking/src/migrations.rs index fad2c6814bfa..201858dedbf6 100644 --- a/substrate/frame/staking/src/migrations.rs +++ b/substrate/frame/staking/src/migrations.rs @@ -20,6 +20,7 @@ use super::*; use frame_election_provider_support::SortedListProvider; use frame_support::{ + migrations::VersionedMigration, pallet_prelude::ValueQuery, storage_alias, traits::{GetStorageVersion, OnRuntimeUpgrade}, @@ -63,56 +64,36 @@ type StorageVersion = StorageValue, ObsoleteReleases, Value pub mod v15 { use super::*; - pub struct MigrateToV15(sp_std::marker::PhantomData); - impl OnRuntimeUpgrade for MigrateToV15 { + pub struct VersionUncheckedMigrateV14ToV15(sp_std::marker::PhantomData); + impl OnRuntimeUpgrade for VersionUncheckedMigrateV14ToV15 { fn on_runtime_upgrade() -> Weight { - let current = Pallet::::current_storage_version(); - let on_chain = Pallet::::on_chain_storage_version(); - - if current == 15 && on_chain == 14 { - current.put::>(); - - let migrated = v14::V14OffendingValidators::::get() - .into_iter() - .map(|p| p.0) - .collect::>(); - OffendingValidators::::set(migrated); - - log!(info, "v15 applied successfully."); - T::DbWeight::get().reads_writes(1, 1) - } else { - log!(warn, "v15 not applied."); - T::DbWeight::get().reads(1) - } - } - - #[cfg(feature = "try-runtime")] - fn pre_upgrade() -> Result, TryRuntimeError> { - frame_support::ensure!( - Pallet::::on_chain_storage_version() == 14, - "Required v14 before upgrading to v15." - ); - - Ok(Default::default()) - } - - #[cfg(feature = "try-runtime")] - fn post_upgrade(_state: Vec) -> Result<(), TryRuntimeError> { - frame_support::ensure!( - Pallet::::on_chain_storage_version() == 15, - "v15 not applied" - ); - Ok(()) + let migrated = v14::OffendingValidators::::get() + .into_iter() + .map(|p| p.0) + .collect::>(); + DisabledValidators::::set(migrated); + + log!(info, "v15 applied successfully."); + T::DbWeight::get().reads_writes(1, 1) } } + + pub type MigrateV14ToV15 = VersionedMigration< + 14, + 15, + VersionUncheckedMigrateV14ToV15, + crate::Pallet, + ::DbWeight, + >; } + /// Migration of era exposure storage items to paged exposures. /// Changelog: [v14.](https://github.com/paritytech/substrate/blob/ankan/paged-rewards-rebased2/frame/staking/CHANGELOG.md#14) pub mod v14 { use super::*; #[frame_support::storage_alias] - pub(crate) type V14OffendingValidators = + pub(crate) type OffendingValidators = StorageValue, Vec<(u32, bool)>, ValueQuery>; pub struct MigrateToV14(sp_std::marker::PhantomData); diff --git a/substrate/frame/staking/src/pallet/impls.rs b/substrate/frame/staking/src/pallet/impls.rs index d15e0fb6bb3f..dcc414b90bdf 100644 --- a/substrate/frame/staking/src/pallet/impls.rs +++ b/substrate/frame/staking/src/pallet/impls.rs @@ -427,7 +427,7 @@ impl Pallet { } // disable all offending validators that have been disabled for the whole era - for index in >::get() { + for index in >::get() { T::SessionInterface::disable_validator(index); } } @@ -509,7 +509,7 @@ impl Pallet { T::RewardRemainder::on_unbalanced(T::Currency::issue(remainder)); // Clear offending validators. - >::kill(); + >::kill(); } } diff --git a/substrate/frame/staking/src/pallet/mod.rs b/substrate/frame/staking/src/pallet/mod.rs index 82895e7d4da6..8cae69f416cf 100644 --- a/substrate/frame/staking/src/pallet/mod.rs +++ b/substrate/frame/staking/src/pallet/mod.rs @@ -640,8 +640,8 @@ pub mod pallet { /// offended using binary search. #[pallet::storage] #[pallet::unbounded] - #[pallet::getter(fn offending_validators)] - pub type OffendingValidators = StorageValue<_, Vec, ValueQuery>; + #[pallet::getter(fn disabled_validators)] + pub type DisabledValidators = StorageValue<_, Vec, ValueQuery>; /// The threshold for when users can start calling `chill_other` for other validators / /// nominators. The threshold is compared to the actual number of validators / nominators diff --git a/substrate/frame/staking/src/slashing.rs b/substrate/frame/staking/src/slashing.rs index 35299097dc46..0f9759fd4144 100644 --- a/substrate/frame/staking/src/slashing.rs +++ b/substrate/frame/staking/src/slashing.rs @@ -50,8 +50,8 @@ //! Based on research at use crate::{ - pallet::disabling_threshold, BalanceOf, Config, Error, Exposure, NegativeImbalanceOf, - NominatorSlashInEra, OffendingValidators, Pallet, Perbill, SessionInterface, SpanSlash, + pallet::disabling_threshold, BalanceOf, Config, DisabledValidators, Error, Exposure, + NegativeImbalanceOf, NominatorSlashInEra, Pallet, Perbill, SessionInterface, SpanSlash, UnappliedSlash, ValidatorSlashInEra, }; use codec::{Decode, Encode, MaxEncodedLen}; @@ -323,7 +323,7 @@ fn kick_out_if_recent(params: SlashParams) { /// If after adding the validator `OffendingValidatorsThreshold` is reached /// a new era will be forced. fn add_offending_validator(stash: &T::AccountId) { - OffendingValidators::::mutate(|offending| { + DisabledValidators::::mutate(|offending| { let validators = T::SessionInterface::validators(); let validator_index = match validators.iter().position(|i| i == stash) { Some(index) => index, @@ -340,7 +340,7 @@ fn add_offending_validator(stash: &T::AccountId) { return } - // Add the validator to `OffendingValidators` and disable it + // Add the validator to `DisabledValidators` and disable it offending.insert(index, validator_index_u32); T::SessionInterface::disable_validator(validator_index_u32); } From 9f8c5986cc80ce4d11cb4a42b285a7b209481e82 Mon Sep 17 00:00:00 2001 From: Tsvetomir Dimitrov Date: Thu, 16 Nov 2023 10:50:45 +0200 Subject: [PATCH 14/67] Rename `disabling_threshold` -> `disabling_limit` --- .../test-staking-e2e/src/mock.rs | 2 +- substrate/frame/staking/src/lib.rs | 2 +- substrate/frame/staking/src/pallet/mod.rs | 6 +++--- substrate/frame/staking/src/slashing.rs | 4 ++-- substrate/frame/staking/src/tests.rs | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/mock.rs b/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/mock.rs index 68a8449a887e..a42a73a2e734 100644 --- a/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/mock.rs +++ b/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/mock.rs @@ -815,7 +815,7 @@ pub(crate) fn add_slash(who: &AccountId) { // Slashes enough validators to cross the `Staking::OffendingValidatorsThreshold`. pub(crate) fn slash_through_offending_threshold() { let validators = Session::validators(); - let mut remaining_slashes = pallet_staking::disabling_threshold(validators.len()); + let mut remaining_slashes = pallet_staking::disabling_limit(validators.len()); for v in validators.into_iter() { if remaining_slashes != 0 { diff --git a/substrate/frame/staking/src/lib.rs b/substrate/frame/staking/src/lib.rs index aa25fe1af0de..8394fb6173f1 100644 --- a/substrate/frame/staking/src/lib.rs +++ b/substrate/frame/staking/src/lib.rs @@ -329,7 +329,7 @@ pub use sp_staking::{Exposure, IndividualExposure, StakerStatus}; use sp_std::{collections::btree_map::BTreeMap, prelude::*}; pub use weights::WeightInfo; -pub use pallet::{disabling_threshold, pallet::*, UseNominatorsAndValidatorsMap, UseValidatorsMap}; +pub use pallet::{disabling_limit, pallet::*, UseNominatorsAndValidatorsMap, UseValidatorsMap}; pub(crate) const STAKING_ID: LockIdentifier = *b"staking "; pub(crate) const LOG_TARGET: &str = "runtime::staking"; diff --git a/substrate/frame/staking/src/pallet/mod.rs b/substrate/frame/staking/src/pallet/mod.rs index 8cae69f416cf..c6611d21a22f 100644 --- a/substrate/frame/staking/src/pallet/mod.rs +++ b/substrate/frame/staking/src/pallet/mod.rs @@ -1873,8 +1873,8 @@ fn is_sorted_and_unique(list: &[u32]) -> bool { list.windows(2).all(|w| w[0] < w[1]) } -/// Disabling threshold calculated from the total number of validators in the active set. When -/// reached no more validators will be disabled. -pub fn disabling_threshold(validators_len: usize) -> usize { +/// Disabling limit calculated from the total number of validators in the active set. When reached +/// no more validators will be disabled. +pub fn disabling_limit(validators_len: usize) -> usize { validators_len.saturating_sub(1) / 3 } diff --git a/substrate/frame/staking/src/slashing.rs b/substrate/frame/staking/src/slashing.rs index 0f9759fd4144..08bce185b0c6 100644 --- a/substrate/frame/staking/src/slashing.rs +++ b/substrate/frame/staking/src/slashing.rs @@ -50,7 +50,7 @@ //! Based on research at use crate::{ - pallet::disabling_threshold, BalanceOf, Config, DisabledValidators, Error, Exposure, + pallet::disabling_limit, BalanceOf, Config, DisabledValidators, Error, Exposure, NegativeImbalanceOf, NominatorSlashInEra, Pallet, Perbill, SessionInterface, SpanSlash, UnappliedSlash, ValidatorSlashInEra, }; @@ -336,7 +336,7 @@ fn add_offending_validator(stash: &T::AccountId) { // this is a new offending validator // we don't want to disable too many validators otherwise we will break consensus - if offending.len() >= disabling_threshold(validators.len()) as usize { + if offending.len() >= disabling_limit(validators.len()) as usize { return } diff --git a/substrate/frame/staking/src/tests.rs b/substrate/frame/staking/src/tests.rs index 81ce61faac2a..27c163a88227 100644 --- a/substrate/frame/staking/src/tests.rs +++ b/substrate/frame/staking/src/tests.rs @@ -3424,7 +3424,7 @@ fn offence_threshold_doesnt_trigger_new_era() { mock::start_active_era(1); assert_eq_uvec!(Session::validators(), vec![11, 21, 31, 41]); - assert_eq!(crate::disabling_threshold(Session::validators().len()), 1); + assert_eq!(crate::disabling_limit(Session::validators().len()), 1); // we have 4 validators and an offending validator threshold of 1/3, // even if the third validator commits an offence a new era should not be forced From 0a39a156756c9a3a3f81725094f20d0c9ac64f80 Mon Sep 17 00:00:00 2001 From: Tsvetomir Dimitrov Date: Thu, 16 Nov 2023 15:39:17 +0200 Subject: [PATCH 15/67] Fix test-staking-e2e --- .../test-staking-e2e/src/lib.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/lib.rs b/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/lib.rs index 1d3f4712b1d6..d1fd27f701b7 100644 --- a/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/lib.rs +++ b/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/lib.rs @@ -159,7 +159,7 @@ fn enters_emergency_phase_after_forcing_before_elect() { // slashes so that staking goes into `Forcing::ForceNew`. slash_through_offending_threshold(); - assert_eq!(pallet_staking::ForceEra::::get(), pallet_staking::Forcing::ForceNew); + assert_eq!(pallet_staking::ForceEra::::get(), pallet_staking::Forcing::NotForcing); advance_session_delayed_solution(pool_state.clone()); assert!(ElectionProviderMultiPhase::current_phase().is_emergency()); @@ -269,7 +269,7 @@ fn continous_slashes_below_offending_threshold() { /// Related to . fn set_validation_intention_after_chilled() { use frame_election_provider_support::SortedListProvider; - use pallet_staking::{Event, Forcing, Nominators}; + use pallet_staking::{Event, Nominators}; let (mut ext, pool_state, _) = ExtBuilder::default() .epm(EpmExtBuilder::default()) @@ -295,7 +295,6 @@ fn set_validation_intention_after_chilled() { staking_events(), [ Event::Chilled { stash: 41 }, - Event::ForceEra { mode: Forcing::ForceNew }, Event::SlashReported { validator: 41, slash_era: 0, From f4f82ec47282f0df1354fa231611364c01d40fd1 Mon Sep 17 00:00:00 2001 From: Tsvetomir Dimitrov Date: Thu, 16 Nov 2023 15:57:46 +0200 Subject: [PATCH 16/67] Fix a compilation error --- substrate/frame/nomination-pools/benchmarking/src/mock.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/substrate/frame/nomination-pools/benchmarking/src/mock.rs b/substrate/frame/nomination-pools/benchmarking/src/mock.rs index 9a7f2197a7b2..471a97456370 100644 --- a/substrate/frame/nomination-pools/benchmarking/src/mock.rs +++ b/substrate/frame/nomination-pools/benchmarking/src/mock.rs @@ -111,7 +111,6 @@ impl pallet_staking::Config for Runtime { type EraPayout = pallet_staking::ConvertCurve; type NextNewSession = (); type MaxExposurePageSize = ConstU32<64>; - type OffendingValidatorsThreshold = (); type ElectionProvider = frame_election_provider_support::NoElection<(AccountId, BlockNumber, Staking, ())>; type GenesisElectionProvider = Self::ElectionProvider; From 879279bea7db96bc7764c8c25264d45bbf221914 Mon Sep 17 00:00:00 2001 From: Tsvetomir Dimitrov Date: Thu, 16 Nov 2023 17:01:45 +0200 Subject: [PATCH 17/67] Fix compilation errors in benchmarks --- substrate/frame/offences/benchmarking/src/mock.rs | 1 - substrate/frame/session/benchmarking/src/mock.rs | 1 - 2 files changed, 2 deletions(-) diff --git a/substrate/frame/offences/benchmarking/src/mock.rs b/substrate/frame/offences/benchmarking/src/mock.rs index 1a458ec90d58..7d0bd9231954 100644 --- a/substrate/frame/offences/benchmarking/src/mock.rs +++ b/substrate/frame/offences/benchmarking/src/mock.rs @@ -177,7 +177,6 @@ impl pallet_staking::Config for Test { type EraPayout = pallet_staking::ConvertCurve; type NextNewSession = Session; type MaxExposurePageSize = ConstU32<64>; - type OffendingValidatorsThreshold = (); type ElectionProvider = onchain::OnChainExecution; type GenesisElectionProvider = Self::ElectionProvider; type VoterList = pallet_staking::UseNominatorsAndValidatorsMap; diff --git a/substrate/frame/session/benchmarking/src/mock.rs b/substrate/frame/session/benchmarking/src/mock.rs index 47c337569a02..3ceb31ec8e49 100644 --- a/substrate/frame/session/benchmarking/src/mock.rs +++ b/substrate/frame/session/benchmarking/src/mock.rs @@ -174,7 +174,6 @@ impl pallet_staking::Config for Test { type EraPayout = pallet_staking::ConvertCurve; type NextNewSession = Session; type MaxExposurePageSize = ConstU32<64>; - type OffendingValidatorsThreshold = (); type ElectionProvider = onchain::OnChainExecution; type GenesisElectionProvider = Self::ElectionProvider; type MaxUnlockingChunks = ConstU32<32>; From 5936d69f3c171bfdff7384443af8d5831855e0fa Mon Sep 17 00:00:00 2001 From: Tsvetomir Dimitrov Date: Fri, 24 Nov 2023 10:13:36 +0200 Subject: [PATCH 18/67] Extract `DisablingStrategy` as a trait --- polkadot/runtime/test-runtime/src/lib.rs | 1 + polkadot/runtime/westend/src/lib.rs | 1 + substrate/bin/node/runtime/src/lib.rs | 1 + substrate/frame/babe/src/mock.rs | 1 + substrate/frame/beefy/src/mock.rs | 1 + .../test-staking-e2e/src/mock.rs | 6 ++- substrate/frame/fast-unstake/src/mock.rs | 1 + substrate/frame/grandpa/src/mock.rs | 1 + .../nomination-pools/test-staking/src/mock.rs | 1 + substrate/frame/root-offences/src/mock.rs | 1 + substrate/frame/session/src/lib.rs | 2 +- substrate/frame/staking/src/lib.rs | 49 ++++++++++++++++++- substrate/frame/staking/src/mock.rs | 1 + substrate/frame/staking/src/pallet/mod.rs | 17 +++---- substrate/frame/staking/src/slashing.rs | 25 +++++----- substrate/frame/staking/src/tests.rs | 7 ++- 16 files changed, 91 insertions(+), 25 deletions(-) diff --git a/polkadot/runtime/test-runtime/src/lib.rs b/polkadot/runtime/test-runtime/src/lib.rs index 0d2160b6c0ad..0695a9067fde 100644 --- a/polkadot/runtime/test-runtime/src/lib.rs +++ b/polkadot/runtime/test-runtime/src/lib.rs @@ -368,6 +368,7 @@ impl pallet_staking::Config for Runtime { type BenchmarkingConfig = runtime_common::StakingBenchmarkingConfig; type EventListeners = (); type WeightInfo = (); + type DisablingStrategy = pallet_staking::UpToByzantineThresholdDisablingStrategy; } parameter_types! { diff --git a/polkadot/runtime/westend/src/lib.rs b/polkadot/runtime/westend/src/lib.rs index b53fc1c446fa..65c85fa5bd39 100644 --- a/polkadot/runtime/westend/src/lib.rs +++ b/polkadot/runtime/westend/src/lib.rs @@ -695,6 +695,7 @@ impl pallet_staking::Config for Runtime { type BenchmarkingConfig = runtime_common::StakingBenchmarkingConfig; type EventListeners = NominationPools; type WeightInfo = weights::pallet_staking::WeightInfo; + type DisablingStrategy = pallet_staking::UpToByzantineThresholdDisablingStrategy; } impl pallet_fast_unstake::Config for Runtime { diff --git a/substrate/bin/node/runtime/src/lib.rs b/substrate/bin/node/runtime/src/lib.rs index f0fc8303bb71..4d501b7c6d44 100644 --- a/substrate/bin/node/runtime/src/lib.rs +++ b/substrate/bin/node/runtime/src/lib.rs @@ -678,6 +678,7 @@ impl pallet_staking::Config for Runtime { type EventListeners = NominationPools; type WeightInfo = pallet_staking::weights::SubstrateWeight; type BenchmarkingConfig = StakingBenchmarkingConfig; + type DisablingStrategy = pallet_staking::UpToByzantineThresholdDisablingStrategy; } impl pallet_fast_unstake::Config for Runtime { diff --git a/substrate/frame/babe/src/mock.rs b/substrate/frame/babe/src/mock.rs index ae7be6d1ead3..d79a47076bf7 100644 --- a/substrate/frame/babe/src/mock.rs +++ b/substrate/frame/babe/src/mock.rs @@ -185,6 +185,7 @@ impl pallet_staking::Config for Test { type EventListeners = (); type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig; type WeightInfo = (); + type DisablingStrategy = pallet_staking::UpToByzantineThresholdDisablingStrategy; } impl pallet_offences::Config for Test { diff --git a/substrate/frame/beefy/src/mock.rs b/substrate/frame/beefy/src/mock.rs index fa5c97b271a0..f9fa3c5f62ea 100644 --- a/substrate/frame/beefy/src/mock.rs +++ b/substrate/frame/beefy/src/mock.rs @@ -203,6 +203,7 @@ impl pallet_staking::Config for Test { type EventListeners = (); type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig; type WeightInfo = (); + type DisablingStrategy = pallet_staking::UpToByzantineThresholdDisablingStrategy; } impl pallet_offences::Config for Test { diff --git a/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/mock.rs b/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/mock.rs index a42a73a2e734..85cf88d3eba7 100644 --- a/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/mock.rs +++ b/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/mock.rs @@ -278,6 +278,7 @@ impl pallet_staking::Config for Runtime { type EventListeners = (); type WeightInfo = pallet_staking::weights::SubstrateWeight; type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig; + type DisablingStrategy = pallet_staking::UpToByzantineThresholdDisablingStrategy; } impl frame_system::offchain::SendTransactionTypes for Runtime @@ -815,7 +816,10 @@ pub(crate) fn add_slash(who: &AccountId) { // Slashes enough validators to cross the `Staking::OffendingValidatorsThreshold`. pub(crate) fn slash_through_offending_threshold() { let validators = Session::validators(); - let mut remaining_slashes = pallet_staking::disabling_limit(validators.len()); + let mut remaining_slashes = + pallet_staking::UpToByzantineThresholdDisablingStrategy::byzantine_threshold( + validators.len(), + ); for v in validators.into_iter() { if remaining_slashes != 0 { diff --git a/substrate/frame/fast-unstake/src/mock.rs b/substrate/frame/fast-unstake/src/mock.rs index 48baa574e73f..26a3efad5eba 100644 --- a/substrate/frame/fast-unstake/src/mock.rs +++ b/substrate/frame/fast-unstake/src/mock.rs @@ -144,6 +144,7 @@ impl pallet_staking::Config for Runtime { type EventListeners = (); type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig; type WeightInfo = (); + type DisablingStrategy = pallet_staking::UpToByzantineThresholdDisablingStrategy; } pub struct BalanceToU256; diff --git a/substrate/frame/grandpa/src/mock.rs b/substrate/frame/grandpa/src/mock.rs index d88e98c7eb54..06dd5998fa91 100644 --- a/substrate/frame/grandpa/src/mock.rs +++ b/substrate/frame/grandpa/src/mock.rs @@ -207,6 +207,7 @@ impl pallet_staking::Config for Test { type EventListeners = (); type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig; type WeightInfo = (); + type DisablingStrategy = pallet_staking::UpToByzantineThresholdDisablingStrategy; } impl pallet_offences::Config for Test { diff --git a/substrate/frame/nomination-pools/test-staking/src/mock.rs b/substrate/frame/nomination-pools/test-staking/src/mock.rs index 3cf9bff56cec..9929629ec052 100644 --- a/substrate/frame/nomination-pools/test-staking/src/mock.rs +++ b/substrate/frame/nomination-pools/test-staking/src/mock.rs @@ -136,6 +136,7 @@ impl pallet_staking::Config for Runtime { type EventListeners = Pools; type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig; type WeightInfo = (); + type DisablingStrategy = pallet_staking::UpToByzantineThresholdDisablingStrategy; } parameter_types! { diff --git a/substrate/frame/root-offences/src/mock.rs b/substrate/frame/root-offences/src/mock.rs index 77111e2c6e9e..3d496f5ccab1 100644 --- a/substrate/frame/root-offences/src/mock.rs +++ b/substrate/frame/root-offences/src/mock.rs @@ -189,6 +189,7 @@ impl pallet_staking::Config for Test { type EventListeners = (); type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig; type WeightInfo = (); + type DisablingStrategy = pallet_staking::UpToByzantineThresholdDisablingStrategy; } impl pallet_session::historical::Config for Test { diff --git a/substrate/frame/session/src/lib.rs b/substrate/frame/session/src/lib.rs index bf4671a247f0..0759f0ef7df6 100644 --- a/substrate/frame/session/src/lib.rs +++ b/substrate/frame/session/src/lib.rs @@ -637,7 +637,7 @@ impl Pallet { Validators::::put(&validators); if changed { - // reset disabled validators + // reset disabled validators if active set was changed >::take(); } diff --git a/substrate/frame/staking/src/lib.rs b/substrate/frame/staking/src/lib.rs index 8394fb6173f1..0d634ab63d48 100644 --- a/substrate/frame/staking/src/lib.rs +++ b/substrate/frame/staking/src/lib.rs @@ -329,7 +329,7 @@ pub use sp_staking::{Exposure, IndividualExposure, StakerStatus}; use sp_std::{collections::btree_map::BTreeMap, prelude::*}; pub use weights::WeightInfo; -pub use pallet::{disabling_limit, pallet::*, UseNominatorsAndValidatorsMap, UseValidatorsMap}; +pub use pallet::{pallet::*, UseNominatorsAndValidatorsMap, UseValidatorsMap}; pub(crate) const STAKING_ID: LockIdentifier = *b"staking "; pub(crate) const LOG_TARGET: &str = "runtime::staking"; @@ -1227,3 +1227,50 @@ impl BenchmarkingConfig for TestBenchmarkingConfig { type MaxValidators = frame_support::traits::ConstU32<100>; type MaxNominators = frame_support::traits::ConstU32<100>; } + +/// A result from a disabling decision made by [`DisablingStrategy`]. +pub struct DisablingDecision { + /// List of validator indices that should be disabled. + disable_offenders: Vec, +} + +/// Controls validator disabling +pub trait DisablingStrategy { + /// Make a decision if an offender should be disabled or not. The result is an instance of + /// `[DisablingDecision]` + fn make_disabling_decision( + offender_idx: u32, + currently_disabled: &Vec, + active_set: &Vec, + ) -> DisablingDecision; +} + +/// Implementation of [`DisablingStrategy`] which disables no more than 1/3 of the validators in the +/// active set. +pub struct UpToByzantineThresholdDisablingStrategy; + +impl UpToByzantineThresholdDisablingStrategy { + /// Disabling limit calculated from the total number of validators in the active set. When + /// reached no more validators will be disabled. + pub fn byzantine_threshold(validators_len: usize) -> usize { + validators_len.saturating_sub(1) / 3 + } +} + +impl DisablingStrategy for UpToByzantineThresholdDisablingStrategy { + fn make_disabling_decision( + offender_idx: u32, + currently_disabled: &Vec, + active_set: &Vec, + ) -> DisablingDecision { + // we don't want to disable more offenders than the byzantine threshold + let disable_offenders = + if currently_disabled.len() >= Self::byzantine_threshold(active_set.len()) as usize { + vec![] + } else { + vec![offender_idx] + }; + + DisablingDecision { disable_offenders } + } +} diff --git a/substrate/frame/staking/src/mock.rs b/substrate/frame/staking/src/mock.rs index eece56af7025..57ff8728f528 100644 --- a/substrate/frame/staking/src/mock.rs +++ b/substrate/frame/staking/src/mock.rs @@ -316,6 +316,7 @@ impl crate::pallet::pallet::Config for Test { type EventListeners = EventListenerMock; type BenchmarkingConfig = TestBenchmarkingConfig; type WeightInfo = (); + type DisablingStrategy = pallet_staking::UpToByzantineThresholdDisablingStrategy; } pub struct WeightedNominationsQuota; diff --git a/substrate/frame/staking/src/pallet/mod.rs b/substrate/frame/staking/src/pallet/mod.rs index c6611d21a22f..27d335f2b8cb 100644 --- a/substrate/frame/staking/src/pallet/mod.rs +++ b/substrate/frame/staking/src/pallet/mod.rs @@ -47,10 +47,10 @@ mod impls; pub use impls::*; use crate::{ - slashing, weights::WeightInfo, AccountIdLookupOf, ActiveEraInfo, BalanceOf, EraPayout, - EraRewardPoints, Exposure, ExposurePage, Forcing, MaxNominationsOf, NegativeImbalanceOf, - Nominations, NominationsQuota, PositiveImbalanceOf, RewardDestination, SessionInterface, - StakingLedger, UnappliedSlash, UnlockChunk, ValidatorPrefs, + slashing, weights::WeightInfo, AccountIdLookupOf, ActiveEraInfo, BalanceOf, DisablingStrategy, + EraPayout, EraRewardPoints, Exposure, ExposurePage, Forcing, MaxNominationsOf, + NegativeImbalanceOf, Nominations, NominationsQuota, PositiveImbalanceOf, RewardDestination, + SessionInterface, StakingLedger, UnappliedSlash, UnlockChunk, ValidatorPrefs, }; // The speculative number of spans are used as an input of the weight annotation of @@ -271,6 +271,9 @@ pub mod pallet { /// WARNING: this only reports slashing events for the time being. type EventListeners: sp_staking::OnStakingUpdate>; + // `DisablingStragegy` controlls how validators are disabled + type DisablingStrategy: DisablingStrategy; + /// Some parameters of the benchmarking. type BenchmarkingConfig: BenchmarkingConfig; @@ -1872,9 +1875,3 @@ pub mod pallet { fn is_sorted_and_unique(list: &[u32]) -> bool { list.windows(2).all(|w| w[0] < w[1]) } - -/// Disabling limit calculated from the total number of validators in the active set. When reached -/// no more validators will be disabled. -pub fn disabling_limit(validators_len: usize) -> usize { - validators_len.saturating_sub(1) / 3 -} diff --git a/substrate/frame/staking/src/slashing.rs b/substrate/frame/staking/src/slashing.rs index 08bce185b0c6..7ca72f42593a 100644 --- a/substrate/frame/staking/src/slashing.rs +++ b/substrate/frame/staking/src/slashing.rs @@ -50,7 +50,7 @@ //! Based on research at use crate::{ - pallet::disabling_limit, BalanceOf, Config, DisabledValidators, Error, Exposure, + BalanceOf, Config, DisabledValidators, DisablingDecision, DisablingStrategy, Error, Exposure, NegativeImbalanceOf, NominatorSlashInEra, Pallet, Perbill, SessionInterface, SpanSlash, UnappliedSlash, ValidatorSlashInEra, }; @@ -323,7 +323,7 @@ fn kick_out_if_recent(params: SlashParams) { /// If after adding the validator `OffendingValidatorsThreshold` is reached /// a new era will be forced. fn add_offending_validator(stash: &T::AccountId) { - DisabledValidators::::mutate(|offending| { + DisabledValidators::::mutate(|disabled| { let validators = T::SessionInterface::validators(); let validator_index = match validators.iter().position(|i| i == stash) { Some(index) => index, @@ -332,17 +332,20 @@ fn add_offending_validator(stash: &T::AccountId) { let validator_index_u32 = validator_index as u32; - if let Err(index) = offending.binary_search_by_key(&validator_index_u32, |index| *index) { - // this is a new offending validator + let DisablingDecision { disable_offenders } = T::DisablingStrategy::make_disabling_decision( + validator_index as u32, + &disabled, + &validators, + ); - // we don't want to disable too many validators otherwise we will break consensus - if offending.len() >= disabling_limit(validators.len()) as usize { - return + for offender in disable_offenders { + // Add the validator to `DisabledValidators` and disable it. Do nothing if it is + // already disabled. + if let Err(index) = disabled.binary_search_by_key(&validator_index_u32, |index| *index) + { + disabled.insert(index, offender); + T::SessionInterface::disable_validator(offender); } - - // Add the validator to `DisabledValidators` and disable it - offending.insert(index, validator_index_u32); - T::SessionInterface::disable_validator(validator_index_u32); } }); } diff --git a/substrate/frame/staking/src/tests.rs b/substrate/frame/staking/src/tests.rs index 27c163a88227..b4127926640b 100644 --- a/substrate/frame/staking/src/tests.rs +++ b/substrate/frame/staking/src/tests.rs @@ -3424,7 +3424,12 @@ fn offence_threshold_doesnt_trigger_new_era() { mock::start_active_era(1); assert_eq_uvec!(Session::validators(), vec![11, 21, 31, 41]); - assert_eq!(crate::disabling_limit(Session::validators().len()), 1); + assert_eq!( + UpToByzantineThresholdDisablingStrategy::byzantine_threshold( + Session::validators().len() + ), + 1 + ); // we have 4 validators and an offending validator threshold of 1/3, // even if the third validator commits an offence a new era should not be forced From 5e8b367593822855d78632d973706331db5d9f69 Mon Sep 17 00:00:00 2001 From: Tsvetomir Dimitrov Date: Wed, 29 Nov 2023 17:59:39 +0200 Subject: [PATCH 19/67] Fix compilation errors after merge --- polkadot/node/core/backing/src/tests/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/polkadot/node/core/backing/src/tests/mod.rs b/polkadot/node/core/backing/src/tests/mod.rs index a748be11c758..a5e878caba31 100644 --- a/polkadot/node/core/backing/src/tests/mod.rs +++ b/polkadot/node/core/backing/src/tests/mod.rs @@ -2222,13 +2222,13 @@ fn validator_ignores_statements_from_disabled_validators() { candidate_receipt, pov: _pov, executor_params: _, - exec_timeout_kind: timeout, + exec_kind, response_sender: tx, }, ) if _pvd == pvd && _validation_code == validation_code && *_pov == pov && &candidate_receipt.descriptor == candidate.descriptor() && - timeout == PvfExecTimeoutKind::Backing && + exec_kind == PvfExecKind::Backing && candidate_commitments_hash == candidate_receipt.commitments_hash => { tx.send(Ok( From f2d4c48a867ab20cdf7b7e9ae8c89de1adb0ff28 Mon Sep 17 00:00:00 2001 From: Tsvetomir Dimitrov Date: Thu, 30 Nov 2023 13:16:48 +0200 Subject: [PATCH 20/67] Filter votes from disabled validators in `BackedCandidates` in process_inherent_data (#1863) Fixes https://github.com/paritytech/polkadot-sdk/issues/1592 Please refer to the issue for details. --------- Co-authored-by: ordian Co-authored-by: ordian Co-authored-by: Maciej --- .../src/runtime/parainherent.md | 31 +++ .../runtime/common/src/assigned_slots/mod.rs | 4 +- .../runtime/common/src/integration_tests.rs | 4 +- .../runtime/common/src/paras_registrar/mod.rs | 4 +- polkadot/runtime/parachains/src/mock.rs | 27 ++- .../parachains/src/paras_inherent/mod.rs | 181 ++++++++++++++-- .../parachains/src/paras_inherent/tests.rs | 193 ++++++++++++++++-- .../src/runtime_api_impl/vstaging.rs | 19 +- polkadot/runtime/parachains/src/scheduler.rs | 7 + polkadot/runtime/parachains/src/shared.rs | 42 +++- polkadot/runtime/rococo/src/lib.rs | 4 +- polkadot/runtime/test-runtime/src/lib.rs | 4 +- polkadot/runtime/westend/src/lib.rs | 4 +- polkadot/xcm/xcm-builder/tests/mock/mod.rs | 4 +- .../xcm-simulator/example/src/relay_chain.rs | 4 +- .../xcm-simulator/fuzzer/src/relay_chain.rs | 4 +- substrate/frame/aura/src/mock.rs | 4 + .../contracts/mock-network/src/relay_chain.rs | 4 +- substrate/frame/session/src/lib.rs | 4 + .../frame/support/src/traits/validation.rs | 7 + 20 files changed, 491 insertions(+), 64 deletions(-) diff --git a/polkadot/roadmap/implementers-guide/src/runtime/parainherent.md b/polkadot/roadmap/implementers-guide/src/runtime/parainherent.md index 4a771f1df644..9489a286a0cb 100644 --- a/polkadot/roadmap/implementers-guide/src/runtime/parainherent.md +++ b/polkadot/roadmap/implementers-guide/src/runtime/parainherent.md @@ -60,3 +60,34 @@ processing it, so the processed inherent data is simply dropped. This also means that the `enter` function keeps data around for no good reason. This seems acceptable though as the size of a block is rather limited. Nevertheless if we ever wanted to optimize this we can easily implement an inherent collector that has two implementations, where one clones and stores the data and the other just passes it on. + +## Data sanitization +`ParasInherent` with the entry point of `create_inherent` sanitizes the input data, while the `enter` entry point +enforces already sanitized input data. If unsanitized data is provided the module generates an error. + +Disputes are included in the block with a priority for a security reasons. It's important to include as many dispute +votes onchain as possible so that disputes conclude faster and the offenders are punished. However if there are too many +disputes to include in a block the dispute set is trimmed so that it respects max block weight. + +Dispute data is first deduplicated and sorted by block number (older first) and dispute location (local then remote). +Concluded and ancient (disputes initiated before the post conclusion acceptance period) disputes are filtered out. +Votes with invalid signatures or from unknown validators (not found in the active set for the current session) are also +filtered out. + +All dispute statements are included in the order described in the previous paragraph until the available block weight is +exhausted. After the dispute data is included all remaining weight is filled in with candidates and availability +bitfields. Bitfields are included with priority, then candidates containing code updates and finally any backed +candidates. If there is not enough weight for all backed candidates they are trimmed by random selection. Disputes are +processed in three separate functions - `deduplicate_and_sort_dispute_data`, `filter_dispute_data` and +`limit_and_sanitize_disputes`. + +Availability bitfields are also sanitized by dropping malformed ones, containing disputed cores or bad signatures. Refer +to `sanitize_bitfields` function for implementation details. + +Backed candidates sanitization removes malformed ones, candidates which have got concluded invalid disputes against them +or candidates produced by unassigned cores. Furthermore any backing votes from disabled validators for a candidate are +dropped. This is part of the validator disabling strategy. After filtering the statements from disabled validators a +backed candidate may end up with votes count less than `minimum_backing_votes` (a parameter from `HostConfiguiration`). +In this case the whole candidate is dropped otherwise it will be rejected by `process_candidates` from pallet inclusion. +All checks related to backed candidates are implemented in `sanitize_backed_candidates` and +`filter_backed_statements_from_disabled_validators`. diff --git a/polkadot/runtime/common/src/assigned_slots/mod.rs b/polkadot/runtime/common/src/assigned_slots/mod.rs index f5e3aaef6324..efd80a16744d 100644 --- a/polkadot/runtime/common/src/assigned_slots/mod.rs +++ b/polkadot/runtime/common/src/assigned_slots/mod.rs @@ -745,7 +745,9 @@ mod tests { type OnNewHead = (); } - impl parachains_shared::Config for Test {} + impl parachains_shared::Config for Test { + type DisabledValidators = (); + } parameter_types! { pub const LeasePeriod: BlockNumber = 3; diff --git a/polkadot/runtime/common/src/integration_tests.rs b/polkadot/runtime/common/src/integration_tests.rs index b0d277a702d6..825115d82905 100644 --- a/polkadot/runtime/common/src/integration_tests.rs +++ b/polkadot/runtime/common/src/integration_tests.rs @@ -197,7 +197,9 @@ impl configuration::Config for Test { type WeightInfo = configuration::TestWeightInfo; } -impl shared::Config for Test {} +impl shared::Config for Test { + type DisabledValidators = (); +} impl origin::Config for Test {} diff --git a/polkadot/runtime/common/src/paras_registrar/mod.rs b/polkadot/runtime/common/src/paras_registrar/mod.rs index 12376ae6f1ff..b966967f21fd 100644 --- a/polkadot/runtime/common/src/paras_registrar/mod.rs +++ b/polkadot/runtime/common/src/paras_registrar/mod.rs @@ -799,7 +799,9 @@ mod tests { type MaxFreezes = ConstU32<1>; } - impl shared::Config for Test {} + impl shared::Config for Test { + type DisabledValidators = (); + } impl origin::Config for Test {} diff --git a/polkadot/runtime/parachains/src/mock.rs b/polkadot/runtime/parachains/src/mock.rs index 222942922f91..75fb1ee1ad43 100644 --- a/polkadot/runtime/parachains/src/mock.rs +++ b/polkadot/runtime/parachains/src/mock.rs @@ -184,7 +184,22 @@ impl crate::configuration::Config for Test { type WeightInfo = crate::configuration::TestWeightInfo; } -impl crate::shared::Config for Test {} +pub struct MockDisabledValidators {} +impl frame_support::traits::DisabledValidators for MockDisabledValidators { + /// Returns true if the given validator is disabled. + fn is_disabled(index: u32) -> bool { + disabled_validators().iter().any(|v| *v == index) + } + + /// Returns a hardcoded list (`DISABLED_VALIDATORS`) of disabled validators + fn disabled_validators() -> Vec { + disabled_validators() + } +} + +impl crate::shared::Config for Test { + type DisabledValidators = MockDisabledValidators; +} impl origin::Config for Test {} @@ -432,6 +447,8 @@ thread_local! { pub static AVAILABILITY_REWARDS: RefCell> = RefCell::new(HashMap::new()); + + pub static DISABLED_VALIDATORS: RefCell> = RefCell::new(vec![]); } pub fn backing_rewards() -> HashMap { @@ -442,6 +459,10 @@ pub fn availability_rewards() -> HashMap { AVAILABILITY_REWARDS.with(|r| r.borrow().clone()) } +pub fn disabled_validators() -> Vec { + DISABLED_VALIDATORS.with(|r| r.borrow().clone()) +} + parameter_types! { pub static Processed: Vec<(ParaId, UpwardMessage)> = vec![]; } @@ -581,3 +602,7 @@ pub(crate) fn deregister_parachain(id: ParaId) { pub(crate) fn try_deregister_parachain(id: ParaId) -> crate::DispatchResult { frame_support::storage::transactional::with_storage_layer(|| Paras::schedule_para_cleanup(id)) } + +pub(crate) fn set_disabled_validators(disabled: Vec) { + DISABLED_VALIDATORS.with(|d| *d.borrow_mut() = disabled) +} diff --git a/polkadot/runtime/parachains/src/paras_inherent/mod.rs b/polkadot/runtime/parachains/src/paras_inherent/mod.rs index 8e918d35d5ff..7a4cb8ae3106 100644 --- a/polkadot/runtime/parachains/src/paras_inherent/mod.rs +++ b/polkadot/runtime/parachains/src/paras_inherent/mod.rs @@ -30,7 +30,8 @@ use crate::{ metrics::METRICS, paras, scheduler::{self, FreedReason}, - shared, ParaId, + shared::{self, AllowedRelayParentsTracker}, + ParaId, }; use bitvec::prelude::BitVec; use frame_support::{ @@ -42,8 +43,8 @@ use frame_support::{ use frame_system::pallet_prelude::*; use pallet_babe::{self, ParentBlockRandomness}; use primitives::{ - BackedCandidate, CandidateHash, CandidateReceipt, CheckedDisputeStatementSet, - CheckedMultiDisputeStatementSet, CoreIndex, DisputeStatementSet, + effective_minimum_backing_votes, BackedCandidate, CandidateHash, CandidateReceipt, + CheckedDisputeStatementSet, CheckedMultiDisputeStatementSet, CoreIndex, DisputeStatementSet, InherentData as ParachainsInherentData, MultiDisputeStatementSet, ScrapedOnChainVotes, SessionIndex, SignedAvailabilityBitfields, SigningContext, UncheckedSignedAvailabilityBitfield, UncheckedSignedAvailabilityBitfields, ValidatorId, ValidatorIndex, ValidityAttestation, @@ -142,6 +143,8 @@ pub mod pallet { DisputeStatementsUnsortedOrDuplicates, /// A dispute statement was invalid. DisputeInvalid, + /// A candidate was backed by a disabled validator + BackedByDisabled, } /// Whether the paras inherent was included within this block. @@ -378,6 +381,7 @@ impl Pallet { let bitfields_weight = signed_bitfields_weight::(&bitfields); let disputes_weight = multi_dispute_statement_sets_weight::(&disputes); + // Weight before filtering/sanitization let all_weight_before = candidates_weight + bitfields_weight + disputes_weight; METRICS.on_before_filter(all_weight_before.ref_time()); @@ -587,17 +591,19 @@ impl Pallet { METRICS.on_candidates_processed_total(backed_candidates.len() as u64); - let backed_candidates = sanitize_backed_candidates::( - backed_candidates, - |candidate_idx: usize, - backed_candidate: &BackedCandidate<::Hash>| - -> bool { - let para_id = backed_candidate.descriptor().para_id; - let prev_context = >::para_most_recent_context(para_id); - let check_ctx = CandidateCheckContext::::new(prev_context); - - // never include a concluded-invalid candidate - current_concluded_invalid_disputes.contains(&backed_candidate.hash()) || + let SanitizedBackedCandidates { backed_candidates, votes_from_disabled_were_dropped } = + sanitize_backed_candidates::( + backed_candidates, + &allowed_relay_parents, + |candidate_idx: usize, + backed_candidate: &BackedCandidate<::Hash>| + -> bool { + let para_id = backed_candidate.descriptor().para_id; + let prev_context = >::para_most_recent_context(para_id); + let check_ctx = CandidateCheckContext::::new(prev_context); + + // never include a concluded-invalid candidate + current_concluded_invalid_disputes.contains(&backed_candidate.hash()) || // Instead of checking the candidates with code upgrades twice // move the checking up here and skip it in the training wheels fallback. // That way we avoid possible duplicate checks while assuring all @@ -607,12 +613,19 @@ impl Pallet { check_ctx .verify_backed_candidate(&allowed_relay_parents, candidate_idx, backed_candidate) .is_err() - }, - &scheduled, - ); + }, + &scheduled, + ); METRICS.on_candidates_sanitized(backed_candidates.len() as u64); + // In `Enter` context (invoked during execution) there should be no backing votes from + // disabled validators because they should have been filtered out during inherent data + // preparation (`ProvideInherent` context). Abort in such cases. + if context == ProcessInherentDataContext::Enter { + ensure!(!votes_from_disabled_were_dropped, Error::::BackedByDisabled); + } + // Process backed candidates according to scheduled cores. let inclusion::ProcessedCandidates::< as HeaderT>::Hash> { core_indices: occupied, @@ -900,7 +913,19 @@ pub(crate) fn sanitize_bitfields( bitfields } -/// Filter out any candidates that have a concluded invalid dispute. +// Result from `sanitize_backed_candidates` +#[derive(Debug, PartialEq)] +struct SanitizedBackedCandidates { + // Sanitized backed candidates. The `Vec` is sorted according to the occupied core index. + backed_candidates: Vec>, + // Set to true if any votes from disabled validators were dropped from the input. + votes_from_disabled_were_dropped: bool, +} + +/// Filter out: +/// 1. any candidates that have a concluded invalid dispute +/// 2. all backing votes from disabled validators +/// 3. any candidates that end up with less than `effective_minimum_backing_votes` backing votes /// /// `scheduled` follows the same naming scheme as provided in the /// guide: Currently `free` but might become `occupied`. @@ -910,15 +935,17 @@ pub(crate) fn sanitize_bitfields( /// `candidate_has_concluded_invalid_dispute` must return `true` if the candidate /// is disputed, false otherwise. The passed `usize` is the candidate index. /// -/// The returned `Vec` is sorted according to the occupied core index. +/// Returns struct `SanitizedBackedCandidates` where `backed_candidates` are sorted according to the +/// occupied core index. fn sanitize_backed_candidates< T: crate::inclusion::Config, F: FnMut(usize, &BackedCandidate) -> bool, >( mut backed_candidates: Vec>, + allowed_relay_parents: &AllowedRelayParentsTracker>, mut candidate_has_concluded_invalid_dispute_or_is_invalid: F, scheduled: &BTreeMap, -) -> Vec> { +) -> SanitizedBackedCandidates { // Remove any candidates that were concluded invalid. // This does not assume sorting. backed_candidates.indexed_retain(move |candidate_idx, backed_candidate| { @@ -936,6 +963,13 @@ fn sanitize_backed_candidates< scheduled.get(&desc.para_id).is_some() }); + // Filter out backing statements from disabled validators + let dropped_disabled = filter_backed_statements_from_disabled_validators::( + &mut backed_candidates, + &allowed_relay_parents, + scheduled, + ); + // Sort the `Vec` last, once there is a guarantee that these // `BackedCandidates` references the expected relay chain parent, // but more importantly are scheduled for a free core. @@ -946,7 +980,10 @@ fn sanitize_backed_candidates< scheduled[&x.descriptor().para_id].cmp(&scheduled[&y.descriptor().para_id]) }); - backed_candidates + SanitizedBackedCandidates { + backed_candidates, + votes_from_disabled_were_dropped: dropped_disabled, + } } /// Derive entropy from babe provided per block randomness. @@ -1029,3 +1066,105 @@ fn limit_and_sanitize_disputes< (checked, checked_disputes_weight) } } + +// Filters statements from disabled validators in `BackedCandidate`, non-scheduled candidates and +// few more sanity checks. Returns `true` if at least one statement is removed and `false` +// otherwise. +fn filter_backed_statements_from_disabled_validators( + backed_candidates: &mut Vec::Hash>>, + allowed_relay_parents: &AllowedRelayParentsTracker>, + scheduled: &BTreeMap, +) -> bool { + let disabled_validators = + BTreeSet::<_>::from_iter(shared::Pallet::::disabled_validators().into_iter()); + + if disabled_validators.is_empty() { + // No disabled validators - nothing to do + return false + } + + let backed_len_before = backed_candidates.len(); + + // Flag which will be returned. Set to `true` if at least one vote is filtered. + let mut filtered = false; + + let minimum_backing_votes = configuration::Pallet::::config().minimum_backing_votes; + + // Process all backed candidates. `validator_indices` in `BackedCandidates` are indices within + // the validator group assigned to the parachain. To obtain this group we need: + // 1. Core index assigned to the parachain which has produced the candidate + // 2. The relay chain block number of the candidate + backed_candidates.retain_mut(|bc| { + // Get `core_idx` assigned to the `para_id` of the candidate + let core_idx = match scheduled.get(&bc.descriptor().para_id) { + Some(core_idx) => *core_idx, + None => { + log::debug!(target: LOG_TARGET, "Can't get core idx of a backed candidate for para id {:?}. Dropping the candidate.", bc.descriptor().para_id); + return false + } + }; + + // Get relay parent block number of the candidate. We need this to get the group index assigned to this core at this block number + let relay_parent_block_number = match allowed_relay_parents + .acquire_info(bc.descriptor().relay_parent, None) { + Some((_, block_num)) => block_num, + None => { + log::debug!(target: LOG_TARGET, "Relay parent {:?} for candidate is not in the allowed relay parents. Dropping the candidate.", bc.descriptor().relay_parent); + return false + } + }; + + // Get the group index for the core + let group_idx = match >::group_assigned_to_core( + core_idx, + relay_parent_block_number + One::one(), + ) { + Some(group_idx) => group_idx, + None => { + log::debug!(target: LOG_TARGET, "Can't get the group index for core idx {:?}. Dropping the candidate.", core_idx); + return false + }, + }; + + // And finally get the validator group for this group index + let validator_group = match >::group_validators(group_idx) { + Some(validator_group) => validator_group, + None => { + log::debug!(target: LOG_TARGET, "Can't get the validators from group {:?}. Dropping the candidate.", group_idx); + return false + } + }; + + // Bitmask with the disabled indices within the validator group + let disabled_indices = BitVec::::from_iter(validator_group.iter().map(|idx| disabled_validators.contains(idx))); + // The indices of statements from disabled validators in `BackedCandidate`. We have to drop these. + let indices_to_drop = disabled_indices.clone() & &bc.validator_indices; + // Apply the bitmask to drop the disabled validator from `validator_indices` + bc.validator_indices &= !disabled_indices; + // Remove the corresponding votes from `validity_votes` + for idx in indices_to_drop.iter_ones().rev() { + bc.validity_votes.remove(idx); + } + + // If at least one statement was dropped we need to return `true` + if indices_to_drop.count_ones() > 0 { + filtered = true; + } + + // By filtering votes we might render the candidate invalid and cause a failure in + // [`process_candidates`]. To avoid this we have to perform a sanity check here. If there + // are not enough backing votes after filtering we will remove the whole candidate. + if bc.validity_votes.len() < effective_minimum_backing_votes( + validator_group.len(), + minimum_backing_votes + + ) { + return false + } + + true + }); + + // Also return `true` if a whole candidate was dropped from the set + filtered || backed_len_before != backed_candidates.len() +} diff --git a/polkadot/runtime/parachains/src/paras_inherent/tests.rs b/polkadot/runtime/parachains/src/paras_inherent/tests.rs index 4fc60792e346..364c6192bebe 100644 --- a/polkadot/runtime/parachains/src/paras_inherent/tests.rs +++ b/polkadot/runtime/parachains/src/paras_inherent/tests.rs @@ -1206,6 +1206,12 @@ mod sanitizers { } mod candidates { + use crate::{ + mock::set_disabled_validators, + scheduler::{common::Assignment, ParasEntry}, + }; + use sp_std::collections::vec_deque::VecDeque; + use super::*; // Backed candidates and scheduled parachains used for `sanitize_backed_candidates` testing @@ -1214,10 +1220,20 @@ mod sanitizers { scheduled_paras: BTreeMap, } - // Generate test data for the candidates test + // Generate test data for the candidates and assert that the evnironment is set as expected + // (check the comments for details) fn get_test_data() -> TestData { const RELAY_PARENT_NUM: u32 = 3; + // Add the relay parent to `shared` pallet. Otherwise some code (e.g. filtering backing + // votes) won't behave correctly + shared::Pallet::::add_allowed_relay_parent( + default_header().hash(), + Default::default(), + RELAY_PARENT_NUM, + 1, + ); + let header = default_header(); let relay_parent = header.hash(); let session_index = SessionIndex::from(0_u32); @@ -1231,6 +1247,7 @@ mod sanitizers { keyring::Sr25519Keyring::Bob, keyring::Sr25519Keyring::Charlie, keyring::Sr25519Keyring::Dave, + keyring::Sr25519Keyring::Eve, ]; for validator in validators.iter() { Keystore::sr25519_generate_new( @@ -1241,11 +1258,36 @@ mod sanitizers { .unwrap(); } + // Set active validators in `shared` pallet + let validator_ids = + validators.iter().map(|v| v.public().into()).collect::>(); + shared::Pallet::::set_active_validators_ascending(validator_ids); + + // Two scheduled parachains - ParaId(1) on CoreIndex(0) and ParaId(2) on CoreIndex(1) let scheduled = (0_usize..2) .into_iter() .map(|idx| (ParaId::from(1_u32 + idx as u32), CoreIndex::from(idx as u32))) .collect::>(); + // Set the validator groups in `scheduler` + scheduler::Pallet::::set_validator_groups(vec![ + vec![ValidatorIndex(0), ValidatorIndex(1)], + vec![ValidatorIndex(2), ValidatorIndex(3)], + ]); + + // Update scheduler's claimqueue with the parachains + scheduler::Pallet::::set_claimqueue(BTreeMap::from([ + ( + CoreIndex::from(0), + VecDeque::from([Some(ParasEntry::new(Assignment::new(1.into()), 1))]), + ), + ( + CoreIndex::from(1), + VecDeque::from([Some(ParasEntry::new(Assignment::new(2.into()), 1))]), + ), + ])); + + // Callback used for backing candidates let group_validators = |group_index: GroupIndex| { match group_index { group_index if group_index == GroupIndex::from(0) => Some(vec![0, 1]), @@ -1255,6 +1297,7 @@ mod sanitizers { .map(|m| m.into_iter().map(ValidatorIndex).collect::>()) }; + // Two backed candidates from each parachain let backed_candidates = (0_usize..2) .into_iter() .map(|idx0| { @@ -1283,6 +1326,22 @@ mod sanitizers { }) .collect::>(); + // State sanity checks + assert_eq!( + >::scheduled_paras().collect::>(), + vec![(CoreIndex(0), ParaId::from(1)), (CoreIndex(1), ParaId::from(2))] + ); + assert_eq!( + shared::Pallet::::active_validator_indices(), + vec![ + ValidatorIndex(0), + ValidatorIndex(1), + ValidatorIndex(2), + ValidatorIndex(3), + ValidatorIndex(4) + ] + ); + TestData { backed_candidates, scheduled_paras: scheduled } } @@ -1297,10 +1356,14 @@ mod sanitizers { assert_eq!( sanitize_backed_candidates::( backed_candidates.clone(), + &>::allowed_relay_parents(), has_concluded_invalid, &scheduled ), - backed_candidates + SanitizedBackedCandidates { + backed_candidates, + votes_from_disabled_were_dropped: false + } ); {} @@ -1316,12 +1379,18 @@ mod sanitizers { let has_concluded_invalid = |_idx: usize, _backed_candidate: &BackedCandidate| -> bool { false }; - assert!(sanitize_backed_candidates::( + let SanitizedBackedCandidates { + backed_candidates: sanitized_backed_candidates, + votes_from_disabled_were_dropped, + } = sanitize_backed_candidates::( backed_candidates.clone(), + &>::allowed_relay_parents(), has_concluded_invalid, - &scheduled - ) - .is_empty()); + &scheduled, + ); + + assert!(sanitized_backed_candidates.is_empty()); + assert!(!votes_from_disabled_were_dropped); }); } @@ -1343,15 +1412,113 @@ mod sanitizers { }; let has_concluded_invalid = |_idx: usize, candidate: &BackedCandidate| set.contains(&candidate.hash()); + let SanitizedBackedCandidates { + backed_candidates: sanitized_backed_candidates, + votes_from_disabled_were_dropped, + } = sanitize_backed_candidates::( + backed_candidates.clone(), + &>::allowed_relay_parents(), + has_concluded_invalid, + &scheduled, + ); + + assert_eq!(sanitized_backed_candidates.len(), backed_candidates.len() / 2); + assert!(!votes_from_disabled_were_dropped); + }); + } + + #[test] + fn disabled_non_signing_validator_doesnt_get_filtered() { + new_test_ext(MockGenesisConfig::default()).execute_with(|| { + let TestData { mut backed_candidates, scheduled_paras } = get_test_data(); + + // Disable Eve + set_disabled_validators(vec![4]); + + let before = backed_candidates.clone(); + + // Eve is disabled but no backing statement is signed by it so nothing should be + // filtered + assert!(!filter_backed_statements_from_disabled_validators::( + &mut backed_candidates, + &>::allowed_relay_parents(), + &scheduled_paras + )); + assert_eq!(backed_candidates, before); + }); + } + + #[test] + fn drop_statements_from_disabled_without_dropping_candidate() { + new_test_ext(MockGenesisConfig::default()).execute_with(|| { + let TestData { mut backed_candidates, scheduled_paras } = get_test_data(); + + // Disable Alice + set_disabled_validators(vec![0]); + + // Update `minimum_backing_votes` in HostConfig. We want `minimum_backing_votes` set + // to one so that the candidate will have enough backing votes even after dropping + // Alice's one. + let mut hc = configuration::Pallet::::config(); + hc.minimum_backing_votes = 1; + configuration::Pallet::::force_set_active_config(hc); + + // Verify the initial state is as expected + assert_eq!(backed_candidates.get(0).unwrap().validity_votes.len(), 2); assert_eq!( - sanitize_backed_candidates::( - backed_candidates.clone(), - has_concluded_invalid, - &scheduled - ) - .len(), - backed_candidates.len() / 2 + backed_candidates.get(0).unwrap().validator_indices.get(0).unwrap(), + true + ); + assert_eq!( + backed_candidates.get(0).unwrap().validator_indices.get(1).unwrap(), + true + ); + let untouched = backed_candidates.get(1).unwrap().clone(); + + assert!(filter_backed_statements_from_disabled_validators::( + &mut backed_candidates, + &>::allowed_relay_parents(), + &scheduled_paras + )); + + // there should still be two backed candidates + assert_eq!(backed_candidates.len(), 2); + // but the first one should have only one validity vote + assert_eq!(backed_candidates.get(0).unwrap().validity_votes.len(), 1); + // Validator 0 vote should be dropped, validator 1 - retained + assert_eq!( + backed_candidates.get(0).unwrap().validator_indices.get(0).unwrap(), + false ); + assert_eq!( + backed_candidates.get(0).unwrap().validator_indices.get(1).unwrap(), + true + ); + // the second candidate shouldn't be modified + assert_eq!(*backed_candidates.get(1).unwrap(), untouched); + }); + } + + #[test] + fn drop_candidate_if_all_statements_are_from_disabled() { + new_test_ext(MockGenesisConfig::default()).execute_with(|| { + let TestData { mut backed_candidates, scheduled_paras } = get_test_data(); + + // Disable Alice and Bob + set_disabled_validators(vec![0, 1]); + + // Verify the initial state is as expected + assert_eq!(backed_candidates.get(0).unwrap().validity_votes.len(), 2); + let untouched = backed_candidates.get(1).unwrap().clone(); + + assert!(filter_backed_statements_from_disabled_validators::( + &mut backed_candidates, + &>::allowed_relay_parents(), + &scheduled_paras + )); + + assert_eq!(backed_candidates.len(), 1); + assert_eq!(*backed_candidates.get(0).unwrap(), untouched); }); } } diff --git a/polkadot/runtime/parachains/src/runtime_api_impl/vstaging.rs b/polkadot/runtime/parachains/src/runtime_api_impl/vstaging.rs index 200fd57915f9..6aa455a7d0c7 100644 --- a/polkadot/runtime/parachains/src/runtime_api_impl/vstaging.rs +++ b/polkadot/runtime/parachains/src/runtime_api_impl/vstaging.rs @@ -18,29 +18,16 @@ use crate::{configuration, initializer, shared}; use primitives::{vstaging::NodeFeatures, ValidatorIndex}; -use sp_std::{collections::btree_map::BTreeMap, prelude::Vec}; +use sp_std::prelude::Vec; /// Implementation for `DisabledValidators` // CAVEAT: this should only be called on the node side // as it might produce incorrect results on session boundaries pub fn disabled_validators() -> Vec where - T: pallet_session::Config + shared::Config, + T: shared::Config, { - let shuffled_indices = >::active_validator_indices(); - // mapping from raw validator index to `ValidatorIndex` - // this computation is the same within a session, but should be cheap - let reverse_index = shuffled_indices - .iter() - .enumerate() - .map(|(i, v)| (v.0, ValidatorIndex(i as u32))) - .collect::>(); - - // we might have disabled validators who are not parachain validators - >::disabled_validators() - .iter() - .filter_map(|v| reverse_index.get(v).cloned()) - .collect() + >::disabled_validators() } /// Returns the current state of the node features. diff --git a/polkadot/runtime/parachains/src/scheduler.rs b/polkadot/runtime/parachains/src/scheduler.rs index b81b68b5745e..e3534ff9c1d2 100644 --- a/polkadot/runtime/parachains/src/scheduler.rs +++ b/polkadot/runtime/parachains/src/scheduler.rs @@ -743,4 +743,11 @@ impl Pallet { pub(crate) fn set_validator_groups(validator_groups: Vec>) { ValidatorGroups::::set(validator_groups); } + + #[cfg(test)] + pub(crate) fn set_claimqueue( + claimqueue: BTreeMap>>>>, + ) { + ClaimQueue::::set(claimqueue); + } } diff --git a/polkadot/runtime/parachains/src/shared.rs b/polkadot/runtime/parachains/src/shared.rs index ad13c9e48448..bdaffcd505f8 100644 --- a/polkadot/runtime/parachains/src/shared.rs +++ b/polkadot/runtime/parachains/src/shared.rs @@ -19,11 +19,14 @@ //! To avoid cyclic dependencies, it is important that this pallet is not //! dependent on any of the other pallets. -use frame_support::pallet_prelude::*; +use frame_support::{pallet_prelude::*, traits::DisabledValidators}; use frame_system::pallet_prelude::BlockNumberFor; use primitives::{SessionIndex, ValidatorId, ValidatorIndex}; use sp_runtime::traits::AtLeast32BitUnsigned; -use sp_std::{collections::vec_deque::VecDeque, vec::Vec}; +use sp_std::{ + collections::{btree_map::BTreeMap, vec_deque::VecDeque}, + vec::Vec, +}; use rand::{seq::SliceRandom, SeedableRng}; use rand_chacha::ChaCha20Rng; @@ -129,7 +132,9 @@ pub mod pallet { pub struct Pallet(_); #[pallet::config] - pub trait Config: frame_system::Config {} + pub trait Config: frame_system::Config { + type DisabledValidators: frame_support::traits::DisabledValidators; + } /// The current session index. #[pallet::storage] @@ -216,6 +221,25 @@ impl Pallet { Self::session_index().saturating_add(SESSION_DELAY) } + /// Fetches disabled validators list from session pallet. + /// CAVEAT: this might produce incorrect results on session boundaries + pub fn disabled_validators() -> Vec { + let shuffled_indices = Pallet::::active_validator_indices(); + // mapping from raw validator index to `ValidatorIndex` + // this computation is the same within a session, but should be cheap + let reverse_index = shuffled_indices + .iter() + .enumerate() + .map(|(i, v)| (v.0, ValidatorIndex(i as u32))) + .collect::>(); + + // we might have disabled validators who are not parachain validators + T::DisabledValidators::disabled_validators() + .iter() + .filter_map(|v| reverse_index.get(v).cloned()) + .collect() + } + /// Test function for setting the current session index. #[cfg(any(feature = "std", feature = "runtime-benchmarks", test))] pub fn set_session_index(index: SessionIndex) { @@ -239,4 +263,16 @@ impl Pallet { ActiveValidatorIndices::::set(indices); ActiveValidatorKeys::::set(keys); } + + #[cfg(test)] + pub(crate) fn add_allowed_relay_parent( + relay_parent: T::Hash, + state_root: T::Hash, + number: BlockNumberFor, + max_ancestry_len: u32, + ) { + AllowedRelayParents::::mutate(|tracker| { + tracker.update(relay_parent, state_root, number, max_ancestry_len) + }) + } } diff --git a/polkadot/runtime/rococo/src/lib.rs b/polkadot/runtime/rococo/src/lib.rs index 5ac92a737324..2593cd9ca106 100644 --- a/polkadot/runtime/rococo/src/lib.rs +++ b/polkadot/runtime/rococo/src/lib.rs @@ -911,7 +911,9 @@ impl parachains_configuration::Config for Runtime { type WeightInfo = weights::runtime_parachains_configuration::WeightInfo; } -impl parachains_shared::Config for Runtime {} +impl parachains_shared::Config for Runtime { + type DisabledValidators = Session; +} impl parachains_session_info::Config for Runtime { type ValidatorSet = Historical; diff --git a/polkadot/runtime/test-runtime/src/lib.rs b/polkadot/runtime/test-runtime/src/lib.rs index 596e65eca068..0abc780dee86 100644 --- a/polkadot/runtime/test-runtime/src/lib.rs +++ b/polkadot/runtime/test-runtime/src/lib.rs @@ -486,7 +486,9 @@ impl parachains_configuration::Config for Runtime { type WeightInfo = parachains_configuration::TestWeightInfo; } -impl parachains_shared::Config for Runtime {} +impl parachains_shared::Config for Runtime { + type DisabledValidators = Session; +} impl parachains_inclusion::Config for Runtime { type RuntimeEvent = RuntimeEvent; diff --git a/polkadot/runtime/westend/src/lib.rs b/polkadot/runtime/westend/src/lib.rs index f7ff2d5e9e1b..4211c073cbfd 100644 --- a/polkadot/runtime/westend/src/lib.rs +++ b/polkadot/runtime/westend/src/lib.rs @@ -1130,7 +1130,9 @@ impl parachains_configuration::Config for Runtime { type WeightInfo = weights::runtime_parachains_configuration::WeightInfo; } -impl parachains_shared::Config for Runtime {} +impl parachains_shared::Config for Runtime { + type DisabledValidators = Session; +} impl parachains_session_info::Config for Runtime { type ValidatorSet = Historical; diff --git a/polkadot/xcm/xcm-builder/tests/mock/mod.rs b/polkadot/xcm/xcm-builder/tests/mock/mod.rs index 968b294c6a43..5427bffe32e9 100644 --- a/polkadot/xcm/xcm-builder/tests/mock/mod.rs +++ b/polkadot/xcm/xcm-builder/tests/mock/mod.rs @@ -126,7 +126,9 @@ impl pallet_balances::Config for Runtime { type MaxFreezes = ConstU32<0>; } -impl shared::Config for Runtime {} +impl shared::Config for Runtime { + type DisabledValidators = (); +} impl configuration::Config for Runtime { type WeightInfo = configuration::TestWeightInfo; diff --git a/polkadot/xcm/xcm-simulator/example/src/relay_chain.rs b/polkadot/xcm/xcm-simulator/example/src/relay_chain.rs index 20070d192b54..1d1ee385d31e 100644 --- a/polkadot/xcm/xcm-simulator/example/src/relay_chain.rs +++ b/polkadot/xcm/xcm-simulator/example/src/relay_chain.rs @@ -120,7 +120,9 @@ impl pallet_uniques::Config for Runtime { type Helper = (); } -impl shared::Config for Runtime {} +impl shared::Config for Runtime { + type DisabledValidators = (); +} impl configuration::Config for Runtime { type WeightInfo = configuration::TestWeightInfo; diff --git a/polkadot/xcm/xcm-simulator/fuzzer/src/relay_chain.rs b/polkadot/xcm/xcm-simulator/fuzzer/src/relay_chain.rs index 085773f30737..572cee3db536 100644 --- a/polkadot/xcm/xcm-simulator/fuzzer/src/relay_chain.rs +++ b/polkadot/xcm/xcm-simulator/fuzzer/src/relay_chain.rs @@ -98,7 +98,9 @@ impl pallet_balances::Config for Runtime { type MaxFreezes = ConstU32<0>; } -impl shared::Config for Runtime {} +impl shared::Config for Runtime { + type DisabledValidators = (); +} impl configuration::Config for Runtime { type WeightInfo = configuration::TestWeightInfo; diff --git a/substrate/frame/aura/src/mock.rs b/substrate/frame/aura/src/mock.rs index 14b87089ce39..d38a8583819e 100644 --- a/substrate/frame/aura/src/mock.rs +++ b/substrate/frame/aura/src/mock.rs @@ -96,6 +96,10 @@ impl DisabledValidators for MockDisabledValidators { fn is_disabled(index: AuthorityIndex) -> bool { DisabledValidatorTestValue::get().binary_search(&index).is_ok() } + + fn disabled_validators() -> Vec { + DisabledValidatorTestValue::get() + } } impl pallet_aura::Config for Test { diff --git a/substrate/frame/contracts/mock-network/src/relay_chain.rs b/substrate/frame/contracts/mock-network/src/relay_chain.rs index 17e36eada259..cc6b2953a666 100644 --- a/substrate/frame/contracts/mock-network/src/relay_chain.rs +++ b/substrate/frame/contracts/mock-network/src/relay_chain.rs @@ -97,7 +97,9 @@ impl pallet_balances::Config for Runtime { type RuntimeFreezeReason = RuntimeFreezeReason; } -impl shared::Config for Runtime {} +impl shared::Config for Runtime { + type DisabledValidators = (); +} impl configuration::Config for Runtime { type WeightInfo = configuration::TestWeightInfo; diff --git a/substrate/frame/session/src/lib.rs b/substrate/frame/session/src/lib.rs index bf4671a247f0..178d43f596b2 100644 --- a/substrate/frame/session/src/lib.rs +++ b/substrate/frame/session/src/lib.rs @@ -918,6 +918,10 @@ impl frame_support::traits::DisabledValidators for Pallet { fn is_disabled(index: u32) -> bool { >::disabled_validators().binary_search(&index).is_ok() } + + fn disabled_validators() -> Vec { + >::disabled_validators() + } } /// Wraps the author-scraping logic for consensus engines that can recover diff --git a/substrate/frame/support/src/traits/validation.rs b/substrate/frame/support/src/traits/validation.rs index 617cdb2d3f46..4b099b2c766f 100644 --- a/substrate/frame/support/src/traits/validation.rs +++ b/substrate/frame/support/src/traits/validation.rs @@ -251,10 +251,17 @@ pub trait ValidatorRegistration { pub trait DisabledValidators { /// Returns true if the given validator is disabled. fn is_disabled(index: u32) -> bool; + + /// Returns all disabled validators + fn disabled_validators() -> Vec; } impl DisabledValidators for () { fn is_disabled(_index: u32) -> bool { false } + + fn disabled_validators() -> Vec { + vec![] + } } From 9cb65f8bd9b858b368af3e5d7c50b9fbdd2d424b Mon Sep 17 00:00:00 2001 From: Tsvetomir Dimitrov Date: Tue, 5 Dec 2023 14:04:56 +0200 Subject: [PATCH 21/67] Fix a typo --- substrate/frame/staking/src/pallet/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/substrate/frame/staking/src/pallet/mod.rs b/substrate/frame/staking/src/pallet/mod.rs index 27d335f2b8cb..caf829134eee 100644 --- a/substrate/frame/staking/src/pallet/mod.rs +++ b/substrate/frame/staking/src/pallet/mod.rs @@ -1111,7 +1111,7 @@ pub mod pallet { /// this call results in a complete removal of all the data related to the stash account. /// In this case, the `num_slashing_spans` must be larger or equal to the number of /// slashing spans associated with the stash account in the [`SlashingSpans`] storage type, - /// otherwise the call will fail. The call weight is directly propotional to + /// otherwise the call will fail. The call weight is directly proportional to /// `num_slashing_spans`. /// /// ## Complexity From ed203d8a72a41fb7bf3b779741e9de26f927dfe1 Mon Sep 17 00:00:00 2001 From: Tsvetomir Dimitrov Date: Thu, 7 Dec 2023 14:22:18 +0200 Subject: [PATCH 22/67] Do not chill validators on offence --- substrate/frame/staking/src/slashing.rs | 9 +- substrate/frame/staking/src/tests.rs | 204 +++++++++++++----------- 2 files changed, 110 insertions(+), 103 deletions(-) diff --git a/substrate/frame/staking/src/slashing.rs b/substrate/frame/staking/src/slashing.rs index 7ca72f42593a..f7a8d3c8f38a 100644 --- a/substrate/frame/staking/src/slashing.rs +++ b/substrate/frame/staking/src/slashing.rs @@ -273,13 +273,8 @@ pub(crate) fn compute_slash( let target_span = spans.compare_and_update_span_slash(params.slash_era, own_slash); if target_span == Some(spans.span_index()) { - // misbehavior occurred within the current slashing span - take appropriate - // actions. - - // chill the validator - it misbehaved in the current span and should - // not continue in the next election. also end the slashing span. + // Check https://github.com/paritytech/polkadot-sdk/issues/2650 for details spans.end_span(params.now); - >::chill_stash(params.stash); } } @@ -312,8 +307,8 @@ fn kick_out_if_recent(params: SlashParams) { ); if spans.era_span(params.slash_era).map(|s| s.index) == Some(spans.span_index()) { + // Check https://github.com/paritytech/polkadot-sdk/issues/2650 for details spans.end_span(params.now); - >::chill_stash(params.stash); } add_offending_validator::(params.stash); diff --git a/substrate/frame/staking/src/tests.rs b/substrate/frame/staking/src/tests.rs index b4127926640b..48df3d7516e4 100644 --- a/substrate/frame/staking/src/tests.rs +++ b/substrate/frame/staking/src/tests.rs @@ -2357,26 +2357,32 @@ fn offence_ensures_new_era_without_clobbering() { #[test] fn offence_deselects_validator_even_when_slash_is_zero() { - ExtBuilder::default().build_and_execute(|| { - assert!(Session::validators().contains(&11)); - assert!(>::contains_key(11)); + ExtBuilder::default() + .validator_count(7) + .set_status(41, StakerStatus::Validator) + .set_status(51, StakerStatus::Validator) + .set_status(201, StakerStatus::Validator) + .set_status(202, StakerStatus::Validator) + .build_and_execute(|| { + assert!(Session::validators().contains(&11)); + assert!(>::contains_key(11)); - on_offence_now( - &[OffenceDetails { - offender: (11, Staking::eras_stakers(active_era(), &11)), - reporters: vec![], - }], - &[Perbill::from_percent(0)], - ); + on_offence_now( + &[OffenceDetails { + offender: (11, Staking::eras_stakers(active_era(), &11)), + reporters: vec![], + }], + &[Perbill::from_percent(0)], + ); - assert_eq!(Staking::force_era(), Forcing::NotForcing); - assert!(!>::contains_key(11)); + assert_eq!(Staking::force_era(), Forcing::NotForcing); + assert!(is_disabled(11)); - mock::start_active_era(1); + mock::start_active_era(1); - assert!(!Session::validators().contains(&11)); - assert!(!>::contains_key(11)); - }); + // The validator should be reenabled in the new era + assert!(!is_disabled(11)); + }); } #[test] @@ -2420,16 +2426,16 @@ fn slash_in_old_span_does_not_deselect() { ); assert_eq!(Staking::force_era(), Forcing::NotForcing); - assert!(!>::contains_key(11)); + assert!(is_disabled(11)); mock::start_active_era(2); - Staking::validate(RuntimeOrigin::signed(11), Default::default()).unwrap(); - assert_eq!(Staking::force_era(), Forcing::NotForcing); - assert!(>::contains_key(11)); - assert!(!Session::validators().contains(&11)); - - mock::start_active_era(3); + // Staking::validate(RuntimeOrigin::signed(11), Default::default()).unwrap(); + // assert_eq!(Staking::force_era(), Forcing::NotForcing); + // assert!(>::contains_key(11)); + // assert!(!Session::validators().contains(&11)); + // + // mock::start_active_era(3); // this staker is in a new slashing span now, having re-registered after // their prior slash. @@ -2919,7 +2925,6 @@ fn deferred_slashes_are_deferred() { assert!(matches!( staking_events_since_last_call().as_slice(), &[ - Event::Chilled { stash: 11 }, Event::SlashReported { validator: 11, slash_era: 1, .. }, Event::StakersElected, .., @@ -2954,7 +2959,6 @@ fn retroactive_deferred_slashes_two_eras_before() { assert!(matches!( staking_events_since_last_call().as_slice(), &[ - Event::Chilled { stash: 11 }, Event::SlashReported { validator: 11, slash_era: 1, .. }, .., Event::Slashed { staker: 11, amount: 100 }, @@ -3226,67 +3230,72 @@ fn remove_multi_deferred() { #[test] fn slash_kicks_validators_not_nominators_and_disables_nominator_for_kicked_validator() { - ExtBuilder::default().build_and_execute(|| { - mock::start_active_era(1); - assert_eq_uvec!(Session::validators(), vec![11, 21]); - - // pre-slash balance - assert_eq!(Balances::free_balance(11), 1000); - assert_eq!(Balances::free_balance(101), 2000); + ExtBuilder::default() + .validator_count(7) + .set_status(41, StakerStatus::Validator) + .set_status(51, StakerStatus::Validator) + .set_status(201, StakerStatus::Validator) + .set_status(202, StakerStatus::Validator) + .build_and_execute(|| { + mock::start_active_era(1); + assert_eq_uvec!(Session::validators(), vec![11, 21, 31, 41, 51, 201, 202]); - // 100 has approval for 11 as of now - assert!(Staking::nominators(101).unwrap().targets.contains(&11)); + // pre-slash balance + assert_eq!(Balances::free_balance(11), 1000); + assert_eq!(Balances::free_balance(101), 2000); - // 11 and 21 both have the support of 100 - let exposure_11 = Staking::eras_stakers(active_era(), &11); - let exposure_21 = Staking::eras_stakers(active_era(), &21); + // 100 has approval for 11 as of now + assert!(Staking::nominators(101).unwrap().targets.contains(&11)); - assert_eq!(exposure_11.total, 1000 + 125); - assert_eq!(exposure_21.total, 1000 + 375); + // 11 and 21 both have the support of 100 + let exposure_11 = Staking::eras_stakers(active_era(), &11); + let exposure_21 = Staking::eras_stakers(active_era(), &21); - on_offence_now( - &[OffenceDetails { offender: (11, exposure_11.clone()), reporters: vec![] }], - &[Perbill::from_percent(10)], - ); + assert_eq!(exposure_11.total, 1000 + 125); + assert_eq!(exposure_21.total, 1000 + 375); - assert_eq!( - staking_events_since_last_call(), - vec![ - Event::StakersElected, - Event::EraPaid { era_index: 0, validator_payout: 11075, remainder: 33225 }, - Event::Chilled { stash: 11 }, - Event::SlashReported { - validator: 11, - fraction: Perbill::from_percent(10), - slash_era: 1 - }, - Event::Slashed { staker: 11, amount: 100 }, - Event::Slashed { staker: 101, amount: 12 }, - ] - ); + on_offence_now( + &[OffenceDetails { offender: (11, exposure_11.clone()), reporters: vec![] }], + &[Perbill::from_percent(10)], + ); - // post-slash balance - let nominator_slash_amount_11 = 125 / 10; - assert_eq!(Balances::free_balance(11), 900); - assert_eq!(Balances::free_balance(101), 2000 - nominator_slash_amount_11); + assert_eq!( + staking_events_since_last_call(), + vec![ + Event::StakersElected, + Event::EraPaid { era_index: 0, validator_payout: 11075, remainder: 33225 }, + Event::SlashReported { + validator: 11, + fraction: Perbill::from_percent(10), + slash_era: 1 + }, + Event::Slashed { staker: 11, amount: 100 }, + Event::Slashed { staker: 101, amount: 12 }, + ] + ); - // check that validator was chilled. - assert!(Validators::::iter().all(|(stash, _)| stash != 11)); + // post-slash balance + let nominator_slash_amount_11 = 125 / 10; + assert_eq!(Balances::free_balance(11), 900); + assert_eq!(Balances::free_balance(101), 2000 - nominator_slash_amount_11); - // actually re-bond the slashed validator - assert_ok!(Staking::validate(RuntimeOrigin::signed(11), Default::default())); + // check that validator was disabled. + assert!(is_disabled(11)); - mock::start_active_era(2); - let exposure_11 = Staking::eras_stakers(active_era(), &11); - let exposure_21 = Staking::eras_stakers(active_era(), &21); + // actually re-bond the slashed validator + assert_ok!(Staking::validate(RuntimeOrigin::signed(11), Default::default())); - // 11's own expo is reduced. sum of support from 11 is less (448), which is 500 - // 900 + 146 - assert!(matches!(exposure_11, Exposure { own: 900, total: 1046, .. })); - // 1000 + 342 - assert!(matches!(exposure_21, Exposure { own: 1000, total: 1342, .. })); - assert_eq!(500 - 146 - 342, nominator_slash_amount_11); - }); + mock::start_active_era(2); + let exposure_11 = Staking::eras_stakers(active_era(), &11); + let exposure_21 = Staking::eras_stakers(active_era(), &21); + + // 11's own expo is reduced. sum of support from 11 is less (448), which is 500 + // 900 + 146 + assert!(matches!(exposure_11, Exposure { own: 900, total: 1046, .. })); + // 1000 + 342 + assert!(matches!(exposure_21, Exposure { own: 1000, total: 1342, .. })); + assert_eq!(500 - 146 - 342, nominator_slash_amount_11); + }); } #[test] @@ -3327,13 +3336,11 @@ fn non_slashable_offence_disables_validator() { vec![ Event::StakersElected, Event::EraPaid { era_index: 0, validator_payout: 11075, remainder: 33225 }, - Event::Chilled { stash: 11 }, Event::SlashReported { validator: 11, fraction: Perbill::from_percent(0), slash_era: 1 }, - Event::Chilled { stash: 21 }, Event::SlashReported { validator: 21, fraction: Perbill::from_percent(25), @@ -3391,13 +3398,11 @@ fn slashing_independent_of_disabling_validator() { vec![ Event::StakersElected, Event::EraPaid { era_index: 0, validator_payout: 11075, remainder: 33225 }, - Event::Chilled { stash: 11 }, Event::SlashReported { validator: 11, fraction: Perbill::from_percent(0), slash_era: 1 }, - Event::Chilled { stash: 21 }, Event::SlashReported { validator: 21, fraction: Perbill::from_percent(25), @@ -3613,27 +3618,34 @@ fn claim_reward_at_the_last_era_and_no_double_claim_and_invalid_claim() { #[test] fn zero_slash_keeps_nominators() { - ExtBuilder::default().build_and_execute(|| { - mock::start_active_era(1); + ExtBuilder::default() + .validator_count(7) + .set_status(41, StakerStatus::Validator) + .set_status(51, StakerStatus::Validator) + .set_status(201, StakerStatus::Validator) + .set_status(202, StakerStatus::Validator) + .build_and_execute(|| { + mock::start_active_era(1); - assert_eq!(Balances::free_balance(11), 1000); + assert_eq!(Balances::free_balance(11), 1000); - let exposure = Staking::eras_stakers(active_era(), &11); - assert_eq!(Balances::free_balance(101), 2000); + let exposure = Staking::eras_stakers(active_era(), &11); + assert_eq!(Balances::free_balance(101), 2000); - on_offence_now( - &[OffenceDetails { offender: (11, exposure.clone()), reporters: vec![] }], - &[Perbill::from_percent(0)], - ); + on_offence_now( + &[OffenceDetails { offender: (11, exposure.clone()), reporters: vec![] }], + &[Perbill::from_percent(0)], + ); - assert_eq!(Balances::free_balance(11), 1000); - assert_eq!(Balances::free_balance(101), 2000); + assert_eq!(Balances::free_balance(11), 1000); + assert_eq!(Balances::free_balance(101), 2000); - // 11 is still removed.. - assert!(Validators::::iter().all(|(stash, _)| stash != 11)); - // but their nominations are kept. - assert_eq!(Staking::nominators(101).unwrap().targets, vec![11, 21]); - }); + // 11 is not removed but disabled + assert!(Validators::::iter().any(|(stash, _)| stash == 11)); + assert!(is_disabled(11)); + // and their nominations are kept. + assert_eq!(Staking::nominators(101).unwrap().targets, vec![11, 21]); + }); } #[test] From 6097a32876c1299bb097f9cc0c75f42843d5a94f Mon Sep 17 00:00:00 2001 From: Tsvetomir Dimitrov Date: Wed, 13 Dec 2023 09:38:23 +0200 Subject: [PATCH 23/67] Don't disable for offences in previous eras --- substrate/frame/staking/src/lib.rs | 30 +++++++++++++++++-------- substrate/frame/staking/src/slashing.rs | 19 ++++++++++------ substrate/frame/staking/src/tests.rs | 30 ++++++++++++------------- 3 files changed, 47 insertions(+), 32 deletions(-) diff --git a/substrate/frame/staking/src/lib.rs b/substrate/frame/staking/src/lib.rs index 6dbb53454236..ceac02f3d8cd 100644 --- a/substrate/frame/staking/src/lib.rs +++ b/substrate/frame/staking/src/lib.rs @@ -1230,12 +1230,20 @@ pub struct DisablingDecision { disable_offenders: Vec, } +/// Input data for [`make_disabling_decision`]. Provides information about the offence so that the +/// implementation of [`DisablingStrategy`] can make a decision how to handle the offender. +pub struct DisablingDecisionContext { + pub offender_idx: u32, + pub slash_era: EraIndex, + pub era_now: EraIndex, +} + /// Controls validator disabling pub trait DisablingStrategy { /// Make a decision if an offender should be disabled or not. The result is an instance of /// `[DisablingDecision]` fn make_disabling_decision( - offender_idx: u32, + offence_ctx: DisablingDecisionContext, currently_disabled: &Vec, active_set: &Vec, ) -> DisablingDecision; @@ -1255,17 +1263,21 @@ impl UpToByzantineThresholdDisablingStrategy { impl DisablingStrategy for UpToByzantineThresholdDisablingStrategy { fn make_disabling_decision( - offender_idx: u32, + offence_ctx: DisablingDecisionContext, currently_disabled: &Vec, active_set: &Vec, ) -> DisablingDecision { - // we don't want to disable more offenders than the byzantine threshold - let disable_offenders = - if currently_disabled.len() >= Self::byzantine_threshold(active_set.len()) as usize { - vec![] - } else { - vec![offender_idx] - }; + // We don't disable more than 1/3 of the validators in the active set + let over_byzantine_threshold = + currently_disabled.len() >= Self::byzantine_threshold(active_set.len()); + // We don't disable for offences in previous eras + let ancient_offence = offence_ctx.era_now > offence_ctx.slash_era; + + let disable_offenders = if over_byzantine_threshold || ancient_offence { + vec![] + } else { + vec![offence_ctx.offender_idx] + }; DisablingDecision { disable_offenders } } diff --git a/substrate/frame/staking/src/slashing.rs b/substrate/frame/staking/src/slashing.rs index c1298c403ac9..08a7856531aa 100644 --- a/substrate/frame/staking/src/slashing.rs +++ b/substrate/frame/staking/src/slashing.rs @@ -50,9 +50,9 @@ //! Based on research at use crate::{ - BalanceOf, Config, DisabledValidators, DisablingDecision, DisablingStrategy, Error, Exposure, - NegativeImbalanceOf, NominatorSlashInEra, Pallet, Perbill, SessionInterface, SpanSlash, - UnappliedSlash, ValidatorSlashInEra, + BalanceOf, Config, DisabledValidators, DisablingDecision, DisablingDecisionContext, + DisablingStrategy, Error, Exposure, NegativeImbalanceOf, NominatorSlashInEra, Pallet, Perbill, + SessionInterface, SpanSlash, UnappliedSlash, ValidatorSlashInEra, }; use codec::{Decode, Encode, MaxEncodedLen}; use frame_support::{ @@ -283,7 +283,7 @@ pub(crate) fn compute_slash( } } - add_offending_validator::(params.stash); + add_offending_validator::(¶ms); let mut nominators_slashed = Vec::new(); reward_payout += slash_nominators::(params.clone(), prior_slash_p, &mut nominators_slashed); @@ -316,13 +316,14 @@ fn kick_out_if_recent(params: SlashParams) { spans.end_span(params.now); } - add_offending_validator::(params.stash); + add_offending_validator::(¶ms); } /// Add the given validator to the offenders list and optionally disable it. /// If after adding the validator `OffendingValidatorsThreshold` is reached /// a new era will be forced. -fn add_offending_validator(stash: &T::AccountId) { +fn add_offending_validator(params: &SlashParams) { + let stash = params.stash; DisabledValidators::::mutate(|disabled| { let validators = T::SessionInterface::validators(); let validator_index = match validators.iter().position(|i| i == stash) { @@ -333,7 +334,11 @@ fn add_offending_validator(stash: &T::AccountId) { let validator_index_u32 = validator_index as u32; let DisablingDecision { disable_offenders } = T::DisablingStrategy::make_disabling_decision( - validator_index as u32, + DisablingDecisionContext { + offender_idx: validator_index as u32, + slash_era: params.slash_era, + era_now: params.now, + }, &disabled, &validators, ); diff --git a/substrate/frame/staking/src/tests.rs b/substrate/frame/staking/src/tests.rs index 6fa284671bc7..8fdcc5e1dc68 100644 --- a/substrate/frame/staking/src/tests.rs +++ b/substrate/frame/staking/src/tests.rs @@ -2389,7 +2389,7 @@ fn slashing_performed_according_exposure() { } #[test] -fn slash_in_old_span_does_not_deselect() { +fn validator_is_not_disabled_for_an_offence_in_previous_era() { ExtBuilder::default() .validator_count(4) .set_status(41, StakerStatus::Validator) @@ -2412,16 +2412,15 @@ fn slash_in_old_span_does_not_deselect() { mock::start_active_era(2); - // Staking::validate(RuntimeOrigin::signed(11), Default::default()).unwrap(); - // assert_eq!(Staking::force_era(), Forcing::NotForcing); - // assert!(>::contains_key(11)); - // assert!(!Session::validators().contains(&11)); - // - // mock::start_active_era(3); + // the validator is not disabled in the new era + Staking::validate(RuntimeOrigin::signed(11), Default::default()).unwrap(); + assert_eq!(Staking::force_era(), Forcing::NotForcing); + assert!(>::contains_key(11)); + assert!(Session::validators().contains(&11)); - // this staker is in a new slashing span now, having re-registered after - // their prior slash. + mock::start_active_era(3); + // an offence committed in era 1 is reported in era 3 on_offence_in_era( &[OffenceDetails { offender: (11, Staking::eras_stakers(active_era(), &11)), @@ -2431,10 +2430,11 @@ fn slash_in_old_span_does_not_deselect() { 1, ); - // the validator doesn't get chilled again + // the validator doesn't get disabled for an old offence assert!(Validators::::iter().any(|(stash, _)| stash == 11)); + assert!(!is_disabled(11)); - // but we are still forcing a new era + // and we are not forcing a new era assert_eq!(Staking::force_era(), Forcing::NotForcing); on_offence_in_era( @@ -2447,12 +2447,10 @@ fn slash_in_old_span_does_not_deselect() { 1, ); - // the validator doesn't get chilled again + // the validator doesn't get disabled again assert!(Validators::::iter().any(|(stash, _)| stash == 11)); - - // but it's disabled - assert!(is_disabled(11)); - // and we are still forcing a new era + assert!(!is_disabled(11)); + // and we are still not forcing a new era assert_eq!(Staking::force_era(), Forcing::NotForcing); }); } From 3109a294bf4268f0269e2946d1e96c2ef30a349d Mon Sep 17 00:00:00 2001 From: Tsvetomir Dimitrov Date: Wed, 13 Dec 2023 16:50:55 +0200 Subject: [PATCH 24/67] Code review feedback - rework `enters_emergency_phase_after_forcing_before_elect` from `test-staking-e2e` --- .../test-staking-e2e/src/lib.rs | 81 +++++-------------- .../test-staking-e2e/src/mock.rs | 9 +-- 2 files changed, 23 insertions(+), 67 deletions(-) diff --git a/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/lib.rs b/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/lib.rs index d1fd27f701b7..acc06d0b9319 100644 --- a/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/lib.rs +++ b/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/lib.rs @@ -23,7 +23,6 @@ pub(crate) const LOG_TARGET: &str = "tests::e2e-epm"; use frame_support::{assert_err, assert_noop, assert_ok}; use mock::*; use sp_core::Get; -use sp_npos_elections::{to_supports, StakedAssignment}; use sp_runtime::Perbill; use crate::mock::RuntimeOrigin; @@ -127,75 +126,35 @@ fn offchainify_works() { } #[test] -/// Replicates the Kusama incident of 8th Dec 2022 and its resolution through the governance +/// Inspired by the Kusama incident of 8th Dec 2022 and its resolution through the governance /// fallback. /// -/// After enough slashes exceeded the `Staking::OffendingValidatorsThreshold`, the staking pallet -/// set `Forcing::ForceNew`. When a new session starts, staking will start to force a new era and -/// calls ::elect(). If at this point EPM and the staking miners did not -/// have enough time to queue a new solution (snapshot + solution submission), the election request -/// fails. If there is no election fallback mechanism in place, EPM enters in emergency mode. -/// Recovery: Once EPM is in emergency mode, subsequent calls to `elect()` will fail until a new -/// solution is added to EPM's `QueuedSolution` queue. This can be achieved through -/// `Call::set_emergency_election_result` or `Call::governance_fallback` dispatchables. Once a new -/// solution is added to the queue, EPM phase transitions to `Phase::Off` and the election flow -/// restarts. Note that in this test case, the emergency throttling is disabled. -fn enters_emergency_phase_after_forcing_before_elect() { +/// Mass slash of validators shoudn't disable more than 1/3 of them (the byzantine threshold). Also +/// no new era should be forced which could lead to EPM entering emergency mode. +fn mass_slash_doesnt_enter_emergency_phase() { let epm_builder = EpmExtBuilder::default().disable_emergency_throttling(); - let (mut ext, pool_state, _) = ExtBuilder::default().epm(epm_builder).build_offchainify(); + let staking_builder = StakingExtBuilder::default().validator_count(7); + let (mut ext, _, _) = ExtBuilder::default() + .epm(epm_builder) + .staking(staking_builder) + .build_offchainify(); ext.execute_with(|| { - log!( - trace, - "current validators (staking): {:?}", - >::validators() - ); - let session_validators_before = Session::validators(); - - roll_to_epm_off(); - assert!(ElectionProviderMultiPhase::current_phase().is_off()); - - assert_eq!(pallet_staking::ForceEra::::get(), pallet_staking::Forcing::NotForcing); - // slashes so that staking goes into `Forcing::ForceNew`. - slash_through_offending_threshold(); - assert_eq!(pallet_staking::ForceEra::::get(), pallet_staking::Forcing::NotForcing); - advance_session_delayed_solution(pool_state.clone()); - assert!(ElectionProviderMultiPhase::current_phase().is_emergency()); - log_current_time(); - - let era_before_delayed_next = Staking::current_era(); - // try to advance 2 eras. - assert!(start_next_active_era_delayed_solution(pool_state.clone()).is_ok()); - assert_eq!(Staking::current_era(), era_before_delayed_next); - assert!(start_next_active_era(pool_state).is_err()); - assert_eq!(Staking::current_era(), era_before_delayed_next); + // Slash more than 1/3 of the active validators + slash_half_the_active_set(); - // EPM is still in emergency phase. - assert!(ElectionProviderMultiPhase::current_phase().is_emergency()); - - // session validator set remains the same. - assert_eq!(Session::validators(), session_validators_before); - - // performs recovery through the set emergency result. - let supports = to_supports(&vec![ - StakedAssignment { who: 21, distribution: vec![(21, 10)] }, - StakedAssignment { who: 31, distribution: vec![(21, 10), (31, 10)] }, - StakedAssignment { who: 41, distribution: vec![(41, 10)] }, - ]); - assert!(ElectionProviderMultiPhase::set_emergency_election_result( - RuntimeOrigin::root(), - supports - ) - .is_ok()); + // We are not forcing a new era + assert_eq!(pallet_staking::ForceEra::::get(), pallet_staking::Forcing::NotForcing); - // EPM can now roll to signed phase to proceed with elections. The validator set is the - // expected (ie. set through `set_emergency_election_result`). - roll_to_epm_signed(); - //assert!(ElectionProviderMultiPhase::current_phase().is_signed()); - assert_eq!(Session::validators(), vec![21, 31, 41]); - assert_eq!(Staking::current_era(), era_before_delayed_next.map(|e| e + 1)); + // And no more than `1/3` of the validators are disabled + assert_eq!( + Session::disabled_validators().len(), + pallet_staking::UpToByzantineThresholdDisablingStrategy::byzantine_threshold( + Session::validators().len() + ) + ); }); } diff --git a/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/mock.rs b/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/mock.rs index 85cf88d3eba7..46702ec93ab9 100644 --- a/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/mock.rs +++ b/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/mock.rs @@ -813,13 +813,10 @@ pub(crate) fn add_slash(who: &AccountId) { ); } -// Slashes enough validators to cross the `Staking::OffendingValidatorsThreshold`. -pub(crate) fn slash_through_offending_threshold() { +// Slashes 1/2 of the active set +pub(crate) fn slash_half_the_active_set() { let validators = Session::validators(); - let mut remaining_slashes = - pallet_staking::UpToByzantineThresholdDisablingStrategy::byzantine_threshold( - validators.len(), - ); + let mut remaining_slashes = validators.len() / 2; for v in validators.into_iter() { if remaining_slashes != 0 { From 41ce97826e0a48143dfd5c6bc40aaefa90974f86 Mon Sep 17 00:00:00 2001 From: Tsvetomir Dimitrov Date: Thu, 14 Dec 2023 10:30:14 +0200 Subject: [PATCH 25/67] Add `DisablingStrategy` to benchmarking mocks --- substrate/frame/nomination-pools/benchmarking/src/mock.rs | 1 + substrate/frame/offences/benchmarking/src/mock.rs | 1 + substrate/frame/session/benchmarking/src/mock.rs | 1 + 3 files changed, 3 insertions(+) diff --git a/substrate/frame/nomination-pools/benchmarking/src/mock.rs b/substrate/frame/nomination-pools/benchmarking/src/mock.rs index 424db6a89a3b..d9fe1fd0d09b 100644 --- a/substrate/frame/nomination-pools/benchmarking/src/mock.rs +++ b/substrate/frame/nomination-pools/benchmarking/src/mock.rs @@ -123,6 +123,7 @@ impl pallet_staking::Config for Runtime { type EventListeners = Pools; type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig; type WeightInfo = (); + type DisablingStrategy = pallet_staking::UpToByzantineThresholdDisablingStrategy; } parameter_types! { diff --git a/substrate/frame/offences/benchmarking/src/mock.rs b/substrate/frame/offences/benchmarking/src/mock.rs index a080f2d1797b..3d2b9c9789c7 100644 --- a/substrate/frame/offences/benchmarking/src/mock.rs +++ b/substrate/frame/offences/benchmarking/src/mock.rs @@ -188,6 +188,7 @@ impl pallet_staking::Config for Test { type EventListeners = (); type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig; type WeightInfo = (); + type DisablingStrategy = pallet_staking::UpToByzantineThresholdDisablingStrategy; } impl pallet_im_online::Config for Test { diff --git a/substrate/frame/session/benchmarking/src/mock.rs b/substrate/frame/session/benchmarking/src/mock.rs index 7c9bc234b42e..4b5c6d8e79cd 100644 --- a/substrate/frame/session/benchmarking/src/mock.rs +++ b/substrate/frame/session/benchmarking/src/mock.rs @@ -185,6 +185,7 @@ impl pallet_staking::Config for Test { type EventListeners = (); type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig; type WeightInfo = (); + type DisablingStrategy = pallet_staking::UpToByzantineThresholdDisablingStrategy; } impl crate::Config for Test {} From 28d50552d35d7fcf685697c0c9eba7e7529efd1c Mon Sep 17 00:00:00 2001 From: Tsvetomir Dimitrov Date: Thu, 14 Dec 2023 11:13:37 +0200 Subject: [PATCH 26/67] Fix a comment --- substrate/primitives/staking/src/offence.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/substrate/primitives/staking/src/offence.rs b/substrate/primitives/staking/src/offence.rs index 52e20b720ada..1cf62a061668 100644 --- a/substrate/primitives/staking/src/offence.rs +++ b/substrate/primitives/staking/src/offence.rs @@ -149,9 +149,6 @@ pub trait OnOffenceHandler { /// /// The `session` parameter is the session index of the offence. /// - /// NOTE: All offenders are disabled immediately unless there are already `byzantine threshold` - /// offenders. In this case they are slashed but not disabled. - /// /// The receiver might decide to not accept this offence. In this case, the call site is /// responsible for queuing the report and re-submitting again. fn on_offence( From 71b6f60c45e97d6ae5860d919b2ff713fc51fb45 Mon Sep 17 00:00:00 2001 From: Tsvetomir Dimitrov Date: Thu, 14 Dec 2023 16:30:38 +0200 Subject: [PATCH 27/67] `try_state` check for `DisabledValidators` --- substrate/frame/staking/src/pallet/impls.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/substrate/frame/staking/src/pallet/impls.rs b/substrate/frame/staking/src/pallet/impls.rs index 3340a81f84dd..2fa71ce137df 100644 --- a/substrate/frame/staking/src/pallet/impls.rs +++ b/substrate/frame/staking/src/pallet/impls.rs @@ -1818,7 +1818,8 @@ impl Pallet { Self::check_exposures()?; Self::check_paged_exposures()?; Self::check_ledgers()?; - Self::check_count() + Self::check_count()?; + Self::ensure_disabled_validators_sorted() } fn check_count() -> Result<(), TryRuntimeError> { @@ -2001,4 +2002,12 @@ impl Pallet { Ok(()) } + + fn ensure_disabled_validators_sorted() -> Result<(), TryRuntimeError> { + ensure!( + DisabledValidators::::get().windows(2).all(|pair| pair[0] <= pair[1]), + "DisabledValidators is not sorted" + ); + Ok(()) + } } From 790cf4add0e0d8672efe7e38fd919908ef070d24 Mon Sep 17 00:00:00 2001 From: Tsvetomir Dimitrov Date: Thu, 14 Dec 2023 16:44:33 +0200 Subject: [PATCH 28/67] Fix another comment --- substrate/frame/staking/src/pallet/mod.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/substrate/frame/staking/src/pallet/mod.rs b/substrate/frame/staking/src/pallet/mod.rs index 7ff81477ee83..d970d32b1efb 100644 --- a/substrate/frame/staking/src/pallet/mod.rs +++ b/substrate/frame/staking/src/pallet/mod.rs @@ -636,8 +636,9 @@ pub mod pallet { pub type CurrentPlannedSession = StorageValue<_, SessionIndex, ValueQuery>; /// Indices of validators that have offended in the active era. The offenders are disabled for a - /// whole era. For this reason they are kept here - only staking pallet knows about eras. No - /// more than `byzantine_threshold` validators are disabled at a given era. + /// whole era. For this reason they are kept here - only staking pallet knows about eras. The + /// implementor of [`DisablingStrategy`] defines if a validator should be disabled which + /// implicitly means that the implementor also controls the max number of disabled validators. /// /// The vec is always kept sorted so that we can find whether a given validator has previously /// offended using binary search. From 6dfd0bc9baa469be1785b42d16cb087f07c26cbe Mon Sep 17 00:00:00 2001 From: Tsvetomir Dimitrov Date: Thu, 14 Dec 2023 16:46:15 +0200 Subject: [PATCH 29/67] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Gonçalo Pestana --- substrate/frame/staking/src/slashing.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/substrate/frame/staking/src/slashing.rs b/substrate/frame/staking/src/slashing.rs index 08a7856531aa..d680e4e4aba3 100644 --- a/substrate/frame/staking/src/slashing.rs +++ b/substrate/frame/staking/src/slashing.rs @@ -278,7 +278,8 @@ pub(crate) fn compute_slash( let target_span = spans.compare_and_update_span_slash(params.slash_era, own_slash); if target_span == Some(spans.span_index()) { - // Check https://github.com/paritytech/polkadot-sdk/issues/2650 for details + // misbehavior occurred within the current slashing span - end current span. + // Check for details. spans.end_span(params.now); } } From cd185b6dbb30d46c484b3681eab22d1bda73523c Mon Sep 17 00:00:00 2001 From: Tsvetomir Dimitrov Date: Thu, 14 Dec 2023 16:51:26 +0200 Subject: [PATCH 30/67] Fix a comment --- substrate/frame/staking/src/slashing.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/substrate/frame/staking/src/slashing.rs b/substrate/frame/staking/src/slashing.rs index d680e4e4aba3..1388bb42a2a9 100644 --- a/substrate/frame/staking/src/slashing.rs +++ b/substrate/frame/staking/src/slashing.rs @@ -320,9 +320,8 @@ fn kick_out_if_recent(params: SlashParams) { add_offending_validator::(¶ms); } -/// Add the given validator to the offenders list and optionally disable it. -/// If after adding the validator `OffendingValidatorsThreshold` is reached -/// a new era will be forced. +/// Inform the [`DisablingStrategy`] implementation about the new offender and disable the list of +/// validators provided by [`DisablingDecision`]. fn add_offending_validator(params: &SlashParams) { let stash = params.stash; DisabledValidators::::mutate(|disabled| { From cc09228383b1d81dcc0b7656b2b08de701fa927c Mon Sep 17 00:00:00 2001 From: Tsvetomir Dimitrov Date: Thu, 14 Dec 2023 17:07:23 +0200 Subject: [PATCH 31/67] debug_assert for `DisabledValidators` being sorted --- substrate/frame/staking/src/slashing.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/substrate/frame/staking/src/slashing.rs b/substrate/frame/staking/src/slashing.rs index 1388bb42a2a9..200f79c2cea9 100644 --- a/substrate/frame/staking/src/slashing.rs +++ b/substrate/frame/staking/src/slashing.rs @@ -353,6 +353,9 @@ fn add_offending_validator(params: &SlashParams) { } } }); + + // `DisabledValidators` should be kept sorted + debug_assert!(DisabledValidators::::get().windows(2).all(|pair| pair[0] <= pair[1])); } /// Slash nominators. Accepts general parameters and the prior slash percentage of the validator. From 2552cb60d41f35e27a5c1f59173aac8c83f1ca0c Mon Sep 17 00:00:00 2001 From: Tsvetomir Dimitrov Date: Mon, 18 Dec 2023 10:36:13 +0200 Subject: [PATCH 32/67] Unit tests for `UpToByzantineThresholdDisablingStrategy` --- substrate/frame/staking/src/tests.rs | 55 ++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/substrate/frame/staking/src/tests.rs b/substrate/frame/staking/src/tests.rs index 8fdcc5e1dc68..858055f90c8a 100644 --- a/substrate/frame/staking/src/tests.rs +++ b/substrate/frame/staking/src/tests.rs @@ -6889,3 +6889,58 @@ mod ledger { }) } } + +mod byzantine_threshold_disabling_strategy { + use crate::{ + tests::Test, DisablingDecisionContext, DisablingStrategy, + UpToByzantineThresholdDisablingStrategy, + }; + + #[test] + fn dont_disable_for_ancient_offence() { + let offence_context = + DisablingDecisionContext { offender_idx: 7, slash_era: 1, era_now: 2 }; + let initially_disabled = vec![]; + let active_set = vec![1, 2, 3, 4, 5, 6, 7]; + + let decision = >::make_disabling_decision( + offence_context, + &initially_disabled, + &active_set, + ); + + assert!(decision.disable_offenders.is_empty()); + } + + #[test] + fn dont_disable_beyond_byzantine_threshold() { + let offence_context = + DisablingDecisionContext { offender_idx: 7, slash_era: 1, era_now: 1 }; + let initially_disabled = vec![1, 2]; + let active_set = vec![1, 2, 3, 4, 5, 6, 7]; + + let decision = >::make_disabling_decision( + offence_context, + &initially_disabled, + &active_set, + ); + + assert!(decision.disable_offenders.is_empty()); + } + + #[test] + fn disable_when_below_byzantine_threshold() { + let offence_context = + DisablingDecisionContext { offender_idx: 7, slash_era: 1, era_now: 1 }; + let initially_disabled = vec![1]; + let active_set = vec![1, 2, 3, 4, 5, 6, 7]; + + let decision = >::make_disabling_decision( + offence_context, + &initially_disabled, + &active_set, + ); + + assert_eq!(decision.disable_offenders, vec![7]); + } +} From 327b9fd89b55887610da0555c393bba56ff674fa Mon Sep 17 00:00:00 2001 From: Tsvetomir Dimitrov Date: Tue, 2 Jan 2024 14:33:25 +0200 Subject: [PATCH 33/67] Remove struct DisablingDecision --- substrate/frame/staking/src/lib.rs | 16 +++++----------- substrate/frame/staking/src/slashing.rs | 10 +++++----- 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/substrate/frame/staking/src/lib.rs b/substrate/frame/staking/src/lib.rs index ceac02f3d8cd..17c199a02ca4 100644 --- a/substrate/frame/staking/src/lib.rs +++ b/substrate/frame/staking/src/lib.rs @@ -1224,12 +1224,6 @@ impl BenchmarkingConfig for TestBenchmarkingConfig { type MaxNominators = frame_support::traits::ConstU32<100>; } -/// A result from a disabling decision made by [`DisablingStrategy`]. -pub struct DisablingDecision { - /// List of validator indices that should be disabled. - disable_offenders: Vec, -} - /// Input data for [`make_disabling_decision`]. Provides information about the offence so that the /// implementation of [`DisablingStrategy`] can make a decision how to handle the offender. pub struct DisablingDecisionContext { @@ -1240,13 +1234,13 @@ pub struct DisablingDecisionContext { /// Controls validator disabling pub trait DisablingStrategy { - /// Make a decision if an offender should be disabled or not. The result is an instance of - /// `[DisablingDecision]` + /// Make a decision if an offender should be disabled or not. The result is a `Vec` of validator + /// indices that should be disabled fn make_disabling_decision( offence_ctx: DisablingDecisionContext, currently_disabled: &Vec, active_set: &Vec, - ) -> DisablingDecision; + ) -> Vec; } /// Implementation of [`DisablingStrategy`] which disables no more than 1/3 of the validators in the @@ -1266,7 +1260,7 @@ impl DisablingStrategy for UpToByzantineThresholdDisablingStrategy offence_ctx: DisablingDecisionContext, currently_disabled: &Vec, active_set: &Vec, - ) -> DisablingDecision { + ) -> Vec { // We don't disable more than 1/3 of the validators in the active set let over_byzantine_threshold = currently_disabled.len() >= Self::byzantine_threshold(active_set.len()); @@ -1279,6 +1273,6 @@ impl DisablingStrategy for UpToByzantineThresholdDisablingStrategy vec![offence_ctx.offender_idx] }; - DisablingDecision { disable_offenders } + disable_offenders } } diff --git a/substrate/frame/staking/src/slashing.rs b/substrate/frame/staking/src/slashing.rs index 200f79c2cea9..40178ee63298 100644 --- a/substrate/frame/staking/src/slashing.rs +++ b/substrate/frame/staking/src/slashing.rs @@ -50,9 +50,9 @@ //! Based on research at use crate::{ - BalanceOf, Config, DisabledValidators, DisablingDecision, DisablingDecisionContext, - DisablingStrategy, Error, Exposure, NegativeImbalanceOf, NominatorSlashInEra, Pallet, Perbill, - SessionInterface, SpanSlash, UnappliedSlash, ValidatorSlashInEra, + BalanceOf, Config, DisabledValidators, DisablingDecisionContext, DisablingStrategy, Error, + Exposure, NegativeImbalanceOf, NominatorSlashInEra, Pallet, Perbill, SessionInterface, + SpanSlash, UnappliedSlash, ValidatorSlashInEra, }; use codec::{Decode, Encode, MaxEncodedLen}; use frame_support::{ @@ -321,7 +321,7 @@ fn kick_out_if_recent(params: SlashParams) { } /// Inform the [`DisablingStrategy`] implementation about the new offender and disable the list of -/// validators provided by [`DisablingDecision`]. +/// validators provided by [`make_disabling_decision`]. fn add_offending_validator(params: &SlashParams) { let stash = params.stash; DisabledValidators::::mutate(|disabled| { @@ -333,7 +333,7 @@ fn add_offending_validator(params: &SlashParams) { let validator_index_u32 = validator_index as u32; - let DisablingDecision { disable_offenders } = T::DisablingStrategy::make_disabling_decision( + let disable_offenders = T::DisablingStrategy::make_disabling_decision( DisablingDecisionContext { offender_idx: validator_index as u32, slash_era: params.slash_era, From c187d505286a17e9b0495b918816e7e34c10fb72 Mon Sep 17 00:00:00 2001 From: Tsvetomir Dimitrov Date: Tue, 2 Jan 2024 17:37:58 +0200 Subject: [PATCH 34/67] pub DisablingDecisionContext --- substrate/frame/staking/src/lib.rs | 23 +++------ substrate/frame/staking/src/slashing.rs | 13 ++--- substrate/frame/staking/src/tests.rs | 65 ++++++++++++++----------- 3 files changed, 48 insertions(+), 53 deletions(-) diff --git a/substrate/frame/staking/src/lib.rs b/substrate/frame/staking/src/lib.rs index 17c199a02ca4..4266bc379582 100644 --- a/substrate/frame/staking/src/lib.rs +++ b/substrate/frame/staking/src/lib.rs @@ -1224,20 +1224,13 @@ impl BenchmarkingConfig for TestBenchmarkingConfig { type MaxNominators = frame_support::traits::ConstU32<100>; } -/// Input data for [`make_disabling_decision`]. Provides information about the offence so that the -/// implementation of [`DisablingStrategy`] can make a decision how to handle the offender. -pub struct DisablingDecisionContext { - pub offender_idx: u32, - pub slash_era: EraIndex, - pub era_now: EraIndex, -} - /// Controls validator disabling pub trait DisablingStrategy { /// Make a decision if an offender should be disabled or not. The result is a `Vec` of validator /// indices that should be disabled fn make_disabling_decision( - offence_ctx: DisablingDecisionContext, + offender_idx: u32, + slash_era: EraIndex, currently_disabled: &Vec, active_set: &Vec, ) -> Vec; @@ -1257,7 +1250,8 @@ impl UpToByzantineThresholdDisablingStrategy { impl DisablingStrategy for UpToByzantineThresholdDisablingStrategy { fn make_disabling_decision( - offence_ctx: DisablingDecisionContext, + offender_idx: u32, + slash_era: EraIndex, currently_disabled: &Vec, active_set: &Vec, ) -> Vec { @@ -1265,13 +1259,10 @@ impl DisablingStrategy for UpToByzantineThresholdDisablingStrategy let over_byzantine_threshold = currently_disabled.len() >= Self::byzantine_threshold(active_set.len()); // We don't disable for offences in previous eras - let ancient_offence = offence_ctx.era_now > offence_ctx.slash_era; + let ancient_offence = Pallet::::current_era().unwrap_or(1) > slash_era; - let disable_offenders = if over_byzantine_threshold || ancient_offence { - vec![] - } else { - vec![offence_ctx.offender_idx] - }; + let disable_offenders = + if over_byzantine_threshold || ancient_offence { vec![] } else { vec![offender_idx] }; disable_offenders } diff --git a/substrate/frame/staking/src/slashing.rs b/substrate/frame/staking/src/slashing.rs index 40178ee63298..6157cd548aec 100644 --- a/substrate/frame/staking/src/slashing.rs +++ b/substrate/frame/staking/src/slashing.rs @@ -50,9 +50,9 @@ //! Based on research at use crate::{ - BalanceOf, Config, DisabledValidators, DisablingDecisionContext, DisablingStrategy, Error, - Exposure, NegativeImbalanceOf, NominatorSlashInEra, Pallet, Perbill, SessionInterface, - SpanSlash, UnappliedSlash, ValidatorSlashInEra, + BalanceOf, Config, DisabledValidators, DisablingStrategy, Error, Exposure, NegativeImbalanceOf, + NominatorSlashInEra, Pallet, Perbill, SessionInterface, SpanSlash, UnappliedSlash, + ValidatorSlashInEra, }; use codec::{Decode, Encode, MaxEncodedLen}; use frame_support::{ @@ -334,11 +334,8 @@ fn add_offending_validator(params: &SlashParams) { let validator_index_u32 = validator_index as u32; let disable_offenders = T::DisablingStrategy::make_disabling_decision( - DisablingDecisionContext { - offender_idx: validator_index as u32, - slash_era: params.slash_era, - era_now: params.now, - }, + validator_index as u32, + params.slash_era, &disabled, &validators, ); diff --git a/substrate/frame/staking/src/tests.rs b/substrate/frame/staking/src/tests.rs index 858055f90c8a..09603a283699 100644 --- a/substrate/frame/staking/src/tests.rs +++ b/substrate/frame/staking/src/tests.rs @@ -6892,55 +6892,62 @@ mod ledger { mod byzantine_threshold_disabling_strategy { use crate::{ - tests::Test, DisablingDecisionContext, DisablingStrategy, - UpToByzantineThresholdDisablingStrategy, + tests::Test, CurrentEra, DisablingStrategy, UpToByzantineThresholdDisablingStrategy, }; + use sp_staking::EraIndex; + + // Common test data - the validator index of the offender and the era of the offence + const OFFENDER_IDX: u32 = 7; + const SLASH_ERA: EraIndex = 1; #[test] fn dont_disable_for_ancient_offence() { - let offence_context = - DisablingDecisionContext { offender_idx: 7, slash_era: 1, era_now: 2 }; - let initially_disabled = vec![]; - let active_set = vec![1, 2, 3, 4, 5, 6, 7]; + sp_io::TestExternalities::default().execute_with(|| { + let initially_disabled = vec![]; + let active_set = vec![1, 2, 3, 4, 5, 6, 7]; + + // Current era is 2 + CurrentEra::::put(2); - let decision = >::make_disabling_decision( - offence_context, - &initially_disabled, - &active_set, + let disable_offenders = >::make_disabling_decision( + OFFENDER_IDX, SLASH_ERA, &initially_disabled, &active_set ); - assert!(decision.disable_offenders.is_empty()); + assert!(disable_offenders.is_empty()); + }); } #[test] fn dont_disable_beyond_byzantine_threshold() { - let offence_context = - DisablingDecisionContext { offender_idx: 7, slash_era: 1, era_now: 1 }; - let initially_disabled = vec![1, 2]; - let active_set = vec![1, 2, 3, 4, 5, 6, 7]; + sp_io::TestExternalities::default().execute_with(|| { + let initially_disabled = vec![1, 2]; + let active_set = vec![1, 2, 3, 4, 5, 6, 7]; - let decision = >::make_disabling_decision( - offence_context, - &initially_disabled, - &active_set, + let disable_offenders = >::make_disabling_decision( + OFFENDER_IDX, SLASH_ERA, &initially_disabled, &active_set ); - assert!(decision.disable_offenders.is_empty()); + assert!(disable_offenders.is_empty()); + }); } #[test] fn disable_when_below_byzantine_threshold() { - let offence_context = - DisablingDecisionContext { offender_idx: 7, slash_era: 1, era_now: 1 }; - let initially_disabled = vec![1]; - let active_set = vec![1, 2, 3, 4, 5, 6, 7]; + sp_io::TestExternalities::default().execute_with(|| { + let initially_disabled = vec![1]; + let active_set = vec![1, 2, 3, 4, 5, 6, 7]; - let decision = >::make_disabling_decision( - offence_context, - &initially_disabled, - &active_set, + let disable_offenders = >::make_disabling_decision( + OFFENDER_IDX, SLASH_ERA, &initially_disabled, &active_set ); - assert_eq!(decision.disable_offenders, vec![7]); + assert_eq!(disable_offenders, vec![7]); + }); } } From e1899ce39fe55f1a19246263d2f837513657274a Mon Sep 17 00:00:00 2001 From: Tsvetomir Dimitrov Date: Thu, 4 Jan 2024 17:19:09 +0200 Subject: [PATCH 35/67] Extract disabling threshold as a generic parameter of `UpToByzantineThresholdDisablingStrategy` --- substrate/frame/staking/src/lib.rs | 18 ++++++++++++------ substrate/frame/staking/src/mock.rs | 6 +++++- substrate/frame/staking/src/tests.rs | 2 +- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/substrate/frame/staking/src/lib.rs b/substrate/frame/staking/src/lib.rs index 4266bc379582..637eca8edd0c 100644 --- a/substrate/frame/staking/src/lib.rs +++ b/substrate/frame/staking/src/lib.rs @@ -1236,11 +1236,15 @@ pub trait DisablingStrategy { ) -> Vec; } -/// Implementation of [`DisablingStrategy`] which disables no more than 1/3 of the validators in the -/// active set. -pub struct UpToByzantineThresholdDisablingStrategy; - -impl UpToByzantineThresholdDisablingStrategy { +/// Implementation of [`DisablingStrategy`] which disables validators from the active set up to a +/// threshold. `DISABLING_THRESHOLD_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. +pub struct UpToByzantineThresholdDisablingStrategy; + +impl + UpToByzantineThresholdDisablingStrategy +{ /// Disabling limit calculated from the total number of validators in the active set. When /// reached no more validators will be disabled. pub fn byzantine_threshold(validators_len: usize) -> usize { @@ -1248,7 +1252,9 @@ impl UpToByzantineThresholdDisablingStrategy { } } -impl DisablingStrategy for UpToByzantineThresholdDisablingStrategy { +impl DisablingStrategy + for UpToByzantineThresholdDisablingStrategy +{ fn make_disabling_decision( offender_idx: u32, slash_era: EraIndex, diff --git a/substrate/frame/staking/src/mock.rs b/substrate/frame/staking/src/mock.rs index c36a2f8cc5c7..22afdb5a86e3 100644 --- a/substrate/frame/staking/src/mock.rs +++ b/substrate/frame/staking/src/mock.rs @@ -289,6 +289,9 @@ impl OnStakingUpdate for EventListenerMock { } } +// Disabling threshold for `UpToByzantineThresholdDisablingStrategy` +pub(crate) const DISABLING_THRESHOLD_FACTOR: u32 = 3; + impl crate::pallet::pallet::Config for Test { type Currency = Balances; type CurrencyBalance = ::Balance; @@ -317,7 +320,8 @@ impl crate::pallet::pallet::Config for Test { type EventListeners = EventListenerMock; type BenchmarkingConfig = TestBenchmarkingConfig; type WeightInfo = (); - type DisablingStrategy = pallet_staking::UpToByzantineThresholdDisablingStrategy; + type DisablingStrategy = + pallet_staking::UpToByzantineThresholdDisablingStrategy; } pub struct WeightedNominationsQuota; diff --git a/substrate/frame/staking/src/tests.rs b/substrate/frame/staking/src/tests.rs index 09603a283699..b69903bb3817 100644 --- a/substrate/frame/staking/src/tests.rs +++ b/substrate/frame/staking/src/tests.rs @@ -3410,7 +3410,7 @@ fn offence_threshold_doesnt_trigger_new_era() { assert_eq_uvec!(Session::validators(), vec![11, 21, 31, 41]); assert_eq!( - UpToByzantineThresholdDisablingStrategy::byzantine_threshold( + UpToByzantineThresholdDisablingStrategy::::byzantine_threshold( Session::validators().len() ), 1 From 1d283dc18a150cac312d82def1957697e3ad9460 Mon Sep 17 00:00:00 2001 From: Tsvetomir Dimitrov Date: Thu, 4 Jan 2024 17:19:29 +0200 Subject: [PATCH 36/67] Changelog and prdoc --- prdoc/pr_2226.prdoc | 29 ++++++++++++++++++++++++++++ substrate/frame/staking/CHANGELOG.md | 20 +++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 prdoc/pr_2226.prdoc diff --git a/prdoc/pr_2226.prdoc b/prdoc/pr_2226.prdoc new file mode 100644 index 000000000000..fd652bbbee71 --- /dev/null +++ b/prdoc/pr_2226.prdoc @@ -0,0 +1,29 @@ +title: Validator disabling strategy in runtime + +doc: + - audience: Node Operator + description: | + On each committed offence (no matter slashable or not) the offending validator will be + disabled for a whole era. + - audience: Runtime Dev + description: | + The disabling strategy in staking pallet is no longer hardcoded but abstracted away via + DisablingStrategy trait. The trait contains a single function (make_disabling_decision) which + is called for each offence. The function makes a decision if (and which) validators should be + disabled. A default implementation is provided - UpToByzantineThresholdDisablingStrategy. It + will be used on Kusama and Polkadot. In nutshel UpToByzantineThresholdDisablingStrategy + disables offenders up to the configured threshold (1/3 of the validators in the active set by + default). Offending validators are not disabled for offences in previous eras. The threshold is + controlled via `DISABLING_THRESHOLD_FACTOR` (a generic parameter of + UpToByzantineThresholdDisablingStrategy). + +migrations: + db: [] + runtime: + - reference: pallet-staking + description: | + Renames OffendingValidators storage item to DisabledValidators and changes its type from + Vec<(u32, bool)> to Vec + +crates: + - name: pallet-staking \ No newline at end of file diff --git a/substrate/frame/staking/CHANGELOG.md b/substrate/frame/staking/CHANGELOG.md index 719aa388755f..6327acdc5c70 100644 --- a/substrate/frame/staking/CHANGELOG.md +++ b/substrate/frame/staking/CHANGELOG.md @@ -7,6 +7,26 @@ on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). We maintain a single integer version number for staking pallet to keep track of all storage migrations. +## [v15] + +### Added + +- New trait `DisablingStrategy` which is responsible for making a decision which offenders should be + disabled on new offence. +- Default implementation of `DisablingStrategy` - `UpToByzantineThresholdDisablingStrategy`. It + disables each new offender up to a threshold (1/3 by default). Offenders are not punished for + offences in previous era(s). +- `OffendingValidators` storage item is replaced with `DisabledValidators`. The former keeps all + offenders and if they are disabled or not. The latter just keeps a list of all offenders as they + are disabled by default. +- TODO: STORAGE ITEM FOR DISABLING THRESHOLD + +### Deprecated + +- `enum DisableStrategy` is no longer needed because disabling is not related to the type of the + offence anymore. A decision if a offender is disabled or not is made by a `DisablingStrategy` + implementation. + ## [v14] ### Added From 1ef6e83c901bdfa4e0096e4984fa2f0b52e63d8e Mon Sep 17 00:00:00 2001 From: Tsvetomir Dimitrov Date: Fri, 5 Jan 2024 11:11:08 +0200 Subject: [PATCH 37/67] Fetch offender info and active set via staking pallet in `make_disabling_decision` --- substrate/frame/staking/src/lib.rs | 22 ++++++++++++++-------- substrate/frame/staking/src/mock.rs | 2 +- substrate/frame/staking/src/slashing.rs | 15 ++------------- 3 files changed, 17 insertions(+), 22 deletions(-) diff --git a/substrate/frame/staking/src/lib.rs b/substrate/frame/staking/src/lib.rs index 637eca8edd0c..850688de3f80 100644 --- a/substrate/frame/staking/src/lib.rs +++ b/substrate/frame/staking/src/lib.rs @@ -1229,10 +1229,9 @@ pub trait DisablingStrategy { /// Make a decision if an offender should be disabled or not. The result is a `Vec` of validator /// indices that should be disabled fn make_disabling_decision( - offender_idx: u32, + offender_stash: &T::AccountId, slash_era: EraIndex, currently_disabled: &Vec, - active_set: &Vec, ) -> Vec; } @@ -1240,27 +1239,34 @@ pub trait DisablingStrategy { /// threshold. `DISABLING_THRESHOLD_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. -pub struct UpToByzantineThresholdDisablingStrategy; +pub struct UpToByzantineThresholdDisablingStrategy; -impl +impl UpToByzantineThresholdDisablingStrategy { /// Disabling limit calculated from the total number of validators in the active set. When /// reached no more validators will be disabled. pub fn byzantine_threshold(validators_len: usize) -> usize { - validators_len.saturating_sub(1) / 3 + validators_len.saturating_sub(1) / DISABLING_THRESHOLD_FACTOR } } -impl DisablingStrategy +impl DisablingStrategy for UpToByzantineThresholdDisablingStrategy { fn make_disabling_decision( - offender_idx: u32, + offender_stash: &T::AccountId, slash_era: EraIndex, currently_disabled: &Vec, - active_set: &Vec, ) -> Vec { + let active_set = T::SessionInterface::validators(); + let offender_idx = if let Some(idx) = active_set.iter().position(|i| i == offender_stash) { + idx as u32 + } else { + // offender not found in the active set, do nothing + return vec![] + }; + // We don't disable more than 1/3 of the validators in the active set let over_byzantine_threshold = currently_disabled.len() >= Self::byzantine_threshold(active_set.len()); diff --git a/substrate/frame/staking/src/mock.rs b/substrate/frame/staking/src/mock.rs index 22afdb5a86e3..56df7001e417 100644 --- a/substrate/frame/staking/src/mock.rs +++ b/substrate/frame/staking/src/mock.rs @@ -290,7 +290,7 @@ impl OnStakingUpdate for EventListenerMock { } // Disabling threshold for `UpToByzantineThresholdDisablingStrategy` -pub(crate) const DISABLING_THRESHOLD_FACTOR: u32 = 3; +pub(crate) const DISABLING_THRESHOLD_FACTOR: usize = 3; impl crate::pallet::pallet::Config for Test { type Currency = Balances; diff --git a/substrate/frame/staking/src/slashing.rs b/substrate/frame/staking/src/slashing.rs index 6157cd548aec..6e9c724d0efe 100644 --- a/substrate/frame/staking/src/slashing.rs +++ b/substrate/frame/staking/src/slashing.rs @@ -323,28 +323,17 @@ fn kick_out_if_recent(params: SlashParams) { /// Inform the [`DisablingStrategy`] implementation about the new offender and disable the list of /// validators provided by [`make_disabling_decision`]. fn add_offending_validator(params: &SlashParams) { - let stash = params.stash; DisabledValidators::::mutate(|disabled| { - let validators = T::SessionInterface::validators(); - let validator_index = match validators.iter().position(|i| i == stash) { - Some(index) => index, - None => return, - }; - - let validator_index_u32 = validator_index as u32; - let disable_offenders = T::DisablingStrategy::make_disabling_decision( - validator_index as u32, + params.stash, params.slash_era, &disabled, - &validators, ); for offender in disable_offenders { // Add the validator to `DisabledValidators` and disable it. Do nothing if it is // already disabled. - if let Err(index) = disabled.binary_search_by_key(&validator_index_u32, |index| *index) - { + if let Err(index) = disabled.binary_search_by_key(&offender, |index| *index) { disabled.insert(index, offender); T::SessionInterface::disable_validator(offender); } From 55ccffe5d3eaaf0f3aaa8a438dea8ba722504bd4 Mon Sep 17 00:00:00 2001 From: Tsvetomir Dimitrov Date: Fri, 5 Jan 2024 11:26:31 +0200 Subject: [PATCH 38/67] UpToByzantineThresholdDisablingStrategy -> UpToThresholdDisablingStrategy --- polkadot/runtime/test-runtime/src/lib.rs | 2 +- polkadot/runtime/westend/src/lib.rs | 2 +- prdoc/pr_2226.prdoc | 6 ++-- substrate/bin/node/runtime/src/lib.rs | 2 +- substrate/frame/babe/src/mock.rs | 2 +- substrate/frame/beefy/src/mock.rs | 2 +- .../test-staking-e2e/src/lib.rs | 2 +- .../test-staking-e2e/src/mock.rs | 4 ++- substrate/frame/fast-unstake/src/mock.rs | 2 +- substrate/frame/grandpa/src/mock.rs | 2 +- .../nomination-pools/benchmarking/src/mock.rs | 2 +- .../nomination-pools/test-staking/src/mock.rs | 2 +- .../frame/offences/benchmarking/src/mock.rs | 2 +- substrate/frame/root-offences/src/mock.rs | 2 +- .../frame/session/benchmarking/src/mock.rs | 2 +- substrate/frame/staking/CHANGELOG.md | 2 +- substrate/frame/staking/src/lib.rs | 17 +++++----- substrate/frame/staking/src/mock.rs | 4 +-- substrate/frame/staking/src/tests.rs | 32 +++++++++---------- 19 files changed, 47 insertions(+), 44 deletions(-) diff --git a/polkadot/runtime/test-runtime/src/lib.rs b/polkadot/runtime/test-runtime/src/lib.rs index 9c792c14af8d..b7bb5752e2a8 100644 --- a/polkadot/runtime/test-runtime/src/lib.rs +++ b/polkadot/runtime/test-runtime/src/lib.rs @@ -358,7 +358,7 @@ impl pallet_staking::Config for Runtime { type BenchmarkingConfig = runtime_common::StakingBenchmarkingConfig; type EventListeners = (); type WeightInfo = (); - type DisablingStrategy = pallet_staking::UpToByzantineThresholdDisablingStrategy; + type DisablingStrategy = pallet_staking::UpToThresholdDisablingStrategy; } parameter_types! { diff --git a/polkadot/runtime/westend/src/lib.rs b/polkadot/runtime/westend/src/lib.rs index c8138aab4a07..3d71cc30a510 100644 --- a/polkadot/runtime/westend/src/lib.rs +++ b/polkadot/runtime/westend/src/lib.rs @@ -701,7 +701,7 @@ impl pallet_staking::Config for Runtime { type BenchmarkingConfig = runtime_common::StakingBenchmarkingConfig; type EventListeners = NominationPools; type WeightInfo = weights::pallet_staking::WeightInfo; - type DisablingStrategy = pallet_staking::UpToByzantineThresholdDisablingStrategy; + type DisablingStrategy = pallet_staking::UpToThresholdDisablingStrategy; } impl pallet_fast_unstake::Config for Runtime { diff --git a/prdoc/pr_2226.prdoc b/prdoc/pr_2226.prdoc index fd652bbbee71..fc75958d103c 100644 --- a/prdoc/pr_2226.prdoc +++ b/prdoc/pr_2226.prdoc @@ -10,12 +10,12 @@ doc: The disabling strategy in staking pallet is no longer hardcoded but abstracted away via DisablingStrategy trait. The trait contains a single function (make_disabling_decision) which is called for each offence. The function makes a decision if (and which) validators should be - disabled. A default implementation is provided - UpToByzantineThresholdDisablingStrategy. It - will be used on Kusama and Polkadot. In nutshel UpToByzantineThresholdDisablingStrategy + disabled. A default implementation is provided - UpToThresholdDisablingStrategy. It + will be used on Kusama and Polkadot. In nutshel UpToThresholdDisablingStrategy disables offenders up to the configured threshold (1/3 of the validators in the active set by default). Offending validators are not disabled for offences in previous eras. The threshold is controlled via `DISABLING_THRESHOLD_FACTOR` (a generic parameter of - UpToByzantineThresholdDisablingStrategy). + UpToThresholdDisablingStrategy). migrations: db: [] diff --git a/substrate/bin/node/runtime/src/lib.rs b/substrate/bin/node/runtime/src/lib.rs index f17ba2253337..af949f9ea3b5 100644 --- a/substrate/bin/node/runtime/src/lib.rs +++ b/substrate/bin/node/runtime/src/lib.rs @@ -671,7 +671,7 @@ impl pallet_staking::Config for Runtime { type EventListeners = NominationPools; type WeightInfo = pallet_staking::weights::SubstrateWeight; type BenchmarkingConfig = StakingBenchmarkingConfig; - type DisablingStrategy = pallet_staking::UpToByzantineThresholdDisablingStrategy; + type DisablingStrategy = pallet_staking::UpToThresholdDisablingStrategy; } impl pallet_fast_unstake::Config for Runtime { diff --git a/substrate/frame/babe/src/mock.rs b/substrate/frame/babe/src/mock.rs index d79a47076bf7..ab6f7a4bef02 100644 --- a/substrate/frame/babe/src/mock.rs +++ b/substrate/frame/babe/src/mock.rs @@ -185,7 +185,7 @@ impl pallet_staking::Config for Test { type EventListeners = (); type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig; type WeightInfo = (); - type DisablingStrategy = pallet_staking::UpToByzantineThresholdDisablingStrategy; + type DisablingStrategy = pallet_staking::UpToThresholdDisablingStrategy; } impl pallet_offences::Config for Test { diff --git a/substrate/frame/beefy/src/mock.rs b/substrate/frame/beefy/src/mock.rs index f9fa3c5f62ea..334c3e602675 100644 --- a/substrate/frame/beefy/src/mock.rs +++ b/substrate/frame/beefy/src/mock.rs @@ -203,7 +203,7 @@ impl pallet_staking::Config for Test { type EventListeners = (); type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig; type WeightInfo = (); - type DisablingStrategy = pallet_staking::UpToByzantineThresholdDisablingStrategy; + type DisablingStrategy = pallet_staking::UpToThresholdDisablingStrategy; } impl pallet_offences::Config for Test { diff --git a/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/lib.rs b/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/lib.rs index acc06d0b9319..dad378cd35f9 100644 --- a/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/lib.rs +++ b/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/lib.rs @@ -151,7 +151,7 @@ fn mass_slash_doesnt_enter_emergency_phase() { // And no more than `1/3` of the validators are disabled assert_eq!( Session::disabled_validators().len(), - pallet_staking::UpToByzantineThresholdDisablingStrategy::byzantine_threshold( + pallet_staking::UpToThresholdDisablingStrategy::::disable_threshold( Session::validators().len() ) ); diff --git a/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/mock.rs b/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/mock.rs index 46702ec93ab9..bdb3eb1e2db3 100644 --- a/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/mock.rs +++ b/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/mock.rs @@ -250,6 +250,8 @@ impl pallet_bags_list::Config for Runtime { /// Upper limit on the number of NPOS nominations. const MAX_QUOTA_NOMINATIONS: u32 = 16; +/// Disabling factor set explicitly to byzantine threshold +pub(crate) const DISABLING_FACTOR: usize = 3; impl pallet_staking::Config for Runtime { type Currency = Balances; @@ -278,7 +280,7 @@ impl pallet_staking::Config for Runtime { type EventListeners = (); type WeightInfo = pallet_staking::weights::SubstrateWeight; type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig; - type DisablingStrategy = pallet_staking::UpToByzantineThresholdDisablingStrategy; + type DisablingStrategy = pallet_staking::UpToThresholdDisablingStrategy; } impl frame_system::offchain::SendTransactionTypes for Runtime diff --git a/substrate/frame/fast-unstake/src/mock.rs b/substrate/frame/fast-unstake/src/mock.rs index 26bffd159c55..3fd603e357c1 100644 --- a/substrate/frame/fast-unstake/src/mock.rs +++ b/substrate/frame/fast-unstake/src/mock.rs @@ -144,7 +144,7 @@ impl pallet_staking::Config for Runtime { type EventListeners = (); type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig; type WeightInfo = (); - type DisablingStrategy = pallet_staking::UpToByzantineThresholdDisablingStrategy; + type DisablingStrategy = pallet_staking::UpToThresholdDisablingStrategy; } pub struct BalanceToU256; diff --git a/substrate/frame/grandpa/src/mock.rs b/substrate/frame/grandpa/src/mock.rs index c96c66a0c299..45e56cd8cefe 100644 --- a/substrate/frame/grandpa/src/mock.rs +++ b/substrate/frame/grandpa/src/mock.rs @@ -208,7 +208,7 @@ impl pallet_staking::Config for Test { type EventListeners = (); type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig; type WeightInfo = (); - type DisablingStrategy = pallet_staking::UpToByzantineThresholdDisablingStrategy; + type DisablingStrategy = pallet_staking::UpToThresholdDisablingStrategy; } impl pallet_offences::Config for Test { diff --git a/substrate/frame/nomination-pools/benchmarking/src/mock.rs b/substrate/frame/nomination-pools/benchmarking/src/mock.rs index d9fe1fd0d09b..f6e3be05fe30 100644 --- a/substrate/frame/nomination-pools/benchmarking/src/mock.rs +++ b/substrate/frame/nomination-pools/benchmarking/src/mock.rs @@ -123,7 +123,7 @@ impl pallet_staking::Config for Runtime { type EventListeners = Pools; type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig; type WeightInfo = (); - type DisablingStrategy = pallet_staking::UpToByzantineThresholdDisablingStrategy; + type DisablingStrategy = pallet_staking::UpToThresholdDisablingStrategy; } parameter_types! { diff --git a/substrate/frame/nomination-pools/test-staking/src/mock.rs b/substrate/frame/nomination-pools/test-staking/src/mock.rs index 10e7905fc957..5784a350dc1f 100644 --- a/substrate/frame/nomination-pools/test-staking/src/mock.rs +++ b/substrate/frame/nomination-pools/test-staking/src/mock.rs @@ -137,7 +137,7 @@ impl pallet_staking::Config for Runtime { type EventListeners = Pools; type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig; type WeightInfo = (); - type DisablingStrategy = pallet_staking::UpToByzantineThresholdDisablingStrategy; + type DisablingStrategy = pallet_staking::UpToThresholdDisablingStrategy; } parameter_types! { diff --git a/substrate/frame/offences/benchmarking/src/mock.rs b/substrate/frame/offences/benchmarking/src/mock.rs index 3d2b9c9789c7..c64400ce5729 100644 --- a/substrate/frame/offences/benchmarking/src/mock.rs +++ b/substrate/frame/offences/benchmarking/src/mock.rs @@ -188,7 +188,7 @@ impl pallet_staking::Config for Test { type EventListeners = (); type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig; type WeightInfo = (); - type DisablingStrategy = pallet_staking::UpToByzantineThresholdDisablingStrategy; + type DisablingStrategy = pallet_staking::UpToThresholdDisablingStrategy; } impl pallet_im_online::Config for Test { diff --git a/substrate/frame/root-offences/src/mock.rs b/substrate/frame/root-offences/src/mock.rs index 12502080764d..b4e5d2d326e0 100644 --- a/substrate/frame/root-offences/src/mock.rs +++ b/substrate/frame/root-offences/src/mock.rs @@ -190,7 +190,7 @@ impl pallet_staking::Config for Test { type EventListeners = (); type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig; type WeightInfo = (); - type DisablingStrategy = pallet_staking::UpToByzantineThresholdDisablingStrategy; + type DisablingStrategy = pallet_staking::UpToThresholdDisablingStrategy; } impl pallet_session::historical::Config for Test { diff --git a/substrate/frame/session/benchmarking/src/mock.rs b/substrate/frame/session/benchmarking/src/mock.rs index 4b5c6d8e79cd..6b749ce02fc5 100644 --- a/substrate/frame/session/benchmarking/src/mock.rs +++ b/substrate/frame/session/benchmarking/src/mock.rs @@ -185,7 +185,7 @@ impl pallet_staking::Config for Test { type EventListeners = (); type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig; type WeightInfo = (); - type DisablingStrategy = pallet_staking::UpToByzantineThresholdDisablingStrategy; + type DisablingStrategy = pallet_staking::UpToThresholdDisablingStrategy; } impl crate::Config for Test {} diff --git a/substrate/frame/staking/CHANGELOG.md b/substrate/frame/staking/CHANGELOG.md index 6327acdc5c70..4719997f5182 100644 --- a/substrate/frame/staking/CHANGELOG.md +++ b/substrate/frame/staking/CHANGELOG.md @@ -13,7 +13,7 @@ migrations. - New trait `DisablingStrategy` which is responsible for making a decision which offenders should be disabled on new offence. -- Default implementation of `DisablingStrategy` - `UpToByzantineThresholdDisablingStrategy`. It +- Default implementation of `DisablingStrategy` - `UpToThresholdDisablingStrategy`. It disables each new offender up to a threshold (1/3 by default). Offenders are not punished for offences in previous era(s). - `OffendingValidators` storage item is replaced with `DisabledValidators`. The former keeps all diff --git a/substrate/frame/staking/src/lib.rs b/substrate/frame/staking/src/lib.rs index 850688de3f80..766ddc661db6 100644 --- a/substrate/frame/staking/src/lib.rs +++ b/substrate/frame/staking/src/lib.rs @@ -1239,20 +1239,21 @@ pub trait DisablingStrategy { /// threshold. `DISABLING_THRESHOLD_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. -pub struct UpToByzantineThresholdDisablingStrategy; +/// By default a factor of 3 is used which is the byzantine threshold. +pub struct UpToThresholdDisablingStrategy; impl - UpToByzantineThresholdDisablingStrategy + UpToThresholdDisablingStrategy { /// Disabling limit calculated from the total number of validators in the active set. When /// reached no more validators will be disabled. - pub fn byzantine_threshold(validators_len: usize) -> usize { + pub fn disable_threshold(validators_len: usize) -> usize { validators_len.saturating_sub(1) / DISABLING_THRESHOLD_FACTOR } } impl DisablingStrategy - for UpToByzantineThresholdDisablingStrategy + for UpToThresholdDisablingStrategy { fn make_disabling_decision( offender_stash: &T::AccountId, @@ -1267,14 +1268,14 @@ impl DisablingStrategy return vec![] }; - // We don't disable more than 1/3 of the validators in the active set - let over_byzantine_threshold = - currently_disabled.len() >= Self::byzantine_threshold(active_set.len()); + // We don't disable more than the threshold + let over_threshold = + currently_disabled.len() >= Self::disable_threshold(active_set.len()); // We don't disable for offences in previous eras let ancient_offence = Pallet::::current_era().unwrap_or(1) > slash_era; let disable_offenders = - if over_byzantine_threshold || ancient_offence { vec![] } else { vec![offender_idx] }; + if over_threshold || ancient_offence { vec![] } else { vec![offender_idx] }; disable_offenders } diff --git a/substrate/frame/staking/src/mock.rs b/substrate/frame/staking/src/mock.rs index 56df7001e417..fbb5a8713e02 100644 --- a/substrate/frame/staking/src/mock.rs +++ b/substrate/frame/staking/src/mock.rs @@ -289,7 +289,7 @@ impl OnStakingUpdate for EventListenerMock { } } -// Disabling threshold for `UpToByzantineThresholdDisablingStrategy` +// Disabling threshold for `UpToThresholdDisablingStrategy` pub(crate) const DISABLING_THRESHOLD_FACTOR: usize = 3; impl crate::pallet::pallet::Config for Test { @@ -321,7 +321,7 @@ impl crate::pallet::pallet::Config for Test { type BenchmarkingConfig = TestBenchmarkingConfig; type WeightInfo = (); type DisablingStrategy = - pallet_staking::UpToByzantineThresholdDisablingStrategy; + pallet_staking::UpToThresholdDisablingStrategy; } pub struct WeightedNominationsQuota; diff --git a/substrate/frame/staking/src/tests.rs b/substrate/frame/staking/src/tests.rs index b69903bb3817..c043f0c20d34 100644 --- a/substrate/frame/staking/src/tests.rs +++ b/substrate/frame/staking/src/tests.rs @@ -3410,7 +3410,7 @@ fn offence_threshold_doesnt_trigger_new_era() { assert_eq_uvec!(Session::validators(), vec![11, 21, 31, 41]); assert_eq!( - UpToByzantineThresholdDisablingStrategy::::byzantine_threshold( + UpToThresholdDisablingStrategy::::disable_threshold( Session::validators().len() ), 1 @@ -6892,27 +6892,27 @@ mod ledger { mod byzantine_threshold_disabling_strategy { use crate::{ - tests::Test, CurrentEra, DisablingStrategy, UpToByzantineThresholdDisablingStrategy, + tests::Test, CurrentEra, DisablingStrategy, UpToThresholdDisablingStrategy, }; use sp_staking::EraIndex; - // Common test data - the validator index of the offender and the era of the offence - const OFFENDER_IDX: u32 = 7; + // Common test data - the stash of the offending validator, the era of the offence and the active set + const OFFENDER_ID: ::AccountId = 7; const SLASH_ERA: EraIndex = 1; + const ACTIVE_SET: [::ValidatorId; 7] = [1, 2, 3, 4, 5, 6, 7]; + const OFFENDER_VALIDATOR_IDX: u32 = 6; // the offender is with index 6 in the active set #[test] fn dont_disable_for_ancient_offence() { sp_io::TestExternalities::default().execute_with(|| { let initially_disabled = vec![]; - let active_set = vec![1, 2, 3, 4, 5, 6, 7]; - - // Current era is 2 + pallet_session::Validators::::put(ACTIVE_SET.to_vec()); CurrentEra::::put(2); - let disable_offenders = >::make_disabling_decision( - OFFENDER_IDX, SLASH_ERA, &initially_disabled, &active_set + &OFFENDER_ID, SLASH_ERA, &initially_disabled ); assert!(disable_offenders.is_empty()); @@ -6923,12 +6923,12 @@ mod byzantine_threshold_disabling_strategy { fn dont_disable_beyond_byzantine_threshold() { sp_io::TestExternalities::default().execute_with(|| { let initially_disabled = vec![1, 2]; - let active_set = vec![1, 2, 3, 4, 5, 6, 7]; + pallet_session::Validators::::put(ACTIVE_SET.to_vec()); - let disable_offenders = >::make_disabling_decision( - OFFENDER_IDX, SLASH_ERA, &initially_disabled, &active_set + &OFFENDER_ID, SLASH_ERA, &initially_disabled ); assert!(disable_offenders.is_empty()); @@ -6939,15 +6939,15 @@ mod byzantine_threshold_disabling_strategy { fn disable_when_below_byzantine_threshold() { sp_io::TestExternalities::default().execute_with(|| { let initially_disabled = vec![1]; - let active_set = vec![1, 2, 3, 4, 5, 6, 7]; + pallet_session::Validators::::put(ACTIVE_SET.to_vec()); - let disable_offenders = >::make_disabling_decision( - OFFENDER_IDX, SLASH_ERA, &initially_disabled, &active_set + &OFFENDER_ID, SLASH_ERA, &initially_disabled ); - assert_eq!(disable_offenders, vec![7]); + assert_eq!(disable_offenders, vec![OFFENDER_VALIDATOR_IDX]); }); } } From 52de0f5bfff5dc38084e1aea6921c534d92cf705 Mon Sep 17 00:00:00 2001 From: Tsvetomir Dimitrov Date: Fri, 5 Jan 2024 13:00:16 +0200 Subject: [PATCH 39/67] Remove `set_validation_intention_after_chilled` from test-staking-e2e. With chilling removed the test is no longer necessary. --- .../test-staking-e2e/src/lib.rs | 69 ------------------- 1 file changed, 69 deletions(-) diff --git a/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/lib.rs b/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/lib.rs index dad378cd35f9..bc762a5be0d9 100644 --- a/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/lib.rs +++ b/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/lib.rs @@ -211,75 +211,6 @@ fn continous_slashes_below_offending_threshold() { }); } -#[test] -/// Slashed validator sets intentions in the same era of slashing. -/// -/// When validators are slashed, they are chilled and removed from the current `VoterList`. Thus, -/// the slashed validator should not be considered in the next validator set. However, if the -/// slashed validator sets its intention to validate again in the same era when it was slashed and -/// chilled, the validator may not be removed from the active validator set across eras, provided -/// it would selected in the subsequent era if there was no slash. Nominators of the slashed -/// validator will also be slashed and chilled, as expected, but the nomination intentions will -/// remain after the validator re-set the intention to be validating again. -/// -/// This behaviour is due to removing implicit chill upon slash -/// . -/// -/// Related to . -fn set_validation_intention_after_chilled() { - use frame_election_provider_support::SortedListProvider; - use pallet_staking::{Event, Nominators}; - - let (mut ext, pool_state, _) = ExtBuilder::default() - .epm(EpmExtBuilder::default()) - .staking(StakingExtBuilder::default()) - .build_offchainify(); - - ext.execute_with(|| { - assert_eq!(active_era(), 0); - // validator is part of the validator set. - assert!(Session::validators().contains(&41)); - assert!(::VoterList::contains(&41)); - - // nominate validator 81. - assert_ok!(Staking::nominate(RuntimeOrigin::signed(21), vec![41])); - assert_eq!(Nominators::::get(21).unwrap().targets, vec![41]); - - // validator is slashed. it is removed from the `VoterList` through chilling but in the - // current era, the validator is still part of the active validator set. - add_slash(&41); - assert!(Session::validators().contains(&41)); - assert!(!::VoterList::contains(&41)); - assert_eq!( - staking_events(), - [ - Event::Chilled { stash: 41 }, - Event::SlashReported { - validator: 41, - slash_era: 0, - fraction: Perbill::from_percent(10) - } - ], - ); - - // after the nominator is slashed and chilled, the nominations remain. - assert_eq!(Nominators::::get(21).unwrap().targets, vec![41]); - - // validator sets intention to stake again in the same era it was chilled. - assert_ok!(Staking::validate(RuntimeOrigin::signed(41), Default::default())); - - // progress era and check that the slashed validator is still part of the validator - // set. - assert!(start_next_active_era(pool_state).is_ok()); - assert_eq!(active_era(), 1); - assert!(Session::validators().contains(&41)); - assert!(::VoterList::contains(&41)); - - // nominations are still active as before the slash. - assert_eq!(Nominators::::get(21).unwrap().targets, vec![41]); - }) -} - #[test] /// Active ledger balance may fall below ED if account chills before unbounding. /// From 87af2932c518669d538378eb6b7ed7fae4a29c16 Mon Sep 17 00:00:00 2001 From: Tsvetomir Dimitrov Date: Fri, 5 Jan 2024 13:05:57 +0200 Subject: [PATCH 40/67] fmt --- substrate/frame/staking/src/lib.rs | 3 +-- substrate/frame/staking/src/tests.rs | 9 ++++----- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/substrate/frame/staking/src/lib.rs b/substrate/frame/staking/src/lib.rs index 766ddc661db6..274a7c938d16 100644 --- a/substrate/frame/staking/src/lib.rs +++ b/substrate/frame/staking/src/lib.rs @@ -1269,8 +1269,7 @@ impl DisablingStrategy }; // We don't disable more than the threshold - let over_threshold = - currently_disabled.len() >= Self::disable_threshold(active_set.len()); + let over_threshold = currently_disabled.len() >= Self::disable_threshold(active_set.len()); // We don't disable for offences in previous eras let ancient_offence = Pallet::::current_era().unwrap_or(1) > slash_era; diff --git a/substrate/frame/staking/src/tests.rs b/substrate/frame/staking/src/tests.rs index c043f0c20d34..23e5c11ce0e7 100644 --- a/substrate/frame/staking/src/tests.rs +++ b/substrate/frame/staking/src/tests.rs @@ -6891,16 +6891,15 @@ mod ledger { } mod byzantine_threshold_disabling_strategy { - use crate::{ - tests::Test, CurrentEra, DisablingStrategy, UpToThresholdDisablingStrategy, - }; + use crate::{tests::Test, CurrentEra, DisablingStrategy, UpToThresholdDisablingStrategy}; use sp_staking::EraIndex; - // Common test data - the stash of the offending validator, the era of the offence and the active set + // Common test data - the stash of the offending validator, the era of the offence and the + // active set const OFFENDER_ID: ::AccountId = 7; const SLASH_ERA: EraIndex = 1; const ACTIVE_SET: [::ValidatorId; 7] = [1, 2, 3, 4, 5, 6, 7]; - const OFFENDER_VALIDATOR_IDX: u32 = 6; // the offender is with index 6 in the active set + const OFFENDER_VALIDATOR_IDX: u32 = 6; // the offender is with index 6 in the active set #[test] fn dont_disable_for_ancient_offence() { From 93f5189a9e1592eb86e50ab844312b4f86d03839 Mon Sep 17 00:00:00 2001 From: Tsvetomir Dimitrov Date: Fri, 5 Jan 2024 15:34:34 +0200 Subject: [PATCH 41/67] Fix a compilation error from merge --- .../runtime/parachains/src/paras_inherent/tests.rs | 10 ++++++++-- polkadot/runtime/parachains/src/scheduler.rs | 4 +--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/polkadot/runtime/parachains/src/paras_inherent/tests.rs b/polkadot/runtime/parachains/src/paras_inherent/tests.rs index 6ffa1092d7a1..6f3eac35685a 100644 --- a/polkadot/runtime/parachains/src/paras_inherent/tests.rs +++ b/polkadot/runtime/parachains/src/paras_inherent/tests.rs @@ -1300,11 +1300,17 @@ mod sanitizers { scheduler::Pallet::::set_claimqueue(BTreeMap::from([ ( CoreIndex::from(0), - VecDeque::from([Some(ParasEntry::new(Assignment::new(1.into()), 1))]), + VecDeque::from([ParasEntry::new( + Assignment::Pool { para_id: 1.into(), core_index: CoreIndex(1) }, + RELAY_PARENT_NUM, + )]), ), ( CoreIndex::from(1), - VecDeque::from([Some(ParasEntry::new(Assignment::new(2.into()), 1))]), + VecDeque::from([ParasEntry::new( + Assignment::Pool { para_id: 2.into(), core_index: CoreIndex(1) }, + RELAY_PARENT_NUM, + )]), ), ])); diff --git a/polkadot/runtime/parachains/src/scheduler.rs b/polkadot/runtime/parachains/src/scheduler.rs index 5224dc3cd70d..a666f5689089 100644 --- a/polkadot/runtime/parachains/src/scheduler.rs +++ b/polkadot/runtime/parachains/src/scheduler.rs @@ -693,9 +693,7 @@ impl Pallet { } #[cfg(test)] - pub(crate) fn set_claimqueue( - claimqueue: BTreeMap>>>>, - ) { + pub(crate) fn set_claimqueue(claimqueue: BTreeMap>>) { ClaimQueue::::set(claimqueue); } } From ceb6941651179bd6047b0d92163047c4a1b42fda Mon Sep 17 00:00:00 2001 From: Tsvetomir Dimitrov Date: Mon, 15 Jan 2024 08:57:31 +0200 Subject: [PATCH 42/67] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Gonçalo Pestana --- prdoc/pr_2226.prdoc | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/prdoc/pr_2226.prdoc b/prdoc/pr_2226.prdoc index fc75958d103c..01d8fba072c4 100644 --- a/prdoc/pr_2226.prdoc +++ b/prdoc/pr_2226.prdoc @@ -8,22 +8,22 @@ doc: - audience: Runtime Dev description: | The disabling strategy in staking pallet is no longer hardcoded but abstracted away via - DisablingStrategy trait. The trait contains a single function (make_disabling_decision) which + `DisablingStrategy` trait. The trait contains a single function (make_disabling_decision) which is called for each offence. The function makes a decision if (and which) validators should be - disabled. A default implementation is provided - UpToThresholdDisablingStrategy. It - will be used on Kusama and Polkadot. In nutshel UpToThresholdDisablingStrategy + disabled. A default implementation is provided - `UpToThresholdDisablingStrategy`. It + will be used on Kusama and Polkadot. In nutshell `UpToThresholdDisablingStrategy` disables offenders up to the configured threshold (1/3 of the validators in the active set by default). Offending validators are not disabled for offences in previous eras. The threshold is controlled via `DISABLING_THRESHOLD_FACTOR` (a generic parameter of - UpToThresholdDisablingStrategy). + `UpToThresholdDisablingStrategy`). migrations: db: [] runtime: - reference: pallet-staking description: | - Renames OffendingValidators storage item to DisabledValidators and changes its type from - Vec<(u32, bool)> to Vec + Renames `OffendingValidators` storage item to `DisabledValidators` and changes its type from + `Vec<(u32, bool)>` to `Vec`. crates: - name: pallet-staking \ No newline at end of file From 8d56d2f6baa4f4bcf5d12a0014d1bdb58271b772 Mon Sep 17 00:00:00 2001 From: Tsvetomir Dimitrov Date: Mon, 15 Jan 2024 11:03:15 +0200 Subject: [PATCH 43/67] Return early in `make_disabling_decision` implementation for `UpToThresholdDisablingStrategy` if disabled validators are above the disabling threshold. Reason - avoid unnecessary storage reads. --- substrate/frame/staking/src/lib.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/substrate/frame/staking/src/lib.rs b/substrate/frame/staking/src/lib.rs index 274a7c938d16..c00e589332ae 100644 --- a/substrate/frame/staking/src/lib.rs +++ b/substrate/frame/staking/src/lib.rs @@ -1269,13 +1269,15 @@ impl DisablingStrategy }; // We don't disable more than the threshold - let over_threshold = currently_disabled.len() >= Self::disable_threshold(active_set.len()); - // We don't disable for offences in previous eras - let ancient_offence = Pallet::::current_era().unwrap_or(1) > slash_era; + if currently_disabled.len() >= Self::disable_threshold(active_set.len()) { + return vec![] + } - let disable_offenders = - if over_threshold || ancient_offence { vec![] } else { vec![offender_idx] }; + // We don't disable for offences in previous eras + if Pallet::::current_era().unwrap_or(1) > slash_era { + return vec![] + } - disable_offenders + vec![offender_idx] } } From db43ae18ad07db29fdf9a4d1ac75c101b2c09092 Mon Sep 17 00:00:00 2001 From: Tsvetomir Dimitrov Date: Mon, 15 Jan 2024 16:56:34 +0200 Subject: [PATCH 44/67] Remove stale TODO --- substrate/frame/staking/CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/substrate/frame/staking/CHANGELOG.md b/substrate/frame/staking/CHANGELOG.md index 4719997f5182..427e993b8138 100644 --- a/substrate/frame/staking/CHANGELOG.md +++ b/substrate/frame/staking/CHANGELOG.md @@ -19,7 +19,6 @@ migrations. - `OffendingValidators` storage item is replaced with `DisabledValidators`. The former keeps all offenders and if they are disabled or not. The latter just keeps a list of all offenders as they are disabled by default. -- TODO: STORAGE ITEM FOR DISABLING THRESHOLD ### Deprecated From 18eae58633e60672ba4634e15a00026e3ca3edce Mon Sep 17 00:00:00 2001 From: Tsvetomir Dimitrov Date: Mon, 15 Jan 2024 17:11:26 +0200 Subject: [PATCH 45/67] `DISABLING_FACTOR` becomes `SLASHING_DISABLING_FACTOR` in test-staking-e2e mock --- .../test-staking-e2e/src/lib.rs | 4 ++-- .../test-staking-e2e/src/mock.rs | 5 +++-- substrate/frame/staking/src/pallet/mod.rs | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/lib.rs b/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/lib.rs index bc762a5be0d9..69fbe785111a 100644 --- a/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/lib.rs +++ b/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/lib.rs @@ -129,7 +129,7 @@ fn offchainify_works() { /// Inspired by the Kusama incident of 8th Dec 2022 and its resolution through the governance /// fallback. /// -/// Mass slash of validators shoudn't disable more than 1/3 of them (the byzantine threshold). Also +/// Mass slash of validators shouldn't disable more than 1/3 of them (the byzantine threshold). Also /// no new era should be forced which could lead to EPM entering emergency mode. fn mass_slash_doesnt_enter_emergency_phase() { let epm_builder = EpmExtBuilder::default().disable_emergency_throttling(); @@ -151,7 +151,7 @@ fn mass_slash_doesnt_enter_emergency_phase() { // And no more than `1/3` of the validators are disabled assert_eq!( Session::disabled_validators().len(), - pallet_staking::UpToThresholdDisablingStrategy::::disable_threshold( + pallet_staking::UpToThresholdDisablingStrategy::::disable_threshold( Session::validators().len() ) ); diff --git a/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/mock.rs b/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/mock.rs index 1e838dbfa9c5..140d9e994595 100644 --- a/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/mock.rs +++ b/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/mock.rs @@ -250,7 +250,7 @@ impl pallet_bags_list::Config for Runtime { /// Upper limit on the number of NPOS nominations. const MAX_QUOTA_NOMINATIONS: u32 = 16; /// Disabling factor set explicitly to byzantine threshold -pub(crate) const DISABLING_FACTOR: usize = 3; +pub(crate) const SLASHING_DISABLING_FACTOR: usize = 3; impl pallet_staking::Config for Runtime { type Currency = Balances; @@ -280,7 +280,8 @@ impl pallet_staking::Config for Runtime { type EventListeners = (); type WeightInfo = pallet_staking::weights::SubstrateWeight; type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig; - type DisablingStrategy = pallet_staking::UpToThresholdDisablingStrategy; + type DisablingStrategy = + pallet_staking::UpToThresholdDisablingStrategy; } impl frame_system::offchain::SendTransactionTypes for Runtime diff --git a/substrate/frame/staking/src/pallet/mod.rs b/substrate/frame/staking/src/pallet/mod.rs index 7b4704183363..4c5304948665 100644 --- a/substrate/frame/staking/src/pallet/mod.rs +++ b/substrate/frame/staking/src/pallet/mod.rs @@ -274,7 +274,7 @@ pub mod pallet { /// WARNING: this only reports slashing events for the time being. type EventListeners: sp_staking::OnStakingUpdate>; - // `DisablingStragegy` controlls how validators are disabled + // `DisablingStragegy` controls how validators are disabled type DisablingStrategy: DisablingStrategy; /// Some parameters of the benchmarking. From cec4bbe89d779abf014b961bc1e7acdf090b39e4 Mon Sep 17 00:00:00 2001 From: Tsvetomir Dimitrov Date: Mon, 15 Jan 2024 18:23:28 +0200 Subject: [PATCH 46/67] Don't divide by 0 in `disable_threshold` from `UpToThresholdDisablingStrategy` --- substrate/frame/staking/src/lib.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/substrate/frame/staking/src/lib.rs b/substrate/frame/staking/src/lib.rs index c00e589332ae..93e6129614f7 100644 --- a/substrate/frame/staking/src/lib.rs +++ b/substrate/frame/staking/src/lib.rs @@ -1248,7 +1248,10 @@ impl /// Disabling limit calculated from the total number of validators in the active set. When /// reached no more validators will be disabled. pub fn disable_threshold(validators_len: usize) -> usize { - validators_len.saturating_sub(1) / DISABLING_THRESHOLD_FACTOR + validators_len + .saturating_sub(1) + .checked_div(DISABLING_THRESHOLD_FACTOR) + .unwrap_or(0) } } From 859770ab9f4f94596faa20bb78d19a96d9ddbaed Mon Sep 17 00:00:00 2001 From: Tsvetomir Dimitrov Date: Tue, 16 Jan 2024 10:53:37 +0200 Subject: [PATCH 47/67] Ensure only the expected set of validators is disabled in `mass_slash_doesnt_enter_emergency_phase` from `test-staking-e2e` --- .../test-staking-e2e/src/lib.rs | 31 +++++++++++++------ .../test-staking-e2e/src/mock.rs | 17 +++++----- 2 files changed, 29 insertions(+), 19 deletions(-) diff --git a/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/lib.rs b/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/lib.rs index 69fbe785111a..9344fad9d390 100644 --- a/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/lib.rs +++ b/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/lib.rs @@ -142,19 +142,30 @@ fn mass_slash_doesnt_enter_emergency_phase() { ext.execute_with(|| { assert_eq!(pallet_staking::ForceEra::::get(), pallet_staking::Forcing::NotForcing); + let active_set_size_before_slash = Session::validators().len(); + // Slash more than 1/3 of the active validators - slash_half_the_active_set(); + let mut slashed = slash_half_the_active_set(); - // We are not forcing a new era - assert_eq!(pallet_staking::ForceEra::::get(), pallet_staking::Forcing::NotForcing); + let active_set_size_after_slash = Session::validators().len(); - // And no more than `1/3` of the validators are disabled - assert_eq!( - Session::disabled_validators().len(), - pallet_staking::UpToThresholdDisablingStrategy::::disable_threshold( - Session::validators().len() - ) - ); + // active set should stay the same before and after the slash + assert_eq!(active_set_size_before_slash, active_set_size_after_slash); + + // Slashed validators are disabled up to a threshold + slashed.truncate(pallet_staking::UpToThresholdDisablingStrategy::< + SLASHING_DISABLING_FACTOR, + >::disable_threshold(active_set_size_after_slash)); + + // Find the indices of the disabled validators + let active_set = Session::validators(); + let expected_disabled = slashed + .into_iter() + .map(|d| active_set.iter().position(|a| *a == d).unwrap() as u32) + .collect::>(); + + assert_eq!(pallet_staking::ForceEra::::get(), pallet_staking::Forcing::NotForcing); + assert_eq!(Session::disabled_validators(), expected_disabled); }); } diff --git a/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/mock.rs b/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/mock.rs index 140d9e994595..fc21ab20436f 100644 --- a/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/mock.rs +++ b/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/mock.rs @@ -816,17 +816,16 @@ pub(crate) fn add_slash(who: &AccountId) { ); } -// Slashes 1/2 of the active set -pub(crate) fn slash_half_the_active_set() { - let validators = Session::validators(); - let mut remaining_slashes = validators.len() / 2; +// Slashes 1/2 of the active set. Returns the `AccountId`s of the slashed validators. +pub(crate) fn slash_half_the_active_set() -> Vec { + let mut slashed = Session::validators(); + slashed.truncate(slashed.len() / 2); - for v in validators.into_iter() { - if remaining_slashes != 0 { - add_slash(&v); - remaining_slashes -= 1; - } + for v in slashed.iter() { + add_slash(v); } + + slashed } // Slashes a percentage of the active nominators that haven't been slashed yet, with From 3511b065c058674d4db691b00112ec5f886e727f Mon Sep 17 00:00:00 2001 From: Tsvetomir Dimitrov Date: Tue, 16 Jan 2024 16:46:10 +0200 Subject: [PATCH 48/67] Use `defensive!` in disabling threshold calculation --- substrate/frame/staking/src/lib.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/substrate/frame/staking/src/lib.rs b/substrate/frame/staking/src/lib.rs index 93e6129614f7..744a11eab944 100644 --- a/substrate/frame/staking/src/lib.rs +++ b/substrate/frame/staking/src/lib.rs @@ -1251,7 +1251,10 @@ impl validators_len .saturating_sub(1) .checked_div(DISABLING_THRESHOLD_FACTOR) - .unwrap_or(0) + .unwrap_or_else(|| { + defensive!("DISABLING_THRESHOLD_FACTOR should not be 0"); + 0 + }) } } From 9a775a53b2a6c8aad2ffcdd0c8ea443e7466b507 Mon Sep 17 00:00:00 2001 From: Tsvetomir Dimitrov Date: Wed, 17 Jan 2024 09:59:30 +0200 Subject: [PATCH 49/67] `make_disabling_decision` returns an `Option` instead of a `Vec` --- substrate/frame/staking/src/lib.rs | 12 ++++++------ substrate/frame/staking/src/slashing.rs | 10 +++------- substrate/frame/staking/src/tests.rs | 12 ++++++------ 3 files changed, 15 insertions(+), 19 deletions(-) diff --git a/substrate/frame/staking/src/lib.rs b/substrate/frame/staking/src/lib.rs index 744a11eab944..a5126098376f 100644 --- a/substrate/frame/staking/src/lib.rs +++ b/substrate/frame/staking/src/lib.rs @@ -1232,7 +1232,7 @@ pub trait DisablingStrategy { offender_stash: &T::AccountId, slash_era: EraIndex, currently_disabled: &Vec, - ) -> Vec; + ) -> Option; } /// Implementation of [`DisablingStrategy`] which disables validators from the active set up to a @@ -1265,25 +1265,25 @@ impl DisablingStrategy offender_stash: &T::AccountId, slash_era: EraIndex, currently_disabled: &Vec, - ) -> Vec { + ) -> Option { let active_set = T::SessionInterface::validators(); let offender_idx = if let Some(idx) = active_set.iter().position(|i| i == offender_stash) { idx as u32 } else { // offender not found in the active set, do nothing - return vec![] + return None }; // We don't disable more than the threshold if currently_disabled.len() >= Self::disable_threshold(active_set.len()) { - return vec![] + return None } // We don't disable for offences in previous eras if Pallet::::current_era().unwrap_or(1) > slash_era { - return vec![] + return None } - vec![offender_idx] + Some(offender_idx) } } diff --git a/substrate/frame/staking/src/slashing.rs b/substrate/frame/staking/src/slashing.rs index 6e9c724d0efe..9654d4264a88 100644 --- a/substrate/frame/staking/src/slashing.rs +++ b/substrate/frame/staking/src/slashing.rs @@ -324,13 +324,9 @@ fn kick_out_if_recent(params: SlashParams) { /// validators provided by [`make_disabling_decision`]. fn add_offending_validator(params: &SlashParams) { DisabledValidators::::mutate(|disabled| { - let disable_offenders = T::DisablingStrategy::make_disabling_decision( - params.stash, - params.slash_era, - &disabled, - ); - - for offender in disable_offenders { + if let Some(offender) = + T::DisablingStrategy::make_disabling_decision(params.stash, params.slash_era, &disabled) + { // Add the validator to `DisabledValidators` and disable it. Do nothing if it is // already disabled. if let Err(index) = disabled.binary_search_by_key(&offender, |index| *index) { diff --git a/substrate/frame/staking/src/tests.rs b/substrate/frame/staking/src/tests.rs index 1807c3ab4436..3b1527892600 100644 --- a/substrate/frame/staking/src/tests.rs +++ b/substrate/frame/staking/src/tests.rs @@ -7085,13 +7085,13 @@ mod byzantine_threshold_disabling_strategy { pallet_session::Validators::::put(ACTIVE_SET.to_vec()); CurrentEra::::put(2); - let disable_offenders = >::make_disabling_decision( &OFFENDER_ID, SLASH_ERA, &initially_disabled ); - assert!(disable_offenders.is_empty()); + assert!(disable_offender.is_none()); }); } @@ -7101,13 +7101,13 @@ mod byzantine_threshold_disabling_strategy { let initially_disabled = vec![1, 2]; pallet_session::Validators::::put(ACTIVE_SET.to_vec()); - let disable_offenders = >::make_disabling_decision( &OFFENDER_ID, SLASH_ERA, &initially_disabled ); - assert!(disable_offenders.is_empty()); + assert!(disable_offender.is_none()); }); } @@ -7117,13 +7117,13 @@ mod byzantine_threshold_disabling_strategy { let initially_disabled = vec![1]; pallet_session::Validators::::put(ACTIVE_SET.to_vec()); - let disable_offenders = >::make_disabling_decision( &OFFENDER_ID, SLASH_ERA, &initially_disabled ); - assert_eq!(disable_offenders, vec![OFFENDER_VALIDATOR_IDX]); + assert_eq!(disable_offender, Some(OFFENDER_VALIDATOR_IDX)); }); } } From 0111e699e796cc633a3a4b678e3a1639ffe758cc Mon Sep 17 00:00:00 2001 From: Tsvetomir Dimitrov Date: Wed, 17 Jan 2024 10:06:04 +0200 Subject: [PATCH 50/67] Update substrate/frame/staking/CHANGELOG.md Co-authored-by: Maciej --- substrate/frame/staking/CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/substrate/frame/staking/CHANGELOG.md b/substrate/frame/staking/CHANGELOG.md index 427e993b8138..d2b0f314bea0 100644 --- a/substrate/frame/staking/CHANGELOG.md +++ b/substrate/frame/staking/CHANGELOG.md @@ -14,8 +14,8 @@ migrations. - New trait `DisablingStrategy` which is responsible for making a decision which offenders should be disabled on new offence. - Default implementation of `DisablingStrategy` - `UpToThresholdDisablingStrategy`. It - disables each new offender up to a threshold (1/3 by default). Offenders are not punished for - offences in previous era(s). + disables each new offender up to a threshold (1/3 by default). Offenders are not runtime disabled for + offences in previous era(s). But they will be low-priority node-side disabled for dispute initiation. - `OffendingValidators` storage item is replaced with `DisabledValidators`. The former keeps all offenders and if they are disabled or not. The latter just keeps a list of all offenders as they are disabled by default. From 460ffd6b09ec24776139ec3e34b9df1e383f3833 Mon Sep 17 00:00:00 2001 From: Tsvetomir Dimitrov Date: Wed, 28 Feb 2024 10:14:15 +0200 Subject: [PATCH 51/67] Fix a compilation error after merge --- .../election-provider-multi-phase/test-staking-e2e/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/lib.rs b/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/lib.rs index c5769f821047..69bb194dd90f 100644 --- a/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/lib.rs +++ b/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/lib.rs @@ -134,7 +134,7 @@ fn offchainify_works() { fn mass_slash_doesnt_enter_emergency_phase() { let epm_builder = EpmExtBuilder::default().disable_emergency_throttling(); let staking_builder = StakingExtBuilder::default().validator_count(7); - let (ext, _, _) = ExtBuilder::default() + let (mut ext, _, _) = ExtBuilder::default() .epm(epm_builder) .staking(staking_builder) .build_offchainify(); From 80fabc5759fd19276758be6ae32cac91faf3ed46 Mon Sep 17 00:00:00 2001 From: Tsvetomir Dimitrov Date: Tue, 12 Mar 2024 14:15:37 +0200 Subject: [PATCH 52/67] Update substrate/frame/staking/src/migrations.rs Co-authored-by: Kian Paimani <5588131+kianenigma@users.noreply.github.com> --- substrate/frame/staking/src/migrations.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/substrate/frame/staking/src/migrations.rs b/substrate/frame/staking/src/migrations.rs index fc7f17c9275e..0f5447020cda 100644 --- a/substrate/frame/staking/src/migrations.rs +++ b/substrate/frame/staking/src/migrations.rs @@ -64,7 +64,7 @@ type StorageVersion = StorageValue, ObsoleteReleases, Value pub mod v15 { use super::*; - pub struct VersionUncheckedMigrateV14ToV15(sp_std::marker::PhantomData); + struct VersionUncheckedMigrateV14ToV15(sp_std::marker::PhantomData); impl OnRuntimeUpgrade for VersionUncheckedMigrateV14ToV15 { fn on_runtime_upgrade() -> Weight { let migrated = v14::OffendingValidators::::get() From 9aa8b70616b00c10ea89486ac56df9f3badb8c14 Mon Sep 17 00:00:00 2001 From: Tsvetomir Dimitrov Date: Tue, 12 Mar 2024 14:05:27 +0200 Subject: [PATCH 53/67] Remove `disabled_validators` getter from `DisabledValidators` --- substrate/frame/staking/src/pallet/mod.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/substrate/frame/staking/src/pallet/mod.rs b/substrate/frame/staking/src/pallet/mod.rs index d224c0bc3ffc..ac3f145c9a06 100644 --- a/substrate/frame/staking/src/pallet/mod.rs +++ b/substrate/frame/staking/src/pallet/mod.rs @@ -653,7 +653,6 @@ pub mod pallet { /// offended using binary search. #[pallet::storage] #[pallet::unbounded] - #[pallet::getter(fn disabled_validators)] pub type DisabledValidators = StorageValue<_, Vec, ValueQuery>; /// The threshold for when users can start calling `chill_other` for other validators / From 4e0c3cf83aa5cb6e084aada96d603393f271d06c Mon Sep 17 00:00:00 2001 From: Tsvetomir Dimitrov Date: Tue, 12 Mar 2024 14:16:24 +0200 Subject: [PATCH 54/67] Fix the migration - clear `OffendingValidators` by using `take` instead of `get` --- substrate/frame/staking/src/migrations.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/substrate/frame/staking/src/migrations.rs b/substrate/frame/staking/src/migrations.rs index 0f5447020cda..1305c079d457 100644 --- a/substrate/frame/staking/src/migrations.rs +++ b/substrate/frame/staking/src/migrations.rs @@ -67,7 +67,7 @@ pub mod v15 { struct VersionUncheckedMigrateV14ToV15(sp_std::marker::PhantomData); impl OnRuntimeUpgrade for VersionUncheckedMigrateV14ToV15 { fn on_runtime_upgrade() -> Weight { - let migrated = v14::OffendingValidators::::get() + let migrated = v14::OffendingValidators::::take() .into_iter() .map(|p| p.0) .collect::>(); From b71e0dbd08d535a6ccb1976afedee06748127614 Mon Sep 17 00:00:00 2001 From: Tsvetomir Dimitrov Date: Tue, 12 Mar 2024 14:31:53 +0200 Subject: [PATCH 55/67] Ensure `OffendingValidators` after migrating to V15 --- substrate/frame/staking/src/migrations.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/substrate/frame/staking/src/migrations.rs b/substrate/frame/staking/src/migrations.rs index 1305c079d457..294bfceaec8d 100644 --- a/substrate/frame/staking/src/migrations.rs +++ b/substrate/frame/staking/src/migrations.rs @@ -64,7 +64,7 @@ type StorageVersion = StorageValue, ObsoleteReleases, Value pub mod v15 { use super::*; - struct VersionUncheckedMigrateV14ToV15(sp_std::marker::PhantomData); + pub struct VersionUncheckedMigrateV14ToV15(sp_std::marker::PhantomData); impl OnRuntimeUpgrade for VersionUncheckedMigrateV14ToV15 { fn on_runtime_upgrade() -> Weight { let migrated = v14::OffendingValidators::::take() @@ -76,6 +76,15 @@ pub mod v15 { log!(info, "v15 applied successfully."); T::DbWeight::get().reads_writes(1, 1) } + + #[cfg(feature = "try-runtime")] + fn post_upgrade(_state: Vec) -> Result<(), TryRuntimeError> { + frame_support::ensure!( + v14::OffendingValidators::::decode_len().is_none(), + "OffendingValidators is not empty after the migration" + ); + Ok(()) + } } pub type MigrateV14ToV15 = VersionedMigration< From 37b7c4a031c3f33281f3994d3a32ceb0a2077abc Mon Sep 17 00:00:00 2001 From: Tsvetomir Dimitrov Date: Tue, 12 Mar 2024 16:10:26 +0200 Subject: [PATCH 56/67] `make_disabling_decision` -> `decision` --- substrate/frame/staking/src/lib.rs | 4 +-- substrate/frame/staking/src/slashing.rs | 2 +- substrate/frame/staking/src/tests.rs | 33 ++++++++++++++----------- 3 files changed, 21 insertions(+), 18 deletions(-) diff --git a/substrate/frame/staking/src/lib.rs b/substrate/frame/staking/src/lib.rs index 479f35c061a0..0a2a399e7881 100644 --- a/substrate/frame/staking/src/lib.rs +++ b/substrate/frame/staking/src/lib.rs @@ -1230,7 +1230,7 @@ impl BenchmarkingConfig for TestBenchmarkingConfig { pub trait DisablingStrategy { /// Make a decision if an offender should be disabled or not. The result is a `Vec` of validator /// indices that should be disabled - fn make_disabling_decision( + fn decision( offender_stash: &T::AccountId, slash_era: EraIndex, currently_disabled: &Vec, @@ -1263,7 +1263,7 @@ impl impl DisablingStrategy for UpToThresholdDisablingStrategy { - fn make_disabling_decision( + fn decision( offender_stash: &T::AccountId, slash_era: EraIndex, currently_disabled: &Vec, diff --git a/substrate/frame/staking/src/slashing.rs b/substrate/frame/staking/src/slashing.rs index 9654d4264a88..d19fd07088d0 100644 --- a/substrate/frame/staking/src/slashing.rs +++ b/substrate/frame/staking/src/slashing.rs @@ -325,7 +325,7 @@ fn kick_out_if_recent(params: SlashParams) { fn add_offending_validator(params: &SlashParams) { DisabledValidators::::mutate(|disabled| { if let Some(offender) = - T::DisablingStrategy::make_disabling_decision(params.stash, params.slash_era, &disabled) + T::DisablingStrategy::decision(params.stash, params.slash_era, &disabled) { // Add the validator to `DisabledValidators` and disable it. Do nothing if it is // already disabled. diff --git a/substrate/frame/staking/src/tests.rs b/substrate/frame/staking/src/tests.rs index ae1e2ab3fdc1..3dbc3d09a245 100644 --- a/substrate/frame/staking/src/tests.rs +++ b/substrate/frame/staking/src/tests.rs @@ -7202,11 +7202,12 @@ mod byzantine_threshold_disabling_strategy { pallet_session::Validators::::put(ACTIVE_SET.to_vec()); CurrentEra::::put(2); - let disable_offender = >::make_disabling_decision( - &OFFENDER_ID, SLASH_ERA, &initially_disabled - ); + let disable_offender = + >::decision( + &OFFENDER_ID, + SLASH_ERA, + &initially_disabled, + ); assert!(disable_offender.is_none()); }); @@ -7218,11 +7219,12 @@ mod byzantine_threshold_disabling_strategy { let initially_disabled = vec![1, 2]; pallet_session::Validators::::put(ACTIVE_SET.to_vec()); - let disable_offender = >::make_disabling_decision( - &OFFENDER_ID, SLASH_ERA, &initially_disabled - ); + let disable_offender = + >::decision( + &OFFENDER_ID, + SLASH_ERA, + &initially_disabled, + ); assert!(disable_offender.is_none()); }); @@ -7234,11 +7236,12 @@ mod byzantine_threshold_disabling_strategy { let initially_disabled = vec![1]; pallet_session::Validators::::put(ACTIVE_SET.to_vec()); - let disable_offender = >::make_disabling_decision( - &OFFENDER_ID, SLASH_ERA, &initially_disabled - ); + let disable_offender = + >::decision( + &OFFENDER_ID, + SLASH_ERA, + &initially_disabled, + ); assert_eq!(disable_offender, Some(OFFENDER_VALIDATOR_IDX)); }); From fdb49a3b28ce86bf7c24e004f979fe6f46b78290 Mon Sep 17 00:00:00 2001 From: Tsvetomir Dimitrov Date: Tue, 12 Mar 2024 16:14:01 +0200 Subject: [PATCH 57/67] `DISABLING_THRESHOLD_FACTOR` -> `DISABLING_LIMIT_FACTOR` --- prdoc/pr_2226.prdoc | 2 +- substrate/frame/staking/src/lib.rs | 16 +++++++--------- substrate/frame/staking/src/mock.rs | 5 ++--- substrate/frame/staking/src/tests.rs | 2 +- 4 files changed, 11 insertions(+), 14 deletions(-) diff --git a/prdoc/pr_2226.prdoc b/prdoc/pr_2226.prdoc index 01d8fba072c4..ca32a4f6dc73 100644 --- a/prdoc/pr_2226.prdoc +++ b/prdoc/pr_2226.prdoc @@ -14,7 +14,7 @@ doc: will be used on Kusama and Polkadot. In nutshell `UpToThresholdDisablingStrategy` disables offenders up to the configured threshold (1/3 of the validators in the active set by default). Offending validators are not disabled for offences in previous eras. The threshold is - controlled via `DISABLING_THRESHOLD_FACTOR` (a generic parameter of + controlled via `DISABLING_LIMIT_FACTOR` (a generic parameter of `UpToThresholdDisablingStrategy`). migrations: diff --git a/substrate/frame/staking/src/lib.rs b/substrate/frame/staking/src/lib.rs index 0a2a399e7881..dfb5efe08665 100644 --- a/substrate/frame/staking/src/lib.rs +++ b/substrate/frame/staking/src/lib.rs @@ -1238,30 +1238,28 @@ pub trait DisablingStrategy { } /// Implementation of [`DisablingStrategy`] which disables validators from the active set up to a -/// threshold. `DISABLING_THRESHOLD_FACTOR` is the factor of the maximum disabled validators in the +/// 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 UpToThresholdDisablingStrategy; +pub struct UpToThresholdDisablingStrategy; -impl - UpToThresholdDisablingStrategy -{ +impl UpToThresholdDisablingStrategy { /// Disabling limit calculated from the total number of validators in the active set. When /// reached no more validators will be disabled. pub fn disable_threshold(validators_len: usize) -> usize { validators_len .saturating_sub(1) - .checked_div(DISABLING_THRESHOLD_FACTOR) + .checked_div(DISABLING_LIMIT_FACTOR) .unwrap_or_else(|| { - defensive!("DISABLING_THRESHOLD_FACTOR should not be 0"); + defensive!("DISABLING_LIMIT_FACTOR should not be 0"); 0 }) } } -impl DisablingStrategy - for UpToThresholdDisablingStrategy +impl DisablingStrategy + for UpToThresholdDisablingStrategy { fn decision( offender_stash: &T::AccountId, diff --git a/substrate/frame/staking/src/mock.rs b/substrate/frame/staking/src/mock.rs index c489fdc39434..e681925e517f 100644 --- a/substrate/frame/staking/src/mock.rs +++ b/substrate/frame/staking/src/mock.rs @@ -263,7 +263,7 @@ impl OnStakingUpdate for EventListenerMock { } // Disabling threshold for `UpToThresholdDisablingStrategy` -pub(crate) const DISABLING_THRESHOLD_FACTOR: usize = 3; +pub(crate) const DISABLING_LIMIT_FACTOR: usize = 3; impl crate::pallet::pallet::Config for Test { type Currency = Balances; @@ -294,8 +294,7 @@ impl crate::pallet::pallet::Config for Test { type EventListeners = EventListenerMock; type BenchmarkingConfig = TestBenchmarkingConfig; type WeightInfo = (); - type DisablingStrategy = - pallet_staking::UpToThresholdDisablingStrategy; + type DisablingStrategy = pallet_staking::UpToThresholdDisablingStrategy; } pub struct WeightedNominationsQuota; diff --git a/substrate/frame/staking/src/tests.rs b/substrate/frame/staking/src/tests.rs index 3dbc3d09a245..12d8b5b0250c 100644 --- a/substrate/frame/staking/src/tests.rs +++ b/substrate/frame/staking/src/tests.rs @@ -3498,7 +3498,7 @@ fn offence_threshold_doesnt_trigger_new_era() { assert_eq_uvec!(Session::validators(), vec![11, 21, 31, 41]); assert_eq!( - UpToThresholdDisablingStrategy::::disable_threshold( + UpToThresholdDisablingStrategy::::disable_threshold( Session::validators().len() ), 1 From 301f48ecf76f501957b4c2e3d35b789eaa472921 Mon Sep 17 00:00:00 2001 From: Tsvetomir Dimitrov Date: Tue, 12 Mar 2024 16:16:06 +0200 Subject: [PATCH 58/67] Update prdoc --- prdoc/pr_2226.prdoc | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/prdoc/pr_2226.prdoc b/prdoc/pr_2226.prdoc index ca32a4f6dc73..21bfe8848671 100644 --- a/prdoc/pr_2226.prdoc +++ b/prdoc/pr_2226.prdoc @@ -12,10 +12,9 @@ doc: is called for each offence. The function makes a decision if (and which) validators should be disabled. A default implementation is provided - `UpToThresholdDisablingStrategy`. It will be used on Kusama and Polkadot. In nutshell `UpToThresholdDisablingStrategy` - disables offenders up to the configured threshold (1/3 of the validators in the active set by - default). Offending validators are not disabled for offences in previous eras. The threshold is - controlled via `DISABLING_LIMIT_FACTOR` (a generic parameter of - `UpToThresholdDisablingStrategy`). + disables offenders up to the configured threshold. Offending validators are not disabled for + offences in previous eras. The threshold is controlled via `DISABLING_LIMIT_FACTOR` (a generic + parameter of `UpToThresholdDisablingStrategy`). migrations: db: [] From cb11fb99548d112e423c418d9497a70bf0e0bd02 Mon Sep 17 00:00:00 2001 From: Tsvetomir Dimitrov Date: Thu, 11 Apr 2024 21:58:52 +0300 Subject: [PATCH 59/67] Fix migration --- substrate/frame/staking/src/migrations.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/substrate/frame/staking/src/migrations.rs b/substrate/frame/staking/src/migrations.rs index ce998f2c476a..dad62d67f651 100644 --- a/substrate/frame/staking/src/migrations.rs +++ b/substrate/frame/staking/src/migrations.rs @@ -23,7 +23,7 @@ use frame_support::{ migrations::VersionedMigration, pallet_prelude::ValueQuery, storage_alias, - traits::{GetStorageVersion, OnRuntimeUpgrade}, + traits::{GetStorageVersion, OnRuntimeUpgrade, UncheckedOnRuntimeUpgrade}, }; #[cfg(feature = "try-runtime")] @@ -64,8 +64,8 @@ type StorageVersion = StorageValue, ObsoleteReleases, Value pub mod v15 { use super::*; - pub struct VersionUncheckedMigrateV14ToV15(sp_std::marker::PhantomData); - impl OnRuntimeUpgrade for VersionUncheckedMigrateV14ToV15 { + pub struct VersionUncheckedMigrateV14ToV15(core::marker::PhantomData); + impl UncheckedOnRuntimeUpgrade for VersionUncheckedMigrateV14ToV15 { fn on_runtime_upgrade() -> Weight { let migrated = v14::OffendingValidators::::take() .into_iter() @@ -91,7 +91,7 @@ pub mod v15 { 14, 15, VersionUncheckedMigrateV14ToV15, - crate::Pallet, + Pallet, ::DbWeight, >; } From 64ba8848fd41d6a02151365378c2a349d8039d8e Mon Sep 17 00:00:00 2001 From: Tsvetomir Dimitrov Date: Fri, 12 Apr 2024 19:51:55 +0300 Subject: [PATCH 60/67] Fix storage migration of `OffendingValidators` (#4083) Not all validators in `OffendingValidators` are disabled. This should be taken in account when migrating the storage item to `DisabledValidators` (where everyone is disabled). Also the[ disabling strategy implementation](https://github.com/paritytech/polkadot-sdk/blob/b71e0dbd08d535a6ccb1976afedee06748127614/substrate/frame/staking/src/lib.rs#L1230) used by the runtime expects a certain number of maximum disabled validators. The storage migration should guarantee that this limit is respected. --------- Co-authored-by: Maciej --- polkadot/runtime/test-runtime/src/lib.rs | 2 +- polkadot/runtime/westend/src/lib.rs | 2 +- prdoc/pr_2226.prdoc | 6 +++--- substrate/bin/node/runtime/src/lib.rs | 2 +- substrate/frame/babe/src/mock.rs | 2 +- substrate/frame/beefy/src/mock.rs | 2 +- .../test-staking-e2e/src/lib.rs | 10 ++++++---- .../test-staking-e2e/src/mock.rs | 3 +-- substrate/frame/fast-unstake/src/mock.rs | 2 +- substrate/frame/grandpa/src/mock.rs | 2 +- .../nomination-pools/benchmarking/src/mock.rs | 2 +- .../nomination-pools/test-staking/src/mock.rs | 2 +- substrate/frame/offences/benchmarking/src/mock.rs | 2 +- substrate/frame/root-offences/src/mock.rs | 2 +- substrate/frame/session/benchmarking/src/mock.rs | 2 +- substrate/frame/staking/CHANGELOG.md | 2 +- substrate/frame/staking/src/lib.rs | 12 ++++++------ substrate/frame/staking/src/migrations.rs | 14 ++++++++++++-- substrate/frame/staking/src/mock.rs | 4 ++-- substrate/frame/staking/src/tests.rs | 10 +++++----- 20 files changed, 48 insertions(+), 37 deletions(-) diff --git a/polkadot/runtime/test-runtime/src/lib.rs b/polkadot/runtime/test-runtime/src/lib.rs index 576635b05e05..1d47cbc1d005 100644 --- a/polkadot/runtime/test-runtime/src/lib.rs +++ b/polkadot/runtime/test-runtime/src/lib.rs @@ -358,7 +358,7 @@ impl pallet_staking::Config for Runtime { type BenchmarkingConfig = runtime_common::StakingBenchmarkingConfig; type EventListeners = (); type WeightInfo = (); - type DisablingStrategy = pallet_staking::UpToThresholdDisablingStrategy; + type DisablingStrategy = pallet_staking::UpToLimitDisablingStrategy; } parameter_types! { diff --git a/polkadot/runtime/westend/src/lib.rs b/polkadot/runtime/westend/src/lib.rs index 7d94b1a90f63..f0e92b937bae 100644 --- a/polkadot/runtime/westend/src/lib.rs +++ b/polkadot/runtime/westend/src/lib.rs @@ -643,7 +643,7 @@ impl pallet_staking::Config for Runtime { type BenchmarkingConfig = runtime_common::StakingBenchmarkingConfig; type EventListeners = NominationPools; type WeightInfo = weights::pallet_staking::WeightInfo; - type DisablingStrategy = pallet_staking::UpToThresholdDisablingStrategy; + type DisablingStrategy = pallet_staking::UpToLimitDisablingStrategy; } impl pallet_fast_unstake::Config for Runtime { diff --git a/prdoc/pr_2226.prdoc b/prdoc/pr_2226.prdoc index 21bfe8848671..f03540a50f6c 100644 --- a/prdoc/pr_2226.prdoc +++ b/prdoc/pr_2226.prdoc @@ -10,11 +10,11 @@ doc: The disabling strategy in staking pallet is no longer hardcoded but abstracted away via `DisablingStrategy` trait. The trait contains a single function (make_disabling_decision) which is called for each offence. The function makes a decision if (and which) validators should be - disabled. A default implementation is provided - `UpToThresholdDisablingStrategy`. It - will be used on Kusama and Polkadot. In nutshell `UpToThresholdDisablingStrategy` + disabled. A default implementation is provided - `UpToLimitDisablingStrategy`. It + will be used on Kusama and Polkadot. In nutshell `UpToLimitDisablingStrategy` disables offenders up to the configured threshold. Offending validators are not disabled for offences in previous eras. The threshold is controlled via `DISABLING_LIMIT_FACTOR` (a generic - parameter of `UpToThresholdDisablingStrategy`). + parameter of `UpToLimitDisablingStrategy`). migrations: db: [] diff --git a/substrate/bin/node/runtime/src/lib.rs b/substrate/bin/node/runtime/src/lib.rs index 4b52087cc3b7..0671ac0e167d 100644 --- a/substrate/bin/node/runtime/src/lib.rs +++ b/substrate/bin/node/runtime/src/lib.rs @@ -701,7 +701,7 @@ impl pallet_staking::Config for Runtime { type EventListeners = NominationPools; type WeightInfo = pallet_staking::weights::SubstrateWeight; type BenchmarkingConfig = StakingBenchmarkingConfig; - type DisablingStrategy = pallet_staking::UpToThresholdDisablingStrategy; + type DisablingStrategy = pallet_staking::UpToLimitDisablingStrategy; } impl pallet_fast_unstake::Config for Runtime { diff --git a/substrate/frame/babe/src/mock.rs b/substrate/frame/babe/src/mock.rs index d7052d773e6d..395a86e65288 100644 --- a/substrate/frame/babe/src/mock.rs +++ b/substrate/frame/babe/src/mock.rs @@ -185,7 +185,7 @@ impl pallet_staking::Config for Test { type EventListeners = (); type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig; type WeightInfo = (); - type DisablingStrategy = pallet_staking::UpToThresholdDisablingStrategy; + type DisablingStrategy = pallet_staking::UpToLimitDisablingStrategy; } impl pallet_offences::Config for Test { diff --git a/substrate/frame/beefy/src/mock.rs b/substrate/frame/beefy/src/mock.rs index 1e9a1201e61c..0b87de6bf5d7 100644 --- a/substrate/frame/beefy/src/mock.rs +++ b/substrate/frame/beefy/src/mock.rs @@ -199,7 +199,7 @@ impl pallet_staking::Config for Test { type EventListeners = (); type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig; type WeightInfo = (); - type DisablingStrategy = pallet_staking::UpToThresholdDisablingStrategy; + type DisablingStrategy = pallet_staking::UpToLimitDisablingStrategy; } impl pallet_offences::Config for Test { diff --git a/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/lib.rs b/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/lib.rs index e3d4ea3b84d9..c00bb66ea130 100644 --- a/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/lib.rs +++ b/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/lib.rs @@ -152,10 +152,12 @@ fn mass_slash_doesnt_enter_emergency_phase() { // active set should stay the same before and after the slash assert_eq!(active_set_size_before_slash, active_set_size_after_slash); - // Slashed validators are disabled up to a threshold - slashed.truncate(pallet_staking::UpToThresholdDisablingStrategy::< - SLASHING_DISABLING_FACTOR, - >::disable_threshold(active_set_size_after_slash)); + // Slashed validators are disabled up to a limit + slashed.truncate( + pallet_staking::UpToLimitDisablingStrategy::::disable_limit( + active_set_size_after_slash, + ), + ); // Find the indices of the disabled validators let active_set = Session::validators(); diff --git a/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/mock.rs b/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/mock.rs index 78063df2f002..8f1775a7e595 100644 --- a/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/mock.rs +++ b/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/mock.rs @@ -320,8 +320,7 @@ impl pallet_staking::Config for Runtime { type EventListeners = Pools; type WeightInfo = pallet_staking::weights::SubstrateWeight; type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig; - type DisablingStrategy = - pallet_staking::UpToThresholdDisablingStrategy; + type DisablingStrategy = pallet_staking::UpToLimitDisablingStrategy; } impl frame_system::offchain::SendTransactionTypes for Runtime diff --git a/substrate/frame/fast-unstake/src/mock.rs b/substrate/frame/fast-unstake/src/mock.rs index e3116b7a530c..d876f9f6171e 100644 --- a/substrate/frame/fast-unstake/src/mock.rs +++ b/substrate/frame/fast-unstake/src/mock.rs @@ -144,7 +144,7 @@ impl pallet_staking::Config for Runtime { type EventListeners = (); type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig; type WeightInfo = (); - type DisablingStrategy = pallet_staking::UpToThresholdDisablingStrategy; + type DisablingStrategy = pallet_staking::UpToLimitDisablingStrategy; } pub struct BalanceToU256; diff --git a/substrate/frame/grandpa/src/mock.rs b/substrate/frame/grandpa/src/mock.rs index 15ae1dd122b9..2d54f525b1f0 100644 --- a/substrate/frame/grandpa/src/mock.rs +++ b/substrate/frame/grandpa/src/mock.rs @@ -187,7 +187,7 @@ impl pallet_staking::Config for Test { type EventListeners = (); type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig; type WeightInfo = (); - type DisablingStrategy = pallet_staking::UpToThresholdDisablingStrategy; + type DisablingStrategy = pallet_staking::UpToLimitDisablingStrategy; } impl pallet_offences::Config for Test { diff --git a/substrate/frame/nomination-pools/benchmarking/src/mock.rs b/substrate/frame/nomination-pools/benchmarking/src/mock.rs index a7c0a4273fe2..2752d53a6b9f 100644 --- a/substrate/frame/nomination-pools/benchmarking/src/mock.rs +++ b/substrate/frame/nomination-pools/benchmarking/src/mock.rs @@ -123,7 +123,7 @@ impl pallet_staking::Config for Runtime { type EventListeners = Pools; type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig; type WeightInfo = (); - type DisablingStrategy = pallet_staking::UpToThresholdDisablingStrategy; + type DisablingStrategy = pallet_staking::UpToLimitDisablingStrategy; } parameter_types! { diff --git a/substrate/frame/nomination-pools/test-staking/src/mock.rs b/substrate/frame/nomination-pools/test-staking/src/mock.rs index 012fa67a235a..93a05ddfae99 100644 --- a/substrate/frame/nomination-pools/test-staking/src/mock.rs +++ b/substrate/frame/nomination-pools/test-staking/src/mock.rs @@ -137,7 +137,7 @@ impl pallet_staking::Config for Runtime { type EventListeners = Pools; type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig; type WeightInfo = (); - type DisablingStrategy = pallet_staking::UpToThresholdDisablingStrategy; + type DisablingStrategy = pallet_staking::UpToLimitDisablingStrategy; } parameter_types! { diff --git a/substrate/frame/offences/benchmarking/src/mock.rs b/substrate/frame/offences/benchmarking/src/mock.rs index ddcc88039720..84cff4b4b9f5 100644 --- a/substrate/frame/offences/benchmarking/src/mock.rs +++ b/substrate/frame/offences/benchmarking/src/mock.rs @@ -188,7 +188,7 @@ impl pallet_staking::Config for Test { type EventListeners = (); type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig; type WeightInfo = (); - type DisablingStrategy = pallet_staking::UpToThresholdDisablingStrategy; + type DisablingStrategy = pallet_staking::UpToLimitDisablingStrategy; } impl pallet_im_online::Config for Test { diff --git a/substrate/frame/root-offences/src/mock.rs b/substrate/frame/root-offences/src/mock.rs index f89fa6205f24..7e7332c3f7e3 100644 --- a/substrate/frame/root-offences/src/mock.rs +++ b/substrate/frame/root-offences/src/mock.rs @@ -163,7 +163,7 @@ impl pallet_staking::Config for Test { type EventListeners = (); type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig; type WeightInfo = (); - type DisablingStrategy = pallet_staking::UpToThresholdDisablingStrategy; + type DisablingStrategy = pallet_staking::UpToLimitDisablingStrategy; } impl pallet_session::historical::Config for Test { diff --git a/substrate/frame/session/benchmarking/src/mock.rs b/substrate/frame/session/benchmarking/src/mock.rs index f65deb5e6b3b..6cefa8f39a8c 100644 --- a/substrate/frame/session/benchmarking/src/mock.rs +++ b/substrate/frame/session/benchmarking/src/mock.rs @@ -185,7 +185,7 @@ impl pallet_staking::Config for Test { type EventListeners = (); type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig; type WeightInfo = (); - type DisablingStrategy = pallet_staking::UpToThresholdDisablingStrategy; + type DisablingStrategy = pallet_staking::UpToLimitDisablingStrategy; } impl crate::Config for Test {} diff --git a/substrate/frame/staking/CHANGELOG.md b/substrate/frame/staking/CHANGELOG.md index d2b0f314bea0..113b7a6200b6 100644 --- a/substrate/frame/staking/CHANGELOG.md +++ b/substrate/frame/staking/CHANGELOG.md @@ -13,7 +13,7 @@ migrations. - New trait `DisablingStrategy` which is responsible for making a decision which offenders should be disabled on new offence. -- Default implementation of `DisablingStrategy` - `UpToThresholdDisablingStrategy`. It +- Default implementation of `DisablingStrategy` - `UpToLimitDisablingStrategy`. It disables each new offender up to a threshold (1/3 by default). Offenders are not runtime disabled for offences in previous era(s). But they will be low-priority node-side disabled for dispute initiation. - `OffendingValidators` storage item is replaced with `DisabledValidators`. The former keeps all diff --git a/substrate/frame/staking/src/lib.rs b/substrate/frame/staking/src/lib.rs index 79b3fc0b2ae6..5b9980460f60 100644 --- a/substrate/frame/staking/src/lib.rs +++ b/substrate/frame/staking/src/lib.rs @@ -1256,12 +1256,12 @@ pub trait DisablingStrategy { /// 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 UpToThresholdDisablingStrategy; +pub struct UpToLimitDisablingStrategy; -impl UpToThresholdDisablingStrategy { +impl UpToLimitDisablingStrategy { /// Disabling limit calculated from the total number of validators in the active set. When /// reached no more validators will be disabled. - pub fn disable_threshold(validators_len: usize) -> usize { + pub fn disable_limit(validators_len: usize) -> usize { validators_len .saturating_sub(1) .checked_div(DISABLING_LIMIT_FACTOR) @@ -1273,7 +1273,7 @@ impl UpToThresholdDisablingStrategy DisablingStrategy - for UpToThresholdDisablingStrategy + for UpToLimitDisablingStrategy { fn decision( offender_stash: &T::AccountId, @@ -1288,8 +1288,8 @@ impl DisablingStrategy return None }; - // We don't disable more than the threshold - if currently_disabled.len() >= Self::disable_threshold(active_set.len()) { + // We don't disable more than the limit + if currently_disabled.len() >= Self::disable_limit(active_set.len()) { return None } diff --git a/substrate/frame/staking/src/migrations.rs b/substrate/frame/staking/src/migrations.rs index dad62d67f651..510252be26c9 100644 --- a/substrate/frame/staking/src/migrations.rs +++ b/substrate/frame/staking/src/migrations.rs @@ -64,13 +64,23 @@ type StorageVersion = StorageValue, ObsoleteReleases, Value pub mod v15 { use super::*; - pub struct VersionUncheckedMigrateV14ToV15(core::marker::PhantomData); + // The disabling strategy used by staking pallet + type DefaultDisablingStrategy = UpToLimitDisablingStrategy; + + pub struct VersionUncheckedMigrateV14ToV15(sp_std::marker::PhantomData); impl UncheckedOnRuntimeUpgrade for VersionUncheckedMigrateV14ToV15 { fn on_runtime_upgrade() -> Weight { - let migrated = v14::OffendingValidators::::take() + let mut migrated = v14::OffendingValidators::::take() .into_iter() + .filter(|p| p.1) // take only disabled validators .map(|p| p.0) .collect::>(); + + // Respect disabling limit + migrated.truncate(DefaultDisablingStrategy::disable_limit( + T::SessionInterface::validators().len(), + )); + DisabledValidators::::set(migrated); log!(info, "v15 applied successfully."); diff --git a/substrate/frame/staking/src/mock.rs b/substrate/frame/staking/src/mock.rs index a3b215bfc644..73e46536656c 100644 --- a/substrate/frame/staking/src/mock.rs +++ b/substrate/frame/staking/src/mock.rs @@ -262,7 +262,7 @@ impl OnStakingUpdate for EventListenerMock { } } -// Disabling threshold for `UpToThresholdDisablingStrategy` +// Disabling threshold for `UpToLimitDisablingStrategy` pub(crate) const DISABLING_LIMIT_FACTOR: usize = 3; impl crate::pallet::pallet::Config for Test { @@ -294,7 +294,7 @@ impl crate::pallet::pallet::Config for Test { type EventListeners = EventListenerMock; type BenchmarkingConfig = TestBenchmarkingConfig; type WeightInfo = (); - type DisablingStrategy = pallet_staking::UpToThresholdDisablingStrategy; + type DisablingStrategy = pallet_staking::UpToLimitDisablingStrategy; } pub struct WeightedNominationsQuota; diff --git a/substrate/frame/staking/src/tests.rs b/substrate/frame/staking/src/tests.rs index f1eb8e9de804..44a9ab6957bc 100644 --- a/substrate/frame/staking/src/tests.rs +++ b/substrate/frame/staking/src/tests.rs @@ -3515,7 +3515,7 @@ fn offence_threshold_doesnt_trigger_new_era() { assert_eq_uvec!(Session::validators(), vec![11, 21, 31, 41]); assert_eq!( - UpToThresholdDisablingStrategy::::disable_threshold( + UpToLimitDisablingStrategy::::disable_limit( Session::validators().len() ), 1 @@ -7730,7 +7730,7 @@ mod ledger_recovery { } mod byzantine_threshold_disabling_strategy { - use crate::{tests::Test, CurrentEra, DisablingStrategy, UpToThresholdDisablingStrategy}; + use crate::{tests::Test, CurrentEra, DisablingStrategy, UpToLimitDisablingStrategy}; use sp_staking::EraIndex; // Common test data - the stash of the offending validator, the era of the offence and the @@ -7748,7 +7748,7 @@ mod byzantine_threshold_disabling_strategy { CurrentEra::::put(2); let disable_offender = - >::decision( + >::decision( &OFFENDER_ID, SLASH_ERA, &initially_disabled, @@ -7765,7 +7765,7 @@ mod byzantine_threshold_disabling_strategy { pallet_session::Validators::::put(ACTIVE_SET.to_vec()); let disable_offender = - >::decision( + >::decision( &OFFENDER_ID, SLASH_ERA, &initially_disabled, @@ -7782,7 +7782,7 @@ mod byzantine_threshold_disabling_strategy { pallet_session::Validators::::put(ACTIVE_SET.to_vec()); let disable_offender = - >::decision( + >::decision( &OFFENDER_ID, SLASH_ERA, &initially_disabled, From 8a509f5637a3f1793201d4f53169bcc030c08304 Mon Sep 17 00:00:00 2001 From: Tsvetomir Dimitrov Date: Tue, 23 Apr 2024 09:17:12 +0300 Subject: [PATCH 61/67] Code review feedback --- substrate/frame/staking/src/lib.rs | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/substrate/frame/staking/src/lib.rs b/substrate/frame/staking/src/lib.rs index 5b9980460f60..ee045e2c9c0f 100644 --- a/substrate/frame/staking/src/lib.rs +++ b/substrate/frame/staking/src/lib.rs @@ -1242,8 +1242,9 @@ impl BenchmarkingConfig for TestBenchmarkingConfig { /// Controls validator disabling pub trait DisablingStrategy { - /// Make a decision if an offender should be disabled or not. The result is a `Vec` of validator - /// indices that should be disabled + /// Make a disabling decision. Returns the index of the validator to disable or `None` if no new + /// validator should be disabled. Note that the returned index might not be the index of the + /// offender. fn decision( offender_stash: &T::AccountId, slash_era: EraIndex, @@ -1281,12 +1282,6 @@ impl DisablingStrategy currently_disabled: &Vec, ) -> Option { let active_set = T::SessionInterface::validators(); - let offender_idx = if let Some(idx) = active_set.iter().position(|i| i == offender_stash) { - idx as u32 - } else { - // offender not found in the active set, do nothing - return None - }; // We don't disable more than the limit if currently_disabled.len() >= Self::disable_limit(active_set.len()) { @@ -1294,10 +1289,17 @@ impl DisablingStrategy } // We don't disable for offences in previous eras - if Pallet::::current_era().unwrap_or(1) > slash_era { + if Pallet::::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 { + // offender not found in the active set, do nothing + return None + }; + Some(offender_idx) } } From d4e4ab316987bdd6efd7ba5a80c600a948202bb4 Mon Sep 17 00:00:00 2001 From: command-bot <> Date: Tue, 23 Apr 2024 07:04:45 +0000 Subject: [PATCH 62/67] ".git/.scripts/commands/fmt/fmt.sh" --- polkadot/runtime/westend/src/lib.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/polkadot/runtime/westend/src/lib.rs b/polkadot/runtime/westend/src/lib.rs index 8fcf16356ece..c9bf1653e681 100644 --- a/polkadot/runtime/westend/src/lib.rs +++ b/polkadot/runtime/westend/src/lib.rs @@ -1646,9 +1646,7 @@ pub mod migrations { } /// Unreleased migrations. Add new ones here: - pub type Unreleased = ( - pallet_staking::migrations::v15::MigrateV14ToV15, - ); + pub type Unreleased = (pallet_staking::migrations::v15::MigrateV14ToV15,); } /// Unchecked extrinsic type as expected by this runtime. From f84162c8ec9424f0875d935e11e81977a6c286f1 Mon Sep 17 00:00:00 2001 From: Tsvetomir Dimitrov Date: Tue, 23 Apr 2024 11:20:28 +0300 Subject: [PATCH 63/67] Fix `virtual_nominators_are_lazily_slashed` --- substrate/frame/staking/src/tests.rs | 111 +++++++++++++++------------ 1 file changed, 60 insertions(+), 51 deletions(-) diff --git a/substrate/frame/staking/src/tests.rs b/substrate/frame/staking/src/tests.rs index d20be666bc41..332f01416970 100644 --- a/substrate/frame/staking/src/tests.rs +++ b/substrate/frame/staking/src/tests.rs @@ -7032,62 +7032,71 @@ mod staking_unchecked { #[test] fn virtual_nominators_are_lazily_slashed() { - ExtBuilder::default().build_and_execute(|| { - mock::start_active_era(1); - let slash_percent = Perbill::from_percent(5); - let initial_exposure = Staking::eras_stakers(active_era(), &11); - // 101 is a nominator for 11 - assert_eq!(initial_exposure.others.first().unwrap().who, 101); - // make 101 a virtual nominator - ::migrate_to_virtual_staker(&101); - // set payee different to self. - assert_ok!(::update_payee(&101, &102)); + ExtBuilder::default() + .validator_count(7) + .set_status(41, StakerStatus::Validator) + .set_status(51, StakerStatus::Validator) + .set_status(201, StakerStatus::Validator) + .set_status(202, StakerStatus::Validator) + .build_and_execute(|| { + mock::start_active_era(1); + let slash_percent = Perbill::from_percent(5); + let initial_exposure = Staking::eras_stakers(active_era(), &11); + // 101 is a nominator for 11 + assert_eq!(initial_exposure.others.first().unwrap().who, 101); + // make 101 a virtual nominator + ::migrate_to_virtual_staker(&101); + // set payee different to self. + assert_ok!(::update_payee(&101, &102)); + + // cache values + let nominator_stake = Staking::ledger(101.into()).unwrap().active; + let nominator_balance = balances(&101).0; + let validator_stake = Staking::ledger(11.into()).unwrap().active; + let validator_balance = balances(&11).0; + let exposed_stake = initial_exposure.total; + let exposed_validator = initial_exposure.own; + let exposed_nominator = initial_exposure.others.first().unwrap().value; + + // 11 goes offline + on_offence_now( + &[OffenceDetails { + offender: (11, initial_exposure.clone()), + reporters: vec![], + }], + &[slash_percent], + ); - // cache values - let nominator_stake = Staking::ledger(101.into()).unwrap().active; - let nominator_balance = balances(&101).0; - let validator_stake = Staking::ledger(11.into()).unwrap().active; - let validator_balance = balances(&11).0; - let exposed_stake = initial_exposure.total; - let exposed_validator = initial_exposure.own; - let exposed_nominator = initial_exposure.others.first().unwrap().value; + let slash_amount = slash_percent * exposed_stake; + let validator_share = + Perbill::from_rational(exposed_validator, exposed_stake) * slash_amount; + let nominator_share = + Perbill::from_rational(exposed_nominator, exposed_stake) * slash_amount; - // 11 goes offline - on_offence_now( - &[OffenceDetails { offender: (11, initial_exposure.clone()), reporters: vec![] }], - &[slash_percent], - ); + // both slash amounts need to be positive for the test to make sense. + assert!(validator_share > 0); + assert!(nominator_share > 0); - let slash_amount = slash_percent * exposed_stake; - let validator_share = - Perbill::from_rational(exposed_validator, exposed_stake) * slash_amount; - let nominator_share = - Perbill::from_rational(exposed_nominator, exposed_stake) * slash_amount; - - // both slash amounts need to be positive for the test to make sense. - assert!(validator_share > 0); - assert!(nominator_share > 0); - - // both stakes must have been decreased pro-rata. - assert_eq!( - Staking::ledger(101.into()).unwrap().active, - nominator_stake - nominator_share - ); - assert_eq!( - Staking::ledger(11.into()).unwrap().active, - validator_stake - validator_share - ); + // both stakes must have been decreased pro-rata. + assert_eq!( + Staking::ledger(101.into()).unwrap().active, + nominator_stake - nominator_share + ); + assert_eq!( + Staking::ledger(11.into()).unwrap().active, + validator_stake - validator_share + ); - // validator balance is slashed as usual - assert_eq!(balances(&11).0, validator_balance - validator_share); - // Because slashing happened. - assert!(is_disabled(11)); + // validator balance is slashed as usual + assert_eq!(balances(&11).0, validator_balance - validator_share); + // Because slashing happened. + assert!(is_disabled(11)); - // but virtual nominator's balance is not slashed. - assert_eq!(Balances::free_balance(&101), nominator_balance); - // but slash is broadcasted to slash observers. - assert_eq!(SlashObserver::get().get(&101).unwrap(), &nominator_share); - }) + // but virtual nominator's balance is not slashed. + assert_eq!(Balances::free_balance(&101), nominator_balance); + // but slash is broadcasted to slash observers. + assert_eq!(SlashObserver::get().get(&101).unwrap(), &nominator_share); + }) } } mod ledger { From 87112ac6b237fc5f5aed9a04f49d848038515b5d Mon Sep 17 00:00:00 2001 From: Tsvetomir Dimitrov Date: Wed, 24 Apr 2024 09:03:25 +0300 Subject: [PATCH 64/67] Fix `decision` from `UpToLimitDisablingStrategy` (#4262) `CurrentEra` in staking pallet is actually the latest planned era. The actual current era is `ActiveEra`. --- .../functional/0010-validator-disabling.toml | 2 +- substrate/frame/staking/src/lib.rs | 17 +++++++++++++++-- substrate/frame/staking/src/tests.rs | 6 ++++-- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/polkadot/zombienet_tests/functional/0010-validator-disabling.toml b/polkadot/zombienet_tests/functional/0010-validator-disabling.toml index c9d79c5f8f23..806f34d7f767 100644 --- a/polkadot/zombienet_tests/functional/0010-validator-disabling.toml +++ b/polkadot/zombienet_tests/functional/0010-validator-disabling.toml @@ -21,7 +21,7 @@ requests = { memory = "2G", cpu = "1" } [[relaychain.node_groups]] name = "honest-validator" count = 3 - args = ["-lparachain=debug"] + args = ["-lparachain=debug,runtime::staking=debug"] [[relaychain.node_groups]] image = "{{MALUS_IMAGE}}" diff --git a/substrate/frame/staking/src/lib.rs b/substrate/frame/staking/src/lib.rs index ee045e2c9c0f..0ebd4e049566 100644 --- a/substrate/frame/staking/src/lib.rs +++ b/substrate/frame/staking/src/lib.rs @@ -1285,21 +1285,34 @@ impl DisablingStrategy // 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 Pallet::::current_era().unwrap_or_default() > slash_era { + if ActiveEra::::get().map(|e| e.index).unwrap_or_default() > slash_era { + log!( + debug, + "Won't disable: current_era {:?} > slash_era {:?}", + Pallet::::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 { - // offender not found in the active set, do nothing + log!(debug, "Won't disable: offender not in active set",); return None }; + log!(debug, "Will disable {:?}", offender_idx); + Some(offender_idx) } } diff --git a/substrate/frame/staking/src/tests.rs b/substrate/frame/staking/src/tests.rs index 332f01416970..6cf5a56e5a6d 100644 --- a/substrate/frame/staking/src/tests.rs +++ b/substrate/frame/staking/src/tests.rs @@ -7958,7 +7958,9 @@ mod ledger_recovery { } mod byzantine_threshold_disabling_strategy { - use crate::{tests::Test, CurrentEra, DisablingStrategy, UpToLimitDisablingStrategy}; + use crate::{ + tests::Test, ActiveEra, ActiveEraInfo, DisablingStrategy, UpToLimitDisablingStrategy, + }; use sp_staking::EraIndex; // Common test data - the stash of the offending validator, the era of the offence and the @@ -7973,7 +7975,7 @@ mod byzantine_threshold_disabling_strategy { sp_io::TestExternalities::default().execute_with(|| { let initially_disabled = vec![]; pallet_session::Validators::::put(ACTIVE_SET.to_vec()); - CurrentEra::::put(2); + ActiveEra::::put(ActiveEraInfo { index: 2, start: None }); let disable_offender = >::decision( From 8db81672de2c8f3982fd0fd002019d1a41ca8494 Mon Sep 17 00:00:00 2001 From: Tsvetomir Dimitrov Date: Wed, 24 Apr 2024 16:17:29 +0300 Subject: [PATCH 65/67] Update substrate/frame/staking/src/slashing.rs Co-authored-by: Ankan <10196091+Ank4n@users.noreply.github.com> --- substrate/frame/staking/src/slashing.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/substrate/frame/staking/src/slashing.rs b/substrate/frame/staking/src/slashing.rs index c5f84fc9775e..f831f625957d 100644 --- a/substrate/frame/staking/src/slashing.rs +++ b/substrate/frame/staking/src/slashing.rs @@ -337,7 +337,7 @@ fn add_offending_validator(params: &SlashParams) { }); // `DisabledValidators` should be kept sorted - debug_assert!(DisabledValidators::::get().windows(2).all(|pair| pair[0] <= pair[1])); + debug_assert!(DisabledValidators::::get().windows(2).all(|pair| pair[0] < pair[1])); } /// Slash nominators. Accepts general parameters and the prior slash percentage of the validator. From b76e5e730a44bb456ec2758abfd67bcffd634adc Mon Sep 17 00:00:00 2001 From: Tsvetomir Dimitrov Date: Wed, 24 Apr 2024 16:17:57 +0300 Subject: [PATCH 66/67] Update substrate/frame/staking/src/pallet/impls.rs Co-authored-by: Ankan <10196091+Ank4n@users.noreply.github.com> --- substrate/frame/staking/src/pallet/impls.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/substrate/frame/staking/src/pallet/impls.rs b/substrate/frame/staking/src/pallet/impls.rs index 6701b4c2531a..f4d4a7133dd5 100644 --- a/substrate/frame/staking/src/pallet/impls.rs +++ b/substrate/frame/staking/src/pallet/impls.rs @@ -596,7 +596,7 @@ impl Pallet { >::insert(&active_era.index, validator_payout); T::RewardRemainder::on_unbalanced(T::Currency::issue(remainder)); - // Clear offending validators. + // Clear disabled validators. >::kill(); } } From a228aff9b24fb30925b84108fb1c944672f6fdcf Mon Sep 17 00:00:00 2001 From: Tsvetomir Dimitrov Date: Thu, 25 Apr 2024 15:47:55 +0300 Subject: [PATCH 67/67] Remove misleading comment --- substrate/frame/staking/src/lib.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/substrate/frame/staking/src/lib.rs b/substrate/frame/staking/src/lib.rs index 0ebd4e049566..047ad6b87cc1 100644 --- a/substrate/frame/staking/src/lib.rs +++ b/substrate/frame/staking/src/lib.rs @@ -1243,8 +1243,7 @@ impl BenchmarkingConfig for TestBenchmarkingConfig { /// Controls validator disabling pub trait DisablingStrategy { /// Make a disabling decision. Returns the index of the validator to disable or `None` if no new - /// validator should be disabled. Note that the returned index might not be the index of the - /// offender. + /// validator should be disabled. fn decision( offender_stash: &T::AccountId, slash_era: EraIndex,