mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-05-30 01:11:04 +00:00
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:
@@ -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))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user