Skip to content
This repository has been archived by the owner on Nov 15, 2023. It is now read-only.

srml-module: Phragmen election #3364

Merged
merged 55 commits into from
Sep 19, 2019
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
8717ccb
phragmen election module.
kianenigma Aug 12, 2019
8e5d5ee
Add new files.
kianenigma Aug 12, 2019
f7d8c13
Some doc update
kianenigma Aug 12, 2019
15506ab
Merge branch 'master' of github.com:paritytech/substrate into kiz-phr…
kianenigma Aug 12, 2019
ac3078c
Update weights.
kianenigma Aug 12, 2019
edcdee1
bump and a few nits.
kianenigma Aug 12, 2019
33b9409
Performance improvement.
kianenigma Aug 13, 2019
141f6be
Merge branch 'master' of github.com:paritytech/substrate into kiz-phr…
kianenigma Aug 13, 2019
9cc03ce
Master.into()
kianenigma Aug 13, 2019
bd7c642
Remote.into()
kianenigma Aug 13, 2019
bb81b9f
Update srml/elections-phragmen/src/lib.rs
kianenigma Aug 14, 2019
b456c9b
Fix build
kianenigma Aug 15, 2019
6433a81
Some fixes.
kianenigma Aug 15, 2019
14da1e2
Master.into()
kianenigma Aug 15, 2019
3d90cd3
Fix build.
kianenigma Aug 15, 2019
88e8108
Proper outgoing and runner-up managment.
kianenigma Aug 17, 2019
cf15877
Bit more sensical weight values.
kianenigma Aug 17, 2019
0e8af21
Update srml/elections-phragmen/src/lib.rs
gavofyork Aug 22, 2019
c4cf8bc
Update srml/elections-phragmen/src/lib.rs
kianenigma Aug 22, 2019
20415ca
Update srml/elections-phragmen/src/lib.rs
kianenigma Aug 22, 2019
6e5c780
Update srml/elections-phragmen/src/lib.rs
kianenigma Aug 22, 2019
ba41fb8
Master.into()
kianenigma Aug 22, 2019
dbfaec3
fix lock file
kianenigma Aug 22, 2019
c163efe
Merge branch 'master' of github.com:paritytech/substrate into kiz-phr…
kianenigma Aug 22, 2019
12bd3c3
Fix build.
kianenigma Aug 22, 2019
aefea27
Remove runner-ups
kianenigma Aug 22, 2019
19af37b
Some refactors.
kianenigma Aug 23, 2019
325c43f
Master.into()
kianenigma Aug 29, 2019
840737c
Add support for reporting voters.
kianenigma Aug 30, 2019
e9c8e34
Fix member check.
kianenigma Aug 30, 2019
308feaf
Remove equlize.rs
kianenigma Aug 30, 2019
75d8f19
Update srml/elections-phragmen/src/lib.rs
gavofyork Sep 2, 2019
44cb54d
Update srml/elections-phragmen/src/lib.rs
gavofyork Sep 2, 2019
8ef41eb
Update srml/elections-phragmen/src/lib.rs
kianenigma Sep 5, 2019
b7a1dc8
Update srml/elections-phragmen/src/lib.rs
kianenigma Sep 5, 2019
fd1c772
Bring back runner ups.
kianenigma Sep 5, 2019
d01edec
Master.into()
kianenigma Sep 5, 2019
a8590fa
use decode_len
kianenigma Sep 5, 2019
6debacb
Better weight values.
kianenigma Sep 5, 2019
28e9372
Update bogus doc
kianenigma Sep 9, 2019
e6f001a
Merge branch 'master' of github.com:paritytech/substrate into kiz-phr…
kianenigma Sep 9, 2019
016e5ff
Bump.
kianenigma Sep 9, 2019
7e4a4ed
Update srml/elections-phragmen/src/lib.rs
kianenigma Sep 9, 2019
dec0b55
Master.into()
kianenigma Sep 12, 2019
f3f2be3
Merge branch 'kiz-phragmen-election' of github.com:paritytech/substra…
kianenigma Sep 12, 2019
b76d95f
Review comments.
kianenigma Sep 12, 2019
8f7f589
One more test
kianenigma Sep 13, 2019
ad94542
Master.into()
kianenigma Sep 14, 2019
509387e
Fix tests
kianenigma Sep 15, 2019
a6e1a3e
Fix build
kianenigma Sep 15, 2019
1adc36b
.. and fix benchmarks.
kianenigma Sep 15, 2019
0375443
Master.into()
kianenigma Sep 18, 2019
3ae3121
Update srml/elections-phragmen/src/lib.rs
gavofyork Sep 19, 2019
70ccf32
Merge remote-tracking branch 'origin/master' into kiz-phragmen-election
gavofyork Sep 19, 2019
91827db
Version bump
gavofyork Sep 19, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ members = [
"srml/collective",
"srml/democracy",
"srml/elections",
"srml/elections-phragmen",
"srml/example",
"srml/executive",
"srml/finality-tracker",
Expand Down
4 changes: 3 additions & 1 deletion core/sr-primitives/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ use codec::{Encode, Decode, CompactAs};
pub mod testing;

pub mod weights;
pub mod phragmen;
pub mod traits;
use traits::{SaturatedConversion, UniqueSaturatedInto, Saturating, Bounded, CheckedSub, CheckedAdd};

pub mod generic;
pub mod transaction_validity;
Expand All @@ -58,6 +58,8 @@ pub use generic::{DigestItem, Digest};
pub use primitives::crypto::{key_types, KeyTypeId, CryptoType};
pub use app_crypto::AppKey;

use traits::{SaturatedConversion, UniqueSaturatedInto, Saturating, Bounded, CheckedSub, CheckedAdd};

/// A message indicating an invalid signature in extrinsic.
pub const BAD_SIGNATURE: &str = "bad signature in extrinsic";

Expand Down
231 changes: 61 additions & 170 deletions srml/staking/src/phragmen.rs → core/sr-primitives/src/phragmen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@
//! Rust implementation of the Phragmén election algorithm.

use rstd::{prelude::*, collections::btree_map::BTreeMap};
use sr_primitives::{PerU128};
use sr_primitives::traits::{Zero, Convert, Saturating};
use crate::{BalanceOf, RawAssignment, ExpoMap, Trait, ValidatorPrefs, IndividualExposure};
use crate::PerU128;
use crate::traits::{Zero, Convert, Member, SimpleArithmetic, MaybeDebug};

/// Type used as the fraction.
type Fraction = PerU128;
/// Wrapper around the type used as the _safe_ wrapper around a `balance`.
pub type ExtendedBalance = u128;
Expand Down Expand Up @@ -75,6 +75,9 @@ pub struct Edge<AccountId> {
candidate_index: usize,
}

/// A ratio of a nominator's (`A`) vote, indicated by `ExtendedBalance` / `ACCURACY`.
pub type PhragmenAssignment<A> = (A, ExtendedBalance);

/// Perform election based on Phragmén algorithm.
///
/// Reference implementation: https://github.com/w3f/consensus
Expand All @@ -85,60 +88,71 @@ pub struct Edge<AccountId> {
/// The returned Option is a tuple consisting of:
/// - The list of elected candidates.
/// - The list of nominators and their associated vote weights.
pub fn elect<T: Trait + 'static, FV, FN, FS>(
pub fn elect<AccountId, Balance, FS, C>(
validator_count: usize,
self_vote: bool,
minimum_validator_count: usize,
validator_iter: FV,
nominator_iter: FN,
initial_candidates: Vec<AccountId>,
initial_voters: Vec<(AccountId, Vec<AccountId>)>,
slashable_balance_of: FS,
) -> Option<(Vec<T::AccountId>, Vec<(T::AccountId, Vec<RawAssignment<T>>)>)> where
FV: Iterator<Item=(T::AccountId, ValidatorPrefs<BalanceOf<T>>)>,
FN: Iterator<Item=(T::AccountId, Vec<T::AccountId>)>,
for <'r> FS: Fn(&'r T::AccountId) -> BalanceOf<T>,
) -> Option<(Vec<(AccountId, ExtendedBalance)>, Vec<(AccountId, Vec<PhragmenAssignment<AccountId>>)>)> where
AccountId: Default + Ord + Member,
Balance: Default + Copy + SimpleArithmetic + MaybeDebug,
for <'r> FS: Fn(&'r AccountId) -> Balance,
C: Convert<Balance, u64> + Convert<u128, Balance>,
{
let to_votes = |b: BalanceOf<T>| <T::CurrencyToVote as Convert<BalanceOf<T>, u64>>::convert(b) as ExtendedBalance;
let to_votes = |b: Balance|
<C as Convert<Balance, u64>>::convert(b) as ExtendedBalance;

// return structures
let mut elected_candidates: Vec<T::AccountId>;
let mut assigned: Vec<(T::AccountId, Vec<RawAssignment<T>>)>;
let mut c_idx_cache = BTreeMap::<T::AccountId, usize>::new();
let mut elected_candidates: Vec<(AccountId, ExtendedBalance)>;
let mut assigned: Vec<(AccountId, Vec<PhragmenAssignment<AccountId>>)>;
let mut c_idx_cache = BTreeMap::<AccountId, usize>::new();

// 1- Pre-process candidates and place them in a container, optimisation and add phantom votes.
// Candidates who have 0 stake => have no votes or all null-votes. Kick them out not.
let mut nominators: Vec<Nominator<T::AccountId>> =
Vec::with_capacity(validator_iter.size_hint().0 + nominator_iter.size_hint().0);
let mut candidates = validator_iter.map(|(who, _)| {
let stash_balance = slashable_balance_of(&who);
(Candidate { who, ..Default::default() }, stash_balance)
})
.filter_map(|(mut c, s)| {
c.approval_stake += to_votes(s);
if c.approval_stake.is_zero() {
None
} else {
Some((c, s))
}
})
.enumerate()
.map(|(idx, (c, s))| {
nominators.push(Nominator {
who: c.who.clone(),
edges: vec![ Edge { who: c.who.clone(), candidate_index: idx, ..Default::default() }],
budget: to_votes(s),
load: Fraction::zero(),
});
c_idx_cache.insert(c.who.clone(), idx);
c
})
.collect::<Vec<Candidate<T::AccountId>>>();
let mut nominators: Vec<Nominator<AccountId>> = Vec::with_capacity(initial_candidates.len() + initial_voters.len());
let mut candidates = if self_vote {
initial_candidates.into_iter().map(|who| {
let stake = slashable_balance_of(&who);
Candidate { who, approval_stake: to_votes(stake), ..Default::default() }
})
.filter_map(|c| {
if c.approval_stake.is_zero() {
None
} else {
Some(c)
}
})
.enumerate()
.map(|(idx, c)| {
nominators.push(Nominator {
who: c.who.clone(),
edges: vec![ Edge { who: c.who.clone(), candidate_index: idx, ..Default::default() }],
budget: c.approval_stake,
load: Fraction::zero(),
});
c_idx_cache.insert(c.who.clone(), idx);
c
})
.collect::<Vec<Candidate<AccountId>>>()
} else {
initial_candidates.into_iter()
.enumerate()
.map(|(idx, who)| {
c_idx_cache.insert(who.clone(), idx);
Candidate { who, ..Default::default() }
})
.collect::<Vec<Candidate<AccountId>>>()
};

// 2- Collect the nominators with the associated votes.
// Also collect approval stake along the way.
nominators.extend(nominator_iter.map(|(who, nominees)| {
nominators.extend(initial_voters.into_iter().map(|(who, nominees)| {
let nominator_stake = slashable_balance_of(&who);
let mut edges: Vec<Edge<T::AccountId>> = Vec::with_capacity(nominees.len());
for n in &nominees {
if let Some(idx) = c_idx_cache.get(n) {
let mut edges: Vec<Edge<AccountId>> = Vec::with_capacity(nominees.len());
for n in nominees {
if let Some(idx) = c_idx_cache.get(&n) {
// This candidate is valid + already cached.
candidates[*idx].approval_stake = candidates[*idx].approval_stake
.saturating_add(to_votes(nominator_stake));
Expand Down Expand Up @@ -202,7 +216,7 @@ pub fn elect<T: Trait + 'static, FV, FN, FS>(
}
}

elected_candidates.push(winner.who.clone());
elected_candidates.push((winner.who.clone(), winner.approval_stake));
} else {
break
}
Expand All @@ -212,8 +226,8 @@ pub fn elect<T: Trait + 'static, FV, FN, FS>(
for n in &mut nominators {
let mut assignment = (n.who.clone(), vec![]);
for e in &mut n.edges {
if let Some(c) = elected_candidates.iter().find(|c| **c == e.who) {
if *c != n.who {
if let Some(c) = elected_candidates.iter().find(|(c, _)| *c == e.who) {
if c.0 != n.who {
let ratio = {
// Full support. No need to calculate.
if *n.load == *e.load { ACCURACY }
Expand Down Expand Up @@ -268,126 +282,3 @@ pub fn elect<T: Trait + 'static, FV, FN, FS>(
}
Some((elected_candidates, assigned))
}

/// Performs equalize post-processing to the output of the election algorithm
/// This function mutates the input parameters, most noticeably it updates the exposure of
/// the elected candidates.
///
/// No value is returned from the function and the `expo_map` parameter is updated.
pub fn equalize<T: Trait + 'static>(
assignments: &mut Vec<(T::AccountId, BalanceOf<T>, Vec<(T::AccountId, ExtendedBalance, ExtendedBalance)>)>,
expo_map: &mut ExpoMap<T>,
tolerance: ExtendedBalance,
iterations: usize,
) {
for _i in 0..iterations {
let mut max_diff = 0;
assignments.iter_mut().for_each(|(n, budget, assignment)| {
let diff = do_equalize::<T>(&n, *budget, assignment, expo_map, tolerance);
if diff > max_diff {
max_diff = diff;
}
});
if max_diff < tolerance {
break;
}
}
}

fn do_equalize<T: Trait + 'static>(
nominator: &T::AccountId,
budget_balance: BalanceOf<T>,
elected_edges: &mut Vec<(T::AccountId, ExtendedBalance, ExtendedBalance)>,
expo_map: &mut ExpoMap<T>,
tolerance: ExtendedBalance
) -> ExtendedBalance {
let to_votes = |b: BalanceOf<T>|
<T::CurrencyToVote as Convert<BalanceOf<T>, u64>>::convert(b) as ExtendedBalance;
let to_balance = |v: ExtendedBalance|
<T::CurrencyToVote as Convert<ExtendedBalance, BalanceOf<T>>>::convert(v);
let budget = to_votes(budget_balance);

// Nothing to do. This nominator had nothing useful.
// Defensive only. Assignment list should always be populated.
if elected_edges.is_empty() { return 0; }

let stake_used = elected_edges
.iter()
.fold(0 as ExtendedBalance, |s, e| s.saturating_add(e.2));

let backed_stakes_iter = elected_edges
.iter()
.filter_map(|e| expo_map.get(&e.0))
.map(|e| to_votes(e.total));

let backing_backed_stake = elected_edges
.iter()
.filter(|e| e.2 > 0)
.filter_map(|e| expo_map.get(&e.0))
.map(|e| to_votes(e.total))
.collect::<Vec<ExtendedBalance>>();

let mut difference;
if backing_backed_stake.len() > 0 {
let max_stake = backing_backed_stake
.iter()
.max()
.expect("vector with positive length will have a max; qed");
let min_stake = backed_stakes_iter
.min()
.expect("iterator with positive length will have a min; qed");

difference = max_stake.saturating_sub(min_stake);
difference = difference.saturating_add(budget.saturating_sub(stake_used));
if difference < tolerance {
return difference;
}
} else {
difference = budget;
}

// Undo updates to exposure
elected_edges.iter_mut().for_each(|e| {
if let Some(expo) = expo_map.get_mut(&e.0) {
expo.total = expo.total.saturating_sub(to_balance(e.2));
expo.others.retain(|i_expo| i_expo.who != *nominator);
}
e.2 = 0;
});

elected_edges.sort_unstable_by_key(|e|
if let Some(e) = expo_map.get(&e.0) { e.total } else { Zero::zero() }
);

let mut cumulative_stake: ExtendedBalance = 0;
let mut last_index = elected_edges.len() - 1;
elected_edges.iter_mut().enumerate().for_each(|(idx, e)| {
if let Some(expo) = expo_map.get_mut(&e.0) {
let stake: ExtendedBalance = to_votes(expo.total);
let stake_mul = stake.saturating_mul(idx as ExtendedBalance);
let stake_sub = stake_mul.saturating_sub(cumulative_stake);
if stake_sub > budget {
last_index = idx.checked_sub(1).unwrap_or(0);
return
}
cumulative_stake = cumulative_stake.saturating_add(stake);
}
});

let last_stake = elected_edges[last_index].2;
let split_ways = last_index + 1;
let excess = budget
.saturating_add(cumulative_stake)
.saturating_sub(last_stake.saturating_mul(split_ways as ExtendedBalance));
elected_edges.iter_mut().take(split_ways).for_each(|e| {
if let Some(expo) = expo_map.get_mut(&e.0) {
e.2 = (excess / split_ways as ExtendedBalance)
.saturating_add(last_stake)
.saturating_sub(to_votes(expo.total));
expo.total = expo.total.saturating_add(to_balance(e.2));
expo.others.push(IndividualExposure { who: nominator.clone(), value: to_balance(e.2)});
}
});

difference
}
30 changes: 30 additions & 0 deletions srml/elections-phragmen/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
[package]
name = "srml-elections-phragmen"
version = "2.0.0"
authors = ["Parity Technologies <[email protected]>"]
edition = "2018"

[dependencies]
serde = { version = "1.0", optional = true }
codec = { package = "parity-scale-codec", version = "1.0.0", default-features = false, features = ["derive"] }
primitives = { package = "substrate-primitives", path = "../../core/primitives", default-features = false }
runtime_io = { package = "sr-io", path = "../../core/sr-io", default-features = false }
sr-primitives = { path = "../../core/sr-primitives", default-features = false }
srml-support = { path = "../support", default-features = false }
system = { package = "srml-system", path = "../system", default-features = false }

[dev-dependencies]
hex-literal = "0.2.0"
balances = { package = "srml-balances", path = "../balances" }

[features]
default = ["std"]
std = [
"codec/std",
"primitives/std",
"serde",
"runtime_io/std",
"srml-support/std",
"sr-primitives/std",
"system/std",
]
Loading