First draft of offchain phragmen weights (#6032)

* Fist draft of offchain weights

* Round of review feedback

* Update frame/staking/src/lib.rs

* Fix fuzzer

* Remove some redundant comment

* Weight refund for submit solution -- potentially revert.

* First version with custom trimming of the result.

* Update frame/staking/src/benchmarking.rs

Co-authored-by: Alexander Popiak <alexander.popiak@parity.io>

* Update frame/staking/src/benchmarking.rs

Co-authored-by: Alexander Popiak <alexander.popiak@parity.io>

* Apply suggestions from code review

Co-authored-by: Alexander Popiak <alexander.popiak@parity.io>
Co-authored-by: thiolliere <gui.thiolliere@gmail.com>

* Update frame/staking/src/benchmarking.rs

Co-authored-by: Alexander Popiak <alexander.popiak@parity.io>

* Update frame/staking/src/benchmarking.rs

Co-authored-by: Alexander Popiak <alexander.popiak@parity.io>

* Some improvements

* Benchmark submit solution without phragmen (PR for First draft of offchain phragmen weights) (#6073)

* implementation of new benchmark

* address comments

* replace test

* Update frame/staking/src/lib.rs

Co-authored-by: Alexander Popiak <alexander.popiak@parity.io>

* update weight

* Fix refund

* Clean and rady for final bench

* Fix line-wdith

* Fix gitlab build

* Fix line-wdith

* Fix test macro

* Update frame/staking/src/lib.rs

Co-authored-by: Alexander Popiak <alexander.popiak@parity.io>

* Update frame/staking/src/benchmarking.rs

Co-authored-by: Alexander Popiak <alexander.popiak@parity.io>

* Better length check

* Update frame/staking/src/lib.rs

Co-authored-by: Alexander Popiak <alexander.popiak@parity.io>

* Update final weight coefficients

* Update frame/staking/src/lib.rs

* Apply suggestions from code review

* Update frame/staking/src/testing_utils.rs

* Try and fix the line-width

* Revert "Try and fix the line-width"

This reverts commit b4e284727220085b9b3daf7682c4bbf29621da09.

* Try and fix the line-width the correct way

* Revert "Try and fix the line-width the correct way"

This reverts commit 04fce128e851c9584f9f0d708a5a73cae799d8c8.

Co-authored-by: Alexander Popiak <alexander.popiak@parity.io>
Co-authored-by: thiolliere <gui.thiolliere@gmail.com>
Co-authored-by: Gavin Wood <gavin@parity.io>
This commit is contained in:
Kian Paimani
2020-05-23 20:08:42 +02:00
committed by GitHub
parent 82a832bc3a
commit 0133185c81
14 changed files with 1201 additions and 854 deletions
+182 -200
View File
@@ -15,142 +15,130 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//! 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 all of this into a normal release
//! build. Just don't do it.
//! Testing utils for staking. Provides some common functions to setup staking state, such as
//! bonding validators, nominators, and generating different types of solutions.
use crate::*;
use codec::{Decode, Encode};
use frame_support::assert_ok;
use crate::Module as Staking;
use frame_benchmarking::{account};
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,
};
use sp_io::hashing::blake2_256;
use rand_chacha::{rand_core::{RngCore, SeedableRng}, ChaChaRng};
use sp_phragmen::*;
const CTRL_PREFIX: u32 = 1000;
const NOMINATOR_PREFIX: u32 = 1_000_000;
const SEED: u32 = 0;
/// 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)
/// Grab a funded user.
pub fn create_funded_user<T: Trait>(string: &'static str, n: u32, balance_factor: u32) -> T::AccountId {
let user = account(string, n, SEED);
let balance = T::Currency::minimum_balance() * balance_factor.into();
T::Currency::make_free_balance_be(&user, balance);
// ensure T::CurrencyToVote will work correctly.
T::Currency::issue(balance);
user
}
/// 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::Closed);
}
/// 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>>,
/// Create a stash and controller pair.
pub fn create_stash_controller<T: Trait>(n: u32, balance_factor: u32)
-> Result<(T::AccountId, T::AccountId), &'static str>
{
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()
));
let stash = create_funded_user::<T>("stash", n, balance_factor);
let controller = create_funded_user::<T>("controller", n, balance_factor);
let controller_lookup: <T::Lookup as StaticLookup>::Source = T::Lookup::unlookup(controller.clone());
let reward_destination = RewardDestination::Staked;
let amount = T::Currency::minimum_balance() * (balance_factor / 10).max(1).into();
Staking::<T>::bond(RawOrigin::Signed(stash.clone()).into(), controller_lookup, amount, reward_destination)?;
return Ok((stash, controller))
}
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));
/// create `max` validators.
pub fn create_validators<T: Trait>(
max: u32,
balance_factor: u32,
) -> Result<Vec<<T::Lookup as StaticLookup>::Source>, &'static str> {
let mut validators: Vec<<T::Lookup as StaticLookup>::Source> = Vec::with_capacity(max as usize);
for i in 0 .. max {
let (stash, controller) = create_stash_controller::<T>(i, balance_factor)?;
let validator_prefs = ValidatorPrefs {
commission: Perbill::from_percent(50),
};
Staking::<T>::validate(RawOrigin::Signed(controller).into(), validator_prefs)?;
let stash_lookup: <T::Lookup as StaticLookup>::Source = T::Lookup::unlookup(stash);
validators.push(stash_lookup);
}
Ok(validators)
}
/// 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(),
);
});
/// This function generates validators and nominators who are randomly nominating
/// `edge_per_nominator` random validators (until `to_nominate` if provided).
///
/// Parameters:
/// - `validators`: number of bonded validators
/// - `nominators`: number of bonded nominators.
/// - `edge_per_nominator`: number of edge (vote) per nominator.
/// - `randomize_stake`: whether to randomize the stakes.
/// - `to_nominate`: if `Some(n)`, only the first `n` bonded validator are voted upon.
/// Else, all of them are considered and `edge_per_nominator` random validators are voted for.
///
/// Return the validators choosen to be nominated.
pub fn create_validators_with_nominators_for_era<T: Trait>(
validators: u32,
nominators: u32,
edge_per_nominator: usize,
randomize_stake: bool,
to_nominate: Option<u32>,
) -> Result<Vec<<T::Lookup as StaticLookup>::Source>, &'static str> {
let mut validators_stash: Vec<<T::Lookup as StaticLookup>::Source>
= Vec::with_capacity(validators as usize);
let mut rng = ChaChaRng::from_seed(SEED.using_encoded(blake2_256));
(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,
);
});
// Create validators
for i in 0 .. validators {
let balance_factor = if randomize_stake { rng.next_u32() % 255 + 10 } else { 100u32 };
let (v_stash, v_controller) = create_stash_controller::<T>(i, balance_factor)?;
let validator_prefs = ValidatorPrefs {
commission: Perbill::from_percent(50),
};
Staking::<T>::validate(RawOrigin::Signed(v_controller.clone()).into(), validator_prefs)?;
let stash_lookup: <T::Lookup as StaticLookup>::Source = T::Lookup::unlookup(v_stash.clone());
validators_stash.push(stash_lookup.clone());
}
<Module<T>>::create_stakers_snapshot();
let to_nominate = to_nominate.unwrap_or(validators_stash.len() as u32) as usize;
let validator_choosen = validators_stash[0..to_nominate].to_vec();
// Create nominators
for j in 0 .. nominators {
let balance_factor = if randomize_stake { rng.next_u32() % 255 + 10 } else { 100u32 };
let (_n_stash, n_controller) = create_stash_controller::<T>(
u32::max_value() - j,
balance_factor,
)?;
// Have them randomly validate
let mut available_validators = validator_choosen.clone();
let mut selected_validators: Vec<<T::Lookup as StaticLookup>::Source> =
Vec::with_capacity(edge_per_nominator);
for _ in 0 .. validators.min(edge_per_nominator as u32) {
let selected = rng.next_u32() as usize % available_validators.len();
let validator = available_validators.remove(selected);
selected_validators.push(validator);
}
Staking::<T>::nominate(RawOrigin::Signed(n_controller.clone()).into(), selected_validators)?;
}
ValidatorCount::put(validators);
Ok(validator_choosen)
}
/// 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) {
) -> (Vec<ValidatorIndex>, CompactAssignments, PhragmenScore, ElectionSize) {
let mut backing_stake_of: BTreeMap<T::AccountId, BalanceOf<T>> = BTreeMap::new();
// self stake
@@ -159,68 +147,19 @@ pub fn get_weak_solution<T: Trait>(
<Module<T>>::slashable_balance_of(&who)
});
// add nominator stuff
<Nominators<T>>::iter().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
// elect winners. We chose the.. least backed ones.
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()
.rev()
.cloned()
.take(<Module<T>>::validator_count() as usize)
.collect();
let mut staked_assignments: Vec<StakedAssignment<T::AccountId>> = Vec::new();
<Nominators<T>>::iter().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,
});
});
// you could at this point start adding some of the nominator's stake, but for now we don't.
// This solution must be bad.
// add self support to winners.
winners.iter().for_each(|w| {
@@ -255,10 +194,10 @@ pub fn get_weak_solution<T: Trait>(
.position(|x| x == a)
.and_then(|i| <usize as TryInto<ValidatorIndex>>::try_into(i).ok())
};
let stake_of = |who: &T::AccountId| -> ExtendedBalance {
let stake_of = |who: &T::AccountId| -> VoteWeight {
<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.
@@ -270,13 +209,10 @@ pub fn get_weak_solution<T: Trait>(
// 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 staked = assignment_ratio_to_staked::<_, OffchainAccuracy, _>(
low_accuracy_assignment.clone(),
stake_of
);
let (support_map, _) =
build_support_map::<T::AccountId>(winners.as_slice(), staked.as_slice());
@@ -304,14 +240,19 @@ pub fn get_weak_solution<T: Trait>(
})
.collect::<Vec<ValidatorIndex>>();
(winners, compact, score)
let size = ElectionSize {
validators: snapshot_validators.len() as ValidatorIndex,
nominators: snapshot_nominators.len() as NominatorIndex,
};
(winners, compact, score, size)
}
/// 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) {
) -> (Vec<ValidatorIndex>, CompactAssignments, PhragmenScore, ElectionSize) {
let sp_phragmen::PhragmenResult {
winners,
assignments,
@@ -320,29 +261,40 @@ pub fn get_seq_phragmen_solution<T: Trait>(
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>>::iter().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>>::iter().for_each(|(k, _)| <Nominators<T>>::remove(k));
<Ledger<T>>::remove_all();
<Bonded<T>>::remove_all();
<QueuedElected<T>>::kill();
QueuedScore::kill();
/// Returns a solution in which only one winner is elected with just a self vote.
pub fn get_single_winner_solution<T: Trait>(
winner: T::AccountId
) -> Result<(Vec<ValidatorIndex>, CompactAssignments, PhragmenScore, ElectionSize), &'static str> {
let snapshot_validators = <Module<T>>::snapshot_validators().unwrap();
let snapshot_nominators = <Module<T>>::snapshot_nominators().unwrap();
let val_index = snapshot_validators.iter().position(|x| *x == winner).ok_or("not a validator")?;
let nom_index = snapshot_nominators.iter().position(|x| *x == winner).ok_or("not a nominator")?;
let stake = <Staking<T>>::slashable_balance_of(&winner);
let stake = <T::CurrencyToVote as Convert<BalanceOf<T>, VoteWeight>>::convert(stake)
as ExtendedBalance;
let val_index = val_index as ValidatorIndex;
let nom_index = nom_index as NominatorIndex;
let winners = vec![val_index];
let compact = CompactAssignments {
votes1: vec![(nom_index, val_index)],
..Default::default()
};
let score = [stake, stake, stake * stake];
let size = ElectionSize {
validators: snapshot_validators.len() as ValidatorIndex,
nominators: snapshot_nominators.len() as NominatorIndex,
};
Ok((winners, compact, score, size))
}
/// get the active era.
pub fn active_era<T: Trait>() -> EraIndex {
<Module<T>>::active_era().unwrap().index
pub fn current_era<T: Trait>() -> EraIndex {
<Module<T>>::current_era().unwrap_or(0)
}
/// initialize the first era.
@@ -352,3 +304,33 @@ pub fn init_active_era() {
start: None,
})
}
/// Create random assignments for the given list of winners. Each assignment will have
/// MAX_NOMINATIONS edges.
pub fn create_assignments_for_offchain<T: Trait>(
num_assignments: u32,
winners: Vec<<T::Lookup as StaticLookup>::Source>,
) -> Result<
(
Vec<(T::AccountId, ExtendedBalance)>,
Vec<Assignment<T::AccountId, OffchainAccuracy>>,
),
&'static str
> {
let ratio = OffchainAccuracy::from_rational_approximation(1, MAX_NOMINATIONS);
let assignments: Vec<Assignment<T::AccountId, OffchainAccuracy>> = <Nominators<T>>::iter()
.take(num_assignments as usize)
.map(|(n, t)| Assignment {
who: n,
distribution: t.targets.iter().map(|v| (v.clone(), ratio)).collect(),
})
.collect();
ensure!(assignments.len() == num_assignments as usize, "must bench for `a` assignments");
let winners = winners.into_iter().map(|v| {
(<T::Lookup as StaticLookup>::lookup(v).unwrap(), 0)
}).collect();
Ok((winners, assignments))
}