From b35bd7be94d970024e755bfd6cd0dc8b2f3020a0 Mon Sep 17 00:00:00 2001 From: Crystalin Date: Fri, 8 May 2020 12:24:34 -0400 Subject: [PATCH] Restore pallets (WIP) --- pallets/mb-core/Cargo.toml | 35 +++ pallets/mb-core/src/lib.rs | 131 +++++++++++ pallets/mb-session/Cargo.toml | 40 ++++ pallets/mb-session/src/lib.rs | 403 ++++++++++++++++++++++++++++++++++ 4 files changed, 609 insertions(+) create mode 100644 pallets/mb-core/Cargo.toml create mode 100644 pallets/mb-core/src/lib.rs create mode 100644 pallets/mb-session/Cargo.toml create mode 100644 pallets/mb-session/src/lib.rs diff --git a/pallets/mb-core/Cargo.toml b/pallets/mb-core/Cargo.toml new file mode 100644 index 0000000000000..48f93068a20b5 --- /dev/null +++ b/pallets/mb-core/Cargo.toml @@ -0,0 +1,35 @@ +[package] +authors = ['PureStake'] +edition = '2018' +name = 'mb-core' +version = '0.1.0' + +[dependencies] +codec = { package = "parity-scale-codec", version = "1.0.0", default-features = false, features = ["derive"] } +safe-mix = { default-features = false, version = '1.0.0' } +serde = { version = "1.0.102", features = ["derive"] } + +# primitives +sp-core = { git = 'https://github.com/paritytech/substrate.git', rev = '992aea815a753da256a9c0bff053df408532df02', default-features = false } +sp-io = { git = 'https://github.com/paritytech/substrate.git', rev = '992aea815a753da256a9c0bff053df408532df02', default-features = false } +sp-runtime = { git = 'https://github.com/paritytech/substrate.git', rev = '992aea815a753da256a9c0bff053df408532df02', default-features = false } +sp-std = { git = 'https://github.com/paritytech/substrate.git', rev = '992aea815a753da256a9c0bff053df408532df02', default-features = false } +sp-staking = { git = 'https://github.com/paritytech/substrate.git', rev = '992aea815a753da256a9c0bff053df408532df02', default-features = false } + +# frame dependencies +frame-support = { git = 'https://github.com/paritytech/substrate.git', rev = '992aea815a753da256a9c0bff053df408532df02', default-features = false } +system = { package = 'frame-system', git = 'https://github.com/paritytech/substrate.git', rev = '992aea815a753da256a9c0bff053df408532df02', default-features = false } +pallet-balances = { git = 'https://github.com/paritytech/substrate.git', rev = '992aea815a753da256a9c0bff053df408532df02', default-features = false } +pallet-staking = { git = 'https://github.com/paritytech/substrate.git', rev = '992aea815a753da256a9c0bff053df408532df02', default-features = false } + +[features] +default = ['std'] +std = [ + "sp-std/std", + "sp-staking/std", + 'codec/std', + 'frame-support/std', + 'safe-mix/std', + 'system/std', + 'pallet-staking/std', +] diff --git a/pallets/mb-core/src/lib.rs b/pallets/mb-core/src/lib.rs new file mode 100644 index 0000000000000..80c8bf16caf0f --- /dev/null +++ b/pallets/mb-core/src/lib.rs @@ -0,0 +1,131 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +use sp_std::prelude::*; + +use frame_support::{decl_module, decl_storage, decl_event}; +use frame_support::dispatch::{DispatchResult}; +use frame_support::traits::{OnUnbalanced,Currency,LockableCurrency,Imbalance}; +use system::{ensure_root,RawOrigin}; +use sp_runtime::{traits::{EnsureOrigin,CheckedAdd,CheckedSub}}; + +type BalanceOf = + <::Currency as Currency<::AccountId>>::Balance; + +type NegativeImbalanceOf = + <::Currency as Currency<::AccountId>>::NegativeImbalance; + +type PositiveImbalanceOf = + <::Currency as Currency<::AccountId>>::PositiveImbalance; + +pub trait Trait: system::Trait + pallet_balances::Trait { + type Event: From> + Into<::Event>; + type Currency: LockableCurrency; +} + +decl_storage! { + trait Store for Module as MoonbeamModule { + Treasury get(treasury): BalanceOf; + GenesisAccounts get(genesis_accounts): Vec; + } + add_extra_genesis { + config(treasury): BalanceOf; + config(genesis_accounts): Vec; + build(|config: &GenesisConfig| { + >::put(config.treasury); + let _ = >::append(config.genesis_accounts.clone()); + }); + } +} + +decl_event!( + pub enum Event + where + AccountId = ::AccountId, + BalanceOf = BalanceOf, + { + Absorbed(BalanceOf, BalanceOf), + Rewarded(BalanceOf, BalanceOf), + TreasuryTransferOk(AccountId, BalanceOf, BalanceOf), + } +); + +decl_module! { + pub struct Module for enum Call where origin: T::Origin { + + fn deposit_event() = default; + + // TODO work in progress mint from pot + fn mint( + origin, _to: T::AccountId, _ammount: BalanceOf + ) -> DispatchResult { + let _caller = ensure_root(origin); + Ok(()) + } + + } +} + +pub struct Collective(AccountId); +impl< + O: Into, O>> + From>, + AccountId +> EnsureOrigin for Collective { + type Success = (); + fn try_origin(_o: O) -> Result { + Ok(()) + } +} + +// https://substrate.dev/rustdocs/pre-v2.0-3e65111/pallet_staking/trait.Trait.html#associatedtype.RewardRemainder +pub struct RewardRemainder(T); +impl OnUnbalanced> for RewardRemainder +{ + fn on_nonzero_unbalanced(_amount: NegativeImbalanceOf) { + // TODO Tokens have been minted and are unused for validator-reward. + let _a = 1; + } +} + +// NegativeImbalance: +// Some balance has been subtracted somewhere, needs to be added somewhere else. +pub struct Absorb(T); +impl OnUnbalanced> for Absorb +{ + fn on_nonzero_unbalanced(amount: NegativeImbalanceOf) { + let raw_amount = amount.peek(); + let treasury = >::get(); + if let Some(next_treasury) = treasury.checked_add(&raw_amount) { + >::put(next_treasury); + } else { + // TODO + } + >::deposit_event( + RawEvent::Absorbed( + raw_amount, + >::get() + ) + ); + } +} + +// PositiveImbalance: +// Some balance has been added somewhere, needs to be subtracted somewhere else. +pub struct Reward(T); +impl OnUnbalanced> for Reward +{ + fn on_nonzero_unbalanced(amount: PositiveImbalanceOf) { + let raw_amount = amount.peek(); + let treasury = >::get(); + if let Some(next_treasury) = treasury.checked_sub(&raw_amount) { + >::put(next_treasury); + } else { + // TODO + } + >::deposit_event( + RawEvent::Rewarded( + raw_amount, + >::get() + ) + ); + } +} \ No newline at end of file diff --git a/pallets/mb-session/Cargo.toml b/pallets/mb-session/Cargo.toml new file mode 100644 index 0000000000000..744ac3fa4b865 --- /dev/null +++ b/pallets/mb-session/Cargo.toml @@ -0,0 +1,40 @@ +[package] +authors = ['PureStake'] +edition = '2018' +name = 'mb-session' +version = '0.1.0' + +[dependencies] +codec = { package = "parity-scale-codec", version = "1.0.0", default-features = false, features = ["derive"] } +safe-mix = { default-features = false, version = '1.0.0' } +serde = { version = "1.0.102", features = ["derive"] } + +# primitives +sp-core = { git = 'https://github.com/paritytech/substrate.git', rev = '992aea815a753da256a9c0bff053df408532df02', default-features = false } +sp-io = { git = 'https://github.com/paritytech/substrate.git', rev = '992aea815a753da256a9c0bff053df408532df02', default-features = false } +sp-runtime = { git = 'https://github.com/paritytech/substrate.git', rev = '992aea815a753da256a9c0bff053df408532df02', default-features = false } +sp-std = { git = 'https://github.com/paritytech/substrate.git', rev = '992aea815a753da256a9c0bff053df408532df02', default-features = false } +sp-session = { git = 'https://github.com/paritytech/substrate.git', rev = '992aea815a753da256a9c0bff053df408532df02', default-features = false } +sp-staking = { git = 'https://github.com/paritytech/substrate.git', rev = '992aea815a753da256a9c0bff053df408532df02', default-features = false } +node-primitives = { git = 'https://github.com/paritytech/substrate.git', rev = '992aea815a753da256a9c0bff053df408532df02', default-features = false } + +# frame dependencies +frame-support = { git = 'https://github.com/paritytech/substrate.git', rev = '992aea815a753da256a9c0bff053df408532df02', default-features = false } +system = { package = 'frame-system', git = 'https://github.com/paritytech/substrate.git', rev = '992aea815a753da256a9c0bff053df408532df02', default-features = false } +pallet-balances = { git = 'https://github.com/paritytech/substrate.git', rev = '992aea815a753da256a9c0bff053df408532df02', default-features = false } +pallet-session = { features = ["historical"], git = 'https://github.com/paritytech/substrate.git', rev = '992aea815a753da256a9c0bff053df408532df02', default-features = false } +pallet-authorship = { git = 'https://github.com/paritytech/substrate.git', rev = '992aea815a753da256a9c0bff053df408532df02', default-features = false } + +[features] +default = ['std'] +std = [ + "sp-std/std", + "sp-staking/std", + 'codec/std', + 'frame-support/std', + 'safe-mix/std', + "sp-session/std", + 'system/std', + "pallet-session/std", + "pallet-authorship/std", +] diff --git a/pallets/mb-session/src/lib.rs b/pallets/mb-session/src/lib.rs new file mode 100644 index 0000000000000..bf6cd72dc4379 --- /dev/null +++ b/pallets/mb-session/src/lib.rs @@ -0,0 +1,403 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +use sp_std::prelude::*; +// use sp_runtime::traits::{OpaqueKeys,Convert}; +use sp_runtime::traits::{SaturatedConversion}; +use frame_support::{decl_module, decl_storage, decl_event, decl_error, debug}; +use frame_support::dispatch::{DispatchResult}; +use frame_support::traits::{Currency,Get}; +use system::{ensure_signed}; + +use sp_core::crypto::KeyTypeId; +use system::offchain::{SubmitSignedTransaction}; + +#[path = "../../../runtime/src/constants.rs"] +#[allow(dead_code)] +mod constants; +use constants::time::{EPOCH_DURATION_IN_BLOCKS}; +use constants::mb_genesis::{VALIDATORS_PER_SESSION}; + +pub const KEY_TYPE: KeyTypeId = KeyTypeId(*b"mbst"); + +type BalanceOf = + <::Currency as Currency<::AccountId>>::Balance; + +pub mod crypto { + pub use super::KEY_TYPE; + use sp_runtime::app_crypto::{app_crypto, sr25519}; + app_crypto!(sr25519, KEY_TYPE); +} + +pub trait Trait: system::Trait + pallet_balances::Trait + pallet_session::Trait { + type Event: From> + Into<::Event>; + type Call: From>; + type SubmitTransaction: SubmitSignedTransaction::Call>; + type Currency: Currency; + type SessionsPerEra: Get; +} + +decl_storage! { + trait Store for Module as MoonbeamStakingModule { + /// The number of Era. + EraIndex: u32; + /// The total validator pool. + Validators: Vec; + /// The current session of an Era. + SessionOfEraIndex: u32; + /// The current Block Index of an Era. + BlockOfEraIndex: u32; + /// The validator set selected for the Era. + SessionValidators get(session_validators): Vec; + /// Number of blocks authored by a given validator in this Era. + SessionValidatorAuthoring: + map hasher(blake2_256) T::AccountId => u32; + /// One to Many Validator -> Endorsers. + ValidatorEndorsers: map hasher(blake2_256) T::AccountId => Vec; + /// One to One Endorser -> Validator. + Endorser: map hasher(blake2_256) T::AccountId => T::AccountId; + /// A timeline of free_balances for an endorser that allows us to calculate + /// the average of free_balance of an era. + /// + /// TODO: Used to select era validators at the start (or end TODO) of an era. + /// We are by now supposing that an endorsement represents all the free_balance + /// of the token holder. + /// When the free_balance of an endorser changes, a new snapshot is created + /// together with the current block_index of the current era. + /// + /// Endorser, Validator => (session_block_index,endorser_balance) + EndorserSnapshots: + double_map hasher(blake2_256) T::AccountId, hasher(blake2_256) T::AccountId => Vec<(u32,BalanceOf)>; + + /// TODO the Treasury balance. It is still unclear if this will be a pallet account or + /// will remain as a Storage balance. + Treasury get(treasury): T::Balance; + } + add_extra_genesis { + config(session_validators): Vec; + config(treasury): T::Balance; + build(|config: &GenesisConfig| { + // set all validators + let _ = >::append(config.session_validators.clone()); + // set initial selected validators + let _ = >::append(config.session_validators.clone()); + // set treasury + >::put(config.treasury); + // set genesis era data + EraIndex::put(1); + BlockOfEraIndex::put(1); + }); + } +} + +decl_error! { + pub enum Error for Module { + AlreadyEndorsing, + NotEndorsing, + } +} + +decl_event!( + pub enum Event + where + AccountId = ::AccountId, + { + BlockAuthored(AccountId), + NewEra(u32), + NewSession(u32), + EndSession(u32), + } +); + +decl_module! { + pub struct Module for enum Call where origin: T::Origin { + + type Error = Error; + fn deposit_event() = default; + + /// Endorsing dispatchable function + pub fn endorse( + origin, to:T::AccountId + ) -> DispatchResult { + let from = ensure_signed(origin)?; + // Check if the Account is already endorsing. + if >::contains_key(&from) { + return Err(Error::::AlreadyEndorsing).map_err(Into::into); + } + // Set One to One endorser->validator association. + >::insert(&from,&to); + // Set One to Many validator->endorsers association. + >::append(&to,vec![&from])?; + // Create a snapshot with the current free balance of the endorser. + Self::set_snapshot(&from,&to,T::Currency::free_balance(&from))?; + Ok(()) + } + + /// Unndorsing dispatchable function + pub fn unendorse( + origin + ) -> DispatchResult { + let from = ensure_signed(origin)?; + // Check if the Account is actively endorsing. + if !>::contains_key(&from) { + return Err(Error::::NotEndorsing).map_err(Into::into); + } + + let validator = >::get(&from); + let mut endorsers = >::get(&validator); + + // Remove One to Many validator->endorsers association + endorsers.retain(|x| x != &from); + >::insert(&validator, endorsers); + // Remove One to One endorser->validator association + >::remove(&from); + // Remove all snapshots associated to the endorser, using the double map prefix + >::remove_prefix(&from); + + Ok(()) + } + + fn offchain_worker(block_number: T::BlockNumber) { + // Set snapshots + Self::offchain_set_snapshots(); + // Select validators off-chain + Self::offchain_validator_selection(block_number); + } + + fn persist_selected_validators( + origin,selected_validators: Vec + ) -> DispatchResult { + ensure_signed(origin)?; + >::put(selected_validators.clone()); + Ok(()) + } + + fn persist_snapshots( + origin,snapshots: Vec<(T::AccountId,T::AccountId,BalanceOf)> + ) -> DispatchResult { + ensure_signed(origin)?; + for s in &snapshots { + Self::set_snapshot(&s.0,&s.1,s.2)?; + } + Ok(()) + } + } +} + +impl Module { + + /// First approach to keep moving forward until we find out how to track BalanceOf + /// changes in real-time. + /// + /// This approach, although functional, is invalid as it has multiple issues like + /// sending signed transactions potentially every block. + /// + /// Other messy ways could be, again a per-block offchain task, pattern matching the + /// >::events() to find pallet_balances events that are registered + /// in the Storage. + fn offchain_set_snapshots() { + let mut output: Vec<(T::AccountId,T::AccountId,BalanceOf)> = vec![]; + let validators = >::get(); + for v in &validators { + let endorsers = >::get(v); + for ed in &endorsers { + let snapshots = >::get(ed,v.clone()); + let len = snapshots.len(); + // Make sure we have a previous block reference in this Era + if len > 0 { + let snapshot_balance = snapshots[len-1].1; + let current_balance = T::Currency::free_balance(ed); + if snapshot_balance != current_balance { + output.push((ed.clone(),v.clone(),current_balance)); + } + } + } + } + // If there are snapshots, send signed transaction + if output.len() > 0 { + let call = Call::persist_snapshots(output); + let res = T::SubmitTransaction::submit_signed(call); + if res.is_empty() { + debug::native::info!("No local accounts found."); + } else { + debug::native::info!("Sending snapshots transaction."); + } + } + } + + /// Offchain task to select validators + fn offchain_validator_selection(block_number: T::BlockNumber) { + // Find out where we are in Era + let current_era: u128 = EraIndex::get() as u128; + let last_block_of_era: u128 = + (current_era * (T::SessionsPerEra::get() as u128) * (EPOCH_DURATION_IN_BLOCKS as u128)).saturated_into(); + let validator_selection_delta: u128 = 5; + let current_block_number: u128 = block_number.saturated_into(); + // When we are 5 blocks away of a new Era, run the validator selection. + if (last_block_of_era - current_block_number) == validator_selection_delta { + // Perform the validator selection + let selected_validators = >::select_validators(); + // Send signed transaction to persist the new validators to the on-chain storage + let call = Call::persist_selected_validators(selected_validators); + let res = T::SubmitTransaction::submit_signed(call); + if res.is_empty() { + debug::native::info!("No local accounts found."); + } else { + debug::native::info!("Sending selected validator transaction."); + } + } + } + + /// Sets a snapshot using the current era's block index and the Account free_balance. + fn set_snapshot( + endorser: &T::AccountId, validator: &T::AccountId, amount: BalanceOf + ) -> DispatchResult { + >::append(&endorser,&validator, + vec![(BlockOfEraIndex::get(),amount)])?; + Ok(()) + } + /// Calculates a single endorser weighted balance for the era by measuring the + /// block index distances. + fn calculate_endorsement( + endorser: &T::AccountId, validator: &T::AccountId + ) -> f64 { + + let duration: u32 = EPOCH_DURATION_IN_BLOCKS; + let points = >::get(endorser,validator); + let points_dim: usize = points.len(); + + if points_dim == 0 { + return 0 as f64; + } + + let n: usize = points_dim-1; + let (points,n) = Self::set_snapshot_boundaries(duration,n,points); + let mut previous: (u32,BalanceOf) = (0,BalanceOf::::from(0)); + // Find the distances between snapshots, weight the free_balance against them. + // Finally sum all values. + let mut endorsement: f64 = points.iter().map(|p| { + let out: f64; + if previous != (0,BalanceOf::::from(0)) { + let delta = p.0-previous.0; + let w = delta as f64 / duration as f64; + out = w * previous.1.saturated_into() as f64; + } else { + out = 0 as f64; + } + previous = *p; + out + }) + .sum::(); + // The above iterative approach excludes the last block, sum it to the result. + endorsement += (1 as f64 / duration as f64) * points[n].1.saturated_into() as f64; + endorsement + } + /// Selects a new validator set based on their amount of weighted endorsement. + fn select_validators() -> Vec { + let validators = >::get(); + // Get the calculated endorsement per validator. + let mut selected_validators = validators.iter().map(|v| { + let endorsers = >::get(v); + let total_validator_endorsement = endorsers.iter().map(|ed| { + Self::calculate_endorsement(ed,v) + }) + .sum::(); + (total_validator_endorsement,v) + }) + .collect::>(); + // Sort descendant validators by amount. + selected_validators.sort_by(|(x0, _y0), (x1, _y1)| x0.partial_cmp(&x1).unwrap()); + selected_validators.reverse(); + // Take the by-configuration amount of validators. + selected_validators.into_iter().take(VALIDATORS_PER_SESSION as usize) + .map(|(_x,y)| y.clone()) + .collect::>() + } + /// Conditionally set the boundary balances to complete a snapshot series. + /// (if no snapshot is defined on block 1 or {era_len} indexes). + fn set_snapshot_boundaries( + duration: u32, + mut last_index: usize, + mut collection: Vec<(u32,BalanceOf)> + ) -> (Vec<(u32,BalanceOf)>,usize) { + + if collection[0].0 != 1 { + collection.insert(0,(1,BalanceOf::::from(0))); + last_index += 1; + } + if collection[last_index].0 != duration { + collection.push((duration,collection[last_index].1)); + } + (collection,last_index) + } + /// All snapshots are reset on era change with a single checkpoint of the + /// current endorser's Account free_balance. + fn reset_snapshots() { + let validators = >::get(); + for validator in validators.iter() { + let endorsers = >::get(validator); + for endorser in endorsers.iter() { + >::insert(endorser,validator,vec![( + 1 as u32, + T::Currency::free_balance(endorser) + )]); + } + } + } +} + +pub struct SessionManager(T); +impl pallet_session::SessionManager for SessionManager { + fn new_session(new_index: u32) -> Option> { + + >::deposit_event( + RawEvent::NewSession(new_index) + ); + + if new_index > 1 { + let current_era = EraIndex::get(); + if new_index > (current_era * (T::SessionsPerEra::get() as u32)) { + // Era change + // Reset SessionOfEraIndex to 1 + SessionOfEraIndex::put(1); + // Reset BlockOfEraIndex to 1 + BlockOfEraIndex::put(1); + // Increase the EraIndex by 1 + let new_era_idx = EraIndex::get().checked_add(1) + .ok_or("SessionOfEraIndex Overflow").unwrap(); + EraIndex::put(new_era_idx); + // Reset all snapshots + >::reset_snapshots(); + } else { + // Same Era, next session. Increase SessionOfEraIndex by 1. + let new_era_session_idx = SessionOfEraIndex::get().checked_add(1) + .ok_or("SessionOfEraIndex Overflow").unwrap(); + SessionOfEraIndex::put(new_era_session_idx); + } + Some(>::get()) + } else { + None + } + } + + fn end_session(end_index: u32) { + >::deposit_event( + RawEvent::EndSession(end_index) + ); + } +} + +pub struct AuthorshipEventHandler(T); +impl pallet_authorship::EventHandler for AuthorshipEventHandler { + fn note_author(author: T::AccountId) { + let authored_blocks = + >::get(&author).checked_add(1).ok_or("Overflow").unwrap(); + >::insert(&author,authored_blocks); + BlockOfEraIndex::mutate(|x| *x += 1); + // >::deposit_event( + // RawEvent::BlockAuthored(author) + // ); + } + fn note_uncle(_author: T::AccountId, _age: u32) { + + } +} \ No newline at end of file