diff --git a/src/governance/election.rs b/src/governance/election.rs index e6d0f109e7..0cc47a5c49 100644 --- a/src/governance/election.rs +++ b/src/governance/election.rs @@ -17,14 +17,14 @@ use super::sealed_vote::SealedVote; pub use super::{ GovernanceCurrency, BalanceOf }; use super::council; -use crate::traits::{IsActiveMember}; +use crate::traits::{Members}; pub trait Trait: system::Trait + council::Trait + GovernanceCurrency { type Event: From> + Into<::Event>; type CouncilElected: CouncilElected>, Self::BlockNumber>; - type IsActiveMember: IsActiveMember; + type Members: Members; } #[derive(Clone, Copy, Encode, Decode)] @@ -154,7 +154,7 @@ impl Module { fn can_participate(sender: &T::AccountId) -> bool { - !T::Currency::free_balance(sender).is_zero() && T::IsActiveMember::is_active_member(sender) + !T::Currency::free_balance(sender).is_zero() && T::Members::is_active_member(sender) } // PUBLIC IMMUTABLES diff --git a/src/governance/mock.rs b/src/governance/mock.rs index ce5bb9bd4a..183b5f3546 100644 --- a/src/governance/mock.rs +++ b/src/governance/mock.rs @@ -3,7 +3,7 @@ use rstd::prelude::*; pub use super::{election, council, proposals, GovernanceCurrency}; pub use system; -use crate::traits; +use crate::traits::{Members}; pub use primitives::{H256, Blake2Hasher}; pub use runtime_primitives::{ @@ -18,16 +18,23 @@ impl_outer_origin! { pub enum Origin for Test {} } -pub struct AnyAccountIsMember {} -impl traits::IsActiveMember for AnyAccountIsMember { +pub struct MockMembership {} +impl Members for MockMembership { + type Id = u32; fn is_active_member(who: &T::AccountId) -> bool { + // all accounts are considered members. + // There is currently no test coverage for non-members. + // Should add some coverage, and update this method to reflect which accounts are or are not members true } + fn lookup_member_id(account_id: &T::AccountId) -> Result { + Err("not implemented!") + } + fn lookup_account_by_member_id(id: Self::Id) -> Result { + Err("not implemented!") + } } -// default trait implementation - any account is not a member -// impl traits::IsActiveMember for () {} - // For testing the module, we construct most of a mock runtime. This means // first constructing a configuration type (`Test`) which `impl`s each of the // configuration traits of modules we want to use. @@ -65,7 +72,7 @@ impl election::Trait for Test { type CouncilElected = (Council,); - type IsActiveMember = AnyAccountIsMember; + type Members = MockMembership; } impl balances::Trait for Test { diff --git a/src/governance/proposals.rs b/src/governance/proposals.rs index 58878dc9e9..18b80fa53b 100644 --- a/src/governance/proposals.rs +++ b/src/governance/proposals.rs @@ -1,4 +1,4 @@ -use srml_support::{StorageValue, StorageMap, dispatch::Result, decl_module, decl_event, decl_storage, ensure}; +use srml_support::{StorageValue, StorageMap, dispatch, decl_module, decl_event, decl_storage, ensure}; use srml_support::traits::{Currency}; use primitives::{storage::well_known_keys}; use runtime_primitives::traits::{As, Hash, Zero}; @@ -8,7 +8,7 @@ use rstd::prelude::*; use super::council; pub use super::{ GovernanceCurrency, BalanceOf }; -use crate::traits::{IsActiveMember}; +use crate::traits::{Members}; const DEFAULT_APPROVAL_QUORUM: u32 = 60; const DEFAULT_MIN_STAKE: u64 = 100; @@ -117,7 +117,7 @@ pub trait Trait: timestamp::Trait + council::Trait + GovernanceCurrency { /// The overarching event type. type Event: From> + Into<::Event>; - type IsActiveMember: IsActiveMember; + type Members: Members; } decl_event!( @@ -349,7 +349,7 @@ impl Module { } fn can_participate(sender: T::AccountId) -> bool { - !T::Currency::free_balance(&sender).is_zero() && T::IsActiveMember::is_active_member(&sender) + !T::Currency::free_balance(&sender).is_zero() && T::Members::is_active_member(&sender) } fn is_councilor(sender: &T::AccountId) -> bool { @@ -368,7 +368,7 @@ impl Module { Self::current_block() >= proposed_at + Self::voting_period() } - fn _process_vote(voter: T::AccountId, proposal_id: u32, vote: VoteKind) -> Result { + fn _process_vote(voter: T::AccountId, proposal_id: u32, vote: VoteKind) -> dispatch::Result { let new_vote = (voter.clone(), vote.clone()); if >::exists(proposal_id) { // Append a new vote to other votes on this proposal: @@ -382,7 +382,7 @@ impl Module { Ok(()) } - fn end_block(now: T::BlockNumber) -> Result { + fn end_block(now: T::BlockNumber) -> dispatch::Result { // TODO refactor this method @@ -395,7 +395,7 @@ impl Module { } /// Get the voters for the current proposal. - pub fn tally(/* proposal_id: u32 */) -> Result { + pub fn tally(/* proposal_id: u32 */) -> dispatch::Result { let councilors: u32 = Self::councilors_count(); let quorum: u32 = Self::approval_quorum_seats(); @@ -475,7 +475,7 @@ impl Module { } /// Updates proposal status and removes proposal from active ids. - fn _update_proposal_status(proposal_id: u32, new_status: ProposalStatus) -> Result { + fn _update_proposal_status(proposal_id: u32, new_status: ProposalStatus) -> dispatch::Result { let all_active_ids = Self::active_proposal_ids(); let all_len = all_active_ids.len(); let other_active_ids: Vec = all_active_ids @@ -503,7 +503,7 @@ impl Module { } /// Slash a proposal. The staked deposit will be slashed. - fn _slash_proposal(proposal_id: u32) -> Result { + fn _slash_proposal(proposal_id: u32) -> dispatch::Result { let proposal = Self::proposals(proposal_id); // Slash proposer's stake: @@ -513,7 +513,7 @@ impl Module { } /// Reject a proposal. The staked deposit will be returned to a proposer. - fn _reject_proposal(proposal_id: u32) -> Result { + fn _reject_proposal(proposal_id: u32) -> dispatch::Result { let proposal = Self::proposals(proposal_id); let proposer = proposal.proposer; @@ -529,7 +529,7 @@ impl Module { } /// Approve a proposal. The staked deposit will be returned. - fn _approve_proposal(proposal_id: u32) -> Result { + fn _approve_proposal(proposal_id: u32) -> dispatch::Result { let proposal = Self::proposals(proposal_id); let wasm_code = Self::wasm_code_by_hash(proposal.wasm_hash); @@ -615,14 +615,24 @@ mod tests { impl Trait for Test { type Event = (); - type IsActiveMember = AnyAccountIsMember; + type Members = MockMembership; } - pub struct AnyAccountIsMember {} - impl IsActiveMember for AnyAccountIsMember { + pub struct MockMembership {} + impl Members for MockMembership { + type Id = u32; fn is_active_member(who: &T::AccountId) -> bool { + // all accounts are considered members. + // There is currently no test coverage for non-members. + // Should add some coverage, and update this method to reflect which accounts are or are not members true } + fn lookup_member_id(account_id: &T::AccountId) -> Result { + Err("not implemented!") + } + fn lookup_account_by_member_id(id: Self::Id) -> Result { + Err("not implemented!") + } } type System = system::Module; @@ -716,7 +726,7 @@ mod tests { b"Proposal Wasm Code".to_vec() } - fn _create_default_proposal() -> Result { + fn _create_default_proposal() -> dispatch::Result { _create_proposal(None, None, None, None, None) } @@ -726,7 +736,7 @@ mod tests { name: Option>, description: Option>, wasm_code: Option> - ) -> Result { + ) -> dispatch::Result { Proposals::create_proposal( Origin::signed(origin.unwrap_or(PROPOSER1)), stake.unwrap_or(min_stake()), diff --git a/src/lib.rs b/src/lib.rs index bf8bba4662..8344253e74 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,6 +23,8 @@ mod traits; mod membership; use membership::members; mod migration; +mod roles; +use roles::actors; use rstd::prelude::*; #[cfg(feature = "std")] @@ -186,7 +188,7 @@ impl balances::Trait for Runtime { /// What to do if a new account is created. type OnNewAccount = Indices; /// Restrict whether an account can transfer funds. We don't place any further restrictions. - type EnsureAccountLiquid = Staking; + type EnsureAccountLiquid = (Staking, Actors); /// The uniquitous event type. type Event = Event; } @@ -214,13 +216,13 @@ impl governance::GovernanceCurrency for Runtime { impl governance::proposals::Trait for Runtime { type Event = Event; - type IsActiveMember = Members; + type Members = Members; } impl governance::election::Trait for Runtime { type Event = Event; type CouncilElected = (Council,); - type IsActiveMember = Members; + type Members = Members; } impl governance::council::Trait for Runtime { @@ -242,12 +244,18 @@ impl members::Trait for Runtime { type MemberId = u64; type PaidTermId = u64; type SubscriptionId = u64; + type Roles = Actors; } impl migration::Trait for Runtime { type Event = Event; } +impl actors::Trait for Runtime { + type Event = Event; + type Members = Members; +} + construct_runtime!( pub enum Runtime with Log(InternalLog: DigestItem) where Block = Block, @@ -270,6 +278,7 @@ construct_runtime!( Memo: memo::{Module, Call, Storage, Event}, Members: members::{Module, Call, Storage, Event, Config}, Migration: migration::{Module, Call, Storage, Event}, + Actors: actors::{Module, Call, Storage, Event}, DataObjectTypeRegistry: data_object_type_registry::{Module, Call, Storage, Event, Config}, } ); diff --git a/src/membership/members.rs b/src/membership/members.rs index 68faed9917..9ac1c9a4b0 100644 --- a/src/membership/members.rs +++ b/src/membership/members.rs @@ -9,7 +9,7 @@ use runtime_primitives::traits::{Zero, SimpleArithmetic, As, Member, MaybeSerial use system::{self, ensure_signed}; use crate::governance::{GovernanceCurrency, BalanceOf }; use {timestamp}; -use crate::traits; +use crate::traits::{Members, Roles}; pub trait Trait: system::Trait + GovernanceCurrency + timestamp::Trait { type Event: From> + Into<::Event>; @@ -22,6 +22,8 @@ pub trait Trait: system::Trait + GovernanceCurrency + timestamp::Trait { type SubscriptionId: Parameter + Member + SimpleArithmetic + Codec + Default + Copy + As + As + MaybeSerializeDebug + PartialEq; + + type Roles: Roles; } const DEFAULT_FIRST_MEMBER_ID: u64 = 1; @@ -173,7 +175,9 @@ impl Module { } } -impl traits::IsActiveMember for Module { +impl Members for Module { + type Id = T::MemberId; + fn is_active_member(who: &T::AccountId) -> bool { match Self::ensure_is_member(who) .and_then(|member_id| Self::ensure_profile(member_id)) @@ -182,6 +186,18 @@ impl traits::IsActiveMember for Module { Err(err) => false } } + + fn lookup_member_id(who: &T::AccountId) -> Result { + Self::ensure_is_member(who) + } + + fn lookup_account_by_member_id(id: Self::Id) -> Result { + if >::exists(&id) { + Ok(Self::account_id_by_member_id(&id)) + } else { + Err("member id doesn't exist") + } + } } decl_module! { @@ -198,6 +214,9 @@ decl_module! { // ensure key not associated with an existing membership Self::ensure_not_member(&who)?; + // ensure account is not in a bonded role + ensure!(!T::Roles::is_role_account(&who), "role key cannot be used for membership"); + // ensure paid_terms_id is active let terms = Self::ensure_active_terms_id(paid_terms_id)?; @@ -270,6 +289,9 @@ decl_module! { // ensure key not associated with an existing membership Self::ensure_not_member(&new_member)?; + // ensure account is not in a bonded role + ensure!(!T::Roles::is_role_account(&new_member), "role key cannot be used for membership"); + let user_info = Self::check_user_registration_info(user_info)?; // ensure handle is not already registered @@ -292,7 +314,7 @@ impl Module { Ok(()) } - fn ensure_is_member(who: &T::AccountId) -> Result { + pub fn ensure_is_member(who: &T::AccountId) -> Result { let member_id = Self::member_id_by_account_id(who).ok_or("no member id found for accountid")?; Ok(member_id) } diff --git a/src/membership/mock.rs b/src/membership/mock.rs index b4e42c1be2..2460c11a81 100644 --- a/src/membership/mock.rs +++ b/src/membership/mock.rs @@ -74,6 +74,7 @@ impl members::Trait for Test { type MemberId = u32; type PaidTermId = u32; type SubscriptionId = u32; + type Roles = (); } pub struct ExtBuilder { diff --git a/src/migration.rs b/src/migration.rs index d835e0a17d..10eb7413f4 100644 --- a/src/migration.rs +++ b/src/migration.rs @@ -6,24 +6,9 @@ use rstd::prelude::*; use runtime_io::print; use crate::{VERSION}; use crate::membership::members; - -pub trait Trait: system::Trait + members::Trait { - type Event: From> + Into<::Event>; -} - -decl_storage! { - trait Store for Module as Migration { - /// Records at what runtime spec version the store was initialized. This allows the runtime - /// to know when to run initialize code if it was installed as an update. - pub SpecVersion get(spec_version) build(|_| Some(VERSION.spec_version)) : Option; - } -} - -decl_event! { - pub enum Event where ::BlockNumber { - Migrated(BlockNumber, u32), - } -} +use crate::roles::actors; +use crate::governance::{GovernanceCurrency, BalanceOf }; +use runtime_primitives::traits::{Zero, Bounded, SimpleArithmetic, As}; // When preparing a new major runtime release version bump this value to match it and update // the initialization code in runtime_initialization(). Because of the way substrate runs runtime code @@ -40,6 +25,23 @@ impl Module { >::initialize_storage(); + // Initialize Storage provider role parameters + >::set_role_parameters(actors::Role::Storage, actors::RoleParameters { + min_stake: BalanceOf::::sa(3000), + max_actors: 10, + reward: BalanceOf::::sa(10), + reward_period: T::BlockNumber::sa(600), + unbonding_period: T::BlockNumber::sa(600), + entry_request_fee: BalanceOf::::sa(50), + + // not currently used + min_actors: 5, + bonding_period: T::BlockNumber::sa(600), + min_service_period: T::BlockNumber::sa(600), + startup_grace_period: T::BlockNumber::sa(600), + }); + >::set_available_roles(vec![actors::Role::Storage]); + // ... // add initialization of other modules introduced in this runtime // ... @@ -48,6 +50,24 @@ impl Module { } } +pub trait Trait: system::Trait + members::Trait + actors::Trait { + type Event: From> + Into<::Event>; +} + +decl_storage! { + trait Store for Module as Migration { + /// Records at what runtime spec version the store was initialized. This allows the runtime + /// to know when to run initialize code if it was installed as an update. + pub SpecVersion get(spec_version) build(|_| Some(VERSION.spec_version)) : Option; + } +} + +decl_event! { + pub enum Event where ::BlockNumber { + Migrated(BlockNumber, u32), + } +} + decl_module! { pub struct Module for enum Call where origin: T::Origin { fn deposit_event() = default; diff --git a/src/roles/actors.rs b/src/roles/actors.rs new file mode 100644 index 0000000000..9ad261d184 --- /dev/null +++ b/src/roles/actors.rs @@ -0,0 +1,381 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +use crate::governance::{BalanceOf, GovernanceCurrency}; +use parity_codec_derive::{Decode, Encode}; +use rstd::prelude::*; +use runtime_primitives::traits::{ + As, Bounded, MaybeDebug, Zero, +}; +use srml_support::traits::{Currency, EnsureAccountLiquid}; +use srml_support::{ + decl_event, decl_module, decl_storage, dispatch, ensure, StorageMap, StorageValue, +}; +use system::{self, ensure_signed}; + +use crate::traits::{Members, Roles}; + +#[derive(Encode, Decode, Copy, Clone, Eq, PartialEq, Debug)] +pub enum Role { + Storage, +} + +#[cfg_attr(feature = "std", derive(Debug))] +#[derive(Encode, Decode, Copy, Clone, Eq, PartialEq)] +pub struct RoleParameters { + // minium balance required to stake to enter a role + pub min_stake: BalanceOf, + + // minimum actors to maintain - if role is unstaking + // and remaining actors would be less that this value - prevent or punish for unstaking + pub min_actors: u32, + + // the maximum number of spots available to fill for a role + pub max_actors: u32, + + // fixed amount of tokens paid to actors' primary account + pub reward: BalanceOf, + + // payouts are made at this block interval + pub reward_period: T::BlockNumber, + + // minimum amount of time before being able to unstake + pub bonding_period: T::BlockNumber, + + // how long tokens remain locked for after unstaking + pub unbonding_period: T::BlockNumber, + + // minimum period required to be in service. unbonding before this time is highly penalized + pub min_service_period: T::BlockNumber, + + // "startup" time allowed for roles that need to sync their infrastructure + // with other providers before they are considered in service and punishable for + // not delivering required level of service. + pub startup_grace_period: T::BlockNumber, + + // small fee burned to make a request to enter role + pub entry_request_fee: BalanceOf, +} + +#[derive(Encode, Decode, Clone)] +pub struct Actor { + pub member_id: MemberId, + pub role: Role, + pub account: T::AccountId, + pub joined_at: T::BlockNumber, +} + +pub trait Trait: system::Trait + GovernanceCurrency + MaybeDebug { + type Event: From> + Into<::Event>; + + type Members: Members; +} + +pub type MemberId = <::Members as Members>::Id; +// actor account, memberid, role, expires +pub type Request = (::AccountId, MemberId, Role, ::BlockNumber); +pub type Requests = Vec>; + +pub const REQUEST_LIFETIME: u64 = 300; +pub const DEFAULT_REQUEST_CLEARING_INTERVAL: u64 = 100; + +decl_storage! { + trait Store for Module as Actors { + /// requirements to enter and maintain status in roles + pub Parameters get(parameters) : map Role => Option>; + + /// the roles members can enter into + pub AvailableRoles get(available_roles) : Vec; + + /// Actors list + pub ActorAccountIds get(actor_account_ids) : Vec; + + /// actor accounts mapped to their actor + pub ActorByAccountId get(actor_by_account_id) : map T::AccountId => Option>; + + /// actor accounts associated with a role + pub AccountIdsByRole get(account_ids_by_role) : map Role => Vec; + + /// actor accounts associated with a member id + pub AccountIdsByMemberId get(account_ids_by_member_id) : map MemberId => Vec; + + /// tokens locked until given block number + pub Bondage get(bondage) : map T::AccountId => T::BlockNumber; + + /// First step before enter a role is registering intent with a new account/key. + /// This is done by sending a role_entry_request() from the new account. + /// The member must then send a stake() transaction to approve the request and enter the desired role. + /// The account making the request will be bonded and must have + /// sufficient balance to cover the minimum stake for the role. + /// Bonding only occurs after successful entry into a role. + /// The request expires after REQUEST_LIFETIME blocks + pub RoleEntryRequests get(role_entry_requests) : Requests; + } +} + +decl_event! { + pub enum Event where + ::AccountId { + EntryRequested(AccountId, Role), + Staked(AccountId, Role), + Unstaked(AccountId, Role), + } +} + +impl Module { + fn is_role_available(role: Role) -> bool { + Self::available_roles().into_iter().any(|r| role == r) + } + + fn ensure_actor(role_key: &T::AccountId) -> Result, &'static str> { + Self::actor_by_account_id(role_key).ok_or("not role key") + } + + fn ensure_actor_is_member( + role_key: &T::AccountId, + member_id: MemberId, + ) -> Result, &'static str> { + let actor = Self::ensure_actor(role_key)?; + if actor.member_id == member_id { + Ok(actor) + } else { + Err("actor not owned by member") + } + } + + fn ensure_role_parameters(role: Role) -> Result, &'static str> { + Self::parameters(role).ok_or("no parameters for role") + } + + // Mutating + + fn remove_actor_from_service(actor_account: T::AccountId, role: Role, member_id: MemberId) { + let accounts: Vec = Self::account_ids_by_role(role) + .into_iter() + .filter(|account| !(*account == actor_account)) + .collect(); + >::insert(role, accounts); + + let accounts: Vec = Self::account_ids_by_member_id(&member_id) + .into_iter() + .filter(|account| !(*account == actor_account)) + .collect(); + >::insert(&member_id, accounts); + + let accounts: Vec = Self::actor_account_ids() + .into_iter() + .filter(|account| !(*account == actor_account)) + .collect(); + >::put(accounts); + + >::remove(&actor_account); + } + + fn apply_unstake( + actor_account: T::AccountId, + role: Role, + member_id: MemberId, + unbonding_period: T::BlockNumber, + ) { + // simple unstaking ...only applying unbonding period + >::insert( + &actor_account, + >::block_number() + unbonding_period, + ); + + Self::remove_actor_from_service(actor_account, role, member_id); + } +} + +impl Roles for Module { + fn is_role_account(account_id: &T::AccountId) -> bool { + >::exists(account_id) || >::exists(account_id) + } +} + +decl_module! { + pub struct Module for enum Call where origin: T::Origin { + fn deposit_event() = default; + + fn on_initialise(now: T::BlockNumber) { + // clear expired requests + if now % T::BlockNumber::sa(DEFAULT_REQUEST_CLEARING_INTERVAL) == T::BlockNumber::zero() { + let requests: Requests = Self::role_entry_requests() + .into_iter() + .filter(|request| request.3 < now) + .collect(); + + >::put(requests); + } + } + + fn on_finalise(now: T::BlockNumber) { + + // payout rewards to actors + for role in Self::available_roles().iter() { + if let Some(params) = Self::parameters(role) { + if !(now % params.reward_period == T::BlockNumber::zero()) { continue } + let accounts = Self::account_ids_by_role(role); + for actor in accounts.into_iter().map(|account| Self::actor_by_account_id(account)) { + if let Some(actor) = actor { + if now > actor.joined_at + params.reward_period { + // send reward to member account - not the actor account + if let Ok(member_account) = T::Members::lookup_account_by_member_id(actor.member_id) { + let _ = T::Currency::reward(&member_account, params.reward); + } + } + } + } + } + } + + if now % T::BlockNumber::sa(100) == T::BlockNumber::zero() { + // clear unbonded accounts + let actor_accounts: Vec = Self::actor_account_ids() + .into_iter() + .filter(|account| { + if >::exists(account) { + if Self::bondage(account) > now { + true + } else { + >::remove(account); + false + } + } else { + true + } + }) + .collect(); + >::put(actor_accounts); + } + + // eject actors not staking the minimum + // iterating over available roles, so if a role has been removed at some point + // and an actor hasn't unstaked .. this will not apply to them.. which doesn't really matter + // because they are no longer incentivised to stay in the role anyway + // TODO: this needs a bit more preparation. The right time to check for each actor is different, as they enter + // role at different times. + // for role in Self::available_roles().iter() { + // } + + } + + pub fn role_entry_request(origin, role: Role, member_id: MemberId) { + let sender = ensure_signed(origin)?; + + ensure!(T::Members::lookup_member_id(&sender).is_err(), "account is a member"); + ensure!(!Self::is_role_account(&sender), "account already used"); + + ensure!(Self::is_role_available(role), "inactive role"); + + let role_parameters = Self::ensure_role_parameters(role)?; + + // pay (burn) entry fee - spam filter + let fee = role_parameters.entry_request_fee; + ensure!(T::Currency::can_slash(&sender, fee), "cannot pay role entry request fee"); + let _ = T::Currency::slash(&sender, fee); + + >::mutate(|requests| { + let expires = >::block_number()+ T::BlockNumber::sa(REQUEST_LIFETIME); + requests.push((sender.clone(), member_id, role, expires)); + }); + Self::deposit_event(RawEvent::EntryRequested(sender, role)); + } + + /// Member activating entry request + pub fn stake(origin, role: Role, actor_account: T::AccountId) { + let sender = ensure_signed(origin)?; + let member_id = T::Members::lookup_member_id(&sender)?; + + if !Self::role_entry_requests() + .iter() + .any(|request| request.0 == actor_account && request.1 == member_id && request.2 == role) + { + return Err("no role entry request matches"); + } + + ensure!(T::Members::lookup_member_id(&actor_account).is_err(), "account is a member"); + ensure!(!Self::is_role_account(&actor_account), "account already used"); + + // make sure role is still available + ensure!(Self::is_role_available(role), "inactive role"); + let role_parameters = Self::ensure_role_parameters(role)?; + + let accounts_in_role = Self::account_ids_by_role(role); + + // ensure there is an empty slot for the role + ensure!(accounts_in_role.len() < role_parameters.max_actors as usize, "role slots full"); + + // ensure the actor account has enough balance + ensure!(T::Currency::free_balance(&actor_account) >= role_parameters.min_stake, "not enough balance to stake"); + + >::mutate(role, |accounts| accounts.push(actor_account.clone())); + >::mutate(&member_id, |accounts| accounts.push(actor_account.clone())); + >::insert(&actor_account, T::BlockNumber::max_value()); + >::insert(&actor_account, Actor { + member_id, + account: actor_account.clone(), + role, + joined_at: >::block_number() + }); + >::mutate(|accounts| accounts.push(actor_account.clone())); + + let requests: Requests = Self::role_entry_requests() + .into_iter() + .filter(|request| request.0 != actor_account) + .collect(); + >::put(requests); + + Self::deposit_event(RawEvent::Staked(actor_account, role)); + } + + pub fn unstake(origin, actor_account: T::AccountId) { + let sender = ensure_signed(origin)?; + let member_id = T::Members::lookup_member_id(&sender)?; + + let actor = Self::ensure_actor_is_member(&actor_account, member_id)?; + + let role_parameters = Self::ensure_role_parameters(actor.role)?; + + Self::apply_unstake(actor.account.clone(), actor.role, actor.member_id, role_parameters.unbonding_period); + + Self::deposit_event(RawEvent::Unstaked(actor.account, actor.role)); + } + + pub fn set_role_parameters(role: Role, params: RoleParameters) { + >::insert(role, params); + } + + pub fn set_available_roles(roles: Vec) { + >::put(roles); + } + + pub fn add_to_available_roles(role: Role) { + if !Self::available_roles().into_iter().any(|r| r == role) { + >::mutate(|roles| roles.push(role)); + } + } + + pub fn remove_from_available_roles(role: Role) { + // Should we eject actors in the role being removed? + let roles: Vec = Self::available_roles().into_iter().filter(|r| role != *r).collect(); + >::put(roles); + } + + pub fn remove_actor(actor_account: T::AccountId) { + let member_id = T::Members::lookup_member_id(&actor_account)?; + let actor = Self::ensure_actor_is_member(&actor_account, member_id)?; + let role_parameters = Self::ensure_role_parameters(actor.role)?; + Self::apply_unstake(actor.account, actor.role, actor.member_id, role_parameters.unbonding_period); + } + } +} + +impl EnsureAccountLiquid for Module { + fn ensure_account_liquid(who: &T::AccountId) -> dispatch::Result { + if Self::bondage(who) <= >::block_number() { + Ok(()) + } else { + Err("cannot transfer illiquid funds") + } + } +} diff --git a/src/roles/mock.rs b/src/roles/mock.rs new file mode 100644 index 0000000000..4dba02bb9a --- /dev/null +++ b/src/roles/mock.rs @@ -0,0 +1,136 @@ +#![cfg(test)] + +pub use super::actors; +pub use crate::governance::GovernanceCurrency; +use crate::traits::Members; +pub use system; + +pub use primitives::{Blake2Hasher, H256}; +pub use runtime_primitives::{ + testing::{Digest, DigestItem, Header, UintAuthorityId}, + traits::{BlakeTwo256, IdentityLookup, OnFinalise}, + BuildStorage, +}; + +use srml_support::impl_outer_origin; + +impl_outer_origin! { + pub enum Origin for Test {} +} + +// For testing the module, we construct most of a mock runtime. This means +// first constructing a configuration type (`Test`) which `impl`s each of the +// configuration traits of modules we want to use. +#[derive(Clone, Eq, PartialEq, Debug)] +pub struct Test; +impl system::Trait for Test { + type Origin = Origin; + type Index = u64; + type BlockNumber = u64; + type Hash = H256; + type Hashing = BlakeTwo256; + type Digest = Digest; + type AccountId = u64; + type Header = Header; + type Event = (); + type Log = DigestItem; + type Lookup = IdentityLookup; +} +impl timestamp::Trait for Test { + type Moment = u64; + type OnTimestampSet = (); +} +impl consensus::Trait for Test { + type SessionKey = UintAuthorityId; + type InherentOfflineReport = (); + type Log = DigestItem; +} + +impl balances::Trait for Test { + type Event = (); + + /// The balance of an account. + type Balance = u32; + + /// A function which is invoked when the free-balance has fallen below the existential deposit and + /// has been reduced to zero. + /// + /// Gives a chance to clean up resources associated with the given account. + type OnFreeBalanceZero = (); + + /// Handler for when a new account is created. + type OnNewAccount = (); + + /// A function that returns true iff a given account can transfer its funds to another account. + type EnsureAccountLiquid = (); +} + +impl GovernanceCurrency for Test { + type Currency = balances::Module; +} + +pub struct MockMembers {} + +impl MockMembers { + pub fn alice_id() -> u32 { + 1 + } + pub fn alice_account() -> u64 { + 1 + } + pub fn bob_id() -> u32 { + 2 + } + pub fn bob_account() -> u64 { + 2 + } +} + +impl Members for MockMembers { + type Id = u32; + fn is_active_member(who: &u64) -> bool { + if *who == Self::alice_account() { + return true; + } + if *who == Self::bob_account() { + return true; + } + false + } + fn lookup_member_id(who: &u64) -> Result { + if *who == Self::alice_account() { + return Ok(Self::alice_id()); + } + if *who == Self::bob_account() { + return Ok(Self::bob_id()); + } + Err("member not found") + } + fn lookup_account_by_member_id(id: Self::Id) -> Result { + if id == Self::alice_id() { + return Ok(Self::alice_account()); + } + if id == Self::bob_id() { + return Ok(Self::bob_account()); + } + Err("account not found") + } +} + +impl actors::Trait for Test { + type Event = (); + type Members = MockMembers; +} + +pub fn initial_test_ext() -> runtime_io::TestExternalities { + let t = system::GenesisConfig::::default() + .build_storage() + .unwrap() + .0; + + runtime_io::TestExternalities::new(t) +} + +pub type System = system::Module; +pub type Balances = balances::Module; +pub type Actors = actors::Module; diff --git a/src/roles/mod.rs b/src/roles/mod.rs new file mode 100644 index 0000000000..4ddb9c4f5d --- /dev/null +++ b/src/roles/mod.rs @@ -0,0 +1,6 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +pub mod actors; + +mod mock; +mod tests; diff --git a/src/roles/tests.rs b/src/roles/tests.rs new file mode 100644 index 0000000000..da9d630b82 --- /dev/null +++ b/src/roles/tests.rs @@ -0,0 +1,191 @@ +#![cfg(test)] + +use super::mock::*; +//use super::*; + +use runtime_io::with_externalities; +use srml_support::*; + +fn init_storage_role() { + let roles: Vec = vec![actors::Role::Storage]; + assert!(Actors::set_available_roles(roles).is_ok(), ""); +} + +fn init_storage_parmeters() -> actors::RoleParameters { + let params = actors::RoleParameters { + // minium balance required to stake to enter a role + min_stake: 100 as u32, + min_actors: 1 as u32, + max_actors: 2 as u32, + reward: 100 as u32, + reward_period: 100 as u64, + bonding_period: 100 as u64, + unbonding_period: 100 as u64, + min_service_period: 100 as u64, + startup_grace_period: 100 as u64, + entry_request_fee: 10 as u32, + }; + assert!( + Actors::set_role_parameters(actors::Role::Storage, params.clone()).is_ok(), + "" + ); + params +} + +#[test] +fn adding_roles() { + with_externalities(&mut initial_test_ext(), || { + init_storage_role(); + assert_eq!(Actors::available_roles(), vec![actors::Role::Storage]); + }); +} + +#[test] +fn adding_role_parameters() { + with_externalities(&mut initial_test_ext(), || { + init_storage_role(); + let params = init_storage_parmeters(); + assert_eq!(Actors::parameters(actors::Role::Storage), Some(params)); + }); +} + +#[test] +fn make_entry_request() { + with_externalities(&mut initial_test_ext(), || { + init_storage_role(); + let storage_params = init_storage_parmeters(); + + let actor_account = 5 as u64; + + let starting_block = 1; + System::set_block_number(starting_block); + + let requests = Actors::role_entry_requests(); + assert_eq!(requests.len(), 0); + + assert!( + Actors::role_entry_request( + Origin::signed(actor_account), + actors::Role::Storage, + MockMembers::alice_id() + ) + .is_err(), + "" + ); + + let surplus_balance = 100; + Balances::set_free_balance( + &actor_account, + storage_params.entry_request_fee + surplus_balance, + ); + + assert!( + Actors::role_entry_request( + Origin::signed(actor_account), + actors::Role::Storage, + MockMembers::alice_id() + ) + .is_ok(), + "" + ); + + assert_eq!(Balances::free_balance(&actor_account), surplus_balance); + + let requests = Actors::role_entry_requests(); + assert_eq!(requests.len(), 1); + let request = requests[0]; + assert_eq!(request.0, actor_account); + assert_eq!(request.1, MockMembers::alice_id()); + assert_eq!(request.2, actors::Role::Storage); + assert_eq!(request.3, starting_block + actors::REQUEST_LIFETIME); + }); +} + +#[test] +fn staking() { + with_externalities(&mut initial_test_ext(), || { + init_storage_role(); + let storage_params = init_storage_parmeters(); + let actor_account = 5; + + let request: actors::Request = ( + actor_account, + MockMembers::alice_id(), + actors::Role::Storage, + 1000, + ); + + >::put(vec![request]); + + Balances::set_free_balance(&actor_account, storage_params.min_stake); + + assert!(Actors::stake( + Origin::signed(MockMembers::alice_account()), + actors::Role::Storage, + actor_account + ) + .is_ok()); + + let ids = Actors::actor_account_ids(); + assert_eq!(ids, vec![actor_account]); + + let actor = Actors::actor_by_account_id(actor_account); + assert!(actor.is_some()); + + let accounts_in_role = Actors::account_ids_by_role(actors::Role::Storage); + assert_eq!(accounts_in_role, vec![actor_account]); + + let account_ids_for_member = Actors::account_ids_by_member_id(MockMembers::alice_id()); + assert_eq!(account_ids_for_member, vec![actor_account]); + + assert!(>::exists(actor_account)); + }); +} + +#[test] +fn unstaking() { + with_externalities(&mut initial_test_ext(), || { + init_storage_role(); + let storage_params = init_storage_parmeters(); + let actor_account = 5; + + assert!( + Actors::unstake(Origin::signed(MockMembers::alice_account()), actor_account).is_err() + ); + + let actor: actors::Actor = actors::Actor { + role: actors::Role::Storage, + member_id: MockMembers::alice_id(), + account: actor_account, + joined_at: 1, + }; + >::put(vec![actor_account]); + >::insert(&actor_account, actor); + >::insert(actors::Role::Storage, vec![actor_account]); + >::insert(MockMembers::alice_id(), vec![actor_account]); + >::insert(&actor_account, 10000); + let current_block = 500; + + System::set_block_number(current_block); + assert!( + Actors::unstake(Origin::signed(MockMembers::alice_account()), actor_account).is_ok() + ); + + assert_eq!(Actors::actor_account_ids().len(), 0); + + let actor = Actors::actor_by_account_id(actor_account); + assert!(actor.is_none()); + + let accounts_in_role = Actors::account_ids_by_role(actors::Role::Storage); + assert_eq!(accounts_in_role.len(), 0); + + let account_ids_for_member = Actors::account_ids_by_member_id(MockMembers::alice_id()); + assert_eq!(account_ids_for_member.len(), 0); + + assert!(>::exists(actor_account)); + assert_eq!( + Actors::bondage(actor_account), + current_block + storage_params.unbonding_period + ); + }); +} diff --git a/src/traits.rs b/src/traits.rs index d6c2a4ef47..4bea919f79 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -2,7 +2,45 @@ use crate::storage::data_object_type_registry; use system; +use parity_codec::Codec; +use srml_support::{Parameter}; +use runtime_primitives::traits::{SimpleArithmetic, As, Member, MaybeSerializeDebug}; +// Members +pub trait Members { + type Id : Parameter + Member + SimpleArithmetic + Codec + Default + Copy + + As + As + MaybeSerializeDebug + PartialEq; + + fn is_active_member(account_id: &T::AccountId) -> bool; + + fn lookup_member_id(account_id: &T::AccountId) -> Result; + + fn lookup_account_by_member_id(member_id: Self::Id) -> Result; +} + +impl Members for () { + type Id = u32; + fn is_active_member(_account_id: &T::AccountId) -> bool { + false + } + fn lookup_member_id(_account_id: &T::AccountId) -> Result { + Err("member not found") + } + fn lookup_account_by_member_id(_member_id: Self::Id) -> Result { + Err("account not found") + } +} + +// Roles +pub trait Roles { + fn is_role_account(account_id: &T::AccountId) -> bool; +} + +impl Roles for () { + fn is_role_account(_who: &T::AccountId) -> bool { false } +} + +// Data Object Types pub trait IsActiveDataObjectType { fn is_active_data_object_type(which: &T::DataObjectTypeId) -> bool { false