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

Delegated staking #262

Merged
merged 1 commit into from
Jun 28, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 115 additions & 15 deletions substrate/runtime/staking/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,9 @@ decl_module! {
pub enum Call where aux: T::PublicAux {
fn transfer(aux, dest: RawAddress<T::AccountId, T::AccountIndex>, 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<T::AccountId, T::AccountIndex>) -> Result = 3;
fn unnominate(aux, target_index: u32) -> Result = 4;
}

#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
Expand All @@ -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;
Expand All @@ -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.
Expand All @@ -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<T::AccountId>;
pub Intentions get(intentions): b"sta:wil:" => default Vec<T::AccountId>;
// 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<T::AccountId> ];
// 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<T::AccountId> ];
// 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;
Expand Down Expand Up @@ -305,27 +315,82 @@ impl<T: Trait> Module<T> {
///
/// 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 = <Intentions<T>>::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());
<Intentions<T>>::put(intentions);
<Bondage<T>>::insert(aux.ref_into(), T::BlockNumber::max_value());
<Bondage<T>>::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 = <Intentions<T>>::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);
<Intentions<T>>::put(intentions);
<Bondage<T>>::insert(aux.ref_into(), Self::current_era() + Self::bonding_duration());
Ok(())
}

fn nominate(aux: &T::PublicAux, target: RawAddress<T::AccountId, T::AccountIndex>) -> 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());
<NominatorsFor<T>>::insert(&target, t);

// update nominating
<Nominating<T>>::insert(aux, &target);

// Update bondage
<Bondage<T>>::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 = <Nominating<T>>::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);
<NominatorsFor<T>>::insert(&target, t);

// update nominating
<Nominating<T>>::remove(source);

// update bondage
<Bondage<T>>::insert(aux.ref_into(), Self::current_era() + Self::bonding_duration());
Ok(())
}

// PRIV DISPATCH

/// Set the number of sessions in an era.
Expand Down Expand Up @@ -496,20 +561,42 @@ impl<T: Trait> Module<T> {
let reward = Self::session_reward() * T::Balance::sa(percent) / T::Balance::sa(65536usize);
// apply good session reward
for v in <session::Module<T>>::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 <session::Module<T>>::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 {
Self::new_era();
}
}

/// 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
Expand All @@ -530,17 +617,30 @@ impl<T: Trait> Module<T> {
// 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
// <ValidatorCount<T>>::get() of them.
// TODO: this is not sound. this should be moved to an off-chain solution mechanism.
let mut intentions = <Intentions<T>>::get()
.into_iter()
.map(|v| (Self::voting_balance(&v), v))
.map(|v| (Self::voting_balance(&v) + Self::nomination_balance(&v), v))
.collect::<Vec<_>>();
intentions.sort_unstable_by(|&(ref b1, _), &(ref b2, _)| b2.cmp(&b1));
<session::Module<T>>::set_validators(
&intentions.into_iter()

<StakeThreshold<T>>::put(
if intentions.len() > 0 {
let i = (<ValidatorCount<T>>::get() as usize).min(intentions.len() - 1);
intentions[i].0.clone()
} else { Zero::zero() }
);
let vals = &intentions.into_iter()
.map(|(_, v)| v)
.take(<ValidatorCount<T>>::get() as usize)
.collect::<Vec<_>>()
);
.collect::<Vec<_>>();
for v in <session::Module<T>>::validators().iter() {
<CurrentNominatorsFor<T>>::remove(v);
}
for v in vals.iter() {
<CurrentNominatorsFor<T>>::insert(v, Self::nominators_for(v));
}
<session::Module<T>>::set_validators(vals);
}

fn enum_set_size() -> T::AccountIndex {
Expand Down
2 changes: 1 addition & 1 deletion substrate/runtime/staking/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<Test>{
period: 5
Expand Down
119 changes: 115 additions & 4 deletions substrate/runtime/staking/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
}
Expand Down Expand Up @@ -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();

Expand All @@ -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]);

Expand All @@ -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), || {
Expand Down