diff --git a/substrate/runtime/staking/src/lib.rs b/substrate/runtime/staking/src/lib.rs index 3a1bdf60bb088..138130886af7d 100644 --- a/substrate/runtime/staking/src/lib.rs +++ b/substrate/runtime/staking/src/lib.rs @@ -135,7 +135,9 @@ decl_module! { pub enum Call where aux: T::PublicAux { fn transfer(aux, dest: RawAddress, value: T::Balance) -> Result = 0; fn stake(aux) -> Result = 1; - fn unstake(aux) -> Result = 2; + fn unstake(aux, index: u32) -> Result = 2; + fn nominate(aux, target: RawAddress) -> Result = 3; + fn unnominate(aux, target_index: u32) -> Result = 4; } #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] @@ -157,6 +159,7 @@ decl_storage! { // The length of a staking era in sessions. pub SessionsPerEra get(sessions_per_era): b"sta:spe" => required T::BlockNumber; // The total amount of stake on the system. + // TODO: this doesn't actually track total stake yet - it should do. pub TotalStake get(total_stake): b"sta:tot" => required T::Balance; // The fee to be paid for making a transaction; the base. pub TransactionBaseFee get(transaction_base_fee): b"sta:basefee" => required T::Balance; @@ -172,7 +175,6 @@ decl_storage! { pub CreationFee get(creation_fee): b"sta:creation_fee" => required T::Balance; // The fee required to create a contract. At least as big as ReclaimRebate. pub ContractFee get(contract_fee): b"sta:contract_fee" => required T::Balance; - // Maximum reward, per validator, that is provided per acceptable session. pub SessionReward get(session_reward): b"sta:session_reward" => required T::Balance; // Slash, per validator that is taken per abnormal era end. @@ -181,11 +183,19 @@ decl_storage! { // The current era index. pub CurrentEra get(current_era): b"sta:era" => required T::BlockNumber; // All the accounts with a desire to stake. - pub Intentions: b"sta:wil:" => default Vec; + pub Intentions get(intentions): b"sta:wil:" => default Vec; + // All nominator -> nominee relationships. + pub Nominating get(nominating): b"sta:nominating" => map [ T::AccountId => T::AccountId ]; + // Nominators for a particular account. + pub NominatorsFor get(nominators_for): b"sta:nominators_for" => default map [ T::AccountId => Vec ]; + // Nominators for a particular account that is in action right now. + pub CurrentNominatorsFor get(current_nominators_for): b"sta:current_nominators_for" => default map [ T::AccountId => Vec ]; // The next value of sessions per era. pub NextSessionsPerEra get(next_sessions_per_era): b"sta:nse" => T::BlockNumber; // The session index at which the era length last changed. pub LastEraLengthChange get(last_era_length_change): b"sta:lec" => default T::BlockNumber; + // The current era stake threshold + pub StakeThreshold get(stake_threshold): b"sta:stake_threshold" => required T::Balance; // The next free enumeration set. pub NextEnumSet get(next_enum_set): b"sta:next_enum" => required T::AccountIndex; @@ -305,27 +315,82 @@ impl Module { /// /// Effects will be felt at the beginning of the next era. fn stake(aux: &T::PublicAux) -> Result { + let aux = aux.ref_into(); + ensure!(Self::nominating(aux).is_none(), "Cannot stake if already nominating."); let mut intentions = >::get(); // can't be in the list twice. - ensure!(intentions.iter().find(|&t| t == aux.ref_into()).is_none(), "Cannot stake if already staked."); - intentions.push(aux.ref_into().clone()); + ensure!(intentions.iter().find(|&t| t == aux).is_none(), "Cannot stake if already staked."); + intentions.push(aux.clone()); >::put(intentions); - >::insert(aux.ref_into(), T::BlockNumber::max_value()); + >::insert(aux, T::BlockNumber::max_value()); Ok(()) } /// Retract the desire to stake for the transactor. /// /// Effects will be felt at the beginning of the next era. - fn unstake(aux: &T::PublicAux) -> Result { + fn unstake(aux: &T::PublicAux, position: u32) -> Result { + let aux = aux.ref_into(); + let position = position as usize; let mut intentions = >::get(); - let position = intentions.iter().position(|t| t == aux.ref_into()).ok_or("Cannot unstake if not already staked.")?; +// let position = intentions.iter().position(|t| t == aux.ref_into()).ok_or("Cannot unstake if not already staked.")?; + if intentions.get(position) != Some(aux) { + return Err("Invalid index") + } intentions.swap_remove(position); >::put(intentions); >::insert(aux.ref_into(), Self::current_era() + Self::bonding_duration()); Ok(()) } + fn nominate(aux: &T::PublicAux, target: RawAddress) -> Result { + let target = Self::lookup(target)?; + let aux = aux.ref_into(); + + ensure!(Self::nominating(aux).is_none(), "Cannot nominate if already nominating."); + ensure!(Self::intentions().iter().find(|&t| t == aux.ref_into()).is_none(), "Cannot nominate if already staked."); + + // update nominators_for + let mut t = Self::nominators_for(&target); + t.push(aux.clone()); + >::insert(&target, t); + + // update nominating + >::insert(aux, &target); + + // Update bondage + >::insert(aux.ref_into(), T::BlockNumber::max_value()); + + Ok(()) + } + + /// Will panic if called when source isn't currently nominating target. + /// Updates Nominating, NominatorsFor and NominationBalance. + fn unnominate(aux: &T::PublicAux, target_index: u32) -> Result { + let source = aux.ref_into(); + let target_index = target_index as usize; + + let target = >::get(source).ok_or("Account must be nominating")?; + + let mut t = Self::nominators_for(&target); + if t.get(target_index) != Some(source) { + return Err("Invalid target index") + } + + // Ok - all valid. + + // update nominators_for + t.swap_remove(target_index); + >::insert(&target, t); + + // update nominating + >::remove(source); + + // update bondage + >::insert(aux.ref_into(), Self::current_era() + Self::bonding_duration()); + Ok(()) + } + // PRIV DISPATCH /// Set the number of sessions in an era. @@ -496,13 +561,30 @@ impl Module { let reward = Self::session_reward() * T::Balance::sa(percent) / T::Balance::sa(65536usize); // apply good session reward for v in >::validators().iter() { - let _ = Self::reward(v, reward); // will never fail as validator accounts must be created, but even if it did, it's just a missed reward. + let noms = Self::current_nominators_for(v); + let total = noms.iter().map(Self::voting_balance).fold(Self::voting_balance(v), |acc, x| acc + x); + if !total.is_zero() { + let safe_mul_rational = |b| b * reward / total;// TODO: avoid overflow + for n in noms.iter() { + let _ = Self::reward(n, safe_mul_rational(Self::voting_balance(n))); + } + let _ = Self::reward(v, safe_mul_rational(Self::voting_balance(v))); + } } } else { // slash let early_era_slash = Self::early_era_slash(); for v in >::validators().iter() { - Self::slash(v, early_era_slash); + if let Some(rem) = Self::slash(v, early_era_slash) { + let noms = Self::current_nominators_for(v); + let total = noms.iter().map(Self::voting_balance).fold(Zero::zero(), |acc, x| acc + x); + for n in noms.iter() { + //let r = Self::voting_balance(n) * reward / total; // correct formula, but might overflow with large slash * total. + let quant = T::Balance::sa(1usize << 31); + let s = (Self::voting_balance(n) * quant / total) * rem / quant; // avoid overflow by using quant as a denominator. + let _ = Self::slash(n, s); // best effort - not much that can be done on fail. + } + } } } if ((session_index - Self::last_era_length_change()) % Self::sessions_per_era()).is_zero() || !normal_rotation { @@ -510,6 +592,11 @@ impl Module { } } + /// Balance of a (potential) validator that includes all nominators. + fn nomination_balance(who: &T::AccountId) -> T::Balance { + Self::nominators_for(who).iter().map(Self::voting_balance).fold(Zero::zero(), |acc, x| acc + x) + } + /// The era has changed - enact new staking set. /// /// NOTE: This always happens immediately before a session change to ensure that new validators @@ -530,17 +617,30 @@ impl Module { // combination of validators, then use session::internal::set_validators(). // for now, this just orders would-be stakers by their balances and chooses the top-most // >::get() of them. + // TODO: this is not sound. this should be moved to an off-chain solution mechanism. let mut intentions = >::get() .into_iter() - .map(|v| (Self::voting_balance(&v), v)) + .map(|v| (Self::voting_balance(&v) + Self::nomination_balance(&v), v)) .collect::>(); intentions.sort_unstable_by(|&(ref b1, _), &(ref b2, _)| b2.cmp(&b1)); - >::set_validators( - &intentions.into_iter() + + >::put( + if intentions.len() > 0 { + let i = (>::get() as usize).min(intentions.len() - 1); + intentions[i].0.clone() + } else { Zero::zero() } + ); + let vals = &intentions.into_iter() .map(|(_, v)| v) .take(>::get() as usize) - .collect::>() - ); + .collect::>(); + for v in >::validators().iter() { + >::remove(v); + } + for v in vals.iter() { + >::insert(v, Self::nominators_for(v)); + } + >::set_validators(vals); } fn enum_set_size() -> T::AccountIndex { diff --git a/substrate/runtime/staking/src/mock.rs b/substrate/runtime/staking/src/mock.rs index 2a6e622e78f3d..3facc46bd590d 100644 --- a/substrate/runtime/staking/src/mock.rs +++ b/substrate/runtime/staking/src/mock.rs @@ -98,7 +98,7 @@ pub fn new_test_ext(ext_deposit: u64, session_length: u64, sessions_per_era: u64 contract_fee: 0, reclaim_rebate: 0, session_reward: reward, - early_era_slash: if monied { 10 } else { 0 }, + early_era_slash: if monied { 20 } else { 0 }, }.build_storage()); t.extend(timestamp::GenesisConfig::{ period: 5 diff --git a/substrate/runtime/staking/src/tests.rs b/substrate/runtime/staking/src/tests.rs index cf419404f652d..f19e84079bd63 100644 --- a/substrate/runtime/staking/src/tests.rs +++ b/substrate/runtime/staking/src/tests.rs @@ -79,11 +79,18 @@ fn slashing_should_work() { assert_eq!(Session::current_index(), 1); assert_eq!(Staking::voting_balance(&10), 11); - System::set_block_number(4); + System::set_block_number(6); + Timestamp::set_timestamp(30); // on time. + Session::check_rotate_session(); + assert_eq!(Staking::current_era(), 0); + assert_eq!(Session::current_index(), 2); + assert_eq!(Staking::voting_balance(&10), 21); + + System::set_block_number(7); Timestamp::set_timestamp(100); // way too late - early exit. Session::check_rotate_session(); assert_eq!(Staking::current_era(), 1); - assert_eq!(Session::current_index(), 2); + assert_eq!(Session::current_index(), 3); assert_eq!(Staking::voting_balance(&10), 1); }); } @@ -192,7 +199,7 @@ fn staking_should_work() { // Block 3: Unstake highest, introduce another staker. No change yet. System::set_block_number(3); assert_ok!(Staking::stake(&3)); - assert_ok!(Staking::unstake(&4)); + assert_ok!(Staking::unstake(&4, Staking::intentions().iter().position(|&x| x == 4).unwrap() as u32)); assert_eq!(Staking::current_era(), 1); Session::check_rotate_session(); @@ -214,7 +221,7 @@ fn staking_should_work() { // Block 7: Unstake three. No change yet. System::set_block_number(7); - assert_ok!(Staking::unstake(&3)); + assert_ok!(Staking::unstake(&3, Staking::intentions().iter().position(|&x| x == 3).unwrap() as u32)); Session::check_rotate_session(); assert_eq!(Session::validators(), vec![1, 3]); @@ -225,6 +232,110 @@ fn staking_should_work() { }); } +#[test] +fn nominating_and_rewards_should_work() { + with_externalities(&mut new_test_ext(0, 1, 1, 0, true, 10), || { + assert_eq!(Staking::era_length(), 1); + assert_eq!(Staking::validator_count(), 2); + assert_eq!(Staking::bonding_duration(), 3); + assert_eq!(Session::validators(), vec![10, 20]); + + System::set_block_number(1); + assert_ok!(Staking::stake(&1)); + assert_ok!(Staking::stake(&2)); + assert_ok!(Staking::stake(&3)); + assert_ok!(Staking::nominate(&4, 1.into())); + Session::check_rotate_session(); + assert_eq!(Staking::current_era(), 1); + assert_eq!(Session::validators(), vec![1, 3]); // 4 + 1, 3 + assert_eq!(Staking::voting_balance(&1), 10); + assert_eq!(Staking::voting_balance(&2), 20); + assert_eq!(Staking::voting_balance(&3), 30); + assert_eq!(Staking::voting_balance(&4), 40); + + System::set_block_number(2); + assert_ok!(Staking::unnominate(&4, 0)); + Session::check_rotate_session(); + assert_eq!(Staking::current_era(), 2); + assert_eq!(Session::validators(), vec![3, 2]); + assert_eq!(Staking::voting_balance(&1), 12); + assert_eq!(Staking::voting_balance(&2), 20); + assert_eq!(Staking::voting_balance(&3), 40); + assert_eq!(Staking::voting_balance(&4), 48); + + System::set_block_number(3); + assert_ok!(Staking::stake(&4)); + assert_ok!(Staking::unstake(&3, Staking::intentions().iter().position(|&x| x == 3).unwrap() as u32)); + assert_ok!(Staking::nominate(&3, 1.into())); + Session::check_rotate_session(); + assert_eq!(Session::validators(), vec![1, 4]); + assert_eq!(Staking::voting_balance(&1), 12); + assert_eq!(Staking::voting_balance(&2), 30); + assert_eq!(Staking::voting_balance(&3), 50); + assert_eq!(Staking::voting_balance(&4), 48); + + System::set_block_number(4); + Session::check_rotate_session(); + assert_eq!(Staking::voting_balance(&1), 13); + assert_eq!(Staking::voting_balance(&2), 30); + assert_eq!(Staking::voting_balance(&3), 58); + assert_eq!(Staking::voting_balance(&4), 58); + }); +} + +#[test] +fn nominating_slashes_should_work() { + with_externalities(&mut new_test_ext(0, 2, 2, 0, true, 10), || { + assert_eq!(Staking::era_length(), 4); + assert_eq!(Staking::validator_count(), 2); + assert_eq!(Staking::bonding_duration(), 3); + assert_eq!(Session::validators(), vec![10, 20]); + + System::set_block_number(2); + Session::check_rotate_session(); + + Timestamp::set_timestamp(15); + System::set_block_number(4); + assert_ok!(Staking::stake(&1)); + assert_ok!(Staking::stake(&3)); + assert_ok!(Staking::nominate(&2, 3.into())); + assert_ok!(Staking::nominate(&4, 1.into())); + Session::check_rotate_session(); + + assert_eq!(Staking::current_era(), 1); + assert_eq!(Session::validators(), vec![1, 3]); // 1 + 4, 3 + 2 + assert_eq!(Staking::voting_balance(&1), 10); + assert_eq!(Staking::voting_balance(&2), 20); + assert_eq!(Staking::voting_balance(&3), 30); + assert_eq!(Staking::voting_balance(&4), 40); + + System::set_block_number(5); + Timestamp::set_timestamp(100); // late + assert_eq!(Session::blocks_remaining(), 1); + assert!(Session::broken_validation()); + Session::check_rotate_session(); + + assert_eq!(Staking::current_era(), 2); + assert_eq!(Staking::voting_balance(&1), 0); + assert_eq!(Staking::voting_balance(&2), 20); + assert_eq!(Staking::voting_balance(&3), 10); + assert_eq!(Staking::voting_balance(&4), 30); + }); +} + +#[test] +fn double_staking_should_fail() { + with_externalities(&mut new_test_ext(0, 1, 2, 0, true, 0), || { + System::set_block_number(1); + assert_ok!(Staking::stake(&1)); + assert_noop!(Staking::stake(&1), "Cannot stake if already staked."); + assert_noop!(Staking::nominate(&1, 1.into()), "Cannot nominate if already staked."); + assert_ok!(Staking::nominate(&2, 1.into())); + assert_noop!(Staking::stake(&2), "Cannot stake if already nominating."); + assert_noop!(Staking::nominate(&2, 1.into()), "Cannot nominate if already nominating."); + }); +} + #[test] fn staking_eras_work() { with_externalities(&mut new_test_ext(0, 1, 2, 0, true, 0), || {