diff --git a/Cargo.lock b/Cargo.lock index e84a3ad..28fb850 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3690,6 +3690,7 @@ dependencies = [ "pallet-transaction-payment-rpc-runtime-api", "pallet-utxo", "pallet-utxo-rpc-runtime-api", + "pallet-utxo-staking", "parity-scale-codec", "sp-api", "sp-block-builder", @@ -4186,6 +4187,7 @@ dependencies = [ "log", "pallet-authorship", "pallet-timestamp", + "pallet-utxo-staking", "parity-scale-codec", "pp-api", "proptest", @@ -4229,6 +4231,24 @@ dependencies = [ "sp-runtime", ] +[[package]] +name = "pallet-utxo-staking" +version = "0.1.0" +dependencies = [ + "frame-election-provider-support", + "frame-support", + "frame-system", + "log", + "pallet-authorship", + "pallet-session", + "parity-scale-codec", + "scale-info", + "serde", + "sp-runtime", + "sp-staking", + "sp-std", +] + [[package]] name = "parity-db" version = "0.3.1" diff --git a/node/src/chain_spec.rs b/node/src/chain_spec.rs index 1711f10..589bf1b 100644 --- a/node/src/chain_spec.rs +++ b/node/src/chain_spec.rs @@ -1,6 +1,6 @@ use node_template_runtime::{ pallet_utxo, AccountId, BalancesConfig, GenesisConfig, PpConfig, SessionConfig, Signature, - StakerStatus, StakingConfig, SudoConfig, SystemConfig, UtxoConfig, MINIMUM_STAKE, + StakerStatus, SudoConfig, SystemConfig, UtxoConfig, UtxoStakingConfig, MINIMUM_STAKE, NUM_OF_VALIDATOR_SLOTS, WASM_BINARY, }; use sc_network::config::MultiaddrWithPeerId; @@ -279,20 +279,20 @@ fn testnet_genesis( genesis_utxos, // The # of validators set should also be the same here. // This should be the same as what's set as the initial authorities - locked_utxos, - // initial_reward_amount: 100 * MLT_UNIT + // locked_utxos + ..Default::default() }, pp: PpConfig { _marker: Default::default(), }, session: SessionConfig { keys: session_keys }, - staking: StakingConfig { + utxo_staking: UtxoStakingConfig { validator_count: NUM_OF_VALIDATOR_SLOTS, // The # of validators set should be the same number of locked_utxos specified in UtxoConfig. minimum_validator_count: 1, invulnerables: initial_authorities.iter().map(|x| x.controller_account_id()).collect(), - slash_reward_fraction: sp_runtime::Perbill::from_percent(0), // nothing, since we're not using this at all. stakers, + min_validator_bond: MINIMUM_STAKE, ..Default::default() }, } diff --git a/pallets/staking/Cargo.toml b/pallets/staking/Cargo.toml new file mode 100644 index 0000000..d0b0e90 --- /dev/null +++ b/pallets/staking/Cargo.toml @@ -0,0 +1,88 @@ +[package] +name = "pallet-utxo-staking" +version = "0.1.0" +authors = ["BCYap <2826165+b-yap@users.noreply.github.com>"] +edition = "2018" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +serde = { version = "1.0.126", optional = true } +log = { version = "0.4.14", default-features = false } +scale-info = { version = "1.0", default-features = false, features = ["derive"] } + +[dependencies.codec] +default-features = false +features = ['derive'] +package = 'parity-scale-codec' +version = '2.0.0' + +[dependencies.sp-std] +default-features = false +git = 'https://github.com/paritytech/substrate.git' +version = '4.0.0-dev' +branch = "master" + +[dependencies.sp-runtime] +default-features = false +git = 'https://github.com/paritytech/substrate.git' +version = '4.0.0-dev' +branch = "master" + + +[dependencies.sp-staking] +default-features = false +git = 'https://github.com/paritytech/substrate.git' +version = '4.0.0-dev' +branch = "master" + +[dependencies.frame-election-provider-support] +default-features = false +git = 'https://github.com/paritytech/substrate.git' +version = '4.0.0-dev' +branch = "master" + +[dependencies.frame-support] +default-features = false +git = 'https://github.com/paritytech/substrate.git' +version = '4.0.0-dev' +branch = "master" + +[dependencies.frame-system] +default-features = false +git = 'https://github.com/paritytech/substrate.git' +version = '4.0.0-dev' +branch = "master" + +[dependencies.pallet-session] +default-features = false +features = ["historical"] +git = 'https://github.com/paritytech/substrate.git' +version = '4.0.0-dev' +branch = "master" + +[dependencies.pallet-authorship] +default-features = false +git = 'https://github.com/paritytech/substrate.git' +version = '4.0.0-dev' +branch = "master" + + + +[features] +default = ["std"] +std = [ + "serde", + "codec/std", + "scale-info/std", + "sp-std/std", + "frame-support/std", + "frame-election-provider-support/std", + "sp-runtime/std", + "sp-staking/std", + "pallet-session/std", + "frame-system/std", + "pallet-authorship/std", + "log/std", +] \ No newline at end of file diff --git a/pallets/staking/src/lib.rs b/pallets/staking/src/lib.rs new file mode 100644 index 0000000..8baf326 --- /dev/null +++ b/pallets/staking/src/lib.rs @@ -0,0 +1,195 @@ +#![cfg_attr(not(feature = "std"), no_std)] +#![recursion_limit = "256"] + +mod locking; +mod pallet; +pub mod weights; + +use codec::{Decode, Encode}; +use frame_support::dispatch::DispatchResult; +use frame_support::sp_runtime::traits::Convert; +pub use locking::*; +pub use pallet::{pallet::*, *}; +use scale_info::TypeInfo; +use sp_runtime::RuntimeDebug; +use sp_staking::SessionIndex; +use sp_std::vec::Vec; + +pub(crate) const LOG_TARGET: &'static str = "runtime::staking"; +pub type Value = u128; + +// syntactic sugar for logging. +#[macro_export] +macro_rules! log { + ($level:tt, $patter:expr $(, $values:expr)* $(,)?) => { + log::$level!( + target: crate::LOG_TARGET, + concat!("[{:?}] 💸 ", $patter), >::block_number() $(, $values)* + ) + }; +} + +/// Information regarding the active era (era in used in session). +#[derive(Encode, Decode, RuntimeDebug, TypeInfo)] +pub struct ActiveEraInfo { + /// Index of era. + pub index: EraIndex, + /// Moment of start expressed as millisecond from `$UNIX_EPOCH`. + /// + /// Start can be none if start hasn't been set for the era yet, + /// Start is set on the first on_finalize of the era to guarantee usage of `Time`. + start: Option, +} + +/// Indicates the initial status of the staker. +#[derive(RuntimeDebug, TypeInfo)] +#[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))] +pub enum StakerStatus { + /// Chilling. + Idle, + /// Declared desire in validating or already participating in it. + Validator, +} + +/// Counter for the number of eras that have passed. +pub type EraIndex = u32; + +/// The ledger of a (bonded) stash. +#[cfg_attr(feature = "runtime-benchmarks", derive(Default))] +#[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, TypeInfo)] +pub struct StakingLedger { + /// The stash account whose balance is actually locked and at stake. + pub stash: AccountId, + /// The total amount of the stash's balance that we are currently accounting for. + pub total: Value, + /// Era number at which point the total balance may eventually + /// be transfered out of the stash. + pub unlocking_era: Option, + /// List of eras for which the stakers behind a validator have claimed rewards. Only updated + /// for validators. + pub claimed_rewards: Vec, +} + +impl StakingLedger { + /// Re-bond funds that were scheduled for unlocking. + fn rebond(mut self) -> Self { + self.unlocking_era = None; + self + } +} + +/// The amount of exposure (to slashing) than an individual nominator has. +#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Encode, Decode, RuntimeDebug, TypeInfo)] +pub struct IndividualExposure { + /// The stash account of the nominator in question. + pub who: AccountId, + /// Amount of funds exposed. + pub value: Value, +} + +/// A snapshot of the stake backing a single validator in the system. +#[derive( + PartialEq, Eq, PartialOrd, Ord, Clone, Encode, Decode, Default, RuntimeDebug, TypeInfo, +)] +pub struct Exposure { + /// The total balance backing this validator. + pub total: Value, + /// The validator's own stash that is exposed. + pub own: Value, + /// The portions of nominators stashes that are exposed. + pub others: Vec>, +} + +/// Means for interacting with a specialized version of the `session` trait. +/// +/// This is needed because `Staking` sets the `ValidatorIdOf` of the `pallet_session::Config` +pub trait SessionInterface: frame_system::Config { + /// Disable a given validator by stash ID. + /// + /// Returns `true` if new era should be forced at the end of this session. + /// This allows preventing a situation where there is too many validators + /// disabled and block production stalls. + fn disable_validator(validator: &AccountId) -> Result; + /// Get the validators from session. + fn validators() -> Vec; + /// Prune historical session tries up to but not including the given index. + fn prune_historical_up_to(up_to: SessionIndex); +} + +impl SessionInterface<::AccountId> for T +where + T: pallet_session::Config::AccountId>, + T: pallet_session::historical::Config< + FullIdentification = Exposure<::AccountId>, + FullIdentificationOf = ExposureOf, + >, + T::SessionHandler: pallet_session::SessionHandler<::AccountId>, + T::SessionManager: pallet_session::SessionManager<::AccountId>, + T::ValidatorIdOf: Convert< + ::AccountId, + Option<::AccountId>, + >, +{ + fn disable_validator(validator: &::AccountId) -> Result { + >::disable(validator) + } + + fn validators() -> Vec<::AccountId> { + >::validators() + } + + fn prune_historical_up_to(up_to: SessionIndex) { + >::prune_up_to(up_to); + } +} + +pub trait SettingSessionKey { + fn can_decode_session_keys(session_key: &Vec) -> bool; + fn set_session_keys(controller: AccountId, session_keys: &Vec) -> DispatchResult; +} + +/// Mode of era-forcing. +#[derive(Copy, Clone, PartialEq, Eq, Encode, Decode, RuntimeDebug, TypeInfo)] +#[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))] +pub enum Forcing { + /// Not forcing anything - just let whatever happen. + NotForcing, + /// Force a new era, then reset to `NotForcing` as soon as it is done. + /// Note that this will force to trigger an election until a new era is triggered, if the + /// election failed, the next session end will trigger a new election again, until success. + ForceNew, + /// Avoid a new era indefinitely. + ForceNone, + /// Force a new era at the end of all sessions indefinitely. + ForceAlways, +} + +impl Default for Forcing { + fn default() -> Self { + Forcing::NotForcing + } +} + +/// A `Convert` implementation that finds the stash of the given controller account, +/// if any. +pub struct StashOf(sp_std::marker::PhantomData); + +impl Convert> for StashOf { + fn convert(controller: T::AccountId) -> Option { + >::ledger(&controller).map(|l| l.stash) + } +} + +/// A typed conversion from stash account ID to the active exposure of nominators +/// on that account. +/// +/// Active exposure is the exposure of the validator set currently validating, i.e. in +/// `active_era`. It can differ from the latest planned exposure in `current_era`. +pub struct ExposureOf(sp_std::marker::PhantomData); + +impl Convert>> for ExposureOf { + fn convert(validator: T::AccountId) -> Option> { + >::active_era() + .map(|active_era| >::eras_stakers(active_era.index, &validator)) + } +} diff --git a/pallets/staking/src/locking.rs b/pallets/staking/src/locking.rs new file mode 100644 index 0000000..5052b2a --- /dev/null +++ b/pallets/staking/src/locking.rs @@ -0,0 +1,142 @@ +use crate::{ + Bonded, Config, CounterForValidators, Error, Ledger, MaxValidatorsCount, Pallet, + SettingSessionKey, Value, +}; +use frame_support::dispatch::{DispatchResult, DispatchResultWithPostInfo}; +use frame_support::ensure; +use frame_support::sp_runtime::traits::StaticLookup; +use frame_system::ensure_signed; +use frame_system::pallet_prelude::OriginFor; +use sp_runtime::DispatchError; +use sp_std::vec::Vec; + +/// A helper trait to expose the balance of the account +pub trait Balance { + fn staking_fee() -> Value; + + fn minimum_stake_balance() -> Value; + + fn can_spend(account: &AccountId, value: Value) -> bool; + + fn lock_for_staking( + stash: AccountId, + controller: AccountId, + session_keys: Vec, + value: Value, + ) -> DispatchResultWithPostInfo; +} + +impl Pallet { + /// Checks if origin account is not yet a stash account + /// Checks if controller account has not yet been paired to a stash account + /// Checks if the minimum stake balance is reached + /// Checks if the session key can be decoded + /// returns a tuple of (Stash,Controller) + pub fn validate_lock_for_staking( + origin: OriginFor, + controller: ::Source, + value: u128, + ) -> Result<(T::AccountId, T::AccountId), DispatchError> { + let stash = ensure_signed(origin)?; + let controller = T::Lookup::lookup(controller)?; + + ensure!( + !>::contains_key(&stash), + Error::::AlreadyBonded + ); + + ensure!( + T::Balance::can_spend(&stash, value + T::Balance::staking_fee()), + Error::::InsufficientBalance + ); + + ensure!( + value >= T::Balance::minimum_stake_balance(), + Error::::InsufficientBond + ); + + ensure!( + !>::contains_key(&controller), + Error::::AlreadyPaired + ); + + // check only if we've set a limit to the maximum number of validators + if let Some(max_validators) = >::get() { + // If this error is reached, we need to adjust the `MinValidatorBond` and start + // calling `chill_other`. Until then, we explicitly block new validators to protect + // the runtime. + ensure!( + >::get() < max_validators, + Error::::TooManyValidators + ); + } + + Ok((stash, controller)) + } + + /// Take the origin account as a stash and lock up `value` of its balance. `controller` will + /// be the account that controls it. + /// + /// `value` must be more than the `minimum_balance`. + /// emits `Bonded` + pub(crate) fn bond( + stash: T::AccountId, + controller: T::AccountId, + value: Value, + ) -> DispatchResult { + frame_system::Pallet::::inc_consumers(&stash).map_err(|_| Error::::BadState)?; + // You're auto-bonded forever, here. + >::insert(&stash, &controller); + Self::add_ledger(stash, controller, value); + + Ok(()) + } + + pub(crate) fn apply_for_validator_role( + stash: T::AccountId, + controller: T::AccountId, + session_keys: Vec, + value: Value, + ) -> DispatchResultWithPostInfo { + Self::do_add_validator(&stash, &controller); + T::Balance::lock_for_staking(stash, controller, session_keys, value) + } + + /// Calls the bond function, with the origin account as a stash. + /// The session key should be from the rpc call `author_rotateKeys` or similar. + /// + /// The dispatch origin for this call must be _Signed_ by the stash account. + /// + pub fn lock_for_staking( + origin: OriginFor, + controller: ::Source, + session_keys: Vec, + value: Value, + ) -> DispatchResultWithPostInfo { + let (stash, controller) = Self::validate_lock_for_staking(origin, controller, value)?; + + Self::bond(stash.clone(), controller.clone(), value)?; + + // set session key here + ensure!( + T::SettingSessionKey::can_decode_session_keys(&session_keys), + Error::::CannotDecodeSessionKey + ); + T::SettingSessionKey::set_session_keys(controller.clone(), &session_keys)?; + + Self::apply_for_validator_role(stash, controller, session_keys, value) + } + + pub fn lock_extra_for_staking( + origin: OriginFor, + max_additional: Value, + ) -> DispatchResultWithPostInfo { + let stash = ensure_signed(origin)?; + let controller = Self::bonded(&stash).ok_or(Error::::NotStash)?; + let mut ledger = Self::ledger(&controller).ok_or(Error::::NotController)?; + + Self::update_ledger(stash, controller, max_additional, &mut ledger); + + Ok(().into()) + } +} diff --git a/pallets/staking/src/pallet/impls.rs b/pallets/staking/src/pallet/impls.rs new file mode 100644 index 0000000..c890766 --- /dev/null +++ b/pallets/staking/src/pallet/impls.rs @@ -0,0 +1,576 @@ +use crate::{ + log, ActiveEra, ActiveEraInfo, BondedEras, Config, CounterForValidators, CurrentEra, + CurrentPlannedSession, EraIndex, ErasStakers, ErasStartSessionIndex, ErasTotalStake, Event, + Exposure, ForceEra, Forcing, IndividualExposure, Ledger, Pallet, SessionInterface, + StakingLedger, Validators, Value, +}; + +use crate::weights::WeightInfo; +use frame_election_provider_support::{ + data_provider, ElectionDataProvider, ElectionProvider, Supports, VoteWeight, +}; +use frame_support::dispatch::{Vec, Weight}; +use frame_support::pallet_prelude::DispatchClass; +use frame_support::sp_runtime::traits::{Bounded, Zero}; +use frame_support::traits::{CurrencyToVote, EstimateNextNewSession}; +use frame_support::{pallet_prelude::*, traits::Get}; +use frame_system::pallet_prelude::BlockNumberFor; +use sp_runtime::traits::Saturating; +use sp_staking::SessionIndex; +use sp_std::vec; + +impl Pallet { + /// The total balance that can be slashed from a stash account as of right now. + pub fn slashable_balance_of(stash: &T::AccountId) -> Value { + // Weight note: consider making the stake accessible through stash. + Self::bonded(stash).and_then(Self::ledger).map(|l| l.total).unwrap_or_default() + } + + /// Internal impl of [`Self::slashable_balance_of`] that returns [`VoteWeight`]. + pub fn slashable_balance_of_vote_weight(stash: &T::AccountId, issuance: Value) -> VoteWeight { + T::CurrencyToVote::to_vote(Self::slashable_balance_of(stash), issuance) + } + + /// Returns a closure around `slashable_balance_of_vote_weight` that can be passed around. + /// + /// This prevents call sites from repeatedly requesting `total_issuance` from backend. But it is + /// important to be only used while the total issuance is not changing. + pub fn weight_of(who: &T::AccountId) -> VoteWeight { + // TODO: this should use the total issuance + let overall_stake = Self::overall_stake_value(); + + Self::slashable_balance_of_vote_weight(who, overall_stake) + } + + // TODO: this should be using the total issuance + pub fn overall_stake_value() -> Value { + let mut total: Value = 0; + Ledger::::iter_values().for_each(|ledger| { + if ledger.unlocking_era.is_none() { + total = total.saturating_add(ledger.total); + } + }); + + total + } + + /// add the ledger for a controller. + pub(crate) fn add_ledger(stash: T::AccountId, controller: T::AccountId, value: u128) { + let current_era = CurrentEra::::get().unwrap_or(0); + let history_depth = Self::history_depth(); + let last_reward_era = current_era.saturating_sub(history_depth); + + let ledger = StakingLedger { + stash: stash.clone(), + total: value, + unlocking_era: None, + claimed_rewards: (last_reward_era..current_era).collect(), + }; + >::insert(controller, ledger); + >::deposit_event(Event::::Bonded(stash, value)); + } + + /// update the ledger for a controller + pub(crate) fn update_ledger( + stash: T::AccountId, + controller: T::AccountId, + value: u128, + ledger: &mut StakingLedger, + ) { + ledger.total += value; + >::insert(controller, ledger); + >::deposit_event(Event::::Bonded(stash, value)); + } + + /// Get all of the voters that are eligible for the npos election. + /// + /// `maybe_max_len` can imposes a cap on the number of voters returned; First all the validator + /// are included in no particular order, then remainder is taken from the nominators, as + /// returned by [`Config::SortedListProvider`]. + /// + /// This will use nominators, and all the validators will inject a self vote. + /// + /// This function is self-weighing as [`DispatchClass::Mandatory`]. + /// + /// ### Slashing + /// + /// All nominations that have been submitted before the last non-zero slash of the validator are + /// auto-chilled, but still count towards the limit imposed by `maybe_max_len`. + pub fn get_npos_voters( + maybe_max_len: Option, + ) -> Vec<(T::AccountId, VoteWeight, Vec)> { + let max_allowed_len = { + let validator_count = CounterForValidators::::get() as usize; + maybe_max_len.unwrap_or(validator_count).min(validator_count) + }; + + let mut all_voters = Vec::<_>::with_capacity(max_allowed_len); + + // grab all validators in no particular order, capped by the maximum allowed length. + let mut validators_taken = 0u32; + for (validator, _) in >::iter().take(max_allowed_len) { + // Append self vote. + let self_vote = ( + validator.clone(), + Self::weight_of(&validator), + vec![validator.clone()], + ); + all_voters.push(self_vote); + validators_taken.saturating_inc(); + } + + // all_voters should have not re-allocated. + debug_assert!(all_voters.capacity() == max_allowed_len); + + Self::register_weight(T::WeightInfo::get_npos_voters(validators_taken, 0 as u32)); + + log!( + info, + "generated {} npos voters, {} from validators", + all_voters.len(), + validators_taken, + ); + all_voters + } + + /// Get the targets for an upcoming npos election. + /// + /// This function is self-weighing as [`DispatchClass::Mandatory`]. + pub fn get_npos_targets() -> Vec { + let mut validator_count = 0u32; + let targets = Validators::::iter() + .map(|(v, _)| { + validator_count.saturating_inc(); + v + }) + .collect::>(); + + Self::register_weight(T::WeightInfo::get_npos_targets(validator_count)); + + targets + } + + /// This function will add a validator to the `Validators` storage map, and keep track of the + /// `CounterForValidators`. + /// + /// NOTE: you must ALWAYS use this function to add a validator to the system. Any access to + /// `Validators`, its counter, or `VoterList` outside of this function is almost certainly + /// wrong. + pub fn do_add_validator(who: &T::AccountId, controller: &T::AccountId) { + CounterForValidators::::mutate(|x| x.saturating_inc()); + Validators::::insert(who, controller); + } + + /// Plan a new session potentially trigger a new era. + fn new_session(session_index: SessionIndex, is_genesis: bool) -> Option> { + if let Some(current_era) = Self::current_era() { + // Initial era has been set. + let current_era_start_session_index = Self::eras_start_session_index(current_era) + .unwrap_or_else(|| { + frame_support::print("Error: start_session_index must be set for current_era"); + 0 + }); + + let era_length = + session_index.checked_sub(current_era_start_session_index).unwrap_or(0); // Must never happen. + + match ForceEra::::get() { + // Will be set to `NotForcing` again if a new era has been triggered. + Forcing::ForceNew => (), + // Short circuit to `try_trigger_new_era`. + Forcing::ForceAlways => (), + // Only go to `try_trigger_new_era` if deadline reached. + Forcing::NotForcing if era_length >= T::SessionsPerEra::get() => (), + _ => { + // Either `Forcing::ForceNone`, + // or `Forcing::NotForcing if era_length >= T::SessionsPerEra::get()`. + return None; + } + } + + // New era. + let maybe_new_era_validators = Self::try_trigger_new_era(session_index, is_genesis); + if maybe_new_era_validators.is_some() + && matches!(ForceEra::::get(), Forcing::ForceNew) + { + ForceEra::::put(Forcing::NotForcing); + } + + maybe_new_era_validators + } else { + // Set initial era. + log!(debug, "Starting the first era."); + Self::try_trigger_new_era(session_index, is_genesis) + } + } + + /// Start a session potentially starting an era. + fn start_session(start_session: SessionIndex) { + let next_active_era = Self::active_era().map(|e| e.index + 1).unwrap_or(0); + // This is only `Some` when current era has already progressed to the next era, while the + // active era is one behind (i.e. in the *last session of the active era*, or *first session + // of the new current era*, depending on how you look at it). + if let Some(next_active_era_start_session_index) = + Self::eras_start_session_index(next_active_era) + { + if next_active_era_start_session_index == start_session { + Self::start_era(start_session); + } else if next_active_era_start_session_index < start_session { + // This arm should never happen, but better handle it than to stall the staking + // pallet. + frame_support::print("Warning: A session appears to have been skipped."); + Self::start_era(start_session); + } + } + } + + /// End a session potentially ending an era. + fn end_session(session_index: SessionIndex) { + if let Some(active_era) = Self::active_era() { + if let Some(next_active_era_start_session_index) = + Self::eras_start_session_index(active_era.index + 1) + { + if next_active_era_start_session_index == session_index + 1 { + // Self::end_era(active_era, session_index); + } + } + } + } + + /// + /// * Increment `active_era.index`, + /// * reset `active_era.start`, + /// * update `BondedEras` and apply slashes. + fn start_era(start_session: SessionIndex) { + let active_era = ActiveEra::::mutate(|active_era| { + let new_index = active_era.as_ref().map(|info| info.index + 1).unwrap_or(0); + *active_era = Some(ActiveEraInfo { + index: new_index, + // Set new active era start in next `on_finalize`. To guarantee usage of `Time` + start: None, + }); + new_index + }); + + let bonding_duration = T::BondingDuration::get(); + + BondedEras::::mutate(|bonded| { + bonded.push((active_era, start_session)); + + if active_era > bonding_duration { + if let Some(&(_, first_session)) = bonded.first() { + T::SessionInterface::prune_historical_up_to(first_session); + } + } + }); + } + + /// Plan a new era. + /// + /// * Bump the current era storage (which holds the latest planned era). + /// * Store start session index for the new planned era. + /// * Clean old era information. + /// * Store staking information for the new planned era + /// + /// Returns the new validator set. + pub fn trigger_new_era( + start_session_index: SessionIndex, + exposures: Vec<(T::AccountId, Exposure)>, + ) -> Vec { + // Increment or set current era. + let new_planned_era = CurrentEra::::mutate(|s| { + *s = Some(s.map(|s| s + 1).unwrap_or(0)); + s.unwrap() + }); + ErasStartSessionIndex::::insert(&new_planned_era, &start_session_index); + + // Clean old era information. + if let Some(old_era) = new_planned_era.checked_sub(Self::history_depth() + 1) { + Self::clear_era_information(old_era); + } + + // Set staking information for the new era. + Self::store_stakers_info(exposures, new_planned_era) + } + + /// Potentially plan a new era. + /// + /// Get election result from `T::ElectionProvider`. + /// In case election result has more than [`MinimumValidatorCount`] validator trigger a new era. + /// + /// In case a new era is planned, the new validator set is returned. + pub(crate) fn try_trigger_new_era( + start_session_index: SessionIndex, + is_genesis: bool, + ) -> Option> { + let election_result = if is_genesis { + T::GenesisElectionProvider::elect().map_err(|e| { + log!(warn, "genesis election provider failed due to {:?}", e); + Self::deposit_event(Event::StakingElectionFailed); + }) + } else { + T::ElectionProvider::elect().map_err(|e| { + log!(warn, "election provider failed due to {:?}", e); + Self::deposit_event(Event::StakingElectionFailed); + }) + } + .ok()?; + + let exposures = Self::collect_exposures(election_result); + + if (exposures.len() as u32) < Self::minimum_validator_count().max(1) { + // Session will panic if we ever return an empty validator set, thus max(1) ^^. + match CurrentEra::::get() { + None => { + // The initial era is allowed to have no exposures. + // In this case the SessionManager is expected to choose a sensible validator + // set. + // TODO: this should be simplified #8911 + CurrentEra::::put(0); + ErasStartSessionIndex::::insert(&0, &start_session_index); + } + Some(current_era) if current_era > 0 => log!( + warn, + "chain does not have enough staking candidates to operate for era {:?} ({} \ + elected, minimum is {})", + CurrentEra::::get().unwrap_or(0), + exposures.len(), + Self::minimum_validator_count(), + ), + _ => (), + } + Self::deposit_event(Event::StakingElectionFailed); + return None; + } + + Self::deposit_event(Event::StakersElected); + Some(Self::trigger_new_era(start_session_index, exposures)) + } + + /// Process the output of the election. + /// + /// Store staking information for the new planned era + pub fn store_stakers_info( + exposures: Vec<(T::AccountId, Exposure)>, + new_planned_era: EraIndex, + ) -> Vec { + let elected_stashes = exposures.iter().cloned().map(|(x, _)| x).collect::>(); + + // Populate stakers, exposures, and the snapshot of validator prefs. + let mut total_stake: Value = 0; + exposures.into_iter().for_each(|(stash, stake)| { + total_stake = total_stake.saturating_add(stake.total); + >::insert(new_planned_era, &stash, &stake); + }); + + // Insert current era staking information + >::insert(&new_planned_era, total_stake); + + if new_planned_era > 0 { + log!( + info, + "new validator set of size {:?} has been processed for era {:?}", + elected_stashes.len(), + new_planned_era, + ); + } + + elected_stashes + } + + /// Consume a set of [`Supports`] from [`sp_npos_elections`] and collect them into a + /// [`(validator, weight)`]. + pub(crate) fn collect_exposures( + supports: Supports, + ) -> Vec<(T::AccountId, Exposure)> { + // TODO: In substrate, the total issuance is used to extract the weight of the stake. + // For now, the total staked will of an account will determine the vote. + let overall_stake = Self::overall_stake_value(); + let to_currency = |e: frame_election_provider_support::ExtendedBalance| { + T::CurrencyToVote::to_currency(e, overall_stake) + }; + + supports + .into_iter() + .map(|(validator, support)| { + // Build `struct exposure` from `support`. + let mut others = Vec::with_capacity(support.voters.len()); + let mut own: Value = 0; + let mut total: Value = 0; + + support.voters.into_iter().for_each(|(nominator, stake)| { + let stake = to_currency(stake); + + if nominator == validator { + log!(info, "voting for myself: {:?}", validator); + own = own.saturating_add(stake); + } else { + log!(info, "account {:?} votes for {:?}", nominator, validator); + others.push(IndividualExposure { + who: nominator, + value: stake, + }); + } + total = total.saturating_add(stake); + }); + + let exposure = Exposure { total, own, others }; + + (validator, exposure) + }) + .collect() + } + + /// Clear all era information for given era. + pub(crate) fn clear_era_information(era_index: EraIndex) { + >::remove_prefix(era_index, None); + >::remove(era_index); + >::remove(era_index); + } + + /// Register some amount of weight directly with the system pallet. + /// + /// This is always mandatory weight. + fn register_weight(weight: Weight) { + >::register_extra_weight_unchecked( + weight, + DispatchClass::Mandatory, + ); + } +} + +impl ElectionDataProvider> for Pallet { + const MAXIMUM_VOTES_PER_VOTER: u32 = 0; + + fn desired_targets() -> data_provider::Result { + Self::register_weight(T::DbWeight::get().reads(1)); + Ok(Self::validator_count()) + } + + fn voters( + maybe_max_len: Option, + ) -> data_provider::Result)>> { + debug_assert!(>::iter().count() as u32 == CounterForValidators::::get()); + + // This can never fail -- if `maybe_max_len` is `Some(_)` we handle it. + let voters = Self::get_npos_voters(maybe_max_len); + debug_assert!(maybe_max_len.map_or(true, |max| voters.len() <= max)); + + Ok(voters) + } + + fn targets(maybe_max_len: Option) -> data_provider::Result> { + let target_count = CounterForValidators::::get(); + + // We can't handle this case yet -- return an error. + if maybe_max_len.map_or(false, |max_len| target_count > max_len as u32) { + return Err("Target snapshot too big"); + } + + Ok(Self::get_npos_targets()) + } + + fn next_election_prediction(now: T::BlockNumber) -> T::BlockNumber { + let current_era = Self::current_era().unwrap_or(0); + let current_session = Self::current_planned_session(); + let current_era_start_session_index = + Self::eras_start_session_index(current_era).unwrap_or(0); + // Number of session in the current era or the maximum session per era if reached. + let era_progress = current_session + .saturating_sub(current_era_start_session_index) + .min(T::SessionsPerEra::get()); + + let until_this_session_end = T::NextNewSession::estimate_next_new_session(now) + .0 + .unwrap_or_default() + .saturating_sub(now); + + let session_length = T::NextNewSession::average_session_length(); + + let sessions_left: T::BlockNumber = match ForceEra::::get() { + Forcing::ForceNone => Bounded::max_value(), + Forcing::ForceNew | Forcing::ForceAlways => Zero::zero(), + Forcing::NotForcing if era_progress >= T::SessionsPerEra::get() => Zero::zero(), + Forcing::NotForcing => T::SessionsPerEra::get() + .saturating_sub(era_progress) + // One session is computed in this_session_end. + .saturating_sub(1) + .into(), + }; + + now.saturating_add( + until_this_session_end.saturating_add(sessions_left.saturating_mul(session_length)), + ) + } +} + +/// In this implementation `new_session(session)` must be called before `end_session(session-1)` +/// i.e. the new session must be planned before the ending of the previous session. +/// +/// Once the first new_session is planned, all session must start and then end in order, though +/// some session can lag in between the newest session planned and the latest session started. +impl pallet_session::SessionManager for Pallet { + fn new_session(new_index: SessionIndex) -> Option> { + log!(trace, "planning new session {}", new_index); + CurrentPlannedSession::::put(new_index); + Self::new_session(new_index, false) + } + fn new_session_genesis(new_index: SessionIndex) -> Option> { + log!(trace, "planning new session {} at genesis", new_index); + CurrentPlannedSession::::put(new_index); + Self::new_session(new_index, true) + } + fn end_session(end_index: SessionIndex) { + log!(trace, "ending session {}", end_index); + Self::end_session(end_index) + } + fn start_session(start_index: SessionIndex) { + log!(trace, "starting session {}", start_index); + Self::start_session(start_index) + } +} + +impl pallet_session::historical::SessionManager> + for Pallet +{ + fn new_session(new_index: SessionIndex) -> Option)>> { + >::new_session(new_index).map(|validators| { + let current_era = Self::current_era() + // Must be some as a new era has been created. + .unwrap_or(0); + + validators + .into_iter() + .map(|v| { + let exposure = Self::eras_stakers(current_era, &v); + (v, exposure) + }) + .collect() + }) + } + fn new_session_genesis( + new_index: SessionIndex, + ) -> Option)>> { + >::new_session_genesis(new_index).map( + |validators| { + let current_era = Self::current_era() + // Must be some as a new era has been created. + .unwrap_or(0); + + validators + .into_iter() + .map(|v| { + let exposure = Self::eras_stakers(current_era, &v); + (v, exposure) + }) + .collect() + }, + ) + } + fn start_session(start_index: SessionIndex) { + >::start_session(start_index) + } + fn end_session(end_index: SessionIndex) { + >::end_session(end_index) + } +} \ No newline at end of file diff --git a/pallets/staking/src/pallet/mod.rs b/pallets/staking/src/pallet/mod.rs new file mode 100644 index 0000000..325d6d3 --- /dev/null +++ b/pallets/staking/src/pallet/mod.rs @@ -0,0 +1,402 @@ +mod impls; + +pub const MAX_UNLOCKING_CHUNKS: usize = 32; + +pub use impls::*; + +#[frame_support::pallet] +pub mod pallet { + use crate::{ + locking::Balance, log, weights::WeightInfo, ActiveEraInfo, EraIndex, Exposure, Forcing, + SessionInterface, SettingSessionKey, StakingLedger, Value, + }; + use frame_support::traits::CurrencyToVote; + use frame_support::{ + pallet_prelude::*, + traits::{EstimateNextNewSession, Get, UnixTime}, + weights::Weight, + }; + use frame_system::pallet_prelude::*; + use sp_runtime::traits::{SaturatedConversion, StaticLookup}; + use sp_staking::SessionIndex; + use sp_std::{convert::From, prelude::*}; + + #[pallet::pallet] + #[pallet::generate_store(pub(crate) trait Store)] + pub struct Pallet(_); + + #[pallet::config] + pub trait Config: frame_system::Config { + type Balance: Balance; + + type SettingSessionKey: SettingSessionKey; + + /// Time used for computing era duration. + /// + /// It is guaranteed to start being called from the first `on_finalize`. Thus value at + /// genesis is not used. + type UnixTime: UnixTime; + + /// Convert a balance into a number used for election calculation. This must fit into a + /// `u64` but is allowed to be sensibly lossy. The `u64` is used to communicate with the + /// [`sp_npos_elections`] crate which accepts u64 numbers and does operations in 128. + /// Consequently, the backward convert is used convert the u128s from sp-elections back to a + /// [`Value`]. + type CurrencyToVote: CurrencyToVote; + + /// Something that provides the election functionality. + type ElectionProvider: frame_election_provider_support::ElectionProvider< + Self::AccountId, + Self::BlockNumber, + // we only accept an election provider that has staking as data provider. + DataProvider = Pallet, + >; + + /// Something that provides the election functionality at genesis. + type GenesisElectionProvider: frame_election_provider_support::ElectionProvider< + Self::AccountId, + Self::BlockNumber, + DataProvider = Pallet, + >; + + /// The overarching event type. + type Event: From> + IsType<::Event>; + + /// Number of sessions per era. + #[pallet::constant] + type SessionsPerEra: Get; + + /// Number of eras that staked funds must remain bonded for. + #[pallet::constant] + type BondingDuration: Get; + + /// Interface for interacting with a session pallet. + type SessionInterface: SessionInterface; + + /// Something that can estimate the next session change, accurately or as a best effort + /// guess. + type NextNewSession: EstimateNextNewSession; + + /// Weight information for extrinsics in this pallet. + type WeightInfo: WeightInfo; + } + + #[pallet::type_value] + pub(crate) fn HistoryDepthOnEmpty() -> u32 { + 84u32 + } + + /// Number of eras to keep in history. + /// + /// Information is kept for eras in `[current_era - history_depth; current_era]`. + /// + /// Must be more than the number of eras delayed by session otherwise. I.e. active era must + /// always be in history. I.e. `active_era > current_era - history_depth` must be + /// guaranteed. + #[pallet::storage] + #[pallet::getter(fn history_depth)] + pub(crate) type HistoryDepth = StorageValue<_, u32, ValueQuery, HistoryDepthOnEmpty>; + + /// The ideal number of staking participants. + #[pallet::storage] + #[pallet::getter(fn validator_count)] + pub type ValidatorCount = StorageValue<_, u32, ValueQuery>; + + /// Minimum number of staking participants before emergency conditions are imposed. + #[pallet::storage] + #[pallet::getter(fn minimum_validator_count)] + pub type MinimumValidatorCount = StorageValue<_, u32, ValueQuery>; + + /// Any validators that may never be slashed or forcibly kicked. It's a Vec since they're + /// easy to initialize and the performance hit is minimal (we expect no more than four + /// invulnerables) and restricted to testnets. + #[pallet::storage] + #[pallet::getter(fn invulnerables)] + pub type Invulnerables = StorageValue<_, Vec, ValueQuery>; + + /// Map from all locked "stash" accounts to the controller account. + #[pallet::storage] + #[pallet::getter(fn bonded)] + pub type Bonded = StorageMap<_, Twox64Concat, T::AccountId, T::AccountId>; + + #[pallet::storage] + pub type MinValidatorBond = StorageValue<_, Value, ValueQuery>; + + /// Map from all (unlocked) "controller" accounts to the info regarding the staking. + #[pallet::storage] + #[pallet::getter(fn ledger)] + pub type Ledger = + StorageMap<_, Blake2_128Concat, T::AccountId, StakingLedger>; + + /// The map from (wannabe) validator stash key to the preferences of that validator. + /// + /// When updating this storage item, you must also update the `CounterForValidators`. + #[pallet::storage] + #[pallet::getter(fn validators)] + pub type Validators = + StorageMap<_, Twox64Concat, T::AccountId, T::AccountId, ValueQuery>; + + /// A tracker to keep count of the number of items in the `Bonded` map. + #[pallet::storage] + pub type CounterForValidators = StorageValue<_, u32, ValueQuery>; + + /// The maximum validator count before we stop allowing new validators to join. + /// + /// When this value is not set, no limits are enforced. + #[pallet::storage] + pub type MaxValidatorsCount = StorageValue<_, u32, OptionQuery>; + + /// The current era index. + /// + /// This is the latest planned era, depending on how the Session pallet queues the validator + /// set, it might be active or not. + #[pallet::storage] + #[pallet::getter(fn current_era)] + pub type CurrentEra = StorageValue<_, EraIndex>; + + /// The active era information, it holds index and start. + /// + /// The active era is the era being currently rewarded. Validator set of this era must be + /// equal to [`SessionInterface::validators`]. + #[pallet::storage] + #[pallet::getter(fn active_era)] + pub type ActiveEra = StorageValue<_, ActiveEraInfo>; + + /// The session index at which the era start for the last `HISTORY_DEPTH` eras. + /// + /// Note: This tracks the starting session (i.e. session index when era start being active) + /// for the eras in `[CurrentEra - HISTORY_DEPTH, CurrentEra]`. + #[pallet::storage] + #[pallet::getter(fn eras_start_session_index)] + pub type ErasStartSessionIndex = StorageMap<_, Twox64Concat, EraIndex, SessionIndex>; + + /// Exposure of validator at era. + /// + /// This is keyed first by the era index to allow bulk deletion and then the stash account. + /// + /// Is it removed after `HISTORY_DEPTH` eras. + /// If stakers hasn't been set or has been removed then empty exposure is returned. + #[pallet::storage] + #[pallet::getter(fn eras_stakers)] + pub type ErasStakers = StorageDoubleMap< + _, + Twox64Concat, + EraIndex, + Twox64Concat, + T::AccountId, + Exposure, + ValueQuery, + >; + + /// The total amount staked for the last `HISTORY_DEPTH` eras. + /// If total hasn't been set or has been removed then 0 stake is returned. + #[pallet::storage] + #[pallet::getter(fn eras_total_stake)] + pub type ErasTotalStake = StorageMap<_, Twox64Concat, EraIndex, Value, ValueQuery>; + + /// Mode of era forcing. + #[pallet::storage] + #[pallet::getter(fn force_era)] + pub type ForceEra = StorageValue<_, Forcing, ValueQuery>; + + /// A mapping from still-bonded eras to the first session index of that era. + /// + /// Must contains information for eras for the range: + /// `[active_era - bounding_duration; active_era]` + #[pallet::storage] + pub(crate) type BondedEras = + StorageValue<_, Vec<(EraIndex, SessionIndex)>, ValueQuery>; + + /// The last planned session scheduled by the session pallet. + /// + /// This is basically in sync with the call to [`pallet_session::SessionManager::new_session`]. + #[pallet::storage] + #[pallet::getter(fn current_planned_session)] + pub type CurrentPlannedSession = StorageValue<_, SessionIndex, ValueQuery>; + + #[pallet::genesis_config] + pub struct GenesisConfig { + pub history_depth: u32, + pub validator_count: u32, + pub minimum_validator_count: u32, + pub invulnerables: Vec, + pub force_era: Forcing, + pub stakers: Vec<(T::AccountId, T::AccountId, Value, crate::StakerStatus)>, + pub min_validator_bond: Value, + } + + #[cfg(feature = "std")] + impl Default for GenesisConfig { + fn default() -> Self { + GenesisConfig { + history_depth: 84u32, + validator_count: Default::default(), + minimum_validator_count: Default::default(), + invulnerables: Default::default(), + force_era: Default::default(), + stakers: Default::default(), + min_validator_bond: Default::default(), + } + } + } + + #[pallet::genesis_build] + impl GenesisBuild for GenesisConfig { + fn build(&self) { + HistoryDepth::::put(self.history_depth); + ValidatorCount::::put(self.validator_count); + MinimumValidatorCount::::put(self.minimum_validator_count); + Invulnerables::::put(&self.invulnerables); + ForceEra::::put(self.force_era); + MinValidatorBond::::put(self.min_validator_bond); + + for &(ref stash, ref controller, balance, ref status) in &self.stakers { + log!( + trace, + "inserting genesis staker: {:?} => {:?} => {:?}", + stash, + balance, + status + ); + assert!( + T::Balance::can_spend(&stash, balance), + "Stash does not have enough balance to bond." + ); + + frame_support::assert_ok!(>::validate_lock_for_staking( + T::Origin::from(Some(stash.clone()).into()), + T::Lookup::unlookup(controller.clone()), + balance + )); + + frame_support::assert_ok!(>::bond( + stash.clone(), + controller.clone(), + balance + )); + + frame_support::assert_ok!(match status { + crate::StakerStatus::Validator => >::apply_for_validator_role( + stash.clone(), + controller.clone(), + vec![], + balance, + ), + _ => Ok(().into()), + }); + } + } + } + + #[pallet::event] + #[pallet::generate_deposit(pub(crate) fn deposit_event)] + pub enum Event { + /// The stash account has been rewarded by this amount. \[utxo\] + Rewarded(T::AccountId, Value), + /// A new set of stakers was elected. + StakersElected, + /// An account has bonded this amount. \[stash, amount\] + /// + /// NOTE: This event is only emitted when funds are bonded via a dispatchable. Notably, + /// it will not be emitted for staking rewards when they are added to stake. + Bonded(T::AccountId, Value), + /// An account has unbonded this amount. \[stash, amount\] + Unbonded(T::AccountId, Value), + /// An account has called `withdraw_unbonded` and removed unbonding chunks worth `Balance` + /// from the unlocking queue. \[stash, amount\] + Withdrawn(T::AccountId, Value), + /// The election failed. No new era is planned. + StakingElectionFailed, + /// An account has stopped participating as validator. + /// \[stash\] + Chilled(T::AccountId), + } + + #[pallet::error] + pub enum Error { + /// Not a controller account. + NotController, + /// Not a stash account. + NotStash, + /// Stash is already bonded. + AlreadyBonded, + /// Controller is already paired. + AlreadyPaired, + /// Targets cannot be empty. + EmptyTargets, + /// Duplicate index. + DuplicateIndex, + /// Can not bond with value less than minimum required. + InsufficientBond, + /// Not enough balance to perform the staking. + InsufficientBalance, + /// Can not schedule more unlock chunks. + NoMoreChunks, + /// Can not rebond without unlocking chunks. + NoUnlockChunk, + /// Attempting to target a stash that still has funds. + FundedTarget, + /// Invalid era to reward. + InvalidEraToReward, + /// Items are not sorted and unique. + NotSortedAndUnique, + /// Incorrect previous history depth input provided. + IncorrectHistoryDepth, + /// Internal state has become somehow corrupted and the operation cannot continue. + BadState, + /// There are too many validators in the system. Governance needs to adjust the staking + /// settings to keep things safe for the runtime. + TooManyValidators, + /// Failed to decode the provided session key. + /// Make sure to get it from the rpc call `author_rotateKeys` + CannotDecodeSessionKey, + } + + #[pallet::hooks] + impl Hooks> for Pallet { + fn on_initialize(_now: BlockNumberFor) -> Weight { + // just return the weight of the on_finalize. + T::DbWeight::get().reads(1) + } + + fn on_finalize(_n: BlockNumberFor) { + // Set the start of the first era. + if let Some(mut active_era) = Self::active_era() { + if active_era.start.is_none() { + let now_as_millis_u64 = T::UnixTime::now().as_millis().saturated_into::(); + active_era.start = Some(now_as_millis_u64); + // This write only ever happens once, we don't include it in the weight in + // general + ActiveEra::::put(active_era); + } + } + // `on_finalize` weight is tracked in `on_initialize` + } + + fn integrity_test() { + sp_std::if_std! { + // sp_io::TestExternalities::new_empty().execute_with(|| + // assert!( + // T::SlashDeferDuration::get() < T::BondingDuration::get() || T::BondingDuration::get() == 0, + // "As per documentation, slash defer duration ({}) should be less than bonding duration ({}).", + // T::SlashDeferDuration::get(), + // T::BondingDuration::get(), + // ) + // ); + } + } + } + + #[pallet::call] + impl Pallet { + #[pallet::weight(T::WeightInfo::lock())] + pub fn lock( + origin: OriginFor, + controller: ::Source, + session_keys: Vec, + value: Value, + ) -> DispatchResultWithPostInfo { + Self::lock_for_staking(origin, controller, session_keys, value) + } + } +} diff --git a/pallets/staking/src/weights.rs b/pallets/staking/src/weights.rs new file mode 100644 index 0000000..b21c7ce --- /dev/null +++ b/pallets/staking/src/weights.rs @@ -0,0 +1,49 @@ +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use sp_std::marker::PhantomData; + +pub trait WeightInfo { + fn lock() -> Weight; + fn get_npos_voters(v: u32, s: u32, ) -> Weight; + fn get_npos_targets(v: u32, ) -> Weight; +} + +pub struct SubstrateWeight(PhantomData); + +impl WeightInfo for SubstrateWeight { + fn lock() -> u64 { + (73_865_000 as Weight) + .saturating_add(T::DbWeight::get().reads(5 as Weight)) + .saturating_add(T::DbWeight::get().writes(4 as Weight)) + } + // Storage: Staking CounterForNominators (r:1 w:0) + // Storage: Staking CounterForValidators (r:1 w:0) + // Storage: Staking Validators (r:501 w:0) + // Storage: Staking Bonded (r:1500 w:0) + // Storage: Staking Ledger (r:1500 w:0) + // Storage: Staking SlashingSpans (r:21 w:0) + // Storage: BagsList ListBags (r:200 w:0) + // Storage: BagsList ListNodes (r:1000 w:0) + // Storage: Staking Nominators (r:1000 w:0) + fn get_npos_voters(v: u32, s: u32, ) -> Weight { + (0 as Weight) + // Standard Error: 91_000 + .saturating_add((26_605_000 as Weight).saturating_mul(v as Weight)) + // Standard Error: 3_122_000 + .saturating_add((16_672_000 as Weight).saturating_mul(s as Weight)) + .saturating_add(RocksDbWeight::get().reads(204 as Weight)) + .saturating_add(RocksDbWeight::get().reads((3 as Weight).saturating_mul(v as Weight))) + .saturating_add(RocksDbWeight::get().reads((1 as Weight).saturating_mul(s as Weight))) + } + // Storage: Staking Validators (r:501 w:0) + fn get_npos_targets(v: u32, ) -> Weight { + (0 as Weight) + // Standard Error: 34_000 + .saturating_add((10_558_000 as Weight).saturating_mul(v as Weight)) + .saturating_add(RocksDbWeight::get().reads(1 as Weight)) + .saturating_add(RocksDbWeight::get().reads((1 as Weight).saturating_mul(v as Weight))) + } +} \ No newline at end of file diff --git a/pallets/utxo/Cargo.toml b/pallets/utxo/Cargo.toml index 6b1d6b6..454dd12 100644 --- a/pallets/utxo/Cargo.toml +++ b/pallets/utxo/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "pallet-utxo" version = "0.1.0" -authors = ["BCarlaYap <2826165+BCarlaYap@users.noreply.github.com>"] +authors = ["BCYap <2826165+b-yap@users.noreply.github.com>"] edition = "2018" [features] @@ -14,6 +14,7 @@ std = [ 'frame-benchmarking/std', 'chainscript/std', 'pallet-timestamp/std', + 'pallet-utxo-staking/std', 'sp-core/std', 'sp-std/std', ] @@ -42,6 +43,11 @@ default-features = false path = '../../libs/chainscript' version = '0.1.0' +[dependencies.pallet-utxo-staking] +default-features = false +path = '../staking' +version = '0.1.0' + [dependencies.codec] default-features = false features = ["derive", "chain-error"] diff --git a/pallets/utxo/src/lib.rs b/pallets/utxo/src/lib.rs index 1b16303..a9ada22 100644 --- a/pallets/utxo/src/lib.rs +++ b/pallets/utxo/src/lib.rs @@ -79,6 +79,7 @@ pub mod pallet { H256, H512, }; pub const MLT_UNIT: Value = 1_000 * 100_000_000; + pub type LockIdentifier = [u8; 8]; #[pallet::error] pub enum Error { @@ -293,7 +294,6 @@ pub mod pallet { )); /// Calculate lock commitment for given destination. - /// /// The `lock` field of the input spending the UTXO has to match this hash. pub fn lock_commitment(&self) -> &H256 { match self { diff --git a/pallets/utxo/src/script.rs b/pallets/utxo/src/script.rs index 4b06e8a..106e3fe 100644 --- a/pallets/utxo/src/script.rs +++ b/pallets/utxo/src/script.rs @@ -166,8 +166,8 @@ pub(crate) mod test { use super::*; use chainscript::Context; use core::time::Duration; - use sp_core::sr25519; use proptest::prelude::*; + use sp_core::sr25519; // Generate block time in seconds pub fn gen_block_time_real() -> impl Strategy { diff --git a/pallets/utxo/src/staking.rs b/pallets/utxo/src/staking.rs index 06b8900..f632acf 100644 --- a/pallets/utxo/src/staking.rs +++ b/pallets/utxo/src/staking.rs @@ -16,8 +16,8 @@ // Author(s): C. Yap use crate::{ - convert_to_h256, tokens::Value, Config, Destination, Error, Event, LockedUtxos, Pallet, - RewardTotal, StakingCount, TransactionOutput, UtxoStore, + convert_to_h256, pick_utxo, tokens::Value, Config, Destination, Error, Event, LockedUtxos, + Pallet, RewardTotal, StakingCount, Transaction, TransactionInput, TransactionOutput, UtxoStore, }; use frame_support::{ dispatch::{DispatchResultWithPostInfo, Vec}, @@ -30,8 +30,120 @@ use sp_runtime::transaction_validity::{TransactionLongevity, ValidTransaction}; use sp_std::vec; use crate::staking::utils::remove_locked_utxos; +use sp_runtime::DispatchError; +use sp_std::marker::PhantomData; pub use validation::*; +pub struct UtxoBalance(PhantomData); + +impl pallet_utxo_staking::Balance for UtxoBalance { + fn staking_fee() -> Value { + T::StakeWithdrawalFee::get() + } + + fn minimum_stake_balance() -> Value { + T::MinimumStake::get() + } + + fn can_spend(account: &T::AccountId, value: Value) -> bool { + let (total, _, _) = pick_utxo::(account, value); + + total >= value + } + + fn lock_for_staking( + stash: T::AccountId, + controller: T::AccountId, + session_keys: Vec, + value: Value, + ) -> DispatchResultWithPostInfo { + let (total, hashes, utxos) = pick_utxo::(&stash, value); + ensure!(total >= value, "Caller doesn't have enough UTXOs"); + + let utxo_staking = + TransactionOutput::new_lock_for_staking(value, stash.clone(), controller, session_keys); + let utxo_change = + TransactionOutput::new_pubkey(total - value, convert_to_h256::(&stash)?); + + let mut inputs: Vec = Vec::new(); + for hash in hashes.iter() { + inputs.push(TransactionInput::new_empty(*hash)); + >::remove(hash); + log::info!("removed from UtxoStore: hash: {:?}", hash); + } + + let tx = Transaction { + inputs, + outputs: vec![utxo_staking.clone(), utxo_change.clone()], + time_lock: Default::default(), + }; + + // --------- TODO: from this point, this should be using the spend function including the signing, + // --------- rather than inserting directly to the storages. + let hash = tx.outpoint(0); + >::insert(hash, utxo_staking); + log::info!("inserted to LockedUtxos: hash: {:?}", hash); + + let hash = tx.outpoint(1); + >::insert(hash, utxo_change); + log::info!("inserted to UtxoStore: hash: {:?}", hash); + + >::deposit_event(Event::::TransactionSuccess(tx)); + + Ok(().into()) + } +} + +pub struct NoStaking(PhantomData); +impl StakingHelper for NoStaking { + fn get_controller_account(stash_account: &T::AccountId) -> Result { + todo!() + } + + fn is_controller_account_exist(controller_account: &T::AccountId) -> bool { + todo!() + } + + fn can_decode_session_key(session_key: &Vec) -> bool { + todo!() + } + + fn are_funds_locked(controller_account: &T::AccountId) -> bool { + todo!() + } + + fn check_accounts_matched( + controller_account: &T::AccountId, + stash_account: &T::AccountId, + ) -> bool { + todo!() + } + + fn lock_for_staking( + stash_account: &T::AccountId, + controller_account: &T::AccountId, + session_key: &Vec, + value: u128, + ) -> DispatchResultWithPostInfo { + todo!() + } + + fn lock_extra_for_staking( + stash_account: &T::AccountId, + value: u128, + ) -> DispatchResultWithPostInfo { + todo!() + } + + fn unlock_request_for_withdrawal(stash_account: &T::AccountId) -> DispatchResultWithPostInfo { + todo!() + } + + fn withdraw(stash_account: &T::AccountId) -> DispatchResultWithPostInfo { + todo!() + } +} + /// A helper trait to handle staking NOT found in pallet-utxo. pub trait StakingHelper { fn get_controller_account(stash_account: &AccountId) -> Result; diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index f947e73..f61092c 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -238,6 +238,10 @@ path = '../pallets/template' default-features = false path = "../pallets/utxo" +[dependencies.pallet-utxo-staking] +default-features = false +path = "../pallets/staking" + [dependencies.pallet-utxo-rpc-runtime-api] default-features = false path = "../pallets/utxo/rpc/runtime-api" @@ -281,6 +285,7 @@ std = [ 'pallet-transaction-payment/std', 'pallet-utxo-rpc-runtime-api/std', 'pallet-utxo/std', + 'pallet-utxo-staking/std', 'pallet-contracts/std', 'pallet-contracts-primitives/std', 'sp-api/std', diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index f9951df..46ce04a 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -40,9 +40,9 @@ pub use frame_support::{ }; pub use pallet_balances::Call as BalancesCall; use pallet_contracts::weights::WeightInfo; -pub use pallet_staking::StakerStatus; pub use pallet_timestamp::Call as TimestampCall; use pallet_transaction_payment::CurrencyAdapter; +pub use pallet_utxo_staking::StakerStatus; #[cfg(any(feature = "std", test))] pub use sp_runtime::BuildStorage; pub use sp_runtime::{Perbill, Percent, Permill}; @@ -343,7 +343,7 @@ impl pallet_utxo::Config for Runtime { .collect() } - type StakingHelper = StakeOps; + type StakingHelper = pallet_utxo::staking::NoStaking; type MinimumStake = MinimumStake; type StakeWithdrawalFee = StakeWithdrawalFee; type InitialReward = InitialReward; @@ -413,17 +413,17 @@ parameter_types! { } impl pallet_session_historical::Config for Runtime { - type FullIdentification = pallet_staking::Exposure; - type FullIdentificationOf = pallet_staking::ExposureOf; + type FullIdentification = pallet_utxo_staking::Exposure; + type FullIdentificationOf = pallet_utxo_staking::ExposureOf; } impl pallet_session::Config for Runtime { type Event = Event; type ValidatorId = AccountId; - type ValidatorIdOf = pallet_staking::StashOf; + type ValidatorIdOf = pallet_utxo_staking::StashOf; type ShouldEndSession = pallet_session::PeriodicSessions; type NextSessionRotation = (); - type SessionManager = pallet_session_historical::NoteHistoricalRoot; + type SessionManager = pallet_session_historical::NoteHistoricalRoot; type SessionHandler = ::KeyTypeIdProviders; type Keys = opaque::SessionKeys; type DisabledValidatorsThreshold = DisabledValidatorsThreshold; @@ -434,9 +434,10 @@ impl onchain::Config for Runtime { type AccountId = AccountId; type BlockNumber = BlockNumber; type Accuracy = Perbill; - type DataProvider = Staking; + type DataProvider = UtxoStaking; } +/* parameter_types! { // TODO: how many sessions is in 1 era? // we've settled on Period * # of Session blocks @@ -476,6 +477,32 @@ impl pallet_staking::Config for Runtime { type WeightInfo = pallet_staking::weights::SubstrateWeight; } +*/ + +parameter_types! { + // we've settled on Period * # of Session blocks + // Note: an era is when the change of validator set happens. + pub const SessionsPerEra: sp_staking::SessionIndex = 2; // Note: upon unlocking funds, it doesn't mean withdrawal is activated. + // TODO: How long should the stake stay "bonded" or "locked", until it's allowed to withdraw? + pub const BondingDuration: pallet_utxo_staking::EraIndex = 2; + +} + +impl pallet_utxo_staking::Config for Runtime { + type Balance = pallet_utxo::staking::UtxoBalance; + type SettingSessionKey = StakeOps; + type UnixTime = Timestamp; + type CurrencyToVote = U128CurrencyToVote; + type ElectionProvider = onchain::OnChainSequentialPhragmen; + type GenesisElectionProvider = Self::ElectionProvider; + type Event = Event; + type SessionsPerEra = SessionsPerEra; + type BondingDuration = BondingDuration; + type SessionInterface = Self; + type NextNewSession = Session; + type WeightInfo = pallet_utxo_staking::weights::SubstrateWeight; +} + parameter_types! { pub const UncleGenerations: BlockNumber = 4; } @@ -517,7 +544,7 @@ construct_runtime!( Pp: pallet_pp::{Pallet, Call, Config, Storage, Event}, Contracts: pallet_contracts::{Pallet, Call, Storage, Event}, Authorship: pallet_authorship::{Pallet, Call, Storage, Inherent}, - Staking: pallet_staking::{Pallet, Call, Config, Storage, Event}, + UtxoStaking: pallet_utxo_staking::{Pallet, Call, Config, Storage, Event}, Session: pallet_session::{Pallet, Call, Config, Storage, Event}, Aura: pallet_aura::{Pallet, Config}, Historical: pallet_session_historical::{Pallet}, diff --git a/runtime/src/staking.rs b/runtime/src/staking.rs index 2abe1a1..33d518b 100644 --- a/runtime/src/staking.rs +++ b/runtime/src/staking.rs @@ -31,6 +31,25 @@ type LookupSourceOf = <::Lookup as StaticLookup>::Source; pub struct StakeOps(sp_core::sp_std::marker::PhantomData); +impl + pallet_utxo_staking::SettingSessionKey for StakeOps +{ + fn can_decode_session_keys(session_key: &Vec) -> bool { + ::Keys::decode(&mut &session_key[..]).is_ok() + } + + fn set_session_keys(controller: StakeAccountId, session_keys: &Vec) -> DispatchResult { + // session keys + let sesh_key = ::Keys::decode(&mut &session_keys[..]) + .expect("SessionKeys decoded successfully"); + pallet_session::Pallet::::set_keys( + RawOrigin::Signed(controller).into(), + sesh_key, + vec![], + ) + } +} + impl StakeOps where StakeAccountId: From<[u8; 32]>,