......@@ -19,7 +19,7 @@
#![cfg(test)]
use crate::{self as pallet_balances, AccountData, Config, CreditOf, Error, Pallet};
use crate::{self as pallet_balances, AccountData, Config, CreditOf, Error, Pallet, TotalIssuance};
use codec::{Decode, Encode, MaxEncodedLen};
use frame_support::{
assert_err, assert_noop, assert_ok, assert_storage_noop, derive_impl,
......@@ -47,6 +47,7 @@ mod currency_tests;
mod dispatchable_tests;
mod fungible_conformance_tests;
mod fungible_tests;
mod general_tests;
mod reentrancy_tests;
type Block = frame_system::mocking::MockBlock<Test>;
......@@ -278,6 +279,23 @@ pub fn info_from_weight(w: Weight) -> DispatchInfo {
DispatchInfo { weight: w, ..Default::default() }
}
/// Check that the total-issuance matches the sum of all accounts' total balances.
pub fn ensure_ti_valid() {
let mut sum = 0;
for acc in frame_system::Account::<Test>::iter_keys() {
if UseSystem::get() {
let data = frame_system::Pallet::<Test>::account(acc);
sum += data.data.total();
} else {
let data = crate::Account::<Test>::get(acc);
sum += data.total();
}
}
assert_eq!(TotalIssuance::<Test>::get(), sum, "Total Issuance wrong");
}
#[test]
fn weights_sane() {
let info = crate::Call::<Test>::transfer_allow_death { dest: 10, value: 4 }.get_dispatch_info();
......
......@@ -111,7 +111,7 @@ pub struct AccountData<Balance> {
const IS_NEW_LOGIC: u128 = 0x80000000_00000000_00000000_00000000u128;
#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, MaxEncodedLen, TypeInfo)]
pub struct ExtraFlags(u128);
pub struct ExtraFlags(pub(crate) u128);
impl Default for ExtraFlags {
fn default() -> Self {
Self(IS_NEW_LOGIC)
......
......@@ -158,7 +158,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();
}
......@@ -188,7 +187,6 @@ impl pallet_staking::Config for Test {
type UnixTime = pallet_timestamp::Pallet<Test>;
type EraPayout = pallet_staking::ConvertCurve<RewardCurve>;
type MaxExposurePageSize = ConstU32<64>;
type OffendingValidatorsThreshold = OffendingValidatorsThreshold;
type NextNewSession = Session;
type ElectionProvider = onchain::OnChainExecution<OnChainSeqPhragmen>;
type GenesisElectionProvider = Self::ElectionProvider;
......@@ -201,6 +199,7 @@ impl pallet_staking::Config for Test {
type EventListeners = ();
type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig;
type WeightInfo = ();
type DisablingStrategy = pallet_staking::UpToLimitDisablingStrategy;
}
impl pallet_offences::Config for Test {
......
......@@ -189,11 +189,15 @@ mod benches {
let config = new_config_record::<T>();
Configuration::<T>::put(config.clone());
let mut extra_cores = n;
// Assume Reservations to be filled for worst case
setup_reservations::<T>(T::MaxReservedCores::get());
setup_reservations::<T>(extra_cores.min(T::MaxReservedCores::get()));
extra_cores = extra_cores.saturating_sub(T::MaxReservedCores::get());
// Assume Leases to be filled for worst case
setup_leases::<T>(T::MaxLeasedCores::get(), 1, 10);
setup_leases::<T>(extra_cores.min(T::MaxLeasedCores::get()), 1, 10);
extra_cores = extra_cores.saturating_sub(T::MaxLeasedCores::get());
let latest_region_begin = Broker::<T>::latest_timeslice_ready_to_commit(&config);
......@@ -203,7 +207,7 @@ mod benches {
T::AdminOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?;
#[extrinsic_call]
_(origin as T::RuntimeOrigin, initial_price, n.try_into().unwrap());
_(origin as T::RuntimeOrigin, initial_price, extra_cores.try_into().unwrap());
assert!(SaleInfo::<T>::get().is_some());
assert_last_event::<T>(
......
......@@ -70,8 +70,16 @@ impl<T: Config> Pallet<T> {
Ok(())
}
pub(crate) fn do_start_sales(price: BalanceOf<T>, core_count: CoreIndex) -> DispatchResult {
pub(crate) fn do_start_sales(price: BalanceOf<T>, extra_cores: CoreIndex) -> DispatchResult {
let config = Configuration::<T>::get().ok_or(Error::<T>::Uninitialized)?;
// Determine the core count
let core_count = Leases::<T>::decode_len().unwrap_or(0) as CoreIndex +
Reservations::<T>::decode_len().unwrap_or(0) as CoreIndex +
extra_cores;
Self::do_request_core_count(core_count)?;
let commit_timeslice = Self::latest_timeslice_ready_to_commit(&config);
let status = StatusRecord {
core_count,
......
......@@ -559,27 +559,22 @@ pub mod pallet {
///
/// - `origin`: Must be Root or pass `AdminOrigin`.
/// - `initial_price`: The price of Bulk Coretime in the first sale.
/// - `total_core_count`: This is the total number of cores the relay chain should have
/// after the sale concludes.
/// - `extra_cores`: Number of extra cores that should be requested on top of the cores
/// required for `Reservations` and `Leases`.
///
/// NOTE: This function does not actually request that new core count from the relay chain.
/// You need to make sure to call `request_core_count` afterwards to bring the relay chain
/// in sync.
///
/// When to call the function depends on the new core count. If it is larger than what it
/// was before, you can call it immediately or even before `start_sales` as non allocated
/// cores will just be `Idle`. If you are actually reducing the number of cores, you should
/// call `request_core_count`, right before the next sale, to avoid shutting down tasks too
/// early.
/// This will call [`Self::request_core_count`] internally to set the correct core count on
/// the relay chain.
#[pallet::call_index(4)]
#[pallet::weight(T::WeightInfo::start_sales((*total_core_count).into()))]
#[pallet::weight(T::WeightInfo::start_sales(
T::MaxLeasedCores::get() + T::MaxReservedCores::get() + *extra_cores as u32
))]
pub fn start_sales(
origin: OriginFor<T>,
initial_price: BalanceOf<T>,
total_core_count: CoreIndex,
extra_cores: CoreIndex,
) -> DispatchResultWithPostInfo {
T::AdminOrigin::ensure_origin_or_root(origin)?;
Self::do_start_sales(initial_price, total_core_count)?;
Self::do_start_sales(initial_price, extra_cores)?;
Ok(Pays::No.into())
}
......
......@@ -329,7 +329,7 @@ fn nft_metadata_works() {
fn migration_works() {
TestExt::new().endow(1, 1000).execute_with(|| {
assert_ok!(Broker::do_set_lease(1000, 8));
assert_ok!(Broker::do_start_sales(100, 2));
assert_ok!(Broker::do_start_sales(100, 1));
// Sale is for regions from TS4..7
// Not ending in this sale period.
......@@ -385,7 +385,7 @@ fn instapool_payouts_work() {
TestExt::new().endow(1, 1000).execute_with(|| {
let item = ScheduleItem { assignment: Pool, mask: CoreMask::complete() };
assert_ok!(Broker::do_reserve(Schedule::truncate_from(vec![item])));
assert_ok!(Broker::do_start_sales(100, 3));
assert_ok!(Broker::do_start_sales(100, 2));
advance_to(2);
let region = Broker::do_purchase(1, u64::max_value()).unwrap();
assert_ok!(Broker::do_pool(region, None, 2, Final));
......@@ -411,7 +411,7 @@ fn instapool_partial_core_payouts_work() {
TestExt::new().endow(1, 1000).execute_with(|| {
let item = ScheduleItem { assignment: Pool, mask: CoreMask::complete() };
assert_ok!(Broker::do_reserve(Schedule::truncate_from(vec![item])));
assert_ok!(Broker::do_start_sales(100, 2));
assert_ok!(Broker::do_start_sales(100, 1));
advance_to(2);
let region = Broker::do_purchase(1, u64::max_value()).unwrap();
let (region1, region2) =
......@@ -477,7 +477,7 @@ fn initialize_with_system_paras_works() {
ScheduleItem { assignment: Task(4u32), mask: 0x00000_00000_00000_fffff.into() },
];
assert_ok!(Broker::do_reserve(Schedule::truncate_from(items)));
assert_ok!(Broker::do_start_sales(100, 2));
assert_ok!(Broker::do_start_sales(100, 0));
advance_to(10);
assert_eq!(
CoretimeTrace::get(),
......@@ -510,7 +510,7 @@ fn initialize_with_leased_slots_works() {
TestExt::new().execute_with(|| {
assert_ok!(Broker::do_set_lease(1000, 6));
assert_ok!(Broker::do_set_lease(1001, 7));
assert_ok!(Broker::do_start_sales(100, 2));
assert_ok!(Broker::do_start_sales(100, 0));
advance_to(18);
let end_hint = None;
assert_eq!(
......@@ -925,7 +925,7 @@ fn leases_can_be_renewed() {
assert_ok!(Broker::do_set_lease(2001, 9));
assert_eq!(Leases::<Test>::get().len(), 1);
// Start the sales with only one core for this lease.
assert_ok!(Broker::do_start_sales(100, 1));
assert_ok!(Broker::do_start_sales(100, 0));
// Advance to sale period 1, we should get an AllowedRenewal for task 2001 for the next
// sale.
......@@ -1018,7 +1018,7 @@ fn short_leases_cannot_be_renewed() {
assert_ok!(Broker::do_set_lease(2001, 3));
assert_eq!(Leases::<Test>::get().len(), 1);
// Start the sales with one core for this lease.
assert_ok!(Broker::do_start_sales(100, 1));
assert_ok!(Broker::do_start_sales(100, 0));
// The lease is removed.
assert_eq!(Leases::<Test>::get().len(), 0);
......@@ -1290,7 +1290,7 @@ fn renewal_works_leases_ended_before_start_sales() {
));
// This intializes the first sale and the period 0.
assert_ok!(Broker::do_start_sales(100, 2));
assert_ok!(Broker::do_start_sales(100, 0));
assert_noop!(Broker::do_renew(1, 1), Error::<Test>::Unavailable);
assert_noop!(Broker::do_renew(1, 0), Error::<Test>::Unavailable);
......@@ -1408,3 +1408,23 @@ fn renewal_works_leases_ended_before_start_sales() {
);
});
}
#[test]
fn start_sales_sets_correct_core_count() {
TestExt::new().endow(1, 1000).execute_with(|| {
advance_to(1);
Broker::do_set_lease(1, 100).unwrap();
Broker::do_set_lease(2, 100).unwrap();
Broker::do_set_lease(3, 100).unwrap();
Broker::do_reserve(Schedule::truncate_from(vec![ScheduleItem {
assignment: Pool,
mask: CoreMask::complete(),
}]))
.unwrap();
Broker::do_start_sales(5, 5).unwrap();
System::assert_has_event(Event::<Test>::CoreCountRequested { core_count: 9 }.into());
})
}
......@@ -144,7 +144,7 @@ parameter_types! {
pub const KsmLocation: Location = Location::parent();
pub const TokenLocation: Location = Here.into_location();
pub const RelayNetwork: NetworkId = ByGenesis([0; 32]);
pub UniversalLocation: InteriorLocation = Parachain(MsgQueue::parachain_id().into()).into();
pub UniversalLocation: InteriorLocation = [GlobalConsensus(RelayNetwork::get()), Parachain(MsgQueue::parachain_id().into())].into();
}
pub type XcmOriginToCallOrigin = (
......
......@@ -14,9 +14,8 @@
// You should have received a copy of the GNU General Public License
// along with Polkadot. If not, see <http://www.gnu.org/licenses/>.
use super::{Balances, Runtime, RuntimeCall, RuntimeEvent};
use crate::parachain::RuntimeHoldReason;
use frame_support::{derive_impl, parameter_types};
use super::{Balances, Runtime, RuntimeCall, RuntimeEvent, RuntimeHoldReason};
use frame_support::{derive_impl, parameter_types, traits::Contains};
parameter_types! {
pub Schedule: pallet_contracts::Schedule<Runtime> = Default::default();
......@@ -29,5 +28,14 @@ impl pallet_contracts::Config for Runtime {
type Currency = Balances;
type Schedule = Schedule;
type Time = super::Timestamp;
type CallFilter = CallFilter;
type Xcm = pallet_xcm::Pallet<Self>;
}
/// In this mock, we only allow other contract calls via XCM.
pub struct CallFilter;
impl Contains<RuntimeCall> for CallFilter {
fn contains(call: &RuntimeCall) -> bool {
matches!(call, RuntimeCall::Contracts(pallet_contracts::Call::call { .. }))
}
}
......@@ -107,7 +107,7 @@ impl configuration::Config for Runtime {
parameter_types! {
pub RelayNetwork: NetworkId = ByGenesis([0; 32]);
pub const TokenLocation: Location = Here.into_location();
pub UniversalLocation: InteriorLocation = Here;
pub UniversalLocation: InteriorLocation = RelayNetwork::get().into();
pub UnitWeightCost: u64 = 1_000;
}
......
......@@ -22,7 +22,10 @@ use crate::{
relay_chain, MockNet, ParaA, ParachainBalances, Relay, ALICE, BOB, INITIAL_BALANCE,
};
use codec::{Decode, Encode};
use frame_support::traits::{fungibles::Mutate, Currency};
use frame_support::{
assert_err,
traits::{fungibles::Mutate, Currency},
};
use pallet_contracts::{test_utils::builder::*, Code};
use pallet_contracts_fixtures::compile_module;
use pallet_contracts_uapi::ReturnErrorCode;
......@@ -81,7 +84,7 @@ fn test_xcm_execute() {
.build();
let result = bare_call(contract_addr.clone())
.data(VersionedXcm::V4(message).encode().encode())
.data(VersionedXcm::V4(message).encode())
.build();
assert_eq!(result.gas_consumed, result.gas_required);
......@@ -118,7 +121,7 @@ fn test_xcm_execute_incomplete() {
.build();
let result = bare_call(contract_addr.clone())
.data(VersionedXcm::V4(message).encode().encode())
.data(VersionedXcm::V4(message).encode())
.build();
assert_eq!(result.gas_consumed, result.gas_required);
......@@ -129,6 +132,26 @@ fn test_xcm_execute_incomplete() {
});
}
#[test]
fn test_xcm_execute_filtered_call() {
MockNet::reset();
let contract_addr = instantiate_test_contract("xcm_execute");
ParaA::execute_with(|| {
// `remark` should be rejected, as it is not allowed by our CallFilter.
let call = parachain::RuntimeCall::System(frame_system::Call::remark { remark: vec![] });
let message: Xcm<parachain::RuntimeCall> = Xcm::builder_unsafe()
.transact(OriginKind::Native, Weight::MAX, call.encode())
.build();
let result = bare_call(contract_addr.clone())
.data(VersionedXcm::V4(message).encode())
.build()
.result;
assert_err!(result, frame_system::Error::<parachain::Runtime>::CallFiltered);
});
}
#[test]
fn test_xcm_execute_reentrant_call() {
MockNet::reset();
......@@ -151,7 +174,7 @@ fn test_xcm_execute_reentrant_call() {
.build();
let result = bare_call(contract_addr.clone())
.data(VersionedXcm::V4(message).encode().encode())
.data(VersionedXcm::V4(message).encode())
.build_and_unwrap_result();
assert_return_code!(&result, ReturnErrorCode::XcmExecutionFailed);
......@@ -182,7 +205,7 @@ fn test_xcm_send() {
.build();
let result = bare_call(contract_addr.clone())
.data((dest, VersionedXcm::V4(message).encode()).encode())
.data((dest, VersionedXcm::V4(message)).encode())
.build_and_unwrap_result();
let mut data = &result.data[..];
......
......@@ -223,14 +223,14 @@ pub struct Environment<T: Config> {
pub struct ApiVersion(u16);
impl Default for ApiVersion {
fn default() -> Self {
Self(2)
Self(3)
}
}
#[test]
fn api_version_is_up_to_date() {
assert_eq!(
109,
111,
crate::wasm::STABLE_API_COUNT,
"Stable API count has changed. Bump the returned value of ApiVersion::default() and update the test."
);
......@@ -307,9 +307,6 @@ pub mod pallet {
/// Therefore please make sure to be restrictive about which dispatchables are allowed
/// in order to not introduce a new DoS vector like memory allocation patterns that can
/// be exploited to drive the runtime into a panic.
///
/// This filter does not apply to XCM transact calls. To impose restrictions on XCM transact
/// calls, you must configure them separately within the XCM pallet itself.
#[pallet::no_default_bounds]
type CallFilter: Contains<<Self as frame_system::Config>::RuntimeCall>;
......
......@@ -25,8 +25,12 @@ use crate::{
};
use codec::{Decode, DecodeLimit, Encode, MaxEncodedLen};
use frame_support::{
dispatch::DispatchInfo, ensure, pallet_prelude::DispatchResultWithPostInfo, parameter_types,
traits::Get, weights::Weight,
dispatch::DispatchInfo,
ensure,
pallet_prelude::{DispatchResult, DispatchResultWithPostInfo},
parameter_types,
traits::Get,
weights::Weight,
};
use pallet_contracts_proc_macro::define_env;
use pallet_contracts_uapi::{CallFlags, ReturnFlags};
......@@ -37,6 +41,9 @@ use sp_runtime::{
};
use sp_std::{fmt, prelude::*};
use wasmi::{core::HostError, errors::LinkerError, Linker, Memory, Store};
use xcm::VersionedXcm;
type CallOf<T> = <T as frame_system::Config>::RuntimeCall;
/// The maximum nesting depth a contract can use when encoding types.
const MAX_DECODE_NESTING: u32 = 256;
......@@ -371,6 +378,29 @@ fn already_charged(_: u32) -> Option<RuntimeCosts> {
None
}
/// Ensure that the XCM program is executable, by checking that it does not contain any [`Transact`]
/// instruction with a call that is not allowed by the CallFilter.
fn ensure_executable<T: Config>(message: &VersionedXcm<CallOf<T>>) -> DispatchResult {
use frame_support::traits::Contains;
use xcm::prelude::{Transact, Xcm};
let mut message: Xcm<CallOf<T>> =
message.clone().try_into().map_err(|_| Error::<T>::XCMDecodeFailed)?;
message.iter_mut().try_for_each(|inst| -> DispatchResult {
let Transact { ref mut call, .. } = inst else { return Ok(()) };
let call = call.ensure_decoded().map_err(|_| Error::<T>::XCMDecodeFailed)?;
if !<T as Config>::CallFilter::contains(call) {
return Err(frame_system::Error::<T>::CallFiltered.into())
}
Ok(())
})?;
Ok(())
}
/// Can only be used for one call.
pub struct Runtime<'a, E: Ext + 'a> {
ext: &'a mut E,
......@@ -2074,7 +2104,6 @@ pub mod env {
/// Execute an XCM program locally, using the contract's address as the origin.
/// See [`pallet_contracts_uapi::HostFn::execute_xcm`].
#[unstable]
fn xcm_execute(
ctx: _,
memory: _,
......@@ -2082,13 +2111,16 @@ pub mod env {
msg_len: u32,
) -> Result<ReturnErrorCode, TrapReason> {
use frame_support::dispatch::DispatchInfo;
use xcm::VersionedXcm;
use xcm_builder::{ExecuteController, ExecuteControllerWeightInfo};
ctx.charge_gas(RuntimeCosts::CopyFromContract(msg_len))?;
let message = ctx.read_sandbox_memory_as_unbounded(memory, msg_ptr, msg_len)?;
let message: VersionedXcm<CallOf<E::T>> =
ctx.read_sandbox_memory_as_unbounded(memory, msg_ptr, msg_len)?;
ensure_executable::<E::T>(&message)?;
let execute_weight =
<<E::T as Config>::Xcm as ExecuteController<_, _>>::WeightInfo::execute_blob();
<<E::T as Config>::Xcm as ExecuteController<_, _>>::WeightInfo::execute();
let weight = ctx.ext.gas_meter().gas_left().max(execute_weight);
let dispatch_info = DispatchInfo { weight, ..Default::default() };
......@@ -2097,9 +2129,9 @@ pub mod env {
RuntimeCosts::CallXcmExecute,
|ctx| {
let origin = crate::RawOrigin::Signed(ctx.ext.address().clone()).into();
let weight_used = <<E::T as Config>::Xcm>::execute_blob(
let weight_used = <<E::T as Config>::Xcm>::execute(
origin,
message,
Box::new(message),
weight.saturating_sub(execute_weight),
)?;
......@@ -2110,7 +2142,6 @@ pub mod env {
/// Send an XCM program from the contract to the specified destination.
/// See [`pallet_contracts_uapi::HostFn::send_xcm`].
#[unstable]
fn xcm_send(
ctx: _,
memory: _,
......@@ -2119,18 +2150,19 @@ pub mod env {
msg_len: u32,
output_ptr: u32,
) -> Result<ReturnErrorCode, TrapReason> {
use xcm::VersionedLocation;
use xcm::{VersionedLocation, VersionedXcm};
use xcm_builder::{SendController, SendControllerWeightInfo};
ctx.charge_gas(RuntimeCosts::CopyFromContract(msg_len))?;
let dest: VersionedLocation = ctx.read_sandbox_memory_as(memory, dest_ptr)?;
let message = ctx.read_sandbox_memory_as_unbounded(memory, msg_ptr, msg_len)?;
let weight = <<E::T as Config>::Xcm as SendController<_>>::WeightInfo::send_blob();
let message: VersionedXcm<()> =
ctx.read_sandbox_memory_as_unbounded(memory, msg_ptr, msg_len)?;
let weight = <<E::T as Config>::Xcm as SendController<_>>::WeightInfo::send();
ctx.charge_gas(RuntimeCosts::CallRuntime(weight))?;
let origin = crate::RawOrigin::Signed(ctx.ext.address().clone()).into();
match <<E::T as Config>::Xcm>::send_blob(origin, dest.into(), message) {
match <<E::T as Config>::Xcm>::send(origin, dest.into(), message.into()) {
Ok(message_id) => {
ctx.write_sandbox_memory(memory, output_ptr, &message_id.encode())?;
Ok(ReturnErrorCode::Success)
......
......@@ -790,7 +790,7 @@ pub trait HostFn {
///
/// # Parameters
///
/// - `dest`: The XCM destination, should be decodable as [MultiLocation](https://paritytech.github.io/polkadot-sdk/master/staging_xcm/enum.VersionedLocation.html),
/// - `dest`: The XCM destination, should be decodable as [VersionedLocation](https://paritytech.github.io/polkadot-sdk/master/staging_xcm/enum.VersionedLocation.html),
/// traps otherwise.
/// - `msg`: The message, should be decodable as a [VersionedXcm](https://paritytech.github.io/polkadot-sdk/master/staging_xcm/enum.VersionedXcm.html),
/// traps otherwise.
......
......@@ -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,48 @@ 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 <EPM as election_provider>::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 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();
let (ext, pool_state, _) = ExtBuilder::default().epm(epm_builder).build_offchainify();
execute_with(ext, || {
log!(
trace,
"current validators (staking): {:?}",
<Runtime as pallet_staking::SessionInterface<AccountId>>::validators()
);
let session_validators_before = Session::validators();
roll_to_epm_off();
assert!(ElectionProviderMultiPhase::current_phase().is_off());
let staking_builder = StakingExtBuilder::default().validator_count(7);
let (mut ext, _, _) = ExtBuilder::default()
.epm(epm_builder)
.staking(staking_builder)
.build_offchainify();
ext.execute_with(|| {
assert_eq!(pallet_staking::ForceEra::<Runtime>::get(), pallet_staking::Forcing::NotForcing);
// slashes so that staking goes into `Forcing::ForceNew`.
slash_through_offending_threshold();
assert_eq!(pallet_staking::ForceEra::<Runtime>::get(), pallet_staking::Forcing::ForceNew);
let active_set_size_before_slash = Session::validators().len();
advance_session_delayed_solution(pool_state.clone());
assert!(ElectionProviderMultiPhase::current_phase().is_emergency());
log_current_time();
// Slash more than 1/3 of the active validators
let mut slashed = slash_half_the_active_set();
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);
let active_set_size_after_slash = Session::validators().len();
// EPM is still in emergency phase.
assert!(ElectionProviderMultiPhase::current_phase().is_emergency());
// active set should stay the same before and after the slash
assert_eq!(active_set_size_before_slash, active_set_size_after_slash);
// 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());
// Slashed validators are disabled up to a limit
slashed.truncate(
pallet_staking::UpToLimitDisablingStrategy::<SLASHING_DISABLING_FACTOR>::disable_limit(
active_set_size_after_slash,
),
);
// 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));
// 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::<Vec<_>>();
assert_eq!(pallet_staking::ForceEra::<Runtime>::get(), pallet_staking::Forcing::NotForcing);
assert_eq!(Session::disabled_validators(), expected_disabled);
});
}
......@@ -253,77 +225,7 @@ fn continuous_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
/// <https://github.com/paritytech/substrate/pull/12420>.
///
/// Related to <https://github.com/paritytech/substrate/issues/13714>.
fn set_validation_intention_after_chilled() {
use frame_election_provider_support::SortedListProvider;
use pallet_staking::{Event, Forcing, Nominators};
let (ext, pool_state, _) = ExtBuilder::default()
.epm(EpmExtBuilder::default())
.staking(StakingExtBuilder::default())
.build_offchainify();
execute_with(ext, || {
assert_eq!(active_era(), 0);
// validator is part of the validator set.
assert!(Session::validators().contains(&41));
assert!(<Runtime as pallet_staking::Config>::VoterList::contains(&41));
// nominate validator 81.
assert_ok!(Staking::nominate(RuntimeOrigin::signed(21), vec![41]));
assert_eq!(Nominators::<Runtime>::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!(!<Runtime as pallet_staking::Config>::VoterList::contains(&41));
assert_eq!(
staking_events(),
[
Event::Chilled { stash: 41 },
Event::ForceEra { mode: Forcing::ForceNew },
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::<Runtime>::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!(<Runtime as pallet_staking::Config>::VoterList::contains(&41));
// nominations are still active as before the slash.
assert_eq!(Nominators::<Runtime>::get(21).unwrap().targets, vec![41]);
})
}
#[test]
/// Active ledger balance may fall below ED if account chills before unbonding.
/// Active ledger balance may fall below ED if account chills before unbounding.
///
/// Unbonding call fails if the remaining ledger's stash balance falls below the existential
/// deposit. However, if the stash is chilled before unbonding, the ledger's active balance may
......
......@@ -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::*;
......@@ -236,7 +236,6 @@ parameter_types! {
pub const SessionsPerEra: sp_staking::SessionIndex = 2;
pub static 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;
}
......@@ -290,6 +289,8 @@ parameter_types! {
/// Upper limit on the number of NPOS nominations.
const MAX_QUOTA_NOMINATIONS: u32 = 16;
/// Disabling factor set explicitly to byzantine threshold
pub(crate) const SLASHING_DISABLING_FACTOR: usize = 3;
impl pallet_staking::Config for Runtime {
type Currency = Balances;
......@@ -308,7 +309,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<OnChainSeqPhragmen>;
type VoterList = BagsList;
......@@ -320,6 +320,7 @@ impl pallet_staking::Config for Runtime {
type EventListeners = Pools;
type WeightInfo = pallet_staking::weights::SubstrateWeight<Runtime>;
type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig;
type DisablingStrategy = pallet_staking::UpToLimitDisablingStrategy<SLASHING_DISABLING_FACTOR>;
}
impl<LocalCall> frame_system::offchain::SendTransactionTypes<LocalCall> for Runtime
......@@ -871,7 +872,6 @@ pub(crate) fn on_offence_now(
offenders,
slash_fraction,
Staking::eras_start_session_index(now).unwrap(),
DisableStrategy::WhenSlashed,
);
}
......@@ -886,19 +886,16 @@ 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 =
<Runtime as pallet_staking::Config>::OffendingValidatorsThreshold::get() *
validators.len() as u32;
// Slashes 1/2 of the active set. Returns the `AccountId`s of the slashed validators.
pub(crate) fn slash_half_the_active_set() -> Vec<AccountId> {
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
......
......@@ -19,6 +19,9 @@
#![cfg_attr(not(feature = "std"), no_std)]
use frame_support::dispatch::DispatchResult;
use frame_system::offchain::SendTransactionTypes;
#[cfg(feature = "experimental")]
use frame_system::offchain::SubmitTransaction;
// Re-export pallet items so that they can be accessed from the crate namespace.
pub use pallet::*;
......@@ -31,10 +34,14 @@ mod benchmarking;
pub mod weights;
pub use weights::*;
#[cfg(feature = "experimental")]
const LOG_TARGET: &str = "pallet-example-tasks";
#[frame_support::pallet(dev_mode)]
pub mod pallet {
use super::*;
use frame_support::pallet_prelude::*;
use frame_system::pallet_prelude::*;
#[pallet::error]
pub enum Error<T> {
......@@ -59,9 +66,36 @@ pub mod pallet {
}
}
#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
#[cfg(feature = "experimental")]
fn offchain_worker(_block_number: BlockNumberFor<T>) {
if let Some(key) = Numbers::<T>::iter_keys().next() {
// Create a valid task
let task = Task::<T>::AddNumberIntoTotal { i: key };
let runtime_task = <T as Config>::RuntimeTask::from(task);
let call = frame_system::Call::<T>::do_task { task: runtime_task.into() };
// Submit the task as an unsigned transaction
let res =
SubmitTransaction::<T, frame_system::Call<T>>::submit_unsigned_transaction(
call.into(),
);
match res {
Ok(_) => log::info!(target: LOG_TARGET, "Submitted the task."),
Err(e) => log::error!(target: LOG_TARGET, "Error submitting task: {:?}", e),
}
}
}
}
#[pallet::config]
pub trait Config: frame_system::Config {
type RuntimeTask: frame_support::traits::Task;
pub trait Config:
SendTransactionTypes<frame_system::Call<Self>> + frame_system::Config
{
type RuntimeTask: frame_support::traits::Task
+ IsType<<Self as frame_system::Config>::RuntimeTask>
+ From<Task<Self>>;
type WeightInfo: WeightInfo;
}
......
......@@ -20,6 +20,7 @@
use crate::{self as tasks_example};
use frame_support::derive_impl;
use sp_runtime::testing::TestXt;
pub type AccountId = u32;
pub type Balance = u32;
......@@ -32,12 +33,32 @@ frame_support::construct_runtime!(
}
);
pub type Extrinsic = TestXt<RuntimeCall, ()>;
#[derive_impl(frame_system::config_preludes::TestDefaultConfig)]
impl frame_system::Config for Runtime {
type Block = Block;
}
impl<LocalCall> frame_system::offchain::SendTransactionTypes<LocalCall> for Runtime
where
RuntimeCall: From<LocalCall>,
{
type OverarchingCall = RuntimeCall;
type Extrinsic = Extrinsic;
}
impl tasks_example::Config for Runtime {
type RuntimeTask = RuntimeTask;
type WeightInfo = ();
}
pub fn advance_to(b: u64) {
#[cfg(feature = "experimental")]
use frame_support::traits::Hooks;
while System::block_number() < b {
System::set_block_number(System::block_number() + 1);
#[cfg(feature = "experimental")]
TasksExample::offchain_worker(System::block_number());
}
}
......@@ -19,7 +19,11 @@
#![cfg(test)]
use crate::{mock::*, Numbers};
#[cfg(feature = "experimental")]
use codec::Decode;
use frame_support::traits::Task;
#[cfg(feature = "experimental")]
use sp_core::offchain::{testing, OffchainWorkerExt, TransactionPoolExt};
use sp_runtime::BuildStorage;
#[cfg(feature = "experimental")]
......@@ -130,3 +134,29 @@ fn task_execution_fails_for_invalid_task() {
);
});
}
#[cfg(feature = "experimental")]
#[test]
fn task_with_offchain_worker() {
let (offchain, _offchain_state) = testing::TestOffchainExt::new();
let (pool, pool_state) = testing::TestTransactionPoolExt::new();
let mut t = sp_io::TestExternalities::default();
t.register_extension(OffchainWorkerExt::new(offchain));
t.register_extension(TransactionPoolExt::new(pool));
t.execute_with(|| {
advance_to(1);
assert!(pool_state.read().transactions.is_empty());
Numbers::<Runtime>::insert(0, 10);
assert_eq!(crate::Total::<Runtime>::get(), (0, 0));
advance_to(2);
let tx = pool_state.write().transactions.pop().unwrap();
assert!(pool_state.read().transactions.is_empty());
let tx = Extrinsic::decode(&mut &*tx).unwrap();
assert_eq!(tx.signature, None);
});
}
......@@ -134,7 +134,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<Self>;
......@@ -145,6 +144,7 @@ impl pallet_staking::Config for Runtime {
type EventListeners = ();
type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig;
type WeightInfo = ();
type DisablingStrategy = pallet_staking::UpToLimitDisablingStrategy;
}
pub struct BalanceToU256;
......