From 575f7b90cea38d4a231b89080f8b4928113bbefd Mon Sep 17 00:00:00 2001
From: tmpolaczyk <44604217+tmpolaczyk@users.noreply.github.com>
Date: Mon, 4 Nov 2024 10:52:27 +0100
Subject: [PATCH] Add pallet_external_validators, remove ValidatorManager
(#722)
---
Cargo.lock | 25 +
Cargo.toml | 1 +
pallets/external-validators/Cargo.toml | 77 ++
.../external-validators/src/benchmarking.rs | 237 +++++
pallets/external-validators/src/lib.rs | 633 ++++++++++++
pallets/external-validators/src/mock.rs | 342 +++++++
pallets/external-validators/src/tests.rs | 269 +++++
pallets/external-validators/src/weights.rs | 220 ++++
pallets/invulnerables/src/lib.rs | 19 +-
pallets/invulnerables/src/mock.rs | 6 +-
pallets/invulnerables/src/tests.rs | 1 -
primitives/traits/src/lib.rs | 53 +-
runtime/common/Cargo.toml | 7 +
runtime/common/src/migrations.rs | 57 +-
runtime/dancebox/src/lib.rs | 8 +-
runtime/flashbox/src/lib.rs | 8 +-
solo-chains/runtime/dancelight/Cargo.toml | 4 +
.../dancelight/src/genesis_config_presets.rs | 11 +-
solo-chains/runtime/dancelight/src/lib.rs | 41 +-
.../dancelight/src/tests/common/mod.rs | 11 +
.../src/tests/external_validators_tests.rs | 633 ++++++++++++
.../dancelight/src/tests/migrations_test.rs | 47 +-
.../runtime/dancelight/src/tests/mod.rs | 1 +
.../dancelight/src/validator_manager.rs | 151 ---
.../runtime/dancelight/src/weights/mod.rs | 1 +
.../src/weights/pallet_external_validators.rs | 132 +++
.../test-external-validators.ts | 47 +
.../dancebox/interfaces/augment-api-events.ts | 11 -
.../dancebox/interfaces/augment-api-query.ts | 2 +-
.../src/dancebox/interfaces/augment-api-tx.ts | 2 +-
.../src/dancebox/interfaces/lookup.ts | 236 +++--
.../src/dancebox/interfaces/types-lookup.ts | 224 ++---
.../interfaces/augment-api-consts.ts | 22 +
.../interfaces/augment-api-errors.ts | 14 +
.../interfaces/augment-api-events.ts | 36 +-
.../interfaces/augment-api-query.ts | 50 +-
.../dancelight/interfaces/augment-api-tx.ts | 70 +-
.../src/dancelight/interfaces/lookup.ts | 929 ++++++++---------
.../src/dancelight/interfaces/registry.ts | 14 +-
.../src/dancelight/interfaces/types-lookup.ts | 938 +++++++++---------
.../flashbox/interfaces/augment-api-events.ts | 13 +-
.../flashbox/interfaces/augment-api-query.ts | 2 +-
.../src/flashbox/interfaces/augment-api-tx.ts | 2 +-
.../src/flashbox/interfaces/lookup.ts | 98 +-
.../src/flashbox/interfaces/types-lookup.ts | 102 +-
45 files changed, 4283 insertions(+), 1524 deletions(-)
create mode 100644 pallets/external-validators/Cargo.toml
create mode 100644 pallets/external-validators/src/benchmarking.rs
create mode 100644 pallets/external-validators/src/lib.rs
create mode 100644 pallets/external-validators/src/mock.rs
create mode 100644 pallets/external-validators/src/tests.rs
create mode 100644 pallets/external-validators/src/weights.rs
create mode 100644 solo-chains/runtime/dancelight/src/tests/external_validators_tests.rs
delete mode 100644 solo-chains/runtime/dancelight/src/validator_manager.rs
create mode 100644 solo-chains/runtime/dancelight/src/weights/pallet_external_validators.rs
create mode 100644 test/suites/dev-tanssi-relay/external-validators/test-external-validators.ts
diff --git a/Cargo.lock b/Cargo.lock
index 8779f6f6c..48c6a9396 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3249,6 +3249,7 @@ dependencies = [
"pallet-data-preservers",
"pallet-democracy",
"pallet-elections-phragmen",
+ "pallet-external-validators",
"pallet-grandpa",
"pallet-identity",
"pallet-inflation-rewards",
@@ -9363,6 +9364,28 @@ dependencies = [
"xcm-primitives",
]
+[[package]]
+name = "pallet-external-validators"
+version = "0.1.0"
+dependencies = [
+ "frame-benchmarking",
+ "frame-support",
+ "frame-system",
+ "log",
+ "pallet-balances",
+ "pallet-session",
+ "pallet-timestamp",
+ "parity-scale-codec",
+ "rand",
+ "scale-info",
+ "sp-core",
+ "sp-io",
+ "sp-runtime",
+ "sp-staking",
+ "sp-std",
+ "tp-traits",
+]
+
[[package]]
name = "pallet-fast-unstake"
version = "36.0.0"
@@ -17556,12 +17579,14 @@ dependencies = [
"pallet-beefy-mmr",
"pallet-configuration",
"pallet-data-preservers",
+ "pallet-external-validators",
"pallet-foreign-asset-creator",
"pallet-invulnerables",
"pallet-migrations 0.1.0",
"pallet-pooled-staking",
"pallet-registrar",
"pallet-services-payment",
+ "pallet-session",
"pallet-treasury",
"pallet-xcm",
"parity-scale-codec",
diff --git a/Cargo.toml b/Cargo.toml
index 152b826c4..9af1b85be 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -60,6 +60,7 @@ pallet-collator-assignment-runtime-api = { path = "pallets/collator-assignment/r
pallet-configuration = { path = "pallets/configuration", default-features = false }
pallet-data-preservers = { path = "pallets/data-preservers", default-features = false }
pallet-data-preservers-runtime-api = { path = "pallets/data-preservers/runtime-api", default-features = false }
+pallet-external-validators = { path = "pallets/external-validators", default-features = false }
pallet-inflation-rewards = { path = "pallets/inflation-rewards", default-features = false }
pallet-initializer = { path = "pallets/initializer", default-features = false }
pallet-invulnerables = { path = "pallets/invulnerables", default-features = false }
diff --git a/pallets/external-validators/Cargo.toml b/pallets/external-validators/Cargo.toml
new file mode 100644
index 000000000..56c18866b
--- /dev/null
+++ b/pallets/external-validators/Cargo.toml
@@ -0,0 +1,77 @@
+[package]
+name = "pallet-external-validators"
+authors = { workspace = true }
+description = "Simple pallet to store external validators."
+edition = "2021"
+license = "GPL-3.0-only"
+version = "0.1.0"
+
+[package.metadata.docs.rs]
+targets = [ "x86_64-unknown-linux-gnu" ]
+
+[lints]
+workspace = true
+
+[dependencies]
+log = { workspace = true }
+parity-scale-codec = { workspace = true }
+rand = { workspace = true, optional = true }
+scale-info = { workspace = true, features = [ "derive" ] }
+
+frame-support = { workspace = true }
+frame-system = { workspace = true }
+sp-runtime = { workspace = true }
+sp-staking = { workspace = true }
+sp-std = { workspace = true }
+tp-traits = { workspace = true }
+
+frame-benchmarking = { workspace = true }
+
+pallet-balances = { workspace = true, optional = true }
+pallet-session = { workspace = true, features = [ "historical" ] }
+
+[dev-dependencies]
+pallet-timestamp = { workspace = true }
+sp-core = { workspace = true }
+sp-io = { workspace = true }
+
+[features]
+default = [ "std" ]
+std = [
+ "frame-benchmarking/std",
+ "frame-support/std",
+ "frame-system/std",
+ "log/std",
+ "pallet-balances/std",
+ "pallet-session/std",
+ "pallet-timestamp/std",
+ "parity-scale-codec/std",
+ "rand?/std",
+ "scale-info/std",
+ "sp-core/std",
+ "sp-io/std",
+ "sp-runtime/std",
+ "sp-staking/std",
+ "sp-std/std",
+ "tp-traits/std",
+]
+runtime-benchmarks = [
+ "frame-benchmarking/runtime-benchmarks",
+ "frame-support/runtime-benchmarks",
+ "frame-system/runtime-benchmarks",
+ "pallet-balances/runtime-benchmarks",
+ "pallet-timestamp/runtime-benchmarks",
+ "rand",
+ "sp-runtime/runtime-benchmarks",
+ "sp-staking/runtime-benchmarks",
+ "tp-traits/runtime-benchmarks",
+]
+
+try-runtime = [
+ "frame-support/try-runtime",
+ "frame-system/try-runtime",
+ "pallet-balances?/try-runtime",
+ "pallet-session/try-runtime",
+ "pallet-timestamp/try-runtime",
+ "sp-runtime/try-runtime",
+]
diff --git a/pallets/external-validators/src/benchmarking.rs b/pallets/external-validators/src/benchmarking.rs
new file mode 100644
index 000000000..73947bb22
--- /dev/null
+++ b/pallets/external-validators/src/benchmarking.rs
@@ -0,0 +1,237 @@
+// Copyright (C) Moondance Labs Ltd.
+// This file is part of Tanssi.
+
+// Tanssi 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.
+
+// Tanssi 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 Tanssi. If not, see
+
+//! Benchmarking setup for pallet_external_validators
+
+use super::*;
+
+#[allow(unused)]
+use crate::Pallet as ExternalValidators;
+use {
+ frame_benchmarking::{account, v2::*, BenchmarkError},
+ frame_support::{
+ pallet_prelude::*,
+ traits::{tokens::fungible::Balanced, Currency, EnsureOrigin, Get},
+ },
+ frame_system::{EventRecord, RawOrigin},
+ pallet_session::{self as session, SessionManager},
+ sp_runtime::traits::{AtLeast32BitUnsigned, Convert},
+ sp_std::prelude::*,
+ tp_traits::DistributeRewards,
+};
+const SEED: u32 = 0;
+
+fn assert_last_event(generic_event: ::RuntimeEvent) {
+ let events = frame_system::Pallet::::events();
+ let system_event: ::RuntimeEvent = generic_event.into();
+ // compare to the last event record
+ let EventRecord { event, .. } = &events[events.len() - 1];
+ assert_eq!(event, &system_event);
+}
+
+fn create_funded_user(
+ string: &'static str,
+ n: u32,
+ balance_factor: u32,
+) -> T::AccountId {
+ let user = account(string, n, SEED);
+ let balance = as Currency>::minimum_balance()
+ * balance_factor.into();
+ let _ = as Currency>::make_free_balance_be(
+ &user, balance,
+ );
+ user
+}
+
+fn keys(c: u32) -> ::Keys {
+ use rand::{RngCore, SeedableRng};
+
+ let keys = {
+ let mut keys = [0u8; 256];
+
+ if c > 0 {
+ let mut rng = rand::rngs::StdRng::seed_from_u64(u64::from(c));
+ rng.fill_bytes(&mut keys);
+ }
+
+ keys
+ };
+
+ Decode::decode(&mut &keys[..]).unwrap()
+}
+
+fn invulnerable(
+ c: u32,
+) -> (
+ T::AccountId,
+ ::ValidatorId,
+ ::Keys,
+) {
+ let funded_user = create_funded_user::("candidate", c, 100);
+ let collator_id = ::ValidatorIdOf::convert(funded_user.clone())
+ .expect("Converstion of account id of collator id failed.");
+ (funded_user, collator_id, keys::(c))
+}
+
+fn invulnerables<
+ T: Config + frame_system::Config + pallet_session::Config + pallet_balances::Config,
+>(
+ count: u32,
+) -> Vec<(T::AccountId, ::ValidatorId)> {
+ let invulnerables = (0..count).map(|c| invulnerable::(c)).collect::>();
+
+ for (who, _collator_id, keys) in invulnerables.clone() {
+ >::set_keys(RawOrigin::Signed(who).into(), keys, Vec::new()).unwrap();
+ }
+
+ invulnerables
+ .into_iter()
+ .map(|(who, collator_id, _)| (who, collator_id))
+ .collect()
+}
+
+#[allow(clippy::multiple_bound_locations)]
+#[benchmarks(where T: session::Config + pallet_balances::Config)]
+mod benchmarks {
+ use super::*;
+
+ #[benchmark]
+ fn skip_external_validators() -> Result<(), BenchmarkError> {
+ let origin =
+ T::UpdateOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?;
+
+ #[extrinsic_call]
+ _(origin as T::RuntimeOrigin, true);
+
+ Ok(())
+ }
+
+ #[benchmark]
+ fn add_whitelisted(
+ b: Linear<1, { T::MaxWhitelistedValidators::get() - 1 }>,
+ ) -> Result<(), BenchmarkError> {
+ let origin =
+ T::UpdateOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?;
+
+ // now we need to fill up invulnerables
+ let invulnerables = invulnerables::(b);
+
+ let (_account_ids, collator_ids): (Vec, Vec<::ValidatorId>) =
+ invulnerables.into_iter().unzip();
+
+ let invulnerables: frame_support::BoundedVec<_, T::MaxWhitelistedValidators> =
+ frame_support::BoundedVec::try_from(collator_ids).unwrap();
+ >::put(invulnerables);
+
+ let (new_invulnerable, _collator_id, keys) = invulnerable::(b + 1);
+ >::set_keys(
+ RawOrigin::Signed(new_invulnerable.clone()).into(),
+ keys,
+ Vec::new(),
+ )
+ .unwrap();
+
+ #[extrinsic_call]
+ _(origin as T::RuntimeOrigin, new_invulnerable.clone());
+
+ assert_last_event::(
+ Event::WhitelistedValidatorAdded {
+ account_id: new_invulnerable,
+ }
+ .into(),
+ );
+ Ok(())
+ }
+
+ #[benchmark]
+ fn remove_whitelisted(
+ b: Linear<{ 1 }, { T::MaxWhitelistedValidators::get() }>,
+ ) -> Result<(), BenchmarkError> {
+ let origin =
+ T::UpdateOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?;
+ let invulnerables = invulnerables::(b);
+
+ let (account_ids, collator_ids): (Vec, Vec<::ValidatorId>) =
+ invulnerables.into_iter().unzip();
+
+ let invulnerables: frame_support::BoundedVec<_, T::MaxWhitelistedValidators> =
+ frame_support::BoundedVec::try_from(collator_ids).unwrap();
+ >::put(invulnerables);
+
+ let to_remove = account_ids.last().unwrap().clone();
+
+ #[extrinsic_call]
+ _(origin as T::RuntimeOrigin, to_remove.clone());
+
+ assert_last_event::(
+ Event::WhitelistedValidatorRemoved {
+ account_id: to_remove,
+ }
+ .into(),
+ );
+ Ok(())
+ }
+
+ #[benchmark]
+ fn force_era() -> Result<(), BenchmarkError> {
+ let origin =
+ T::UpdateOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?;
+
+ #[extrinsic_call]
+ _(origin as T::RuntimeOrigin, Forcing::ForceNew);
+
+ Ok(())
+ }
+
+ // worst case for new session.
+ #[benchmark]
+ fn new_session(
+ r: Linear<1, { T::MaxWhitelistedValidators::get() }>,
+ ) -> Result<(), BenchmarkError> {
+ // start fresh
+ WhitelistedValidators::::kill();
+
+ let origin =
+ T::UpdateOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?;
+
+ frame_system::Pallet::::set_block_number(0u32.into());
+ // now we need to fill up invulnerables
+ let invulnerables = invulnerables::(r);
+
+ let (account_ids, _collator_ids): (Vec, Vec<::ValidatorId>) =
+ invulnerables.into_iter().unzip();
+
+ for account in account_ids {
+ >::add_whitelisted(origin.clone(), account)
+ .expect("add whitelisted failed");
+ }
+
+ let new_era_session = T::SessionsPerEra::get();
+
+ #[block]
+ {
+ as SessionManager<_>>::new_session(new_era_session);
+ }
+
+ Ok(())
+ }
+
+ impl_benchmark_test_suite!(
+ ExternalValidators,
+ crate::mock::new_test_ext(),
+ crate::mock::Test,
+ );
+}
diff --git a/pallets/external-validators/src/lib.rs b/pallets/external-validators/src/lib.rs
new file mode 100644
index 000000000..debfcca95
--- /dev/null
+++ b/pallets/external-validators/src/lib.rs
@@ -0,0 +1,633 @@
+// Copyright (C) Moondance Labs Ltd.
+// This file is part of Tanssi.
+
+// Tanssi 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.
+
+// Tanssi 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 Tanssi. If not, see
+
+//! ExternalValidators pallet.
+//!
+//! A pallet to manage external validators for a solochain.
+//!
+//! ## Terminology
+//!
+//! - WhitelistedValidators: Fixed validators set by root/governance. Have priority over the external validators.
+//! - ExternalValidators: Validators set using storage proofs from another blockchain. Can be disabled by setting
+//! `SkipExternalValidators` to true.
+//!
+//! Validators only change once per era. By default the era changes after a fixed number of sessions, but new eras
+//! can be forced or disabled using a root extrinsic.
+//!
+//! The structure of this pallet and the concept of eras is inspired by `pallet_staking` from Polkadot.
+
+#![cfg_attr(not(feature = "std"), no_std)]
+
+pub use pallet::*;
+use {
+ frame_support::pallet_prelude::Weight,
+ log::log,
+ parity_scale_codec::{Decode, Encode, MaxEncodedLen},
+ scale_info::TypeInfo,
+ sp_runtime::traits::Get,
+ sp_runtime::RuntimeDebug,
+ sp_staking::SessionIndex,
+ sp_std::collections::btree_set::BTreeSet,
+ sp_std::vec::Vec,
+ tp_traits::{
+ ActiveEraInfo, EraIndex, EraIndexProvider, InvulnerablesProvider, OnEraEnd, OnEraStart,
+ ValidatorProvider,
+ },
+};
+
+#[cfg(test)]
+mod mock;
+
+#[cfg(test)]
+mod tests;
+
+#[cfg(feature = "runtime-benchmarks")]
+mod benchmarking;
+pub mod weights;
+
+#[frame_support::pallet]
+pub mod pallet {
+ pub use crate::weights::WeightInfo;
+
+ #[cfg(feature = "runtime-benchmarks")]
+ use frame_support::traits::Currency;
+
+ use {
+ super::*,
+ frame_support::{
+ dispatch::DispatchResultWithPostInfo,
+ pallet_prelude::*,
+ traits::{EnsureOrigin, UnixTime, ValidatorRegistration},
+ BoundedVec, DefaultNoBound,
+ },
+ frame_system::pallet_prelude::*,
+ sp_runtime::{traits::Convert, SaturatedConversion},
+ sp_std::vec::Vec,
+ };
+
+ /// Configure the pallet by specifying the parameters and types on which it depends.
+ #[pallet::config]
+ pub trait Config: frame_system::Config {
+ /// Overarching event type.
+ type RuntimeEvent: From> + IsType<::RuntimeEvent>;
+
+ /// Origin that can dictate updating parameters of this pallet.
+ type UpdateOrigin: EnsureOrigin;
+
+ /// Number of eras to keep in history.
+ ///
+ /// Following information is kept for eras in `[current_era -
+ /// HistoryDepth, current_era]`: `ErasStartSessionIndex`
+ ///
+ /// Must be more than the number of eras delayed by session.
+ /// I.e. active era must always be in history. I.e. `active_era >
+ /// current_era - history_depth` must be guaranteed.
+ ///
+ /// If migrating an existing pallet from storage value to config value,
+ /// this should be set to same value or greater as in storage.
+ #[pallet::constant]
+ type HistoryDepth: Get;
+
+ /// Maximum number of whitelisted validators.
+ #[pallet::constant]
+ type MaxWhitelistedValidators: Get;
+
+ /// Maximum number of external validators.
+ #[pallet::constant]
+ type MaxExternalValidators: Get;
+
+ /// A stable ID for a validator.
+ type ValidatorId: Member
+ + Parameter
+ + Ord
+ + MaybeSerializeDeserialize
+ + MaxEncodedLen
+ + TryFrom;
+
+ /// A conversion from account ID to validator ID.
+ ///
+ /// Its cost must be at most one storage read.
+ type ValidatorIdOf: Convert>;
+
+ /// Validate a user is registered
+ type ValidatorRegistration: ValidatorRegistration;
+
+ /// Time used for computing era duration.
+ ///
+ /// It is guaranteed to start being called from the first `on_finalize`. Thus value at
+ /// genesis is not used.
+ type UnixTime: UnixTime;
+
+ /// Number of sessions per era.
+ #[pallet::constant]
+ type SessionsPerEra: Get;
+
+ type OnEraStart: OnEraStart;
+ type OnEraEnd: OnEraEnd;
+
+ /// The weight information of this pallet.
+ type WeightInfo: WeightInfo;
+
+ #[cfg(feature = "runtime-benchmarks")]
+ type Currency: Currency
+ + frame_support::traits::fungible::Balanced;
+ }
+
+ #[pallet::pallet]
+ pub struct Pallet(_);
+
+ /// Fixed validators set by root/governance. Have priority over the external validators.
+ #[pallet::storage]
+ pub type WhitelistedValidators =
+ StorageValue<_, BoundedVec, ValueQuery>;
+
+ /// Validators set using storage proofs from another blockchain. Ignored if `SkipExternalValidators` is true.
+ #[pallet::storage]
+ pub type ExternalValidators =
+ StorageValue<_, BoundedVec, ValueQuery>;
+
+ /// Allow to disable external validators.
+ #[pallet::storage]
+ pub type SkipExternalValidators = StorageValue<_, bool, ValueQuery>;
+
+ /// The current era information, it is either ActiveEra or ActiveEra + 1 if the new era validators have been queued.
+ #[pallet::storage]
+ pub type CurrentEra = StorageValue<_, EraIndex>;
+
+ /// The active era information, it holds index and start.
+ #[pallet::storage]
+ pub type ActiveEra = StorageValue<_, ActiveEraInfo>;
+
+ /// The session index at which the era start for the last [`Config::HistoryDepth`] eras.
+ ///
+ /// Note: This tracks the starting session (i.e. session index when era start being active)
+ /// for the eras in `[CurrentEra - HISTORY_DEPTH, CurrentEra]`.
+ #[pallet::storage]
+ pub type ErasStartSessionIndex = StorageMap<_, Twox64Concat, EraIndex, SessionIndex>;
+
+ /// Mode of era forcing.
+ #[pallet::storage]
+ pub type ForceEra = StorageValue<_, Forcing, ValueQuery>;
+
+ #[pallet::genesis_config]
+ #[derive(DefaultNoBound)]
+ pub struct GenesisConfig {
+ pub skip_external_validators: bool,
+ pub whitelisted_validators: Vec,
+ }
+
+ #[pallet::genesis_build]
+ impl BuildGenesisConfig for GenesisConfig {
+ fn build(&self) {
+ let duplicate_validators = self
+ .whitelisted_validators
+ .iter()
+ // T::ValidatorId does not impl Ord or Hash so we cannot collect into set directly,
+ // but we can check for duplicates if we encode them first.
+ .map(|x| x.encode())
+ .collect::>();
+ assert!(
+ duplicate_validators.len() == self.whitelisted_validators.len(),
+ "duplicate validators in genesis."
+ );
+
+ let bounded_validators = BoundedVec::<_, T::MaxWhitelistedValidators>::try_from(
+ self.whitelisted_validators.clone(),
+ )
+ .expect("genesis validators are more than T::MaxWhitelistedValidators");
+
+ >::put(bounded_validators);
+ >::put(self.skip_external_validators);
+ }
+ }
+
+ #[pallet::event]
+ #[pallet::generate_deposit(pub(super) fn deposit_event)]
+ pub enum Event {
+ /// A new whitelisted validator was added.
+ WhitelistedValidatorAdded { account_id: T::AccountId },
+ /// A whitelisted validator was removed.
+ WhitelistedValidatorRemoved { account_id: T::AccountId },
+ /// A new era has started.
+ NewEra { era: EraIndex },
+ /// A new force era mode was set.
+ ForceEra { mode: Forcing },
+ }
+
+ #[pallet::error]
+ pub enum Error {
+ /// There are too many whitelisted validators.
+ TooManyWhitelisted,
+ /// Account is already whitelisted.
+ AlreadyWhitelisted,
+ /// Account is not whitelisted.
+ NotWhitelisted,
+ /// Account does not have keys registered
+ NoKeysRegistered,
+ /// Unable to derive validator id from account id
+ UnableToDeriveValidatorId,
+ }
+
+ #[pallet::call]
+ impl Pallet {
+ /// Allow to ignore external validators and use only whitelisted ones.
+ ///
+ /// The origin for this call must be the `UpdateOrigin`.
+ #[pallet::call_index(0)]
+ #[pallet::weight(T::WeightInfo::skip_external_validators())]
+ pub fn skip_external_validators(origin: OriginFor, skip: bool) -> DispatchResult {
+ T::UpdateOrigin::ensure_origin(origin)?;
+
+ >::put(skip);
+
+ Ok(())
+ }
+
+ /// Add a new account `who` to the list of `WhitelistedValidators`.
+ ///
+ /// The origin for this call must be the `UpdateOrigin`.
+ #[pallet::call_index(1)]
+ #[pallet::weight(T::WeightInfo::add_whitelisted(
+ T::MaxWhitelistedValidators::get().saturating_sub(1),
+ ))]
+ pub fn add_whitelisted(
+ origin: OriginFor,
+ who: T::AccountId,
+ ) -> DispatchResultWithPostInfo {
+ T::UpdateOrigin::ensure_origin(origin)?;
+ // don't let one unprepared collator ruin things for everyone.
+ let maybe_validator_id = T::ValidatorIdOf::convert(who.clone())
+ .filter(T::ValidatorRegistration::is_registered);
+
+ let validator_id = maybe_validator_id.ok_or(Error::::NoKeysRegistered)?;
+
+ >::try_mutate(|whitelisted| -> DispatchResult {
+ if whitelisted.contains(&validator_id) {
+ Err(Error::::AlreadyWhitelisted)?;
+ }
+ whitelisted
+ .try_push(validator_id.clone())
+ .map_err(|_| Error::::TooManyWhitelisted)?;
+ Ok(())
+ })?;
+
+ Self::deposit_event(Event::WhitelistedValidatorAdded { account_id: who });
+
+ let weight_used = ::WeightInfo::add_whitelisted(
+ WhitelistedValidators::::decode_len()
+ .unwrap_or_default()
+ .try_into()
+ .unwrap_or(T::MaxWhitelistedValidators::get().saturating_sub(1)),
+ );
+
+ Ok(Some(weight_used).into())
+ }
+
+ /// Remove an account `who` from the list of `WhitelistedValidators` collators.
+ ///
+ /// The origin for this call must be the `UpdateOrigin`.
+ #[pallet::call_index(2)]
+ #[pallet::weight(T::WeightInfo::remove_whitelisted(T::MaxWhitelistedValidators::get()))]
+ pub fn remove_whitelisted(origin: OriginFor, who: T::AccountId) -> DispatchResult {
+ T::UpdateOrigin::ensure_origin(origin)?;
+
+ let validator_id = T::ValidatorIdOf::convert(who.clone())
+ .ok_or(Error::::UnableToDeriveValidatorId)?;
+
+ >::try_mutate(|whitelisted| -> DispatchResult {
+ let pos = whitelisted
+ .iter()
+ .position(|x| x == &validator_id)
+ .ok_or(Error::::NotWhitelisted)?;
+ whitelisted.remove(pos);
+ Ok(())
+ })?;
+
+ Self::deposit_event(Event::WhitelistedValidatorRemoved { account_id: who });
+ Ok(())
+ }
+
+ /// Force when the next era will start. Possible values: next session, never, same as always.
+ #[pallet::call_index(3)]
+ #[pallet::weight(T::WeightInfo::force_era())]
+ pub fn force_era(origin: OriginFor, mode: Forcing) -> DispatchResult {
+ T::UpdateOrigin::ensure_origin(origin)?;
+ Self::set_force_era(mode);
+ Ok(())
+ }
+ }
+
+ impl Pallet {
+ pub fn set_external_validators(validators: Vec) -> DispatchResult {
+ // If more validators than max, take the first n
+ let validators = BoundedVec::truncate_from(validators);
+ >::put(validators);
+
+ Ok(())
+ }
+
+ /// Helper to set a new `ForceEra` mode.
+ pub(crate) fn set_force_era(mode: Forcing) {
+ log::info!("Setting force era mode {:?}.", mode);
+ ForceEra::::put(mode);
+ Self::deposit_event(Event::::ForceEra { mode });
+ }
+
+ pub fn whitelisted_validators() -> Vec {
+ >::get().into()
+ }
+
+ pub fn active_era() -> Option {
+ >::get()
+ }
+
+ pub fn current_era() -> Option {
+ >::get()
+ }
+
+ pub fn eras_start_session_index(era: EraIndex) -> Option {
+ >::get(era)
+ }
+
+ /// Returns validators for the next session. Whitelisted validators first, then external validators.
+ /// The returned list is deduplicated, but the order is respected.
+ /// If `SkipExternalValidators` is true, this function will ignore external validators.
+ pub fn validators() -> Vec {
+ let mut validators: Vec<_> = WhitelistedValidators::::get().into();
+
+ if !SkipExternalValidators::::get() {
+ validators.extend(ExternalValidators::::get())
+ }
+
+ remove_duplicates(validators)
+ }
+
+ /// Plan a new session potentially trigger a new era.
+ pub(crate) fn new_session(session_index: SessionIndex) -> Option> {
+ if let Some(current_era) = Self::current_era() {
+ // Initial era has been set.
+ let current_era_start_session_index = Self::eras_start_session_index(current_era)
+ .unwrap_or_else(|| {
+ frame_support::print(
+ "Error: start_session_index must be set for current_era",
+ );
+ 0
+ });
+
+ let era_length = session_index.saturating_sub(current_era_start_session_index); // Must never happen.
+
+ match ForceEra::::get() {
+ // Will be set to `NotForcing` again if a new era has been triggered.
+ Forcing::ForceNew => (),
+ // Short circuit to `try_trigger_new_era`.
+ Forcing::ForceAlways => (),
+ // Only go to `try_trigger_new_era` if deadline reached.
+ Forcing::NotForcing if era_length >= T::SessionsPerEra::get() => (),
+ _ => {
+ // Either `Forcing::ForceNone`,
+ // or `Forcing::NotForcing if era_length < T::SessionsPerEra::get()`.
+ return None;
+ }
+ }
+
+ // New era.
+ let maybe_new_era_validators = Self::try_trigger_new_era(session_index);
+ if maybe_new_era_validators.is_some()
+ && matches!(ForceEra::::get(), Forcing::ForceNew)
+ {
+ Self::set_force_era(Forcing::NotForcing);
+ }
+
+ maybe_new_era_validators
+ } else {
+ // Set initial era.
+ log!(log::Level::Debug, "Starting the first era.");
+ Self::try_trigger_new_era(session_index)
+ }
+ }
+
+ /// Start a session potentially starting an era.
+ pub(crate) fn start_session(start_session: SessionIndex) {
+ let next_active_era = Self::active_era().map(|e| e.index + 1).unwrap_or(0);
+ // This is only `Some` when current era has already progressed to the next era, while the
+ // active era is one behind (i.e. in the *last session of the active era*, or *first session
+ // of the new current era*, depending on how you look at it).
+ if let Some(next_active_era_start_session_index) =
+ Self::eras_start_session_index(next_active_era)
+ {
+ if next_active_era_start_session_index == start_session {
+ Self::start_era(start_session);
+ } else if next_active_era_start_session_index < start_session {
+ // This arm should never happen, but better handle it than to stall the pallet.
+ frame_support::print("Warning: A session appears to have been skipped.");
+ Self::start_era(start_session);
+ }
+ }
+ }
+
+ /// End a session potentially ending an era.
+ pub(crate) fn end_session(session_index: SessionIndex) {
+ if let Some(active_era) = Self::active_era() {
+ if let Some(next_active_era_start_session_index) =
+ Self::eras_start_session_index(active_era.index + 1)
+ {
+ if next_active_era_start_session_index == session_index + 1 {
+ Self::end_era(active_era, session_index);
+ }
+ }
+ }
+ }
+
+ /// Start a new era. It does:
+ /// * Increment `active_era.index`,
+ /// * reset `active_era.start`,
+ /// * emit `NewEra` event,
+ /// * call `OnEraStart` hook,
+ pub(crate) fn start_era(start_session: SessionIndex) {
+ let active_era = ActiveEra::::mutate(|active_era| {
+ let new_index = active_era.as_ref().map(|info| info.index + 1).unwrap_or(0);
+ *active_era = Some(ActiveEraInfo {
+ index: new_index,
+ // Set new active era start in next `on_finalize`. To guarantee usage of `Time`
+ start: None,
+ });
+ new_index
+ });
+ Self::deposit_event(Event::NewEra { era: active_era });
+ T::OnEraStart::on_era_start(active_era, start_session);
+ }
+
+ /// End era. It does:
+ /// * call `OnEraEnd` hook,
+ pub(crate) fn end_era(active_era: ActiveEraInfo, _session_index: SessionIndex) {
+ // Note: active_era.start can be None if end era is called during genesis config.
+ T::OnEraEnd::on_era_end(active_era.index);
+ }
+
+ /// Plan a new era.
+ ///
+ /// * Bump the current era storage (which holds the latest planned era).
+ /// * Store start session index for the new planned era.
+ /// * Clean old era information.
+ ///
+ /// Returns the new validator set.
+ pub fn trigger_new_era(start_session_index: SessionIndex) -> Vec {
+ // Increment or set current era.
+ let new_planned_era = CurrentEra::::mutate(|s| {
+ *s = Some(s.map(|s| s + 1).unwrap_or(0));
+ s.unwrap()
+ });
+ ErasStartSessionIndex::::insert(&new_planned_era, &start_session_index);
+
+ // Clean old era information.
+ if let Some(old_era) = new_planned_era.checked_sub(T::HistoryDepth::get() + 1) {
+ Self::clear_era_information(old_era);
+ }
+
+ // Returns new validators
+ Self::validators()
+ }
+
+ /// Potentially plan a new era.
+ ///
+ /// In case a new era is planned, the new validator set is returned.
+ pub(crate) fn try_trigger_new_era(
+ start_session_index: SessionIndex,
+ ) -> Option> {
+ Some(Self::trigger_new_era(start_session_index))
+ }
+
+ /// Clear all era information for given era.
+ pub(crate) fn clear_era_information(era_index: EraIndex) {
+ ErasStartSessionIndex::::remove(era_index);
+ }
+ }
+
+ #[pallet::hooks]
+ impl Hooks> for Pallet {
+ fn on_initialize(_now: BlockNumberFor) -> Weight {
+ // just return the weight of the on_finalize.
+ T::DbWeight::get().reads(1)
+ }
+
+ fn on_finalize(_n: BlockNumberFor) {
+ // Set the start of the first era.
+ if let Some(mut active_era) = >::get() {
+ if active_era.start.is_none() {
+ let now_as_millis_u64 = T::UnixTime::now().as_millis().saturated_into::();
+ active_era.start = Some(now_as_millis_u64);
+ // This write only ever happens once, we don't include it in the weight in
+ // general
+ ActiveEra::::put(active_era);
+ }
+ }
+ // `on_finalize` weight is tracked in `on_initialize`
+ }
+ }
+}
+
+/// Keeps only the first instance of each element in the input vec. Respects ordering of elements.
+fn remove_duplicates(input: Vec) -> Vec {
+ let mut seen = BTreeSet::new();
+ let mut result = Vec::with_capacity(input.len());
+
+ for item in input {
+ if seen.insert(item.clone()) {
+ result.push(item);
+ }
+ }
+
+ result
+}
+
+impl pallet_session::SessionManager for Pallet {
+ fn new_session(new_index: SessionIndex) -> Option> {
+ log!(log::Level::Trace, "planning new session {}", new_index);
+ Self::new_session(new_index)
+ }
+ fn new_session_genesis(new_index: SessionIndex) -> Option> {
+ log!(
+ log::Level::Trace,
+ "planning new session {} at genesis",
+ new_index
+ );
+ Self::new_session(new_index)
+ }
+ fn start_session(start_index: SessionIndex) {
+ log!(log::Level::Trace, "starting session {}", start_index);
+ Self::start_session(start_index)
+ }
+ fn end_session(end_index: SessionIndex) {
+ log!(log::Level::Trace, "ending session {}", end_index);
+ Self::end_session(end_index)
+ }
+}
+
+impl pallet_session::historical::SessionManager for Pallet {
+ fn new_session(new_index: SessionIndex) -> Option> {
+ >::new_session(new_index)
+ .map(|r| r.into_iter().map(|v| (v, Default::default())).collect())
+ }
+
+ fn start_session(start_index: SessionIndex) {
+ >::start_session(start_index)
+ }
+
+ fn end_session(end_index: SessionIndex) {
+ >::end_session(end_index)
+ }
+}
+
+impl EraIndexProvider for Pallet {
+ fn active_era() -> ActiveEraInfo {
+ >::get().unwrap_or(ActiveEraInfo {
+ index: 0,
+ start: None,
+ })
+ }
+
+ fn era_to_session_start(era_index: EraIndex) -> Option {
+ >::get(era_index)
+ }
+}
+
+impl ValidatorProvider for Pallet {
+ fn validators() -> Vec {
+ Self::validators()
+ }
+}
+
+impl InvulnerablesProvider for Pallet {
+ fn invulnerables() -> Vec {
+ Self::whitelisted_validators()
+ }
+}
+
+/// Mode of era-forcing.
+#[derive(
+ Copy, Clone, PartialEq, Eq, Default, Encode, Decode, RuntimeDebug, TypeInfo, MaxEncodedLen,
+)]
+pub enum Forcing {
+ /// Not forcing anything - just let whatever happen.
+ #[default]
+ NotForcing,
+ /// Force a new era on the next session start, then reset to `NotForcing` as soon as it is done.
+ ForceNew,
+ /// Avoid a new era indefinitely.
+ ForceNone,
+ /// Force a new era at the end of all sessions indefinitely.
+ ForceAlways,
+}
diff --git a/pallets/external-validators/src/mock.rs b/pallets/external-validators/src/mock.rs
new file mode 100644
index 000000000..4cc6c81b5
--- /dev/null
+++ b/pallets/external-validators/src/mock.rs
@@ -0,0 +1,342 @@
+// Copyright (C) Moondance Labs Ltd.
+// This file is part of Tanssi.
+
+// Tanssi 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.
+// Tanssi 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 Tanssi. If not, see
+
+use {
+ super::*,
+ crate as pallet_external_validators,
+ frame_support::{
+ assert_ok, ord_parameter_types, parameter_types,
+ traits::{
+ fungible::Mutate, ConstU32, ConstU64, OnFinalize, OnInitialize, ValidatorRegistration,
+ },
+ },
+ frame_system::{self as system, EnsureSignedBy},
+ pallet_balances::AccountData,
+ sp_core::H256,
+ sp_runtime::{
+ testing::UintAuthorityId,
+ traits::{BlakeTwo256, ConvertInto, IdentityLookup, OpaqueKeys},
+ BuildStorage, RuntimeAppPublic,
+ },
+};
+
+type Block = frame_system::mocking::MockBlock;
+
+// Configure a mock runtime to test the pallet.
+frame_support::construct_runtime!(
+ pub enum Test
+ {
+ System: frame_system,
+ ExternalValidators: pallet_external_validators,
+ Session: pallet_session,
+ Balances: pallet_balances,
+ Timestamp: pallet_timestamp,
+ Mock: mock_data,
+ }
+);
+
+parameter_types! {
+ pub const BlockHashCount: u64 = 250;
+ pub const SS58Prefix: u8 = 42;
+}
+
+impl system::Config for Test {
+ type BaseCallFilter = frame_support::traits::Everything;
+ type BlockWeights = ();
+ type BlockLength = ();
+ type DbWeight = ();
+ type RuntimeOrigin = RuntimeOrigin;
+ type RuntimeCall = RuntimeCall;
+ type Hash = H256;
+ type Hashing = BlakeTwo256;
+ type AccountId = u64;
+ type Lookup = IdentityLookup;
+ type RuntimeEvent = RuntimeEvent;
+ type BlockHashCount = BlockHashCount;
+ type Version = ();
+ type PalletInfo = PalletInfo;
+ type AccountData = AccountData;
+ type OnNewAccount = ();
+ type OnKilledAccount = ();
+ type SystemWeightInfo = ();
+ type SS58Prefix = SS58Prefix;
+ type OnSetCode = ();
+ type MaxConsumers = frame_support::traits::ConstU32<16>;
+ type Nonce = u64;
+ type Block = Block;
+ type RuntimeTask = ();
+ type SingleBlockMigrations = ();
+ type MultiBlockMigrator = ();
+ type PreInherents = ();
+ type PostInherents = ();
+ type PostTransactions = ();
+}
+
+parameter_types! {
+ pub const ExistentialDeposit: u64 = 5;
+ pub const MaxReserves: u32 = 50;
+}
+
+impl pallet_balances::Config for Test {
+ type RuntimeEvent = RuntimeEvent;
+ type WeightInfo = ();
+ type Balance = u64;
+ type DustRemoval = ();
+ type ExistentialDeposit = ExistentialDeposit;
+ type AccountStore = System;
+ type ReserveIdentifier = [u8; 8];
+ type RuntimeHoldReason = ();
+ type RuntimeFreezeReason = ();
+ type FreezeIdentifier = ();
+ type MaxLocks = ();
+ type MaxReserves = MaxReserves;
+ type MaxFreezes = ConstU32<0>;
+}
+
+impl pallet_timestamp::Config for Test {
+ type Moment = u64;
+ type OnTimestampSet = ();
+ type MinimumPeriod = ConstU64<5>;
+ type WeightInfo = ();
+}
+
+ord_parameter_types! {
+ pub const RootAccount: u64 = 777;
+}
+
+pub struct IsRegistered;
+impl ValidatorRegistration for IsRegistered {
+ fn is_registered(id: &u64) -> bool {
+ *id != 42u64
+ }
+}
+
+parameter_types! {
+ pub const SessionsPerEra: SessionIndex = 6;
+}
+
+impl Config for Test {
+ type RuntimeEvent = RuntimeEvent;
+ type UpdateOrigin = EnsureSignedBy;
+ type HistoryDepth = ConstU32<84>;
+ type MaxWhitelistedValidators = ConstU32<20>;
+ type MaxExternalValidators = ConstU32<20>;
+ type ValidatorId = ::AccountId;
+ type ValidatorIdOf = ConvertInto;
+ type ValidatorRegistration = IsRegistered;
+ type UnixTime = Timestamp;
+ type SessionsPerEra = SessionsPerEra;
+ type OnEraStart = Mock;
+ type OnEraEnd = Mock;
+ type WeightInfo = ();
+ #[cfg(feature = "runtime-benchmarks")]
+ type Currency = Balances;
+}
+
+sp_runtime::impl_opaque_keys! {
+ pub struct MockSessionKeys {
+ // a key for aura authoring
+ pub aura: UintAuthorityId,
+ }
+}
+
+impl From for MockSessionKeys {
+ fn from(aura: sp_runtime::testing::UintAuthorityId) -> Self {
+ Self { aura }
+ }
+}
+
+parameter_types! {
+ pub static SessionHandlerCollators: Vec = Vec::new();
+ pub static SessionChangeBlock: u64 = 0;
+}
+
+pub struct TestSessionHandler;
+impl pallet_session::SessionHandler for TestSessionHandler {
+ const KEY_TYPE_IDS: &'static [sp_runtime::KeyTypeId] = &[UintAuthorityId::ID];
+ fn on_genesis_session(keys: &[(u64, Ks)]) {
+ SessionHandlerCollators::set(keys.iter().map(|(a, _)| *a).collect::>())
+ }
+ fn on_new_session(_: bool, keys: &[(u64, Ks)], _: &[(u64, Ks)]) {
+ SessionChangeBlock::set(System::block_number());
+ SessionHandlerCollators::set(keys.iter().map(|(a, _)| *a).collect::>())
+ }
+ fn on_before_session_ending() {}
+ fn on_disabled(_: u32) {}
+}
+
+parameter_types! {
+ pub const Offset: u64 = 0;
+ pub const Period: u64 = 5;
+}
+
+impl pallet_session::Config for Test {
+ type RuntimeEvent = RuntimeEvent;
+ type ValidatorId = ::AccountId;
+ // we don't have stash and controller, thus we don't need the convert as well.
+ type ValidatorIdOf = ConvertInto;
+ type ShouldEndSession = pallet_session::PeriodicSessions;
+ type NextSessionRotation = pallet_session::PeriodicSessions;
+ type SessionManager = ExternalValidators;
+ type SessionHandler = TestSessionHandler;
+ type Keys = MockSessionKeys;
+ type WeightInfo = ();
+}
+
+// Pallet to provide some mock data, used to test
+#[frame_support::pallet]
+pub mod mock_data {
+ use {crate::mock::Mocks, frame_support::pallet_prelude::*};
+
+ #[pallet::config]
+ pub trait Config: frame_system::Config {}
+
+ #[pallet::call]
+ impl Pallet {}
+
+ #[pallet::pallet]
+ #[pallet::without_storage_info]
+ pub struct Pallet(_);
+
+ #[pallet::storage]
+ pub(super) type Mock = StorageValue<_, Mocks, ValueQuery>;
+
+ impl Pallet {
+ pub fn mock() -> Mocks {
+ Mock::::get()
+ }
+ pub fn mutate(f: F) -> R
+ where
+ F: FnOnce(&mut Mocks) -> R,
+ {
+ Mock::::mutate(f)
+ }
+ }
+}
+
+#[derive(Clone, Encode, Decode, PartialEq, sp_core::RuntimeDebug, scale_info::TypeInfo)]
+pub enum HookCall {
+ OnEraStart { era: u32, session: u32 },
+ OnEraEnd { era: u32 },
+}
+
+impl mock_data::Config for Test {}
+
+#[derive(
+ Clone, Default, Encode, Decode, PartialEq, sp_core::RuntimeDebug, scale_info::TypeInfo,
+)]
+pub struct Mocks {
+ pub called_hooks: Vec,
+}
+
+// We use the mock_data pallet to test hooks: we store a list of all the calls, and then check that
+// no eras are skipped.
+impl OnEraStart for mock_data::Pallet {
+ fn on_era_start(era_index: EraIndex, session_start: u32) {
+ Mock::mutate(|m| {
+ m.called_hooks.push(HookCall::OnEraStart {
+ era: era_index,
+ session: session_start,
+ });
+ });
+ }
+}
+
+impl OnEraEnd for mock_data::Pallet {
+ fn on_era_end(era_index: EraIndex) {
+ Mock::mutate(|m| {
+ m.called_hooks.push(HookCall::OnEraEnd { era: era_index });
+ });
+ }
+}
+
+pub fn new_test_ext() -> sp_io::TestExternalities {
+ let mut t = frame_system::GenesisConfig::::default()
+ .build_storage()
+ .unwrap();
+ let whitelisted_validators = vec![1, 2];
+
+ let balances = vec![(1, 100), (2, 100), (3, 100), (4, 100), (5, 100)];
+ let keys = balances
+ .iter()
+ .map(|&(i, _)| {
+ (
+ i,
+ i,
+ MockSessionKeys {
+ aura: UintAuthorityId(i),
+ },
+ )
+ })
+ .collect::>();
+ let session = pallet_session::GenesisConfig:: {
+ keys,
+ ..Default::default()
+ };
+ pallet_balances::GenesisConfig:: { balances }
+ .assimilate_storage(&mut t)
+ .unwrap();
+ pallet_external_validators::GenesisConfig:: {
+ skip_external_validators: false,
+ whitelisted_validators,
+ }
+ .assimilate_storage(&mut t)
+ .unwrap();
+ session.assimilate_storage(&mut t).unwrap();
+
+ let mut ext: sp_io::TestExternalities = t.into();
+
+ // Initialize accounts and keys for external validators
+ ext.execute_with(|| {
+ initialize_validators(vec![50, 51]);
+ });
+
+ ext
+}
+
+fn initialize_validators(validators: Vec) {
+ for x in validators {
+ assert_ok!(Balances::mint_into(&x, 10_000_000_000));
+ assert_ok!(Session::set_keys(
+ RuntimeOrigin::signed(x),
+ MockSessionKeys::from(UintAuthorityId(x)),
+ vec![]
+ ));
+ }
+}
+
+pub const INIT_TIMESTAMP: u64 = 30_000;
+pub const BLOCK_TIME: u64 = 1000;
+
+pub fn run_to_session(n: u32) {
+ let block_number = Period::get() * u64::from(n);
+ run_to_block(block_number + 1);
+}
+
+pub fn run_to_block(n: u64) {
+ let old_block_number = System::block_number();
+
+ for x in old_block_number..n {
+ ExternalValidators::on_finalize(System::block_number());
+ Session::on_finalize(System::block_number());
+
+ System::reset_events();
+ System::set_block_number(x + 1);
+ Timestamp::set_timestamp(System::block_number() * BLOCK_TIME + INIT_TIMESTAMP);
+
+ ExternalValidators::on_initialize(System::block_number());
+ Session::on_initialize(System::block_number());
+ }
+}
diff --git a/pallets/external-validators/src/tests.rs b/pallets/external-validators/src/tests.rs
new file mode 100644
index 000000000..173e64316
--- /dev/null
+++ b/pallets/external-validators/src/tests.rs
@@ -0,0 +1,269 @@
+// Copyright (C) Moondance Labs Ltd.
+// This file is part of Tanssi.
+
+// Tanssi 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.
+// Tanssi 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 Tanssi. If not, see
+
+use {
+ crate::{
+ mock::{
+ new_test_ext, run_to_block, run_to_session, ExternalValidators, HookCall, Mock,
+ RootAccount, RuntimeEvent, RuntimeOrigin, Session, System, Test,
+ },
+ Error,
+ },
+ frame_support::{assert_noop, assert_ok},
+ sp_runtime::traits::BadOrigin,
+ tp_traits::ValidatorProvider,
+};
+
+#[test]
+fn basic_setup_works() {
+ new_test_ext().execute_with(|| {
+ assert_eq!(ExternalValidators::whitelisted_validators(), vec![1, 2]);
+ });
+}
+
+#[test]
+fn add_whitelisted_works() {
+ new_test_ext().execute_with(|| {
+ run_to_block(1);
+ assert_eq!(ExternalValidators::whitelisted_validators(), vec![1, 2]);
+ let new = 3;
+
+ // function runs
+ assert_ok!(ExternalValidators::add_whitelisted(
+ RuntimeOrigin::signed(RootAccount::get()),
+ new
+ ));
+
+ System::assert_last_event(RuntimeEvent::ExternalValidators(
+ crate::Event::WhitelistedValidatorAdded { account_id: new },
+ ));
+
+ // same element cannot be added more than once
+ assert_noop!(
+ ExternalValidators::add_whitelisted(RuntimeOrigin::signed(RootAccount::get()), new),
+ Error::::AlreadyWhitelisted
+ );
+
+ // new element is now part of the invulnerables list
+ assert!(ExternalValidators::whitelisted_validators()
+ .to_vec()
+ .contains(&new));
+
+ // cannot add with non-root
+ assert_noop!(
+ ExternalValidators::add_whitelisted(RuntimeOrigin::signed(1), new),
+ BadOrigin
+ );
+ });
+}
+
+#[test]
+fn add_whitelisted_does_not_work_if_not_registered() {
+ new_test_ext().execute_with(|| {
+ run_to_block(1);
+ assert_eq!(ExternalValidators::whitelisted_validators(), vec![1, 2]);
+ let new = 42;
+
+ assert_noop!(
+ ExternalValidators::add_whitelisted(RuntimeOrigin::signed(RootAccount::get()), new),
+ Error::::NoKeysRegistered
+ );
+ });
+}
+
+#[test]
+fn validator_limit_works() {
+ new_test_ext().execute_with(|| {
+ assert_eq!(ExternalValidators::whitelisted_validators(), vec![1, 2]);
+
+ // MaxExternalValidators: u32 = 20
+ for ii in 3..=21 {
+ if ii < 21 {
+ assert_ok!(ExternalValidators::add_whitelisted(
+ RuntimeOrigin::signed(RootAccount::get()),
+ ii
+ ));
+ } else {
+ assert_noop!(
+ ExternalValidators::add_whitelisted(
+ RuntimeOrigin::signed(RootAccount::get()),
+ ii
+ ),
+ Error::::TooManyWhitelisted
+ );
+ }
+ }
+ let expected: Vec = (1..=20).collect();
+ assert_eq!(ExternalValidators::whitelisted_validators(), expected);
+ });
+}
+
+#[test]
+fn remove_whitelisted_works() {
+ new_test_ext().execute_with(|| {
+ run_to_block(1);
+ assert_eq!(ExternalValidators::whitelisted_validators(), vec![1, 2]);
+
+ assert_ok!(ExternalValidators::add_whitelisted(
+ RuntimeOrigin::signed(RootAccount::get()),
+ 4
+ ));
+ assert_ok!(ExternalValidators::add_whitelisted(
+ RuntimeOrigin::signed(RootAccount::get()),
+ 3
+ ));
+
+ assert_eq!(
+ ExternalValidators::whitelisted_validators(),
+ vec![1, 2, 4, 3]
+ );
+
+ assert_ok!(ExternalValidators::remove_whitelisted(
+ RuntimeOrigin::signed(RootAccount::get()),
+ 2
+ ));
+
+ System::assert_last_event(RuntimeEvent::ExternalValidators(
+ crate::Event::WhitelistedValidatorRemoved { account_id: 2 },
+ ));
+ assert_eq!(ExternalValidators::whitelisted_validators(), vec![1, 4, 3]);
+
+ // cannot remove invulnerable not in the list
+ assert_noop!(
+ ExternalValidators::remove_whitelisted(RuntimeOrigin::signed(RootAccount::get()), 2),
+ Error::::NotWhitelisted
+ );
+
+ // cannot remove without privilege
+ assert_noop!(
+ ExternalValidators::remove_whitelisted(RuntimeOrigin::signed(1), 3),
+ BadOrigin
+ );
+ });
+}
+
+#[test]
+fn whitelisted_and_external_order() {
+ new_test_ext().execute_with(|| {
+ run_to_block(1);
+ assert_eq!(ExternalValidators::whitelisted_validators(), vec![1, 2]);
+ assert_ok!(ExternalValidators::set_external_validators(vec![50, 51]));
+
+ run_to_session(6);
+ let validators = Session::validators();
+ assert_eq!(validators, vec![1, 2, 50, 51]);
+ });
+}
+
+#[test]
+fn validator_provider_returns_all_validators() {
+ new_test_ext().execute_with(|| {
+ run_to_block(1);
+ assert_eq!(ExternalValidators::whitelisted_validators(), vec![1, 2]);
+ assert_ok!(ExternalValidators::set_external_validators(vec![50, 51]));
+
+ run_to_session(6);
+ let validators_new_session = Session::validators();
+ let validators_provider = >::validators();
+ assert_eq!(validators_new_session, validators_provider);
+ });
+}
+
+#[test]
+fn can_skip_external_validators() {
+ new_test_ext().execute_with(|| {
+ run_to_block(1);
+ assert_eq!(ExternalValidators::whitelisted_validators(), vec![1, 2]);
+ assert_ok!(ExternalValidators::set_external_validators(vec![50, 51]));
+ assert_ok!(ExternalValidators::skip_external_validators(
+ RuntimeOrigin::signed(RootAccount::get()),
+ true
+ ));
+
+ run_to_session(6);
+ let validators = Session::validators();
+ assert_eq!(validators, vec![1, 2]);
+ });
+}
+
+#[test]
+fn duplicate_validators_are_deduplicated() {
+ new_test_ext().execute_with(|| {
+ run_to_block(1);
+ assert_eq!(ExternalValidators::whitelisted_validators(), vec![1, 2]);
+ assert_ok!(ExternalValidators::set_external_validators(vec![2]));
+
+ run_to_session(6);
+ let validators = Session::validators();
+ assert_eq!(validators, vec![1, 2]);
+ });
+}
+
+#[test]
+fn duplicate_validator_order_is_preserved() {
+ new_test_ext().execute_with(|| {
+ run_to_block(1);
+ // Whitelisted validators have priority, so their ordering should be respected
+ // Need to manually remove and add each whitelisted because there is no "set_whitelisted"
+ assert_ok!(ExternalValidators::remove_whitelisted(
+ RuntimeOrigin::signed(RootAccount::get()),
+ 1
+ ));
+ assert_ok!(ExternalValidators::remove_whitelisted(
+ RuntimeOrigin::signed(RootAccount::get()),
+ 2
+ ));
+ assert_ok!(ExternalValidators::add_whitelisted(
+ RuntimeOrigin::signed(RootAccount::get()),
+ 3
+ ));
+ assert_ok!(ExternalValidators::add_whitelisted(
+ RuntimeOrigin::signed(RootAccount::get()),
+ 1
+ ));
+ assert_ok!(ExternalValidators::add_whitelisted(
+ RuntimeOrigin::signed(RootAccount::get()),
+ 2
+ ));
+ assert_eq!(ExternalValidators::whitelisted_validators(), vec![3, 1, 2]);
+ assert_ok!(ExternalValidators::set_external_validators(vec![
+ 3, 2, 1, 4
+ ]));
+
+ run_to_session(6);
+ let validators = Session::validators();
+ assert_eq!(validators, vec![3, 1, 2, 4]);
+ });
+}
+
+#[test]
+fn era_hooks() {
+ new_test_ext().execute_with(|| {
+ run_to_session(14);
+
+ let expected_calls = vec![
+ HookCall::OnEraStart { era: 0, session: 0 },
+ HookCall::OnEraEnd { era: 0 },
+ HookCall::OnEraStart { era: 1, session: 6 },
+ HookCall::OnEraEnd { era: 1 },
+ HookCall::OnEraStart {
+ era: 2,
+ session: 12,
+ },
+ ];
+
+ assert_eq!(Mock::mock().called_hooks, expected_calls);
+ });
+}
diff --git a/pallets/external-validators/src/weights.rs b/pallets/external-validators/src/weights.rs
new file mode 100644
index 000000000..1ea819a2b
--- /dev/null
+++ b/pallets/external-validators/src/weights.rs
@@ -0,0 +1,220 @@
+// Copyright (C) Moondance Labs Ltd.
+// This file is part of Tanssi.
+
+// Tanssi 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.
+
+// Tanssi 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 Tanssi. If not, see
+
+
+//! Autogenerated weights for pallet_external_validators
+//!
+//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 42.0.0
+//! DATE: 2024-10-22, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]`
+//! WORST CASE MAP SIZE: `1000000`
+//! HOSTNAME: `tomasz-XPS-15-9520`, CPU: `12th Gen Intel(R) Core(TM) i7-12700H`
+//! EXECUTION: , WASM-EXECUTION: Compiled, CHAIN: Some("dancelight-dev"), DB CACHE: 1024
+
+// Executed Command:
+// target/release/tanssi-relay
+// benchmark
+// pallet
+// --execution=wasm
+// --wasm-execution=compiled
+// --pallet
+// pallet_external_validators
+// --extrinsic
+// *
+// --chain=dancelight-dev
+// --steps
+// 50
+// --repeat
+// 20
+// --template=benchmarking/frame-weight-pallet-template.hbs
+// --json-file
+// raw.json
+// --output
+// tmp/dancelight_weights/pallet_external_validators.rs
+
+#![cfg_attr(rustfmt, rustfmt_skip)]
+#![allow(unused_parens)]
+#![allow(unused_imports)]
+
+use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}};
+use sp_std::marker::PhantomData;
+
+/// Weight functions needed for pallet_external_validators.
+pub trait WeightInfo {
+ fn skip_external_validators() -> Weight;
+ fn add_whitelisted(b: u32, ) -> Weight;
+ fn remove_whitelisted(b: u32, ) -> Weight;
+ fn force_era() -> Weight;
+ fn new_session(r: u32, ) -> Weight;
+}
+
+/// Weights for pallet_external_validators using the Substrate node and recommended hardware.
+pub struct SubstrateWeight(PhantomData);
+impl WeightInfo for SubstrateWeight {
+ /// Storage: `ExternalValidators::SkipExternalValidators` (r:0 w:1)
+ /// Proof: `ExternalValidators::SkipExternalValidators` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`)
+ fn skip_external_validators() -> Weight {
+ // Proof Size summary in bytes:
+ // Measured: `0`
+ // Estimated: `0`
+ // Minimum execution time: 1_391_000 picoseconds.
+ Weight::from_parts(1_484_000, 0)
+ .saturating_add(T::DbWeight::get().writes(1_u64))
+ }
+ /// Storage: `Session::NextKeys` (r:1 w:0)
+ /// Proof: `Session::NextKeys` (`max_values`: None, `max_size`: None, mode: `Measured`)
+ /// Storage: `ExternalValidators::WhitelistedValidators` (r:1 w:1)
+ /// Proof: `ExternalValidators::WhitelistedValidators` (`max_values`: Some(1), `max_size`: Some(3202), added: 3697, mode: `MaxEncodedLen`)
+ /// The range of component `b` is `[1, 99]`.
+ fn add_whitelisted(b: u32, ) -> Weight {
+ // Proof Size summary in bytes:
+ // Measured: `845 + b * (36 ±0)`
+ // Estimated: `4687 + b * (37 ±0)`
+ // Minimum execution time: 12_829_000 picoseconds.
+ Weight::from_parts(17_541_907, 4687)
+ // Standard Error: 1_560
+ .saturating_add(Weight::from_parts(62_143, 0).saturating_mul(b.into()))
+ .saturating_add(T::DbWeight::get().reads(2_u64))
+ .saturating_add(T::DbWeight::get().writes(1_u64))
+ .saturating_add(Weight::from_parts(0, 37).saturating_mul(b.into()))
+ }
+ /// Storage: `ExternalValidators::WhitelistedValidators` (r:1 w:1)
+ /// Proof: `ExternalValidators::WhitelistedValidators` (`max_values`: Some(1), `max_size`: Some(3202), added: 3697, mode: `MaxEncodedLen`)
+ /// The range of component `b` is `[1, 100]`.
+ fn remove_whitelisted(b: u32, ) -> Weight {
+ // Proof Size summary in bytes:
+ // Measured: `137 + b * (32 ±0)`
+ // Estimated: `4687`
+ // Minimum execution time: 7_269_000 picoseconds.
+ Weight::from_parts(9_100_286, 4687)
+ // Standard Error: 626
+ .saturating_add(Weight::from_parts(35_303, 0).saturating_mul(b.into()))
+ .saturating_add(T::DbWeight::get().reads(1_u64))
+ .saturating_add(T::DbWeight::get().writes(1_u64))
+ }
+ /// Storage: `ExternalValidators::ForceEra` (r:0 w:1)
+ /// Proof: `ExternalValidators::ForceEra` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`)
+ fn force_era() -> Weight {
+ // Proof Size summary in bytes:
+ // Measured: `0`
+ // Estimated: `0`
+ // Minimum execution time: 4_578_000 picoseconds.
+ Weight::from_parts(4_924_000, 0)
+ .saturating_add(T::DbWeight::get().writes(1_u64))
+ }
+ /// Storage: `ExternalValidators::ForceEra` (r:1 w:0)
+ /// Proof: `ExternalValidators::ForceEra` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`)
+ /// Storage: `ExternalValidators::EraSessionStart` (r:1 w:1)
+ /// Proof: `ExternalValidators::EraSessionStart` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
+ /// Storage: `ExternalValidators::ActiveEra` (r:1 w:1)
+ /// Proof: `ExternalValidators::ActiveEra` (`max_values`: Some(1), `max_size`: Some(13), added: 508, mode: `MaxEncodedLen`)
+ /// Storage: `ExternalValidators::WhitelistedValidators` (r:1 w:0)
+ /// Proof: `ExternalValidators::WhitelistedValidators` (`max_values`: Some(1), `max_size`: Some(3202), added: 3697, mode: `MaxEncodedLen`)
+ /// Storage: `ExternalValidators::SkipExternalValidators` (r:1 w:0)
+ /// Proof: `ExternalValidators::SkipExternalValidators` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`)
+ /// Storage: `ExternalValidators::ExternalValidators` (r:1 w:0)
+ /// Proof: `ExternalValidators::ExternalValidators` (`max_values`: Some(1), `max_size`: Some(3202), added: 3697, mode: `MaxEncodedLen`)
+ /// The range of component `r` is `[1, 100]`.
+ fn new_session(r: u32, ) -> Weight {
+ // Proof Size summary in bytes:
+ // Measured: `137 + r * (32 ±0)`
+ // Estimated: `4687`
+ // Minimum execution time: 8_587_000 picoseconds.
+ Weight::from_parts(10_453_582, 4687)
+ // Standard Error: 555
+ .saturating_add(Weight::from_parts(27_159, 0).saturating_mul(r.into()))
+ .saturating_add(T::DbWeight::get().reads(6_u64))
+ .saturating_add(T::DbWeight::get().writes(2_u64))
+ }
+}
+
+// For backwards compatibility and tests
+impl WeightInfo for () {
+ /// Storage: `ExternalValidators::SkipExternalValidators` (r:0 w:1)
+ /// Proof: `ExternalValidators::SkipExternalValidators` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`)
+ fn skip_external_validators() -> Weight {
+ // Proof Size summary in bytes:
+ // Measured: `0`
+ // Estimated: `0`
+ // Minimum execution time: 1_391_000 picoseconds.
+ Weight::from_parts(1_484_000, 0)
+ .saturating_add(RocksDbWeight::get().writes(1_u64))
+ }
+ /// Storage: `Session::NextKeys` (r:1 w:0)
+ /// Proof: `Session::NextKeys` (`max_values`: None, `max_size`: None, mode: `Measured`)
+ /// Storage: `ExternalValidators::WhitelistedValidators` (r:1 w:1)
+ /// Proof: `ExternalValidators::WhitelistedValidators` (`max_values`: Some(1), `max_size`: Some(3202), added: 3697, mode: `MaxEncodedLen`)
+ /// The range of component `b` is `[1, 99]`.
+ fn add_whitelisted(b: u32, ) -> Weight {
+ // Proof Size summary in bytes:
+ // Measured: `845 + b * (36 ±0)`
+ // Estimated: `4687 + b * (37 ±0)`
+ // Minimum execution time: 12_829_000 picoseconds.
+ Weight::from_parts(17_541_907, 4687)
+ // Standard Error: 1_560
+ .saturating_add(Weight::from_parts(62_143, 0).saturating_mul(b.into()))
+ .saturating_add(RocksDbWeight::get().reads(2_u64))
+ .saturating_add(RocksDbWeight::get().writes(1_u64))
+ .saturating_add(Weight::from_parts(0, 37).saturating_mul(b.into()))
+ }
+ /// Storage: `ExternalValidators::WhitelistedValidators` (r:1 w:1)
+ /// Proof: `ExternalValidators::WhitelistedValidators` (`max_values`: Some(1), `max_size`: Some(3202), added: 3697, mode: `MaxEncodedLen`)
+ /// The range of component `b` is `[1, 100]`.
+ fn remove_whitelisted(b: u32, ) -> Weight {
+ // Proof Size summary in bytes:
+ // Measured: `137 + b * (32 ±0)`
+ // Estimated: `4687`
+ // Minimum execution time: 7_269_000 picoseconds.
+ Weight::from_parts(9_100_286, 4687)
+ // Standard Error: 626
+ .saturating_add(Weight::from_parts(35_303, 0).saturating_mul(b.into()))
+ .saturating_add(RocksDbWeight::get().reads(1_u64))
+ .saturating_add(RocksDbWeight::get().writes(1_u64))
+ }
+ /// Storage: `ExternalValidators::ForceEra` (r:0 w:1)
+ /// Proof: `ExternalValidators::ForceEra` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`)
+ fn force_era() -> Weight {
+ // Proof Size summary in bytes:
+ // Measured: `0`
+ // Estimated: `0`
+ // Minimum execution time: 4_578_000 picoseconds.
+ Weight::from_parts(4_924_000, 0)
+ .saturating_add(RocksDbWeight::get().writes(1_u64))
+ }
+ /// Storage: `ExternalValidators::ForceEra` (r:1 w:0)
+ /// Proof: `ExternalValidators::ForceEra` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`)
+ /// Storage: `ExternalValidators::EraSessionStart` (r:1 w:1)
+ /// Proof: `ExternalValidators::EraSessionStart` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
+ /// Storage: `ExternalValidators::ActiveEra` (r:1 w:1)
+ /// Proof: `ExternalValidators::ActiveEra` (`max_values`: Some(1), `max_size`: Some(13), added: 508, mode: `MaxEncodedLen`)
+ /// Storage: `ExternalValidators::WhitelistedValidators` (r:1 w:0)
+ /// Proof: `ExternalValidators::WhitelistedValidators` (`max_values`: Some(1), `max_size`: Some(3202), added: 3697, mode: `MaxEncodedLen`)
+ /// Storage: `ExternalValidators::SkipExternalValidators` (r:1 w:0)
+ /// Proof: `ExternalValidators::SkipExternalValidators` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`)
+ /// Storage: `ExternalValidators::ExternalValidators` (r:1 w:0)
+ /// Proof: `ExternalValidators::ExternalValidators` (`max_values`: Some(1), `max_size`: Some(3202), added: 3697, mode: `MaxEncodedLen`)
+ /// The range of component `r` is `[1, 100]`.
+ fn new_session(r: u32, ) -> Weight {
+ // Proof Size summary in bytes:
+ // Measured: `137 + r * (32 ±0)`
+ // Estimated: `4687`
+ // Minimum execution time: 8_587_000 picoseconds.
+ Weight::from_parts(10_453_582, 4687)
+ // Standard Error: 555
+ .saturating_add(Weight::from_parts(27_159, 0).saturating_mul(r.into()))
+ .saturating_add(RocksDbWeight::get().reads(6_u64))
+ .saturating_add(RocksDbWeight::get().writes(2_u64))
+ }
+}
diff --git a/pallets/invulnerables/src/lib.rs b/pallets/invulnerables/src/lib.rs
index f6dddd382..bf832ca25 100644
--- a/pallets/invulnerables/src/lib.rs
+++ b/pallets/invulnerables/src/lib.rs
@@ -65,15 +65,6 @@ pub mod pallet {
/// The current storage version.
const STORAGE_VERSION: StorageVersion = StorageVersion::new(0);
- /// A convertor from collators id. Since this pallet does not have stash/controller, this is
- /// just identity.
- pub struct IdentityCollator;
- impl sp_runtime::traits::Convert> for IdentityCollator {
- fn convert(t: T) -> Option {
- Some(t)
- }
- }
-
/// Configure the pallet by specifying the parameters and types on which it depends.
#[pallet::config]
pub trait Config: frame_system::Config {
@@ -110,7 +101,7 @@ pub mod pallet {
#[pallet::storage_version(STORAGE_VERSION)]
pub struct Pallet(_);
- /// The invulnerable, permissioned collators. This list must be sorted.
+ /// The invulnerable, permissioned collators.
#[pallet::storage]
pub type Invulnerables =
StorageValue<_, BoundedVec, ValueQuery>;
@@ -144,15 +135,10 @@ pub mod pallet {
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event {
- /// New Invulnerables were set.
- NewInvulnerables { invulnerables: Vec },
/// A new Invulnerable was added.
InvulnerableAdded { account_id: T::AccountId },
/// An Invulnerable was removed.
InvulnerableRemoved { account_id: T::AccountId },
- /// An account was unable to be added to the Invulnerables because they did not have keys
- /// registered. Other Invulnerables may have been set.
- InvalidInvulnerableSkipped { account_id: T::AccountId },
}
#[pallet::error]
@@ -211,8 +197,7 @@ pub mod pallet {
Ok(Some(weight_used).into())
}
- /// Remove an account `who` from the list of `Invulnerables` collators. `Invulnerables` must
- /// be sorted.
+ /// Remove an account `who` from the list of `Invulnerables` collators.
///
/// The origin for this call must be the `UpdateOrigin`.
#[pallet::call_index(2)]
diff --git a/pallets/invulnerables/src/mock.rs b/pallets/invulnerables/src/mock.rs
index 22672aac2..b019f778a 100644
--- a/pallets/invulnerables/src/mock.rs
+++ b/pallets/invulnerables/src/mock.rs
@@ -25,7 +25,7 @@ use {
sp_core::H256,
sp_runtime::{
testing::UintAuthorityId,
- traits::{BlakeTwo256, IdentityLookup, OpaqueKeys},
+ traits::{BlakeTwo256, ConvertInto, IdentityLookup, OpaqueKeys},
BuildStorage, RuntimeAppPublic,
},
};
@@ -117,7 +117,7 @@ impl Config for Test {
type UpdateOrigin = EnsureSignedBy;
type MaxInvulnerables = ConstU32<20>;
type CollatorId = ::AccountId;
- type CollatorIdOf = IdentityCollator;
+ type CollatorIdOf = ConvertInto;
type CollatorRegistration = IsRegistered;
type WeightInfo = ();
#[cfg(feature = "runtime-benchmarks")]
@@ -165,7 +165,7 @@ impl pallet_session::Config for Test {
type RuntimeEvent = RuntimeEvent;
type ValidatorId = ::AccountId;
// we don't have stash and controller, thus we don't need the convert as well.
- type ValidatorIdOf = IdentityCollator;
+ type ValidatorIdOf = ConvertInto;
type ShouldEndSession = pallet_session::PeriodicSessions;
type NextSessionRotation = pallet_session::PeriodicSessions;
type SessionManager = Invulnerables;
diff --git a/pallets/invulnerables/src/tests.rs b/pallets/invulnerables/src/tests.rs
index 928aff0bb..88d4aba4f 100644
--- a/pallets/invulnerables/src/tests.rs
+++ b/pallets/invulnerables/src/tests.rs
@@ -28,7 +28,6 @@ use {
#[test]
fn basic_setup_works() {
new_test_ext().execute_with(|| {
- // genesis should sort input
assert_eq!(Invulnerables::invulnerables(), vec![1, 2]);
});
}
diff --git a/primitives/traits/src/lib.rs b/primitives/traits/src/lib.rs
index f7bbd8f48..9894616de 100644
--- a/primitives/traits/src/lib.rs
+++ b/primitives/traits/src/lib.rs
@@ -37,12 +37,13 @@ use {
pallet_prelude::{Decode, DispatchResultWithPostInfo, Encode, Get, MaxEncodedLen, Weight},
BoundedVec,
},
+ scale_info::TypeInfo,
serde::{Deserialize, Serialize},
sp_core::H256,
sp_runtime::{
app_crypto::sp_core,
traits::{CheckedAdd, CheckedMul},
- ArithmeticError, DispatchResult, Perbill,
+ ArithmeticError, DispatchResult, Perbill, RuntimeDebug,
},
sp_std::{collections::btree_set::BTreeSet, vec::Vec},
};
@@ -450,3 +451,53 @@ impl MaybeSelfChainBlockAuthor for () {
None
}
}
+
+/// Information regarding the active era (era in used in session).
+#[derive(Encode, Decode, RuntimeDebug, TypeInfo, MaxEncodedLen)]
+pub struct ActiveEraInfo {
+ /// Index of era.
+ pub index: EraIndex,
+ /// Moment of start expressed as millisecond from `$UNIX_EPOCH`.
+ ///
+ /// Start can be none if start hasn't been set for the era yet,
+ /// Start is set on the first on_finalize of the era to guarantee usage of `Time`.
+ pub start: Option,
+}
+
+/// Counter for the number of eras that have passed.
+pub type EraIndex = u32;
+
+pub trait EraIndexProvider {
+ fn active_era() -> ActiveEraInfo;
+ fn era_to_session_start(era_index: EraIndex) -> Option;
+}
+
+pub trait ValidatorProvider {
+ fn validators() -> Vec;
+}
+
+pub trait InvulnerablesProvider {
+ fn invulnerables() -> Vec;
+}
+
+pub trait OnEraStart {
+ fn on_era_start(_era_index: EraIndex, _session_start: u32) {}
+}
+
+#[impl_trait_for_tuples::impl_for_tuples(5)]
+impl OnEraStart for Tuple {
+ fn on_era_start(era_index: EraIndex, session_start: u32) {
+ for_tuples!( #( Tuple::on_era_start(era_index, session_start); )* );
+ }
+}
+
+pub trait OnEraEnd {
+ fn on_era_end(_era_index: EraIndex) {}
+}
+
+#[impl_trait_for_tuples::impl_for_tuples(5)]
+impl OnEraEnd for Tuple {
+ fn on_era_end(era_index: EraIndex) {
+ for_tuples!( #( Tuple::on_era_end(era_index); )* );
+ }
+}
diff --git a/runtime/common/Cargo.toml b/runtime/common/Cargo.toml
index dbf2b01d6..792cbdcfc 100644
--- a/runtime/common/Cargo.toml
+++ b/runtime/common/Cargo.toml
@@ -21,11 +21,13 @@ scale-info = { workspace = true, features = [ "derive" ] }
# Own
pallet-configuration = { workspace = true }
pallet-data-preservers = { workspace = true }
+pallet-external-validators = { workspace = true }
pallet-foreign-asset-creator = { workspace = true }
pallet-invulnerables = { workspace = true }
pallet-pooled-staking = { workspace = true }
pallet-registrar = { workspace = true }
pallet-services-payment = { workspace = true }
+pallet-session = { workspace = true }
pallet-treasury = { workspace = true }
# Moonkit
@@ -68,12 +70,14 @@ std = [
"pallet-beefy-mmr/std",
"pallet-configuration/std",
"pallet-data-preservers/std",
+ "pallet-external-validators/std",
"pallet-foreign-asset-creator/std",
"pallet-invulnerables/std",
"pallet-migrations/std",
"pallet-pooled-staking/std",
"pallet-registrar/std",
"pallet-services-payment/std",
+ "pallet-session/std",
"pallet-treasury/std",
"pallet-xcm/std",
"parity-scale-codec/std",
@@ -92,6 +96,7 @@ runtime-benchmarks = [
"pallet-balances/runtime-benchmarks",
"pallet-configuration/runtime-benchmarks",
"pallet-data-preservers/runtime-benchmarks",
+ "pallet-external-validators/runtime-benchmarks",
"pallet-foreign-asset-creator/runtime-benchmarks",
"pallet-invulnerables/runtime-benchmarks",
"pallet-migrations/runtime-benchmarks",
@@ -112,12 +117,14 @@ try-runtime = [
"pallet-beefy-mmr/try-runtime",
"pallet-configuration/try-runtime",
"pallet-data-preservers/try-runtime",
+ "pallet-external-validators/try-runtime",
"pallet-foreign-asset-creator/try-runtime",
"pallet-invulnerables/try-runtime",
"pallet-migrations/try-runtime",
"pallet-pooled-staking/try-runtime",
"pallet-registrar/try-runtime",
"pallet-services-payment/try-runtime",
+ "pallet-session/try-runtime",
"pallet-treasury/try-runtime",
"pallet-xcm/try-runtime",
"sp-runtime/try-runtime",
diff --git a/runtime/common/src/migrations.rs b/runtime/common/src/migrations.rs
index 90b9a9934..1eb5138b1 100644
--- a/runtime/common/src/migrations.rs
+++ b/runtime/common/src/migrations.rs
@@ -897,6 +897,51 @@ where
}
}
+pub struct ExternalValidatorsInitialMigration(pub PhantomData);
+
+impl Migration for ExternalValidatorsInitialMigration
+where
+ Runtime: pallet_external_validators::Config,
+ Runtime: pallet_session::Config<
+ ValidatorId = ::ValidatorId,
+ >,
+{
+ fn friendly_name(&self) -> &str {
+ "TM_ExternalValidatorsInitialMigration"
+ }
+
+ fn migrate(&self, _available_weight: Weight) -> Weight {
+ use frame_support::pallet_prelude::*;
+
+ // Set initial WhitelistedValidators to current validators from pallet session
+ let session_keys = pallet_session::QueuedKeys::::get();
+ let session_validators = BoundedVec::truncate_from(
+ session_keys
+ .into_iter()
+ .map(|(validator, _keys)| validator)
+ .collect(),
+ );
+ pallet_external_validators::WhitelistedValidators::::put(session_validators);
+
+ // Kill storage of ValidatorManager pallet
+ let pallet_prefix: &[u8] = b"ValidatorManager";
+ let _ = clear_storage_prefix(pallet_prefix, b"", b"", None, None);
+
+ // One db read and one db write per element, plus the on-chain storage
+ Runtime::DbWeight::get().reads_writes(1, 1)
+ }
+
+ #[cfg(feature = "try-runtime")]
+ fn pre_upgrade(&self) -> Result, sp_runtime::DispatchError> {
+ Ok(vec![])
+ }
+
+ #[cfg(feature = "try-runtime")]
+ fn post_upgrade(&self, _state: Vec) -> Result<(), sp_runtime::DispatchError> {
+ Ok(())
+ }
+}
+
pub struct FlashboxMigrations(PhantomData);
impl GetMigrations for FlashboxMigrations
@@ -1070,9 +1115,19 @@ pub struct DancelightMigrations(PhantomData);
impl GetMigrations for DancelightMigrations
where
Runtime: frame_system::Config,
+ Runtime: pallet_external_validators::Config,
+ Runtime: pallet_session::Config<
+ ValidatorId = ::ValidatorId,
+ >,
{
fn get_migrations() -> Vec> {
let migrate_mmr_leaf_pallet = MigrateMMRLeafPallet::(Default::default());
- vec![Box::new(migrate_mmr_leaf_pallet)]
+ let migrate_external_validators =
+ ExternalValidatorsInitialMigration::(Default::default());
+
+ vec![
+ Box::new(migrate_mmr_leaf_pallet),
+ Box::new(migrate_external_validators),
+ ]
}
}
diff --git a/runtime/dancebox/src/lib.rs b/runtime/dancebox/src/lib.rs
index a275c237c..6f5eb2ed6 100644
--- a/runtime/dancebox/src/lib.rs
+++ b/runtime/dancebox/src/lib.rs
@@ -97,8 +97,8 @@ use {
sp_runtime::{
create_runtime_str, generic, impl_opaque_keys,
traits::{
- AccountIdConversion, AccountIdLookup, BlakeTwo256, Block as BlockT, Hash as HashT,
- IdentityLookup, Verify,
+ AccountIdConversion, AccountIdLookup, BlakeTwo256, Block as BlockT, ConvertInto,
+ Hash as HashT, IdentityLookup, Verify,
},
transaction_validity::{TransactionSource, TransactionValidity},
AccountId32, ApplyExtrinsicResult,
@@ -726,7 +726,7 @@ impl pallet_session::Config for Runtime {
type RuntimeEvent = RuntimeEvent;
type ValidatorId = CollatorId;
// we don't have stash and controller, thus we don't need the convert as well.
- type ValidatorIdOf = pallet_invulnerables::IdentityCollator;
+ type ValidatorIdOf = ConvertInto;
type ShouldEndSession = pallet_session::PeriodicSessions;
type NextSessionRotation = pallet_session::PeriodicSessions;
type SessionManager = CollatorsFromInvulnerablesAndThenFromStaking;
@@ -1123,7 +1123,7 @@ impl pallet_invulnerables::Config for Runtime {
type UpdateOrigin = EnsureRoot;
type MaxInvulnerables = MaxInvulnerables;
type CollatorId = ::AccountId;
- type CollatorIdOf = pallet_invulnerables::IdentityCollator;
+ type CollatorIdOf = ConvertInto;
type CollatorRegistration = Session;
type WeightInfo = weights::pallet_invulnerables::SubstrateWeight;
#[cfg(feature = "runtime-benchmarks")]
diff --git a/runtime/flashbox/src/lib.rs b/runtime/flashbox/src/lib.rs
index 1963ca6fb..0d8b4ac60 100644
--- a/runtime/flashbox/src/lib.rs
+++ b/runtime/flashbox/src/lib.rs
@@ -88,8 +88,8 @@ use {
sp_runtime::{
create_runtime_str, generic, impl_opaque_keys,
traits::{
- AccountIdConversion, AccountIdLookup, BlakeTwo256, Block as BlockT, IdentityLookup,
- Verify,
+ AccountIdConversion, AccountIdLookup, BlakeTwo256, Block as BlockT, ConvertInto,
+ IdentityLookup, Verify,
},
transaction_validity::{TransactionSource, TransactionValidity},
AccountId32, ApplyExtrinsicResult,
@@ -608,7 +608,7 @@ impl pallet_session::Config for Runtime {
type RuntimeEvent = RuntimeEvent;
type ValidatorId = ::AccountId;
// we don't have stash and controller, thus we don't need the convert as well.
- type ValidatorIdOf = pallet_invulnerables::IdentityCollator;
+ type ValidatorIdOf = ConvertInto;
type ShouldEndSession = pallet_session::PeriodicSessions;
type NextSessionRotation = pallet_session::PeriodicSessions;
type SessionManager = CollatorsFromInvulnerables;
@@ -971,7 +971,7 @@ impl pallet_invulnerables::Config for Runtime {
type UpdateOrigin = EnsureRoot;
type MaxInvulnerables = MaxInvulnerables;
type CollatorId = CollatorId;
- type CollatorIdOf = pallet_invulnerables::IdentityCollator;
+ type CollatorIdOf = ConvertInto;
type CollatorRegistration = Session;
type WeightInfo = weights::pallet_invulnerables::SubstrateWeight;
#[cfg(feature = "runtime-benchmarks")]
diff --git a/solo-chains/runtime/dancelight/Cargo.toml b/solo-chains/runtime/dancelight/Cargo.toml
index c6d094c71..56da752ae 100644
--- a/solo-chains/runtime/dancelight/Cargo.toml
+++ b/solo-chains/runtime/dancelight/Cargo.toml
@@ -71,6 +71,7 @@ pallet-collective = { workspace = true }
pallet-conviction-voting = { workspace = true }
pallet-democracy = { workspace = true }
pallet-elections-phragmen = { workspace = true }
+pallet-external-validators = { workspace = true }
pallet-grandpa = { workspace = true }
pallet-identity = { workspace = true }
pallet-initializer = { workspace = true }
@@ -207,6 +208,7 @@ std = [
"pallet-data-preservers/std",
"pallet-democracy/std",
"pallet-elections-phragmen/std",
+ "pallet-external-validators/std",
"pallet-grandpa/std",
"pallet-identity/std",
"pallet-inflation-rewards/std",
@@ -307,6 +309,7 @@ runtime-benchmarks = [
"pallet-data-preservers/runtime-benchmarks",
"pallet-democracy/runtime-benchmarks",
"pallet-elections-phragmen/runtime-benchmarks",
+ "pallet-external-validators/runtime-benchmarks",
"pallet-grandpa/runtime-benchmarks",
"pallet-identity/runtime-benchmarks",
"pallet-inflation-rewards/runtime-benchmarks",
@@ -373,6 +376,7 @@ try-runtime = [
"pallet-data-preservers/try-runtime",
"pallet-democracy/try-runtime",
"pallet-elections-phragmen/try-runtime",
+ "pallet-external-validators/try-runtime",
"pallet-grandpa/try-runtime",
"pallet-identity/try-runtime",
"pallet-inflation-rewards/try-runtime",
diff --git a/solo-chains/runtime/dancelight/src/genesis_config_presets.rs b/solo-chains/runtime/dancelight/src/genesis_config_presets.rs
index cb2da717c..7134acdcc 100644
--- a/solo-chains/runtime/dancelight/src/genesis_config_presets.rs
+++ b/solo-chains/runtime/dancelight/src/genesis_config_presets.rs
@@ -409,7 +409,16 @@ fn dancelight_testnet_genesis(
"collatorConfiguration": crate::CollatorConfigurationConfig {
config: host_configuration,
..Default::default()
- }
+ },
+ "externalValidators": crate::ExternalValidatorsConfig {
+ skip_external_validators: false,
+ whitelisted_validators: initial_authorities
+ .iter()
+ .map(|x| {
+ x.stash.clone()
+ })
+ .collect::>(),
+ },
})
}
diff --git a/solo-chains/runtime/dancelight/src/lib.rs b/solo-chains/runtime/dancelight/src/lib.rs
index 788cf9418..e6813e680 100644
--- a/solo-chains/runtime/dancelight/src/lib.rs
+++ b/solo-chains/runtime/dancelight/src/lib.rs
@@ -79,7 +79,10 @@ use {
serde::{Deserialize, Serialize},
sp_core::{storage::well_known_keys as StorageWellKnownKeys, Get},
sp_genesis_builder::PresetId,
- sp_runtime::{traits::BlockNumberProvider, AccountId32},
+ sp_runtime::{
+ traits::{BlockNumberProvider, ConvertInto},
+ AccountId32,
+ },
sp_std::{
cmp::Ordering,
collections::{btree_map::BTreeMap, btree_set::BTreeSet, vec_deque::VecDeque},
@@ -162,7 +165,6 @@ use {
mod tests;
pub mod genesis_config_presets;
-mod validator_manager;
impl_runtime_weights!(dancelight_runtime_constants);
@@ -469,7 +471,7 @@ impl pallet_session::Config for Runtime {
type ValidatorIdOf = ValidatorIdOf;
type ShouldEndSession = Babe;
type NextSessionRotation = Babe;
- type SessionManager = pallet_session::historical::NoteHistoricalRoot;
+ type SessionManager = pallet_session::historical::NoteHistoricalRoot;
type SessionHandler = ::KeyTypeIdProviders;
type Keys = SessionKeys;
type WeightInfo = ();
@@ -488,7 +490,6 @@ impl pallet_session::historical::Config for Runtime {
}
parameter_types! {
- pub const SessionsPerEra: SessionIndex = 6;
pub const BondingDuration: sp_staking::EraIndex = 28;
}
@@ -1210,14 +1211,25 @@ impl pallet_beefy_mmr::Config for Runtime {
impl paras_sudo_wrapper::Config for Runtime {}
parameter_types! {
- pub const PermanentSlotLeasePeriodLength: u32 = 365;
- pub const TemporarySlotLeasePeriodLength: u32 = 5;
- pub const MaxTemporarySlotPerLeasePeriod: u32 = 5;
+ pub const SessionsPerEra: SessionIndex = runtime_common::prod_or_fast!(6, 3);
}
-impl validator_manager::Config for Runtime {
+impl pallet_external_validators::Config for Runtime {
type RuntimeEvent = RuntimeEvent;
- type PrivilegedOrigin = EnsureRoot;
+ type UpdateOrigin = EnsureRoot;
+ type HistoryDepth = ConstU32<84>;
+ type MaxWhitelistedValidators = MaxWhitelistedValidators;
+ type MaxExternalValidators = MaxExternalValidators;
+ type ValidatorId = AccountId;
+ type ValidatorIdOf = ValidatorIdOf;
+ type ValidatorRegistration = Session;
+ type UnixTime = Timestamp;
+ type SessionsPerEra = SessionsPerEra;
+ type OnEraStart = ();
+ type OnEraEnd = ();
+ type WeightInfo = weights::pallet_external_validators::SubstrateWeight