Offchain Phragmén BREAKING. (#4517)

* Initial skeleton for offchain phragmen

* Basic compact encoding decoding for results

* add compact files

* Bring back Self::ensure_storage_upgraded();

* Make staking use compact stuff.

* First seemingly working version of reduce, full of todos

* Everything phragmen related works again.

* Signing made easier, still issues.

* Signing from offchain compile fine 😎

* make compact work with staked asssignment

* Evaluation basics are in place.

* Move reduce into crate. Document stuff

* move reduce into no_std

* Add files

* Remove other std deps. Runtime compiles

* Seemingly it is al stable; cycle implemented but not integrated.

* Add fuzzing code.

* Cleanup reduce a bit more.

* a metric ton of tests for staking; wip 🔨

* Implement a lot more of the tests.

* wip getting the unsigned stuff to work

* A bit gleanup for unsigned debug

* Clean and finalize compact code.

* Document reduce.

* Still problems with signing

* We officaly duct taped the transaction submission stuff. 🤓

* Deadlock with keys again

* Runtime builds

* Unsigned test works 🙌

* Some cleanups

* Make all the tests compile and stuff

* Minor cleanup

* fix more merge stuff

* Most tests work again.

* a very nasty bug in reduce

* Fix all integrations

* Fix more todos

* Revamp everything and everything

* Remove bogus test

* Some review grumbles.

* Some fixes

* Fix doc test

* loop for submission

* Fix cli, keyring etc.

* some cleanup

* Fix staking tests again

* fix per-things; bring patches from benchmarking

* better score prediction

* Add fuzzer, more patches.

* Some fixes

* More docs

* Remove unused generics

* Remove max-nominator footgun

* Better fuzzer

* Disable it 

* Bump.

* Another round of self-review

* Refactor a lot

* More major fixes in perThing

* Add new fuzz file

* Update lock

* fix fuzzing code.

* Fix nominator retain test

* Add slashing check

* Update frame/staking/src/tests.rs

Co-Authored-By: Joshy Orndorff <JoshOrndorff@users.noreply.github.com>

* Some formatting nits

* Review comments.

* Fix cargo file

* Almost all tests work again

* Update frame/staking/src/tests.rs

Co-Authored-By: thiolliere <gui.thiolliere@gmail.com>

* Fix review comments

* More review stuff

* Some nits

* Fix new staking / session / babe relation

* Update primitives/phragmen/src/lib.rs

Co-Authored-By: thiolliere <gui.thiolliere@gmail.com>

* Update primitives/phragmen/src/lib.rs

Co-Authored-By: thiolliere <gui.thiolliere@gmail.com>

* Update primitives/phragmen/compact/src/lib.rs

Co-Authored-By: thiolliere <gui.thiolliere@gmail.com>

* Some doc updates to slashing

* Fix derive

* Remove imports

* Remove unimplemented tests

* nits

* Remove dbg

* Better fuzzing params

* Remove unused pref map

* Deferred Slashing/Offence for offchain Phragmen  (#5151)

* Some boilerplate

* Add test

* One more test

* Review comments

* Fix build

* review comments

* fix more

* fix build

* Some cleanups and self-reviews

* More minor self reviews

* Final nits

* Some merge fixes.

* opt comment

* Fix build

* Fix build again.

* Update frame/staking/fuzz/fuzz_targets/submit_solution.rs

Co-Authored-By: Gavin Wood <gavin@parity.io>

* Update frame/staking/src/slashing.rs

Co-Authored-By: Gavin Wood <gavin@parity.io>

* Update frame/staking/src/offchain_election.rs

Co-Authored-By: Gavin Wood <gavin@parity.io>

* Fix review comments

* fix test

* === 🔑 Revamp without staking key.

* final round of changes.

* Fix cargo-deny

* Update frame/staking/src/lib.rs

Co-Authored-By: Gavin Wood <gavin@parity.io>

Co-authored-by: Joshy Orndorff <JoshOrndorff@users.noreply.github.com>
Co-authored-by: thiolliere <gui.thiolliere@gmail.com>
Co-authored-by: Gavin Wood <gavin@parity.io>
This commit is contained in:
Kian Paimani
2020-03-26 15:37:40 +01:00
committed by GitHub
parent 2a67e6c437
commit 970c5f94f2
64 changed files with 11953 additions and 892 deletions
+18 -2
View File
@@ -11,7 +11,6 @@ description = "FRAME pallet staking"
[dependencies]
serde = { version = "1.0.101", optional = true }
codec = { package = "parity-scale-codec", version = "1.2.0", default-features = false, features = ["derive"] }
sp-keyring = { version = "2.0.0-alpha.5", optional = true, path = "../../primitives/keyring" }
sp-std = { version = "2.0.0-alpha.5", default-features = false, path = "../../primitives/std" }
sp-phragmen = { version = "2.0.0-alpha.5", default-features = false, path = "../../primitives/phragmen" }
sp-io ={ path = "../../primitives/io", default-features = false , version = "2.0.0-alpha.5"}
@@ -21,7 +20,15 @@ frame-support = { version = "2.0.0-alpha.5", default-features = false, path = ".
frame-system = { version = "2.0.0-alpha.5", default-features = false, path = "../system" }
pallet-session = { version = "2.0.0-alpha.5", features = ["historical"], path = "../session", default-features = false }
pallet-authorship = { version = "2.0.0-alpha.5", default-features = false, path = "../authorship" }
sp-application-crypto = { version = "2.0.0-alpha.4", default-features = false, path = "../../primitives/application-crypto" }
static_assertions = "1.1.0"
# Optional imports for tesing-utils feature
pallet-indices = { version = "2.0.0-alpha.4", optional = true, path = "../indices", default-features = false }
sp-core = { version = "2.0.0-alpha.4", optional = true, path = "../../primitives/core", default-features = false }
rand = { version = "0.7.3", optional = true, default-features = false }
# Optional imports for benchmarking
frame-benchmarking = { version = "2.0.0-alpha.5", default-features = false, path = "../benchmarking", optional = true }
rand_chacha = { version = "0.2", default-features = false, optional = true }
@@ -33,13 +40,20 @@ pallet-staking-reward-curve = { version = "2.0.0-alpha.5", path = "../staking/r
substrate-test-utils = { version = "2.0.0-alpha.5", path = "../../test-utils" }
frame-benchmarking = { version = "2.0.0-alpha.5", path = "../benchmarking" }
rand_chacha = { version = "0.2" }
parking_lot = "0.10.0"
env_logger = "0.7.1"
hex = "0.4"
[features]
testing-utils = [
"std",
"pallet-indices/std",
"sp-core/std",
"rand/std",
]
default = ["std"]
std = [
"serde",
"sp-keyring",
"codec/std",
"sp-std/std",
"sp-phragmen/std",
@@ -50,6 +64,8 @@ std = [
"pallet-session/std",
"frame-system/std",
"pallet-authorship/std",
"sp-application-crypto/std",
"sp-core/std",
]
runtime-benchmarks = [
"rand_chacha",
+4
View File
@@ -0,0 +1,4 @@
target
corpus
artifacts
File diff suppressed because it is too large Load Diff
+35
View File
@@ -0,0 +1,35 @@
[package]
name = "pallet-staking-fuzz"
version = "0.0.0"
authors = ["Automatically generated"]
publish = false
edition = "2018"
[package.metadata]
cargo-fuzz = true
[dependencies]
libfuzzer-sys = "0.3"
codec = { package = "parity-scale-codec", version = "1.2.0", default-features = false, features = ["derive"] }
pallet-staking = { version = "2.0.0-alpha.2", path = "..", features = ["testing-utils"] }
pallet-staking-reward-curve = { version = "2.0.0-alpha.2", path = "../reward-curve" }
pallet-session = { version = "2.0.0-alpha.2", path = "../../session" }
pallet-indices = { version = "2.0.0-alpha.2", path = "../../indices" }
pallet-balances = { version = "2.0.0-alpha.2", path = "../../balances" }
pallet-timestamp = { version = "2.0.0-alpha.2", path = "../../timestamp" }
frame-system = { version = "2.0.0-alpha.2", path = "../../system" }
frame-support = { version = "2.0.0-alpha.2", path = "../../support" }
sp-std = { version = "2.0.0-alpha.2", path = "../../../primitives/std" }
sp-io ={ version = "2.0.0-alpha.2", path = "../../../primitives/io" }
sp-core = { version = "2.0.0-alpha.2", path = "../../../primitives/core" }
sp-phragmen = { version = "2.0.0-alpha.2", path = "../../../primitives/phragmen" }
sp-runtime = { version = "2.0.0-alpha.2", path = "../../../primitives/runtime" }
rand = "0.7.3"
# Prevent this from interfering with workspaces
[workspace]
members = ["."]
[[bin]]
name = "submit_solution"
path = "fuzz_targets/submit_solution.rs"
@@ -0,0 +1,182 @@
// Copyright 2020 Parity Technologies (UK) Ltd.
// This file is part of Substrate.
// Substrate is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Substrate is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Substrate. If not, see <http://www.gnu.org/licenses/>.
//! Mock file for staking fuzzing.
use sp_runtime::traits::{Convert, SaturatedConversion};
use frame_support::{impl_outer_origin, impl_outer_dispatch, parameter_types};
type AccountId = u64;
type AccountIndex = u32;
type BlockNumber = u64;
type Balance = u64;
type System = frame_system::Module<Test>;
type Balances = pallet_balances::Module<Test>;
type Staking = pallet_staking::Module<Test>;
type Indices = pallet_indices::Module<Test>;
type Session = pallet_session::Module<Test>;
impl_outer_origin! {
pub enum Origin for Test where system = frame_system {}
}
impl_outer_dispatch! {
pub enum Call for Test where origin: Origin {
staking::Staking,
}
}
pub struct CurrencyToVoteHandler;
impl Convert<u64, u64> for CurrencyToVoteHandler {
fn convert(x: u64) -> u64 {
x
}
}
impl Convert<u128, u64> for CurrencyToVoteHandler {
fn convert(x: u128) -> u64 {
x.saturated_into()
}
}
#[derive(Clone, Eq, PartialEq, Debug)]
pub struct Test;
impl frame_system::Trait for Test {
type Origin = Origin;
type Index = AccountIndex;
type BlockNumber = BlockNumber;
type Call = Call;
type Hash = sp_core::H256;
type Hashing = ::sp_runtime::traits::BlakeTwo256;
type AccountId = AccountId;
type Lookup = Indices;
type Header = sp_runtime::testing::Header;
type Event = ();
type BlockHashCount = ();
type MaximumBlockWeight = ();
type AvailableBlockRatio = ();
type MaximumBlockLength = ();
type Version = ();
type ModuleToIndex = ();
type AccountData = pallet_balances::AccountData<u64>;
type OnNewAccount = ();
type OnKilledAccount = (Balances,);
}
parameter_types! {
pub const ExistentialDeposit: Balance = 10;
}
impl pallet_balances::Trait for Test {
type Balance = Balance;
type Event = ();
type DustRemoval = ();
type ExistentialDeposit = ExistentialDeposit;
type AccountStore = System;
}
impl pallet_indices::Trait for Test {
type AccountIndex = AccountIndex;
type Event = ();
type Currency = Balances;
type Deposit = ();
}
parameter_types! {
pub const MinimumPeriod: u64 = 5;
}
impl pallet_timestamp::Trait for Test {
type Moment = u64;
type OnTimestampSet = ();
type MinimumPeriod = MinimumPeriod;
}
impl pallet_session::historical::Trait for Test {
type FullIdentification = pallet_staking::Exposure<AccountId, Balance>;
type FullIdentificationOf = pallet_staking::ExposureOf<Test>;
}
sp_runtime::impl_opaque_keys! {
pub struct SessionKeys {
pub foo: sp_runtime::testing::UintAuthorityId,
}
}
pub struct TestSessionHandler;
impl pallet_session::SessionHandler<AccountId> for TestSessionHandler {
const KEY_TYPE_IDS: &'static [sp_runtime::KeyTypeId] = &[];
fn on_genesis_session<Ks: sp_runtime::traits::OpaqueKeys>(_validators: &[(AccountId, Ks)]) {}
fn on_new_session<Ks: sp_runtime::traits::OpaqueKeys>(
_: bool,
_: &[(AccountId, Ks)],
_: &[(AccountId, Ks)],
) {}
fn on_disabled(_: usize) {}
}
impl pallet_session::Trait for Test {
type SessionManager = pallet_session::historical::NoteHistoricalRoot<Test, Staking>;
type Keys = SessionKeys;
type ShouldEndSession = pallet_session::PeriodicSessions<(), ()>;
type NextSessionRotation = pallet_session::PeriodicSessions<(), ()>;
type SessionHandler = TestSessionHandler;
type Event = ();
type ValidatorId = AccountId;
type ValidatorIdOf = pallet_staking::StashOf<Test>;
type DisabledValidatorsThreshold = ();
}
pallet_staking_reward_curve::build! {
const I_NPOS: sp_runtime::curve::PiecewiseLinear<'static> = curve!(
min_inflation: 0_025_000,
max_inflation: 0_100_000,
ideal_stake: 0_500_000,
falloff: 0_050_000,
max_piece_count: 40,
test_precision: 0_005_000,
);
}
parameter_types! {
pub const RewardCurve: &'static sp_runtime::curve::PiecewiseLinear<'static> = &I_NPOS;
pub const MaxNominatorRewardedPerValidator: u32 = 64;
}
pub type Extrinsic = sp_runtime::testing::TestXt<Call, ()>;
type SubmitTransaction = frame_system::offchain::TransactionSubmitter<
sp_runtime::testing::UintAuthorityId,
Test,
Extrinsic,
>;
impl pallet_staking::Trait for Test {
type Currency = Balances;
type Time = pallet_timestamp::Module<Self>;
type CurrencyToVote = CurrencyToVoteHandler;
type RewardRemainder = ();
type Event = ();
type Slash = ();
type Reward = ();
type SessionsPerEra = ();
type SlashDeferDuration = ();
type SlashCancelOrigin = frame_system::EnsureRoot<Self::AccountId>;
type BondingDuration = ();
type SessionInterface = Self;
type RewardCurve = RewardCurve;
type NextNewSession = Session;
type ElectionLookahead = ();
type Call = Call;
type SubmitTransaction = SubmitTransaction;
type KeyType = sp_runtime::testing::UintAuthorityId;
type MaxNominatorRewardedPerValidator = MaxNominatorRewardedPerValidator;
}
@@ -0,0 +1,130 @@
// Copyright 2020 Parity Technologies (UK) Ltd.
// This file is part of Substrate.
// Substrate is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Substrate is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Substrate. If not, see <http://www.gnu.org/licenses/>.
//! Fuzzing for staking pallet.
#![no_main]
use libfuzzer_sys::fuzz_target;
use mock::Test;
use pallet_staking::testing_utils::{
self, USER, get_seq_phragmen_solution, get_weak_solution, setup_chain_stakers,
set_validator_count, signed_account,
};
use frame_support::assert_ok;
use sp_runtime::{traits::Dispatchable, DispatchError};
mod mock;
#[repr(u32)]
#[allow(dead_code)]
#[derive(Debug, Clone, Copy)]
enum Mode {
/// Initial submission. This will be rather cheap.
InitialSubmission,
/// A better submission that will replace the previous ones. This is the most expensive.
StrongerSubmission,
/// A weak submission that will be rejected. This will be rather cheap.
WeakerSubmission,
}
pub fn new_test_ext() -> Result<sp_io::TestExternalities, std::string::String> {
frame_system::GenesisConfig::default().build_storage::<mock::Test>().map(Into::into)
}
fuzz_target!(|do_reduce: bool| {
let ext = new_test_ext();
let mode: Mode = unsafe { std::mem::transmute(testing_utils::random(0, 2)) };
let num_validators = testing_utils::random(50, 500);
let num_nominators = testing_utils::random(200, 2000);
let edge_per_voter = testing_utils::random(1, 16);
let to_elect = testing_utils::random(10, num_validators);
println!("+++ instance with params {} / {} / {} / {:?} / {}",
num_nominators,
num_validators,
edge_per_voter,
mode,
to_elect,
);
ext.unwrap_or_default().execute_with(|| {
// initial setup
set_validator_count::<Test>(to_elect);
setup_chain_stakers::<Test>(
num_validators,
num_nominators,
edge_per_voter,
);
println!("++ Chain setup done.");
// stuff to submit
let (winners, compact, score) = match mode {
Mode::InitialSubmission => {
/* No need to setup anything */
get_seq_phragmen_solution::<Test>(do_reduce)
},
Mode::StrongerSubmission => {
let (winners, compact, score) = get_weak_solution::<Test>(false);
assert_ok!(
<pallet_staking::Module<Test>>::submit_election_solution(
signed_account::<Test>(USER),
winners,
compact,
score,
)
);
get_seq_phragmen_solution::<Test>(do_reduce)
},
Mode::WeakerSubmission => {
let (winners, compact, score) = get_seq_phragmen_solution::<Test>(do_reduce);
assert_ok!(
<pallet_staking::Module<Test>>::submit_election_solution(
signed_account::<Test>(USER),
winners,
compact,
score,
)
);
get_weak_solution::<Test>(false)
}
};
println!("++ Submission ready.");
// must have chosen correct number of winners.
assert_eq!(winners.len() as u32, <pallet_staking::Module<Test>>::validator_count());
// final call and origin
let call = pallet_staking::Call::<Test>::submit_election_solution(
winners,
compact,
score,
);
let caller = signed_account::<Test>(USER);
// actually submit
match mode {
Mode::WeakerSubmission => {
assert_eq!(
call.dispatch(caller.into()).unwrap_err(),
DispatchError::Module { index: 0, error: 11, message: Some("PhragmenWeakSubmission") },
);
},
_ => assert_ok!(call.dispatch(caller.into())),
};
})
});
+3 -3
View File
@@ -402,7 +402,7 @@ mod tests {
#[test]
fn create_validators_with_nominators_for_era_works() {
ExtBuilder::default().stakers(false).build().execute_with(|| {
ExtBuilder::default().has_stakers(false).build().execute_with(|| {
let v = 10;
let n = 100;
@@ -418,7 +418,7 @@ mod tests {
#[test]
fn create_validator_with_nominators_works() {
ExtBuilder::default().stakers(false).build().execute_with(|| {
ExtBuilder::default().has_stakers(false).build().execute_with(|| {
let n = 10;
let validator = create_validator_with_nominators::<Test>(
@@ -441,7 +441,7 @@ mod tests {
#[test]
fn create_nominator_with_validators_works() {
ExtBuilder::default().stakers(false).build().execute_with(|| {
ExtBuilder::default().has_stakers(false).build().execute_with(|| {
let v = 5;
let (nominator, validators) = create_nominator_with_validators::<Test>(v).unwrap();
File diff suppressed because it is too large Load Diff
+453 -91
View File
@@ -17,57 +17,74 @@
//! Test utilities
use std::{collections::{HashSet, HashMap}, cell::RefCell};
use sp_runtime::{Perbill, KeyTypeId};
use sp_runtime::Perbill;
use sp_runtime::curve::PiecewiseLinear;
use sp_runtime::traits::{IdentityLookup, Convert, OpaqueKeys, SaturatedConversion};
use sp_runtime::testing::{Header, UintAuthorityId};
use sp_runtime::traits::{IdentityLookup, Convert, SaturatedConversion, Zero};
use sp_runtime::testing::{Header, UintAuthorityId, TestXt};
use sp_staking::{SessionIndex, offence::{OffenceDetails, OnOffenceHandler}};
use sp_core::{H256, crypto::key_types};
use sp_io;
use sp_core::H256;
use frame_support::{
assert_ok, impl_outer_origin, parameter_types, StorageValue, StorageMap,
StorageDoubleMap, IterableStorageMap,
traits::{Currency, Get, FindAuthor, OnFinalize, OnInitialize}, weights::Weight,
assert_ok, impl_outer_origin, parameter_types, impl_outer_dispatch, impl_outer_event,
StorageValue, StorageMap, StorageDoubleMap, IterableStorageMap,
traits::{Currency, Get, FindAuthor, OnFinalize, OnInitialize},
weights::Weight,
};
use frame_system::offchain::TransactionSubmitter;
use sp_io;
use sp_phragmen::{
build_support_map, evaluate_support, reduce, ExtendedBalance, StakedAssignment, PhragmenScore,
};
use crate::{
EraIndex, GenesisConfig, Module, Trait, StakerStatus, ValidatorPrefs, RewardDestination,
Nominators, inflation, SessionInterface, Exposure, ErasStakers, ErasRewardPoints
Nominators, inflation, SessionInterface, Exposure, ErasStakers, ErasRewardPoints,
CompactAssignments, ValidatorIndex, NominatorIndex, Validators, OffchainAccuracy,
};
/// The AccountId alias in this test module.
pub type AccountId = u64;
pub type BlockNumber = u64;
pub type Balance = u64;
pub(crate) type AccountId = u64;
pub(crate) type AccountIndex = u64;
pub(crate) type BlockNumber = u64;
pub(crate) type Balance = u64;
/// Simple structure that exposes how u64 currency can be represented as... u64.
pub struct CurrencyToVoteHandler;
impl Convert<u64, u64> for CurrencyToVoteHandler {
fn convert(x: u64) -> u64 { x }
fn convert(x: u64) -> u64 {
x
}
}
impl Convert<u128, u64> for CurrencyToVoteHandler {
fn convert(x: u128) -> u64 { x.saturated_into() }
fn convert(x: u128) -> u64 {
x.saturated_into()
}
}
thread_local! {
static SESSION: RefCell<(Vec<AccountId>, HashSet<AccountId>)> = RefCell::new(Default::default());
static SESSION_PER_ERA: RefCell<SessionIndex> = RefCell::new(3);
static EXISTENTIAL_DEPOSIT: RefCell<u64> = RefCell::new(0);
static SLASH_DEFER_DURATION: RefCell<EraIndex> = RefCell::new(0);
static ELECTION_LOOKAHEAD: RefCell<BlockNumber> = RefCell::new(0);
static PERIOD: RefCell<BlockNumber> = RefCell::new(1);
}
pub struct TestSessionHandler;
impl pallet_session::SessionHandler<AccountId> for TestSessionHandler {
const KEY_TYPE_IDS: &'static [KeyTypeId] = &[key_types::DUMMY];
/// Another session handler struct to test on_disabled.
pub struct OtherSessionHandler;
impl pallet_session::OneSessionHandler<AccountId> for OtherSessionHandler {
type Key = UintAuthorityId;
fn on_genesis_session<Ks: OpaqueKeys>(_validators: &[(AccountId, Ks)]) {}
fn on_genesis_session<'a, I: 'a>(_: I)
where I: Iterator<Item=(&'a AccountId, Self::Key)>, AccountId: 'a {}
fn on_new_session<Ks: OpaqueKeys>(
_changed: bool,
validators: &[(AccountId, Ks)],
_queued_validators: &[(AccountId, Ks)],
) {
SESSION.with(|x|
*x.borrow_mut() = (validators.iter().map(|x| x.0.clone()).collect(), HashSet::new())
);
fn on_new_session<'a, I: 'a>(_: bool, validators: I, _: I,)
where I: Iterator<Item=(&'a AccountId, Self::Key)>, AccountId: 'a
{
SESSION.with(|x| {
*x.borrow_mut() = (
validators.map(|x| x.0.clone()).collect(),
HashSet::new(),
)
});
}
fn on_disabled(validator_index: usize) {
@@ -79,6 +96,10 @@ impl pallet_session::SessionHandler<AccountId> for TestSessionHandler {
}
}
impl sp_runtime::BoundToRuntimeAppPublic for OtherSessionHandler {
type Public = UintAuthorityId;
}
pub fn is_disabled(controller: AccountId) -> bool {
let stash = Staking::ledger(&controller).unwrap().stash;
SESSION.with(|d| d.borrow().1.contains(&stash))
@@ -91,6 +112,32 @@ impl Get<u64> for ExistentialDeposit {
}
}
pub struct SessionsPerEra;
impl Get<SessionIndex> for SessionsPerEra {
fn get() -> SessionIndex {
SESSION_PER_ERA.with(|v| *v.borrow())
}
}
impl Get<BlockNumber> for SessionsPerEra {
fn get() -> BlockNumber {
SESSION_PER_ERA.with(|v| *v.borrow() as BlockNumber)
}
}
pub struct ElectionLookahead;
impl Get<BlockNumber> for ElectionLookahead {
fn get() -> BlockNumber {
ELECTION_LOOKAHEAD.with(|v| *v.borrow())
}
}
pub struct Period;
impl Get<BlockNumber> for Period {
fn get() -> BlockNumber {
PERIOD.with(|v| *v.borrow())
}
}
pub struct SlashDeferDuration;
impl Get<EraIndex> for SlashDeferDuration {
fn get() -> EraIndex {
@@ -98,23 +145,47 @@ impl Get<EraIndex> for SlashDeferDuration {
}
}
impl_outer_origin!{
impl_outer_origin! {
pub enum Origin for Test where system = frame_system {}
}
impl_outer_dispatch! {
pub enum Call for Test where origin: Origin {
staking::Staking,
}
}
mod staking {
// Re-export needed for `impl_outer_event!`.
pub use super::super::*;
}
use frame_system as system;
use pallet_balances as balances;
use pallet_session as session;
impl_outer_event! {
pub enum MetaEvent for Test {
system<T>,
balances<T>,
session,
staking<T>,
}
}
/// Author of block is always 11
pub struct Author11;
impl FindAuthor<u64> for Author11 {
fn find_author<'a, I>(_digests: I) -> Option<u64>
where I: 'a + IntoIterator<Item=(frame_support::ConsensusEngineId, &'a [u8])>
where I: 'a + IntoIterator<Item = (frame_support::ConsensusEngineId, &'a [u8])>,
{
Some(11)
}
}
// Workaround for https://github.com/rust-lang/rust/issues/26925 . Remove when sorted.
#[derive(Clone, PartialEq, Eq, Debug)]
#[derive(Clone, Eq, PartialEq, Debug)]
pub struct Test;
parameter_types! {
pub const BlockHashCount: u64 = 250;
pub const MaximumBlockWeight: Weight = 1024;
@@ -123,15 +194,15 @@ parameter_types! {
}
impl frame_system::Trait for Test {
type Origin = Origin;
type Index = u64;
type Index = AccountIndex;
type BlockNumber = BlockNumber;
type Call = ();
type Call = Call;
type Hash = H256;
type Hashing = ::sp_runtime::traits::BlakeTwo256;
type AccountId = AccountId;
type Lookup = IdentityLookup<Self::AccountId>;
type Header = Header;
type Event = ();
type Event = MetaEvent;
type BlockHashCount = BlockHashCount;
type MaximumBlockWeight = MaximumBlockWeight;
type AvailableBlockRatio = AvailableBlockRatio;
@@ -144,26 +215,31 @@ impl frame_system::Trait for Test {
}
impl pallet_balances::Trait for Test {
type Balance = Balance;
type Event = MetaEvent;
type DustRemoval = ();
type Event = ();
type ExistentialDeposit = ExistentialDeposit;
type AccountStore = System;
}
parameter_types! {
pub const Period: BlockNumber = 1;
pub const Offset: BlockNumber = 0;
pub const UncleGenerations: u64 = 0;
pub const DisabledValidatorsThreshold: Perbill = Perbill::from_percent(25);
}
sp_runtime::impl_opaque_keys! {
pub struct SessionKeys {
pub other: OtherSessionHandler,
}
}
impl pallet_session::Trait for Test {
type Event = ();
type SessionManager = pallet_session::historical::NoteHistoricalRoot<Test, Staking>;
type Keys = SessionKeys;
type ShouldEndSession = pallet_session::PeriodicSessions<Period, Offset>;
type SessionHandler = (OtherSessionHandler,);
type Event = MetaEvent;
type ValidatorId = AccountId;
type ValidatorIdOf = crate::StashOf<Test>;
type ShouldEndSession = pallet_session::PeriodicSessions<Period, Offset>;
type SessionManager = pallet_session::historical::NoteHistoricalRoot<Test, Staking>;
type SessionHandler = TestSessionHandler;
type Keys = UintAuthorityId;
type DisabledValidatorsThreshold = DisabledValidatorsThreshold;
type NextSessionRotation = pallet_session::PeriodicSessions<Period, Offset>;
}
impl pallet_session::historical::Trait for Test {
@@ -195,17 +271,17 @@ pallet_staking_reward_curve::build! {
);
}
parameter_types! {
pub const SessionsPerEra: SessionIndex = 3;
pub const BondingDuration: EraIndex = 3;
pub const RewardCurve: &'static PiecewiseLinear<'static> = &I_NPOS;
pub const MaxNominatorRewardedPerValidator: u32 = 64;
}
impl Trait for Test {
type Currency = Balances;
type UnixTime = Timestamp;
type CurrencyToVote = CurrencyToVoteHandler;
type RewardRemainder = ();
type Event = ();
type Event = MetaEvent;
type Slash = ();
type Reward = ();
type SessionsPerEra = SessionsPerEra;
@@ -214,11 +290,21 @@ impl Trait for Test {
type BondingDuration = BondingDuration;
type SessionInterface = Self;
type RewardCurve = RewardCurve;
type NextNewSession = Session;
type ElectionLookahead = ElectionLookahead;
type Call = Call;
type SubmitTransaction = SubmitTransaction;
type MaxNominatorRewardedPerValidator = MaxNominatorRewardedPerValidator;
}
pub type Extrinsic = TestXt<Call, ()>;
type SubmitTransaction = TransactionSubmitter<(), Test, Extrinsic>;
pub struct ExtBuilder {
existential_deposit: u64,
session_length: BlockNumber,
election_lookahead: BlockNumber,
session_per_era: SessionIndex,
existential_deposit: Balance,
validator_pool: bool,
nominate: bool,
validator_count: u32,
@@ -226,13 +312,16 @@ pub struct ExtBuilder {
slash_defer_duration: EraIndex,
fair: bool,
num_validators: Option<u32>,
invulnerables: Vec<u64>,
stakers: bool,
invulnerables: Vec<AccountId>,
has_stakers: bool,
}
impl Default for ExtBuilder {
fn default() -> Self {
Self {
session_length: 1,
election_lookahead: 0,
session_per_era: 3,
existential_deposit: 1,
validator_pool: false,
nominate: true,
@@ -242,7 +331,7 @@ impl Default for ExtBuilder {
fair: true,
num_validators: None,
invulnerables: vec![],
stakers: true,
has_stakers: true,
}
}
}
@@ -284,18 +373,40 @@ impl ExtBuilder {
self.invulnerables = invulnerables;
self
}
pub fn set_associated_consts(&self) {
EXISTENTIAL_DEPOSIT.with(|v| *v.borrow_mut() = self.existential_deposit);
SLASH_DEFER_DURATION.with(|v| *v.borrow_mut() = self.slash_defer_duration);
}
pub fn stakers(mut self, has_stakers: bool) -> Self {
self.stakers = has_stakers;
pub fn session_per_era(mut self, length: SessionIndex) -> Self {
self.session_per_era = length;
self
}
pub fn election_lookahead(mut self, look: BlockNumber) -> Self {
self.election_lookahead = look;
self
}
pub fn session_length(mut self, length: BlockNumber) -> Self {
self.session_length = length;
self
}
pub fn has_stakers(mut self, has: bool) -> Self {
self.has_stakers = has;
self
}
pub fn offchain_phragmen_ext(self) -> Self {
self.session_per_era(4)
.session_length(5)
.election_lookahead(3)
}
pub fn set_associated_constants(&self) {
EXISTENTIAL_DEPOSIT.with(|v| *v.borrow_mut() = self.existential_deposit);
SLASH_DEFER_DURATION.with(|v| *v.borrow_mut() = self.slash_defer_duration);
SESSION_PER_ERA.with(|v| *v.borrow_mut() = self.session_per_era);
ELECTION_LOOKAHEAD.with(|v| *v.borrow_mut() = self.election_lookahead);
PERIOD.with(|v| *v.borrow_mut() = self.session_length);
}
pub fn build(self) -> sp_io::TestExternalities {
self.set_associated_consts();
let mut storage = frame_system::GenesisConfig::default().build_storage::<Test>().unwrap();
let _ = env_logger::try_init();
self.set_associated_constants();
let mut storage = frame_system::GenesisConfig::default()
.build_storage::<Test>()
.unwrap();
let balance_factor = if self.existential_deposit > 1 {
256
} else {
@@ -307,7 +418,7 @@ impl ExtBuilder {
.map(|x| ((x + 1) * 10 + 1) as u64)
.collect::<Vec<_>>();
let _ = pallet_balances::GenesisConfig::<Test>{
let _ = pallet_balances::GenesisConfig::<Test> {
balances: vec![
(1, 10 * balance_factor),
(2, 20 * balance_factor),
@@ -329,7 +440,7 @@ impl ExtBuilder {
}.assimilate_storage(&mut storage);
let mut stakers = vec![];
if self.stakers {
if self.has_stakers {
let stake_21 = if self.fair { 1000 } else { 2000 };
let stake_31 = if self.validator_pool { balance_factor * 1000 } else { 1 };
let status_41 = if self.validator_pool {
@@ -355,18 +466,21 @@ impl ExtBuilder {
invulnerables: self.invulnerables,
slash_reward_fraction: Perbill::from_percent(10),
..Default::default()
}.assimilate_storage(&mut storage);
}
.assimilate_storage(&mut storage);
let _ = pallet_session::GenesisConfig::<Test> {
keys: validators.iter().map(|x| (*x, *x, UintAuthorityId(*x))).collect(),
keys: validators.iter().map(|x| (
*x,
*x,
SessionKeys { other: UintAuthorityId(*x) }
)).collect(),
}.assimilate_storage(&mut storage);
let mut ext = sp_io::TestExternalities::from(storage);
ext.execute_with(|| {
let validators = Session::validators();
SESSION.with(|x|
*x.borrow_mut() = (validators.clone(), HashSet::new())
);
SESSION.with(|x| *x.borrow_mut() = (validators.clone(), HashSet::new()));
});
ext
}
@@ -378,6 +492,10 @@ pub type Session = pallet_session::Module<Test>;
pub type Timestamp = pallet_timestamp::Module<Test>;
pub type Staking = Module<Test>;
pub fn active_era() -> EraIndex {
Staking::active_era().unwrap().index
}
pub fn check_exposure_all(era: EraIndex) {
ErasStakers::<Test>::iter_prefix(era).for_each(check_exposure)
}
@@ -390,8 +508,9 @@ pub fn check_nominator_all(era: EraIndex) {
/// Check for each selected validator: expo.total = Sum(expo.other) + expo.own
pub fn check_exposure(expo: Exposure<AccountId, Balance>) {
assert_eq!(
expo.total as u128, expo.own as u128 + expo.others.iter().map(|e| e.value as u128).sum::<u128>(),
"wrong total exposure {:?}", expo,
expo.total as u128,
expo.own as u128 + expo.others.iter().map(|e| e.value as u128).sum::<u128>(),
"wrong total exposure",
);
}
@@ -400,17 +519,18 @@ pub fn check_exposure(expo: Exposure<AccountId, Balance>) {
pub fn check_nominator_exposure(era: EraIndex, stash: AccountId) {
assert_is_stash(stash);
let mut sum = 0;
ErasStakers::<Test>::iter_prefix(era)
.for_each(|exposure| {
exposure.others.iter()
.filter(|i| i.who == stash)
.for_each(|i| sum += i.value)
});
Session::validators()
.iter()
.map(|v| Staking::eras_stakers(era, v))
.for_each(|e| e.others.iter().filter(|i| i.who == stash).for_each(|i| sum += i.value));
let nominator_stake = Staking::slashable_balance_of(&stash);
// a nominator cannot over-spend.
assert!(
nominator_stake >= sum,
"failed: Nominator({}) stake({}) >= sum divided({})", stash, nominator_stake, sum,
"failed: Nominator({}) stake({}) >= sum divided({})",
stash,
nominator_stake,
sum,
);
}
@@ -426,20 +546,41 @@ pub fn assert_ledger_consistent(stash: AccountId) {
assert_eq!(real_total, ledger.total);
}
pub fn bond_validator(acc: u64, val: u64) {
// a = controller
// a + 1 = stash
let _ = Balances::make_free_balance_be(&(acc + 1), val);
assert_ok!(Staking::bond(Origin::signed(acc + 1), acc, val, RewardDestination::Controller));
assert_ok!(Staking::validate(Origin::signed(acc), ValidatorPrefs::default()));
pub fn bond_validator(stash: u64, ctrl: u64, val: u64) {
let _ = Balances::make_free_balance_be(&stash, val);
assert_ok!(Staking::bond(
Origin::signed(stash),
ctrl,
val,
RewardDestination::Controller,
));
assert_ok!(Staking::validate(
Origin::signed(ctrl),
ValidatorPrefs::default()
));
}
pub fn bond_nominator(acc: u64, val: u64, target: Vec<u64>) {
// a = controller
// a + 1 = stash
let _ = Balances::make_free_balance_be(&(acc + 1), val);
assert_ok!(Staking::bond(Origin::signed(acc + 1), acc, val, RewardDestination::Controller));
assert_ok!(Staking::nominate(Origin::signed(acc), target));
pub fn bond_nominator(stash: u64, ctrl: u64, val: u64, target: Vec<u64>) {
let _ = Balances::make_free_balance_be(&stash, val);
assert_ok!(Staking::bond(
Origin::signed(stash),
ctrl,
val,
RewardDestination::Controller,
));
assert_ok!(Staking::nominate(Origin::signed(ctrl), target));
}
pub fn run_to_block(n: BlockNumber) {
Staking::on_finalize(System::block_number());
for b in System::block_number() + 1..=n {
System::set_block_number(b);
Session::on_initialize(b);
Staking::on_initialize(b);
if b != n {
Staking::on_finalize(System::block_number());
}
}
}
pub fn advance_session() {
@@ -448,19 +589,21 @@ pub fn advance_session() {
}
pub fn start_session(session_index: SessionIndex) {
assert_eq!(<Period as Get<BlockNumber>>::get(), 1, "start_session can only be used with session length 1.");
for i in Session::current_index()..session_index {
Staking::on_finalize(System::block_number());
System::set_block_number((i + 1).into());
Timestamp::set_timestamp(System::block_number() * 1000);
Session::on_initialize(System::block_number());
Staking::on_initialize(System::block_number());
}
assert_eq!(Session::current_index(), session_index);
}
pub fn start_era(era_index: EraIndex) {
start_session((era_index * 3).into());
assert_eq!(Staking::active_era().unwrap().index, era_index);
start_session((era_index * <SessionsPerEra as Get<u32>>::get()).into());
assert_eq!(Staking::current_era().unwrap(), era_index);
}
pub fn current_total_payout_for_duration(duration: u64) -> u64 {
@@ -473,33 +616,45 @@ pub fn current_total_payout_for_duration(duration: u64) -> u64 {
}
pub fn reward_all_elected() {
let rewards = <Test as Trait>::SessionInterface::validators().into_iter()
let rewards = <Test as Trait>::SessionInterface::validators()
.into_iter()
.map(|v| (v, 1));
<Module<Test>>::reward_by_ids(rewards)
}
pub fn validator_controllers() -> Vec<AccountId> {
Session::validators().into_iter().map(|s| Staking::bonded(&s).expect("no controller for validator")).collect()
Session::validators()
.into_iter()
.map(|s| Staking::bonded(&s).expect("no controller for validator"))
.collect()
}
pub fn on_offence_in_era(
offenders: &[OffenceDetails<AccountId, pallet_session::historical::IdentificationTuple<Test>>],
offenders: &[OffenceDetails<
AccountId,
pallet_session::historical::IdentificationTuple<Test>,
>],
slash_fraction: &[Perbill],
era: EraIndex,
) {
let bonded_eras = crate::BondedEras::get();
for &(bonded_era, start_session) in bonded_eras.iter() {
if bonded_era == era {
Staking::on_offence(offenders, slash_fraction, start_session);
return
let _ = Staking::on_offence(offenders, slash_fraction, start_session).unwrap();
return;
} else if bonded_era > era {
break
break;
}
}
if Staking::active_era().unwrap().index == era {
Staking::on_offence(offenders, slash_fraction, Staking::eras_start_session_index(era).unwrap());
let _ =
Staking::on_offence(
offenders,
slash_fraction,
Staking::eras_start_session_index(era).unwrap()
).unwrap();
} else {
panic!("cannot slash in era {}", era);
}
@@ -513,6 +668,193 @@ pub fn on_offence_now(
on_offence_in_era(offenders, slash_fraction, now)
}
// winners will be chosen by simply their unweighted total backing stake. Nominator stake is
// distributed evenly.
pub fn horrible_phragmen_with_post_processing(
do_reduce: bool,
) -> (CompactAssignments, Vec<ValidatorIndex>, PhragmenScore) {
use std::collections::BTreeMap;
let mut backing_stake_of: BTreeMap<AccountId, Balance> = BTreeMap::new();
// self stake
<Validators<Test>>::iter().for_each(|(who, _p)| {
*backing_stake_of.entry(who).or_insert(Zero::zero()) += Staking::slashable_balance_of(&who)
});
// add nominator stuff
<Nominators<Test>>::iter().for_each(|(who, nomination)| {
nomination.targets.iter().for_each(|v| {
*backing_stake_of.entry(*v).or_insert(Zero::zero()) +=
Staking::slashable_balance_of(&who)
})
});
// elect winners
let mut sorted: Vec<AccountId> = backing_stake_of.keys().cloned().collect();
sorted.sort_by_key(|x| backing_stake_of.get(x).unwrap());
let winners: Vec<AccountId> = sorted
.iter()
.cloned()
.take(Staking::validator_count() as usize)
.collect();
// create assignments
let mut staked_assignment: Vec<StakedAssignment<AccountId>> = Vec::new();
<Nominators<Test>>::iter().for_each(|(who, nomination)| {
let mut dist: Vec<(AccountId, ExtendedBalance)> = Vec::new();
nomination.targets.iter().for_each(|v| {
if winners.iter().find(|w| *w == v).is_some() {
dist.push((*v, ExtendedBalance::zero()));
}
});
if dist.len() == 0 {
return;
}
// assign real stakes. just split the stake.
let stake = Staking::slashable_balance_of(&who) as ExtendedBalance;
let mut sum: ExtendedBalance = Zero::zero();
let dist_len = dist.len();
{
dist.iter_mut().for_each(|(_, w)| {
let partial = stake / (dist_len as ExtendedBalance);
*w = partial;
sum += partial;
});
}
// assign the leftover to last.
{
let leftover = stake - sum;
let last = dist.last_mut().unwrap();
last.1 += leftover;
}
staked_assignment.push(StakedAssignment {
who,
distribution: dist,
});
});
// Ensure that this result is worse than seq-phragmen. Otherwise, it should not have been used
// for testing.
let score = {
let (_, _, better_score) = prepare_submission_with(true, |_| {});
let support = build_support_map::<AccountId>(&winners, &staked_assignment).0;
let score = evaluate_support(&support);
assert!(sp_phragmen::is_score_better(score, better_score));
score
};
if do_reduce {
reduce(&mut staked_assignment);
}
let snapshot_validators = Staking::snapshot_validators().unwrap();
let snapshot_nominators = Staking::snapshot_nominators().unwrap();
let nominator_index = |a: &AccountId| -> Option<NominatorIndex> {
snapshot_nominators.iter().position(|x| x == a).map(|i| i as NominatorIndex)
};
let validator_index = |a: &AccountId| -> Option<ValidatorIndex> {
snapshot_validators.iter().position(|x| x == a).map(|i| i as ValidatorIndex)
};
// convert back to ratio assignment. This takes less space.
let assignments_reduced =
sp_phragmen::assignment_staked_to_ratio::<AccountId, OffchainAccuracy>(staked_assignment);
let compact =
CompactAssignments::from_assignment(assignments_reduced, nominator_index, validator_index)
.unwrap();
// winner ids to index
let winners = winners.into_iter().map(|w| validator_index(&w).unwrap()).collect::<Vec<_>>();
(compact, winners, score)
}
// Note: this should always logically reproduce [`offchain_election::prepare_submission`], yet we
// cannot do it since we want to have `tweak` injected into the process.
pub fn prepare_submission_with(
do_reduce: bool,
tweak: impl FnOnce(&mut Vec<StakedAssignment<AccountId>>),
) -> (CompactAssignments, Vec<ValidatorIndex>, PhragmenScore) {
// run phragmen on the default stuff.
let sp_phragmen::PhragmenResult {
winners,
assignments,
} = Staking::do_phragmen::<OffchainAccuracy>().unwrap();
let winners = winners.into_iter().map(|(w, _)| w).collect::<Vec<AccountId>>();
let stake_of = |who: &AccountId| -> ExtendedBalance {
<CurrencyToVoteHandler as Convert<Balance, u64>>::convert(
Staking::slashable_balance_of(&who)
) as ExtendedBalance
};
let mut staked = sp_phragmen::assignment_ratio_to_staked(assignments, stake_of);
// apply custom tweaks. awesome for testing.
tweak(&mut staked);
if do_reduce {
reduce(&mut staked);
}
// convert back to ratio assignment. This takes less space.
let snapshot_validators = Staking::snapshot_validators().expect("snapshot not created.");
let snapshot_nominators = Staking::snapshot_nominators().expect("snapshot not created.");
let nominator_index = |a: &AccountId| -> Option<NominatorIndex> {
snapshot_nominators
.iter()
.position(|x| x == a)
.map_or_else(
|| { println!("unable to find nominator index for {:?}", a); None },
|i| Some(i as NominatorIndex),
)
};
let validator_index = |a: &AccountId| -> Option<ValidatorIndex> {
snapshot_validators
.iter()
.position(|x| x == a)
.map_or_else(
|| { println!("unable to find validator index for {:?}", a); None },
|i| Some(i as ValidatorIndex),
)
};
let assignments_reduced = sp_phragmen::assignment_staked_to_ratio(staked);
// re-compute score by converting, yet again, into staked type
let score = {
let staked = sp_phragmen::assignment_ratio_to_staked(
assignments_reduced.clone(),
Staking::slashable_balance_of_extended,
);
let (support_map, _) = build_support_map::<AccountId>(
winners.as_slice(),
staked.as_slice(),
);
evaluate_support::<AccountId>(&support_map)
};
let compact =
CompactAssignments::from_assignment(assignments_reduced, nominator_index, validator_index)
.map_err(|e| { println!("error in compact: {:?}", e); e })
.expect("Failed to create compact");
// winner ids to index
let winners = winners.into_iter().map(|w| validator_index(&w).unwrap()).collect::<Vec<_>>();
(compact, winners, score)
}
/// Make all validator and nominator request their payment
pub fn make_all_reward_payment(era: EraIndex) {
let validators_with_reward = ErasRewardPoints::<Test>::get(era).individual.keys()
@@ -544,3 +886,23 @@ pub fn make_all_reward_payment(era: EraIndex) {
assert_ok!(Staking::payout_validator(Origin::signed(validator_controller), era));
}
}
#[macro_export]
macro_rules! assert_session_era {
($session:expr, $era:expr) => {
assert_eq!(
Session::current_index(),
$session,
"wrong session {} != {}",
Session::current_index(),
$session,
);
assert_eq!(
Staking::active_era().unwrap().index,
$era,
"wrong active era {} != {}",
Staking::active_era().unwrap().index,
$era,
);
};
}
@@ -0,0 +1,219 @@
// Copyright 2020 Parity Technologies (UK) Ltd.
// This file is part of Substrate.
// Substrate is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Substrate is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Substrate. If not, see <http://www.gnu.org/licenses/>.
//! Helpers for offchain worker election.
use crate::{
Call, CompactAssignments, Module, NominatorIndex, OffchainAccuracy, Trait, ValidatorIndex,
};
use frame_system::offchain::SubmitUnsignedTransaction;
use sp_phragmen::{
build_support_map, evaluate_support, reduce, Assignment, ExtendedBalance, PhragmenResult,
PhragmenScore,
};
use sp_runtime::offchain::storage::StorageValueRef;
use sp_runtime::PerThing;
use sp_runtime::RuntimeDebug;
use sp_std::{convert::TryInto, prelude::*};
/// Error types related to the offchain election machinery.
#[derive(RuntimeDebug)]
pub enum OffchainElectionError {
/// Phragmen election returned None. This means less candidate that minimum number of needed
/// validators were present. The chain is in trouble and not much that we can do about it.
ElectionFailed,
/// Submission to the transaction pool failed.
PoolSubmissionFailed,
/// The snapshot data is not available.
SnapshotUnavailable,
/// Error from phragmen crate. This usually relates to compact operation.
PhragmenError(sp_phragmen::Error),
/// One of the computed winners is invalid.
InvalidWinner,
}
impl From<sp_phragmen::Error> for OffchainElectionError {
fn from(e: sp_phragmen::Error) -> Self {
Self::PhragmenError(e)
}
}
/// Storage key used to store the persistent offchain worker status.
pub(crate) const OFFCHAIN_HEAD_DB: &[u8] = b"parity/staking-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;
/// Default number of blocks for which the unsigned transaction should stay in the pool
pub(crate) const DEFAULT_LONGEVITY: u64 = 25;
/// 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 set_check_offchain_execution_status<T: Trait>(
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),
}
}
/// The internal logic of the offchain worker of this module. This runs the phragmen election,
/// compacts and reduces the solution, computes the score and submits it back to the chain as an
/// unsigned transaction, without any signature.
pub(crate) fn compute_offchain_election<T: Trait>() -> Result<(), OffchainElectionError> {
// compute raw solution. Note that we use `OffchainAccuracy`.
let PhragmenResult {
winners,
assignments,
} = <Module<T>>::do_phragmen::<OffchainAccuracy>()
.ok_or(OffchainElectionError::ElectionFailed)?;
// process and prepare it for submission.
let (winners, compact, score) = prepare_submission::<T>(assignments, winners, true)?;
// defensive-only: active era can never be none except genesis.
let era = <Module<T>>::active_era().map(|e| e.index).unwrap_or_default();
// send it.
let call: <T as Trait>::Call = Call::submit_election_solution_unsigned(
winners,
compact,
score,
era,
).into();
T::SubmitTransaction::submit_unsigned(call)
.map_err(|_| OffchainElectionError::PoolSubmissionFailed)
}
/// Takes a phragmen result and spits out some data that can be submitted to the chain.
///
/// This does a lot of stuff; read the inline comments.
pub fn prepare_submission<T: Trait>(
assignments: Vec<Assignment<T::AccountId, OffchainAccuracy>>,
winners: Vec<(T::AccountId, ExtendedBalance)>,
do_reduce: bool,
) -> Result<(Vec<ValidatorIndex>, CompactAssignments, PhragmenScore), OffchainElectionError> where
ExtendedBalance: From<<OffchainAccuracy as PerThing>::Inner>,
{
// make sure that the snapshot is available.
let snapshot_validators =
<Module<T>>::snapshot_validators().ok_or(OffchainElectionError::SnapshotUnavailable)?;
let snapshot_nominators =
<Module<T>>::snapshot_nominators().ok_or(OffchainElectionError::SnapshotUnavailable)?;
// all helper closures
let nominator_index = |a: &T::AccountId| -> Option<NominatorIndex> {
snapshot_nominators
.iter()
.position(|x| x == a)
.and_then(|i| <usize as TryInto<NominatorIndex>>::try_into(i).ok())
};
let validator_index = |a: &T::AccountId| -> Option<ValidatorIndex> {
snapshot_validators
.iter()
.position(|x| x == a)
.and_then(|i| <usize as TryInto<ValidatorIndex>>::try_into(i).ok())
};
// Clean winners.
let winners = winners
.into_iter()
.map(|(w, _)| w)
.collect::<Vec<T::AccountId>>();
// convert into absolute value and to obtain the reduced version.
let mut staked = sp_phragmen::assignment_ratio_to_staked(
assignments,
<Module<T>>::slashable_balance_of_extended,
);
if do_reduce {
reduce(&mut staked);
}
// Convert back to ratio assignment. This takes less space.
let low_accuracy_assignment = sp_phragmen::assignment_staked_to_ratio(staked);
// convert back to staked to compute the score in the receiver's accuracy. This can be done
// nicer, for now we do it as such since this code is not time-critical. This ensure that the
// score _predicted_ here is the same as the one computed on chain and you will not get a
// `PhragmenBogusScore` error. This is totally NOT needed if we don't do reduce. This whole
// _accuracy glitch_ happens because reduce breaks that assumption of rounding and **scale**.
// The initial phragmen results are computed in `OffchainAccuracy` and the initial `staked`
// assignment set is also all multiples of this value. After reduce, this no longer holds. Hence
// converting to ratio thereafter is not trivially reversible.
let score = {
let staked = sp_phragmen::assignment_ratio_to_staked(
low_accuracy_assignment.clone(),
<Module<T>>::slashable_balance_of_extended,
);
let (support_map, _) = build_support_map::<T::AccountId>(&winners, &staked);
evaluate_support::<T::AccountId>(&support_map)
};
// compact encode the assignment.
let compact = CompactAssignments::from_assignment(
low_accuracy_assignment,
nominator_index,
validator_index,
).map_err(|e| OffchainElectionError::from(e))?;
// winners to index. Use a simple for loop for a more expressive early exit in case of error.
let mut winners_indexed: Vec<ValidatorIndex> = Vec::with_capacity(winners.len());
for w in winners {
if let Some(idx) = snapshot_validators.iter().position(|v| *v == w) {
let compact_index: ValidatorIndex = idx
.try_into()
.map_err(|_| OffchainElectionError::InvalidWinner)?;
winners_indexed.push(compact_index);
} else {
return Err(OffchainElectionError::InvalidWinner);
}
}
Ok((winners_indexed, compact, score))
}
+8 -8
View File
@@ -16,11 +16,11 @@
//! A slashing implementation for NPoS systems.
//!
//! For the purposes of the economic model, it is easiest to think of each validator
//! of a nominator which nominates only its own identity.
//! For the purposes of the economic model, it is easiest to think of each validator as a nominator
//! which nominates only its own identity.
//!
//! The act of nomination signals intent to unify economic identity with the validator - to take part in the
//! rewards of a job well done, and to take part in the punishment of a job done badly.
//! The act of nomination signals intent to unify economic identity with the validator - to take
//! part in the rewards of a job well done, and to take part in the punishment of a job done badly.
//!
//! There are 3 main difficulties to account for with slashing in NPoS:
//! - A nominator can nominate multiple validators and be slashed via any of them.
@@ -52,7 +52,7 @@ use super::{
EraIndex, Trait, Module, Store, BalanceOf, Exposure, Perbill, SessionInterface,
NegativeImbalanceOf, UnappliedSlash,
};
use sp_runtime::{traits::{Zero, Saturating}, PerThing};
use sp_runtime::{traits::{Zero, Saturating}, PerThing, RuntimeDebug};
use frame_support::{
StorageMap, StorageDoubleMap,
traits::{Currency, OnUnbalanced, Imbalance},
@@ -65,7 +65,7 @@ use codec::{Encode, Decode};
const REWARD_F1: Perbill = Perbill::from_percent(50);
/// The index of a slashing span - unique to each stash.
pub(crate) type SpanIndex = u32;
pub type SpanIndex = u32;
// A range of start..end eras for a slashing span.
#[derive(Encode, Decode)]
@@ -83,7 +83,7 @@ impl SlashingSpan {
}
/// An encoding of all of a nominator's slashing spans.
#[derive(Encode, Decode)]
#[derive(Encode, Decode, RuntimeDebug)]
pub struct SlashingSpans {
// the index of the current slashing span of the nominator. different for
// every stash, resets when the account hits free balance 0.
@@ -143,7 +143,7 @@ impl SlashingSpans {
}
/// Yields the era index where the most recent non-zero slash occurred.
pub(crate) fn last_nonzero_slash(&self) -> EraIndex {
pub fn last_nonzero_slash(&self) -> EraIndex {
self.last_nonzero_slash
}
@@ -0,0 +1,340 @@
// Copyright 2020 Parity Technologies (UK) Ltd.
// This file is part of Substrate.
// Substrate is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Substrate is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Substrate. If not, see <http://www.gnu.org/licenses/>.
//! Testing utils for staking. Needs the `testing-utils` feature to be enabled.
//!
//! Note that these helpers should NOT be used with the actual crate tests, but are rather designed
//! for when the module is being externally tested (i.e. fuzzing, benchmarking, e2e tests). Enabling
//! this feature in the current crate's Cargo.toml will leak the all of this into a normal release
//! build. Just don't do it.
use crate::*;
use codec::{Decode, Encode};
use frame_support::assert_ok;
use frame_system::RawOrigin;
use pallet_indices::address::Address;
use rand::Rng;
use sp_core::hashing::blake2_256;
use sp_phragmen::{
build_support_map, evaluate_support, reduce, Assignment, PhragmenScore, StakedAssignment,
};
const CTRL_PREFIX: u32 = 1000;
const NOMINATOR_PREFIX: u32 = 1_000_000;
/// A dummy suer.
pub const USER: u32 = 999_999_999;
/// Address type of the `T`
pub type AddressOf<T> = Address<<T as frame_system::Trait>::AccountId, u32>;
/// Random number in the range `[a, b]`.
pub fn random(a: u32, b: u32) -> u32 {
rand::thread_rng().gen_range(a, b)
}
/// Set the desired validator count, with related storage items.
pub fn set_validator_count<T: Trait>(to_elect: u32) {
ValidatorCount::put(to_elect);
MinimumValidatorCount::put(to_elect / 2);
<EraElectionStatus<T>>::put(ElectionStatus::Open(T::BlockNumber::from(1u32)));
}
/// Build an account with the given index.
pub fn account<T: Trait>(index: u32) -> T::AccountId {
let entropy = (b"benchmark/staking", index).using_encoded(blake2_256);
T::AccountId::decode(&mut &entropy[..]).unwrap_or_default()
}
/// Build an address given Index
pub fn address<T: Trait>(index: u32) -> AddressOf<T> {
pallet_indices::address::Address::Id(account::<T>(index))
}
/// Generate signed origin from `who`.
pub fn signed<T: Trait>(who: T::AccountId) -> T::Origin {
RawOrigin::Signed(who).into()
}
/// Generate signed origin from `index`.
pub fn signed_account<T: Trait>(index: u32) -> T::Origin {
signed::<T>(account::<T>(index))
}
/// Bond a validator.
pub fn bond_validator<T: Trait>(stash: T::AccountId, ctrl: u32, val: BalanceOf<T>)
where
T::Lookup: StaticLookup<Source = AddressOf<T>>,
{
let _ = T::Currency::make_free_balance_be(&stash, val);
assert_ok!(<Module<T>>::bond(
signed::<T>(stash),
address::<T>(ctrl),
val,
RewardDestination::Controller
));
assert_ok!(<Module<T>>::validate(
signed_account::<T>(ctrl),
ValidatorPrefs::default()
));
}
pub fn bond_nominator<T: Trait>(
stash: T::AccountId,
ctrl: u32,
val: BalanceOf<T>,
target: Vec<AddressOf<T>>,
) where
T::Lookup: StaticLookup<Source = AddressOf<T>>,
{
let _ = T::Currency::make_free_balance_be(&stash, val);
assert_ok!(<Module<T>>::bond(
signed::<T>(stash),
address::<T>(ctrl),
val,
RewardDestination::Controller
));
assert_ok!(<Module<T>>::nominate(signed_account::<T>(ctrl), target));
}
/// Bond `nun_validators` validators and `num_nominator` nominators with `edge_per_voter` random
/// votes per nominator.
pub fn setup_chain_stakers<T: Trait>(num_validators: u32, num_voters: u32, edge_per_voter: u32)
where
T::Lookup: StaticLookup<Source = AddressOf<T>>,
{
(0..num_validators).for_each(|i| {
bond_validator::<T>(
account::<T>(i),
i + CTRL_PREFIX,
<BalanceOf<T>>::from(random(1, 1000)) * T::Currency::minimum_balance(),
);
});
(0..num_voters).for_each(|i| {
let mut targets: Vec<AddressOf<T>> = Vec::with_capacity(edge_per_voter as usize);
let mut all_targets = (0..num_validators)
.map(|t| address::<T>(t))
.collect::<Vec<_>>();
assert!(num_validators >= edge_per_voter);
(0..edge_per_voter).for_each(|_| {
let target = all_targets.remove(random(0, all_targets.len() as u32 - 1) as usize);
targets.push(target);
});
bond_nominator::<T>(
account::<T>(i + NOMINATOR_PREFIX),
i + NOMINATOR_PREFIX + CTRL_PREFIX,
<BalanceOf<T>>::from(random(1, 1000)) * T::Currency::minimum_balance(),
targets,
);
});
<Module<T>>::create_stakers_snapshot();
}
/// Build a _really bad_ but acceptable solution for election. This should always yield a solution
/// which has a less score than the seq-phragmen.
pub fn get_weak_solution<T: Trait>(
do_reduce: bool,
) -> (Vec<ValidatorIndex>, CompactAssignments, PhragmenScore) {
let mut backing_stake_of: BTreeMap<T::AccountId, BalanceOf<T>> = BTreeMap::new();
// self stake
<Validators<T>>::enumerate().for_each(|(who, _p)| {
*backing_stake_of.entry(who.clone()).or_insert(Zero::zero()) +=
<Module<T>>::slashable_balance_of(&who)
});
// add nominator stuff
<Nominators<T>>::enumerate().for_each(|(who, nomination)| {
nomination.targets.into_iter().for_each(|v| {
*backing_stake_of.entry(v).or_insert(Zero::zero()) +=
<Module<T>>::slashable_balance_of(&who)
})
});
// elect winners
let mut sorted: Vec<T::AccountId> = backing_stake_of.keys().cloned().collect();
sorted.sort_by_key(|x| backing_stake_of.get(x).unwrap());
let winners: Vec<T::AccountId> = sorted
.iter()
.cloned()
.take(<Module<T>>::validator_count() as usize)
.collect();
let mut staked_assignments: Vec<StakedAssignment<T::AccountId>> = Vec::new();
<Nominators<T>>::enumerate().for_each(|(who, nomination)| {
let mut dist: Vec<(T::AccountId, ExtendedBalance)> = Vec::new();
nomination.targets.into_iter().for_each(|v| {
if winners.iter().find(|&w| *w == v).is_some() {
dist.push((v, ExtendedBalance::zero()));
}
});
if dist.len() == 0 {
return;
}
// assign real stakes. just split the stake.
let stake = <T::CurrencyToVote as Convert<BalanceOf<T>, u64>>::convert(
<Module<T>>::slashable_balance_of(&who),
) as ExtendedBalance;
let mut sum: ExtendedBalance = Zero::zero();
let dist_len = dist.len() as ExtendedBalance;
// assign main portion
// only take the first half into account. This should highly imbalance stuff, which is good.
dist.iter_mut()
.take(if dist_len > 1 {
(dist_len as usize) / 2
} else {
1
})
.for_each(|(_, w)| {
let partial = stake / dist_len;
*w = partial;
sum += partial;
});
// assign the leftover to last.
let leftover = stake - sum;
let last = dist.last_mut().unwrap();
last.1 += leftover;
staked_assignments.push(StakedAssignment {
who,
distribution: dist,
});
});
// add self support to winners.
winners.iter().for_each(|w| {
staked_assignments.push(StakedAssignment {
who: w.clone(),
distribution: vec![(
w.clone(),
<T::CurrencyToVote as Convert<BalanceOf<T>, u64>>::convert(
<Module<T>>::slashable_balance_of(&w),
) as ExtendedBalance,
)],
})
});
if do_reduce {
reduce(&mut staked_assignments);
}
// helpers for building the compact
let snapshot_validators = <Module<T>>::snapshot_validators().unwrap();
let snapshot_nominators = <Module<T>>::snapshot_nominators().unwrap();
let nominator_index = |a: &T::AccountId| -> Option<NominatorIndex> {
snapshot_nominators
.iter()
.position(|x| x == a)
.and_then(|i| <usize as TryInto<NominatorIndex>>::try_into(i).ok())
};
let validator_index = |a: &T::AccountId| -> Option<ValidatorIndex> {
snapshot_validators
.iter()
.position(|x| x == a)
.and_then(|i| <usize as TryInto<ValidatorIndex>>::try_into(i).ok())
};
let stake_of = |who: &T::AccountId| -> ExtendedBalance {
<T::CurrencyToVote as Convert<BalanceOf<T>, u64>>::convert(
<Module<T>>::slashable_balance_of(who),
) as ExtendedBalance
};
// convert back to ratio assignment. This takes less space.
let low_accuracy_assignment: Vec<Assignment<T::AccountId, OffchainAccuracy>> =
staked_assignments
.into_iter()
.map(|sa| sa.into_assignment(true))
.collect();
// re-calculate score based on what the chain will decode.
let score = {
let staked: Vec<StakedAssignment<T::AccountId>> = low_accuracy_assignment
.iter()
.map(|a| {
let stake = stake_of(&a.who);
a.clone().into_staked(stake, true)
})
.collect();
let (support_map, _) =
build_support_map::<T::AccountId>(winners.as_slice(), staked.as_slice());
evaluate_support::<T::AccountId>(&support_map)
};
// compact encode the assignment.
let compact = CompactAssignments::from_assignment(
low_accuracy_assignment,
nominator_index,
validator_index,
)
.unwrap();
// winners to index.
let winners = winners
.into_iter()
.map(|w| {
snapshot_validators
.iter()
.position(|v| *v == w)
.unwrap()
.try_into()
.unwrap()
})
.collect::<Vec<ValidatorIndex>>();
(winners, compact, score)
}
/// Create a solution for seq-phragmen. This uses the same internal function as used by the offchain
/// worker code.
pub fn get_seq_phragmen_solution<T: Trait>(
do_reduce: bool,
) -> (Vec<ValidatorIndex>, CompactAssignments, PhragmenScore) {
let sp_phragmen::PhragmenResult {
winners,
assignments,
} = <Module<T>>::do_phragmen::<OffchainAccuracy>().unwrap();
offchain_election::prepare_submission::<T>(assignments, winners, do_reduce).unwrap()
}
/// Remove all validator, nominators, votes and exposures.
pub fn clean<T: Trait>(era: EraIndex)
where
<T as frame_system::Trait>::AccountId: codec::EncodeLike<u32>,
u32: codec::EncodeLike<T::AccountId>,
{
<Validators<T>>::enumerate().for_each(|(k, _)| {
let ctrl = <Module<T>>::bonded(&k).unwrap();
<Bonded<T>>::remove(&k);
<Validators<T>>::remove(&k);
<Ledger<T>>::remove(&ctrl);
<ErasStakers<T>>::remove(k, era);
});
<Nominators<T>>::enumerate().for_each(|(k, _)| <Nominators<T>>::remove(k));
<Ledger<T>>::remove_all();
<Bonded<T>>::remove_all();
<QueuedElected<T>>::kill();
QueuedScore::kill();
}
File diff suppressed because it is too large Load Diff