diff --git a/Cargo.lock b/Cargo.lock index 648fa3858..95b8cd542 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -811,16 +811,6 @@ dependencies = [ "sct", ] -[[package]] -name = "ctor" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd8ce37ad4184ab2ce004c33bf6379185d3b1c95801cab51026bd271bf68eedc" -dependencies = [ - "quote 1.0.2", - "syn 1.0.11", -] - [[package]] name = "ctr" version = "0.3.2" @@ -935,7 +925,6 @@ dependencies = [ "frame-system", "pallet-authorship", "pallet-session", - "pallet-staking-reward-curve", "pallet-timestamp", "parity-scale-codec", "safe-mix", @@ -977,12 +966,6 @@ dependencies = [ "syn 1.0.11", ] -[[package]] -name = "difference" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198" - [[package]] name = "digest" version = "0.8.1" @@ -3145,10 +3128,8 @@ dependencies = [ name = "node-primitives" version = "0.4.0" dependencies = [ - "pretty_assertions", "sp-core", "sp-runtime", - "sp-serializer", ] [[package]] @@ -3223,7 +3204,6 @@ dependencies = [ "sp-consensus-babe", "sp-core", "sp-inherents", - "sp-io", "sp-keyring", "sp-offchain", "sp-runtime", @@ -3398,15 +3378,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "output_vt100" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53cdc5b785b7a58c5aad8216b3dfa114df64b0b06ae6e1501cef91df2fbdf8f9" -dependencies = [ - "winapi 0.3.8", -] - [[package]] name = "owning_ref" version = "0.4.0" @@ -4104,18 +4075,6 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74490b50b9fbe561ac330df47c08f3f33073d2d00c150f719147d7c54522fa1b" -[[package]] -name = "pretty_assertions" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f81e1644e1b54f5a68959a29aa86cde704219254669da328ecfdf6a1f09d427" -dependencies = [ - "ansi_term 0.11.0", - "ctor", - "difference", - "output_vt100", -] - [[package]] name = "primitive-types" version = "0.6.1" diff --git a/bin/node/primitives/Cargo.toml b/bin/node/primitives/Cargo.toml index c86059871..119563433 100644 --- a/bin/node/primitives/Cargo.toml +++ b/bin/node/primitives/Cargo.toml @@ -8,10 +8,6 @@ edition = "2018" sp-core = { version = "2.0.0", default-features = false, git = "https://github.com/paritytech/substrate.git", rev = "c2fccb36ffacd118fc3502aa93453580a07dc402" } sp-runtime = { version = "2.0.0", default-features = false, git = "https://github.com/paritytech/substrate.git", rev = "c2fccb36ffacd118fc3502aa93453580a07dc402" } -[dev-dependencies] -sp-serializer = { version = "2.0.0", git = "https://github.com/paritytech/substrate.git", rev = "c2fccb36ffacd118fc3502aa93453580a07dc402" } -pretty_assertions = "0.6.1" - [features] default = ["std"] std = [ diff --git a/bin/node/runtime/Cargo.toml b/bin/node/runtime/Cargo.toml index 21c91040f..dc9e541c5 100644 --- a/bin/node/runtime/Cargo.toml +++ b/bin/node/runtime/Cargo.toml @@ -66,9 +66,6 @@ pallet-staking = { package = "darwinia-staking", default-features = false, featu [build-dependencies] wasm-builder-runner = { version = "1.0.4", package = "substrate-wasm-builder-runner", git = "https://github.com/paritytech/substrate.git", rev = "c2fccb36ffacd118fc3502aa93453580a07dc402" } -[dev-dependencies] -sp-io = { version = "2.0.0", git = "https://github.com/paritytech/substrate.git", rev = "c2fccb36ffacd118fc3502aa93453580a07dc402" } - [features] default = ["std"] std = [ diff --git a/bin/node/runtime/src/lib.rs b/bin/node/runtime/src/lib.rs index e0e8211ff..f18d77457 100644 --- a/bin/node/runtime/src/lib.rs +++ b/bin/node/runtime/src/lib.rs @@ -74,7 +74,7 @@ include!(concat!(env!("OUT_DIR"), "/wasm_binary.rs")); /// Runtime version. pub const VERSION: RuntimeVersion = RuntimeVersion { - spec_name: create_runtime_str!("node"), + spec_name: create_runtime_str!("darwinia"), impl_name: create_runtime_str!("darwinia-node"), authoring_version: 4, // Per convention: if the runtime behavior changes, increment spec_version @@ -240,7 +240,7 @@ impl pallet_session::Trait for Runtime { } impl pallet_session::historical::Trait for Runtime { - type FullIdentification = Exposure; + type FullIdentification = Exposure; type FullIdentificationOf = ExposureOf; } @@ -457,7 +457,6 @@ parameter_types! { impl pallet_staking::Trait for Runtime { type Time = Timestamp; - // type PowerToVotes = PowerToVotesHandler; type Event = Event; type SessionsPerEra = SessionsPerEra; type BondingDurationInEra = BondingDurationInEra; diff --git a/frame/staking/Cargo.toml b/frame/staking/Cargo.toml index 47936576f..b81f03252 100644 --- a/frame/staking/Cargo.toml +++ b/frame/staking/Cargo.toml @@ -24,14 +24,14 @@ sp-staking = { version = "2.0.0", default-features = false, git = "https://githu sp-std = { version = "2.0.0", default-features = false, git = "https://github.com/paritytech/substrate.git", rev = "c2fccb36ffacd118fc3502aa93453580a07dc402" } # darwinia -sp-phragmen = { package = "darwinia-phragmen", default-features = false, path = "../../primitives/phragmen" } -darwinia-support = { path = "../support", default-features = false } +darwinia-phragmen = { default-features = false, path = "../../primitives/phragmen" } +darwinia-support = { default-features = false, path = "../support" } [dev-dependencies] -pallet-ring = { package = "darwinia-ring", path = "../../frame/balances/ring" } -pallet-staking-reward-curve = { version = "2.0.0", git = "https://github.com/paritytech/substrate.git", rev = "c2fccb36ffacd118fc3502aa93453580a07dc402" } substrate-test-utils = { version = "2.0.0", git = "https://github.com/paritytech/substrate.git", rev = "c2fccb36ffacd118fc3502aa93453580a07dc402" } +darwinia-ring = { path = "../../frame/balances/ring" } + [features] equalize = [] migrate = [] @@ -48,10 +48,10 @@ std = [ "pallet-timestamp/std", "sp-io/std", "sp-keyring", - "sp-phragmen/std", "sp-runtime/std", "sp-staking/std", "sp-std/std", + "darwinia-phragmen/std", "darwinia-support/std", ] diff --git a/frame/staking/src/lib.rs b/frame/staking/src/lib.rs index 95de7a436..51e0b1d2b 100644 --- a/frame/staking/src/lib.rs +++ b/frame/staking/src/lib.rs @@ -243,8 +243,9 @@ //! - [Session](../pallet_session/index.html): Used to manage sessions. Also, a list of new validators //! is stored in the Session module's `Validators` at the end of each era. -#![recursion_limit = "128"] #![cfg_attr(not(feature = "std"), no_std)] +#![feature(drain_filter)] +#![recursion_limit = "128"] mod inflation; mod migration; @@ -263,10 +264,6 @@ mod types { pub type Moment = Timestamp; /// Balance of an account. pub type Balance = u128; - /// Power of an account. - pub type Power = u32; - /// Votes of an account. - pub type Votes = u32; pub type RingBalance = as Currency>>::Balance; pub type RingPositiveImbalance = as Currency>>::PositiveImbalance; @@ -311,11 +308,9 @@ use frame_support::{ }; use frame_system::{self as system, ensure_root, ensure_signed}; use pallet_session::{historical::OnSessionEnding, SelectInitialValidators}; -use sp_phragmen::PhragmenStakedAssignment; use sp_runtime::{ traits::{ - Bounded, CheckedSub, Convert, EnsureOrigin, One, SaturatedConversion, Saturating, SimpleArithmetic, - StaticLookup, Zero, + CheckedSub, Convert, EnsureOrigin, One, SaturatedConversion, Saturating, SimpleArithmetic, StaticLookup, Zero, }, Perbill, Perquintill, RuntimeDebug, }; @@ -327,6 +322,7 @@ use sp_staking::{ }; use sp_std::{borrow::ToOwned, convert::TryInto, marker::PhantomData, vec, vec::Vec}; +use darwinia_phragmen::{PhragmenStakedAssignment, Power, Votes}; use darwinia_support::{ LockIdentifier, LockableCurrency, NormalLock, StakingLock, WithdrawLock, WithdrawReason, WithdrawReasons, }; @@ -415,6 +411,7 @@ where RingBalance: HasCompact, KtonBalance: HasCompact, { + All, RingBalance(RingBalance), KtonBalance(KtonBalance), } @@ -478,52 +475,127 @@ impl where RingBalance: SimpleArithmetic + Saturating + Copy, KtonBalance: SimpleArithmetic + Saturating + Copy, + BlockNumber: PartialOrd, + Timestamp: PartialOrd, { - // /// Slash the validator for a given amount of balance. This can grow the value - // /// of the slash in the case that the validator has less than `minimum_balance` - // /// active funds. Returns the amount of funds actually slashed. - // /// - // /// Slashes from `active` funds first, and then `unlocking`, starting with the - // /// chunks that are closest to unlocking. - // fn slash(&mut self, mut value: Balance, minimum_balance: Balance) -> Balance { - // let pre_total = self.total; - // let total = &mut self.total; - // let active = &mut self.active; - // - // let slash_out_of = |total_remaining: &mut Balance, target: &mut Balance, value: &mut Balance| { - // let mut slash_from_target = (*value).min(*target); - // - // if !slash_from_target.is_zero() { - // *target -= slash_from_target; - // - // // don't leave a dust balance in the staking system. - // if *target <= minimum_balance { - // slash_from_target += *target; - // *value += sp_std::mem::replace(target, Zero::zero()); - // } - // - // *total_remaining = total_remaining.saturating_sub(slash_from_target); - // *value -= slash_from_target; - // } - // }; - // - // slash_out_of(total, active, &mut value); - // - // let i = self - // .unlocking - // .iter_mut() - // .map(|chunk| { - // slash_out_of(total, &mut chunk.value, &mut value); - // chunk.value - // }) - // .take_while(|value| value.is_zero()) // take all fully-consumed chunks out. - // .count(); - // - // // kill all drained chunks. - // let _ = self.unlocking.drain(..i); - // - // pre_total.saturating_sub(*total) - // } + /// Slash the validator for a given amount of balance. This can grow the value + /// of the slash in the case that the validator has less than `minimum_balance` + /// active funds. Returns the amount of funds actually slashed. + /// + /// Slashes from `active` funds first, and then `unlocking`, starting with the + /// chunks that are closest to unlocking. + fn slash( + &mut self, + slash_ring: RingBalance, + slash_kton: KtonBalance, + bn: BlockNumber, + ts: Timestamp, + ) -> (RingBalance, KtonBalance) { + let slash_out_of = |active_ring: &mut RingBalance, + active_deposit_ring: &mut RingBalance, + deposit_item: &mut Vec>, + active_kton: &mut KtonBalance, + slash_ring: &mut RingBalance, + slash_kton: &mut KtonBalance| { + let slash_from_active_ring = (*slash_ring).min(*active_ring); + let slash_from_active_kton = (*slash_kton).min(*active_kton); + + if !slash_from_active_ring.is_zero() { + let normal_ring = *active_ring - *active_deposit_ring; + if normal_ring < *slash_ring { + let mut slash_deposit_ring = *slash_ring - (*active_ring - *active_deposit_ring); + *active_deposit_ring -= slash_deposit_ring; + + deposit_item.drain_filter(|item| { + if ts >= item.expire_time { + true + } else { + if slash_deposit_ring.is_zero() { + false + } else { + if slash_deposit_ring > item.value { + slash_deposit_ring -= item.value; + true + } else { + item.value -= sp_std::mem::replace(&mut slash_deposit_ring, Zero::zero()); + false + } + } + } + }); + } + *active_ring -= slash_from_active_ring; + *slash_ring -= slash_from_active_ring; + } + + if !slash_from_active_kton.is_zero() { + *active_kton -= slash_from_active_kton; + *slash_kton -= slash_from_active_kton; + } + }; + + let (mut apply_slash_ring, mut apply_slash_kton) = (slash_ring, slash_kton); + let StakingLedger { + active_ring, + active_deposit_ring, + deposit_items, + active_kton, + ring_staking_lock, + kton_staking_lock, + .. + } = self; + + slash_out_of( + active_ring, + active_deposit_ring, + deposit_items, + active_kton, + &mut apply_slash_ring, + &mut apply_slash_kton, + ); + + if !apply_slash_ring.is_zero() { + ring_staking_lock.unbondings.drain_filter(|lock| { + if bn >= lock.until { + true + } else { + if apply_slash_ring.is_zero() { + false + } else { + if apply_slash_ring > lock.amount { + apply_slash_ring -= lock.amount; + true + } else { + lock.amount -= sp_std::mem::replace(&mut apply_slash_ring, Zero::zero()); + false + } + } + } + }); + } + if !apply_slash_kton.is_zero() { + kton_staking_lock.unbondings.drain_filter(|lock| { + if bn >= lock.until { + true + } else { + if apply_slash_kton.is_zero() { + false + } else { + if apply_slash_kton > lock.amount { + apply_slash_kton -= lock.amount; + + true + } else { + lock.amount -= sp_std::mem::replace(&mut apply_slash_kton, Zero::zero()); + false + } + } + } + }); + } + + (slash_ring - apply_slash_ring, slash_kton - apply_slash_kton) + } } /// A record of the nominations made by a specific account. @@ -539,33 +611,50 @@ pub struct Nominations { /// The amount of exposure (to slashing) than an individual nominator has. #[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Encode, Decode, RuntimeDebug)] -pub struct IndividualExposure { +pub struct IndividualExposure +where + RingBalance: HasCompact, + KtonBalance: HasCompact, +{ /// The stash account of the nominator in question. who: AccountId, /// Amount of funds exposed. #[codec(compact)] - value: Power, + ring_balance: RingBalance, + #[codec(compact)] + kton_balance: KtonBalance, + power: Power, } /// A snapshot of the stake backing a single validator in the system. #[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Encode, Decode, Default, RuntimeDebug)] -pub struct Exposure { +pub struct Exposure +where + RingBalance: HasCompact, + KtonBalance: HasCompact, +{ + /// The validator's own stash that is exposed. + #[codec(compact)] + pub own_ring_balance: RingBalance, + #[codec(compact)] + pub own_kton_balance: KtonBalance, + pub own_power: Power, /// The total balance backing this validator. #[codec(compact)] - pub total: Power, - /// The validator's own stash that is exposed. + pub total_ring_balance: RingBalance, #[codec(compact)] - pub own: Power, + pub total_kton_balance: KtonBalance, + pub total_power: Power, /// The portions of nominators stashes that are exposed. - pub others: Vec>, + pub others: Vec>, } /// A typed conversion from stash account ID to the current exposure of nominators /// on that account. pub struct ExposureOf(PhantomData); -impl Convert>> for ExposureOf { - fn convert(validator: T::AccountId) -> Option> { +impl Convert, KtonBalance>>> for ExposureOf { + fn convert(validator: T::AccountId) -> Option, KtonBalance>> { Some(>::stakers(&validator)) } } @@ -591,17 +680,17 @@ pub struct NominatorReward { /// A pending slash record. The value of the slash has been computed but not applied yet, /// rather deferred for several eras. #[derive(Encode, Decode, Default, RuntimeDebug)] -pub struct UnappliedSlash { +pub struct UnappliedSlash { /// The stash ID of the offending validator. validator: AccountId, /// The validator's own slash. - own: Power, + own: slashing::RK, /// All other slashed stakers and amounts. - others: Vec<(AccountId, Power)>, + others: Vec<(AccountId, slashing::RK)>, /// Reporters of the offence; bounty payout recipients. reporters: Vec, /// The amount of payout. - payout: Power, + payout: slashing::RK, } /// Means for interacting with a specialized version of the `session` trait. @@ -624,7 +713,7 @@ impl SessionInterface<::AccountId> for T where T: pallet_session::Trait::AccountId>, T: pallet_session::historical::Trait< - FullIdentification = Exposure<::AccountId, Power>, + FullIdentification = Exposure<::AccountId, RingBalance, KtonBalance>, FullIdentificationOf = ExposureOf, >, T::SessionHandler: pallet_session::SessionHandler<::AccountId>, @@ -649,13 +738,6 @@ pub trait Trait: frame_system::Trait { /// Time used for computing era duration. type Time: Time; - // /// Convert a balance into a number used for election calculation. - // /// This must fit into a `u64` but is allowed to be sensibly lossy. - // /// TODO: #1377 - // /// The backward convert should be removed as the new Phragmen API returns ratio. - // /// The post-processing needs it but will be moved to off-chain. TODO: #2908 - // type PowerToVotes: Convert; - /// The overarching event type. type Event: From> + Into<::Event>; @@ -758,7 +840,7 @@ decl_storage! { /// through validators here, but you can find them in the Session module. /// /// This is keyed by the stash account. - pub Stakers get(fn stakers): map T::AccountId => Exposure; + pub Stakers get(fn stakers): map T::AccountId => Exposure, KtonBalance>; /// The currently elected validator set keyed by stash account ID. pub CurrentElected get(fn current_elected): Vec; @@ -800,7 +882,7 @@ decl_storage! { pub CanceledSlashPayout get(fn canceled_payout) config(): Power; /// All unapplied slashes that are queued for later. - pub UnappliedSlashes: map EraIndex => Vec>; + pub UnappliedSlashes: map EraIndex => Vec, KtonBalance>>; /// Total *Ring* in pool. pub RingPool get(fn ring_pool): RingBalance; @@ -817,17 +899,18 @@ decl_storage! { /// All slashing events on validators, mapped by era to the highest slash proportion /// and slash value of the era. - ValidatorSlashInEra: double_map EraIndex, twox_128(T::AccountId) => Option<(Perbill, Power)>; + ValidatorSlashInEra: + double_map EraIndex, twox_128(T::AccountId) => Option<(Perbill, slashing::RKT)>; /// All slashing events on nominators, mapped by era to the highest slash value of the era. - NominatorSlashInEra: double_map EraIndex, twox_128(T::AccountId) => Option; + NominatorSlashInEra: double_map EraIndex, twox_128(T::AccountId) => Option>; /// Slashing spans for stash accounts. SlashingSpans: map T::AccountId => Option; /// Records information about the maximum slash of a stash within a slashing span, /// as well as how much reward has been paid out. - SpanSlash: map (T::AccountId, slashing::SpanIndex) => slashing::SpanRecord; + SpanSlash: map (T::AccountId, slashing::SpanIndex) => slashing::SpanRecord, KtonBalance>; /// The earliest era for which we have a pending, unapplied slash. EarliestUnappliedSlash: Option; @@ -899,7 +982,7 @@ decl_event!( Reward(RingBalance, RingBalance, Vec>), /// One validator (and its nominators) has been slashed by the given amount. - Slash(AccountId, Power), + Slash(AccountId, RingBalance, KtonBalance), /// An old slashing report from a prior era was discarded because it could /// not be processed. OldSlashingReportDiscarded(SessionIndex), @@ -1035,6 +1118,7 @@ decl_module! { >::mutate(|k| *k += value); Self::deposit_event(RawEvent::BondKton(value)); }, + _ => (), } // You're auto-bonded forever, here. We might improve this by only bonding when @@ -1092,6 +1176,7 @@ decl_module! { Self::deposit_event(RawEvent::BondKton(extra)); } }, + _ => (), } } @@ -1215,6 +1300,7 @@ decl_module! { Self::deposit_event(RawEvent::UnbondKton(unbond_kton, now)); } }, + _ => (), } let StakingLedger { @@ -1564,6 +1650,13 @@ impl Module { .unwrap_or_default() } + pub fn stake_of(stash: &T::AccountId) -> (RingBalance, KtonBalance) { + Self::bonded(stash) + .and_then(Self::ledger) + .map(|l| (l.active_ring, l.active_kton)) + .unwrap_or_default() + } + // Update the ledger while bonding ring and compute the kton should return. fn bond_ring( stash: &T::AccountId, @@ -1653,6 +1746,23 @@ impl Module { WithdrawReasons::all(), ); } + _ => { + ledger.ring_staking_lock.staking_amount = ledger.active_ring; + ledger.kton_staking_lock.staking_amount = ledger.active_kton; + + T::RingCurrency::set_lock( + STAKING_ID, + &ledger.stash, + WithdrawLock::WithStaking(ledger.ring_staking_lock.clone()), + WithdrawReasons::all(), + ); + T::KtonCurrency::set_lock( + STAKING_ID, + &ledger.stash, + WithdrawLock::WithStaking(ledger.kton_staking_lock.clone()), + WithdrawReasons::all(), + ); + } } >::insert(controller, ledger); @@ -1677,7 +1787,8 @@ impl Module { RewardDestination::Controller => Self::bonded(stash) .and_then(|controller| T::RingCurrency::deposit_into_existing(&controller, amount).ok()), RewardDestination::Stash => T::RingCurrency::deposit_into_existing(stash, amount).ok(), - RewardDestination::Staked { promise_month } => Self::bonded(stash) + // TODO month + RewardDestination::Staked { promise_month: _ } => Self::bonded(stash) .and_then(|c| Self::ledger(&c).map(|l| (c, l))) .and_then(|(c, mut l)| { l.active_ring += amount; @@ -1701,10 +1812,10 @@ impl Module { Zero::zero() } else { let exposure = Self::stakers(stash); - let total = exposure.total.max(One::one()); + let total = exposure.total_power.max(One::one()); for i in &exposure.others { - let per_u64 = Perbill::from_rational_approximation(i.value, total); + let per_u64 = Perbill::from_rational_approximation(i.power, total); let nominator_reward = per_u64 * reward; imbalance.maybe_subsume(Self::make_payout(&i.who, nominator_reward)); @@ -1714,7 +1825,7 @@ impl Module { }); } - let per_u64 = Perbill::from_rational_approximation(exposure.own, total); + let per_u64 = Perbill::from_rational_approximation(exposure.own_power, total); per_u64 * reward }; let validator_reward = validator_cut + off_the_table; @@ -1728,7 +1839,10 @@ impl Module { /// with the exposure of the prior validator set. fn new_session( session_index: SessionIndex, - ) -> Option<(Vec, Vec<(T::AccountId, Exposure)>)> { + ) -> Option<( + Vec, + Vec<(T::AccountId, Exposure, KtonBalance>)>, + )> { let era_length = session_index .checked_sub(Self::current_era_start_session_index()) .unwrap_or(0); @@ -1886,13 +2000,12 @@ impl Module { all_nominators.extend(nominator_votes); - let maybe_phragmen_result = sp_phragmen::elect::<_, _>( + let maybe_phragmen_result = darwinia_phragmen::elect::<_, _>( Self::validator_count() as usize, Self::minimum_validator_count().max(1) as usize, all_validators, all_nominators, Self::power_of, - T::TotalPower::get(), ); if let Some(phragmen_result) = maybe_phragmen_result { @@ -1902,14 +2015,26 @@ impl Module { .map(|(s, _)| s.clone()) .collect::>(); let assignments = phragmen_result.assignments; - let mut supports = sp_phragmen::build_support_map::<_, _>(&elected_stashes, &assignments, Self::power_of); + + let to_votes = |p: Power| p as Votes; + let to_power = |v: Votes| v as Power; + + let mut supports = darwinia_phragmen::build_support_map::<_, _, _, _, _>( + &elected_stashes, + &assignments, + Self::power_of, + Self::stake_of, + ); if cfg!(feature = "equalize") { - let mut staked_assignments: Vec<(T::AccountId, Vec>)> = - Vec::with_capacity(assignments.len()); + let mut staked_assignments: Vec<( + T::AccountId, + Vec, KtonBalance>>, + )> = Vec::with_capacity(assignments.len()); for (n, assignment) in assignments.iter() { - let mut staked_assignment: Vec> = - Vec::with_capacity(assignment.len()); + let mut staked_assignment: Vec< + PhragmenStakedAssignment, KtonBalance>, + > = Vec::with_capacity(assignment.len()); // If this is a self vote, then we don't need to equalise it at all. While the // staking system does not allow nomination and validation at the same time, @@ -1918,16 +2043,31 @@ impl Module { continue; } for (c, per_thing) in assignment.iter() { - let nominator_stake = Self::power_of(n); + let nominator_stake = to_votes(Self::power_of(n)); + let (ring_balance, kton_balance) = { + let (r, k) = Self::stake_of(n); + (*per_thing * r, *per_thing * k) + }; let other_stake = *per_thing * nominator_stake; - staked_assignment.push((c.clone(), other_stake)); + staked_assignment.push(PhragmenStakedAssignment { + account_id: c.clone(), + votes: other_stake, + ring_balance, + kton_balance, + }); } staked_assignments.push((n.clone(), staked_assignment)); } let tolerance: Votes = 0; let iterations = 2_usize; - sp_phragmen::equalize::<_, _>(staked_assignments, &mut supports, tolerance, iterations, Self::power_of); + darwinia_phragmen::equalize::<_, _, _, _>( + staked_assignments, + &mut supports, + tolerance, + iterations, + Self::power_of, + ); } // Clear Stakers. @@ -1940,20 +2080,25 @@ impl Module { for (c, s) in supports.into_iter() { // build `struct exposure` from `support` let exposure = Exposure { - own: s.own, - // This might reasonably saturate and we cannot do much about it. The sum of - // someone's stake might exceed the balance type if they have the maximum amount - // of balance and receive some support. This is super unlikely to happen, yet - // we simulate it in some tests. - total: s.total, + own_ring_balance: s.own_ring_balance, + own_kton_balance: s.own_kton_balance, + own_power: to_power(s.own_votes), + total_ring_balance: s.total_ring_balance, + total_kton_balance: s.total_kton_balance, + total_power: to_power(s.total_votes), others: s .others .into_iter() - .map(|(who, value)| IndividualExposure { who, value }) - .collect::>>(), + .map(|assignment| IndividualExposure { + who: assignment.account_id, + ring_balance: assignment.ring_balance, + kton_balance: assignment.kton_balance, + power: to_power(assignment.votes), + }) + .collect::>>(), }; - if exposure.total < slot_stake { - slot_stake = exposure.total; + if exposure.total_power < slot_stake { + slot_stake = exposure.total_power; } >::insert(&c, exposure.clone()); @@ -2054,11 +2199,14 @@ impl pallet_session::OnSessionEnding for Module { } } -impl OnSessionEnding> for Module { +impl OnSessionEnding, KtonBalance>> for Module { fn on_session_ending( _ending: SessionIndex, start_session: SessionIndex, - ) -> Option<(Vec, Vec<(T::AccountId, Exposure)>)> { + ) -> Option<( + Vec, + Vec<(T::AccountId, Exposure, KtonBalance>)>, + )> { Self::ensure_storage_upgraded(); Self::new_session(start_session - 1) } @@ -2105,7 +2253,7 @@ impl OnOffenceHandler::AccountId>, T: pallet_session::historical::Trait< - FullIdentification = Exposure<::AccountId, Power>, + FullIdentification = Exposure<::AccountId, RingBalance, KtonBalance>, FullIdentificationOf = ExposureOf, >, T::SessionHandler: pallet_session::SessionHandler<::AccountId>, diff --git a/frame/staking/src/slashing.rs b/frame/staking/src/slashing.rs index d5516377e..aa04f76d2 100644 --- a/frame/staking/src/slashing.rs +++ b/frame/staking/src/slashing.rs @@ -50,13 +50,23 @@ use codec::{Decode, Encode}; use frame_support::{ - traits::{Currency, Imbalance, OnUnbalanced}, + traits::{Currency, Imbalance, OnUnbalanced, Time}, StorageDoubleMap, StorageMap, }; -use sp_runtime::traits::{Saturating, Zero}; -use sp_std::{vec, vec::Vec}; +use sp_runtime::{ + traits::{Saturating, Zero}, + Perbill, RuntimeDebug, +}; +use sp_std::{ + ops::{Add, AddAssign, Sub}, + vec, + vec::Vec, +}; -use crate::{EraIndex, Exposure, Module, Perbill, Power, SessionInterface, Store, Trait, UnappliedSlash}; +use crate::{ + EraIndex, Exposure, KtonBalance, KtonNegativeImbalance, Module, RawEvent, RingBalance, RingNegativeImbalance, + SessionInterface, StakingBalance, Store, Trait, UnappliedSlash, +}; /// The proportion of the slashing reward to be paid out on the first slashing detection. /// This is f_1 in the paper. @@ -65,6 +75,97 @@ const REWARD_F1: Perbill = Perbill::from_percent(50); /// The index of a slashing span - unique to each stash. pub(crate) type SpanIndex = u32; +// TODO doc +pub(crate) type RKT = RK, KtonBalance>; + +// TODO doc +#[derive(Clone, Copy, Default, Eq, PartialEq, Ord, PartialOrd, Encode, Decode, RuntimeDebug)] +pub struct RK { + pub(crate) r: R, + pub(crate) k: K, +} + +impl RK +where + R: Zero, + K: Zero, +{ + pub(crate) fn zero() -> Self { + Self { + r: Zero::zero(), + k: Zero::zero(), + } + } +} + +impl Add for RK +where + R: Add, + K: Add, +{ + type Output = Self; + + fn add(self, rhs: Self) -> Self::Output { + Self { + r: self.r + rhs.r, + k: self.k + rhs.k, + } + } +} + +impl AddAssign for RK +where + R: AddAssign, + K: AddAssign, +{ + fn add_assign(&mut self, rhs: Self) { + self.r += rhs.r; + self.k += rhs.k; + } +} + +impl Sub for RK +where + R: Sub, + K: Sub, +{ + type Output = Self; + + fn sub(self, rhs: Self) -> Self::Output { + Self { + r: self.r - rhs.r, + k: self.k - rhs.k, + } + } +} + +impl Saturating for RK +where + R: Copy + Saturating, + K: Copy + Saturating, +{ + fn saturating_add(self, o: Self) -> Self { + Self { + r: self.r.saturating_add(o.r), + k: self.k.saturating_add(o.k), + } + } + + fn saturating_sub(self, o: Self) -> Self { + Self { + r: self.r.saturating_sub(o.r), + k: self.k.saturating_sub(o.k), + } + } + + fn saturating_mul(self, o: Self) -> Self { + Self { + r: self.r.saturating_mul(o.r), + k: self.k.saturating_mul(o.k), + } + } +} + // A range of start..end eras for a slashing span. #[derive(Encode, Decode)] #[cfg_attr(test, derive(Debug, PartialEq))] @@ -149,43 +250,43 @@ impl SlashingSpans { self.last_start } - // // prune the slashing spans against a window, whose start era index is given. - // // - // // If this returns `Some`, then it includes a range start..end of all the span - // // indices which were pruned. - // fn prune(&mut self, window_start: EraIndex) -> Option<(SpanIndex, SpanIndex)> { - // let old_idx = self - // .iter() - // .skip(1) // skip ongoing span. - // .position(|span| span.length.map_or(false, |len| span.start + len <= window_start)); + // prune the slashing spans against a window, whose start era index is given. // - // let earliest_span_index = self.span_index - self.prior.len() as SpanIndex; - // let pruned = match old_idx { - // Some(o) => { - // self.prior.truncate(o); - // let new_earliest = self.span_index - self.prior.len() as SpanIndex; - // Some((earliest_span_index, new_earliest)) - // } - // None => None, - // }; - // - // // readjust the ongoing span, if it started before the beginning of the window. - // self.last_start = sp_std::cmp::max(self.last_start, window_start); - // pruned - // } + // If this returns `Some`, then it includes a range start..end of all the span + // indices which were pruned. + fn prune(&mut self, window_start: EraIndex) -> Option<(SpanIndex, SpanIndex)> { + let old_idx = self + .iter() + .skip(1) // skip ongoing span. + .position(|span| span.length.map_or(false, |len| span.start + len <= window_start)); + + let earliest_span_index = self.span_index - self.prior.len() as SpanIndex; + let pruned = match old_idx { + Some(o) => { + self.prior.truncate(o); + let new_earliest = self.span_index - self.prior.len() as SpanIndex; + Some((earliest_span_index, new_earliest)) + } + None => None, + }; + + // readjust the ongoing span, if it started before the beginning of the window. + self.last_start = sp_std::cmp::max(self.last_start, window_start); + pruned + } } /// A slashing-span record for a particular stash. #[derive(Encode, Decode, Default)] -pub(crate) struct SpanRecord { - slashed: Power, - paid_out: Power, +pub(crate) struct SpanRecord { + slashed: RK, + paid_out: RK, } -impl SpanRecord { +impl SpanRecord { /// The value of stash balance slashed in this span. #[cfg(test)] - pub(crate) fn amount_slashed(&self) -> &Power { + pub(crate) fn amount_slashed(&self) -> &RK { &self.slashed } } @@ -198,7 +299,7 @@ pub(crate) struct SlashParams<'a, T: 'a + Trait> { /// The proportion of the slash. pub(crate) slash: Perbill, /// The exposure of the stash and all nominators. - pub(crate) exposure: &'a Exposure, + pub(crate) exposure: &'a Exposure, KtonBalance>, /// The era where the offence occurred. pub(crate) slash_era: EraIndex, /// The first era in the current bonding period. @@ -216,7 +317,9 @@ pub(crate) struct SlashParams<'a, T: 'a + Trait> { /// /// The pending slash record returned does not have initialized reporters. Those have /// to be set at a higher level, if any. -pub(crate) fn compute_slash(params: SlashParams) -> Option> { +pub(crate) fn compute_slash( + params: SlashParams, +) -> Option, KtonBalance>> { let SlashParams { stash, slash, @@ -227,20 +330,23 @@ pub(crate) fn compute_slash(params: SlashParams) -> Option>::zero(); + let mut val_slashed = >::zero(); // is the slash amount here a maximum for the era? - let own_slash = slash * exposure.own; - if slash * exposure.total == 0 { + let own_slash = RK { + r: slash * exposure.own_ring_balance, + k: slash * exposure.own_kton_balance, + }; + if (slash * exposure.total_power).is_zero() { // kick out the validator even if they won't be slashed, // as long as the misbehavior is from their most recent slashing span. kick_out_if_recent::(params); return None; } - let (prior_slash_p, _era_slash) = - as Store>::ValidatorSlashInEra::get(&slash_era, stash).unwrap_or((Perbill::zero(), 0)); + let (prior_slash_p, _era_slash) = as Store>::ValidatorSlashInEra::get(&slash_era, stash) + .unwrap_or((Perbill::zero(), >::zero())); // compare slash proportions rather than slash values to avoid issues due to rounding // error. @@ -302,8 +408,8 @@ pub(crate) fn compute_slash(params: SlashParams) -> Option(params: SlashParams) { // these are not updated by era-span or end-span. - let mut reward_payout = 0; - let mut val_slashed = 0; + let mut reward_payout = RK::zero(); + let mut val_slashed = RK::zero(); let mut spans = fetch_spans::( params.stash, params.window_start, @@ -330,8 +436,8 @@ fn kick_out_if_recent(params: SlashParams) { fn slash_nominators( params: SlashParams, prior_slash_p: Perbill, - nominators_slashed: &mut Vec<(T::AccountId, Power)>, -) -> Power { + nominators_slashed: &mut Vec<(T::AccountId, RKT)>, +) -> RKT { let SlashParams { stash: _, slash, @@ -342,21 +448,27 @@ fn slash_nominators( reward_proportion, } = params; - let mut reward_payout = 0; + let mut reward_payout = >::zero(); nominators_slashed.reserve(exposure.others.len()); for nominator in &exposure.others { let stash = &nominator.who; - let mut nom_slashed = 0; + let mut nom_slashed = >::zero(); // the era slash of a nominator always grows, if the validator // had a new max slash for the era. let era_slash = { - let own_slash_prior = prior_slash_p * nominator.value; - let own_slash_by_validator = slash * nominator.value; + let own_slash_prior = RK { + r: prior_slash_p * nominator.ring_balance, + k: prior_slash_p * nominator.kton_balance, + }; + let own_slash_by_validator = RK { + r: slash * nominator.ring_balance, + k: slash * nominator.kton_balance, + }; let own_slash_difference = own_slash_by_validator.saturating_sub(own_slash_prior); - let mut era_slash = as Store>::NominatorSlashInEra::get(&slash_era, stash).unwrap_or(0); + let mut era_slash = as Store>::NominatorSlashInEra::get(&slash_era, stash).unwrap_or_default(); era_slash += own_slash_difference; @@ -402,8 +514,8 @@ struct InspectingSpans<'a, T: Trait + 'a> { window_start: EraIndex, stash: &'a T::AccountId, spans: SlashingSpans, - paid_out: &'a mut Power, - slash_of: &'a mut Power, + paid_out: &'a mut RKT, + slash_of: &'a mut RKT, reward_proportion: Perbill, _marker: sp_std::marker::PhantomData, } @@ -412,8 +524,8 @@ struct InspectingSpans<'a, T: Trait + 'a> { fn fetch_spans<'a, T: Trait + 'a>( stash: &'a T::AccountId, window_start: EraIndex, - paid_out: &'a mut Power, - slash_of: &'a mut Power, + paid_out: &'a mut RKT, + slash_of: &'a mut RKT, reward_proportion: Perbill, ) -> InspectingSpans<'a, T> { let spans = as Store>::SlashingSpans::get(stash).unwrap_or_else(|| { @@ -443,7 +555,7 @@ impl<'a, T: 'a + Trait> InspectingSpans<'a, T> { self.dirty = self.spans.end_span(now) || self.dirty; } - fn add_slash(&mut self, amount: Power) { + fn add_slash(&mut self, amount: RKT) { *self.slash_of += amount; } @@ -456,7 +568,7 @@ impl<'a, T: 'a + Trait> InspectingSpans<'a, T> { // if it's higher, applies the difference of the slashes and then updates the span on disk. // // returns the span index of the era where the slash occurred, if any. - fn compare_and_update_span_slash(&mut self, slash_era: EraIndex, slash: Power) -> Option { + fn compare_and_update_span_slash(&mut self, slash_era: EraIndex, slash: RKT) -> Option { let target_span = self.era_span(slash_era)?; let span_slash_key = (self.stash.clone(), target_span.index); let mut span_record = as Store>::SpanSlash::get(&span_slash_key); @@ -468,7 +580,15 @@ impl<'a, T: 'a + Trait> InspectingSpans<'a, T> { span_record.slashed = slash; // compute reward. - let reward = REWARD_F1 * (self.reward_proportion * slash).saturating_sub(span_record.paid_out); + let slash = RK { + r: self.reward_proportion * slash.r, + k: self.reward_proportion * slash.k, + }; + let slash = slash.saturating_sub(span_record.paid_out); + let reward = RK { + r: REWARD_F1 * slash.r, + k: REWARD_F1 * slash.k, + }; self.add_slash(difference); changed = true; @@ -476,12 +596,20 @@ impl<'a, T: 'a + Trait> InspectingSpans<'a, T> { reward } else if span_record.slashed == slash { // compute reward. no slash difference to apply. - REWARD_F1 * (self.reward_proportion * slash).saturating_sub(span_record.paid_out) + let slash = RK { + r: self.reward_proportion * slash.r, + k: self.reward_proportion * slash.k, + }; + let slash = slash.saturating_sub(span_record.paid_out); + RK { + r: REWARD_F1 * slash.r, + k: REWARD_F1 * slash.k, + } } else { - 0 + >::zero() }; - if !reward.is_zero() { + if !reward.r.is_zero() || !reward.k.is_zero() { changed = true; span_record.paid_out += reward; *self.paid_out += reward; @@ -496,22 +624,22 @@ impl<'a, T: 'a + Trait> InspectingSpans<'a, T> { } } -//impl<'a, T: 'a + Trait> Drop for InspectingSpans<'a, T> { -// fn drop(&mut self) { -// // only update on disk if we slashed this account. -// if !self.dirty { -// return; -// } -// -// if let Some((start, end)) = self.spans.prune(self.window_start) { -// for span_index in start..end { -// as Store>::SpanSlash::remove(&(self.stash.clone(), span_index)); -// } -// } -// -// as Store>::SlashingSpans::insert(self.stash, &self.spans); -// } -//} +impl<'a, T: 'a + Trait> Drop for InspectingSpans<'a, T> { + fn drop(&mut self) { + // only update on disk if we slashed this account. + if !self.dirty { + return; + } + + if let Some((start, end)) = self.spans.prune(self.window_start) { + for span_index in start..end { + as Store>::SpanSlash::remove(&(self.stash.clone(), span_index)); + } + } + + as Store>::SlashingSpans::insert(self.stash, &self.spans); + } +} /// Clear slashing metadata for an obsolete era. pub(crate) fn clear_era_metadata(obsolete_era: EraIndex) { @@ -536,92 +664,179 @@ pub(crate) fn clear_stash_metadata(stash: &T::AccountId) { } } -//// apply the slash to a stash account, deducting any missing funds from the reward -//// payout, saturating at 0. this is mildly unfair but also an edge-case that -//// can only occur when overlapping locked funds have been slashed. -//fn do_slash(stash: &T::AccountId, value: Power, reward_payout: &mut Power, slashed_imbalance: &mut Power) { -// let controller = match >::bonded(stash) { -// None => return, // defensive: should always exist. -// Some(c) => c, -// }; -// -// let mut ledger = match >::ledger(&controller) { -// Some(ledger) => ledger, -// None => return, // nothing to do. -// }; -// -// let value = ledger.slash(value, T::Currency::minimum_balance()); -// -// if !value.is_zero() { -// let (imbalance, missing) = T::Currency::slash(stash, value); -// slashed_imbalance.subsume(imbalance); -// -// if !missing.is_zero() { -// // deduct overslash from the reward payout -// *reward_payout = reward_payout.saturating_sub(missing); -// } -// -// >::update_ledger(&controller, &ledger); -// -// // trigger the event -// >::deposit_event(super::RawEvent::Slash(stash.clone(), value)); -// } -//} +// apply the slash to a stash account, deducting any missing funds from the reward +// payout, saturating at 0. this is mildly unfair but also an edge-case that +// can only occur when overlapping locked funds have been slashed. +fn do_slash( + stash: &T::AccountId, + value: RKT, + reward_payout: &mut RKT, + slashed_ring: &mut RingNegativeImbalance, + slashed_kton: &mut KtonNegativeImbalance, +) { + let controller = match >::bonded(stash) { + None => return, // defensive: should always exist. + Some(c) => c, + }; + + let mut ledger = match >::ledger(&controller) { + Some(ledger) => ledger, + None => return, // nothing to do. + }; + + let (slash_ring, slash_kton) = ledger.slash( + value.r, + value.k, + >::block_number(), + T::Time::now(), + ); + let mut changed = false; + + if !slash_ring.is_zero() { + changed = true; + + let (imbalance, missing) = T::RingCurrency::slash(stash, slash_ring); + slashed_ring.subsume(imbalance); + + if !missing.is_zero() { + // deduct overslash from the reward payout + reward_payout.r = reward_payout.r.saturating_sub(missing); + } + } + + if !slash_kton.is_zero() { + changed = true; + + let (imbalance, missing) = T::KtonCurrency::slash(stash, slash_kton); + slashed_kton.subsume(imbalance); + + if !missing.is_zero() { + // deduct overslash from the reward payout + reward_payout.k = reward_payout.k.saturating_sub(missing); + } + } + + if changed { + >::update_ledger(&controller, &mut ledger, StakingBalance::All); + >::deposit_event(RawEvent::Slash(stash.clone(), value.r, value.k)); + } +} /// Apply a previously-unapplied slash. -pub(crate) fn apply_slash(unapplied_slash: UnappliedSlash) { - let mut slashed_power = 0; +pub(crate) fn apply_slash(unapplied_slash: UnappliedSlash, KtonBalance>) { let mut reward_payout = unapplied_slash.payout; + let mut slashed_ring = >::zero(); + let mut slashed_kton = >::zero(); - // do_slash::( - // &unapplied_slash.validator, - // unapplied_slash.own, - // &mut reward_payout, - // &mut slashed_power, - // ); + do_slash::( + &unapplied_slash.validator, + unapplied_slash.own, + &mut reward_payout, + &mut slashed_ring, + &mut slashed_kton, + ); - // for &(ref nominator, nominator_slash) in &unapplied_slash.others { - // do_slash::(&nominator, nominator_slash, &mut reward_payout, &mut slashed_imbalance); - // } - // - // pay_reporters::(reward_payout, slashed_imbalance, &unapplied_slash.reporters); + for &(ref nominator, nominator_slash) in &unapplied_slash.others { + do_slash::( + &nominator, + nominator_slash, + &mut reward_payout, + &mut slashed_ring, + &mut slashed_kton, + ); + } + + pay_reporters::(reward_payout, slashed_ring, slashed_kton, &unapplied_slash.reporters); } -///// Apply a reward payout to some reporters, paying the rewards out of the slashed imbalance. -//fn pay_reporters( -// reward_payout: BalanceOf, -// slashed_imbalance: NegativeImbalanceOf, -// reporters: &[T::AccountId], -//) { -// if reward_payout.is_zero() || reporters.is_empty() { -// // nobody to pay out to or nothing to pay; -// // just treat the whole value as slashed. -// T::Slash::on_unbalanced(slashed_imbalance); -// return; -// } -// -// // take rewards out of the slashed imbalance. -// let reward_payout = reward_payout.min(slashed_imbalance.peek()); -// let (mut reward_payout, mut value_slashed) = slashed_imbalance.split(reward_payout); -// -// let per_reporter = reward_payout.peek() / (reporters.len() as u32).into(); -// for reporter in reporters { -// let (reporter_reward, rest) = reward_payout.split(per_reporter); -// reward_payout = rest; -// -// // this cancels out the reporter reward imbalance internally, leading -// // to no change in total issuance. -// T::Currency::resolve_creating(reporter, reporter_reward); -// } -// -// // the rest goes to the on-slash imbalance handler (e.g. treasury) -// value_slashed.subsume(reward_payout); // remainder of reward division remains. -// T::Slash::on_unbalanced(value_slashed); -//} -// -//// TODO: function for undoing a slash. -//// -// +/// Apply a reward payout to some reporters, paying the rewards out of the slashed imbalance. +fn pay_reporters( + reward_payout: RKT, + slashed_ring: RingNegativeImbalance, + slashed_kton: KtonNegativeImbalance, + reporters: &[T::AccountId], +) { + if reporters.is_empty() || (!reward_payout.r.is_zero() && !reward_payout.k.is_zero()) { + // nobody to pay out to or nothing to pay; + // just treat the whole value as slashed. + T::RingSlash::on_unbalanced(slashed_ring); + T::KtonSlash::on_unbalanced(slashed_kton); + + return; + } + + if !reward_payout.r.is_zero() && reward_payout.k.is_zero() { + // take rewards out of the slashed imbalance. + let ring_reward_payout = reward_payout.r.min(slashed_ring.peek()); + let (mut ring_reward_payout, mut ring_slashed) = slashed_ring.split(ring_reward_payout); + let ring_per_reporter = ring_reward_payout.peek() / (reporters.len() as u32).into(); + + for reporter in reporters { + let (ring_reporter_reward, ring_rest) = ring_reward_payout.split(ring_per_reporter); + ring_reward_payout = ring_rest; + + // this cancels out the reporter reward imbalance internally, leading + // to no change in total issuance. + T::RingCurrency::resolve_creating(reporter, ring_reporter_reward); + } + + // the rest goes to the on-slash imbalance handler (e.g. treasury) + ring_slashed.subsume(ring_reward_payout); // remainder of reward division remains. + T::RingSlash::on_unbalanced(ring_slashed); + } else if reward_payout.r.is_zero() && !reward_payout.k.is_zero() { + let kton_reward_payout = reward_payout.k.min(slashed_kton.peek()); + let (mut kton_reward_payout, mut kton_slashed) = slashed_kton.split(kton_reward_payout); + let kton_per_reporter = kton_reward_payout.peek() / (reporters.len() as u32).into(); + + for reporter in reporters { + let (kton_reporter_reward, kton_rest) = kton_reward_payout.split(kton_per_reporter); + kton_reward_payout = kton_rest; + + // this cancels out the reporter reward imbalance internally, leading + // to no change in total issuance. + T::KtonCurrency::resolve_creating(reporter, kton_reporter_reward); + } + + // the rest goes to the on-slash imbalance handler (e.g. treasury) + kton_slashed.subsume(kton_reward_payout); // remainder of reward division remains. + T::KtonSlash::on_unbalanced(kton_slashed); + } else if !reward_payout.r.is_zero() && !reward_payout.k.is_zero() { + // take rewards out of the slashed imbalance. + let ring_reward_payout = reward_payout.r.min(slashed_ring.peek()); + let (mut ring_reward_payout, mut ring_slashed) = slashed_ring.split(ring_reward_payout); + let ring_per_reporter = ring_reward_payout.peek() / (reporters.len() as u32).into(); + let kton_reward_payout = reward_payout.k.min(slashed_kton.peek()); + let (mut kton_reward_payout, mut kton_slashed) = slashed_kton.split(kton_reward_payout); + let kton_per_reporter = kton_reward_payout.peek() / (reporters.len() as u32).into(); + + for reporter in reporters { + let (ring_reporter_reward, ring_rest) = ring_reward_payout.split(ring_per_reporter); + ring_reward_payout = ring_rest; + + // this cancels out the reporter reward imbalance internally, leading + // to no change in total issuance. + T::RingCurrency::resolve_creating(reporter, ring_reporter_reward); + + let (kton_reporter_reward, kton_rest) = kton_reward_payout.split(kton_per_reporter); + kton_reward_payout = kton_rest; + + // this cancels out the reporter reward imbalance internally, leading + // to no change in total issuance. + T::KtonCurrency::resolve_creating(reporter, kton_reporter_reward); + } + + // the rest goes to the on-slash imbalance handler (e.g. treasury) + ring_slashed.subsume(ring_reward_payout); // remainder of reward division remains. + T::RingSlash::on_unbalanced(ring_slashed); + + // the rest goes to the on-slash imbalance handler (e.g. treasury) + kton_slashed.subsume(kton_reward_payout); // remainder of reward division remains. + T::KtonSlash::on_unbalanced(kton_slashed); + } +} + +// TODO: function for undoing a slash. + //#[cfg(test)] //mod tests { // use super::*; diff --git a/frame/support/src/lib.rs b/frame/support/src/lib.rs index 65d70a181..f5d3c6e23 100644 --- a/frame/support/src/lib.rs +++ b/frame/support/src/lib.rs @@ -96,9 +96,9 @@ mod structs { /// A wrapper for any rational number with a u32 bit numerator and denominator. #[derive(Clone, Copy, Default, Eq, RuntimeDebug)] - pub struct Rational32(u32, u32); + pub struct Rational64(u64, u64); - impl Rational32 { + impl Rational64 { /// Nothing. pub fn zero() -> Self { Self(0, 1) @@ -110,40 +110,40 @@ mod structs { } /// Build from a raw `n/d`. - pub fn from(n: u32, d: u32) -> Self { + pub fn from(n: u64, d: u64) -> Self { Self(n, d.max(1)) } /// Build from a raw `n/d`. This could lead to / 0 if not properly handled. - pub fn from_unchecked(n: u32, d: u32) -> Self { + pub fn from_unchecked(n: u64, d: u64) -> Self { Self(n, d) } /// Return the numerator. - pub fn n(&self) -> u32 { + pub fn n(&self) -> u64 { self.0 } /// Return the denominator. - pub fn d(&self) -> u32 { + pub fn d(&self) -> u64 { self.1 } /// A saturating add that assumes `self` and `other` have the same denominator. - pub fn lazy_add(self, other: Self) -> Self { + pub fn lazy_saturating_add(self, other: Self) -> Self { if other.is_zero() { self } else { - Self(self.0 + other.0, self.1) + Self(self.0.saturating_add(other.0), self.1) } } /// A saturating subtraction that assumes `self` and `other` have the same denominator. - pub fn lazy_sub(self, other: Self) -> Self { + pub fn lazy_saturating_sub(self, other: Self) -> Self { if other.is_zero() { self } else { - Self(self.0 - other.0, self.1) + Self(self.0.saturating_sub(other.0), self.1) } } @@ -152,7 +152,7 @@ mod structs { /// - Else, convert them both into big numbers and re-try. /// /// Invariant: c must be greater than or equal to 1. - pub fn multiply_by_rational(a: u32, b: u32, mut c: u32) -> u32 { + pub fn multiply_by_rational(a: u64, b: u64, mut c: u64) -> u64 { if a.is_zero() || b.is_zero() { return 0; } @@ -172,17 +172,17 @@ mod structs { c = 1; } - ((a as u64 * b as u64) / c as u64) as _ + ((a as u128 * b as u128) / c as u128) as _ } } - impl PartialOrd for Rational32 { + impl PartialOrd for Rational64 { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } - impl Ord for Rational32 { + impl Ord for Rational64 { fn cmp(&self, other: &Self) -> Ordering { // handle some edge cases. if self.1 == other.1 { @@ -200,7 +200,7 @@ mod structs { } } - impl PartialEq for Rational32 { + impl PartialEq for Rational64 { fn eq(&self, other: &Self) -> bool { // handle some edge cases. if self.1 == other.1 { diff --git a/primitives/phragmen/src/lib.rs b/primitives/phragmen/src/lib.rs index d1f31e94b..a08812a64 100644 --- a/primitives/phragmen/src/lib.rs +++ b/primitives/phragmen/src/lib.rs @@ -33,16 +33,32 @@ #![cfg_attr(not(feature = "std"), no_std)] +#[cfg(test)] +mod mock; +#[cfg(test)] +mod tests; + use sp_runtime::{ - traits::{Member, Saturating, Zero}, + traits::{Member, Saturating, SimpleArithmetic, Zero}, Perbill, RuntimeDebug, }; use sp_std::{collections::btree_map::BTreeMap, prelude::*}; -use darwinia_support::Rational32; +use darwinia_support::Rational64; +use sp_runtime::traits::SaturatedConversion; + +/// Power of an account. +pub type Power = u32; + +/// A type in which performing operations on power and voters are safe. +/// +/// `Power` is `u32`. Hence, `u64` is a safe type for arithmetic operations over them. +/// +/// Power type converted to this is referred to as `Votes`. +pub type Votes = u64; -/// `Votes` is `Power`. -pub type Votes = u32; +/// The denominator used for loads. For maximum accuracy we simply use u64; +const DEN: u64 = u64::max_value(); /// A candidate entity for phragmen election. #[derive(Clone, Default, RuntimeDebug)] @@ -50,7 +66,7 @@ pub struct Candidate { /// Identifier. pub who: AccountId, /// Intermediary value used to sort candidates. - pub score: Rational32, + pub score: Rational64, /// Sum of the stake of this candidate based on received votes. approval_stake: Votes, /// Flag for being elected. @@ -67,7 +83,7 @@ pub struct Voter { /// The stake of this voter. budget: Votes, /// Incremented each time a candidate that this voter voted for has been elected. - load: Rational32, + load: Rational64, } /// A candidate being backed by a voter. @@ -76,7 +92,7 @@ pub struct Edge { /// Identifier. who: AccountId, /// Load of this vote. - load: Rational32, + load: Rational64, /// Index of the candidate stored in the 'candidates' vector. candidate_index: usize, } @@ -85,7 +101,14 @@ pub struct Edge { pub type PhragmenAssignment = (AccountId, Perbill); /// Means a particular `AccountId` was backed by `Votes` of a nominator's stake. -pub type PhragmenStakedAssignment = (AccountId, Votes); +#[derive(RuntimeDebug)] +#[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))] +pub struct PhragmenStakedAssignment { + pub account_id: AccountId, + pub ring_balance: RingBalance, + pub kton_balance: KtonBalance, + pub votes: Votes, +} /// Final result of the phragmen election. #[derive(RuntimeDebug)] @@ -107,17 +130,21 @@ pub struct PhragmenResult { /// they do not necessarily have to be the same. #[derive(Default, RuntimeDebug)] #[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))] -pub struct Support { +pub struct Support { /// The amount of support as the effect of self-vote. - pub own: Votes, + pub own_votes: Votes, + pub own_ring_balance: RingBalance, + pub own_kton_balance: KtonBalance, /// Total support. - pub total: Votes, + pub total_votes: Votes, + pub total_ring_balance: RingBalance, + pub total_kton_balance: KtonBalance, /// Support from voters. - pub others: Vec>, + pub others: Vec>, } /// A linkage from a candidate and its [`Support`]. -pub type SupportMap = BTreeMap>; +pub type SupportMap = BTreeMap>; /// Perform election based on Phragmén algorithm. /// @@ -141,12 +168,13 @@ pub fn elect( initial_candidates: Vec, initial_voters: Vec<(AccountId, Vec)>, power_of: FS, - total_power: Votes, ) -> Option> where AccountId: Default + Ord + Member, - for<'r> FS: Fn(&'r AccountId) -> Votes, + for<'r> FS: Fn(&'r AccountId) -> Power, { + let to_votes = |p: Power| p as Votes; + // return structures let mut elected_candidates: Vec<(AccountId, Votes)>; let mut assigned: Vec<(AccountId, Vec>)>; @@ -185,7 +213,7 @@ where for v in votes { if let Some(idx) = c_idx_cache.get(&v) { // This candidate is valid + already cached. - candidates[*idx].approval_stake += voter_stake; + candidates[*idx].approval_stake = candidates[*idx].approval_stake.saturating_add(to_votes(voter_stake)); edges.push(Edge { who: v.clone(), candidate_index: *idx, @@ -196,8 +224,8 @@ where Voter { who, edges, - budget: voter_stake, - load: Rational32::zero(), + budget: to_votes(voter_stake), + load: Rational64::zero(), } })); @@ -215,9 +243,9 @@ where // 1 / approval_stake == (total_power / approval_stake) / total_power. If approval_stake is zero, // then the ratio should be as large as possible, essentially `infinity`. if c.approval_stake.is_zero() { - c.score = Rational32::from_unchecked(total_power, 0); + c.score = Rational64::from_unchecked(DEN as _, 0); } else { - c.score = Rational32::from(total_power / c.approval_stake, total_power); + c.score = Rational64::from(DEN / c.approval_stake as u64, DEN); } } } @@ -227,10 +255,10 @@ where for e in &n.edges { let c = &mut candidates[e.candidate_index]; if !c.elected && !c.approval_stake.is_zero() { - let temp_n = Rational32::multiply_by_rational(n.load.n(), n.budget, c.approval_stake); + let temp_n = Rational64::multiply_by_rational(n.load.n(), n.budget as _, c.approval_stake as _); let temp_d = n.load.d(); - let temp = Rational32::from(temp_n, temp_d); - c.score = c.score.lazy_add(temp); + let temp = Rational64::from(temp_n, temp_d); + c.score = c.score.lazy_saturating_add(temp); } } } @@ -242,7 +270,7 @@ where for n in &mut voters { for e in &mut n.edges { if e.who == winner.who { - e.load = winner.score.lazy_sub(n.load); + e.load = winner.score.lazy_saturating_sub(n.load); n.load = winner.score; } } @@ -266,8 +294,8 @@ where } else { if e.load.d() == n.load.d() { // return e.load / n.load. - let desired_scale: Votes = Perbill::accuracy().into(); - Rational32::multiply_by_rational(desired_scale, e.load.n(), n.load.n()) + let desired_scale = Perbill::accuracy().into(); + Rational64::multiply_by_rational(desired_scale, e.load.n(), n.load.n()) } else { // defensive only. Both edge and nominator loads are built from // scores, hence MUST have the same denominator. @@ -276,7 +304,7 @@ where } }; // safer to .min() inside as well to argue as u32 is safe. - let per_thing = Perbill::from_parts(per_bill_parts.min(Perbill::accuracy().into())); + let per_thing = Perbill::from_parts(per_bill_parts.min(Perbill::accuracy().into()) as u32); assignment.1.push((e.who.clone(), per_thing)); } } @@ -318,17 +346,22 @@ where } /// Build the support map from the given phragmen result. -pub fn build_support_map( +pub fn build_support_map( elected_stashes: &Vec, assignments: &Vec<(AccountId, Vec>)>, power_of: FS, -) -> SupportMap + stake_of: FSS, +) -> SupportMap where AccountId: Default + Ord + Member, - for<'r> FS: Fn(&'r AccountId) -> Votes, + RingBalance: Default + Copy + SimpleArithmetic, + KtonBalance: Default + Copy + SimpleArithmetic, + for<'r> FS: Fn(&'r AccountId) -> Power, + for<'r> FSS: Fn(&'r AccountId) -> (RingBalance, KtonBalance), { + let to_votes = |p: Power| p as Votes; // Initialize the support of each candidate. - let mut supports = >::new(); + let mut supports = >::new(); elected_stashes.iter().for_each(|e| { supports.insert(e.clone(), Default::default()); }); @@ -336,24 +369,43 @@ where // build support struct. for (n, assignment) in assignments.iter() { for (c, per_thing) in assignment.iter() { - let nominator_stake = power_of(n); - // AUDIT: it is crucially important for the `Mul` implementation of all - // per-things to be sound. - let other_stake = *per_thing * nominator_stake; if let Some(support) = supports.get_mut(c) { + let nominator_stake = to_votes(power_of(n)); + let (ring_balance, kton_balance) = { + let (r, k) = stake_of(n); + (*per_thing * r, *per_thing * k) + }; + // AUDIT: it is crucially important for the `Mul` implementation of all + // per-things to be sound. + let other_stake = *per_thing * nominator_stake; if c == n { // This is a nomination from `n` to themselves. This will increase both the // `own` and `total` field. debug_assert!(*per_thing == Perbill::one()); // TODO: deal with this: do we want it? - support.own += other_stake; - support.total += other_stake; + + support.own_ring_balance = support.own_ring_balance.saturating_add(ring_balance); + support.total_ring_balance = support.total_ring_balance.saturating_add(ring_balance); + + support.own_kton_balance = support.own_kton_balance.saturating_add(kton_balance); + support.total_kton_balance = support.total_kton_balance.saturating_add(kton_balance); + + support.own_votes = support.own_votes.saturating_add(other_stake); + support.total_votes = support.total_votes.saturating_add(other_stake); } else { // This is a nomination from `n` to someone else. Increase `total` and add an entry // inside `others`. // For an astronomically rich validator with more astronomically rich // set of nominators, this might saturate. - support.total += other_stake; - support.others.push((n.clone(), other_stake)); + support.total_ring_balance = support.total_ring_balance.saturating_add(ring_balance); + support.total_kton_balance = support.total_kton_balance.saturating_add(kton_balance); + support.total_votes = support.total_votes.saturating_add(other_stake); + + support.others.push(PhragmenStakedAssignment { + account_id: n.clone(), + ring_balance, + kton_balance, + votes: other_stake, + }); } } } @@ -372,15 +424,20 @@ where /// * `tolerance`: maximum difference that can occur before an early quite happens. /// * `iterations`: maximum number of iterations that will be processed. /// * `power_of`: something that can return the stake stake of a particular candidate or voter. -pub fn equalize( - mut assignments: Vec<(AccountId, Vec>)>, - supports: &mut SupportMap, +pub fn equalize( + mut assignments: Vec<( + AccountId, + Vec>, + )>, + supports: &mut SupportMap, tolerance: Votes, iterations: usize, power_of: FS, ) where AccountId: Ord + Clone, - for<'r> FS: Fn(&'r AccountId) -> Votes, + RingBalance: Copy + SimpleArithmetic, + KtonBalance: Copy + SimpleArithmetic, + for<'r> FS: Fn(&'r AccountId) -> Power, { // prepare the data for equalise for _i in 0..iterations { @@ -389,7 +446,7 @@ pub fn equalize( for (voter, assignment) in assignments.iter_mut() { let voter_budget = power_of(&voter); - let diff = do_equalize::<_>(voter, voter_budget, assignment, supports, tolerance); + let diff = do_equalize::<_, _, _>(voter, voter_budget, assignment, supports, tolerance); if diff > max_diff { max_diff = diff; } @@ -403,14 +460,20 @@ pub fn equalize( /// actually perform equalize. same interface is `equalize`. Just called in loops with a check for /// maximum difference. -fn do_equalize( +fn do_equalize( voter: &AccountId, - budget_balance: Votes, - elected_edges: &mut Vec>, - support_map: &mut SupportMap, + budget_balance: Power, + elected_edges: &mut Vec>, + support_map: &mut SupportMap, tolerance: Votes, -) -> Votes { - let budget = budget_balance; +) -> Votes +where + AccountId: Ord + Clone, + RingBalance: Copy + SimpleArithmetic, + KtonBalance: Copy + SimpleArithmetic, +{ + let to_votes = |p: Power| p as Votes; + let budget = to_votes(budget_balance); // Nothing to do. This voter had nothing useful. // Defensive only. Assignment list should always be populated. @@ -418,18 +481,18 @@ fn do_equalize( return 0; } - let stake_used = elected_edges.iter().fold(0 as Votes, |s, e| s + e.1); + let stake_used = elected_edges.iter().fold(0 as Votes, |s, e| s.saturating_add(e.votes)); let backed_stakes_iter = elected_edges .iter() - .filter_map(|e| support_map.get(&e.0)) - .map(|e| e.total); + .filter_map(|e| support_map.get(&e.account_id)) + .map(|e| e.total_votes); let backing_backed_stake = elected_edges .iter() - .filter(|e| e.1 > 0) - .filter_map(|e| support_map.get(&e.0)) - .map(|e| e.total) + .filter(|e| e.votes > 0) + .filter_map(|e| support_map.get(&e.account_id)) + .map(|e| e.total_votes) .collect::>(); let mut difference; @@ -443,7 +506,7 @@ fn do_equalize( .expect("iterator with positive length will have a min; qed"); difference = max_stake.saturating_sub(min_stake); - difference += budget.saturating_sub(stake_used); + difference = difference.saturating_add(budget.saturating_sub(stake_used)); if difference < tolerance { return difference; } @@ -453,16 +516,16 @@ fn do_equalize( // Undo updates to support elected_edges.iter_mut().for_each(|e| { - if let Some(support) = support_map.get_mut(&e.0) { - support.total = support.total.saturating_sub(e.1); - support.others.retain(|i_support| i_support.0 != *voter); + if let Some(support) = support_map.get_mut(&e.account_id) { + support.total_votes = support.total_votes.saturating_sub(e.votes); + support.others.retain(|i_support| i_support.account_id != *voter); } - e.1 = 0; + e.votes = 0; }); elected_edges.sort_unstable_by_key(|e| { - if let Some(e) = support_map.get(&e.0) { - e.total + if let Some(e) = support_map.get(&e.account_id) { + e.total_votes } else { Zero::zero() } @@ -472,27 +535,46 @@ fn do_equalize( let mut last_index = elected_edges.len() - 1; let mut idx = 0usize; for e in &mut elected_edges[..] { - if let Some(support) = support_map.get_mut(&e.0) { - let stake = support.total; + if let Some(support) = support_map.get_mut(&e.account_id) { + let stake = support.total_votes; let stake_mul = stake.saturating_mul(idx as Votes); let stake_sub = stake_mul.saturating_sub(cumulative_stake); if stake_sub > budget { last_index = idx.checked_sub(1).unwrap_or(0); break; } - cumulative_stake += stake; + cumulative_stake = cumulative_stake.saturating_add(stake); } idx += 1; } - let last_stake = elected_edges[last_index].1; + let PhragmenStakedAssignment { + ring_balance: last_ring_balance, + kton_balance: last_kton_balance, + votes: last_votes, + .. + } = elected_edges[last_index]; let split_ways = last_index + 1; - let excess = (budget + cumulative_stake).saturating_sub(last_stake.saturating_mul(split_ways as Votes)); + let excess = budget + .saturating_add(cumulative_stake) + .saturating_sub(last_votes.saturating_mul(split_ways as Votes)); elected_edges.iter_mut().take(split_ways).for_each(|e| { - if let Some(support) = support_map.get_mut(&e.0) { - e.1 = ((excess / split_ways as Votes) + last_stake).saturating_sub(support.total); - support.total += e.1; - support.others.push((voter.clone(), e.1)); + if let Some(support) = support_map.get_mut(&e.account_id) { + e.ring_balance = ((excess.saturated_into::() / split_ways.saturated_into::()) + + last_ring_balance) + .saturating_sub(support.total_ring_balance); + e.kton_balance = ((excess.saturated_into::() / split_ways.saturated_into::()) + + last_kton_balance) + .saturating_sub(support.total_kton_balance); + e.votes = ((excess / split_ways as Votes) + last_votes).saturating_sub(support.total_votes); + + support.total_votes = support.total_votes.saturating_add(e.votes); + support.others.push(PhragmenStakedAssignment { + account_id: voter.clone(), + ring_balance: e.ring_balance, + kton_balance: e.kton_balance, + votes: e.votes, + }); } }); diff --git a/primitives/phragmen/src/mock.rs b/primitives/phragmen/src/mock.rs new file mode 100644 index 000000000..0f2dee159 --- /dev/null +++ b/primitives/phragmen/src/mock.rs @@ -0,0 +1,371 @@ +#![cfg(test)] + +use std::collections::BTreeMap; + +use sp_runtime::{assert_eq_error_rate, traits::Member, Perbill}; + +use crate::{elect, PhragmenAssignment, PhragmenResult, Power, Votes}; + +pub(crate) type AccountId = u64; + +pub(crate) type _PhragmenAssignment = (A, f64); +pub(crate) type _SupportMap = BTreeMap>; + +#[derive(Default, Debug)] +pub(crate) struct _Candidate { + who: A, + score: f64, + approval_stake: f64, + elected: bool, +} + +#[derive(Default, Debug)] +pub(crate) struct _Voter { + who: A, + edges: Vec<_Edge>, + budget: f64, + load: f64, +} + +#[derive(Default, Debug)] +pub(crate) struct _Edge { + who: A, + load: f64, + candidate_index: usize, +} + +#[derive(Default, Debug, PartialEq)] +pub(crate) struct _Support { + pub own: f64, + pub total: f64, + pub others: Vec<_PhragmenAssignment>, +} + +#[derive(Debug, Clone)] +pub(crate) struct _PhragmenResult { + pub winners: Vec<(A, Votes)>, + pub assignments: Vec<(A, Vec<_PhragmenAssignment>)>, +} + +pub(crate) fn create_stake_of(stakes: &[(AccountId, Power)]) -> Box Power> { + let mut storage = BTreeMap::::new(); + stakes.iter().for_each(|s| { + storage.insert(s.0, s.1); + }); + let stake_of = move |who: &AccountId| -> Power { storage.get(who).unwrap().to_owned() }; + Box::new(stake_of) +} + +pub(crate) fn auto_generate_self_voters(candidates: &[A]) -> Vec<(A, Vec)> { + candidates.iter().map(|c| (c.clone(), vec![c.clone()])).collect() +} + +pub(crate) fn check_assignments(assignments: Vec<(AccountId, Vec>)>) { + for (_, a) in assignments { + let sum: u32 = a.iter().map(|(_, p)| p.deconstruct()).sum(); + assert_eq_error_rate!(sum, Perbill::accuracy(), 5); + } +} + +pub(crate) fn run_and_compare( + candidates: Vec, + voters: Vec<(AccountId, Vec)>, + stake_of: Box Power>, + to_elect: usize, + min_to_elect: usize, +) { + // run fixed point code. + let PhragmenResult { winners, assignments } = + elect::<_, _>(to_elect, min_to_elect, candidates.clone(), voters.clone(), &stake_of).unwrap(); + + // run float poc code. + let truth_value = elect_float(to_elect, min_to_elect, candidates, voters, &stake_of).unwrap(); + + assert_eq!(winners, truth_value.winners); + + for (nominator, assigned) in assignments.clone() { + if let Some(float_assignments) = truth_value.assignments.iter().find(|x| x.0 == nominator) { + for (candidate, per_thingy) in assigned { + if let Some(float_assignment) = float_assignments.1.iter().find(|x| x.0 == candidate) { + assert_eq_error_rate!( + Perbill::from_fraction(float_assignment.1).deconstruct(), + per_thingy.deconstruct(), + 1, + ); + } else { + panic!("candidate mismatch. This should never happen.") + } + } + } else { + panic!("nominator mismatch. This should never happen.") + } + } + + check_assignments(assignments); +} + +pub(crate) fn elect_float( + candidate_count: usize, + minimum_candidate_count: usize, + initial_candidates: Vec, + initial_voters: Vec<(A, Vec)>, + stake_of: FS, +) -> Option<_PhragmenResult> +where + A: Default + Ord + Member + Copy, + for<'r> FS: Fn(&'r A) -> Power, +{ + let mut elected_candidates: Vec<(A, Votes)>; + let mut assigned: Vec<(A, Vec<_PhragmenAssignment>)>; + let mut c_idx_cache = BTreeMap::::new(); + let num_voters = initial_candidates.len() + initial_voters.len(); + let mut voters: Vec<_Voter> = Vec::with_capacity(num_voters); + + let mut candidates = initial_candidates + .into_iter() + .enumerate() + .map(|(idx, who)| { + c_idx_cache.insert(who.clone(), idx); + _Candidate { + who, + ..Default::default() + } + }) + .collect::>>(); + + if candidates.len() < minimum_candidate_count { + return None; + } + + voters.extend(initial_voters.into_iter().map(|(who, votes)| { + let voter_stake = stake_of(&who) as f64; + let mut edges: Vec<_Edge> = Vec::with_capacity(votes.len()); + for v in votes { + if let Some(idx) = c_idx_cache.get(&v) { + candidates[*idx].approval_stake = candidates[*idx].approval_stake + voter_stake; + edges.push(_Edge { + who: v.clone(), + candidate_index: *idx, + ..Default::default() + }); + } + } + _Voter { + who, + edges, + budget: voter_stake, + load: 0f64, + } + })); + + let to_elect = candidate_count.min(candidates.len()); + elected_candidates = Vec::with_capacity(candidate_count); + assigned = Vec::with_capacity(candidate_count); + + for _round in 0..to_elect { + for c in &mut candidates { + if !c.elected { + c.score = 1.0 / c.approval_stake; + } + } + for n in &voters { + for e in &n.edges { + let c = &mut candidates[e.candidate_index]; + if !c.elected && !(c.approval_stake == 0f64) { + c.score += n.budget * n.load / c.approval_stake; + } + } + } + + if let Some(winner) = candidates + .iter_mut() + .filter(|c| !c.elected) + .min_by(|x, y| x.score.partial_cmp(&y.score).unwrap_or(sp_std::cmp::Ordering::Equal)) + { + winner.elected = true; + for n in &mut voters { + for e in &mut n.edges { + if e.who == winner.who { + e.load = winner.score - n.load; + n.load = winner.score; + } + } + } + + elected_candidates.push((winner.who.clone(), winner.approval_stake as Votes)); + } else { + break; + } + } + + for n in &mut voters { + let mut assignment = (n.who.clone(), vec![]); + for e in &mut n.edges { + if let Some(c) = elected_candidates.iter().cloned().map(|(c, _)| c).find(|c| *c == e.who) { + if c != n.who { + let ratio = e.load / n.load; + assignment.1.push((e.who.clone(), ratio)); + } + } + } + if assignment.1.len() > 0 { + assigned.push(assignment); + } + } + + Some(_PhragmenResult { + winners: elected_candidates, + assignments: assigned, + }) +} + +pub(crate) fn build_support_map(result: &mut _PhragmenResult, stake_of: FS) -> _SupportMap +where + for<'r> FS: Fn(&'r AccountId) -> Power, +{ + let mut supports = <_SupportMap>::new(); + result + .winners + .iter() + .map(|(e, _)| (e, stake_of(e) as f64)) + .for_each(|(e, s)| { + let item = _Support { + own: s, + total: s, + ..Default::default() + }; + supports.insert(e.clone(), item); + }); + + for (n, assignment) in result.assignments.iter_mut() { + for (c, r) in assignment.iter_mut() { + let nominator_stake = stake_of(n) as f64; + let other_stake = nominator_stake * *r; + if let Some(support) = supports.get_mut(c) { + support.total = support.total + other_stake; + support.others.push((n.clone(), other_stake)); + } + *r = other_stake; + } + } + supports +} + +pub(crate) fn equalize_float( + mut assignments: Vec<(A, Vec<_PhragmenAssignment>)>, + supports: &mut _SupportMap, + tolerance: f64, + iterations: usize, + stake_of: FS, +) where + for<'r> FS: Fn(&'r A) -> Power, + A: Ord + Clone + std::fmt::Debug, +{ + for _i in 0..iterations { + let mut max_diff = 0.0; + for (voter, assignment) in assignments.iter_mut() { + let voter_budget = stake_of(&voter); + let diff = do_equalize_float(voter, voter_budget, assignment, supports, tolerance); + if diff > max_diff { + max_diff = diff; + } + } + + if max_diff < tolerance { + break; + } + } +} + +pub(crate) fn do_equalize_float( + voter: &A, + budget_balance: Power, + elected_edges: &mut Vec<_PhragmenAssignment>, + support_map: &mut _SupportMap, + tolerance: f64, +) -> f64 +where + A: Ord + Clone, +{ + let budget = budget_balance as f64; + if elected_edges.is_empty() { + return 0.0; + } + + let stake_used = elected_edges.iter().fold(0.0, |s, e| s + e.1); + + let backed_stakes_iter = elected_edges + .iter() + .filter_map(|e| support_map.get(&e.0)) + .map(|e| e.total); + + let backing_backed_stake = elected_edges + .iter() + .filter(|e| e.1 > 0.0) + .filter_map(|e| support_map.get(&e.0)) + .map(|e| e.total) + .collect::>(); + + let mut difference; + if backing_backed_stake.len() > 0 { + let max_stake = backing_backed_stake + .iter() + .max_by(|x, y| x.partial_cmp(&y).unwrap_or(sp_std::cmp::Ordering::Equal)) + .expect("vector with positive length will have a max; qed"); + let min_stake = backed_stakes_iter + .min_by(|x, y| x.partial_cmp(&y).unwrap_or(sp_std::cmp::Ordering::Equal)) + .expect("iterator with positive length will have a min; qed"); + + difference = max_stake - min_stake; + difference = difference + budget - stake_used; + if difference < tolerance { + return difference; + } + } else { + difference = budget; + } + + // Undo updates to support + elected_edges.iter_mut().for_each(|e| { + if let Some(support) = support_map.get_mut(&e.0) { + support.total = support.total - e.1; + support.others.retain(|i_support| i_support.0 != *voter); + } + e.1 = 0.0; + }); + + elected_edges.sort_unstable_by(|x, y| { + support_map + .get(&x.0) + .and_then(|x| support_map.get(&y.0).and_then(|y| x.total.partial_cmp(&y.total))) + .unwrap_or(sp_std::cmp::Ordering::Equal) + }); + + let mut cumulative_stake = 0.0; + let mut last_index = elected_edges.len() - 1; + elected_edges.iter_mut().enumerate().for_each(|(idx, e)| { + if let Some(support) = support_map.get_mut(&e.0) { + let stake = support.total; + let stake_mul = stake * (idx as f64); + let stake_sub = stake_mul - cumulative_stake; + if stake_sub > budget { + last_index = idx.checked_sub(1).unwrap_or(0); + return; + } + cumulative_stake = cumulative_stake + stake; + } + }); + + let last_stake = elected_edges[last_index].1; + let split_ways = last_index + 1; + let excess = budget + cumulative_stake - last_stake * (split_ways as f64); + elected_edges.iter_mut().take(split_ways).for_each(|e| { + if let Some(support) = support_map.get_mut(&e.0) { + e.1 = excess / (split_ways as f64) + last_stake - support.total; + support.total = support.total + e.1; + support.others.push((voter.clone(), e.1)); + } + }); + + difference +} diff --git a/primitives/phragmen/src/tests.rs b/primitives/phragmen/src/tests.rs new file mode 100644 index 000000000..6c3a9093f --- /dev/null +++ b/primitives/phragmen/src/tests.rs @@ -0,0 +1,320 @@ +#![cfg(test)] + +use sp_runtime::Perbill; +use substrate_test_utils::assert_eq_uvec; + +use crate::{elect, mock::*, PhragmenResult}; + +#[test] +fn float_phragmen_poc_works() { + let candidates = vec![1, 2, 3]; + let voters = vec![(10, vec![1, 2]), (20, vec![1, 3]), (30, vec![2, 3])]; + let stake_of = create_stake_of(&[(10, 10), (20, 20), (30, 30), (1, 0), (2, 0), (3, 0)]); + let mut phragmen_result = elect_float(2, 2, candidates, voters, &stake_of).unwrap(); + let winners = phragmen_result.clone().winners; + let assignments = phragmen_result.clone().assignments; + + assert_eq_uvec!(winners, vec![(2, 40), (3, 50)]); + assert_eq_uvec!( + assignments, + vec![ + (10, vec![(2, 1.0)]), + (20, vec![(3, 1.0)]), + (30, vec![(2, 0.5), (3, 0.5)]), + ] + ); + + let mut support_map = build_support_map(&mut phragmen_result, &stake_of); + + assert_eq!( + support_map.get(&2).unwrap(), + &_Support { + own: 0.0, + total: 25.0, + others: vec![(10u64, 10.0), (30u64, 15.0)] + } + ); + assert_eq!( + support_map.get(&3).unwrap(), + &_Support { + own: 0.0, + total: 35.0, + others: vec![(20u64, 20.0), (30u64, 15.0)] + } + ); + + equalize_float(phragmen_result.assignments, &mut support_map, 0.0, 2, stake_of); + + assert_eq!( + support_map.get(&2).unwrap(), + &_Support { + own: 0.0, + total: 30.0, + others: vec![(10u64, 10.0), (30u64, 20.0)] + } + ); + assert_eq!( + support_map.get(&3).unwrap(), + &_Support { + own: 0.0, + total: 30.0, + others: vec![(20u64, 20.0), (30u64, 10.0)] + } + ); +} + +#[test] +fn phragmen_poc_works() { + let candidates = vec![1, 2, 3]; + let voters = vec![(10, vec![1, 2]), (20, vec![1, 3]), (30, vec![2, 3])]; + + let PhragmenResult { winners, assignments } = elect::<_, _>( + 2, + 2, + candidates, + voters, + create_stake_of(&[(10, 10), (20, 20), (30, 30)]), + ) + .unwrap(); + + assert_eq_uvec!(winners, vec![(2, 40), (3, 50)]); + assert_eq_uvec!( + assignments, + vec![ + (10, vec![(2, Perbill::from_percent(100))]), + (20, vec![(3, Perbill::from_percent(100))]), + ( + 30, + vec![(2, Perbill::from_percent(100 / 2)), (3, Perbill::from_percent(100 / 2))] + ), + ] + ); +} + +#[test] +fn phragmen_poc_2_works() { + let candidates = vec![10, 20, 30]; + let voters = vec![(2, vec![10, 20, 30]), (4, vec![10, 20, 40])]; + let stake_of = create_stake_of(&[(10, 1_000), (20, 1_000), (30, 1_000), (40, 1_000), (2, 500), (4, 500)]); + + run_and_compare(candidates, voters, stake_of, 2, 2); +} + +#[test] +fn phragmen_poc_3_works() { + let candidates = vec![10, 20, 30]; + let voters = vec![(2, vec![10, 20, 30]), (4, vec![10, 20, 40])]; + let stake_of = create_stake_of(&[(10, 1_000), (20, 1_000), (30, 1_000), (2, 50), (4, 1_000)]); + + run_and_compare(candidates, voters, stake_of, 2, 2); +} + +#[test] +fn phragmen_accuracy_on_large_scale_only_validators() { + // because of this particular situation we had per_u128 and now rational128. In practice, a + // candidate can have the maximum amount of tokens, and also supported by the maximum. + let candidates = vec![1, 2, 3, 4, 5]; + let stake_of = create_stake_of(&[ + (1, (u32::max_value() - 1).into()), + (2, (u32::max_value() - 4).into()), + (3, (u32::max_value() - 5).into()), + (4, (u32::max_value() - 3).into()), + (5, (u32::max_value() - 2).into()), + ]); + + let PhragmenResult { winners, assignments } = elect::<_, _>( + 2, + 2, + candidates.clone(), + auto_generate_self_voters(&candidates), + stake_of, + ) + .unwrap(); + + assert_eq_uvec!(winners, vec![(1, 4_294_967_294_u64), (5, 4_294_967_293_u64)]); + assert_eq!(assignments.len(), 2); + check_assignments(assignments); +} + +#[test] +fn phragmen_accuracy_on_large_scale_validators_and_nominators() { + let candidates = vec![1, 2, 3, 4, 5]; + let mut voters = vec![(13, vec![1, 3, 5]), (14, vec![2, 4])]; + voters.extend(auto_generate_self_voters(&candidates)); + let stake_of = create_stake_of(&[ + (1, (u32::max_value() - 1).into()), + (2, (u32::max_value() - 4).into()), + (3, (u32::max_value() - 5).into()), + (4, (u32::max_value() - 3).into()), + (5, (u32::max_value() - 2).into()), + (13, (u32::max_value() - 10).into()), + (14, u32::max_value().into()), + ]); + + let PhragmenResult { winners, assignments } = elect::<_, _>(2, 2, candidates, voters, stake_of).unwrap(); + + assert_eq_uvec!(winners, vec![(2, 8_589_934_586_u64), (1, 8_589_934_579_u64)]); + assert_eq!( + assignments, + vec![ + (13, vec![(1, Perbill::one())]), + (14, vec![(2, Perbill::one())]), + (1, vec![(1, Perbill::one())]), + (2, vec![(2, Perbill::one())]), + ] + ); + check_assignments(assignments); +} + +#[test] +fn phragmen_accuracy_on_small_scale_self_vote() { + let candidates = vec![40, 10, 20, 30]; + let voters = auto_generate_self_voters(&candidates); + let stake_of = create_stake_of(&[(40, 0), (10, 1), (20, 2), (30, 1)]); + + let PhragmenResult { + winners, + assignments: _, + } = elect::<_, _>(3, 3, candidates, voters, stake_of).unwrap(); + + assert_eq_uvec!(winners, vec![(20, 2), (10, 1), (30, 1)]); +} + +#[test] +fn phragmen_accuracy_on_small_scale_no_self_vote() { + let candidates = vec![40, 10, 20, 30]; + let voters = vec![(1, vec![10]), (2, vec![20]), (3, vec![30]), (4, vec![40])]; + let stake_of = create_stake_of(&[ + (40, 1000), // don't care + (10, 1000), // don't care + (20, 1000), // don't care + (30, 1000), // don't care + (4, 0), + (1, 1), + (2, 2), + (3, 1), + ]); + + let PhragmenResult { + winners, + assignments: _, + } = elect::<_, _>(3, 3, candidates, voters, stake_of).unwrap(); + + assert_eq_uvec!(winners, vec![(20, 2), (10, 1), (30, 1)]); +} + +#[test] +fn phragmen_large_scale_test() { + let candidates = vec![2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24]; + let mut voters = vec![(50, vec![2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24])]; + voters.extend(auto_generate_self_voters(&candidates)); + let stake_of = create_stake_of(&[ + (2, 1), + (4, 10), + (6, 100), + (8, 10_100), + (10, 10_200), + (12, 10_300), + (14, 40_000), + (16, 41_000), + (18, 1_800_000), + (20, 2_000_000), + (22, 50_100_000), + (24, 50_200_000), + (50, 99_000_000), + ]); + + let PhragmenResult { winners, assignments } = elect::<_, _>(2, 2, candidates, voters, stake_of).unwrap(); + + assert_eq_uvec!(winners, vec![(24, 149_200_000_u64), (22, 149_100_000_u64)]); + check_assignments(assignments); +} + +#[test] +fn phragmen_large_scale_test_2() { + let nom_budget: u64 = 999_999_990; + let c_budget: u64 = 1; + + let candidates = vec![2, 4]; + let mut voters = vec![(50, vec![2, 4])]; + voters.extend(auto_generate_self_voters(&candidates)); + + let stake_of = create_stake_of(&[(2, c_budget as _), (4, c_budget as _), (50, nom_budget as _)]); + + let PhragmenResult { winners, assignments } = elect::<_, _>(2, 2, candidates, voters, stake_of).unwrap(); + + assert_eq_uvec!(winners, vec![(2, 999_999_991_u64), (4, 999_999_991_u64)]); + assert_eq!( + assignments, + vec![ + ( + 50, + vec![ + (2, Perbill::from_parts(500_000_001)), + (4, Perbill::from_parts(499_999_999)) + ] + ), + (2, vec![(2, Perbill::one())]), + (4, vec![(4, Perbill::one())]), + ], + ); + check_assignments(assignments); +} + +#[test] +fn phragmen_linear_equalize() { + let candidates = vec![11, 21, 31, 41, 51, 61, 71]; + let voters = vec![ + (2, vec![11]), + (4, vec![11, 21]), + (6, vec![21, 31]), + (8, vec![31, 41]), + (110, vec![41, 51]), + (120, vec![51, 61]), + (130, vec![61, 71]), + ]; + let stake_of = create_stake_of(&[ + (11, 1000), + (21, 1000), + (31, 1000), + (41, 1000), + (51, 1000), + (61, 1000), + (71, 1000), + (2, 2000), + (4, 1000), + (6, 1000), + (8, 1000), + (110, 1000), + (120, 1000), + (130, 1000), + ]); + + run_and_compare(candidates, voters, stake_of, 2, 2); +} + +#[test] +fn elect_has_no_entry_barrier() { + let candidates = vec![10, 20, 30]; + let voters = vec![(1, vec![10]), (2, vec![20])]; + let stake_of = create_stake_of(&[(1, 10), (2, 10)]); + + let PhragmenResult { + winners, + assignments: _, + } = elect::<_, _>(3, 3, candidates, voters, stake_of).unwrap(); + + // 30 is elected with stake 0. The caller is responsible for stripping this. + assert_eq_uvec!(winners, vec![(10, 10), (20, 10), (30, 0),]); +} + +#[test] +fn minimum_to_elect_is_respected() { + let candidates = vec![10, 20, 30]; + let voters = vec![(1, vec![10]), (2, vec![20])]; + let stake_of = create_stake_of(&[(1, 10), (2, 10)]); + + let maybe_result = elect::<_, _>(10, 10, candidates, voters, stake_of); + + assert!(maybe_result.is_none()); +}