Decouple Staking and Election - Part 2 Unsigned Phase (#7909)

* Base features and traits.

* pallet and unsigned phase

* Undo bad formattings.

* some formatting cleanup.

* Small self-cleanup.

* Make it all build

* self-review

* Some doc tests.

* Some changes from other PR

* Fix session test

* Update Cargo.lock

* Update frame/election-provider-multi-phase/src/lib.rs

Co-authored-by: Guillaume Thiolliere <gui.thiolliere@gmail.com>

* Some review comments

* Rename + make encode/decode

* Do an assert as well, just in case.

* Fix build

* Update frame/election-provider-multi-phase/src/unsigned.rs

Co-authored-by: Guillaume Thiolliere <gui.thiolliere@gmail.com>

* Las comment

* fix staking fuzzer.

* cargo run --release --features=runtime-benchmarks --manifest-path=bin/node/cli/Cargo.toml -- benchmark --chain=dev --steps=50 --repeat=20 --pallet=pallet_election_provider_multi_phase --extrinsic=* --execution=wasm --wasm-execution=compiled --heap-pages=4096 --output=./frame/election-provider-multi-phase/src/weights.rs --template=./.maintain/frame-weight-template.hbs

* Add one last layer of feasibility check as well.

* Last fixes to benchmarks

* Some more docs.

* cargo run --release --features=runtime-benchmarks --manifest-path=bin/node/cli/Cargo.toml -- benchmark --chain=dev --steps=50 --repeat=20 --pallet=pallet_election_provider_multi_phase --extrinsic=* --execution=wasm --wasm-execution=compiled --heap-pages=4096 --output=./frame/election-provider-multi-phase/src/weights.rs --template=./.maintain/frame-weight-template.hbs

* cargo run --release --features=runtime-benchmarks --manifest-path=bin/node/cli/Cargo.toml -- benchmark --chain=dev --steps=50 --repeat=20 --pallet=pallet_election_provider_multi_phase --extrinsic=* --execution=wasm --wasm-execution=compiled --heap-pages=4096 --output=./frame/election-provider-multi-phase/src/weights.rs --template=./.maintain/frame-weight-template.hbs

* Some nits

* cargo run --release --features=runtime-benchmarks --manifest-path=bin/node/cli/Cargo.toml -- benchmark --chain=dev --steps=50 --repeat=20 --pallet=pallet_staking --extrinsic=* --execution=wasm --wasm-execution=compiled --heap-pages=4096 --output=./frame/staking/src/weights.rs --template=./.maintain/frame-weight-template.hbs

* Fix doc

* Mkae ci green

Co-authored-by: Shawn Tabrizi <shawntabrizi@gmail.com>
Co-authored-by: Guillaume Thiolliere <gui.thiolliere@gmail.com>
Co-authored-by: Parity Benchmarking Bot <admin@parity.io>
This commit is contained in:
Kian Paimani
2021-02-23 14:46:17 +00:00
committed by GitHub
parent ba659f9440
commit 7205eea40d
34 changed files with 4092 additions and 231 deletions
+33
View File
@@ -4111,6 +4111,7 @@ dependencies = [
"pallet-contracts-primitives",
"pallet-contracts-rpc-runtime-api",
"pallet-democracy",
"pallet-election-provider-multi-phase",
"pallet-elections-phragmen",
"pallet-grandpa",
"pallet-identity",
@@ -4536,6 +4537,7 @@ dependencies = [
"sp-consensus-babe",
"sp-consensus-vrf",
"sp-core",
"sp-election-providers",
"sp-io",
"sp-runtime",
"sp-session",
@@ -4695,6 +4697,32 @@ dependencies = [
"substrate-test-utils",
]
[[package]]
name = "pallet-election-provider-multi-phase"
version = "3.0.0"
dependencies = [
"frame-benchmarking",
"frame-support",
"frame-system",
"hex-literal",
"pallet-balances",
"parity-scale-codec",
"parking_lot 0.11.1",
"paste 1.0.4",
"rand 0.7.3",
"serde",
"sp-arithmetic",
"sp-core",
"sp-election-providers",
"sp-io",
"sp-npos-elections",
"sp-runtime",
"sp-std",
"sp-tracing",
"static_assertions",
"substrate-test-utils",
]
[[package]]
name = "pallet-elections"
version = "3.0.0"
@@ -4796,6 +4824,7 @@ dependencies = [
"serde",
"sp-application-crypto",
"sp-core",
"sp-election-providers",
"sp-finality-grandpa",
"sp-io",
"sp-keyring",
@@ -5002,6 +5031,7 @@ dependencies = [
"parity-scale-codec",
"serde",
"sp-core",
"sp-election-providers",
"sp-io",
"sp-runtime",
"sp-staking",
@@ -5124,6 +5154,7 @@ dependencies = [
"rand 0.7.3",
"serde",
"sp-core",
"sp-election-providers",
"sp-io",
"sp-runtime",
"sp-session",
@@ -5165,6 +5196,7 @@ dependencies = [
"serde",
"sp-application-crypto",
"sp-core",
"sp-election-providers",
"sp-io",
"sp-npos-elections",
"sp-runtime",
@@ -5192,6 +5224,7 @@ dependencies = [
"parity-scale-codec",
"serde",
"sp-core",
"sp-election-providers",
"sp-io",
"sp-npos-elections",
"sp-runtime",
+1
View File
@@ -75,6 +75,7 @@ members = [
"frame/democracy",
"frame/try-runtime",
"frame/elections",
"frame/election-provider-multi-phase",
"frame/example",
"frame/example-offchain-worker",
"frame/example-parallel",
+3
View File
@@ -56,6 +56,7 @@ pallet-contracts-primitives = { version = "2.0.0", default-features = false, pat
pallet-contracts-rpc-runtime-api = { version = "0.8.0", default-features = false, path = "../../../frame/contracts/rpc/runtime-api/" }
pallet-democracy = { version = "3.0.0", default-features = false, path = "../../../frame/democracy" }
pallet-elections-phragmen = { version = "3.0.0", default-features = false, path = "../../../frame/elections-phragmen" }
pallet-election-provider-multi-phase = { version = "3.0.0", default-features = false, path = "../../../frame/election-provider-multi-phase" }
pallet-grandpa = { version = "3.0.0", default-features = false, path = "../../../frame/grandpa" }
pallet-im-online = { version = "3.0.0", default-features = false, path = "../../../frame/im-online" }
pallet-indices = { version = "3.0.0", default-features = false, path = "../../../frame/indices" }
@@ -141,6 +142,7 @@ std = [
"frame-benchmarking/std",
"frame-system-rpc-runtime-api/std",
"frame-system/std",
"pallet-election-provider-multi-phase/std",
"pallet-timestamp/std",
"pallet-tips/std",
"pallet-transaction-payment-rpc-runtime-api/std",
@@ -157,6 +159,7 @@ runtime-benchmarks = [
"frame-benchmarking",
"frame-support/runtime-benchmarks",
"frame-system/runtime-benchmarks",
"pallet-election-provider-multi-phase/runtime-benchmarks",
"sp-runtime/runtime-benchmarks",
"pallet-assets/runtime-benchmarks",
"pallet-babe/runtime-benchmarks",
+1 -1
View File
@@ -35,7 +35,7 @@ pub mod time {
use node_primitives::{Moment, BlockNumber};
/// Since BABE is probabilistic this is the average expected block time that
/// we are targetting. Blocks will be produced at a minimum duration defined
/// we are targeting. Blocks will be produced at a minimum duration defined
/// by `SLOT_DURATION`, but some slots will not be allocated to any
/// authority and hence no block will be produced. We expect to have this
/// block time on average following the defined slot duration and the value
+49 -8
View File
@@ -28,7 +28,8 @@ use frame_support::{
construct_runtime, parameter_types, debug, RuntimeDebug,
weights::{
Weight, IdentityFee,
constants::{BlockExecutionWeight, ExtrinsicBaseWeight, RocksDbWeight, WEIGHT_PER_SECOND}, DispatchClass,
constants::{BlockExecutionWeight, ExtrinsicBaseWeight, RocksDbWeight, WEIGHT_PER_SECOND},
DispatchClass,
},
traits::{
Currency, Imbalance, KeyOwnerProofSystem, OnUnbalanced, Randomness, LockIdentifier,
@@ -50,14 +51,14 @@ pub use node_primitives::{AccountId, Signature};
use node_primitives::{AccountIndex, Balance, BlockNumber, Hash, Index, Moment};
use sp_api::impl_runtime_apis;
use sp_runtime::{
Permill, Perbill, Perquintill, Percent, ApplyExtrinsicResult,
impl_opaque_keys, generic, create_runtime_str, ModuleId, FixedPointNumber,
Permill, Perbill, Perquintill, Percent, ApplyExtrinsicResult, impl_opaque_keys, generic,
create_runtime_str, ModuleId, FixedPointNumber,
};
use sp_runtime::curve::PiecewiseLinear;
use sp_runtime::transaction_validity::{TransactionValidity, TransactionSource, TransactionPriority};
use sp_runtime::traits::{
self, BlakeTwo256, Block as BlockT, StaticLookup, SaturatedConversion,
ConvertInto, OpaqueKeys, NumberFor,
self, BlakeTwo256, Block as BlockT, StaticLookup, SaturatedConversion, ConvertInto, OpaqueKeys,
NumberFor,
};
use sp_version::RuntimeVersion;
#[cfg(any(feature = "std", test))]
@@ -145,7 +146,7 @@ impl OnUnbalanced<NegativeImbalance> for DealWithFees {
}
}
/// We assume that ~10% of the block weight is consumed by `on_initalize` handlers.
/// We assume that ~10% of the block weight is consumed by `on_initialize` handlers.
/// This is used to limit the maximal weight of a single extrinsic.
const AVERAGE_ON_INITIALIZE_RATIO: Perbill = Perbill::from_percent(10);
/// We allow `Normal` extrinsics to fill up the block up to 75%, the rest can be used
@@ -490,18 +491,56 @@ impl pallet_staking::Config for Runtime {
type SessionInterface = Self;
type RewardCurve = RewardCurve;
type NextNewSession = Session;
type MaxNominatorRewardedPerValidator = MaxNominatorRewardedPerValidator;
type ElectionLookahead = ElectionLookahead;
type Call = Call;
type MaxIterations = MaxIterations;
type MinSolutionScoreBump = MinSolutionScoreBump;
type MaxNominatorRewardedPerValidator = MaxNominatorRewardedPerValidator;
type UnsignedPriority = StakingUnsignedPriority;
// The unsigned solution weight targeted by the OCW. We set it to the maximum possible value of
// a single extrinsic.
type OffchainSolutionWeightLimit = OffchainSolutionWeightLimit;
type ElectionProvider = ElectionProviderMultiPhase;
type WeightInfo = pallet_staking::weights::SubstrateWeight<Runtime>;
}
parameter_types! {
// phase durations. 1/4 of the last session for each.
pub const SignedPhase: u32 = EPOCH_DURATION_IN_BLOCKS / 4;
pub const UnsignedPhase: u32 = EPOCH_DURATION_IN_BLOCKS / 4;
// fallback: no need to do on-chain phragmen initially.
pub const Fallback: pallet_election_provider_multi_phase::FallbackStrategy =
pallet_election_provider_multi_phase::FallbackStrategy::Nothing;
pub SolutionImprovementThreshold: Perbill = Perbill::from_rational_approximation(1u32, 10_000);
// miner configs
pub const MultiPhaseUnsignedPriority: TransactionPriority = StakingUnsignedPriority::get() - 1u64;
pub const MinerMaxIterations: u32 = 10;
pub MinerMaxWeight: Weight = RuntimeBlockWeights::get()
.get(DispatchClass::Normal)
.max_extrinsic.expect("Normal extrinsics have a weight limit configured; qed")
.saturating_sub(BlockExecutionWeight::get());
}
impl pallet_election_provider_multi_phase::Config for Runtime {
type Event = Event;
type Currency = Balances;
type SignedPhase = SignedPhase;
type UnsignedPhase = UnsignedPhase;
type SolutionImprovementThreshold = MinSolutionScoreBump;
type MinerMaxIterations = MinerMaxIterations;
type MinerMaxWeight = MinerMaxWeight;
type MinerTxPriority = MultiPhaseUnsignedPriority;
type DataProvider = Staking;
type OnChainAccuracy = Perbill;
type CompactSolution = pallet_staking::CompactAssignments;
type Fallback = Fallback;
type WeightInfo = pallet_election_provider_multi_phase::weights::SubstrateWeight<Runtime>;
type BenchmarkingConfig = ();
}
parameter_types! {
pub const LaunchPeriod: BlockNumber = 28 * 24 * 60 * MINUTES;
pub const VotingPeriod: BlockNumber = 28 * 24 * 60 * MINUTES;
@@ -548,7 +587,7 @@ impl pallet_democracy::Config for Runtime {
>;
type BlacklistOrigin = EnsureRoot<AccountId>;
// Any single technical committee member may veto a coming council proposal, however they can
// only do it once and it lasts only for the cooloff period.
// only do it once and it lasts only for the cool-off period.
type VetoOrigin = pallet_collective::EnsureMember<AccountId, TechnicalCollective>;
type CooloffPeriod = CooloffPeriod;
type PreimageByteDeposit = PreimageByteDeposit;
@@ -1020,6 +1059,7 @@ construct_runtime!(
Indices: pallet_indices::{Module, Call, Storage, Config<T>, Event<T>},
Balances: pallet_balances::{Module, Call, Storage, Config<T>, Event<T>},
TransactionPayment: pallet_transaction_payment::{Module, Storage},
ElectionProviderMultiPhase: pallet_election_provider_multi_phase::{Module, Call, Storage, Event<T>, ValidateUnsigned},
Staking: pallet_staking::{Module, Call, Config<T>, Storage, Event<T>, ValidateUnsigned},
Session: pallet_session::{Module, Call, Storage, Event, Config<T>},
Democracy: pallet_democracy::{Module, Call, Storage, Config, Event<T>},
@@ -1386,6 +1426,7 @@ impl_runtime_apis! {
add_benchmark!(params, batches, pallet_contracts, Contracts);
add_benchmark!(params, batches, pallet_democracy, Democracy);
add_benchmark!(params, batches, pallet_elections_phragmen, Elections);
add_benchmark!(params, batches, pallet_election_provider_multi_phase, ElectionProviderMultiPhase);
add_benchmark!(params, batches, pallet_grandpa, Grandpa);
add_benchmark!(params, batches, pallet_identity, Identity);
add_benchmark!(params, batches, pallet_im_online, ImOnline);
+1 -1
View File
@@ -32,12 +32,12 @@ sp-std = { version = "3.0.0", default-features = false, path = "../../primitives
sp-timestamp = { version = "3.0.0", default-features = false, path = "../../primitives/timestamp" }
[dev-dependencies]
frame-benchmarking = { version = "3.0.0", path = "../benchmarking" }
pallet-balances = { version = "3.0.0", path = "../balances" }
pallet-offences = { version = "3.0.0", path = "../offences" }
pallet-staking = { version = "3.0.0", path = "../staking" }
pallet-staking-reward-curve = { version = "3.0.0", path = "../staking/reward-curve" }
sp-core = { version = "3.0.0", path = "../../primitives/core" }
sp-election-providers = { version = "3.0.0", path = "../../primitives/election-providers" }
[features]
default = ["std"]
+9 -3
View File
@@ -413,12 +413,14 @@ impl<T: Config> Module<T> {
/// In other word, this is only accurate if no slots are missed. Given missed slots, the slot
/// number will grow while the block number will not. Hence, the result can be interpreted as an
/// upper bound.
// -------------- IMPORTANT NOTE --------------
//
// ## IMPORTANT NOTE
//
// This implementation is linked to how [`should_epoch_change`] is working. This might need to
// be updated accordingly, if the underlying mechanics of slot and epochs change.
//
// WEIGHT NOTE: This function is tied to the weight of `EstimateNextSessionRotation`. If you update
// this function, you must also update the corresponding weight.
// WEIGHT NOTE: This function is tied to the weight of `EstimateNextSessionRotation`. If you
// update this function, you must also update the corresponding weight.
pub fn next_expected_epoch_change(now: T::BlockNumber) -> Option<T::BlockNumber> {
let next_slot = Self::current_epoch_start().saturating_add(T::EpochDuration::get());
next_slot
@@ -754,6 +756,10 @@ impl<T: Config> OnTimestampSet<T::Moment> for Module<T> {
}
impl<T: Config> frame_support::traits::EstimateNextSessionRotation<T::BlockNumber> for Module<T> {
fn average_session_length() -> T::BlockNumber {
T::EpochDuration::get().saturated_into()
}
fn estimate_next_session_rotation(now: T::BlockNumber) -> Option<T::BlockNumber> {
Self::next_expected_epoch_change(now)
}
+9
View File
@@ -37,6 +37,7 @@ use sp_consensus_babe::{AuthorityId, AuthorityPair, Slot};
use sp_consensus_vrf::schnorrkel::{VRFOutput, VRFProof};
use sp_staking::SessionIndex;
use pallet_staking::EraIndex;
use sp_election_providers::onchain;
use pallet_session::historical as pallet_session_historical;
type DummyValidatorId = u64;
@@ -183,6 +184,13 @@ parameter_types! {
pub const StakingUnsignedPriority: u64 = u64::max_value() / 2;
}
impl onchain::Config for Test {
type AccountId = <Self as frame_system::Config>::AccountId;
type BlockNumber = <Self as frame_system::Config>::BlockNumber;
type Accuracy = Perbill;
type DataProvider = Staking;
}
impl pallet_staking::Config for Test {
type RewardRemainder = ();
type CurrencyToVote = frame_support::traits::SaturatingCurrencyToVote;
@@ -205,6 +213,7 @@ impl pallet_staking::Config for Test {
type MaxIterations = ();
type MinSolutionScoreBump = ();
type OffchainSolutionWeightLimit = ();
type ElectionProvider = onchain::OnChainSequentialPhragmen<Self>;
type WeightInfo = ();
}
@@ -0,0 +1,66 @@
[package]
name = "pallet-election-provider-multi-phase"
version = "3.0.0"
authors = ["Parity Technologies <admin@parity.io>"]
edition = "2018"
license = "Apache-2.0"
homepage = "https://substrate.dev"
repository = "https://github.com/paritytech/substrate/"
description = "PALLET two phase election providers"
readme = "README.md"
[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]
[dependencies]
static_assertions = "1.1.0"
serde = { version = "1.0.101", optional = true }
codec = { package = "parity-scale-codec", version = "2.0.0", default-features = false, features = ["derive"] }
frame-support = { version = "3.0.0", default-features = false, path = "../support" }
frame-system = { version = "3.0.0", default-features = false, path = "../system" }
sp-io ={ version = "3.0.0", default-features = false, path = "../../primitives/io" }
sp-std = { version = "3.0.0", default-features = false, path = "../../primitives/std" }
sp-runtime = { version = "3.0.0", default-features = false, path = "../../primitives/runtime" }
sp-npos-elections = { version = "3.0.0", default-features = false, path = "../../primitives/npos-elections" }
sp-arithmetic = { version = "3.0.0", default-features = false, path = "../../primitives/arithmetic" }
sp-election-providers = { version = "3.0.0", default-features = false, path = "../../primitives/election-providers" }
# Optional imports for benchmarking
frame-benchmarking = { version = "3.0.0", default-features = false, path = "../benchmarking", optional = true }
rand = { version = "0.7.3", default-features = false, optional = true, features = ["alloc", "small_rng"] }
[dev-dependencies]
paste = "1.0.3"
parking_lot = "0.11.0"
rand = { version = "0.7.3" }
hex-literal = "0.3.1"
substrate-test-utils = { version = "3.0.0", path = "../../test-utils" }
sp-io = { version = "3.0.0", path = "../../primitives/io" }
sp-core = { version = "3.0.0", path = "../../primitives/core" }
sp-tracing = { version = "3.0.0", path = "../../primitives/tracing" }
sp-election-providers = { version = "3.0.0", features = ["runtime-benchmarks"], path = "../../primitives/election-providers" }
pallet-balances = { version = "3.0.0", path = "../balances" }
frame-benchmarking = { path = "../benchmarking" }
[features]
default = ["std"]
std = [
"serde",
"codec/std",
"frame-support/std",
"frame-system/std",
"sp-io/std",
"sp-std/std",
"sp-runtime/std",
"sp-npos-elections/std",
"sp-arithmetic/std",
"sp-election-providers/std",
]
runtime-benchmarks = [
"frame-benchmarking",
"rand",
]
@@ -0,0 +1,282 @@
// This file is part of Substrate.
// Copyright (C) 2020 Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Two phase election pallet benchmarking.
use super::*;
use crate::Module as MultiPhase;
pub use frame_benchmarking::{account, benchmarks, whitelist_account, whitelisted_caller};
use frame_support::{assert_ok, traits::OnInitialize};
use frame_system::RawOrigin;
use rand::{prelude::SliceRandom, rngs::SmallRng, SeedableRng};
use sp_election_providers::Assignment;
use sp_arithmetic::traits::One;
use sp_runtime::InnerOf;
use sp_std::convert::TryInto;
const SEED: u32 = 0;
/// Creates a **valid** solution with exactly the given size.
///
/// The snapshot is also created internally.
fn solution_with_size<T: Config>(
size: SolutionOrSnapshotSize,
active_voters_count: u32,
desired_targets: u32,
) -> RawSolution<CompactOf<T>> {
assert!(size.targets >= desired_targets, "must have enough targets");
assert!(
size.targets >= (<CompactOf<T>>::LIMIT * 2) as u32,
"must have enough targets for unique votes."
);
assert!(size.voters >= active_voters_count, "must have enough voters");
assert!(
(<CompactOf<T>>::LIMIT as u32) < desired_targets,
"must have enough winners to give them votes."
);
let ed: VoteWeight = T::Currency::minimum_balance().saturated_into::<u64>();
let stake: VoteWeight = ed.max(One::one()).saturating_mul(100);
// first generates random targets.
let targets: Vec<T::AccountId> =
(0..size.targets).map(|i| account("Targets", i, SEED)).collect();
let mut rng = SmallRng::seed_from_u64(999u64);
// decide who are the winners.
let winners = targets
.as_slice()
.choose_multiple(&mut rng, desired_targets as usize)
.cloned()
.collect::<Vec<_>>();
// first generate active voters who must vote for a subset of winners.
let active_voters = (0..active_voters_count)
.map(|i| {
// chose a random subset of winners.
let winner_votes = winners
.as_slice()
.choose_multiple(&mut rng, <CompactOf<T>>::LIMIT)
.cloned()
.collect::<Vec<_>>();
let voter = account::<T::AccountId>("Voter", i, SEED);
(voter, stake, winner_votes)
})
.collect::<Vec<_>>();
// rest of the voters. They can only vote for non-winners.
let non_winners =
targets.iter().filter(|t| !winners.contains(t)).cloned().collect::<Vec<T::AccountId>>();
let rest_voters = (active_voters_count..size.voters)
.map(|i| {
let votes = (&non_winners)
.choose_multiple(&mut rng, <CompactOf<T>>::LIMIT)
.cloned()
.collect::<Vec<T::AccountId>>();
let voter = account::<T::AccountId>("Voter", i, SEED);
(voter, stake, votes)
})
.collect::<Vec<_>>();
let mut all_voters = active_voters.clone();
all_voters.extend(rest_voters);
all_voters.shuffle(&mut rng);
assert_eq!(active_voters.len() as u32, active_voters_count);
assert_eq!(all_voters.len() as u32, size.voters);
assert_eq!(winners.len() as u32, desired_targets);
<SnapshotMetadata<T>>::put(SolutionOrSnapshotSize {
voters: all_voters.len() as u32,
targets: targets.len() as u32,
});
<DesiredTargets<T>>::put(desired_targets);
<Snapshot<T>>::put(RoundSnapshot { voters: all_voters.clone(), targets: targets.clone() });
// write the snapshot to staking or whoever is the data provider.
T::DataProvider::put_snapshot(all_voters.clone(), targets.clone());
let cache = helpers::generate_voter_cache::<T>(&all_voters);
let stake_of = helpers::stake_of_fn::<T>(&all_voters, &cache);
let voter_index = helpers::voter_index_fn::<T>(&cache);
let target_index = helpers::target_index_fn_linear::<T>(&targets);
let voter_at = helpers::voter_at_fn::<T>(&all_voters);
let target_at = helpers::target_at_fn::<T>(&targets);
let assignments = active_voters
.iter()
.map(|(voter, _stake, votes)| {
let percent_per_edge: InnerOf<CompactAccuracyOf<T>> =
(100 / votes.len()).try_into().unwrap_or_else(|_| panic!("failed to convert"));
Assignment {
who: voter.clone(),
distribution: votes
.iter()
.map(|t| (t.clone(), <CompactAccuracyOf<T>>::from_percent(percent_per_edge)))
.collect::<Vec<_>>(),
}
})
.collect::<Vec<_>>();
let compact =
<CompactOf<T>>::from_assignment(assignments, &voter_index, &target_index).unwrap();
let score = compact.clone().score(&winners, stake_of, voter_at, target_at).unwrap();
let round = <MultiPhase<T>>::round();
RawSolution { compact, score, round }
}
benchmarks! {
on_initialize_nothing {
assert!(<MultiPhase<T>>::current_phase().is_off());
}: {
<MultiPhase<T>>::on_initialize(1u32.into());
} verify {
assert!(<MultiPhase<T>>::current_phase().is_off());
}
on_initialize_open_signed {
// NOTE: this benchmark currently doesn't have any components because the length of a db
// read/write is not captured. Otherwise, it is quite influenced by how much data
// `T::ElectionDataProvider` is reading and passing on.
assert!(<MultiPhase<T>>::snapshot().is_none());
assert!(<MultiPhase<T>>::current_phase().is_off());
}: {
<MultiPhase<T>>::on_initialize_open_signed();
} verify {
assert!(<MultiPhase<T>>::snapshot().is_some());
assert!(<MultiPhase<T>>::current_phase().is_signed());
}
on_initialize_open_unsigned_with_snapshot {
assert!(<MultiPhase<T>>::snapshot().is_none());
assert!(<MultiPhase<T>>::current_phase().is_off());
}: {
<MultiPhase<T>>::on_initialize_open_unsigned(true, true, 1u32.into());
} verify {
assert!(<MultiPhase<T>>::snapshot().is_some());
assert!(<MultiPhase<T>>::current_phase().is_unsigned());
}
on_initialize_open_unsigned_without_snapshot {
// need to assume signed phase was open before
<MultiPhase<T>>::on_initialize_open_signed();
assert!(<MultiPhase<T>>::snapshot().is_some());
assert!(<MultiPhase<T>>::current_phase().is_signed());
}: {
<MultiPhase<T>>::on_initialize_open_unsigned(false, true, 1u32.into());
} verify {
assert!(<MultiPhase<T>>::snapshot().is_some());
assert!(<MultiPhase<T>>::current_phase().is_unsigned());
}
#[extra]
create_snapshot {
assert!(<MultiPhase<T>>::snapshot().is_none());
}: {
<MultiPhase::<T>>::create_snapshot()
} verify {
assert!(<MultiPhase<T>>::snapshot().is_some());
}
submit_unsigned {
// number of votes in snapshot.
let v in (T::BenchmarkingConfig::VOTERS[0]) .. T::BenchmarkingConfig::VOTERS[1];
// number of targets in snapshot.
let t in (T::BenchmarkingConfig::TARGETS[0]) .. T::BenchmarkingConfig::TARGETS[1];
// number of assignments, i.e. compact.len(). This means the active nominators, thus must be
// a subset of `v` component.
let a in (T::BenchmarkingConfig::ACTIVE_VOTERS[0]) .. T::BenchmarkingConfig::ACTIVE_VOTERS[1];
// number of desired targets. Must be a subset of `t` component.
let d in (T::BenchmarkingConfig::DESIRED_TARGETS[0]) .. T::BenchmarkingConfig::DESIRED_TARGETS[1];
let witness = SolutionOrSnapshotSize { voters: v, targets: t };
let raw_solution = solution_with_size::<T>(witness, a, d);
assert!(<MultiPhase<T>>::queued_solution().is_none());
<CurrentPhase<T>>::put(Phase::Unsigned((true, 1u32.into())));
// encode the most significant storage item that needs to be decoded in the dispatch.
let encoded_snapshot = <MultiPhase<T>>::snapshot().unwrap().encode();
let encoded_call = <Call<T>>::submit_unsigned(raw_solution.clone(), witness).encode();
}: {
assert_ok!(<MultiPhase<T>>::submit_unsigned(RawOrigin::None.into(), raw_solution, witness));
let _decoded_snap = <RoundSnapshot<T::AccountId> as Decode>::decode(&mut &*encoded_snapshot).unwrap();
let _decoded_call = <Call<T> as Decode>::decode(&mut &*encoded_call).unwrap();
} verify {
assert!(<MultiPhase<T>>::queued_solution().is_some());
}
// This is checking a valid solution. The worse case is indeed a valid solution.
feasibility_check {
// number of votes in snapshot.
let v in (T::BenchmarkingConfig::VOTERS[0]) .. T::BenchmarkingConfig::VOTERS[1];
// number of targets in snapshot.
let t in (T::BenchmarkingConfig::TARGETS[0]) .. T::BenchmarkingConfig::TARGETS[1];
// number of assignments, i.e. compact.len(). This means the active nominators, thus must be
// a subset of `v` component.
let a in (T::BenchmarkingConfig::ACTIVE_VOTERS[0]) .. T::BenchmarkingConfig::ACTIVE_VOTERS[1];
// number of desired targets. Must be a subset of `t` component.
let d in (T::BenchmarkingConfig::DESIRED_TARGETS[0]) .. T::BenchmarkingConfig::DESIRED_TARGETS[1];
let size = SolutionOrSnapshotSize { voters: v, targets: t };
let raw_solution = solution_with_size::<T>(size, a, d);
assert_eq!(raw_solution.compact.voter_count() as u32, a);
assert_eq!(raw_solution.compact.unique_targets().len() as u32, d);
// encode the most significant storage item that needs to be decoded in the dispatch.
let encoded_snapshot = <MultiPhase<T>>::snapshot().unwrap().encode();
}: {
assert_ok!(<MultiPhase<T>>::feasibility_check(raw_solution, ElectionCompute::Unsigned));
let _decoded_snap = <RoundSnapshot<T::AccountId> as Decode>::decode(&mut &*encoded_snapshot).unwrap();
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::mock::*;
#[test]
fn test_benchmarks() {
ExtBuilder::default().build_and_execute(|| {
assert_ok!(test_benchmark_feasibility_check::<Runtime>());
});
ExtBuilder::default().build_and_execute(|| {
assert_ok!(test_benchmark_submit_unsigned::<Runtime>());
});
ExtBuilder::default().build_and_execute(|| {
assert_ok!(test_benchmark_on_initialize_open_unsigned_with_snapshot::<Runtime>());
});
ExtBuilder::default().build_and_execute(|| {
assert_ok!(test_benchmark_on_initialize_open_unsigned_without_snapshot::<Runtime>());
});
ExtBuilder::default().build_and_execute(|| {
assert_ok!(test_benchmark_on_initialize_nothing::<Runtime>());
});
ExtBuilder::default().build_and_execute(|| {
assert_ok!(test_benchmark_create_snapshot::<Runtime>());
});
}
}
@@ -0,0 +1,159 @@
// This file is part of Substrate.
// Copyright (C) 2020 Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Some helper functions/macros for this crate.
use super::{Config, VoteWeight, CompactVoterIndexOf, CompactTargetIndexOf};
use sp_std::{collections::btree_map::BTreeMap, convert::TryInto, boxed::Box, prelude::*};
#[macro_export]
macro_rules! log {
($level:tt, $patter:expr $(, $values:expr)* $(,)?) => {
frame_support::debug::$level!(
target: $crate::LOG_TARGET,
concat!("🗳 ", $patter) $(, $values)*
)
};
}
/// Generate a btree-map cache of the voters and their indices.
///
/// This can be used to efficiently build index getter closures.
pub fn generate_voter_cache<T: Config>(
snapshot: &Vec<(T::AccountId, VoteWeight, Vec<T::AccountId>)>,
) -> BTreeMap<T::AccountId, usize> {
let mut cache: BTreeMap<T::AccountId, usize> = BTreeMap::new();
snapshot.iter().enumerate().for_each(|(i, (x, _, _))| {
let _existed = cache.insert(x.clone(), i);
// if a duplicate exists, we only consider the last one. Defensive only, should never
// happen.
debug_assert!(_existed.is_none());
});
cache
}
/// Create a function the returns the index a voter in the snapshot.
///
/// The returning index type is the same as the one defined in [`T::CompactSolution::Voter`].
///
/// ## Warning
///
/// The snapshot must be the same is the one used to create `cache`.
pub fn voter_index_fn<T: Config>(
cache: &BTreeMap<T::AccountId, usize>,
) -> Box<dyn Fn(&T::AccountId) -> Option<CompactVoterIndexOf<T>> + '_> {
Box::new(move |who| {
cache.get(who).and_then(|i| <usize as TryInto<CompactVoterIndexOf<T>>>::try_into(*i).ok())
})
}
/// Same as [`voter_index_fn`], but the returning index is converted into usize, if possible.
///
/// ## Warning
///
/// The snapshot must be the same is the one used to create `cache`.
pub fn voter_index_fn_usize<T: Config>(
cache: &BTreeMap<T::AccountId, usize>,
) -> Box<dyn Fn(&T::AccountId) -> Option<usize> + '_> {
Box::new(move |who| cache.get(who).cloned())
}
/// A non-optimized, linear version of [`voter_index_fn`] that does not need a cache and does a
/// linear search.
///
/// ## Warning
///
/// Not meant to be used in production.
pub fn voter_index_fn_linear<T: Config>(
snapshot: &Vec<(T::AccountId, VoteWeight, Vec<T::AccountId>)>,
) -> Box<dyn Fn(&T::AccountId) -> Option<CompactVoterIndexOf<T>> + '_> {
Box::new(move |who| {
snapshot
.iter()
.position(|(x, _, _)| x == who)
.and_then(|i| <usize as TryInto<CompactVoterIndexOf<T>>>::try_into(i).ok())
})
}
/// Create a function the returns the index a targets in the snapshot.
///
/// The returning index type is the same as the one defined in [`T::CompactSolution::Target`].
pub fn target_index_fn_linear<T: Config>(
snapshot: &Vec<T::AccountId>,
) -> Box<dyn Fn(&T::AccountId) -> Option<CompactTargetIndexOf<T>> + '_> {
Box::new(move |who| {
snapshot
.iter()
.position(|x| x == who)
.and_then(|i| <usize as TryInto<CompactTargetIndexOf<T>>>::try_into(i).ok())
})
}
/// Create a function that can map a voter index ([`CompactVoterIndexOf`]) to the actual voter
/// account using a linearly indexible snapshot.
pub fn voter_at_fn<T: Config>(
snapshot: &Vec<(T::AccountId, VoteWeight, Vec<T::AccountId>)>,
) -> Box<dyn Fn(CompactVoterIndexOf<T>) -> Option<T::AccountId> + '_> {
Box::new(move |i| {
<CompactVoterIndexOf<T> as TryInto<usize>>::try_into(i)
.ok()
.and_then(|i| snapshot.get(i).map(|(x, _, _)| x).cloned())
})
}
/// Create a function that can map a target index ([`CompactTargetIndexOf`]) to the actual target
/// account using a linearly indexible snapshot.
pub fn target_at_fn<T: Config>(
snapshot: &Vec<T::AccountId>,
) -> Box<dyn Fn(CompactTargetIndexOf<T>) -> Option<T::AccountId> + '_> {
Box::new(move |i| {
<CompactTargetIndexOf<T> as TryInto<usize>>::try_into(i)
.ok()
.and_then(|i| snapshot.get(i).cloned())
})
}
/// Create a function to get the stake of a voter.
///
/// This is not optimized and uses a linear search.
pub fn stake_of_fn_linear<T: Config>(
snapshot: &Vec<(T::AccountId, VoteWeight, Vec<T::AccountId>)>,
) -> Box<dyn Fn(&T::AccountId) -> VoteWeight + '_> {
Box::new(move |who| {
snapshot.iter().find(|(x, _, _)| x == who).map(|(_, x, _)| *x).unwrap_or_default()
})
}
/// Create a function to get the stake of a voter.
///
/// ## Warning
///
/// The cache need must be derived from the same snapshot. Zero is returned if a voter is
/// non-existent.
pub fn stake_of_fn<'a, T: Config>(
snapshot: &'a Vec<(T::AccountId, VoteWeight, Vec<T::AccountId>)>,
cache: &'a BTreeMap<T::AccountId, usize>,
) -> Box<dyn Fn(&T::AccountId) -> VoteWeight + 'a> {
Box::new(move |who| {
if let Some(index) = cache.get(who) {
snapshot.get(*index).map(|(_, x, _)| x).cloned().unwrap_or_default()
} else {
0
}
})
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,381 @@
// This file is part of Substrate.
// Copyright (C) 2021 Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use super::*;
use crate as multi_phase;
pub use frame_support::{assert_noop, assert_ok};
use frame_support::{
parameter_types,
traits::{Hooks},
weights::Weight,
};
use parking_lot::RwLock;
use sp_core::{
offchain::{
testing::{PoolState, TestOffchainExt, TestTransactionPoolExt},
OffchainExt, TransactionPoolExt,
},
H256,
};
use sp_election_providers::ElectionDataProvider;
use sp_npos_elections::{
assignment_ratio_to_staked_normalized, seq_phragmen, to_supports, to_without_backing,
CompactSolution, ElectionResult, EvaluateSupport,
};
use sp_runtime::{
testing::Header,
traits::{BlakeTwo256, IdentityLookup},
PerU16,
};
use std::sync::Arc;
pub type Block = sp_runtime::generic::Block<Header, UncheckedExtrinsic>;
pub type UncheckedExtrinsic = sp_runtime::generic::UncheckedExtrinsic<AccountId, Call, (), ()>;
frame_support::construct_runtime!(
pub enum Runtime where
Block = Block,
NodeBlock = Block,
UncheckedExtrinsic = UncheckedExtrinsic
{
System: frame_system::{Module, Call, Event<T>, Config},
Balances: pallet_balances::{Module, Call, Event<T>, Config<T>},
MultiPhase: multi_phase::{Module, Call, Event<T>},
}
);
pub(crate) type Balance = u64;
pub(crate) type AccountId = u64;
sp_npos_elections::generate_solution_type!(
#[compact]
pub struct TestCompact::<u32, u16, PerU16>(16)
);
/// All events of this pallet.
pub(crate) fn multi_phase_events() -> Vec<super::Event<Runtime>> {
System::events()
.into_iter()
.map(|r| r.event)
.filter_map(|e| if let Event::multi_phase(inner) = e { Some(inner) } else { None })
.collect::<Vec<_>>()
}
/// To from `now` to block `n`.
pub fn roll_to(n: u64) {
let now = System::block_number();
for i in now + 1..=n {
System::set_block_number(i);
MultiPhase::on_initialize(i);
}
}
pub fn roll_to_with_ocw(n: u64) {
let now = System::block_number();
for i in now + 1..=n {
System::set_block_number(i);
MultiPhase::on_initialize(i);
MultiPhase::offchain_worker(i);
}
}
/// Spit out a verifiable raw solution.
///
/// This is a good example of what an offchain miner would do.
pub fn raw_solution() -> RawSolution<CompactOf<Runtime>> {
let RoundSnapshot { voters, targets } = MultiPhase::snapshot().unwrap();
let desired_targets = MultiPhase::desired_targets().unwrap();
// closures
let cache = helpers::generate_voter_cache::<Runtime>(&voters);
let voter_index = helpers::voter_index_fn_linear::<Runtime>(&voters);
let target_index = helpers::target_index_fn_linear::<Runtime>(&targets);
let stake_of = helpers::stake_of_fn::<Runtime>(&voters, &cache);
let ElectionResult { winners, assignments } = seq_phragmen::<_, CompactAccuracyOf<Runtime>>(
desired_targets as usize,
targets.clone(),
voters.clone(),
None,
)
.unwrap();
let winners = to_without_backing(winners);
let score = {
let staked = assignment_ratio_to_staked_normalized(assignments.clone(), &stake_of).unwrap();
to_supports(&winners, &staked).unwrap().evaluate()
};
let compact =
<CompactOf<Runtime>>::from_assignment(assignments, &voter_index, &target_index).unwrap();
let round = MultiPhase::round();
RawSolution { compact, score, round }
}
pub fn witness() -> SolutionOrSnapshotSize {
MultiPhase::snapshot()
.map(|snap| SolutionOrSnapshotSize {
voters: snap.voters.len() as u32,
targets: snap.targets.len() as u32,
})
.unwrap_or_default()
}
impl frame_system::Config for Runtime {
type SS58Prefix = ();
type BaseCallFilter = ();
type Origin = Origin;
type Index = u64;
type BlockNumber = u64;
type Call = Call;
type Hash = H256;
type Hashing = BlakeTwo256;
type AccountId = AccountId;
type Lookup = IdentityLookup<Self::AccountId>;
type Header = Header;
type Event = Event;
type BlockHashCount = ();
type DbWeight = ();
type BlockLength = ();
type BlockWeights = BlockWeights;
type Version = ();
type PalletInfo = PalletInfo;
type AccountData = pallet_balances::AccountData<u64>;
type OnNewAccount = ();
type OnKilledAccount = ();
type SystemWeightInfo = ();
}
const NORMAL_DISPATCH_RATIO: Perbill = Perbill::from_percent(75);
parameter_types! {
pub const ExistentialDeposit: u64 = 1;
pub BlockWeights: frame_system::limits::BlockWeights = frame_system::limits::BlockWeights
::with_sensible_defaults(2 * frame_support::weights::constants::WEIGHT_PER_SECOND, NORMAL_DISPATCH_RATIO);
}
impl pallet_balances::Config for Runtime {
type Balance = Balance;
type Event = Event;
type DustRemoval = ();
type ExistentialDeposit = ExistentialDeposit;
type AccountStore = System;
type MaxLocks = ();
type WeightInfo = ();
}
parameter_types! {
pub static Targets: Vec<AccountId> = vec![10, 20, 30, 40];
pub static Voters: Vec<(AccountId, VoteWeight, Vec<AccountId>)> = vec![
(1, 10, vec![10, 20]),
(2, 10, vec![30, 40]),
(3, 10, vec![40]),
(4, 10, vec![10, 20, 30, 40]),
// self votes.
(10, 10, vec![10]),
(20, 20, vec![20]),
(30, 30, vec![30]),
(40, 40, vec![40]),
];
pub static Fallback: FallbackStrategy = FallbackStrategy::OnChain;
pub static DesiredTargets: u32 = 2;
pub static SignedPhase: u64 = 10;
pub static UnsignedPhase: u64 = 5;
pub static MaxSignedSubmissions: u32 = 5;
pub static MinerMaxIterations: u32 = 5;
pub static MinerTxPriority: u64 = 100;
pub static SolutionImprovementThreshold: Perbill = Perbill::zero();
pub static MinerMaxWeight: Weight = BlockWeights::get().max_block;
pub static MockWeightInfo: bool = false;
pub static EpochLength: u64 = 30;
}
// Hopefully this won't be too much of a hassle to maintain.
pub struct DualMockWeightInfo;
impl multi_phase::weights::WeightInfo for DualMockWeightInfo {
fn on_initialize_nothing() -> Weight {
if MockWeightInfo::get() {
Zero::zero()
} else {
<() as multi_phase::weights::WeightInfo>::on_initialize_nothing()
}
}
fn on_initialize_open_signed() -> Weight {
if MockWeightInfo::get() {
Zero::zero()
} else {
<() as multi_phase::weights::WeightInfo>::on_initialize_open_signed()
}
}
fn on_initialize_open_unsigned_with_snapshot() -> Weight {
if MockWeightInfo::get() {
Zero::zero()
} else {
<() as multi_phase::weights::WeightInfo>::on_initialize_open_unsigned_with_snapshot()
}
}
fn on_initialize_open_unsigned_without_snapshot() -> Weight {
if MockWeightInfo::get() {
Zero::zero()
} else {
<() as multi_phase::weights::WeightInfo>::on_initialize_open_unsigned_without_snapshot()
}
}
fn submit_unsigned(v: u32, t: u32, a: u32, d: u32) -> Weight {
if MockWeightInfo::get() {
// 10 base
// 5 per edge.
(10 as Weight).saturating_add((5 as Weight).saturating_mul(a as Weight))
} else {
<() as multi_phase::weights::WeightInfo>::submit_unsigned(v, t, a, d)
}
}
fn feasibility_check(v: u32, t: u32, a: u32, d: u32) -> Weight {
if MockWeightInfo::get() {
// 10 base
// 5 per edge.
(10 as Weight).saturating_add((5 as Weight).saturating_mul(a as Weight))
} else {
<() as multi_phase::weights::WeightInfo>::feasibility_check(v, t, a, d)
}
}
}
impl crate::Config for Runtime {
type Event = Event;
type Currency = Balances;
type SignedPhase = SignedPhase;
type UnsignedPhase = UnsignedPhase;
type SolutionImprovementThreshold = SolutionImprovementThreshold;
type MinerMaxIterations = MinerMaxIterations;
type MinerMaxWeight = MinerMaxWeight;
type MinerTxPriority = MinerTxPriority;
type DataProvider = StakingMock;
type WeightInfo = DualMockWeightInfo;
type BenchmarkingConfig = ();
type OnChainAccuracy = Perbill;
type Fallback = Fallback;
type CompactSolution = TestCompact;
}
impl<LocalCall> frame_system::offchain::SendTransactionTypes<LocalCall> for Runtime
where
Call: From<LocalCall>,
{
type OverarchingCall = Call;
type Extrinsic = Extrinsic;
}
pub type Extrinsic = sp_runtime::testing::TestXt<Call, ()>;
#[derive(Default)]
pub struct ExtBuilder {}
pub struct StakingMock;
impl ElectionDataProvider<AccountId, u64> for StakingMock {
fn targets() -> Vec<AccountId> {
Targets::get()
}
fn voters() -> Vec<(AccountId, VoteWeight, Vec<AccountId>)> {
Voters::get()
}
fn desired_targets() -> u32 {
DesiredTargets::get()
}
fn next_election_prediction(now: u64) -> u64 {
now + EpochLength::get() - now % EpochLength::get()
}
}
impl ExtBuilder {
pub fn miner_tx_priority(self, p: u64) -> Self {
<MinerTxPriority>::set(p);
self
}
pub fn solution_improvement_threshold(self, p: Perbill) -> Self {
<SolutionImprovementThreshold>::set(p);
self
}
pub fn phases(self, signed: u64, unsigned: u64) -> Self {
<SignedPhase>::set(signed);
<UnsignedPhase>::set(unsigned);
self
}
pub fn fallabck(self, fallback: FallbackStrategy) -> Self {
<Fallback>::set(fallback);
self
}
pub fn miner_weight(self, weight: Weight) -> Self {
<MinerMaxWeight>::set(weight);
self
}
pub fn mock_weight_info(self, mock: bool) -> Self {
<MockWeightInfo>::set(mock);
self
}
pub fn desired_targets(self, t: u32) -> Self {
<DesiredTargets>::set(t);
self
}
pub fn add_voter(self, who: AccountId, stake: Balance, targets: Vec<AccountId>) -> Self {
VOTERS.with(|v| v.borrow_mut().push((who, stake, targets)));
self
}
pub fn build(self) -> sp_io::TestExternalities {
sp_tracing::try_init_simple();
let mut storage =
frame_system::GenesisConfig::default().build_storage::<Runtime>().unwrap();
let _ = pallet_balances::GenesisConfig::<Runtime> {
balances: vec![
// bunch of account for submitting stuff only.
(99, 100),
(999, 100),
(9999, 100),
],
}
.assimilate_storage(&mut storage);
sp_io::TestExternalities::from(storage)
}
pub fn build_offchainify(
self,
iters: u32,
) -> (sp_io::TestExternalities, Arc<RwLock<PoolState>>) {
let mut ext = self.build();
let (offchain, offchain_state) = TestOffchainExt::new();
let (pool, pool_state) = TestTransactionPoolExt::new();
let mut seed = [0_u8; 32];
seed[0..4].copy_from_slice(&iters.to_le_bytes());
offchain_state.write().seed = seed;
ext.register_extension(OffchainExt::new(offchain));
ext.register_extension(TransactionPoolExt::new(pool));
(ext, pool_state)
}
pub fn build_and_execute(self, test: impl FnOnce() -> ()) {
self.build().execute_with(test)
}
}
@@ -0,0 +1,873 @@
// This file is part of Substrate.
// Copyright (C) 2020 Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! The unsigned phase implementation.
use crate::*;
use frame_support::dispatch::DispatchResult;
use frame_system::offchain::SubmitTransaction;
use sp_npos_elections::{
seq_phragmen, CompactSolution, ElectionResult, assignment_ratio_to_staked_normalized,
assignment_staked_to_ratio_normalized,
};
use sp_runtime::{offchain::storage::StorageValueRef, traits::TrailingZeroInput};
use sp_std::cmp::Ordering;
/// Storage key used to store the persistent offchain worker status.
pub(crate) const OFFCHAIN_HEAD_DB: &[u8] = b"parity/multi-phase-unsigned-election";
/// The repeat threshold of the offchain worker. This means we won't run the offchain worker twice
/// within a window of 5 blocks.
pub(crate) const OFFCHAIN_REPEAT: u32 = 5;
#[derive(Debug, Eq, PartialEq)]
pub enum MinerError {
/// An internal error in the NPoS elections crate.
NposElections(sp_npos_elections::Error),
/// Snapshot data was unavailable unexpectedly.
SnapshotUnAvailable,
/// Submitting a transaction to the pool failed.
PoolSubmissionFailed,
/// The pre-dispatch checks failed for the mined solution.
PreDispatchChecksFailed,
/// The solution generated from the miner is not feasible.
Feasibility(FeasibilityError),
}
impl From<sp_npos_elections::Error> for MinerError {
fn from(e: sp_npos_elections::Error) -> Self {
MinerError::NposElections(e)
}
}
impl From<FeasibilityError> for MinerError {
fn from(e: FeasibilityError) -> Self {
MinerError::Feasibility(e)
}
}
impl<T: Config> Pallet<T> {
/// Mine a new solution, and submit it back to the chain as an unsigned transaction.
pub fn mine_check_and_submit() -> Result<(), MinerError> {
let iters = Self::get_balancing_iters();
// get the solution, with a load of checks to ensure if submitted, IT IS ABSOLUTELY VALID.
let (raw_solution, witness) = Self::mine_and_check(iters)?;
let call = Call::submit_unsigned(raw_solution, witness).into();
SubmitTransaction::<T, Call<T>>::submit_unsigned_transaction(call)
.map_err(|_| MinerError::PoolSubmissionFailed)
}
/// Mine a new npos solution, with all the relevant checks to make sure that it will be accepted
/// to the chain.
///
/// If you want an unchecked solution, use [`Pallet::mine_solution`].
/// If you want a checked solution and submit it at the same time, use
/// [`Pallet::mine_check_and_submit`].
pub fn mine_and_check(
iters: usize,
) -> Result<(RawSolution<CompactOf<T>>, SolutionOrSnapshotSize), MinerError> {
let (raw_solution, witness) = Self::mine_solution(iters)?;
// ensure that this will pass the pre-dispatch checks
Self::unsigned_pre_dispatch_checks(&raw_solution).map_err(|e| {
log!(warn, "pre-dispatch-checks failed for mined solution: {:?}", e);
MinerError::PreDispatchChecksFailed
})?;
// ensure that this is a feasible solution
let _ = Self::feasibility_check(raw_solution.clone(), ElectionCompute::Unsigned).map_err(
|e| {
log!(warn, "feasibility-check failed for mined solution: {:?}", e);
MinerError::from(e)
},
)?;
Ok((raw_solution, witness))
}
/// Mine a new npos solution.
pub fn mine_solution(
iters: usize,
) -> Result<(RawSolution<CompactOf<T>>, SolutionOrSnapshotSize), MinerError> {
let RoundSnapshot { voters, targets } =
Self::snapshot().ok_or(MinerError::SnapshotUnAvailable)?;
let desired_targets = Self::desired_targets().ok_or(MinerError::SnapshotUnAvailable)?;
seq_phragmen::<_, CompactAccuracyOf<T>>(
desired_targets as usize,
targets,
voters,
Some((iters, 0)),
)
.map_err(Into::into)
.and_then(Self::prepare_election_result)
}
/// Convert a raw solution from [`sp_npos_elections::ElectionResult`] to [`RawSolution`], which
/// is ready to be submitted to the chain.
///
/// Will always reduce the solution as well.
pub fn prepare_election_result(
election_result: ElectionResult<T::AccountId, CompactAccuracyOf<T>>,
) -> Result<(RawSolution<CompactOf<T>>, SolutionOrSnapshotSize), MinerError> {
// NOTE: This code path is generally not optimized as it is run offchain. Could use some at
// some point though.
// storage items. Note: we have already read this from storage, they must be in cache.
let RoundSnapshot { voters, targets } =
Self::snapshot().ok_or(MinerError::SnapshotUnAvailable)?;
let desired_targets = Self::desired_targets().ok_or(MinerError::SnapshotUnAvailable)?;
// closures.
let cache = helpers::generate_voter_cache::<T>(&voters);
let voter_index = helpers::voter_index_fn::<T>(&cache);
let target_index = helpers::target_index_fn_linear::<T>(&targets);
let voter_at = helpers::voter_at_fn::<T>(&voters);
let target_at = helpers::target_at_fn::<T>(&targets);
let stake_of = helpers::stake_of_fn::<T>(&voters, &cache);
let ElectionResult { assignments, winners } = election_result;
// convert to staked and reduce.
let mut staked = assignment_ratio_to_staked_normalized(assignments, &stake_of)
.map_err::<MinerError, _>(Into::into)?;
sp_npos_elections::reduce(&mut staked);
// convert back to ration and make compact.
let ratio = assignment_staked_to_ratio_normalized(staked)?;
let compact = <CompactOf<T>>::from_assignment(ratio, &voter_index, &target_index)?;
let size =
SolutionOrSnapshotSize { voters: voters.len() as u32, targets: targets.len() as u32 };
let maximum_allowed_voters = Self::maximum_voter_for_weight::<T::WeightInfo>(
desired_targets,
size,
T::MinerMaxWeight::get(),
);
log!(
debug,
"miner: current compact solution voters = {}, maximum_allowed = {}",
compact.voter_count(),
maximum_allowed_voters,
);
let compact = Self::trim_compact(maximum_allowed_voters, compact, &voter_index)?;
// re-calc score.
let winners = sp_npos_elections::to_without_backing(winners);
let score = compact.clone().score(&winners, stake_of, voter_at, target_at)?;
let round = Self::round();
Ok((RawSolution { compact, score, round }, size))
}
/// Get a random number of iterations to run the balancing in the OCW.
///
/// Uses the offchain seed to generate a random number, maxed with
/// [`Config::MinerMaxIterations`].
pub fn get_balancing_iters() -> usize {
match T::MinerMaxIterations::get() {
0 => 0,
max @ _ => {
let seed = sp_io::offchain::random_seed();
let random = <u32>::decode(&mut TrailingZeroInput::new(seed.as_ref()))
.expect("input is padded with zeroes; qed")
% max.saturating_add(1);
random as usize
}
}
}
/// Greedily reduce the size of the a solution to fit into the block, w.r.t. weight.
///
/// The weight of the solution is foremost a function of the number of voters (i.e.
/// `compact.len()`). Aside from this, the other components of the weight are invariant. The
/// number of winners shall not be changed (otherwise the solution is invalid) and the
/// `ElectionSize` is merely a representation of the total number of stakers.
///
/// Thus, we reside to stripping away some voters. This means only changing the `compact`
/// struct.
///
/// Note that the solution is already computed, and the winners are elected based on the merit
/// of the entire stake in the system. Nonetheless, some of the voters will be removed further
/// down the line.
///
/// Indeed, the score must be computed **after** this step. If this step reduces the score too
/// much or remove a winner, then the solution must be discarded **after** this step.
pub fn trim_compact<FN>(
maximum_allowed_voters: u32,
mut compact: CompactOf<T>,
voter_index: FN,
) -> Result<CompactOf<T>, MinerError>
where
for<'r> FN: Fn(&'r T::AccountId) -> Option<CompactVoterIndexOf<T>>,
{
match compact.voter_count().checked_sub(maximum_allowed_voters as usize) {
Some(to_remove) if to_remove > 0 => {
// grab all voters and sort them by least stake.
let RoundSnapshot { voters, .. } =
Self::snapshot().ok_or(MinerError::SnapshotUnAvailable)?;
let mut voters_sorted = voters
.into_iter()
.map(|(who, stake, _)| (who.clone(), stake))
.collect::<Vec<_>>();
voters_sorted.sort_by_key(|(_, y)| *y);
// start removing from the least stake. Iterate until we know enough have been
// removed.
let mut removed = 0;
for (maybe_index, _stake) in
voters_sorted.iter().map(|(who, stake)| (voter_index(&who), stake))
{
let index = maybe_index.ok_or(MinerError::SnapshotUnAvailable)?;
if compact.remove_voter(index) {
removed += 1
}
if removed >= to_remove {
break;
}
}
Ok(compact)
}
_ => {
// nada, return as-is
Ok(compact)
}
}
}
/// Find the maximum `len` that a compact can have in order to fit into the block weight.
///
/// This only returns a value between zero and `size.nominators`.
pub fn maximum_voter_for_weight<W: WeightInfo>(
desired_winners: u32,
size: SolutionOrSnapshotSize,
max_weight: Weight,
) -> u32 {
if size.voters < 1 {
return size.voters;
}
let max_voters = size.voters.max(1);
let mut voters = max_voters;
// helper closures.
let weight_with = |active_voters: u32| -> Weight {
W::submit_unsigned(size.voters, size.targets, active_voters, desired_winners)
};
let next_voters = |current_weight: Weight, voters: u32, step: u32| -> Result<u32, ()> {
match current_weight.cmp(&max_weight) {
Ordering::Less => {
let next_voters = voters.checked_add(step);
match next_voters {
Some(voters) if voters < max_voters => Ok(voters),
_ => Err(()),
}
}
Ordering::Greater => voters.checked_sub(step).ok_or(()),
Ordering::Equal => Ok(voters),
}
};
// First binary-search the right amount of voters
let mut step = voters / 2;
let mut current_weight = weight_with(voters);
while step > 0 {
match next_voters(current_weight, voters, step) {
// proceed with the binary search
Ok(next) if next != voters => {
voters = next;
}
// we are out of bounds, break out of the loop.
Err(()) => {
break;
}
// we found the right value - early exit the function.
Ok(next) => return next,
}
step = step / 2;
current_weight = weight_with(voters);
}
// Time to finish. We might have reduced less than expected due to rounding error. Increase
// one last time if we have any room left, the reduce until we are sure we are below limit.
while voters + 1 <= max_voters && weight_with(voters + 1) < max_weight {
voters += 1;
}
while voters.checked_sub(1).is_some() && weight_with(voters) > max_weight {
voters -= 1;
}
debug_assert!(
weight_with(voters.min(size.voters)) <= max_weight,
"weight_with({}) <= {}",
voters.min(size.voters),
max_weight,
);
voters.min(size.voters)
}
/// Checks if an execution of the offchain worker is permitted at the given block number, or
/// not.
///
/// This essentially makes sure that we don't run on previous blocks in case of a re-org, and we
/// don't run twice within a window of length [`OFFCHAIN_REPEAT`].
///
/// Returns `Ok(())` if offchain worker should happen, `Err(reason)` otherwise.
pub(crate) fn try_acquire_offchain_lock(now: T::BlockNumber) -> Result<(), &'static str> {
let storage = StorageValueRef::persistent(&OFFCHAIN_HEAD_DB);
let threshold = T::BlockNumber::from(OFFCHAIN_REPEAT);
let mutate_stat =
storage.mutate::<_, &'static str, _>(|maybe_head: Option<Option<T::BlockNumber>>| {
match maybe_head {
Some(Some(head)) if now < head => Err("fork."),
Some(Some(head)) if now >= head && now <= head + threshold => {
Err("recently executed.")
}
Some(Some(head)) if now > head + threshold => {
// we can run again now. Write the new head.
Ok(now)
}
_ => {
// value doesn't exists. Probably this node just booted up. Write, and run
Ok(now)
}
}
});
match mutate_stat {
// all good
Ok(Ok(_)) => Ok(()),
// failed to write.
Ok(Err(_)) => Err("failed to write to offchain db."),
// fork etc.
Err(why) => Err(why),
}
}
/// Do the basics checks that MUST happen during the validation and pre-dispatch of an unsigned
/// transaction.
///
/// Can optionally also be called during dispatch, if needed.
///
/// NOTE: Ideally, these tests should move more and more outside of this and more to the miner's
/// code, so that we do less and less storage reads here.
pub(crate) fn unsigned_pre_dispatch_checks(
solution: &RawSolution<CompactOf<T>>,
) -> DispatchResult {
// ensure solution is timely. Don't panic yet. This is a cheap check.
ensure!(Self::current_phase().is_unsigned_open(), Error::<T>::PreDispatchEarlySubmission);
// ensure correct number of winners.
ensure!(
Self::desired_targets().unwrap_or_default()
== solution.compact.unique_targets().len() as u32,
Error::<T>::PreDispatchWrongWinnerCount,
);
// ensure score is being improved. Panic henceforth.
ensure!(
Self::queued_solution().map_or(true, |q: ReadySolution<_>| is_score_better::<Perbill>(
solution.score,
q.score,
T::SolutionImprovementThreshold::get()
)),
Error::<T>::PreDispatchWeakSubmission,
);
Ok(())
}
}
#[cfg(test)]
mod max_weight {
#![allow(unused_variables)]
use super::{mock::*, *};
struct TestWeight;
impl crate::weights::WeightInfo for TestWeight {
fn on_initialize_nothing() -> Weight {
unreachable!()
}
fn on_initialize_open_signed() -> Weight {
unreachable!()
}
fn on_initialize_open_unsigned_with_snapshot() -> Weight {
unreachable!()
}
fn on_initialize_open_unsigned_without_snapshot() -> Weight {
unreachable!()
}
fn submit_unsigned(v: u32, t: u32, a: u32, d: u32) -> Weight {
(0 * v + 0 * t + 1000 * a + 0 * d) as Weight
}
fn feasibility_check(v: u32, _t: u32, a: u32, d: u32) -> Weight {
unreachable!()
}
}
#[test]
fn find_max_voter_binary_search_works() {
let w = SolutionOrSnapshotSize { voters: 10, targets: 0 };
assert_eq!(MultiPhase::maximum_voter_for_weight::<TestWeight>(0, w, 0), 0);
assert_eq!(MultiPhase::maximum_voter_for_weight::<TestWeight>(0, w, 1), 0);
assert_eq!(MultiPhase::maximum_voter_for_weight::<TestWeight>(0, w, 999), 0);
assert_eq!(MultiPhase::maximum_voter_for_weight::<TestWeight>(0, w, 1000), 1);
assert_eq!(MultiPhase::maximum_voter_for_weight::<TestWeight>(0, w, 1001), 1);
assert_eq!(MultiPhase::maximum_voter_for_weight::<TestWeight>(0, w, 1990), 1);
assert_eq!(MultiPhase::maximum_voter_for_weight::<TestWeight>(0, w, 1999), 1);
assert_eq!(MultiPhase::maximum_voter_for_weight::<TestWeight>(0, w, 2000), 2);
assert_eq!(MultiPhase::maximum_voter_for_weight::<TestWeight>(0, w, 2001), 2);
assert_eq!(MultiPhase::maximum_voter_for_weight::<TestWeight>(0, w, 2010), 2);
assert_eq!(MultiPhase::maximum_voter_for_weight::<TestWeight>(0, w, 2990), 2);
assert_eq!(MultiPhase::maximum_voter_for_weight::<TestWeight>(0, w, 2999), 2);
assert_eq!(MultiPhase::maximum_voter_for_weight::<TestWeight>(0, w, 3000), 3);
assert_eq!(MultiPhase::maximum_voter_for_weight::<TestWeight>(0, w, 3333), 3);
assert_eq!(MultiPhase::maximum_voter_for_weight::<TestWeight>(0, w, 5500), 5);
assert_eq!(MultiPhase::maximum_voter_for_weight::<TestWeight>(0, w, 7777), 7);
assert_eq!(MultiPhase::maximum_voter_for_weight::<TestWeight>(0, w, 9999), 9);
assert_eq!(MultiPhase::maximum_voter_for_weight::<TestWeight>(0, w, 10_000), 10);
assert_eq!(MultiPhase::maximum_voter_for_weight::<TestWeight>(0, w, 10_999), 10);
assert_eq!(MultiPhase::maximum_voter_for_weight::<TestWeight>(0, w, 11_000), 10);
assert_eq!(MultiPhase::maximum_voter_for_weight::<TestWeight>(0, w, 22_000), 10);
let w = SolutionOrSnapshotSize { voters: 1, targets: 0 };
assert_eq!(MultiPhase::maximum_voter_for_weight::<TestWeight>(0, w, 0), 0);
assert_eq!(MultiPhase::maximum_voter_for_weight::<TestWeight>(0, w, 1), 0);
assert_eq!(MultiPhase::maximum_voter_for_weight::<TestWeight>(0, w, 999), 0);
assert_eq!(MultiPhase::maximum_voter_for_weight::<TestWeight>(0, w, 1000), 1);
assert_eq!(MultiPhase::maximum_voter_for_weight::<TestWeight>(0, w, 1001), 1);
assert_eq!(MultiPhase::maximum_voter_for_weight::<TestWeight>(0, w, 1990), 1);
assert_eq!(MultiPhase::maximum_voter_for_weight::<TestWeight>(0, w, 1999), 1);
assert_eq!(MultiPhase::maximum_voter_for_weight::<TestWeight>(0, w, 2000), 1);
assert_eq!(MultiPhase::maximum_voter_for_weight::<TestWeight>(0, w, 2001), 1);
assert_eq!(MultiPhase::maximum_voter_for_weight::<TestWeight>(0, w, 2010), 1);
assert_eq!(MultiPhase::maximum_voter_for_weight::<TestWeight>(0, w, 3333), 1);
let w = SolutionOrSnapshotSize { voters: 2, targets: 0 };
assert_eq!(MultiPhase::maximum_voter_for_weight::<TestWeight>(0, w, 0), 0);
assert_eq!(MultiPhase::maximum_voter_for_weight::<TestWeight>(0, w, 1), 0);
assert_eq!(MultiPhase::maximum_voter_for_weight::<TestWeight>(0, w, 999), 0);
assert_eq!(MultiPhase::maximum_voter_for_weight::<TestWeight>(0, w, 1000), 1);
assert_eq!(MultiPhase::maximum_voter_for_weight::<TestWeight>(0, w, 1001), 1);
assert_eq!(MultiPhase::maximum_voter_for_weight::<TestWeight>(0, w, 1999), 1);
assert_eq!(MultiPhase::maximum_voter_for_weight::<TestWeight>(0, w, 2000), 2);
assert_eq!(MultiPhase::maximum_voter_for_weight::<TestWeight>(0, w, 2001), 2);
assert_eq!(MultiPhase::maximum_voter_for_weight::<TestWeight>(0, w, 2010), 2);
assert_eq!(MultiPhase::maximum_voter_for_weight::<TestWeight>(0, w, 3333), 2);
}
}
#[cfg(test)]
mod tests {
use super::{
mock::{Origin, *},
Call, *,
};
use frame_support::{dispatch::Dispatchable, traits::OffchainWorker};
use mock::Call as OuterCall;
use sp_election_providers::Assignment;
use sp_runtime::{traits::ValidateUnsigned, PerU16};
#[test]
fn validate_unsigned_retracts_wrong_phase() {
ExtBuilder::default().desired_targets(0).build_and_execute(|| {
let solution = RawSolution::<TestCompact> { score: [5, 0, 0], ..Default::default() };
let call = Call::submit_unsigned(solution.clone(), witness());
// initial
assert_eq!(MultiPhase::current_phase(), Phase::Off);
assert!(matches!(
<MultiPhase as ValidateUnsigned>::validate_unsigned(TransactionSource::Local, &call)
.unwrap_err(),
TransactionValidityError::Invalid(InvalidTransaction::Custom(0))
));
assert!(matches!(
<MultiPhase as ValidateUnsigned>::pre_dispatch(&call).unwrap_err(),
TransactionValidityError::Invalid(InvalidTransaction::Custom(0))
));
// signed
roll_to(15);
assert_eq!(MultiPhase::current_phase(), Phase::Signed);
assert!(matches!(
<MultiPhase as ValidateUnsigned>::validate_unsigned(TransactionSource::Local, &call)
.unwrap_err(),
TransactionValidityError::Invalid(InvalidTransaction::Custom(0))
));
assert!(matches!(
<MultiPhase as ValidateUnsigned>::pre_dispatch(&call).unwrap_err(),
TransactionValidityError::Invalid(InvalidTransaction::Custom(0))
));
// unsigned
roll_to(25);
assert!(MultiPhase::current_phase().is_unsigned());
assert!(<MultiPhase as ValidateUnsigned>::validate_unsigned(
TransactionSource::Local,
&call
)
.is_ok());
assert!(<MultiPhase as ValidateUnsigned>::pre_dispatch(&call).is_ok());
// unsigned -- but not enabled.
<CurrentPhase<Runtime>>::put(Phase::Unsigned((false, 25)));
assert!(MultiPhase::current_phase().is_unsigned());
assert!(matches!(
<MultiPhase as ValidateUnsigned>::validate_unsigned(TransactionSource::Local, &call)
.unwrap_err(),
TransactionValidityError::Invalid(InvalidTransaction::Custom(0))
));
assert!(matches!(
<MultiPhase as ValidateUnsigned>::pre_dispatch(&call).unwrap_err(),
TransactionValidityError::Invalid(InvalidTransaction::Custom(0))
));
})
}
#[test]
fn validate_unsigned_retracts_low_score() {
ExtBuilder::default().desired_targets(0).build_and_execute(|| {
roll_to(25);
assert!(MultiPhase::current_phase().is_unsigned());
let solution = RawSolution::<TestCompact> { score: [5, 0, 0], ..Default::default() };
let call = Call::submit_unsigned(solution.clone(), witness());
// initial
assert!(<MultiPhase as ValidateUnsigned>::validate_unsigned(
TransactionSource::Local,
&call
)
.is_ok());
assert!(<MultiPhase as ValidateUnsigned>::pre_dispatch(&call).is_ok());
// set a better score
let ready = ReadySolution { score: [10, 0, 0], ..Default::default() };
<QueuedSolution<Runtime>>::put(ready);
// won't work anymore.
assert!(matches!(
<MultiPhase as ValidateUnsigned>::validate_unsigned(
TransactionSource::Local,
&call
)
.unwrap_err(),
TransactionValidityError::Invalid(InvalidTransaction::Custom(2))
));
assert!(matches!(
<MultiPhase as ValidateUnsigned>::pre_dispatch(&call).unwrap_err(),
TransactionValidityError::Invalid(InvalidTransaction::Custom(2))
));
})
}
#[test]
fn validate_unsigned_retracts_incorrect_winner_count() {
ExtBuilder::default().desired_targets(1).build_and_execute(|| {
roll_to(25);
assert!(MultiPhase::current_phase().is_unsigned());
let solution = RawSolution::<TestCompact> { score: [5, 0, 0], ..Default::default() };
let call = Call::submit_unsigned(solution.clone(), witness());
assert_eq!(solution.compact.unique_targets().len(), 0);
// won't work anymore.
assert!(matches!(
<MultiPhase as ValidateUnsigned>::validate_unsigned(
TransactionSource::Local,
&call
)
.unwrap_err(),
TransactionValidityError::Invalid(InvalidTransaction::Custom(1))
));
})
}
#[test]
fn priority_is_set() {
ExtBuilder::default().miner_tx_priority(20).desired_targets(0).build_and_execute(|| {
roll_to(25);
assert!(MultiPhase::current_phase().is_unsigned());
let solution = RawSolution::<TestCompact> { score: [5, 0, 0], ..Default::default() };
let call = Call::submit_unsigned(solution.clone(), witness());
assert_eq!(
<MultiPhase as ValidateUnsigned>::validate_unsigned(
TransactionSource::Local,
&call
)
.unwrap()
.priority,
25
);
})
}
#[test]
#[should_panic(expected = "Invalid unsigned submission must produce invalid block and \
deprive validator from their authoring reward.: \
DispatchError::Module { index: 2, error: 1, message: \
Some(\"PreDispatchWrongWinnerCount\") }")]
fn unfeasible_solution_panics() {
ExtBuilder::default().build_and_execute(|| {
roll_to(25);
assert!(MultiPhase::current_phase().is_unsigned());
// This is in itself an invalid BS solution.
let solution = RawSolution::<TestCompact> { score: [5, 0, 0], ..Default::default() };
let call = Call::submit_unsigned(solution.clone(), witness());
let outer_call: OuterCall = call.into();
let _ = outer_call.dispatch(Origin::none());
})
}
#[test]
#[should_panic(expected = "Invalid unsigned submission must produce invalid block and \
deprive validator from their authoring reward.")]
fn wrong_witness_panics() {
ExtBuilder::default().build_and_execute(|| {
roll_to(25);
assert!(MultiPhase::current_phase().is_unsigned());
// This solution is unfeasible as well, but we won't even get there.
let solution = RawSolution::<TestCompact> { score: [5, 0, 0], ..Default::default() };
let mut correct_witness = witness();
correct_witness.voters += 1;
correct_witness.targets -= 1;
let call = Call::submit_unsigned(solution.clone(), correct_witness);
let outer_call: OuterCall = call.into();
let _ = outer_call.dispatch(Origin::none());
})
}
#[test]
fn miner_works() {
ExtBuilder::default().build_and_execute(|| {
roll_to(25);
assert!(MultiPhase::current_phase().is_unsigned());
// ensure we have snapshots in place.
assert!(MultiPhase::snapshot().is_some());
assert_eq!(MultiPhase::desired_targets().unwrap(), 2);
// mine seq_phragmen solution with 2 iters.
let (solution, witness) = MultiPhase::mine_solution(2).unwrap();
// ensure this solution is valid.
assert!(MultiPhase::queued_solution().is_none());
assert_ok!(MultiPhase::submit_unsigned(Origin::none(), solution, witness));
assert!(MultiPhase::queued_solution().is_some());
})
}
#[test]
fn miner_trims_weight() {
ExtBuilder::default().miner_weight(100).mock_weight_info(true).build_and_execute(|| {
roll_to(25);
assert!(MultiPhase::current_phase().is_unsigned());
let (solution, witness) = MultiPhase::mine_solution(2).unwrap();
let solution_weight = <Runtime as Config>::WeightInfo::submit_unsigned(
witness.voters,
witness.targets,
solution.compact.voter_count() as u32,
solution.compact.unique_targets().len() as u32,
);
// default solution will have 5 edges (5 * 5 + 10)
assert_eq!(solution_weight, 35);
assert_eq!(solution.compact.voter_count(), 5);
// now reduce the max weight
<MinerMaxWeight>::set(25);
let (solution, witness) = MultiPhase::mine_solution(2).unwrap();
let solution_weight = <Runtime as Config>::WeightInfo::submit_unsigned(
witness.voters,
witness.targets,
solution.compact.voter_count() as u32,
solution.compact.unique_targets().len() as u32,
);
// default solution will have 5 edges (5 * 5 + 10)
assert_eq!(solution_weight, 25);
assert_eq!(solution.compact.voter_count(), 3);
})
}
#[test]
fn miner_will_not_submit_if_not_enough_winners() {
let (mut ext, _) = ExtBuilder::default().desired_targets(8).build_offchainify(0);
ext.execute_with(|| {
roll_to(25);
assert!(MultiPhase::current_phase().is_unsigned());
// mine seq_phragmen solution with 2 iters.
assert_eq!(
MultiPhase::mine_check_and_submit().unwrap_err(),
MinerError::PreDispatchChecksFailed,
);
})
}
#[test]
fn unsigned_per_dispatch_checks_can_only_submit_threshold_better() {
ExtBuilder::default()
.desired_targets(1)
.add_voter(7, 2, vec![10])
.add_voter(8, 5, vec![10])
.solution_improvement_threshold(Perbill::from_percent(50))
.build_and_execute(|| {
roll_to(25);
assert!(MultiPhase::current_phase().is_unsigned());
assert_eq!(MultiPhase::desired_targets().unwrap(), 1);
// an initial solution
let result = ElectionResult {
// note: This second element of backing stake is not important here.
winners: vec![(10, 10)],
assignments: vec![Assignment {
who: 10,
distribution: vec![(10, PerU16::one())],
}],
};
let (solution, witness) = MultiPhase::prepare_election_result(result).unwrap();
assert_ok!(MultiPhase::unsigned_pre_dispatch_checks(&solution));
assert_ok!(MultiPhase::submit_unsigned(Origin::none(), solution, witness));
assert_eq!(MultiPhase::queued_solution().unwrap().score[0], 10);
// trial 1: a solution who's score is only 2, i.e. 20% better in the first element.
let result = ElectionResult {
winners: vec![(10, 12)],
assignments: vec![
Assignment { who: 10, distribution: vec![(10, PerU16::one())] },
Assignment {
who: 7,
// note: this percent doesn't even matter, in compact it is 100%.
distribution: vec![(10, PerU16::one())],
},
],
};
let (solution, _) = MultiPhase::prepare_election_result(result).unwrap();
// 12 is not 50% more than 10
assert_eq!(solution.score[0], 12);
assert_noop!(
MultiPhase::unsigned_pre_dispatch_checks(&solution),
Error::<Runtime>::PreDispatchWeakSubmission,
);
// submitting this will actually panic.
// trial 2: a solution who's score is only 7, i.e. 70% better in the first element.
let result = ElectionResult {
winners: vec![(10, 12)],
assignments: vec![
Assignment { who: 10, distribution: vec![(10, PerU16::one())] },
Assignment { who: 7, distribution: vec![(10, PerU16::one())] },
Assignment {
who: 8,
// note: this percent doesn't even matter, in compact it is 100%.
distribution: vec![(10, PerU16::one())],
},
],
};
let (solution, witness) = MultiPhase::prepare_election_result(result).unwrap();
assert_eq!(solution.score[0], 17);
// and it is fine
assert_ok!(MultiPhase::unsigned_pre_dispatch_checks(&solution));
assert_ok!(MultiPhase::submit_unsigned(Origin::none(), solution, witness));
})
}
#[test]
fn ocw_check_prevent_duplicate() {
let (mut ext, _) = ExtBuilder::default().build_offchainify(0);
ext.execute_with(|| {
roll_to(25);
assert!(MultiPhase::current_phase().is_unsigned());
// first execution -- okay.
assert!(MultiPhase::try_acquire_offchain_lock(25).is_ok());
// next block: rejected.
assert!(MultiPhase::try_acquire_offchain_lock(26).is_err());
// allowed after `OFFCHAIN_REPEAT`
assert!(MultiPhase::try_acquire_offchain_lock((26 + OFFCHAIN_REPEAT).into()).is_ok());
// a fork like situation: re-execute last 3.
assert!(
MultiPhase::try_acquire_offchain_lock((26 + OFFCHAIN_REPEAT - 3).into()).is_err()
);
assert!(
MultiPhase::try_acquire_offchain_lock((26 + OFFCHAIN_REPEAT - 2).into()).is_err()
);
assert!(
MultiPhase::try_acquire_offchain_lock((26 + OFFCHAIN_REPEAT - 1).into()).is_err()
);
})
}
#[test]
fn ocw_only_runs_when_signed_open_now() {
let (mut ext, pool) = ExtBuilder::default().build_offchainify(0);
ext.execute_with(|| {
roll_to(25);
assert_eq!(MultiPhase::current_phase(), Phase::Unsigned((true, 25)));
// we must clear the offchain storage to ensure the offchain execution check doesn't get
// in the way.
let mut storage = StorageValueRef::persistent(&OFFCHAIN_HEAD_DB);
MultiPhase::offchain_worker(24);
assert!(pool.read().transactions.len().is_zero());
storage.clear();
MultiPhase::offchain_worker(26);
assert!(pool.read().transactions.len().is_zero());
storage.clear();
// submits!
MultiPhase::offchain_worker(25);
assert!(!pool.read().transactions.len().is_zero());
})
}
#[test]
fn ocw_can_submit_to_pool() {
let (mut ext, pool) = ExtBuilder::default().build_offchainify(0);
ext.execute_with(|| {
roll_to_with_ocw(25);
assert_eq!(MultiPhase::current_phase(), Phase::Unsigned((true, 25)));
// OCW must have submitted now
let encoded = pool.read().transactions[0].clone();
let extrinsic: Extrinsic = Decode::decode(&mut &*encoded).unwrap();
let call = extrinsic.call;
assert!(matches!(call, OuterCall::MultiPhase(Call::submit_unsigned(_, _))));
})
}
}
@@ -0,0 +1,150 @@
// This file is part of Substrate.
// Copyright (C) 2021 Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Autogenerated weights for pallet_election_provider_multi_phase
//!
//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 3.0.0
//! DATE: 2021-02-12, STEPS: [50, ], REPEAT: 20, LOW RANGE: [], HIGH RANGE: []
//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 128
// Executed Command:
// target/release/substrate
// benchmark
// --chain=dev
// --steps=50
// --repeat=20
// --pallet=pallet_election_provider_multi_phase
// --extrinsic=*
// --execution=wasm
// --wasm-execution=compiled
// --heap-pages=4096
// --output=./frame/election-provider-multi-phase/src/weights.rs
// --template=./.maintain/frame-weight-template.hbs
#![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_election_provider_multi_phase.
pub trait WeightInfo {
fn on_initialize_nothing() -> Weight;
fn on_initialize_open_signed() -> Weight;
fn on_initialize_open_unsigned_with_snapshot() -> Weight;
fn on_initialize_open_unsigned_without_snapshot() -> Weight;
fn submit_unsigned(v: u32, t: u32, a: u32, d: u32, ) -> Weight;
fn feasibility_check(v: u32, t: u32, a: u32, d: u32, ) -> Weight;
}
/// Weights for pallet_election_provider_multi_phase using the Substrate node and recommended hardware.
pub struct SubstrateWeight<T>(PhantomData<T>);
impl<T: frame_system::Config> WeightInfo for SubstrateWeight<T> {
fn on_initialize_nothing() -> Weight {
(23_401_000 as Weight)
.saturating_add(T::DbWeight::get().reads(7 as Weight))
}
fn on_initialize_open_signed() -> Weight {
(79_260_000 as Weight)
.saturating_add(T::DbWeight::get().reads(7 as Weight))
.saturating_add(T::DbWeight::get().writes(4 as Weight))
}
fn on_initialize_open_unsigned_with_snapshot() -> Weight {
(77_745_000 as Weight)
.saturating_add(T::DbWeight::get().reads(7 as Weight))
.saturating_add(T::DbWeight::get().writes(4 as Weight))
}
fn on_initialize_open_unsigned_without_snapshot() -> Weight {
(21_764_000 as Weight)
.saturating_add(T::DbWeight::get().reads(1 as Weight))
.saturating_add(T::DbWeight::get().writes(1 as Weight))
}
fn submit_unsigned(v: u32, t: u32, a: u32, d: u32, ) -> Weight {
(0 as Weight)
// Standard Error: 23_000
.saturating_add((4_171_000 as Weight).saturating_mul(v as Weight))
// Standard Error: 78_000
.saturating_add((229_000 as Weight).saturating_mul(t as Weight))
// Standard Error: 23_000
.saturating_add((13_661_000 as Weight).saturating_mul(a as Weight))
// Standard Error: 117_000
.saturating_add((4_499_000 as Weight).saturating_mul(d as Weight))
.saturating_add(T::DbWeight::get().reads(6 as Weight))
.saturating_add(T::DbWeight::get().writes(1 as Weight))
}
fn feasibility_check(v: u32, t: u32, a: u32, d: u32, ) -> Weight {
(0 as Weight)
// Standard Error: 12_000
.saturating_add((4_232_000 as Weight).saturating_mul(v as Weight))
// Standard Error: 42_000
.saturating_add((636_000 as Weight).saturating_mul(t as Weight))
// Standard Error: 12_000
.saturating_add((10_294_000 as Weight).saturating_mul(a as Weight))
// Standard Error: 64_000
.saturating_add((4_428_000 as Weight).saturating_mul(d as Weight))
.saturating_add(T::DbWeight::get().reads(3 as Weight))
}
}
// For backwards compatibility and tests
impl WeightInfo for () {
fn on_initialize_nothing() -> Weight {
(23_401_000 as Weight)
.saturating_add(RocksDbWeight::get().reads(7 as Weight))
}
fn on_initialize_open_signed() -> Weight {
(79_260_000 as Weight)
.saturating_add(RocksDbWeight::get().reads(7 as Weight))
.saturating_add(RocksDbWeight::get().writes(4 as Weight))
}
fn on_initialize_open_unsigned_with_snapshot() -> Weight {
(77_745_000 as Weight)
.saturating_add(RocksDbWeight::get().reads(7 as Weight))
.saturating_add(RocksDbWeight::get().writes(4 as Weight))
}
fn on_initialize_open_unsigned_without_snapshot() -> Weight {
(21_764_000 as Weight)
.saturating_add(RocksDbWeight::get().reads(1 as Weight))
.saturating_add(RocksDbWeight::get().writes(1 as Weight))
}
fn submit_unsigned(v: u32, t: u32, a: u32, d: u32, ) -> Weight {
(0 as Weight)
// Standard Error: 23_000
.saturating_add((4_171_000 as Weight).saturating_mul(v as Weight))
// Standard Error: 78_000
.saturating_add((229_000 as Weight).saturating_mul(t as Weight))
// Standard Error: 23_000
.saturating_add((13_661_000 as Weight).saturating_mul(a as Weight))
// Standard Error: 117_000
.saturating_add((4_499_000 as Weight).saturating_mul(d as Weight))
.saturating_add(RocksDbWeight::get().reads(6 as Weight))
.saturating_add(RocksDbWeight::get().writes(1 as Weight))
}
fn feasibility_check(v: u32, t: u32, a: u32, d: u32, ) -> Weight {
(0 as Weight)
// Standard Error: 12_000
.saturating_add((4_232_000 as Weight).saturating_mul(v as Weight))
// Standard Error: 42_000
.saturating_add((636_000 as Weight).saturating_mul(t as Weight))
// Standard Error: 12_000
.saturating_add((10_294_000 as Weight).saturating_mul(a as Weight))
// Standard Error: 64_000
.saturating_add((4_428_000 as Weight).saturating_mul(d as Weight))
.saturating_add(RocksDbWeight::get().reads(3 as Weight))
}
}
+1
View File
@@ -38,6 +38,7 @@ pallet-offences = { version = "3.0.0", path = "../offences" }
pallet-staking = { version = "3.0.0", path = "../staking" }
pallet-staking-reward-curve = { version = "3.0.0", path = "../staking/reward-curve" }
pallet-timestamp = { version = "3.0.0", path = "../timestamp" }
sp-election-providers = { version = "3.0.0", path = "../../primitives/election-providers" }
[features]
default = ["std"]
+9 -1
View File
@@ -30,7 +30,6 @@ use frame_support::{
use pallet_staking::EraIndex;
use sp_core::{crypto::KeyTypeId, H256};
use sp_finality_grandpa::{RoundNumber, SetId, GRANDPA_ENGINE_ID};
use sp_io;
use sp_keyring::Ed25519Keyring;
use sp_runtime::{
curve::PiecewiseLinear,
@@ -41,6 +40,7 @@ use sp_runtime::{
};
use sp_staking::SessionIndex;
use pallet_session::historical as pallet_session_historical;
use sp_election_providers::onchain;
type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic<Test>;
type Block = frame_system::mocking::MockBlock<Test>;
@@ -190,6 +190,13 @@ parameter_types! {
pub const StakingUnsignedPriority: u64 = u64::max_value() / 2;
}
impl onchain::Config for Test {
type AccountId = <Self as frame_system::Config>::AccountId;
type BlockNumber = <Self as frame_system::Config>::BlockNumber;
type Accuracy = Perbill;
type DataProvider = Staking;
}
impl pallet_staking::Config for Test {
type RewardRemainder = ();
type CurrencyToVote = frame_support::traits::SaturatingCurrencyToVote;
@@ -212,6 +219,7 @@ impl pallet_staking::Config for Test {
type MaxIterations = ();
type MinSolutionScoreBump = ();
type OffchainSolutionWeightLimit = ();
type ElectionProvider = onchain::OnChainSequentialPhragmen<Self>;
type WeightInfo = ();
}
@@ -27,6 +27,7 @@ pallet-staking = { version = "3.0.0", default-features = false, features = ["run
sp-runtime = { version = "3.0.0", default-features = false, path = "../../../primitives/runtime" }
sp-staking = { version = "3.0.0", default-features = false, path = "../../../primitives/staking" }
sp-std = { version = "3.0.0", default-features = false, path = "../../../primitives/std" }
sp-election-providers = { version = "3.0.0", default-features = false, path = "../../../primitives/election-providers" }
[dev-dependencies]
pallet-staking-reward-curve = { version = "3.0.0", path = "../../staking/reward-curve" }
@@ -50,6 +51,7 @@ std = [
"pallet-staking/std",
"sp-runtime/std",
"sp-staking/std",
"sp-election-providers/std",
"sp-std/std",
"codec/std",
]
@@ -29,6 +29,7 @@ use sp_runtime::{
traits::IdentityLookup,
testing::{Header, UintAuthorityId},
};
use sp_election_providers::onchain;
use pallet_session::historical as pallet_session_historical;
type AccountId = u64;
@@ -148,6 +149,13 @@ parameter_types! {
pub type Extrinsic = sp_runtime::testing::TestXt<Call, ()>;
impl onchain::Config for Test {
type AccountId = AccountId;
type BlockNumber = BlockNumber;
type Accuracy = Perbill;
type DataProvider = Staking;
}
impl pallet_staking::Config for Test {
type Currency = Balances;
type UnixTime = pallet_timestamp::Module<Self>;
@@ -170,6 +178,7 @@ impl pallet_staking::Config for Test {
type MaxIterations = ();
type MinSolutionScoreBump = ();
type OffchainSolutionWeightLimit = ();
type ElectionProvider = onchain::OnChainSequentialPhragmen<Self>;
type WeightInfo = ();
}
@@ -13,9 +13,9 @@ readme = "README.md"
targets = ["x86_64-unknown-linux-gnu"]
[dependencies]
sp-std = { version = "3.0.0", default-features = false, path = "../../../primitives/std" }
sp-session = { version = "3.0.0", default-features = false, path = "../../../primitives/session" }
sp-runtime = { version = "3.0.0", default-features = false, path = "../../../primitives/runtime" }
sp-std = { version = "3.0.0", default-features = false, path = "../../../primitives/std" }
frame-system = { version = "3.0.0", default-features = false, path = "../../system" }
frame-benchmarking = { version = "3.0.0", default-features = false, path = "../../benchmarking" }
frame-support = { version = "3.0.0", default-features = false, path = "../../support" }
@@ -31,12 +31,14 @@ pallet-staking-reward-curve = { version = "3.0.0", path = "../../staking/reward-
sp-io ={ version = "3.0.0", path = "../../../primitives/io" }
pallet-timestamp = { version = "3.0.0", path = "../../timestamp" }
pallet-balances = { version = "3.0.0", path = "../../balances" }
sp-election-providers = { version = "3.0.0", path = "../../../primitives/election-providers" }
[features]
default = ["std"]
std = [
"sp-std/std",
"sp-session/std",
"sp-election-providers/std",
"sp-runtime/std",
"frame-system/std",
"frame-benchmarking/std",
@@ -20,6 +20,7 @@
#![cfg(test)]
use sp_runtime::traits::IdentityLookup;
use sp_election_providers::onchain;
use frame_support::parameter_types;
type AccountId = u64;
@@ -145,13 +146,21 @@ parameter_types! {
pub type Extrinsic = sp_runtime::testing::TestXt<Call, ()>;
impl<C> frame_system::offchain::SendTransactionTypes<C> for Test where
impl<C> frame_system::offchain::SendTransactionTypes<C> for Test
where
Call: From<C>,
{
type OverarchingCall = Call;
type Extrinsic = Extrinsic;
}
impl onchain::Config for Test {
type AccountId = AccountId;
type BlockNumber = BlockNumber;
type Accuracy = sp_runtime::Perbill;
type DataProvider = Staking;
}
impl pallet_staking::Config for Test {
type Currency = Balances;
type UnixTime = pallet_timestamp::Module<Self>;
@@ -174,6 +183,7 @@ impl pallet_staking::Config for Test {
type MaxIterations = ();
type MinSolutionScoreBump = ();
type OffchainSolutionWeightLimit = ();
type ElectionProvider = onchain::OnChainSequentialPhragmen<Self>;
type WeightInfo = ();
}
+14 -4
View File
@@ -169,11 +169,13 @@ impl<
Some(if now > offset {
let block_after_last_session = (now.clone() - offset) % period.clone();
if block_after_last_session > Zero::zero() {
now.saturating_add(
period.saturating_sub(block_after_last_session)
)
now.saturating_add(period.saturating_sub(block_after_last_session))
} else {
now
// this branch happens when the session is already rotated or will rotate in this
// block (depending on being called before or after `session::on_initialize`). Here,
// we assume the latter, namely that this is called after `session::on_initialize`,
// and thus we add period to it as well.
now + period
}
} else {
offset
@@ -187,6 +189,10 @@ impl<
// reasonable to come back here and properly calculate the weight of this function.
0
}
fn average_session_length() -> BlockNumber {
Period::get()
}
}
/// A trait for managing creation of new validator set.
@@ -833,6 +839,10 @@ impl<T: Config> EstimateNextNewSession<T::BlockNumber> for Module<T> {
T::NextSessionRotation::estimate_next_session_rotation(now)
}
fn average_session_length() -> T::BlockNumber {
T::NextSessionRotation::average_session_length()
}
fn weight(now: T::BlockNumber) -> Weight {
T::NextSessionRotation::weight(now)
}
+1 -1
View File
@@ -275,7 +275,7 @@ fn periodic_session_works() {
}
assert!(P::should_end_session(13u64));
assert_eq!(P::estimate_next_session_rotation(13u64).unwrap(), 13);
assert_eq!(P::estimate_next_session_rotation(13u64).unwrap(), 23);
assert!(!P::should_end_session(14u64));
assert_eq!(P::estimate_next_session_rotation(14u64).unwrap(), 23);
+6 -1
View File
@@ -17,6 +17,7 @@ static_assertions = "1.1.0"
serde = { version = "1.0.101", optional = true }
codec = { package = "parity-scale-codec", version = "2.0.0", default-features = false, features = ["derive"] }
sp-std = { version = "3.0.0", default-features = false, path = "../../primitives/std" }
# TWO_PHASE_NOTE:: ideally we should be able to get rid of this.
sp-npos-elections = { version = "3.0.0", default-features = false, path = "../../primitives/npos-elections" }
sp-io ={ version = "3.0.0", default-features = false, path = "../../primitives/io" }
sp-runtime = { version = "3.0.0", default-features = false, path = "../../primitives/runtime" }
@@ -26,20 +27,22 @@ frame-system = { version = "3.0.0", default-features = false, path = "../system"
pallet-session = { version = "3.0.0", default-features = false, features = ["historical"], path = "../session" }
pallet-authorship = { version = "3.0.0", default-features = false, path = "../authorship" }
sp-application-crypto = { version = "3.0.0", default-features = false, path = "../../primitives/application-crypto" }
sp-election-providers = { version = "3.0.0", default-features = false, path = "../../primitives/election-providers" }
# Optional imports for benchmarking
frame-benchmarking = { version = "3.0.0", default-features = false, path = "../benchmarking", optional = true }
rand_chacha = { version = "0.2", default-features = false, optional = true }
[dev-dependencies]
sp-core = { version = "3.0.0", path = "../../primitives/core" }
sp-storage = { version = "3.0.0", path = "../../primitives/storage" }
sp-tracing = { version = "3.0.0", path = "../../primitives/tracing" }
sp-core = { version = "3.0.0", path = "../../primitives/core" }
pallet-balances = { version = "3.0.0", path = "../balances" }
pallet-timestamp = { version = "3.0.0", path = "../timestamp" }
pallet-staking-reward-curve = { version = "3.0.0", path = "../staking/reward-curve" }
substrate-test-utils = { version = "3.0.0", path = "../../test-utils" }
frame-benchmarking = { version = "3.0.0", path = "../benchmarking" }
sp-election-providers = { version = "3.0.0", features = ["runtime-benchmarks"], path = "../../primitives/election-providers" }
rand_chacha = { version = "0.2" }
parking_lot = "0.11.1"
hex = "0.4"
@@ -59,8 +62,10 @@ std = [
"frame-system/std",
"pallet-authorship/std",
"sp-application-crypto/std",
"sp-election-providers/std",
]
runtime-benchmarks = [
"frame-benchmarking",
"sp-election-providers/runtime-benchmarks",
"rand_chacha",
]
@@ -28,6 +28,7 @@ sp-io ={ version = "3.0.0", path = "../../../primitives/io" }
sp-core = { version = "3.0.0", path = "../../../primitives/core" }
sp-npos-elections = { version = "3.0.0", path = "../../../primitives/npos-elections" }
sp-runtime = { version = "3.0.0", path = "../../../primitives/runtime" }
sp-election-providers = { version = "3.0.0", path = "../../../primitives/election-providers" }
serde = "1.0.101"
[features]
+13 -1
View File
@@ -149,13 +149,24 @@ parameter_types! {
pub type Extrinsic = sp_runtime::testing::TestXt<Call, ()>;
impl<C> frame_system::offchain::SendTransactionTypes<C> for Test where
impl<C> frame_system::offchain::SendTransactionTypes<C> for Test
where
Call: From<C>,
{
type OverarchingCall = Call;
type Extrinsic = Extrinsic;
}
pub struct MockElectionProvider;
impl sp_election_providers::ElectionProvider<AccountId, BlockNumber> for MockElectionProvider {
type Error = ();
type DataProvider = pallet_staking::Module<Test>;
fn elect() -> Result<sp_npos_elections::Supports<AccountId>, Self::Error> {
Err(())
}
}
impl pallet_staking::Config for Test {
type Currency = Balances;
type UnixTime = pallet_timestamp::Module<Self>;
@@ -179,4 +190,5 @@ impl pallet_staking::Config for Test {
type UnsignedPriority = ();
type OffchainSolutionWeightLimit = ();
type WeightInfo = ();
type ElectionProvider = MockElectionProvider;
}
@@ -164,7 +164,7 @@ fn main() {
assert_eq!(
call.dispatch_bypass_filter(origin.into()).unwrap_err().error,
DispatchError::Module {
index: 0,
index: 2,
error: 16,
message: Some("OffchainElectionWeakSubmission"),
},
+275 -51
View File
@@ -328,15 +328,13 @@ use frame_system::{
};
use sp_npos_elections::{
ExtendedBalance, Assignment, ElectionScore, ElectionResult as PrimitiveElectionResult,
to_support_map, EvaluateSupport, seq_phragmen, generate_solution_type, is_score_better,
SupportMap, VoteWeight, CompactSolution, PerThing128,
to_supports, EvaluateSupport, seq_phragmen, generate_solution_type, is_score_better, Supports,
VoteWeight, CompactSolution, PerThing128,
};
use sp_election_providers::ElectionProvider;
pub use weights::WeightInfo;
const STAKING_ID: LockIdentifier = *b"staking ";
pub const MAX_UNLOCKING_CHUNKS: usize = 32;
pub const MAX_NOMINATIONS: usize = <CompactAssignments as CompactSolution>::LIMIT;
pub(crate) const LOG_TARGET: &'static str = "staking";
// syntactic sugar for logging.
@@ -345,7 +343,7 @@ macro_rules! log {
($level:tt, $patter:expr $(, $values:expr)* $(,)?) => {
frame_support::debug::$level!(
target: crate::LOG_TARGET,
$patter $(, $values)*
concat!("💸 ", $patter) $(, $values)*
)
};
}
@@ -365,6 +363,10 @@ static_assertions::const_assert!(size_of::<NominatorIndex>() <= size_of::<u32>()
/// Maximum number of stakers that can be stored in a snapshot.
pub(crate) const MAX_VALIDATORS: usize = ValidatorIndex::max_value() as usize;
pub(crate) const MAX_NOMINATORS: usize = NominatorIndex::max_value() as usize;
pub const MAX_NOMINATIONS: usize =
<CompactAssignments as sp_npos_elections::CompactSolution>::LIMIT;
pub const MAX_UNLOCKING_CHUNKS: usize = 32;
/// Counter for the number of eras that have passed.
pub type EraIndex = u32;
@@ -388,10 +390,12 @@ pub type OffchainAccuracy = PerU16;
pub type BalanceOf<T> =
<<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;
type PositiveImbalanceOf<T> =
<<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::PositiveImbalance;
type NegativeImbalanceOf<T> =
<<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::NegativeImbalance;
type PositiveImbalanceOf<T> = <<T as Config>::Currency as Currency<
<T as frame_system::Config>::AccountId,
>>::PositiveImbalance;
type NegativeImbalanceOf<T> = <<T as Config>::Currency as Currency<
<T as frame_system::Config>::AccountId,
>>::NegativeImbalance;
/// Information regarding the active era (era in used in session).
#[derive(Encode, Decode, RuntimeDebug)]
@@ -778,7 +782,7 @@ impl<T: Config> SessionInterface<<T as frame_system::Config>::AccountId> for T w
pub trait Config: frame_system::Config + SendTransactionTypes<Call<Self>> {
/// The staking balance.
type Currency: LockableCurrency<Self::AccountId, Moment=Self::BlockNumber>;
type Currency: LockableCurrency<Self::AccountId, Moment = Self::BlockNumber>;
/// Time used for computing era duration.
///
@@ -793,6 +797,14 @@ pub trait Config: frame_system::Config + SendTransactionTypes<Call<Self>> {
/// [`BalanceOf`].
type CurrencyToVote: CurrencyToVote<BalanceOf<Self>>;
/// Something that provides the election functionality.
type ElectionProvider: sp_election_providers::ElectionProvider<
Self::AccountId,
Self::BlockNumber,
// we only accept an election provider that has staking as data provider.
DataProvider = Module<Self>,
>;
/// Tokens have been minted and are unused for validator-reward.
/// See [Era payout](./index.html#era-payout).
type RewardRemainder: OnUnbalanced<NegativeImbalanceOf<Self>>;
@@ -889,7 +901,9 @@ pub enum Forcing {
}
impl Default for Forcing {
fn default() -> Self { Forcing::NotForcing }
fn default() -> Self {
Forcing::NotForcing
}
}
// A value placed in storage that represents the current version of the Staking storage. This value
@@ -1066,28 +1080,45 @@ decl_storage! {
/// The earliest era for which we have a pending, unapplied slash.
EarliestUnappliedSlash: Option<EraIndex>;
/// The last planned session scheduled by the session pallet.
///
/// This is basically in sync with the call to [`SessionManager::new_session`].
pub CurrentPlannedSession get(fn current_planned_session): SessionIndex;
/// Snapshot of validators at the beginning of the current election window. This should only
/// have a value when [`EraElectionStatus`] == `ElectionStatus::Open(_)`.
///
/// TWO_PHASE_NOTE: should be removed once we switch to multi-phase.
pub SnapshotValidators get(fn snapshot_validators): Option<Vec<T::AccountId>>;
/// Snapshot of nominators at the beginning of the current election window. This should only
/// have a value when [`EraElectionStatus`] == `ElectionStatus::Open(_)`.
///
/// TWO_PHASE_NOTE: should be removed once we switch to multi-phase.
pub SnapshotNominators get(fn snapshot_nominators): Option<Vec<T::AccountId>>;
/// The next validator set. At the end of an era, if this is available (potentially from the
/// result of an offchain worker), it is immediately used. Otherwise, the on-chain election
/// is executed.
///
/// TWO_PHASE_NOTE: should be removed once we switch to multi-phase.
pub QueuedElected get(fn queued_elected): Option<ElectionResult<T::AccountId, BalanceOf<T>>>;
/// The score of the current [`QueuedElected`].
///
/// TWO_PHASE_NOTE: should be removed once we switch to multi-phase.
pub QueuedScore get(fn queued_score): Option<ElectionScore>;
/// Flag to control the execution of the offchain election. When `Open(_)`, we accept
/// solutions to be submitted.
///
/// TWO_PHASE_NOTE: should be removed once we switch to multi-phase.
pub EraElectionStatus get(fn era_election_status): ElectionStatus<T::BlockNumber>;
/// True if the current **planned** session is final. Note that this does not take era
/// forcing into account.
///
/// TWO_PHASE_NOTE: should be removed once we switch to multi-phase.
pub IsCurrentSessionFinal get(fn is_current_session_final): bool = false;
/// True if network has been upgraded to this version.
@@ -1345,14 +1376,14 @@ decl_module! {
ElectionStatus::<T::BlockNumber>::Open(now)
);
add_weight(0, 1, 0);
log!(info, "💸 Election window is Open({:?}). Snapshot created", now);
log!(info, "Election window is Open({:?}). Snapshot created", now);
} else {
log!(warn, "💸 Failed to create snapshot at {:?}.", now);
log!(warn, "Failed to create snapshot at {:?}.", now);
}
}
}
} else {
log!(warn, "💸 Estimating next session change failed.");
log!(warn, "Estimating next session change failed.");
}
add_weight(0, 0, T::NextNewSession::weight(now))
}
@@ -1367,16 +1398,15 @@ decl_module! {
/// to open. If so, it runs the offchain worker code.
fn offchain_worker(now: T::BlockNumber) {
use offchain_election::{set_check_offchain_execution_status, compute_offchain_election};
if Self::era_election_status().is_open_at(now) {
let offchain_status = set_check_offchain_execution_status::<T>(now);
if let Err(why) = offchain_status {
log!(warn, "💸 skipping offchain worker in open election window due to [{}]", why);
log!(warn, "skipping offchain worker in open election window due to [{}]", why);
} else {
if let Err(e) = compute_offchain_election::<T>() {
log!(error, "💸 Error in election offchain worker: {:?}", e);
log!(error, "Error in election offchain worker: {:?}", e);
} else {
log!(debug, "💸 Executed offchain worker thread without errors.");
log!(debug, "Executed offchain worker thread without errors.");
}
}
}
@@ -2267,7 +2297,10 @@ impl<T: Config> Module<T> {
}
/// Internal impl of [`Self::slashable_balance_of`] that returns [`VoteWeight`].
pub fn slashable_balance_of_vote_weight(stash: &T::AccountId, issuance: BalanceOf<T>) -> VoteWeight {
pub fn slashable_balance_of_vote_weight(
stash: &T::AccountId,
issuance: BalanceOf<T>,
) -> VoteWeight {
T::CurrencyToVote::to_vote(Self::slashable_balance_of(stash), issuance)
}
@@ -2306,7 +2339,7 @@ impl<T: Config> Module<T> {
{
log!(
warn,
"💸 Snapshot size too big [{} <> {}][{} <> {}].",
"Snapshot size too big [{} <> {}][{} <> {}].",
num_validators,
MAX_VALIDATORS,
num_nominators,
@@ -2330,10 +2363,7 @@ impl<T: Config> Module<T> {
<SnapshotNominators<T>>::kill();
}
fn do_payout_stakers(
validator_stash: T::AccountId,
era: EraIndex,
) -> DispatchResult {
fn do_payout_stakers(validator_stash: T::AccountId, era: EraIndex) -> DispatchResult {
// Validate input data
let current_era = CurrentEra::get().ok_or(Error::<T>::InvalidEraToReward)?;
ensure!(era <= current_era, Error::<T>::InvalidEraToReward);
@@ -2626,7 +2656,7 @@ impl<T: Config> Module<T> {
validator_at,
).map_err(|e| {
// log the error since it is not propagated into the runtime error.
log!(warn, "💸 un-compacting solution failed due to {:?}", e);
log!(warn, "un-compacting solution failed due to {:?}", e);
Error::<T>::OffchainElectionBogusCompact
})?;
@@ -2641,7 +2671,7 @@ impl<T: Config> Module<T> {
// all of the indices must map to either a validator or a nominator. If this is ever
// not the case, then the locking system of staking is most likely faulty, or we
// have bigger problems.
log!(error, "💸 detected an error in the staking locking and snapshot.");
log!(error, "detected an error in the staking locking and snapshot.");
// abort.
return Err(Error::<T>::OffchainElectionBogusNominator.into());
}
@@ -2690,7 +2720,7 @@ impl<T: Config> Module<T> {
);
// build the support map thereof in order to evaluate.
let supports = to_support_map::<T::AccountId>(&winners, &staked_assignments)
let supports = to_supports(&winners, &staked_assignments)
.map_err(|_| Error::<T>::OffchainElectionBogusEdge)?;
// Check if the score is the same as the claimed one.
@@ -2698,10 +2728,11 @@ impl<T: Config> Module<T> {
ensure!(submitted_score == claimed_score, Error::<T>::OffchainElectionBogusScore);
// At last, alles Ok. Exposures and store the result.
let exposures = Self::collect_exposure(supports);
let exposures = Self::collect_exposures(supports);
log!(
info,
"💸 A better solution (with compute {:?} and score {:?}) has been validated and stored on chain.",
"A better solution (with compute {:?} and score {:?}) has been validated and stored \
on chain.",
compute,
submitted_score,
);
@@ -2834,6 +2865,8 @@ impl<T: Config> Module<T> {
// Set staking information for new era.
let maybe_new_validators = Self::select_and_update_validators(current_era);
// TWO_PHASE_NOTE: use this later on.
let _unused_new_validators = Self::enact_election(current_era);
maybe_new_validators
}
@@ -2901,7 +2934,7 @@ impl<T: Config> Module<T> {
log!(
info,
"💸 new validator set of size {:?} has been elected via {:?} for era {:?}",
"new validator set of size {:?} has been elected via {:?} for staring era {:?}",
elected_stashes.len(),
compute,
current_era,
@@ -2950,20 +2983,20 @@ impl<T: Config> Module<T> {
Self::slashable_balance_of_fn(),
);
let supports = to_support_map::<T::AccountId>(
let supports = to_supports(
&elected_stashes,
&staked_assignments,
)
.map_err(|_|
log!(
error,
"💸 on-chain phragmen is failing due to a problem in the result. This must be a bug."
"on-chain phragmen is failing due to a problem in the result. This must be a bug."
)
)
.ok()?;
// collect exposures
let exposures = Self::collect_exposure(supports);
let exposures = Self::collect_exposures(supports);
// In order to keep the property required by `on_session_ending` that we must return the
// new validator set even if it's the same as the old, as long as any underlying
@@ -3025,7 +3058,7 @@ impl<T: Config> Module<T> {
// If we don't have enough candidates, nothing to do.
log!(
warn,
"💸 Chain does not have enough staking candidates to operate. Era {:?}.",
"chain does not have enough staking candidates to operate. Era {:?}.",
Self::current_era()
);
None
@@ -3041,9 +3074,10 @@ impl<T: Config> Module<T> {
}
}
/// Consume a set of [`Supports`] from [`sp_npos_elections`] and collect them into a [`Exposure`]
fn collect_exposure(
supports: SupportMap<T::AccountId>,
/// Consume a set of [`Supports`] from [`sp_npos_elections`] and collect them into a
/// [`Exposure`].
fn collect_exposures(
supports: Supports<T::AccountId>,
) -> Vec<(T::AccountId, Exposure<T::AccountId, BalanceOf<T>>)> {
let total_issuance = T::Currency::total_issuance();
let to_currency = |e: ExtendedBalance| T::CurrencyToVote::to_currency(e, total_issuance);
@@ -3075,6 +3109,80 @@ impl<T: Config> Module<T> {
}).collect::<Vec<(T::AccountId, Exposure<_, _>)>>()
}
/// Process the output of the election.
///
/// This ensures enough validators have been elected, converts all supports to exposures and
/// writes them to the associated storage.
///
/// Returns `Err(())` if less than [`MinimumValidatorCount`] validators have been elected, `Ok`
/// otherwise.
// TWO_PHASE_NOTE: remove the dead code.
#[allow(dead_code)]
pub fn process_election(
flat_supports: sp_npos_elections::Supports<T::AccountId>,
current_era: EraIndex,
) -> Result<Vec<T::AccountId>, ()> {
let exposures = Self::collect_exposures(flat_supports);
let elected_stashes = exposures.iter().cloned().map(|(x, _)| x).collect::<Vec<_>>();
if (elected_stashes.len() as u32) <= Self::minimum_validator_count() {
log!(
warn,
"chain does not have enough staking candidates to operate for era {:?}",
current_era,
);
return Err(());
}
// Populate Stakers and write slot stake.
let mut total_stake: BalanceOf<T> = Zero::zero();
exposures.into_iter().for_each(|(stash, exposure)| {
total_stake = total_stake.saturating_add(exposure.total);
<ErasStakers<T>>::insert(current_era, &stash, &exposure);
let mut exposure_clipped = exposure;
let clipped_max_len = T::MaxNominatorRewardedPerValidator::get() as usize;
if exposure_clipped.others.len() > clipped_max_len {
exposure_clipped.others.sort_by(|a, b| a.value.cmp(&b.value).reverse());
exposure_clipped.others.truncate(clipped_max_len);
}
<ErasStakersClipped<T>>::insert(&current_era, &stash, exposure_clipped);
});
// Insert current era staking information
<ErasTotalStake<T>>::insert(&current_era, total_stake);
// collect the pref of all winners
for stash in &elected_stashes {
let pref = Self::validators(stash);
<ErasValidatorPrefs<T>>::insert(&current_era, stash, pref);
}
// emit event
// TWO_PHASE_NOTE: remove the inner value.
Self::deposit_event(RawEvent::StakingElection(ElectionCompute::Signed));
log!(
info,
"new validator set of size {:?} has been processed for era {:?}",
elected_stashes.len(),
current_era,
);
Ok(elected_stashes)
}
/// Enact and process the election using the `ElectionProvider` type.
///
/// This will also process the election, as noted in [`process_election`].
fn enact_election(_current_era: EraIndex) -> Option<Vec<T::AccountId>> {
let _outcome = T::ElectionProvider::elect().map(|_| ());
log!(debug, "Experimental election provider outputted {:?}", _outcome);
// TWO_PHASE_NOTE: This code path shall not return anything for now. Later on, redirect the
// results to `process_election`.
None
}
/// Remove all associated data of a stash account from the staking system.
///
/// Assumes storage is upgraded before calling.
@@ -3167,7 +3275,11 @@ impl<T: Config> Module<T> {
}
#[cfg(feature = "runtime-benchmarks")]
pub fn add_era_stakers(current_era: EraIndex, controller: T::AccountId, exposure: Exposure<T::AccountId, BalanceOf<T>>) {
pub fn add_era_stakers(
current_era: EraIndex,
controller: T::AccountId,
exposure: Exposure<T::AccountId, BalanceOf<T>>,
) {
<ErasStakers<T>>::insert(&current_era, &controller, &exposure);
}
@@ -3180,6 +3292,109 @@ impl<T: Config> Module<T> {
pub fn set_slash_reward_fraction(fraction: Perbill) {
SlashRewardFraction::put(fraction);
}
/// Get all of the voters that are eligible for the npos election.
///
/// This will use all on-chain nominators, and all the validators will inject a self vote.
///
/// ### Slashing
///
/// All nominations that have been submitted before the last non-zero slash of the validator are
/// auto-chilled.
///
/// Note that this is VERY expensive. Use with care.
pub fn get_npos_voters() -> Vec<(T::AccountId, VoteWeight, Vec<T::AccountId>)> {
let weight_of = Self::slashable_balance_of_fn();
let mut all_voters = Vec::new();
for (validator, _) in <Validators<T>>::iter() {
// append self vote
let self_vote = (validator.clone(), weight_of(&validator), vec![validator.clone()]);
all_voters.push(self_vote);
}
for (nominator, nominations) in <Nominators<T>>::iter() {
let Nominations { submitted_in, mut targets, suppressed: _ } = nominations;
// Filter out nomination targets which were nominated before the most recent
// slashing span.
targets.retain(|stash| {
Self::slashing_spans(&stash)
.map_or(true, |spans| submitted_in >= spans.last_nonzero_slash())
});
let vote_weight = weight_of(&nominator);
all_voters.push((nominator, vote_weight, targets))
}
all_voters
}
pub fn get_npos_targets() -> Vec<T::AccountId> {
<Validators<T>>::iter().map(|(v, _)| v).collect::<Vec<_>>()
}
}
impl<T: Config> sp_election_providers::ElectionDataProvider<T::AccountId, T::BlockNumber>
for Module<T>
{
fn desired_targets() -> u32 {
Self::validator_count()
}
fn voters() -> Vec<(T::AccountId, VoteWeight, Vec<T::AccountId>)> {
Self::get_npos_voters()
}
fn targets() -> Vec<T::AccountId> {
Self::get_npos_targets()
}
fn next_election_prediction(now: T::BlockNumber) -> T::BlockNumber {
let current_era = Self::current_era().unwrap_or(0);
let current_session = Self::current_planned_session();
let current_era_start_session_index =
Self::eras_start_session_index(current_era).unwrap_or(0);
let era_length = current_session
.saturating_sub(current_era_start_session_index)
.min(T::SessionsPerEra::get());
let session_length = T::NextNewSession::average_session_length();
let until_this_session_end = T::NextNewSession::estimate_next_new_session(now)
.unwrap_or_default()
.saturating_sub(now);
let sessions_left: T::BlockNumber = T::SessionsPerEra::get()
.saturating_sub(era_length)
// one session is computed in this_session_end.
.saturating_sub(1)
.into();
now.saturating_add(
until_this_session_end.saturating_add(sessions_left.saturating_mul(session_length)),
)
}
#[cfg(any(feature = "runtime-benchmarks", test))]
fn put_snapshot(
voters: Vec<(T::AccountId, VoteWeight, Vec<T::AccountId>)>,
targets: Vec<T::AccountId>,
) {
targets.into_iter().for_each(|v| {
<Validators<T>>::insert(
v,
ValidatorPrefs { commission: Perbill::zero(), blocked: false },
);
});
voters.into_iter().for_each(|(v, _s, t)| {
<Nominators<T>>::insert(
v,
Nominations { targets: t, submitted_in: 0, suppressed: false },
);
});
}
}
/// In this implementation `new_session(session)` must be called before `end_session(session-1)`
@@ -3195,6 +3410,7 @@ impl<T: Config> pallet_session::SessionManager<T::AccountId> for Module<T> {
<frame_system::Module<T>>::block_number(),
new_index
);
CurrentPlannedSession::put(new_index);
Self::new_session(new_index)
}
fn start_session(start_index: SessionIndex) {
@@ -3217,10 +3433,12 @@ impl<T: Config> pallet_session::SessionManager<T::AccountId> for Module<T> {
}
}
impl<T: Config> historical::SessionManager<T::AccountId, Exposure<T::AccountId, BalanceOf<T>>> for Module<T> {
fn new_session(new_index: SessionIndex)
-> Option<Vec<(T::AccountId, Exposure<T::AccountId, BalanceOf<T>>)>>
{
impl<T: Config> historical::SessionManager<T::AccountId, Exposure<T::AccountId, BalanceOf<T>>>
for Module<T>
{
fn new_session(
new_index: SessionIndex,
) -> Option<Vec<(T::AccountId, Exposure<T::AccountId, BalanceOf<T>>)>> {
<Self as pallet_session::SessionManager<_>>::new_session(new_index).map(|validators| {
let current_era = Self::current_era()
// Must be some as a new era has been created.
@@ -3245,8 +3463,8 @@ impl<T: Config> historical::SessionManager<T::AccountId, Exposure<T::AccountId,
/// * 2 points to the block producer for each reference to a previously unreferenced uncle, and
/// * 1 point to the producer of each referenced uncle block.
impl<T> pallet_authorship::EventHandler<T::AccountId, T::BlockNumber> for Module<T>
where
T: Config + pallet_authorship::Config + pallet_session::Config
where
T: Config + pallet_authorship::Config + pallet_session::Config,
{
fn note_author(author: T::AccountId) {
Self::reward_by_ids(vec![(author, 20)])
@@ -3289,9 +3507,10 @@ impl<T: Config> Convert<T::AccountId, Option<Exposure<T::AccountId, BalanceOf<T>
}
/// This is intended to be used with `FilterHistoricalOffences`.
impl <T: Config>
impl<T: Config>
OnOffenceHandler<T::AccountId, pallet_session::historical::IdentificationTuple<T>, Weight>
for Module<T> where
for Module<T>
where
T: pallet_session::Config<ValidatorId = <T as frame_system::Config>::AccountId>,
T: pallet_session::historical::Config<
FullIdentification = Exposure<<T as frame_system::Config>::AccountId, BalanceOf<T>>,
@@ -3305,12 +3524,15 @@ for Module<T> where
>,
{
fn on_offence(
offenders: &[OffenceDetails<T::AccountId, pallet_session::historical::IdentificationTuple<T>>],
offenders: &[OffenceDetails<
T::AccountId,
pallet_session::historical::IdentificationTuple<T>,
>],
slash_fraction: &[Perbill],
slash_session: SessionIndex,
) -> Result<Weight, ()> {
if !Self::can_report() {
return Err(())
return Err(());
}
let reward_proportion = SlashRewardFraction::get();
@@ -3421,6 +3643,7 @@ for Module<T> where
}
fn can_report() -> bool {
// TWO_PHASE_NOTE: we can get rid of this API
Self::era_election_status().is_closed()
}
}
@@ -3431,7 +3654,8 @@ pub struct FilterHistoricalOffences<T, R> {
}
impl<T, Reporter, Offender, R, O> ReportOffence<Reporter, Offender, O>
for FilterHistoricalOffences<Module<T>, R> where
for FilterHistoricalOffences<Module<T>, R>
where
T: Config,
R: ReportOffence<Reporter, Offender, O>,
O: Offence<Offender>,
@@ -3488,7 +3712,7 @@ impl<T: Config> frame_support::unsigned::ValidateUnsigned for Module<T> {
return invalid.into();
}
log!(debug, "💸 validateUnsigned succeeded for a solution at era {}.", era);
log!(debug, "validateUnsigned succeeded for a solution at era {}.", era);
ValidTransaction::with_tag_prefix("StakingOffchain")
// The higher the score[0], the better a solution is.
+17 -10
View File
@@ -28,7 +28,7 @@ use frame_support::{
use sp_core::H256;
use sp_io;
use sp_npos_elections::{
to_support_map, EvaluateSupport, reduce, ExtendedBalance, StakedAssignment, ElectionScore,
to_supports, reduce, ExtendedBalance, StakedAssignment, ElectionScore, EvaluateSupport,
};
use sp_runtime::{
curve::PiecewiseLinear,
@@ -37,6 +37,7 @@ use sp_runtime::{
};
use sp_staking::offence::{OffenceDetails, OnOffenceHandler};
use std::{cell::RefCell, collections::HashSet};
use sp_election_providers::onchain;
pub const INIT_TIMESTAMP: u64 = 30_000;
pub const BLOCK_TIME: u64 = 1000;
@@ -239,6 +240,12 @@ impl OnUnbalanced<NegativeImbalanceOf<Test>> for RewardRemainderMock {
}
}
impl onchain::Config for Test {
type AccountId = AccountId;
type BlockNumber = BlockNumber;
type Accuracy = Perbill;
type DataProvider = Staking;
}
impl Config for Test {
type Currency = Balances;
type UnixTime = Timestamp;
@@ -261,6 +268,7 @@ impl Config for Test {
type MaxNominatorRewardedPerValidator = MaxNominatorRewardedPerValidator;
type UnsignedPriority = UnsignedPriority;
type OffchainSolutionWeightLimit = OffchainSolutionWeightLimit;
type ElectionProvider = onchain::OnChainSequentialPhragmen<Self>;
type WeightInfo = ();
}
@@ -760,7 +768,7 @@ pub(crate) fn add_slash(who: &AccountId) {
on_offence_now(
&[
OffenceDetails {
offender: (who.clone(), Staking::eras_stakers(Staking::active_era().unwrap().index, who.clone())),
offender: (who.clone(), Staking::eras_stakers(active_era(), who.clone())),
reporters: vec![],
},
],
@@ -841,7 +849,7 @@ pub(crate) fn horrible_npos_solution(
let score = {
let (_, _, better_score) = prepare_submission_with(true, true, 0, |_| {});
let support = to_support_map::<AccountId>(&winners, &staked_assignment).unwrap();
let support = to_supports::<AccountId>(&winners, &staked_assignment).unwrap();
let score = support.evaluate();
assert!(sp_npos_elections::is_score_better::<Perbill>(
@@ -941,7 +949,7 @@ pub(crate) fn prepare_submission_with(
Staking::slashable_balance_of_fn(),
);
let support_map = to_support_map::<AccountId>(
let support_map = to_supports(
winners.as_slice(),
staked.as_slice(),
).unwrap();
@@ -962,9 +970,8 @@ pub(crate) fn prepare_submission_with(
/// Make all validator and nominator request their payment
pub(crate) fn make_all_reward_payment(era: EraIndex) {
let validators_with_reward = ErasRewardPoints::<Test>::get(era).individual.keys()
.cloned()
.collect::<Vec<_>>();
let validators_with_reward =
ErasRewardPoints::<Test>::get(era).individual.keys().cloned().collect::<Vec<_>>();
// reward validators
for validator_controller in validators_with_reward.iter().filter_map(Staking::bonded) {
@@ -988,10 +995,10 @@ macro_rules! assert_session_era {
$session,
);
assert_eq!(
Staking::active_era().unwrap().index,
Staking::current_era().unwrap(),
$era,
"wrong active era {} != {}",
Staking::active_era().unwrap().index,
"wrong current era {} != {}",
Staking::current_era().unwrap(),
$era,
);
};
@@ -25,7 +25,7 @@ use codec::Decode;
use frame_support::{traits::Get, weights::Weight, IterableStorageMap};
use frame_system::offchain::SubmitTransaction;
use sp_npos_elections::{
to_support_map, EvaluateSupport, reduce, Assignment, ElectionResult, ElectionScore,
to_supports, EvaluateSupport, reduce, Assignment, ElectionResult, ElectionScore,
ExtendedBalance, CompactSolution,
};
use sp_runtime::{
@@ -127,7 +127,7 @@ pub(crate) fn compute_offchain_election<T: Config>() -> Result<(), OffchainElect
crate::log!(
info,
"💸 prepared a seq-phragmen solution with {} balancing iterations and score {:?}",
"prepared a seq-phragmen solution with {} balancing iterations and score {:?}",
iters,
score,
);
@@ -284,7 +284,7 @@ where
if compact.remove_voter(index) {
crate::log!(
trace,
"💸 removed a voter at index {} with stake {:?} from compact to reduce the size",
"removed a voter at index {} with stake {:?} from compact to reduce the size",
index,
_stake,
);
@@ -297,19 +297,17 @@ where
}
crate::log!(
warn,
"💸 {} nominators out of {} had to be removed from compact solution due to size limits.",
removed,
compact.voter_count() + removed,
);
warn,
"{} nominators out of {} had to be removed from compact solution due to size \
limits.",
removed,
compact.voter_count() + removed,
);
Ok(compact)
}
_ => {
// nada, return as-is
crate::log!(
info,
"💸 Compact solution did not get trimmed due to block weight limits.",
);
crate::log!(info, "Compact solution did not get trimmed due to block weight limits.",);
Ok(compact)
}
}
@@ -390,13 +388,16 @@ pub fn prepare_submission<T: Config>(
let maximum_allowed_voters =
maximum_compact_len::<T::WeightInfo>(winners.len() as u32, size, maximum_weight);
crate::log!(debug, "💸 Maximum weight = {:?} // current weight = {:?} // maximum voters = {:?} // current votes = {:?}",
crate::log!(
debug,
"Maximum weight = {:?} // current weight = {:?} // maximum voters = {:?} // current votes \
= {:?}",
maximum_weight,
T::WeightInfo::submit_solution_better(
size.validators.into(),
size.nominators.into(),
compact.voter_count() as u32,
winners.len() as u32,
size.validators.into(),
size.nominators.into(),
compact.voter_count() as u32,
winners.len() as u32,
),
maximum_allowed_voters,
compact.voter_count(),
@@ -415,7 +416,7 @@ pub fn prepare_submission<T: Config>(
<Module<T>>::slashable_balance_of_fn(),
);
let support_map = to_support_map::<T::AccountId>(&winners, &staked)
let support_map = to_supports::<T::AccountId>(&winners, &staked)
.map_err(|_| OffchainElectionError::ElectionFailed)?;
support_map.evaluate()
};
+1 -1
View File
@@ -247,7 +247,7 @@ pub fn get_weak_solution<T: Config>(
);
let support_map =
to_support_map::<T::AccountId>(winners.as_slice(), staked.as_slice()).unwrap();
to_supports::<T::AccountId>(winners.as_slice(), staked.as_slice()).unwrap();
support_map.evaluate()
};
+93 -4
View File
@@ -1833,6 +1833,7 @@ fn bond_with_duplicate_vote_should_be_ignored_by_npos_election() {
}
assert_ok!(Staking::bond(Origin::signed(1), 2, 1000, RewardDestination::Controller));
// 11 should not be elected. All of these count as ONE vote.
assert_ok!(Staking::nominate(Origin::signed(2), vec![11, 11, 11, 21, 31,]));
assert_ok!(Staking::bond(Origin::signed(3), 4, 1000, RewardDestination::Controller));
@@ -1886,7 +1887,6 @@ fn bond_with_duplicate_vote_should_be_ignored_by_npos_election_elected() {
assert_ok!(Staking::nominate(Origin::signed(4), vec![21, 31]));
// winners should be 21 and 31. Otherwise this election is taking duplicates into account.
let sp_npos_elections::ElectionResult {
winners,
assignments,
@@ -2029,7 +2029,7 @@ fn reward_from_authorship_event_handler_works() {
fn add_reward_points_fns_works() {
ExtBuilder::default().build_and_execute(|| {
// Not mandatory but must be coherent with rewards
assert_eq!(Session::validators(), vec![21, 11]);
assert_eq_uvec!(Session::validators(), vec![21, 11]);
<Module<Test>>::reward_by_ids(vec![
(21, 1),
@@ -3048,7 +3048,7 @@ mod offchain_election {
assert_eq!(Staking::era_election_status(), ElectionStatus::Open(37));
run_to_block(40);
assert_session_era!(4, 0);
assert_session_era!(4, 1);
assert_eq!(Staking::era_election_status(), ElectionStatus::Closed);
assert!(Staking::snapshot_nominators().is_none());
assert!(Staking::snapshot_validators().is_none());
@@ -3066,7 +3066,7 @@ mod offchain_election {
assert!(Staking::snapshot_validators().is_some());
run_to_block(90);
assert_session_era!(9, 1);
assert_session_era!(9, 2);
assert_eq!(Staking::era_election_status(), ElectionStatus::Closed);
assert!(Staking::snapshot_nominators().is_none());
assert!(Staking::snapshot_validators().is_none());
@@ -5015,3 +5015,92 @@ fn do_not_die_when_active_is_ed() {
);
})
}
mod election_data_provider {
use super::*;
use sp_election_providers::ElectionDataProvider;
#[test]
fn voters_include_self_vote() {
ExtBuilder::default().nominate(false).build().execute_with(|| {
assert!(<Validators<Test>>::iter().map(|(x, _)| x).all(|v| Staking::voters()
.into_iter()
.find(|(w, _, t)| { v == *w && t[0] == *w })
.is_some()))
})
}
#[test]
fn voters_exclude_slashed() {
ExtBuilder::default().build().execute_with(|| {
assert_eq!(Staking::nominators(101).unwrap().targets, vec![11, 21]);
assert_eq!(
<Staking as ElectionDataProvider<AccountId, BlockNumber>>::voters()
.iter()
.find(|x| x.0 == 101)
.unwrap()
.2,
vec![11, 21]
);
start_active_era(1);
add_slash(&11);
// 11 is gone.
start_active_era(2);
assert_eq!(
<Staking as ElectionDataProvider<AccountId, BlockNumber>>::voters()
.iter()
.find(|x| x.0 == 101)
.unwrap()
.2,
vec![21]
);
// resubmit and it is back
assert_ok!(Staking::nominate(Origin::signed(100), vec![11, 21]));
assert_eq!(
<Staking as ElectionDataProvider<AccountId, BlockNumber>>::voters()
.iter()
.find(|x| x.0 == 101)
.unwrap()
.2,
vec![11, 21]
);
})
}
#[test]
fn estimate_next_election_works() {
ExtBuilder::default().session_per_era(5).period(5).build().execute_with(|| {
// first session is always length 0.
for b in 1..20 {
run_to_block(b);
assert_eq!(Staking::next_election_prediction(System::block_number()), 20);
}
// election
run_to_block(20);
assert_eq!(Staking::next_election_prediction(System::block_number()), 45);
assert_eq!(staking_events().len(), 1);
assert_eq!(
*staking_events().last().unwrap(),
RawEvent::StakingElection(ElectionCompute::OnChain)
);
for b in 21..45 {
run_to_block(b);
assert_eq!(Staking::next_election_prediction(System::block_number()), 45);
}
// election
run_to_block(45);
assert_eq!(Staking::next_election_prediction(System::block_number()), 70);
assert_eq!(staking_events().len(), 3);
assert_eq!(
*staking_events().last().unwrap(),
RawEvent::StakingElection(ElectionCompute::OnChain)
);
})
}
}
+116 -116
View File
@@ -17,8 +17,8 @@
//! Autogenerated weights for pallet_staking
//!
//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 2.0.1
//! DATE: 2021-01-19, STEPS: [50, ], REPEAT: 20, LOW RANGE: [], HIGH RANGE: []
//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 3.0.0
//! DATE: 2021-02-13, STEPS: [50, ], REPEAT: 20, LOW RANGE: [], HIGH RANGE: []
//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 128
// Executed Command:
@@ -75,171 +75,171 @@ pub trait WeightInfo {
pub struct SubstrateWeight<T>(PhantomData<T>);
impl<T: frame_system::Config> WeightInfo for SubstrateWeight<T> {
fn bond() -> Weight {
(76_281_000 as Weight)
(81_642_000 as Weight)
.saturating_add(T::DbWeight::get().reads(5 as Weight))
.saturating_add(T::DbWeight::get().writes(4 as Weight))
}
fn bond_extra() -> Weight {
(62_062_000 as Weight)
(66_025_000 as Weight)
.saturating_add(T::DbWeight::get().reads(4 as Weight))
.saturating_add(T::DbWeight::get().writes(2 as Weight))
}
fn unbond() -> Weight {
(57_195_000 as Weight)
(60_810_000 as Weight)
.saturating_add(T::DbWeight::get().reads(5 as Weight))
.saturating_add(T::DbWeight::get().writes(3 as Weight))
}
fn withdraw_unbonded_update(s: u32, ) -> Weight {
(58_043_000 as Weight)
(61_537_000 as Weight)
// Standard Error: 1_000
.saturating_add((52_000 as Weight).saturating_mul(s as Weight))
.saturating_add((60_000 as Weight).saturating_mul(s as Weight))
.saturating_add(T::DbWeight::get().reads(5 as Weight))
.saturating_add(T::DbWeight::get().writes(3 as Weight))
}
fn withdraw_unbonded_kill(s: u32, ) -> Weight {
(89_920_000 as Weight)
// Standard Error: 3_000
.saturating_add((2_526_000 as Weight).saturating_mul(s as Weight))
(95_741_000 as Weight)
// Standard Error: 1_000
.saturating_add((2_754_000 as Weight).saturating_mul(s as Weight))
.saturating_add(T::DbWeight::get().reads(7 as Weight))
.saturating_add(T::DbWeight::get().writes(8 as Weight))
.saturating_add(T::DbWeight::get().writes((1 as Weight).saturating_mul(s as Weight)))
}
fn validate() -> Weight {
(20_228_000 as Weight)
(21_009_000 as Weight)
.saturating_add(T::DbWeight::get().reads(2 as Weight))
.saturating_add(T::DbWeight::get().writes(2 as Weight))
}
fn kick(k: u32, ) -> Weight {
(31_066_000 as Weight)
// Standard Error: 11_000
.saturating_add((17_754_000 as Weight).saturating_mul(k as Weight))
(31_832_000 as Weight)
// Standard Error: 15_000
.saturating_add((19_418_000 as Weight).saturating_mul(k as Weight))
.saturating_add(T::DbWeight::get().reads(2 as Weight))
.saturating_add(T::DbWeight::get().reads((1 as Weight).saturating_mul(k as Weight)))
.saturating_add(T::DbWeight::get().writes((1 as Weight).saturating_mul(k as Weight)))
}
fn nominate(n: u32, ) -> Weight {
(33_494_000 as Weight)
// Standard Error: 23_000
.saturating_add((5_253_000 as Weight).saturating_mul(n as Weight))
(34_304_000 as Weight)
// Standard Error: 20_000
.saturating_add((5_643_000 as Weight).saturating_mul(n as Weight))
.saturating_add(T::DbWeight::get().reads(4 as Weight))
.saturating_add(T::DbWeight::get().reads((1 as Weight).saturating_mul(n as Weight)))
.saturating_add(T::DbWeight::get().writes(2 as Weight))
}
fn chill() -> Weight {
(19_396_000 as Weight)
(20_103_000 as Weight)
.saturating_add(T::DbWeight::get().reads(2 as Weight))
.saturating_add(T::DbWeight::get().writes(2 as Weight))
}
fn set_payee() -> Weight {
(13_449_000 as Weight)
(13_858_000 as Weight)
.saturating_add(T::DbWeight::get().reads(1 as Weight))
.saturating_add(T::DbWeight::get().writes(1 as Weight))
}
fn set_controller() -> Weight {
(29_184_000 as Weight)
(30_269_000 as Weight)
.saturating_add(T::DbWeight::get().reads(3 as Weight))
.saturating_add(T::DbWeight::get().writes(3 as Weight))
}
fn set_validator_count() -> Weight {
(2_266_000 as Weight)
(2_444_000 as Weight)
.saturating_add(T::DbWeight::get().writes(1 as Weight))
}
fn force_no_eras() -> Weight {
(2_462_000 as Weight)
(2_766_000 as Weight)
.saturating_add(T::DbWeight::get().writes(1 as Weight))
}
fn force_new_era() -> Weight {
(2_483_000 as Weight)
(2_724_000 as Weight)
.saturating_add(T::DbWeight::get().writes(1 as Weight))
}
fn force_new_era_always() -> Weight {
(2_495_000 as Weight)
(2_702_000 as Weight)
.saturating_add(T::DbWeight::get().writes(1 as Weight))
}
fn set_invulnerables(v: u32, ) -> Weight {
(2_712_000 as Weight)
(2_914_000 as Weight)
// Standard Error: 0
.saturating_add((9_000 as Weight).saturating_mul(v as Weight))
.saturating_add((35_000 as Weight).saturating_mul(v as Weight))
.saturating_add(T::DbWeight::get().writes(1 as Weight))
}
fn force_unstake(s: u32, ) -> Weight {
(60_508_000 as Weight)
// Standard Error: 1_000
.saturating_add((2_525_000 as Weight).saturating_mul(s as Weight))
(64_032_000 as Weight)
// Standard Error: 2_000
.saturating_add((2_787_000 as Weight).saturating_mul(s as Weight))
.saturating_add(T::DbWeight::get().reads(4 as Weight))
.saturating_add(T::DbWeight::get().writes(8 as Weight))
.saturating_add(T::DbWeight::get().writes((1 as Weight).saturating_mul(s as Weight)))
}
fn cancel_deferred_slash(s: u32, ) -> Weight {
(5_886_772_000 as Weight)
// Standard Error: 393_000
.saturating_add((34_849_000 as Weight).saturating_mul(s as Weight))
(5_903_394_000 as Weight)
// Standard Error: 391_000
.saturating_add((34_834_000 as Weight).saturating_mul(s as Weight))
.saturating_add(T::DbWeight::get().reads(1 as Weight))
.saturating_add(T::DbWeight::get().writes(1 as Weight))
}
fn payout_stakers_dead_controller(n: u32, ) -> Weight {
(127_627_000 as Weight)
// Standard Error: 27_000
.saturating_add((49_354_000 as Weight).saturating_mul(n as Weight))
(141_724_000 as Weight)
// Standard Error: 24_000
.saturating_add((53_018_000 as Weight).saturating_mul(n as Weight))
.saturating_add(T::DbWeight::get().reads(11 as Weight))
.saturating_add(T::DbWeight::get().reads((3 as Weight).saturating_mul(n as Weight)))
.saturating_add(T::DbWeight::get().writes(2 as Weight))
.saturating_add(T::DbWeight::get().writes((1 as Weight).saturating_mul(n as Weight)))
}
fn payout_stakers_alive_staked(n: u32, ) -> Weight {
(156_838_000 as Weight)
// Standard Error: 24_000
.saturating_add((62_653_000 as Weight).saturating_mul(n as Weight))
(159_994_000 as Weight)
// Standard Error: 28_000
.saturating_add((67_746_000 as Weight).saturating_mul(n as Weight))
.saturating_add(T::DbWeight::get().reads(12 as Weight))
.saturating_add(T::DbWeight::get().reads((5 as Weight).saturating_mul(n as Weight)))
.saturating_add(T::DbWeight::get().writes(3 as Weight))
.saturating_add(T::DbWeight::get().writes((3 as Weight).saturating_mul(n as Weight)))
}
fn rebond(l: u32, ) -> Weight {
(40_110_000 as Weight)
(42_177_000 as Weight)
// Standard Error: 1_000
.saturating_add((78_000 as Weight).saturating_mul(l as Weight))
.saturating_add((82_000 as Weight).saturating_mul(l as Weight))
.saturating_add(T::DbWeight::get().reads(4 as Weight))
.saturating_add(T::DbWeight::get().writes(3 as Weight))
}
fn set_history_depth(e: u32, ) -> Weight {
(0 as Weight)
// Standard Error: 70_000
.saturating_add((32_883_000 as Weight).saturating_mul(e as Weight))
// Standard Error: 65_000
.saturating_add((34_151_000 as Weight).saturating_mul(e as Weight))
.saturating_add(T::DbWeight::get().reads(2 as Weight))
.saturating_add(T::DbWeight::get().writes(4 as Weight))
.saturating_add(T::DbWeight::get().writes((7 as Weight).saturating_mul(e as Weight)))
}
fn reap_stash(s: u32, ) -> Weight {
(64_605_000 as Weight)
// Standard Error: 1_000
.saturating_add((2_506_000 as Weight).saturating_mul(s as Weight))
(68_377_000 as Weight)
// Standard Error: 0
.saturating_add((2_757_000 as Weight).saturating_mul(s as Weight))
.saturating_add(T::DbWeight::get().reads(4 as Weight))
.saturating_add(T::DbWeight::get().writes(8 as Weight))
.saturating_add(T::DbWeight::get().writes((1 as Weight).saturating_mul(s as Weight)))
}
fn new_era(v: u32, n: u32, ) -> Weight {
(0 as Weight)
// Standard Error: 926_000
.saturating_add((548_212_000 as Weight).saturating_mul(v as Weight))
// Standard Error: 46_000
.saturating_add((78_343_000 as Weight).saturating_mul(n as Weight))
.saturating_add(T::DbWeight::get().reads(7 as Weight))
// Standard Error: 908_000
.saturating_add((588_562_000 as Weight).saturating_mul(v as Weight))
// Standard Error: 45_000
.saturating_add((83_485_000 as Weight).saturating_mul(n as Weight))
.saturating_add(T::DbWeight::get().reads(9 as Weight))
.saturating_add(T::DbWeight::get().reads((4 as Weight).saturating_mul(v as Weight)))
.saturating_add(T::DbWeight::get().reads((3 as Weight).saturating_mul(n as Weight)))
.saturating_add(T::DbWeight::get().writes(8 as Weight))
.saturating_add(T::DbWeight::get().writes(13 as Weight))
.saturating_add(T::DbWeight::get().writes((3 as Weight).saturating_mul(v as Weight)))
}
fn submit_solution_better(v: u32, n: u32, a: u32, w: u32, ) -> Weight {
(0 as Weight)
// Standard Error: 48_000
.saturating_add((937_000 as Weight).saturating_mul(v as Weight))
// Standard Error: 19_000
.saturating_add((657_000 as Weight).saturating_mul(n as Weight))
// Standard Error: 48_000
.saturating_add((70_669_000 as Weight).saturating_mul(a as Weight))
// Standard Error: 101_000
.saturating_add((7_658_000 as Weight).saturating_mul(w as Weight))
// Standard Error: 52_000
.saturating_add((750_000 as Weight).saturating_mul(v as Weight))
// Standard Error: 20_000
.saturating_add((556_000 as Weight).saturating_mul(n as Weight))
// Standard Error: 52_000
.saturating_add((76_201_000 as Weight).saturating_mul(a as Weight))
// Standard Error: 108_000
.saturating_add((7_271_000 as Weight).saturating_mul(w as Weight))
.saturating_add(T::DbWeight::get().reads(6 as Weight))
.saturating_add(T::DbWeight::get().reads((4 as Weight).saturating_mul(a as Weight)))
.saturating_add(T::DbWeight::get().reads((1 as Weight).saturating_mul(w as Weight)))
@@ -250,171 +250,171 @@ impl<T: frame_system::Config> WeightInfo for SubstrateWeight<T> {
// For backwards compatibility and tests
impl WeightInfo for () {
fn bond() -> Weight {
(76_281_000 as Weight)
(81_642_000 as Weight)
.saturating_add(RocksDbWeight::get().reads(5 as Weight))
.saturating_add(RocksDbWeight::get().writes(4 as Weight))
}
fn bond_extra() -> Weight {
(62_062_000 as Weight)
(66_025_000 as Weight)
.saturating_add(RocksDbWeight::get().reads(4 as Weight))
.saturating_add(RocksDbWeight::get().writes(2 as Weight))
}
fn unbond() -> Weight {
(57_195_000 as Weight)
(60_810_000 as Weight)
.saturating_add(RocksDbWeight::get().reads(5 as Weight))
.saturating_add(RocksDbWeight::get().writes(3 as Weight))
}
fn withdraw_unbonded_update(s: u32, ) -> Weight {
(58_043_000 as Weight)
(61_537_000 as Weight)
// Standard Error: 1_000
.saturating_add((52_000 as Weight).saturating_mul(s as Weight))
.saturating_add((60_000 as Weight).saturating_mul(s as Weight))
.saturating_add(RocksDbWeight::get().reads(5 as Weight))
.saturating_add(RocksDbWeight::get().writes(3 as Weight))
}
fn withdraw_unbonded_kill(s: u32, ) -> Weight {
(89_920_000 as Weight)
// Standard Error: 3_000
.saturating_add((2_526_000 as Weight).saturating_mul(s as Weight))
(95_741_000 as Weight)
// Standard Error: 1_000
.saturating_add((2_754_000 as Weight).saturating_mul(s as Weight))
.saturating_add(RocksDbWeight::get().reads(7 as Weight))
.saturating_add(RocksDbWeight::get().writes(8 as Weight))
.saturating_add(RocksDbWeight::get().writes((1 as Weight).saturating_mul(s as Weight)))
}
fn validate() -> Weight {
(20_228_000 as Weight)
(21_009_000 as Weight)
.saturating_add(RocksDbWeight::get().reads(2 as Weight))
.saturating_add(RocksDbWeight::get().writes(2 as Weight))
}
fn kick(k: u32, ) -> Weight {
(31_066_000 as Weight)
// Standard Error: 11_000
.saturating_add((17_754_000 as Weight).saturating_mul(k as Weight))
(31_832_000 as Weight)
// Standard Error: 15_000
.saturating_add((19_418_000 as Weight).saturating_mul(k as Weight))
.saturating_add(RocksDbWeight::get().reads(2 as Weight))
.saturating_add(RocksDbWeight::get().reads((1 as Weight).saturating_mul(k as Weight)))
.saturating_add(RocksDbWeight::get().writes((1 as Weight).saturating_mul(k as Weight)))
}
fn nominate(n: u32, ) -> Weight {
(33_494_000 as Weight)
// Standard Error: 23_000
.saturating_add((5_253_000 as Weight).saturating_mul(n as Weight))
(34_304_000 as Weight)
// Standard Error: 20_000
.saturating_add((5_643_000 as Weight).saturating_mul(n as Weight))
.saturating_add(RocksDbWeight::get().reads(4 as Weight))
.saturating_add(RocksDbWeight::get().reads((1 as Weight).saturating_mul(n as Weight)))
.saturating_add(RocksDbWeight::get().writes(2 as Weight))
}
fn chill() -> Weight {
(19_396_000 as Weight)
(20_103_000 as Weight)
.saturating_add(RocksDbWeight::get().reads(2 as Weight))
.saturating_add(RocksDbWeight::get().writes(2 as Weight))
}
fn set_payee() -> Weight {
(13_449_000 as Weight)
(13_858_000 as Weight)
.saturating_add(RocksDbWeight::get().reads(1 as Weight))
.saturating_add(RocksDbWeight::get().writes(1 as Weight))
}
fn set_controller() -> Weight {
(29_184_000 as Weight)
(30_269_000 as Weight)
.saturating_add(RocksDbWeight::get().reads(3 as Weight))
.saturating_add(RocksDbWeight::get().writes(3 as Weight))
}
fn set_validator_count() -> Weight {
(2_266_000 as Weight)
(2_444_000 as Weight)
.saturating_add(RocksDbWeight::get().writes(1 as Weight))
}
fn force_no_eras() -> Weight {
(2_462_000 as Weight)
(2_766_000 as Weight)
.saturating_add(RocksDbWeight::get().writes(1 as Weight))
}
fn force_new_era() -> Weight {
(2_483_000 as Weight)
(2_724_000 as Weight)
.saturating_add(RocksDbWeight::get().writes(1 as Weight))
}
fn force_new_era_always() -> Weight {
(2_495_000 as Weight)
(2_702_000 as Weight)
.saturating_add(RocksDbWeight::get().writes(1 as Weight))
}
fn set_invulnerables(v: u32, ) -> Weight {
(2_712_000 as Weight)
(2_914_000 as Weight)
// Standard Error: 0
.saturating_add((9_000 as Weight).saturating_mul(v as Weight))
.saturating_add((35_000 as Weight).saturating_mul(v as Weight))
.saturating_add(RocksDbWeight::get().writes(1 as Weight))
}
fn force_unstake(s: u32, ) -> Weight {
(60_508_000 as Weight)
// Standard Error: 1_000
.saturating_add((2_525_000 as Weight).saturating_mul(s as Weight))
(64_032_000 as Weight)
// Standard Error: 2_000
.saturating_add((2_787_000 as Weight).saturating_mul(s as Weight))
.saturating_add(RocksDbWeight::get().reads(4 as Weight))
.saturating_add(RocksDbWeight::get().writes(8 as Weight))
.saturating_add(RocksDbWeight::get().writes((1 as Weight).saturating_mul(s as Weight)))
}
fn cancel_deferred_slash(s: u32, ) -> Weight {
(5_886_772_000 as Weight)
// Standard Error: 393_000
.saturating_add((34_849_000 as Weight).saturating_mul(s as Weight))
(5_903_394_000 as Weight)
// Standard Error: 391_000
.saturating_add((34_834_000 as Weight).saturating_mul(s as Weight))
.saturating_add(RocksDbWeight::get().reads(1 as Weight))
.saturating_add(RocksDbWeight::get().writes(1 as Weight))
}
fn payout_stakers_dead_controller(n: u32, ) -> Weight {
(127_627_000 as Weight)
// Standard Error: 27_000
.saturating_add((49_354_000 as Weight).saturating_mul(n as Weight))
(141_724_000 as Weight)
// Standard Error: 24_000
.saturating_add((53_018_000 as Weight).saturating_mul(n as Weight))
.saturating_add(RocksDbWeight::get().reads(11 as Weight))
.saturating_add(RocksDbWeight::get().reads((3 as Weight).saturating_mul(n as Weight)))
.saturating_add(RocksDbWeight::get().writes(2 as Weight))
.saturating_add(RocksDbWeight::get().writes((1 as Weight).saturating_mul(n as Weight)))
}
fn payout_stakers_alive_staked(n: u32, ) -> Weight {
(156_838_000 as Weight)
// Standard Error: 24_000
.saturating_add((62_653_000 as Weight).saturating_mul(n as Weight))
(159_994_000 as Weight)
// Standard Error: 28_000
.saturating_add((67_746_000 as Weight).saturating_mul(n as Weight))
.saturating_add(RocksDbWeight::get().reads(12 as Weight))
.saturating_add(RocksDbWeight::get().reads((5 as Weight).saturating_mul(n as Weight)))
.saturating_add(RocksDbWeight::get().writes(3 as Weight))
.saturating_add(RocksDbWeight::get().writes((3 as Weight).saturating_mul(n as Weight)))
}
fn rebond(l: u32, ) -> Weight {
(40_110_000 as Weight)
(42_177_000 as Weight)
// Standard Error: 1_000
.saturating_add((78_000 as Weight).saturating_mul(l as Weight))
.saturating_add((82_000 as Weight).saturating_mul(l as Weight))
.saturating_add(RocksDbWeight::get().reads(4 as Weight))
.saturating_add(RocksDbWeight::get().writes(3 as Weight))
}
fn set_history_depth(e: u32, ) -> Weight {
(0 as Weight)
// Standard Error: 70_000
.saturating_add((32_883_000 as Weight).saturating_mul(e as Weight))
// Standard Error: 65_000
.saturating_add((34_151_000 as Weight).saturating_mul(e as Weight))
.saturating_add(RocksDbWeight::get().reads(2 as Weight))
.saturating_add(RocksDbWeight::get().writes(4 as Weight))
.saturating_add(RocksDbWeight::get().writes((7 as Weight).saturating_mul(e as Weight)))
}
fn reap_stash(s: u32, ) -> Weight {
(64_605_000 as Weight)
// Standard Error: 1_000
.saturating_add((2_506_000 as Weight).saturating_mul(s as Weight))
(68_377_000 as Weight)
// Standard Error: 0
.saturating_add((2_757_000 as Weight).saturating_mul(s as Weight))
.saturating_add(RocksDbWeight::get().reads(4 as Weight))
.saturating_add(RocksDbWeight::get().writes(8 as Weight))
.saturating_add(RocksDbWeight::get().writes((1 as Weight).saturating_mul(s as Weight)))
}
fn new_era(v: u32, n: u32, ) -> Weight {
(0 as Weight)
// Standard Error: 926_000
.saturating_add((548_212_000 as Weight).saturating_mul(v as Weight))
// Standard Error: 46_000
.saturating_add((78_343_000 as Weight).saturating_mul(n as Weight))
.saturating_add(RocksDbWeight::get().reads(7 as Weight))
// Standard Error: 908_000
.saturating_add((588_562_000 as Weight).saturating_mul(v as Weight))
// Standard Error: 45_000
.saturating_add((83_485_000 as Weight).saturating_mul(n as Weight))
.saturating_add(RocksDbWeight::get().reads(9 as Weight))
.saturating_add(RocksDbWeight::get().reads((4 as Weight).saturating_mul(v as Weight)))
.saturating_add(RocksDbWeight::get().reads((3 as Weight).saturating_mul(n as Weight)))
.saturating_add(RocksDbWeight::get().writes(8 as Weight))
.saturating_add(RocksDbWeight::get().writes(13 as Weight))
.saturating_add(RocksDbWeight::get().writes((3 as Weight).saturating_mul(v as Weight)))
}
fn submit_solution_better(v: u32, n: u32, a: u32, w: u32, ) -> Weight {
(0 as Weight)
// Standard Error: 48_000
.saturating_add((937_000 as Weight).saturating_mul(v as Weight))
// Standard Error: 19_000
.saturating_add((657_000 as Weight).saturating_mul(n as Weight))
// Standard Error: 48_000
.saturating_add((70_669_000 as Weight).saturating_mul(a as Weight))
// Standard Error: 101_000
.saturating_add((7_658_000 as Weight).saturating_mul(w as Weight))
// Standard Error: 52_000
.saturating_add((750_000 as Weight).saturating_mul(v as Weight))
// Standard Error: 20_000
.saturating_add((556_000 as Weight).saturating_mul(n as Weight))
// Standard Error: 52_000
.saturating_add((76_201_000 as Weight).saturating_mul(a as Weight))
// Standard Error: 108_000
.saturating_add((7_271_000 as Weight).saturating_mul(w as Weight))
.saturating_add(RocksDbWeight::get().reads(6 as Weight))
.saturating_add(RocksDbWeight::get().reads((4 as Weight).saturating_mul(a as Weight)))
.saturating_add(RocksDbWeight::get().reads((1 as Weight).saturating_mul(w as Weight)))
+27 -7
View File
@@ -490,10 +490,16 @@ impl<
}
}
/// Something that can estimate at which block the next session rotation will happen. This should
/// be the same logical unit that dictates `ShouldEndSession` to the session module. No Assumptions
/// are made about the scheduling of the sessions.
/// Something that can estimate at which block the next session rotation will happen.
///
/// This should be the same logical unit that dictates `ShouldEndSession` to the session module. No
/// Assumptions are made about the scheduling of the sessions.
pub trait EstimateNextSessionRotation<BlockNumber> {
/// Return the average length of a session.
///
/// This may or may not be accurate.
fn average_session_length() -> BlockNumber;
/// Return the block number at which the next session rotation is estimated to happen.
///
/// None should be returned if the estimation fails to come to an answer
@@ -503,7 +509,11 @@ pub trait EstimateNextSessionRotation<BlockNumber> {
fn weight(now: BlockNumber) -> Weight;
}
impl<BlockNumber: Bounded> EstimateNextSessionRotation<BlockNumber> for () {
impl<BlockNumber: Bounded + Default> EstimateNextSessionRotation<BlockNumber> for () {
fn average_session_length() -> BlockNumber {
Default::default()
}
fn estimate_next_session_rotation(_: BlockNumber) -> Option<BlockNumber> {
Default::default()
}
@@ -513,9 +523,15 @@ impl<BlockNumber: Bounded> EstimateNextSessionRotation<BlockNumber> for () {
}
}
/// Something that can estimate at which block the next `new_session` will be triggered. This must
/// always be implemented by the session module.
/// Something that can estimate at which block the next `new_session` will be triggered.
///
/// This must always be implemented by the session module.
pub trait EstimateNextNewSession<BlockNumber> {
/// Return the average length of a session.
///
/// This may or may not be accurate.
fn average_session_length() -> BlockNumber;
/// Return the block number at which the next new session is estimated to happen.
fn estimate_next_new_session(now: BlockNumber) -> Option<BlockNumber>;
@@ -523,7 +539,11 @@ pub trait EstimateNextNewSession<BlockNumber> {
fn weight(now: BlockNumber) -> Weight;
}
impl<BlockNumber: Bounded> EstimateNextNewSession<BlockNumber> for () {
impl<BlockNumber: Bounded + Default> EstimateNextNewSession<BlockNumber> for () {
fn average_session_length() -> BlockNumber {
Default::default()
}
fn estimate_next_new_session(_: BlockNumber) -> Option<BlockNumber> {
Default::default()
}