Browse Source

Merge pull request #188 from shamil-gadelshin/monorepo_external_modules

External joystream modules migrated to the runtime monorepo.
Mokhtar Naamani 5 years ago
parent
commit
b8722f29de
37 changed files with 10798 additions and 33 deletions
  1. 0 6
      Cargo.lock
  2. 7 13
      Cargo.toml
  3. 6 12
      runtime-modules/content-working-group/Cargo.toml
  4. 50 0
      runtime-modules/forum/Cargo.toml
  5. 1318 0
      runtime-modules/forum/src/lib.rs
  6. 530 0
      runtime-modules/forum/src/mock.rs
  7. 981 0
      runtime-modules/forum/src/tests.rs
  8. 1 2
      runtime-modules/hiring/Cargo.toml
  9. 56 0
      runtime-modules/recurring-reward/Cargo.toml
  10. 397 0
      runtime-modules/recurring-reward/src/lib.rs
  11. 103 0
      runtime-modules/recurring-reward/src/mock/mod.rs
  12. 64 0
      runtime-modules/recurring-reward/src/mock/status_handler.rs
  13. 259 0
      runtime-modules/recurring-reward/src/tests.rs
  14. 50 0
      runtime-modules/stake/Cargo.toml
  15. 158 0
      runtime-modules/stake/src/errors.rs
  16. 1083 0
      runtime-modules/stake/src/lib.rs
  17. 19 0
      runtime-modules/stake/src/macroes.rs
  18. 115 0
      runtime-modules/stake/src/mock.rs
  19. 804 0
      runtime-modules/stake/src/tests.rs
  20. 50 0
      runtime-modules/token-minting/Cargo.toml
  21. 260 0
      runtime-modules/token-minting/src/lib.rs
  22. 153 0
      runtime-modules/token-minting/src/mint.rs
  23. 90 0
      runtime-modules/token-minting/src/mock.rs
  24. 202 0
      runtime-modules/token-minting/src/tests.rs
  25. 44 0
      runtime-modules/versioned-store-permissions/Cargo.toml
  26. 28 0
      runtime-modules/versioned-store-permissions/src/constraint.rs
  27. 57 0
      runtime-modules/versioned-store-permissions/src/credentials.rs
  28. 646 0
      runtime-modules/versioned-store-permissions/src/lib.rs
  29. 164 0
      runtime-modules/versioned-store-permissions/src/mock.rs
  30. 135 0
      runtime-modules/versioned-store-permissions/src/operations.rs
  31. 154 0
      runtime-modules/versioned-store-permissions/src/permissions.rs
  32. 665 0
      runtime-modules/versioned-store-permissions/src/tests.rs
  33. 50 0
      runtime-modules/versioned-store/Cargo.toml
  34. 528 0
      runtime-modules/versioned-store/src/example.rs
  35. 809 0
      runtime-modules/versioned-store/src/lib.rs
  36. 259 0
      runtime-modules/versioned-store/src/mock.rs
  37. 503 0
      runtime-modules/versioned-store/src/tests.rs

+ 0 - 6
Cargo.lock

@@ -3370,7 +3370,6 @@ dependencies = [
 [[package]]
 name = "substrate-forum-module"
 version = "1.1.1"
-source = "git+https://github.com/joystream/substrate-forum-module?tag=v1.1.1#5918fc90d25faeac06311b0d6b05305cbe722a27"
 dependencies = [
  "hex-literal 0.1.4",
  "parity-scale-codec",
@@ -3564,7 +3563,6 @@ dependencies = [
 [[package]]
 name = "substrate-recurring-reward-module"
 version = "1.0.1"
-source = "git+https://github.com/Joystream/substrate-recurring-reward-module?tag=v1.0.1#2c4bda1dea315629313643737c2f59979579fb50"
 dependencies = [
  "hex-literal 0.1.4",
  "parity-scale-codec",
@@ -3633,7 +3631,6 @@ dependencies = [
 [[package]]
 name = "substrate-stake-module"
 version = "1.0.1"
-source = "git+https://github.com/Joystream/substrate-stake-module/?tag=v1.0.1#af5860c3cde5b11e37728df0b1dfbbdf9a9fa2f3"
 dependencies = [
  "hex-literal 0.1.4",
  "parity-scale-codec",
@@ -3711,7 +3708,6 @@ dependencies = [
 [[package]]
 name = "substrate-token-mint-module"
 version = "1.0.1"
-source = "git+https://github.com/joystream/substrate-token-minting-module/?tag=v1.0.1#7905ce50136cf8483a808a1946fbf123b9ca4bb8"
 dependencies = [
  "hex-literal 0.1.4",
  "parity-scale-codec",
@@ -3745,7 +3741,6 @@ dependencies = [
 [[package]]
 name = "substrate-versioned-store"
 version = "1.0.1"
-source = "git+https://github.com/joystream/substrate-versioned-store-module?tag=v1.0.1#24bcd60e84c1ece74a8a3130beb740f6fa760145"
 dependencies = [
  "hex-literal 0.1.4",
  "parity-scale-codec",
@@ -3765,7 +3760,6 @@ dependencies = [
 [[package]]
 name = "substrate-versioned-store-permissions-module"
 version = "1.0.1"
-source = "git+https://github.com/joystream/substrate-versioned-store-permissions-module?tag=v1.0.1#dd75f4bfe283673685c4ccf9de14384a546daa6e"
 dependencies = [
  "hex-literal 0.1.4",
  "parity-scale-codec",

+ 7 - 13
Cargo.toml

@@ -1,5 +1,5 @@
 [package]
-authors = ['Joystream']
+authors = ['Joystream contributors']
 edition = '2018'
 name = 'joystream-node-runtime'
 # Follow convention: https://github.com/Joystream/substrate-runtime-joystream/issues/1
@@ -269,26 +269,22 @@ version = '1.0.4'
 [dependencies.forum]
 default_features = false
 package = 'substrate-forum-module'
-git = 'https://github.com/joystream/substrate-forum-module'
-tag = 'v1.1.1'
+path = 'runtime-modules/forum'
 
 [dependencies.minting]
 default_features = false
 package = 'substrate-token-mint-module'
-git = 'https://github.com/joystream/substrate-token-minting-module/'
-tag = 'v1.0.1'
+path = 'runtime-modules/token-minting'
 
 [dependencies.stake]
 default_features = false
 package = 'substrate-stake-module'
-git = 'https://github.com/Joystream/substrate-stake-module/'
-tag = 'v1.0.1'
+path = 'runtime-modules/stake'
 
 [dependencies.recurringrewards]
 default_features = false
 package = 'substrate-recurring-reward-module'
-git = 'https://github.com/Joystream/substrate-recurring-reward-module'
-tag = 'v1.0.1'
+path = 'runtime-modules/recurring-reward'
 
 [dependencies.hiring]
 default_features = false
@@ -298,14 +294,12 @@ path = 'runtime-modules/hiring'
 [dependencies.versioned_store]
 default_features = false
 package ='substrate-versioned-store'
-git = 'https://github.com/joystream/substrate-versioned-store-module'
-tag = 'v1.0.1'
+path = 'runtime-modules/versioned-store'
 
 [dependencies.versioned_store_permissions]
 default_features = false
 package = 'substrate-versioned-store-permissions-module'
-git = 'https://github.com/joystream/substrate-versioned-store-permissions-module'
-tag = 'v1.0.1'
+path = 'runtime-modules/versioned-store-permissions'
 
 [dependencies.common]
 default_features = false

+ 6 - 12
runtime-modules/content-working-group/Cargo.toml

@@ -69,26 +69,22 @@ version = '1.0.0'
 [dependencies.forum]
 default_features = false
 package = 'substrate-forum-module'
-git = 'https://github.com/joystream/substrate-forum-module'
-tag = 'v1.1.1'
+path = '../forum'
 
 [dependencies.minting]
 default_features = false
 package = 'substrate-token-mint-module'
-git = 'https://github.com/joystream/substrate-token-minting-module/'
-tag = 'v1.0.1'
+path = '../token-minting'
 
 [dependencies.stake]
 default_features = false
 package = 'substrate-stake-module'
-git = 'https://github.com/Joystream/substrate-stake-module/'
-tag = 'v1.0.1'
+path = '../stake'
 
 [dependencies.recurringrewards]
 default_features = false
 package = 'substrate-recurring-reward-module'
-git = 'https://github.com/Joystream/substrate-recurring-reward-module'
-tag = 'v1.0.1'
+path = '../recurring-reward'
 
 [dependencies.hiring]
 default_features = false
@@ -98,14 +94,12 @@ path = '../hiring'
 [dependencies.versioned_store]
 default_features = false
 package ='substrate-versioned-store'
-git = 'https://github.com/joystream/substrate-versioned-store-module'
-tag = 'v1.0.1'
+path = '../versioned-store'
 
 [dependencies.versioned_store_permissions]
 default_features = false
 package = 'substrate-versioned-store-permissions-module'
-git = 'https://github.com/joystream/substrate-versioned-store-permissions-module'
-tag = 'v1.0.1'
+path = '../versioned-store-permissions'
 
 [dependencies.membership]
 default_features = false

+ 50 - 0
runtime-modules/forum/Cargo.toml

@@ -0,0 +1,50 @@
+[package]
+name = 'substrate-forum-module'
+version = '1.1.1'
+authors = ['Joystream contributors']
+edition = '2018'
+
+[dependencies]
+hex-literal = '0.1.0'
+serde = { version = '1.0.101', optional = true}
+serde_derive = { version = '1.0.101', optional = true }
+rstd = { package = 'sr-std', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+runtime-primitives = { package = 'sr-primitives', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+srml-support = { package = 'srml-support', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+srml-support-procedural = { package = 'srml-support-procedural', git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+system = { package = 'srml-system', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+balances = { package = 'srml-balances', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+codec = { package = 'parity-scale-codec', version = '1.0.0', default-features = false, features = ['derive'] }
+# https://users.rust-lang.org/t/failure-derive-compilation-error/39062
+quote = '<=1.0.2'
+
+[dependencies.timestamp]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'srml-timestamp'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dependencies.runtime-io]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'sr-io'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dev-dependencies]
+runtime-io = { package = 'sr-io', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+primitives = { package = 'substrate-primitives', git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+
+[features]
+default = ['std']
+std = [
+	'serde',
+	'serde_derive',
+	'codec/std',
+	'rstd/std',
+	'runtime-io/std',
+	'runtime-primitives/std',
+	'srml-support/std',
+	'system/std',
+  	'balances/std',
+	'timestamp/std',
+]

+ 1318 - 0
runtime-modules/forum/src/lib.rs

@@ -0,0 +1,1318 @@
+// Copyright 2017-2019 Parity Technologies (UK) Ltd.
+
+// This is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Substrate.  If not, see <http://www.gnu.org/licenses/>.
+
+// Copyright 2019 Joystream Contributors
+
+//! # Runtime Example Module
+//!
+//! <!-- Original author of paragraph: @gavofyork -->
+//! The Example: A simple example of a runtime module demonstrating
+//! concepts, APIs and structures common to most runtime modules.
+//!
+//! Run `cargo doc --package runtime-example-module --open` to view this module's documentation.
+//!
+//! ### Documentation Template:<br>
+//! Add heading with custom module name
+//!
+//! # <INSERT_CUSTOM_MODULE_NAME> Module
+//!
+//! Add simple description
+//!
+//! Include the following links that shows what trait needs to be implemented to use the module
+//! and the supported dispatchables that are documented in the Call enum.
+//!
+//! - [`<INSERT_CUSTOM_MODULE_NAME>::Trait`](./trait.Trait.html)
+//! - [`Call`](./enum.Call.html)
+//! - [`Module`](./struct.Module.html)
+//!
+//! ## Overview
+//!
+//! <!-- Original author of paragraph: Various. See https://github.com/paritytech/substrate-developer-hub/issues/44 -->
+//! Short description of module purpose.
+//! Links to Traits that should be implemented.
+//! What this module is for.
+//! What functionality the module provides.
+//! When to use the module (use case examples).
+//! How it is used.
+//! Inputs it uses and the source of each input.
+//! Outputs it produces.
+//!
+//! <!-- Original author of paragraph: @Kianenigma in PR https://github.com/paritytech/substrate/pull/1951 -->
+//! <!-- and comment https://github.com/paritytech/substrate-developer-hub/issues/44#issuecomment-471982710 -->
+//!
+//! ## Terminology
+//!
+//! Add terminology used in the custom module. Include concepts, storage items, or actions that you think
+//! deserve to be noted to give context to the rest of the documentation or module usage. The author needs to
+//! use some judgment about what is included. We don't want a list of every storage item nor types - the user
+//! can go to the code for that. For example, "transfer fee" is obvious and should not be included, but
+//! "free balance" and "reserved balance" should be noted to give context to the module.
+//! Please do not link to outside resources. The reference docs should be the ultimate source of truth.
+//!
+//! <!-- Original author of heading: @Kianenigma in PR https://github.com/paritytech/substrate/pull/1951 -->
+//!
+//! ## Goals
+//!
+//! Add goals that the custom module is designed to achieve.
+//!
+//! <!-- Original author of heading: @Kianenigma in PR https://github.com/paritytech/substrate/pull/1951 -->
+//!
+//! ### Scenarios
+//!
+//! <!-- Original author of paragraph: @Kianenigma. Based on PR https://github.com/paritytech/substrate/pull/1951 -->
+//!
+//! #### <INSERT_SCENARIO_NAME>
+//!
+//! Describe requirements prior to interacting with the custom module.
+//! Describe the process of interacting with the custom module for this scenario and public API functions used.
+//!
+//! ## Interface
+//!
+//! ### Supported Origins
+//!
+//! What origins are used and supported in this module (root, signed, inherent)
+//! i.e. root when `ensure_root` used
+//! i.e. inherent when `ensure_inherent` used
+//! i.e. signed when `ensure_signed` used
+//!
+//! `inherent` <INSERT_DESCRIPTION>
+//!
+//! <!-- Original author of paragraph: @Kianenigma in comment -->
+//! <!-- https://github.com/paritytech/substrate-developer-hub/issues/44#issuecomment-471982710 -->
+//!
+//! ### Types
+//!
+//! Type aliases. Include any associated types and where the user would typically define them.
+//!
+//! `ExampleType` <INSERT_DESCRIPTION>
+//!
+//! <!-- Original author of paragraph: ??? -->
+//!
+//!
+//! ### Dispatchable Functions
+//!
+//! <!-- Original author of paragraph: @AmarRSingh & @joepetrowski -->
+//!
+//! // A brief description of dispatchable functions and a link to the rustdoc with their actual documentation.
+//!
+//! <b>MUST</b> have link to Call enum
+//! <b>MUST</b> have origin information included in function doc
+//! <b>CAN</b> have more info up to the user
+//!
+//! ### Public Functions
+//!
+//! <!-- Original author of paragraph: @joepetrowski -->
+//!
+//! A link to the rustdoc and any notes about usage in the module, not for specific functions.
+//! For example, in the balances module: "Note that when using the publicly exposed functions,
+//! you (the runtime developer) are responsible for implementing any necessary checks
+//! (e.g. that the sender is the signer) before calling a function that will affect storage."
+//!
+//! <!-- Original author of paragraph: @AmarRSingh -->
+//!
+//! It is up to the writer of the respective module (with respect to how much information to provide).
+//!
+//! #### Public Inspection functions - Immutable (getters)
+//!
+//! Insert a subheading for each getter function signature
+//!
+//! ##### `example_getter_name()`
+//!
+//! What it returns
+//! Why, when, and how often to call it
+//! When it could panic or error
+//! When safety issues to consider
+//!
+//! #### Public Mutable functions (changing state)
+//!
+//! Insert a subheading for each setter function signature
+//!
+//! ##### `example_setter_name(origin, parameter_name: T::ExampleType)`
+//!
+//! What state it changes
+//! Why, when, and how often to call it
+//! When it could panic or error
+//! When safety issues to consider
+//! What parameter values are valid and why
+//!
+//! ### Storage Items
+//!
+//! Explain any storage items included in this module
+//!
+//! ### Digest Items
+//!
+//! Explain any digest items included in this module
+//!
+//! ### Inherent Data
+//!
+//! Explain what inherent data (if any) is defined in the module and any other related types
+//!
+//! ### Events:
+//!
+//! Insert events for this module if any
+//!
+//! ### Errors:
+//!
+//! Explain what generates errors
+//!
+//! ## Usage
+//!
+//! Insert 2-3 examples of usage and code snippets that show how to use <INSERT_CUSTOM_MODULE_NAME> module in a custom module.
+//!
+//! ### Prerequisites
+//!
+//! Show how to include necessary imports for <INSERT_CUSTOM_MODULE_NAME> and derive
+//! your module configuration trait with the `INSERT_CUSTOM_MODULE_NAME` trait.
+//!
+//! ```rust
+//! // use <INSERT_CUSTOM_MODULE_NAME>;
+//!
+//! // pub trait Trait: <INSERT_CUSTOM_MODULE_NAME>::Trait { }
+//! ```
+//!
+//! ### Simple Code Snippet
+//!
+//! Show a simple example (e.g. how to query a public getter function of <INSERT_CUSTOM_MODULE_NAME>)
+//!
+//! ## Genesis Config
+//!
+//! <!-- Original author of paragraph: @joepetrowski -->
+//!
+//! ## Dependencies
+//!
+//! Dependencies on other SRML modules and the genesis config should be mentioned,
+//! but not the Rust Standard Library.
+//! Genesis configuration modifications that may be made to incorporate this module
+//! Interaction with other modules
+//!
+//! <!-- Original author of heading: @AmarRSingh -->
+//!
+//! ## Related Modules
+//!
+//! Interaction with other modules in the form of a bullet point list
+//!
+//! ## References
+//!
+//! <!-- Original author of paragraph: @joepetrowski -->
+//!
+//! Links to reference material, if applicable. For example, Phragmen, W3F research, etc.
+//! that the implementation is based on.
+
+// Ensure we're `no_std` when compiling for Wasm.
+#![cfg_attr(not(feature = "std"), no_std)]
+
+#[cfg(feature = "std")]
+use serde_derive::{Deserialize, Serialize};
+
+use rstd::prelude::*;
+
+use codec::{Decode, Encode};
+use srml_support::{decl_event, decl_module, decl_storage, dispatch, ensure};
+
+mod mock;
+mod tests;
+
+/*
+ * MOVE ALL OF THESE OUT TO COMMON LATER
+ */
+
+/// Length constraint for input validation
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))]
+#[derive(Encode, Decode, Default, Clone, PartialEq, Eq)]
+pub struct InputValidationLengthConstraint {
+    /// Minimum length
+    pub min: u16,
+
+    /// Difference between minimum length and max length.
+    /// While having max would have been more direct, this
+    /// way makes max < min unrepresentable semantically,
+    /// which is safer.
+    pub max_min_diff: u16,
+}
+
+impl InputValidationLengthConstraint {
+    /// Helper for computing max
+    pub fn max(&self) -> u16 {
+        self.min + self.max_min_diff
+    }
+
+    pub fn ensure_valid(
+        &self,
+        len: usize,
+        too_short_msg: &'static str,
+        too_long_msg: &'static str,
+    ) -> Result<(), &'static str> {
+        let length = len as u16;
+        if length < self.min {
+            Err(too_short_msg)
+        } else if length > self.max() {
+            Err(too_long_msg)
+        } else {
+            Ok(())
+        }
+    }
+}
+
+/// Constants
+/////////////////////////////////////////////////////////////////
+
+/// The greatest valid depth of a category.
+/// The depth of a root category is 0.
+const MAX_CATEGORY_DEPTH: u16 = 3;
+
+/// Error messages for dispatchables
+const ERROR_FORUM_SUDO_NOT_SET: &str = "Forum sudo not set.";
+const ERROR_ORIGIN_NOT_FORUM_SUDO: &str = "Origin not forum sudo.";
+const ERROR_CATEGORY_TITLE_TOO_SHORT: &str = "Category title too short.";
+const ERROR_CATEGORY_TITLE_TOO_LONG: &str = "Category title too long.";
+const ERROR_CATEGORY_DESCRIPTION_TOO_SHORT: &str = "Category description too long.";
+const ERROR_CATEGORY_DESCRIPTION_TOO_LONG: &str = "Category description too long.";
+const ERROR_ANCESTOR_CATEGORY_IMMUTABLE: &str =
+    "Ancestor category immutable, i.e. deleted or archived";
+const ERROR_MAX_VALID_CATEGORY_DEPTH_EXCEEDED: &str = "Maximum valid category depth exceeded.";
+const ERROR_CATEGORY_DOES_NOT_EXIST: &str = "Category does not exist.";
+const ERROR_NOT_FORUM_USER: &str = "Not forum user.";
+const ERROR_THREAD_TITLE_TOO_SHORT: &str = "Thread title too short.";
+const ERROR_THREAD_TITLE_TOO_LONG: &str = "Thread title too long.";
+const ERROR_POST_TEXT_TOO_SHORT: &str = "Post text too short.";
+const ERROR_POST_TEXT_TOO_LONG: &str = "Post too long.";
+const ERROR_THREAD_DOES_NOT_EXIST: &str = "Thread does not exist";
+const ERROR_THREAD_MODERATION_RATIONALE_TOO_SHORT: &str = "Thread moderation rationale too short.";
+const ERROR_THREAD_MODERATION_RATIONALE_TOO_LONG: &str = "Thread moderation rationale too long.";
+const ERROR_THREAD_ALREADY_MODERATED: &str = "Thread already moderated.";
+const ERROR_THREAD_MODERATED: &str = "Thread is moderated.";
+const ERROR_POST_DOES_NOT_EXIST: &str = "Post does not exist.";
+const ERROR_ACCOUNT_DOES_NOT_MATCH_POST_AUTHOR: &str = "Account does not match post author.";
+const ERROR_POST_MODERATED: &str = "Post is moderated.";
+const ERROR_POST_MODERATION_RATIONALE_TOO_SHORT: &str = "Post moderation rationale too short.";
+const ERROR_POST_MODERATION_RATIONALE_TOO_LONG: &str = "Post moderation rationale too long.";
+const ERROR_CATEGORY_NOT_BEING_UPDATED: &str = "Category not being updated.";
+const ERROR_CATEGORY_CANNOT_BE_UNARCHIVED_WHEN_DELETED: &str =
+    "Category cannot be unarchived when deleted.";
+
+//use srml_support::storage::*;
+
+//use sr_io::{StorageOverlay, ChildrenStorageOverlay};
+
+//#[cfg(feature = "std")]
+//use runtime_io::{StorageOverlay, ChildrenStorageOverlay};
+
+//#[cfg(any(feature = "std", test))]
+//use sr_primitives::{StorageOverlay, ChildrenStorageOverlay};
+
+use system;
+use system::{ensure_root, ensure_signed};
+
+/// Represents a user in this forum.
+#[derive(Debug, Copy, Clone)]
+pub struct ForumUser<AccountId> {
+    /// Identifier of user
+    pub id: AccountId, // In the future one could add things like
+                       // - updating post count of a user
+                       // - updating status (e.g. hero, new, etc.)
+                       //
+}
+
+/// Represents a regsitry of `ForumUser` instances.
+pub trait ForumUserRegistry<AccountId> {
+    fn get_forum_user(id: &AccountId) -> Option<ForumUser<AccountId>>;
+}
+
+/// Convenient composite time stamp
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))]
+#[derive(Encode, Decode, Default, Clone, PartialEq, Eq)]
+pub struct BlockchainTimestamp<BlockNumber, Moment> {
+    block: BlockNumber,
+    time: Moment,
+}
+
+/// Represents a moderation outcome applied to a post or a thread.
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))]
+#[derive(Encode, Decode, Default, Clone, PartialEq, Eq)]
+pub struct ModerationAction<BlockNumber, Moment, AccountId> {
+    /// When action occured.
+    moderated_at: BlockchainTimestamp<BlockNumber, Moment>,
+
+    /// Account forum sudo which acted.
+    moderator_id: AccountId,
+
+    /// Moderation rationale
+    rationale: Vec<u8>,
+}
+
+/// Represents a revision of the text of a Post
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))]
+#[derive(Encode, Decode, Default, Clone, PartialEq, Eq)]
+pub struct PostTextChange<BlockNumber, Moment> {
+    /// When this expiration occured
+    expired_at: BlockchainTimestamp<BlockNumber, Moment>,
+
+    /// Text that expired
+    text: Vec<u8>,
+}
+
+/// Represents a post identifier
+pub type PostId = u64;
+
+/// Represents a thread post
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))]
+#[derive(Encode, Decode, Default, Clone, PartialEq, Eq)]
+pub struct Post<BlockNumber, Moment, AccountId> {
+    /// Post identifier
+    id: PostId,
+
+    /// Id of thread to which this post corresponds.
+    thread_id: ThreadId,
+
+    /// The post number of this post in its thread, i.e. total number of posts added (including this)
+    /// to a thread when it was added.
+    /// Is needed to give light clients assurance about getting all posts in a given range,
+    // `created_at` is not sufficient.
+    /// Starts at 1 for first post in thread.
+    nr_in_thread: u32,
+
+    /// Current text of post
+    current_text: Vec<u8>,
+
+    /// Possible moderation of this post
+    moderation: Option<ModerationAction<BlockNumber, Moment, AccountId>>,
+
+    /// Edits of post ordered chronologically by edit time.
+    text_change_history: Vec<PostTextChange<BlockNumber, Moment>>,
+
+    /// When post was submitted.
+    created_at: BlockchainTimestamp<BlockNumber, Moment>,
+
+    /// Author of post.
+    author_id: AccountId,
+}
+
+/// Represents a thread identifier
+pub type ThreadId = u64;
+
+/// Represents a thread
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))]
+#[derive(Encode, Decode, Default, Clone, PartialEq, Eq)]
+pub struct Thread<BlockNumber, Moment, AccountId> {
+    /// Thread identifier
+    id: ThreadId,
+
+    /// Title
+    title: Vec<u8>,
+
+    /// Category in which this thread lives
+    category_id: CategoryId,
+
+    /// The thread number of this thread in its category, i.e. total number of thread added (including this)
+    /// to a category when it was added.
+    /// Is needed to give light clients assurance about getting all threads in a given range,
+    /// `created_at` is not sufficient.
+    /// Starts at 1 for first thread in category.
+    nr_in_category: u32,
+
+    /// Possible moderation of this thread
+    moderation: Option<ModerationAction<BlockNumber, Moment, AccountId>>,
+
+    /// Number of unmoderated and moderated posts in this thread.
+    /// The sum of these two only increases, and former is incremented
+    /// for each new post added to this thread. A new post is added
+    /// with a `nr_in_thread` equal to this sum
+    ///
+    /// When there is a moderation
+    /// of a post, the variables are incremented and decremented, respectively.
+    ///
+    /// These values are vital for light clients, in order to validate that they are
+    /// not being censored from posts in a thread.
+    num_unmoderated_posts: u32,
+    num_moderated_posts: u32,
+
+    /// When thread was established.
+    created_at: BlockchainTimestamp<BlockNumber, Moment>,
+
+    /// Author of post.
+    author_id: AccountId,
+}
+
+impl<BlockNumber, Moment, AccountId> Thread<BlockNumber, Moment, AccountId> {
+    fn num_posts_ever_created(&self) -> u32 {
+        self.num_unmoderated_posts + self.num_moderated_posts
+    }
+}
+
+/// Represents a category identifier
+pub type CategoryId = u64;
+
+/// Represents
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))]
+#[derive(Encode, Decode, Default, Clone, PartialEq, Eq)]
+pub struct ChildPositionInParentCategory {
+    /// Id of parent category
+    parent_id: CategoryId,
+
+    /// Nr of the child in the parent
+    /// Starts at 1
+    child_nr_in_parent_category: u32,
+}
+
+/// Represents a category
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))]
+#[derive(Encode, Decode, Default, Clone, PartialEq, Eq)]
+pub struct Category<BlockNumber, Moment, AccountId> {
+    /// Category identifier
+    id: CategoryId,
+
+    /// Title
+    title: Vec<u8>,
+
+    /// Description
+    description: Vec<u8>,
+
+    /// When category was established.
+    created_at: BlockchainTimestamp<BlockNumber, Moment>,
+
+    /// Whether category is deleted.
+    deleted: bool,
+
+    /// Whether category is archived.
+    archived: bool,
+
+    /// Number of subcategories (deleted, archived or neither),
+    /// unmoderated threads and moderated threads, _directly_ in this category.
+    ///
+    /// As noted, the first is unaffected by any change in state of direct subcategory.
+    ///
+    /// The sum of the latter two only increases, and former is incremented
+    /// for each new thread added to this category. A new thread is added
+    /// with a `nr_in_category` equal to this sum.
+    ///
+    /// When there is a moderation
+    /// of a thread, the variables are incremented and decremented, respectively.
+    ///
+    /// These values are vital for light clients, in order to validate that they are
+    /// not being censored from subcategories or threads in a category.
+    num_direct_subcategories: u32,
+    num_direct_unmoderated_threads: u32,
+    num_direct_moderated_threads: u32,
+
+    /// Position as child in parent, if present, otherwise this category is a root category
+    position_in_parent_category: Option<ChildPositionInParentCategory>,
+
+    /// Account of the moderator which created category.
+    moderator_id: AccountId,
+}
+
+impl<BlockNumber, Moment, AccountId> Category<BlockNumber, Moment, AccountId> {
+    fn num_threads_created(&self) -> u32 {
+        self.num_direct_unmoderated_threads + self.num_direct_moderated_threads
+    }
+}
+
+/// Represents a sequence of categories which have child-parent relatioonship
+/// where last element is final ancestor, or root, in the context of the category tree.
+type CategoryTreePath<BlockNumber, Moment, AccountId> =
+    Vec<Category<BlockNumber, Moment, AccountId>>;
+
+pub trait Trait: system::Trait + timestamp::Trait + Sized {
+    type Event: From<Event<Self>> + Into<<Self as system::Trait>::Event>;
+
+    type MembershipRegistry: ForumUserRegistry<Self::AccountId>;
+}
+
+decl_storage! {
+    trait Store for Module<T: Trait> as Forum {
+
+        /// Map category identifier to corresponding category.
+        pub CategoryById get(category_by_id) config(): map CategoryId => Category<T::BlockNumber, T::Moment, T::AccountId>;
+
+        /// Category identifier value to be used for the next Category created.
+        pub NextCategoryId get(next_category_id) config(): CategoryId;
+
+        /// Map thread identifier to corresponding thread.
+        pub ThreadById get(thread_by_id) config(): map ThreadId => Thread<T::BlockNumber, T::Moment, T::AccountId>;
+
+        /// Thread identifier value to be used for next Thread in threadById.
+        pub NextThreadId get(next_thread_id) config(): ThreadId;
+
+        /// Map post identifier to corresponding post.
+        pub PostById get(post_by_id) config(): map PostId => Post<T::BlockNumber, T::Moment, T::AccountId>;
+
+        /// Post identifier value to be used for for next post created.
+        pub NextPostId get(next_post_id) config(): PostId;
+
+        /// Account of forum sudo.
+        pub ForumSudo get(forum_sudo) config(): Option<T::AccountId>;
+
+        /// Input constraints
+        /// These are all forward looking, that is they are enforced on all
+        /// future calls.
+        pub CategoryTitleConstraint get(category_title_constraint) config(): InputValidationLengthConstraint;
+        pub CategoryDescriptionConstraint get(category_description_constraint) config(): InputValidationLengthConstraint;
+        pub ThreadTitleConstraint get(thread_title_constraint) config(): InputValidationLengthConstraint;
+        pub PostTextConstraint get(post_text_constraint) config(): InputValidationLengthConstraint;
+        pub ThreadModerationRationaleConstraint get(thread_moderation_rationale_constraint) config(): InputValidationLengthConstraint;
+        pub PostModerationRationaleConstraint get(post_moderation_rationale_constraint) config(): InputValidationLengthConstraint;
+    }
+    /*
+    JUST GIVING UP ON ALL THIS FOR NOW BECAUSE ITS TAKING TOO LONG
+    Review : https://github.com/paritytech/polkadot/blob/620b8610431e7b5fdd71ce3e94c3ee0177406dcc/runtime/src/parachains.rs#L123-L141
+
+    add_extra_genesis {
+
+        // Explain why we need to put this here.
+        config(initial_forum_sudo) : Option<T::AccountId>;
+
+        build(|
+            storage: &mut generator::StorageOverlay,
+            _: &mut generator::ChildrenStorageOverlay,
+            config: &GenesisConfig<T>
+            | {
+
+
+            if let Some(account_id) = &config.initial_forum_sudo {
+                println!("{}: <ForumSudo<T>>::put(account_id)", account_id);
+                <ForumSudo<T> as generator::StorageValue<_>>::put(&account_id, storage);
+            }
+        })
+    }
+    */
+}
+
+decl_event!(
+    pub enum Event<T>
+    where
+        <T as system::Trait>::AccountId,
+    {
+        /// A category was introduced
+        CategoryCreated(CategoryId),
+
+        /// A category with given id was updated.
+        /// The second argument reflects the new archival status of the category, if changed.
+        /// The third argument reflects the new deletion status of the category, if changed.
+        CategoryUpdated(CategoryId, Option<bool>, Option<bool>),
+
+        /// A thread with given id was created.
+        ThreadCreated(ThreadId),
+
+        /// A thread with given id was moderated.
+        ThreadModerated(ThreadId),
+
+        /// Post with given id was created.
+        PostAdded(PostId),
+
+        /// Post with givne id was moderated.
+        PostModerated(PostId),
+
+        /// Post with given id had its text updated.
+        /// The second argument reflects the number of total edits when the text update occurs.
+        PostTextUpdated(PostId, u64),
+
+        /// Given account was set as forum sudo.
+        ForumSudoSet(Option<AccountId>, Option<AccountId>),
+    }
+);
+
+decl_module! {
+    pub struct Module<T: Trait> for enum Call where origin: T::Origin {
+
+        fn deposit_event() = default;
+
+        /// Set forum sudo.
+        fn set_forum_sudo(origin, new_forum_sudo: Option<T::AccountId>) -> dispatch::Result {
+            ensure_root(origin)?;
+
+            /*
+             * Question: when this routine is called by non sudo or with bad signature, what error is raised?
+             * Update ERror set in spec
+             */
+
+            // Hold on to old value
+            let old_forum_sudo = <ForumSudo<T>>::get().clone();
+
+            // Update forum sudo
+            match new_forum_sudo.clone() {
+                Some(account_id) => <ForumSudo<T>>::put(account_id),
+                None => <ForumSudo<T>>::kill()
+            };
+
+            // Generate event
+            Self::deposit_event(RawEvent::ForumSudoSet(old_forum_sudo, new_forum_sudo));
+
+            // All good.
+            Ok(())
+        }
+
+        /// Add a new category.
+        fn create_category(origin, parent: Option<CategoryId>, title: Vec<u8>, description: Vec<u8>) -> dispatch::Result {
+
+            // Check that its a valid signature
+            let who = ensure_signed(origin)?;
+
+            // Not signed by forum SUDO
+            Self::ensure_is_forum_sudo(&who)?;
+
+            // Validate title
+            Self::ensure_category_title_is_valid(&title)?;
+
+            // Validate description
+            Self::ensure_category_description_is_valid(&description)?;
+
+            // Position in parent field value for new category
+            let mut position_in_parent_category_field = None;
+
+            // If not root, then check that we can create in parent category
+            if let Some(parent_category_id) = parent {
+
+                let category_tree_path = Self::ensure_valid_category_and_build_category_tree_path(parent_category_id)?;
+
+                // Can we mutate in this category?
+                Self::ensure_can_add_subcategory_path_leaf(&category_tree_path)?;
+
+                /*
+                 * Here we are safe to mutate
+                 */
+
+                // Increment number of subcategories to reflect this new category being
+                // added as a child
+                <CategoryById<T>>::mutate(parent_category_id, |c| {
+                    c.num_direct_subcategories += 1;
+                });
+
+                // Set `position_in_parent_category_field`
+                let parent_category = category_tree_path.first().unwrap();
+
+                position_in_parent_category_field = Some(ChildPositionInParentCategory{
+                    parent_id: parent_category_id,
+                    child_nr_in_parent_category: parent_category.num_direct_subcategories
+                });
+
+            }
+
+            /*
+             * Here we are safe to mutate
+             */
+
+            let next_category_id = NextCategoryId::get();
+
+            // Create new category
+            let new_category = Category {
+                id : next_category_id,
+                title : title.clone(),
+                description: description.clone(),
+                created_at : Self::current_block_and_time(),
+                deleted: false,
+                archived: false,
+                num_direct_subcategories: 0,
+                num_direct_unmoderated_threads: 0,
+                num_direct_moderated_threads: 0,
+                position_in_parent_category: position_in_parent_category_field,
+                moderator_id: who
+            };
+
+            // Insert category in map
+            <CategoryById<T>>::insert(new_category.id, new_category);
+
+            // Update other things
+            NextCategoryId::put(next_category_id + 1);
+
+            // Generate event
+            Self::deposit_event(RawEvent::CategoryCreated(next_category_id));
+
+            Ok(())
+        }
+
+        /// Update category
+        fn update_category(origin, category_id: CategoryId, new_archival_status: Option<bool>, new_deletion_status: Option<bool>) -> dispatch::Result {
+
+            // Check that its a valid signature
+            let who = ensure_signed(origin)?;
+
+            // Not signed by forum SUDO
+            Self::ensure_is_forum_sudo(&who)?;
+
+            // Make sure something is actually being changed
+            ensure!(
+                new_archival_status.is_some() || new_deletion_status.is_some(),
+                ERROR_CATEGORY_NOT_BEING_UPDATED
+            );
+
+            // Get path from parent to root of category tree.
+            let category_tree_path = Self::ensure_valid_category_and_build_category_tree_path(category_id)?;
+
+            // When we are dealing with a non-root category, we
+            // must ensure mutability of our category by traversing to
+            // root.
+            if category_tree_path.len() > 1  {
+
+                // We must skip checking category itself.
+                // NB: This is kind of hacky way to avoid last element,
+                // something clearn can be done later.
+                let mut path_to_check = category_tree_path.clone();
+                path_to_check.remove(0);
+
+                Self::ensure_can_mutate_in_path_leaf(&path_to_check)?;
+            }
+
+            // If the category itself is already deleted, then this
+            // update *must* simultaneously do an undelete, otherwise it is blocked,
+            // as we do not permit unarchiving a deleted category. Doing
+            // a simultanous undelete and unarchive is accepted.
+
+            let category = <CategoryById<T>>::get(category_id);
+
+            ensure!(
+                !category.deleted || (new_deletion_status == Some(false)),
+                ERROR_CATEGORY_CANNOT_BE_UNARCHIVED_WHEN_DELETED
+            );
+
+            // Mutate category, and set possible new change parameters
+
+            <CategoryById<T>>::mutate(category_id, |c| {
+
+                if let Some(archived) = new_archival_status {
+                    c.archived = archived;
+                }
+
+                if let Some(deleted) = new_deletion_status {
+                    c.deleted = deleted;
+                }
+            });
+
+            // Generate event
+            Self::deposit_event(RawEvent::CategoryUpdated(category_id, new_archival_status, new_deletion_status));
+
+            Ok(())
+        }
+
+        /// Create new thread in category
+        fn create_thread(origin, category_id: CategoryId, title: Vec<u8>, text: Vec<u8>) -> dispatch::Result {
+
+            /*
+             * Update SPEC with new errors,
+             * and mutation of Category class,
+             * as well as side effect to update Category::num_threads_created.
+             */
+
+            // Check that its a valid signature
+            let who = ensure_signed(origin)?;
+
+            // Check that account is forum member
+            Self::ensure_is_forum_member(&who)?;
+
+            // Get path from parent to root of category tree.
+            let category_tree_path = Self::ensure_valid_category_and_build_category_tree_path(category_id)?;
+
+            // No ancestor is blocking us doing mutation in this category
+            Self::ensure_can_mutate_in_path_leaf(&category_tree_path)?;
+
+            // Validate title
+            Self::ensure_thread_title_is_valid(&title)?;
+
+            // Validate post text
+            Self::ensure_post_text_is_valid(&text)?;
+
+            /*
+             * Here it is safe to mutate state.
+             */
+
+            // Add thread
+            let thread = Self::add_new_thread(category_id, &title, &who);
+
+            // Add inital post to thread
+            Self::add_new_post(thread.id, &text, &who);
+
+            // Generate event
+            Self::deposit_event(RawEvent::ThreadCreated(thread.id));
+
+            Ok(())
+        }
+
+        /// Moderate thread
+        fn moderate_thread(origin, thread_id: ThreadId, rationale: Vec<u8>) -> dispatch::Result {
+
+            // Check that its a valid signature
+            let who = ensure_signed(origin)?;
+
+            // Signed by forum SUDO
+            Self::ensure_is_forum_sudo(&who)?;
+
+            // Get thread
+            let mut thread = Self::ensure_thread_exists(&thread_id)?;
+
+            // Thread is not already moderated
+            ensure!(thread.moderation.is_none(), ERROR_THREAD_ALREADY_MODERATED);
+
+            // Rationale valid
+            Self::ensure_thread_moderation_rationale_is_valid(&rationale)?;
+
+            // Can mutate in corresponding category
+            let path = Self::build_category_tree_path(thread.category_id);
+
+            // Path must be non-empty, as category id is from thread in state
+            assert!(!path.is_empty());
+
+            Self::ensure_can_mutate_in_path_leaf(&path)?;
+
+            /*
+             * Here we are safe to mutate
+             */
+
+            // Add moderation to thread
+            thread.moderation = Some(ModerationAction {
+                moderated_at: Self::current_block_and_time(),
+                moderator_id: who,
+                rationale: rationale.clone()
+            });
+
+            <ThreadById<T>>::insert(thread_id, thread.clone());
+
+            // Update moderation/umoderation count of corresponding category
+            <CategoryById<T>>::mutate(thread.category_id, |category| {
+                category.num_direct_unmoderated_threads -= 1;
+                category.num_direct_moderated_threads += 1;
+            });
+
+            // Generate event
+            Self::deposit_event(RawEvent::ThreadModerated(thread_id));
+
+            Ok(())
+        }
+
+        /// Edit post text
+        fn add_post(origin, thread_id: ThreadId, text: Vec<u8>) -> dispatch::Result {
+
+            /*
+             * Update SPEC with new errors,
+             */
+
+            // Check that its a valid signature
+            let who = ensure_signed(origin)?;
+
+            // Check that account is forum member
+            Self::ensure_is_forum_member(&who)?;
+
+            // Validate post text
+            Self::ensure_post_text_is_valid(&text)?;
+
+            // Make sure thread exists and is mutable
+            let thread = Self::ensure_thread_is_mutable(&thread_id)?;
+
+            // Get path from parent to root of category tree.
+            let category_tree_path = Self::ensure_valid_category_and_build_category_tree_path(thread.category_id)?;
+
+            // No ancestor is blocking us doing mutation in this category
+            Self::ensure_can_mutate_in_path_leaf(&category_tree_path)?;
+
+            /*
+             * Here we are safe to mutate
+             */
+
+            let post = Self::add_new_post(thread_id, &text, &who);
+
+            // Generate event
+            Self::deposit_event(RawEvent::PostAdded(post.id));
+
+            Ok(())
+        }
+
+        /// Edit post text
+        fn edit_post_text(origin, post_id: PostId, new_text: Vec<u8>) -> dispatch::Result {
+
+            /* Edit spec.
+              - forum member guard missing
+              - check that both post and thread and category are mutable
+            */
+
+            // Check that its a valid signature
+            let who = ensure_signed(origin)?;
+
+            // Check that account is forum member
+            Self::ensure_is_forum_member(&who)?;
+
+            // Validate post text
+            Self::ensure_post_text_is_valid(&new_text)?;
+
+            // Make sure there exists a mutable post with post id `post_id`
+            let post = Self::ensure_post_is_mutable(&post_id)?;
+
+            // Signer does not match creator of post with identifier postId
+            ensure!(post.author_id == who, ERROR_ACCOUNT_DOES_NOT_MATCH_POST_AUTHOR);
+
+            /*
+             * Here we are safe to mutate
+             */
+
+            <PostById<T>>::mutate(post_id, |p| {
+
+                let expired_post_text = PostTextChange {
+                    expired_at: Self::current_block_and_time(),
+                    text: post.current_text.clone()
+                };
+
+                // Set current text to new text
+                p.current_text = new_text;
+
+                // Copy current text to history of expired texts
+                p.text_change_history.push(expired_post_text);
+            });
+
+            // Generate event
+            Self::deposit_event(RawEvent::PostTextUpdated(post.id, post.text_change_history.len() as u64));
+
+            Ok(())
+        }
+
+        /// Moderate post
+        fn moderate_post(origin, post_id: PostId, rationale: Vec<u8>) -> dispatch::Result {
+
+            // Check that its a valid signature
+            let who = ensure_signed(origin)?;
+
+            // Signed by forum SUDO
+            Self::ensure_is_forum_sudo(&who)?;
+
+            // Make sure post exists and is mutable
+            let post = Self::ensure_post_is_mutable(&post_id)?;
+
+            Self::ensure_post_moderation_rationale_is_valid(&rationale)?;
+
+            /*
+             * Here we are safe to mutate
+             */
+
+            // Update moderation action on post
+            let moderation_action = ModerationAction{
+                moderated_at: Self::current_block_and_time(),
+                moderator_id: who,
+                rationale: rationale.clone()
+            };
+
+            <PostById<T>>::mutate(post_id, |p| {
+                p.moderation = Some(moderation_action);
+            });
+
+            // Update moderated and unmoderated post count of corresponding thread
+            <ThreadById<T>>::mutate(post.thread_id, |t| {
+                t.num_unmoderated_posts -= 1;
+                t.num_moderated_posts += 1;
+            });
+
+            // Generate event
+            Self::deposit_event(RawEvent::PostModerated(post.id));
+
+            Ok(())
+        }
+
+    }
+}
+
+impl<T: Trait> Module<T> {
+    fn ensure_category_title_is_valid(title: &Vec<u8>) -> dispatch::Result {
+        CategoryTitleConstraint::get().ensure_valid(
+            title.len(),
+            ERROR_CATEGORY_TITLE_TOO_SHORT,
+            ERROR_CATEGORY_TITLE_TOO_LONG,
+        )
+    }
+
+    fn ensure_category_description_is_valid(description: &Vec<u8>) -> dispatch::Result {
+        CategoryDescriptionConstraint::get().ensure_valid(
+            description.len(),
+            ERROR_CATEGORY_DESCRIPTION_TOO_SHORT,
+            ERROR_CATEGORY_DESCRIPTION_TOO_LONG,
+        )
+    }
+
+    fn ensure_thread_moderation_rationale_is_valid(rationale: &Vec<u8>) -> dispatch::Result {
+        ThreadModerationRationaleConstraint::get().ensure_valid(
+            rationale.len(),
+            ERROR_THREAD_MODERATION_RATIONALE_TOO_SHORT,
+            ERROR_THREAD_MODERATION_RATIONALE_TOO_LONG,
+        )
+    }
+
+    fn ensure_thread_title_is_valid(title: &Vec<u8>) -> dispatch::Result {
+        ThreadTitleConstraint::get().ensure_valid(
+            title.len(),
+            ERROR_THREAD_TITLE_TOO_SHORT,
+            ERROR_THREAD_TITLE_TOO_LONG,
+        )
+    }
+
+    fn ensure_post_text_is_valid(text: &Vec<u8>) -> dispatch::Result {
+        PostTextConstraint::get().ensure_valid(
+            text.len(),
+            ERROR_POST_TEXT_TOO_SHORT,
+            ERROR_POST_TEXT_TOO_LONG,
+        )
+    }
+
+    fn ensure_post_moderation_rationale_is_valid(rationale: &Vec<u8>) -> dispatch::Result {
+        PostModerationRationaleConstraint::get().ensure_valid(
+            rationale.len(),
+            ERROR_POST_MODERATION_RATIONALE_TOO_SHORT,
+            ERROR_POST_MODERATION_RATIONALE_TOO_LONG,
+        )
+    }
+
+    fn current_block_and_time() -> BlockchainTimestamp<T::BlockNumber, T::Moment> {
+        BlockchainTimestamp {
+            block: <system::Module<T>>::block_number(),
+            time: <timestamp::Module<T>>::now(),
+        }
+    }
+
+    fn ensure_post_is_mutable(
+        post_id: &PostId,
+    ) -> Result<Post<T::BlockNumber, T::Moment, T::AccountId>, &'static str> {
+        // Make sure post exists
+        let post = Self::ensure_post_exists(post_id)?;
+
+        // and is unmoderated
+        ensure!(post.moderation.is_none(), ERROR_POST_MODERATED);
+
+        // and make sure thread is mutable
+        Self::ensure_thread_is_mutable(&post.thread_id)?;
+
+        Ok(post)
+    }
+
+    fn ensure_post_exists(
+        post_id: &PostId,
+    ) -> Result<Post<T::BlockNumber, T::Moment, T::AccountId>, &'static str> {
+        if <PostById<T>>::exists(post_id) {
+            Ok(<PostById<T>>::get(post_id))
+        } else {
+            Err(ERROR_POST_DOES_NOT_EXIST)
+        }
+    }
+
+    fn ensure_thread_is_mutable(
+        thread_id: &ThreadId,
+    ) -> Result<Thread<T::BlockNumber, T::Moment, T::AccountId>, &'static str> {
+        // Make sure thread exists
+        let thread = Self::ensure_thread_exists(&thread_id)?;
+
+        // and is unmoderated
+        ensure!(thread.moderation.is_none(), ERROR_THREAD_MODERATED);
+
+        // and corresponding category is mutable
+        Self::ensure_catgory_is_mutable(thread.category_id)?;
+
+        Ok(thread)
+    }
+
+    fn ensure_thread_exists(
+        thread_id: &ThreadId,
+    ) -> Result<Thread<T::BlockNumber, T::Moment, T::AccountId>, &'static str> {
+        if <ThreadById<T>>::exists(thread_id) {
+            Ok(<ThreadById<T>>::get(thread_id))
+        } else {
+            Err(ERROR_THREAD_DOES_NOT_EXIST)
+        }
+    }
+
+    fn ensure_forum_sudo_set() -> Result<T::AccountId, &'static str> {
+        match <ForumSudo<T>>::get() {
+            Some(account_id) => Ok(account_id),
+            None => Err(ERROR_FORUM_SUDO_NOT_SET),
+        }
+    }
+
+    fn ensure_is_forum_sudo(account_id: &T::AccountId) -> dispatch::Result {
+        let forum_sudo_account = Self::ensure_forum_sudo_set()?;
+
+        ensure!(
+            *account_id == forum_sudo_account,
+            ERROR_ORIGIN_NOT_FORUM_SUDO
+        );
+        Ok(())
+    }
+
+    fn ensure_is_forum_member(
+        account_id: &T::AccountId,
+    ) -> Result<ForumUser<T::AccountId>, &'static str> {
+        let forum_user_query = T::MembershipRegistry::get_forum_user(account_id);
+
+        if let Some(forum_user) = forum_user_query {
+            Ok(forum_user)
+        } else {
+            Err(ERROR_NOT_FORUM_USER)
+        }
+    }
+
+    fn ensure_catgory_is_mutable(category_id: CategoryId) -> dispatch::Result {
+        let category_tree_path = Self::build_category_tree_path(category_id);
+
+        Self::ensure_can_mutate_in_path_leaf(&category_tree_path)
+    }
+
+    fn ensure_can_mutate_in_path_leaf(
+        category_tree_path: &CategoryTreePath<T::BlockNumber, T::Moment, T::AccountId>,
+    ) -> dispatch::Result {
+        // Is parent category directly or indirectly deleted or archived category
+        ensure!(
+            !category_tree_path.iter().any(
+                |c: &Category<T::BlockNumber, T::Moment, T::AccountId>| c.deleted || c.archived
+            ),
+            ERROR_ANCESTOR_CATEGORY_IMMUTABLE
+        );
+
+        Ok(())
+    }
+
+    fn ensure_can_add_subcategory_path_leaf(
+        category_tree_path: &CategoryTreePath<T::BlockNumber, T::Moment, T::AccountId>,
+    ) -> dispatch::Result {
+        Self::ensure_can_mutate_in_path_leaf(category_tree_path)?;
+
+        // Does adding a new category exceed maximum depth
+        let depth_of_new_category = 1 + 1 + category_tree_path.len();
+
+        ensure!(
+            depth_of_new_category <= MAX_CATEGORY_DEPTH as usize,
+            ERROR_MAX_VALID_CATEGORY_DEPTH_EXCEEDED
+        );
+
+        Ok(())
+    }
+
+    fn ensure_valid_category_and_build_category_tree_path(
+        category_id: CategoryId,
+    ) -> Result<CategoryTreePath<T::BlockNumber, T::Moment, T::AccountId>, &'static str> {
+        ensure!(
+            <CategoryById<T>>::exists(&category_id),
+            ERROR_CATEGORY_DOES_NOT_EXIST
+        );
+
+        // Get path from parent to root of category tree.
+        let category_tree_path = Self::build_category_tree_path(category_id);
+
+        assert!(category_tree_path.len() > 0);
+
+        Ok(category_tree_path)
+    }
+
+    /// Builds path and populates in `path`.
+    /// Requires that `category_id` is valid
+    fn build_category_tree_path(
+        category_id: CategoryId,
+    ) -> CategoryTreePath<T::BlockNumber, T::Moment, T::AccountId> {
+        // Get path from parent to root of category tree.
+        let mut category_tree_path = vec![];
+
+        Self::_build_category_tree_path(category_id, &mut category_tree_path);
+
+        category_tree_path
+    }
+
+    /// Builds path and populates in `path`.
+    /// Requires that `category_id` is valid
+    fn _build_category_tree_path(
+        category_id: CategoryId,
+        path: &mut CategoryTreePath<T::BlockNumber, T::Moment, T::AccountId>,
+    ) {
+        // Grab category
+        let category = <CategoryById<T>>::get(category_id);
+
+        // Copy out position_in_parent_category
+        let position_in_parent_category_field = category.position_in_parent_category.clone();
+
+        // Add category to path container
+        path.push(category);
+
+        // Make recursive call on parent if we are not at root
+        if let Some(child_position_in_parent) = position_in_parent_category_field {
+            assert!(<CategoryById<T>>::exists(
+                &child_position_in_parent.parent_id
+            ));
+
+            Self::_build_category_tree_path(child_position_in_parent.parent_id, path);
+        }
+    }
+
+    fn add_new_thread(
+        category_id: CategoryId,
+        title: &Vec<u8>,
+        author_id: &T::AccountId,
+    ) -> Thread<T::BlockNumber, T::Moment, T::AccountId> {
+        // Get category
+        let category = <CategoryById<T>>::get(category_id);
+
+        // Create and add new thread
+        let new_thread_id = NextThreadId::get();
+
+        let new_thread = Thread {
+            id: new_thread_id,
+            title: title.clone(),
+            category_id: category_id,
+            nr_in_category: category.num_threads_created() + 1,
+            moderation: None,
+            num_unmoderated_posts: 0,
+            num_moderated_posts: 0,
+            created_at: Self::current_block_and_time(),
+            author_id: author_id.clone(),
+        };
+
+        // Store thread
+        <ThreadById<T>>::insert(new_thread_id, new_thread.clone());
+
+        // Update next thread id
+        NextThreadId::mutate(|n| {
+            *n += 1;
+        });
+
+        // Update unmoderated thread count in corresponding category
+        <CategoryById<T>>::mutate(category_id, |c| {
+            c.num_direct_unmoderated_threads += 1;
+        });
+
+        new_thread
+    }
+
+    /// Creates and ads a new post ot the given thread, and makes all required state updates
+    /// `thread_id` must be valid
+    fn add_new_post(
+        thread_id: ThreadId,
+        text: &Vec<u8>,
+        author_id: &T::AccountId,
+    ) -> Post<T::BlockNumber, T::Moment, T::AccountId> {
+        // Get thread
+        let thread = <ThreadById<T>>::get(thread_id);
+
+        // Make and add initial post
+        let new_post_id = NextPostId::get();
+
+        let new_post = Post {
+            id: new_post_id,
+            thread_id: thread_id,
+            nr_in_thread: thread.num_posts_ever_created() + 1,
+            current_text: text.clone(),
+            moderation: None,
+            text_change_history: vec![],
+            created_at: Self::current_block_and_time(),
+            author_id: author_id.clone(),
+        };
+
+        // Store post
+        <PostById<T>>::insert(new_post_id, new_post.clone());
+
+        // Update next post id
+        NextPostId::mutate(|n| {
+            *n += 1;
+        });
+
+        // Update unmoderated post count of thread
+        <ThreadById<T>>::mutate(thread_id, |t| {
+            t.num_unmoderated_posts += 1;
+        });
+
+        new_post
+    }
+}

+ 530 - 0
runtime-modules/forum/src/mock.rs

@@ -0,0 +1,530 @@
+#![cfg(test)]
+
+use crate::*;
+
+use primitives::H256;
+
+use crate::{GenesisConfig, Module, Trait};
+use runtime_primitives::{
+    testing::Header,
+    traits::{BlakeTwo256, IdentityLookup},
+    Perbill,
+};
+use srml_support::{impl_outer_origin, parameter_types};
+
+/// Module which has a full Substrate module for
+/// mocking behaviour of MembershipRegistry
+pub mod registry {
+
+    use super::*;
+    // use srml_support::*;
+
+    #[derive(Encode, Decode, Default, Clone, PartialEq, Eq)]
+    pub struct Member<AccountId> {
+        pub id: AccountId,
+    }
+
+    decl_storage! {
+        trait Store for Module<T: Trait> as MockForumUserRegistry {
+
+            pub ForumUserById get(forum_user_by_id) config(): map T::AccountId => Member<T::AccountId>;
+
+        }
+    }
+
+    decl_module! {
+        pub struct Module<T: Trait> for enum Call where origin: T::Origin {}
+    }
+
+    impl<T: Trait> Module<T> {
+        pub fn add_member(member: &Member<T::AccountId>) {
+            <ForumUserById<T>>::insert(member.id.clone(), member.clone());
+        }
+    }
+
+    impl<T: Trait> ForumUserRegistry<T::AccountId> for Module<T> {
+        fn get_forum_user(id: &T::AccountId) -> Option<ForumUser<T::AccountId>> {
+            if <ForumUserById<T>>::exists(id) {
+                let m = <ForumUserById<T>>::get(id);
+
+                Some(ForumUser { id: m.id })
+            } else {
+                None
+            }
+        }
+    }
+
+    pub type TestMembershipRegistryModule = Module<Runtime>;
+}
+
+impl_outer_origin! {
+    pub enum Origin for Runtime {}
+}
+
+// Workaround for https://github.com/rust-lang/rust/issues/26925 . Remove when sorted.
+#[derive(Clone, PartialEq, Eq, Debug)]
+pub struct Runtime;
+parameter_types! {
+    pub const BlockHashCount: u64 = 250;
+    pub const MaximumBlockWeight: u32 = 1024;
+    pub const MaximumBlockLength: u32 = 2 * 1024;
+    pub const AvailableBlockRatio: Perbill = Perbill::one();
+    pub const MinimumPeriod: u64 = 5;
+}
+
+impl system::Trait for Runtime {
+    type Origin = Origin;
+    type Index = u64;
+    type BlockNumber = u64;
+    type Call = ();
+    type Hash = H256;
+    type Hashing = BlakeTwo256;
+    type AccountId = u64;
+    type Lookup = IdentityLookup<Self::AccountId>;
+    type Header = Header;
+    // type WeightMultiplierUpdate = ();
+    type Event = ();
+    type BlockHashCount = BlockHashCount;
+    type MaximumBlockWeight = MaximumBlockWeight;
+    type MaximumBlockLength = MaximumBlockLength;
+    type AvailableBlockRatio = AvailableBlockRatio;
+    type Version = ();
+}
+
+impl timestamp::Trait for Runtime {
+    type Moment = u64;
+    type OnTimestampSet = ();
+    type MinimumPeriod = MinimumPeriod;
+}
+
+impl Trait for Runtime {
+    type Event = ();
+    type MembershipRegistry = registry::TestMembershipRegistryModule;
+}
+
+#[derive(Clone)]
+pub enum OriginType {
+    Signed(<Runtime as system::Trait>::AccountId),
+    //Inherent, <== did not find how to make such an origin yet
+    Root,
+}
+
+pub fn mock_origin(origin: OriginType) -> mock::Origin {
+    match origin {
+        OriginType::Signed(account_id) => Origin::signed(account_id),
+        //OriginType::Inherent => Origin::inherent,
+        OriginType::Root => system::RawOrigin::Root.into(), //Origin::root
+    }
+}
+
+pub const NOT_FORUM_SUDO_ORIGIN: OriginType = OriginType::Signed(111);
+
+pub const NOT_MEMBER_ORIGIN: OriginType = OriginType::Signed(222);
+
+pub const INVLAID_CATEGORY_ID: CategoryId = 333;
+
+pub const INVLAID_THREAD_ID: ThreadId = 444;
+
+pub const INVLAID_POST_ID: ThreadId = 555;
+
+pub fn generate_text(len: usize) -> Vec<u8> {
+    vec![b'x'; len]
+}
+
+pub fn good_category_title() -> Vec<u8> {
+    b"Great new category".to_vec()
+}
+
+pub fn good_category_description() -> Vec<u8> {
+    b"This is a great new category for the forum".to_vec()
+}
+
+pub fn good_thread_title() -> Vec<u8> {
+    b"Great new thread".to_vec()
+}
+
+pub fn good_thread_text() -> Vec<u8> {
+    b"The first post in this thread".to_vec()
+}
+
+pub fn good_post_text() -> Vec<u8> {
+    b"A response in the thread".to_vec()
+}
+
+pub fn good_rationale() -> Vec<u8> {
+    b"This post violates our community rules".to_vec()
+}
+
+/*
+ * These test fixtures can be heavily refactored to avoid repotition, needs macros, and event
+ * assertions are also missing.
+ */
+
+pub struct CreateCategoryFixture {
+    pub origin: OriginType,
+    pub parent: Option<CategoryId>,
+    pub title: Vec<u8>,
+    pub description: Vec<u8>,
+    pub result: dispatch::Result,
+}
+
+impl CreateCategoryFixture {
+    pub fn call_and_assert(&self) {
+        assert_eq!(
+            TestForumModule::create_category(
+                mock_origin(self.origin.clone()),
+                self.parent,
+                self.title.clone(),
+                self.description.clone()
+            ),
+            self.result
+        )
+    }
+}
+
+pub struct UpdateCategoryFixture {
+    pub origin: OriginType,
+    pub category_id: CategoryId,
+    pub new_archival_status: Option<bool>,
+    pub new_deletion_status: Option<bool>,
+    pub result: dispatch::Result,
+}
+
+impl UpdateCategoryFixture {
+    pub fn call_and_assert(&self) {
+        assert_eq!(
+            TestForumModule::update_category(
+                mock_origin(self.origin.clone()),
+                self.category_id,
+                self.new_archival_status.clone(),
+                self.new_deletion_status.clone()
+            ),
+            self.result
+        )
+    }
+}
+
+pub struct CreateThreadFixture {
+    pub origin: OriginType,
+    pub category_id: CategoryId,
+    pub title: Vec<u8>,
+    pub text: Vec<u8>,
+    pub result: dispatch::Result,
+}
+
+impl CreateThreadFixture {
+    pub fn call_and_assert(&self) {
+        assert_eq!(
+            TestForumModule::create_thread(
+                mock_origin(self.origin.clone()),
+                self.category_id,
+                self.title.clone(),
+                self.text.clone()
+            ),
+            self.result
+        )
+    }
+}
+
+pub struct CreatePostFixture {
+    pub origin: OriginType,
+    pub thread_id: ThreadId,
+    pub text: Vec<u8>,
+    pub result: dispatch::Result,
+}
+
+impl CreatePostFixture {
+    pub fn call_and_assert(&self) {
+        assert_eq!(
+            TestForumModule::add_post(
+                mock_origin(self.origin.clone()),
+                self.thread_id,
+                self.text.clone()
+            ),
+            self.result
+        )
+    }
+}
+
+pub fn create_forum_member() -> OriginType {
+    let member_id = 123;
+    let new_member = registry::Member { id: member_id };
+    registry::TestMembershipRegistryModule::add_member(&new_member);
+    OriginType::Signed(member_id)
+}
+
+pub fn assert_create_category(
+    forum_sudo: OriginType,
+    parent_category_id: Option<CategoryId>,
+    expected_result: dispatch::Result,
+) {
+    CreateCategoryFixture {
+        origin: forum_sudo,
+        parent: parent_category_id,
+        title: good_category_title(),
+        description: good_category_description(),
+        result: expected_result,
+    }
+    .call_and_assert();
+}
+
+pub fn assert_create_thread(
+    forum_sudo: OriginType,
+    category_id: CategoryId,
+    expected_result: dispatch::Result,
+) {
+    CreateThreadFixture {
+        origin: forum_sudo,
+        category_id,
+        title: good_thread_title(),
+        text: good_thread_text(),
+        result: expected_result,
+    }
+    .call_and_assert();
+}
+
+pub fn assert_create_post(
+    forum_sudo: OriginType,
+    thread_id: ThreadId,
+    expected_result: dispatch::Result,
+) {
+    CreatePostFixture {
+        origin: forum_sudo,
+        thread_id,
+        text: good_thread_text(),
+        result: expected_result,
+    }
+    .call_and_assert();
+}
+
+pub fn create_category(
+    forum_sudo: OriginType,
+    parent_category_id: Option<CategoryId>,
+) -> CategoryId {
+    let category_id = TestForumModule::next_category_id();
+    assert_create_category(forum_sudo, parent_category_id, Ok(()));
+    category_id
+}
+
+pub fn create_root_category(forum_sudo: OriginType) -> CategoryId {
+    create_category(forum_sudo, None)
+}
+
+pub fn create_root_category_and_thread(
+    forum_sudo: OriginType,
+) -> (OriginType, CategoryId, ThreadId) {
+    let member_origin = create_forum_member();
+    let category_id = create_root_category(forum_sudo);
+    let thread_id = TestForumModule::next_thread_id();
+
+    CreateThreadFixture {
+        origin: member_origin.clone(),
+        category_id,
+        title: good_thread_title(),
+        text: good_thread_text(),
+        result: Ok(()),
+    }
+    .call_and_assert();
+
+    (member_origin, category_id, thread_id)
+}
+
+pub fn create_root_category_and_thread_and_post(
+    forum_sudo: OriginType,
+) -> (OriginType, CategoryId, ThreadId, PostId) {
+    let (member_origin, category_id, thread_id) = create_root_category_and_thread(forum_sudo);
+    let post_id = TestForumModule::next_post_id();
+
+    CreatePostFixture {
+        origin: member_origin.clone(),
+        thread_id: thread_id.clone(),
+        text: good_post_text(),
+        result: Ok(()),
+    }
+    .call_and_assert();
+
+    (member_origin, category_id, thread_id, post_id)
+}
+
+pub fn moderate_thread(
+    forum_sudo: OriginType,
+    thread_id: ThreadId,
+    rationale: Vec<u8>,
+) -> dispatch::Result {
+    TestForumModule::moderate_thread(mock_origin(forum_sudo), thread_id, rationale)
+}
+
+pub fn moderate_post(
+    forum_sudo: OriginType,
+    post_id: PostId,
+    rationale: Vec<u8>,
+) -> dispatch::Result {
+    TestForumModule::moderate_post(mock_origin(forum_sudo), post_id, rationale)
+}
+
+pub fn archive_category(forum_sudo: OriginType, category_id: CategoryId) -> dispatch::Result {
+    TestForumModule::update_category(mock_origin(forum_sudo), category_id, Some(true), None)
+}
+
+pub fn unarchive_category(forum_sudo: OriginType, category_id: CategoryId) -> dispatch::Result {
+    TestForumModule::update_category(mock_origin(forum_sudo), category_id, Some(false), None)
+}
+
+pub fn delete_category(forum_sudo: OriginType, category_id: CategoryId) -> dispatch::Result {
+    TestForumModule::update_category(mock_origin(forum_sudo), category_id, None, Some(true))
+}
+
+pub fn undelete_category(forum_sudo: OriginType, category_id: CategoryId) -> dispatch::Result {
+    TestForumModule::update_category(mock_origin(forum_sudo), category_id, None, Some(false))
+}
+
+pub fn assert_not_forum_sudo_cannot_update_category(
+    update_operation: fn(OriginType, CategoryId) -> dispatch::Result,
+) {
+    let config = default_genesis_config();
+    let origin = OriginType::Signed(config.forum_sudo);
+
+    build_test_externalities(config).execute_with(|| {
+        let category_id = create_root_category(origin.clone());
+        assert_eq!(
+            update_operation(NOT_FORUM_SUDO_ORIGIN, category_id),
+            Err(ERROR_ORIGIN_NOT_FORUM_SUDO)
+        );
+    });
+}
+
+// This function basically just builds a genesis storage key/value store according to
+// our desired mockup.
+
+// refactor
+/// - add each config as parameter, then
+///
+
+pub fn default_genesis_config() -> GenesisConfig<Runtime> {
+    GenesisConfig::<Runtime> {
+        category_by_id: vec![], // endowed_accounts.iter().cloned().map(|k|(k, 1 << 60)).collect(),
+        next_category_id: 1,
+        thread_by_id: vec![],
+        next_thread_id: 1,
+        post_by_id: vec![],
+        next_post_id: 1,
+
+        forum_sudo: 33,
+
+        category_title_constraint: InputValidationLengthConstraint {
+            min: 10,
+            max_min_diff: 140,
+        },
+
+        category_description_constraint: InputValidationLengthConstraint {
+            min: 10,
+            max_min_diff: 140,
+        },
+
+        thread_title_constraint: InputValidationLengthConstraint {
+            min: 3,
+            max_min_diff: 43,
+        },
+
+        post_text_constraint: InputValidationLengthConstraint {
+            min: 1,
+            max_min_diff: 1001,
+        },
+
+        thread_moderation_rationale_constraint: InputValidationLengthConstraint {
+            min: 10,
+            max_min_diff: 2000,
+        },
+
+        post_moderation_rationale_constraint: InputValidationLengthConstraint {
+            min: 10,
+            max_min_diff: 2000,
+        }, // JUST GIVING UP ON ALL THIS FOR NOW BECAUSE ITS TAKING TOO LONG
+
+           // Extra genesis fields
+           //initial_forum_sudo: Some(143)
+    }
+}
+
+pub type RuntimeMap<K, V> = std::vec::Vec<(K, V)>;
+pub type RuntimeCategory = Category<
+    <Runtime as system::Trait>::BlockNumber,
+    <Runtime as timestamp::Trait>::Moment,
+    <Runtime as system::Trait>::AccountId,
+>;
+pub type RuntimeThread = Thread<
+    <Runtime as system::Trait>::BlockNumber,
+    <Runtime as timestamp::Trait>::Moment,
+    <Runtime as system::Trait>::AccountId,
+>;
+pub type RuntimePost = Post<
+    <Runtime as system::Trait>::BlockNumber,
+    <Runtime as timestamp::Trait>::Moment,
+    <Runtime as system::Trait>::AccountId,
+>;
+pub type RuntimeBlockchainTimestamp = BlockchainTimestamp<
+    <Runtime as system::Trait>::BlockNumber,
+    <Runtime as timestamp::Trait>::Moment,
+>;
+
+pub fn genesis_config(
+    category_by_id: &RuntimeMap<CategoryId, RuntimeCategory>,
+    next_category_id: u64,
+    thread_by_id: &RuntimeMap<ThreadId, RuntimeThread>,
+    next_thread_id: u64,
+    post_by_id: &RuntimeMap<PostId, RuntimePost>,
+    next_post_id: u64,
+    forum_sudo: <Runtime as system::Trait>::AccountId,
+    category_title_constraint: &InputValidationLengthConstraint,
+    category_description_constraint: &InputValidationLengthConstraint,
+    thread_title_constraint: &InputValidationLengthConstraint,
+    post_text_constraint: &InputValidationLengthConstraint,
+    thread_moderation_rationale_constraint: &InputValidationLengthConstraint,
+    post_moderation_rationale_constraint: &InputValidationLengthConstraint,
+) -> GenesisConfig<Runtime> {
+    GenesisConfig::<Runtime> {
+        category_by_id: category_by_id.clone(),
+        next_category_id: next_category_id,
+        thread_by_id: thread_by_id.clone(),
+        next_thread_id: next_thread_id,
+        post_by_id: post_by_id.clone(),
+        next_post_id: next_post_id,
+        forum_sudo: forum_sudo,
+        category_title_constraint: category_title_constraint.clone(),
+        category_description_constraint: category_description_constraint.clone(),
+        thread_title_constraint: thread_title_constraint.clone(),
+        post_text_constraint: post_text_constraint.clone(),
+        thread_moderation_rationale_constraint: thread_moderation_rationale_constraint.clone(),
+        post_moderation_rationale_constraint: post_moderation_rationale_constraint.clone(),
+    }
+}
+
+// MockForumUserRegistry
+pub fn default_mock_forum_user_registry_genesis_config() -> registry::GenesisConfig<Runtime> {
+    registry::GenesisConfig::<Runtime> {
+        forum_user_by_id: vec![],
+    }
+}
+
+// NB!:
+// Wanted to have payload: a: &GenesisConfig<Test>
+// but borrow checker made my life miserabl, so giving up for now.
+pub fn build_test_externalities(config: GenesisConfig<Runtime>) -> runtime_io::TestExternalities {
+    let mut t = system::GenesisConfig::default()
+        .build_storage::<Runtime>()
+        .unwrap();
+
+    config.assimilate_storage(&mut t).unwrap();
+
+    // Add mock registry configuration
+    default_mock_forum_user_registry_genesis_config()
+        .assimilate_storage(&mut t)
+        .unwrap();
+
+    t.into()
+}
+
+// pub type System = system::Module<Runtime>;
+
+/// Export forum module on a test runtime
+pub type TestForumModule = Module<Runtime>;

+ 981 - 0
runtime-modules/forum/src/tests.rs

@@ -0,0 +1,981 @@
+#![cfg(test)]
+
+use super::*;
+use crate::mock::*;
+
+use srml_support::{assert_err, assert_ok};
+
+/*
+* NB!: No test checks for event emission!!!!
+*/
+
+/*
+ * set_forum_sudo
+ * ==============================================================================
+ *
+ * Missing cases
+ *
+ * set_forum_bad_origin
+ *
+ */
+
+#[test]
+fn set_forum_sudo_unset() {
+    let config = default_genesis_config();
+
+    build_test_externalities(config).execute_with(|| {
+        // Ensure that forum sudo is default
+        assert_eq!(TestForumModule::forum_sudo(), Some(33));
+
+        // Unset forum sudo
+        assert_ok!(TestForumModule::set_forum_sudo(
+            mock_origin(OriginType::Root),
+            None
+        ));
+
+        // Sudo no longer set
+        assert!(TestForumModule::forum_sudo().is_none());
+
+        // event emitted?!
+    });
+}
+
+#[test]
+fn set_forum_sudo_update() {
+    let config = default_genesis_config();
+
+    build_test_externalities(config).execute_with(|| {
+        // Ensure that forum sudo is default
+        assert_eq!(
+            TestForumModule::forum_sudo(),
+            Some(default_genesis_config().forum_sudo)
+        );
+
+        let new_forum_sudo_account_id = 780;
+
+        // Unset forum sudo
+        assert_ok!(TestForumModule::set_forum_sudo(
+            mock_origin(OriginType::Root),
+            Some(new_forum_sudo_account_id)
+        ));
+
+        // Sudo no longer set
+        assert_eq!(
+            TestForumModule::forum_sudo(),
+            Some(new_forum_sudo_account_id)
+        );
+    });
+}
+
+/*
+ * create_category
+ * ==============================================================================
+ *
+ * Missing cases
+ *
+ * create_category_bad_origin
+ * create_category_forum_sudo_not_set
+ */
+
+#[test]
+fn create_root_category_successfully() {
+    let config = default_genesis_config();
+    let origin = OriginType::Signed(config.forum_sudo);
+
+    build_test_externalities(config).execute_with(|| {
+        assert_create_category(origin, None, Ok(()));
+    });
+}
+
+#[test]
+fn create_subcategory_successfully() {
+    let config = default_genesis_config();
+    let origin = OriginType::Signed(config.forum_sudo);
+
+    build_test_externalities(config).execute_with(|| {
+        let root_category_id = create_root_category(origin.clone());
+        assert_create_category(origin, Some(root_category_id), Ok(()));
+    });
+}
+
+#[test]
+fn create_category_title_too_short() {
+    let config = default_genesis_config();
+    let origin = OriginType::Signed(config.forum_sudo);
+    let min_len = config.category_title_constraint.min as usize;
+
+    build_test_externalities(config).execute_with(|| {
+        CreateCategoryFixture {
+            origin,
+            parent: None,
+            title: generate_text(min_len - 1),
+            description: good_category_description(),
+            result: Err(ERROR_CATEGORY_TITLE_TOO_SHORT),
+        }
+        .call_and_assert();
+    });
+}
+
+#[test]
+fn create_category_title_too_long() {
+    let config = default_genesis_config();
+    let origin = OriginType::Signed(config.forum_sudo);
+    let max_len = config.category_title_constraint.max() as usize;
+
+    build_test_externalities(config).execute_with(|| {
+        CreateCategoryFixture {
+            origin,
+            parent: None,
+            title: generate_text(max_len + 1),
+            description: good_category_description(),
+            result: Err(ERROR_CATEGORY_TITLE_TOO_LONG),
+        }
+        .call_and_assert();
+    });
+}
+
+#[test]
+fn create_category_description_too_short() {
+    let config = default_genesis_config();
+    let origin = OriginType::Signed(config.forum_sudo);
+    let min_len = config.category_description_constraint.min as usize;
+
+    build_test_externalities(config).execute_with(|| {
+        CreateCategoryFixture {
+            origin,
+            parent: None,
+            title: good_category_title(),
+            description: generate_text(min_len - 1),
+            result: Err(ERROR_CATEGORY_DESCRIPTION_TOO_SHORT),
+        }
+        .call_and_assert();
+    });
+}
+
+#[test]
+fn create_category_description_too_long() {
+    let config = default_genesis_config();
+    let origin = OriginType::Signed(config.forum_sudo);
+    let max_len = config.category_description_constraint.max() as usize;
+
+    build_test_externalities(config).execute_with(|| {
+        CreateCategoryFixture {
+            origin,
+            parent: None,
+            title: good_category_title(),
+            description: generate_text(max_len + 1),
+            result: Err(ERROR_CATEGORY_DESCRIPTION_TOO_LONG),
+        }
+        .call_and_assert();
+    });
+}
+
+/*
+ * update_category
+ * ==============================================================================
+ *
+ * Missing cases
+ *
+ * create_category_bad_origin
+ * create_category_forum_sudo_not_set
+ * create_category_origin_not_forum_sudo
+ * create_category_immutable_ancestor_category
+ */
+
+#[test]
+fn update_category_undelete_and_unarchive() {
+    /*
+     * Create an initial state with two levels of categories, where
+     * leaf category is deleted, and then try to undelete.
+     */
+
+    let forum_sudo = 32;
+
+    let created_at = RuntimeBlockchainTimestamp { block: 0, time: 0 };
+
+    let category_by_id = vec![
+        // A root category
+        (
+            1,
+            Category {
+                id: 1,
+                title: "New root".as_bytes().to_vec(),
+                description: "This is a new root category".as_bytes().to_vec(),
+                created_at: created_at.clone(),
+                deleted: false,
+                archived: false,
+                num_direct_subcategories: 1,
+                num_direct_unmoderated_threads: 0,
+                num_direct_moderated_threads: 0,
+                position_in_parent_category: None,
+                moderator_id: forum_sudo,
+            },
+        ),
+        // A subcategory of the one above
+        (
+            2,
+            Category {
+                id: 2,
+                title: "New subcategory".as_bytes().to_vec(),
+                description: "This is a new subcategory to root category"
+                    .as_bytes()
+                    .to_vec(),
+                created_at: created_at.clone(),
+                deleted: true,
+                archived: false,
+                num_direct_subcategories: 0,
+                num_direct_unmoderated_threads: 0,
+                num_direct_moderated_threads: 0,
+                position_in_parent_category: Some(ChildPositionInParentCategory {
+                    parent_id: 1,
+                    child_nr_in_parent_category: 1,
+                }),
+                moderator_id: forum_sudo,
+            },
+        ),
+    ];
+
+    // Set constraints to be sloppy, we don't care about enforcing them.
+    let sloppy_constraint = InputValidationLengthConstraint {
+        min: 0,
+        max_min_diff: 1000,
+    };
+
+    let config = genesis_config(
+        &category_by_id,             // category_by_id
+        category_by_id.len() as u64, // next_category_id
+        &vec![],                     // thread_by_id
+        1,                           // next_thread_id
+        &vec![],                     // post_by_id
+        1,                           // next_post_id
+        forum_sudo,
+        &sloppy_constraint,
+        &sloppy_constraint,
+        &sloppy_constraint,
+        &sloppy_constraint,
+        &sloppy_constraint,
+        &sloppy_constraint,
+    );
+
+    build_test_externalities(config).execute_with(|| {
+        UpdateCategoryFixture {
+            origin: OriginType::Signed(forum_sudo),
+            category_id: 2,
+            new_archival_status: None,        // same as before
+            new_deletion_status: Some(false), // undelete
+            result: Ok(()),
+        }
+        .call_and_assert();
+    });
+}
+
+/*
+ * create_thread
+ * ==============================================================================
+ *
+ * Missing cases
+ *
+ * create_thread_bad_origin
+ * create_thread_forum_sudo_not_set
+ * ...
+ */
+
+#[test]
+fn create_thread_successfully() {
+    let config = default_genesis_config();
+    let origin = OriginType::Signed(config.forum_sudo);
+
+    build_test_externalities(config).execute_with(|| {
+        let category_id = create_root_category(origin);
+        let member_origin = create_forum_member();
+
+        CreateThreadFixture {
+            origin: member_origin,
+            category_id,
+            title: good_thread_title(),
+            text: good_thread_text(),
+            result: Ok(()),
+        }
+        .call_and_assert();
+    });
+}
+
+#[test]
+fn create_thread_title_too_short() {
+    let config = default_genesis_config();
+    let origin = OriginType::Signed(config.forum_sudo);
+    let min_len = config.thread_title_constraint.min as usize;
+
+    build_test_externalities(config).execute_with(|| {
+        let category_id = create_root_category(origin);
+        let member_origin = create_forum_member();
+
+        CreateThreadFixture {
+            origin: member_origin,
+            category_id,
+            title: generate_text(min_len - 1),
+            text: good_thread_text(),
+            result: Err(ERROR_THREAD_TITLE_TOO_SHORT),
+        }
+        .call_and_assert();
+    });
+}
+
+#[test]
+fn create_thread_title_too_long() {
+    let config = default_genesis_config();
+    let origin = OriginType::Signed(config.forum_sudo);
+    let max_len = config.thread_title_constraint.max() as usize;
+
+    build_test_externalities(config).execute_with(|| {
+        let category_id = create_root_category(origin);
+        let member_origin = create_forum_member();
+
+        CreateThreadFixture {
+            origin: member_origin,
+            category_id,
+            title: generate_text(max_len + 1),
+            text: good_thread_text(),
+            result: Err(ERROR_THREAD_TITLE_TOO_LONG),
+        }
+        .call_and_assert();
+    });
+}
+
+#[test]
+fn create_thread_text_too_short() {
+    let config = default_genesis_config();
+    let origin = OriginType::Signed(config.forum_sudo);
+    let min_len = config.post_text_constraint.min as usize;
+
+    build_test_externalities(config).execute_with(|| {
+        let category_id = create_root_category(origin);
+        let member_origin = create_forum_member();
+
+        CreateThreadFixture {
+            origin: member_origin,
+            category_id,
+            title: good_thread_title(),
+            text: generate_text(min_len - 1),
+            result: Err(ERROR_POST_TEXT_TOO_SHORT),
+        }
+        .call_and_assert();
+    });
+}
+
+#[test]
+fn create_thread_text_too_long() {
+    let config = default_genesis_config();
+    let origin = OriginType::Signed(config.forum_sudo);
+    let max_len = config.post_text_constraint.max() as usize;
+
+    build_test_externalities(config).execute_with(|| {
+        let category_id = create_root_category(origin);
+        let member_origin = create_forum_member();
+
+        CreateThreadFixture {
+            origin: member_origin,
+            category_id,
+            title: good_thread_title(),
+            text: generate_text(max_len + 1),
+            result: Err(ERROR_POST_TEXT_TOO_LONG),
+        }
+        .call_and_assert();
+    });
+}
+
+#[test]
+fn create_post_successfully() {
+    let config = default_genesis_config();
+    let origin = OriginType::Signed(config.forum_sudo);
+
+    build_test_externalities(config).execute_with(|| {
+        let (_, _, _, _) = create_root_category_and_thread_and_post(origin);
+    });
+}
+
+#[test]
+fn create_post_text_too_short() {
+    let config = default_genesis_config();
+    let origin = OriginType::Signed(config.forum_sudo);
+    let min_len = config.post_text_constraint.min as usize;
+
+    build_test_externalities(config).execute_with(|| {
+        let (member_origin, _, thread_id) = create_root_category_and_thread(origin);
+
+        CreatePostFixture {
+            origin: member_origin,
+            thread_id,
+            text: generate_text(min_len - 1),
+            result: Err(ERROR_POST_TEXT_TOO_SHORT),
+        }
+        .call_and_assert();
+    });
+}
+
+#[test]
+fn create_post_text_too_long() {
+    let config = default_genesis_config();
+    let origin = OriginType::Signed(config.forum_sudo);
+    let max_len = config.post_text_constraint.max() as usize;
+
+    build_test_externalities(config).execute_with(|| {
+        let (member_origin, _, thread_id) = create_root_category_and_thread(origin);
+
+        CreatePostFixture {
+            origin: member_origin,
+            thread_id,
+            text: generate_text(max_len + 1),
+            result: Err(ERROR_POST_TEXT_TOO_LONG),
+        }
+        .call_and_assert();
+    });
+}
+
+// Test moderation:
+// -----------------------------------------------------------------------------
+
+#[test]
+fn moderate_thread_successfully() {
+    let config = default_genesis_config();
+    let origin = OriginType::Signed(config.forum_sudo);
+
+    build_test_externalities(config).execute_with(|| {
+        let (_, _, thread_id) = create_root_category_and_thread(origin.clone());
+        assert_eq!(moderate_thread(origin, thread_id, good_rationale()), Ok(()));
+    });
+}
+
+#[test]
+fn cannot_moderate_already_moderated_thread() {
+    let config = default_genesis_config();
+    let origin = OriginType::Signed(config.forum_sudo);
+
+    build_test_externalities(config).execute_with(|| {
+        let (_, _, thread_id) = create_root_category_and_thread(origin.clone());
+        assert_eq!(
+            moderate_thread(origin.clone(), thread_id.clone(), good_rationale()),
+            Ok(())
+        );
+        assert_eq!(
+            moderate_thread(origin, thread_id, good_rationale()),
+            Err(ERROR_THREAD_ALREADY_MODERATED)
+        );
+    });
+}
+
+#[test]
+fn moderate_thread_rationale_too_short() {
+    let config = default_genesis_config();
+    let origin = OriginType::Signed(config.forum_sudo);
+    let min_len = config.thread_moderation_rationale_constraint.min as usize;
+
+    build_test_externalities(config).execute_with(|| {
+        let (_, _, thread_id) = create_root_category_and_thread(origin.clone());
+        let bad_rationale = generate_text(min_len - 1);
+        assert_eq!(
+            moderate_thread(origin, thread_id, bad_rationale),
+            Err(ERROR_THREAD_MODERATION_RATIONALE_TOO_SHORT)
+        );
+    });
+}
+
+#[test]
+fn moderate_thread_rationale_too_long() {
+    let config = default_genesis_config();
+    let origin = OriginType::Signed(config.forum_sudo);
+    let max_len = config.thread_moderation_rationale_constraint.max() as usize;
+
+    build_test_externalities(config).execute_with(|| {
+        let (_, _, thread_id) = create_root_category_and_thread(origin.clone());
+        let bad_rationale = generate_text(max_len + 1);
+        assert_eq!(
+            moderate_thread(origin, thread_id, bad_rationale),
+            Err(ERROR_THREAD_MODERATION_RATIONALE_TOO_LONG)
+        );
+    });
+}
+
+#[test]
+fn moderate_post_successfully() {
+    let config = default_genesis_config();
+    let origin = OriginType::Signed(config.forum_sudo);
+
+    build_test_externalities(config).execute_with(|| {
+        let (_, _, _, post_id) = create_root_category_and_thread_and_post(origin.clone());
+        assert_eq!(moderate_post(origin, post_id, good_rationale()), Ok(()));
+    });
+}
+
+#[test]
+fn moderate_post_rationale_too_short() {
+    let config = default_genesis_config();
+    let origin = OriginType::Signed(config.forum_sudo);
+    let min_len = config.post_moderation_rationale_constraint.min as usize;
+
+    build_test_externalities(config).execute_with(|| {
+        let (_, _, _, post_id) = create_root_category_and_thread_and_post(origin.clone());
+        let bad_rationale = generate_text(min_len - 1);
+        assert_eq!(
+            moderate_post(origin, post_id, bad_rationale),
+            Err(ERROR_POST_MODERATION_RATIONALE_TOO_SHORT)
+        );
+    });
+}
+
+#[test]
+fn moderate_post_rationale_too_long() {
+    let config = default_genesis_config();
+    let origin = OriginType::Signed(config.forum_sudo);
+    let max_len = config.post_moderation_rationale_constraint.max() as usize;
+
+    build_test_externalities(config).execute_with(|| {
+        let (_, _, _, post_id) = create_root_category_and_thread_and_post(origin.clone());
+        let bad_rationale = generate_text(max_len + 1);
+        assert_eq!(
+            moderate_post(origin, post_id, bad_rationale),
+            Err(ERROR_POST_MODERATION_RATIONALE_TOO_LONG)
+        );
+    });
+}
+
+#[test]
+fn cannot_moderate_already_moderated_post() {
+    let config = default_genesis_config();
+    let origin = OriginType::Signed(config.forum_sudo);
+
+    build_test_externalities(config).execute_with(|| {
+        let (_, _, _, post_id) = create_root_category_and_thread_and_post(origin.clone());
+        assert_eq!(
+            moderate_post(origin.clone(), post_id.clone(), good_rationale()),
+            Ok(())
+        );
+        assert_eq!(
+            moderate_post(origin, post_id, good_rationale()),
+            Err(ERROR_POST_MODERATED)
+        );
+    });
+}
+
+// Not a forum sudo:
+// -----------------------------------------------------------------------------
+
+#[test]
+fn not_forum_sudo_cannot_create_root_category() {
+    let config = default_genesis_config();
+
+    build_test_externalities(config).execute_with(|| {
+        assert_create_category(
+            NOT_FORUM_SUDO_ORIGIN,
+            None,
+            Err(ERROR_ORIGIN_NOT_FORUM_SUDO),
+        );
+    });
+}
+
+#[test]
+fn not_forum_sudo_cannot_create_subcategory() {
+    let config = default_genesis_config();
+    let origin = OriginType::Signed(config.forum_sudo);
+
+    build_test_externalities(config).execute_with(|| {
+        let root_category_id = create_root_category(origin);
+        assert_create_category(
+            NOT_FORUM_SUDO_ORIGIN,
+            Some(root_category_id),
+            Err(ERROR_ORIGIN_NOT_FORUM_SUDO),
+        );
+    });
+}
+
+#[test]
+fn not_forum_sudo_cannot_archive_category() {
+    assert_not_forum_sudo_cannot_update_category(archive_category);
+}
+
+#[test]
+fn not_forum_sudo_cannot_unarchive_category() {
+    assert_not_forum_sudo_cannot_update_category(unarchive_category);
+}
+
+#[test]
+fn not_forum_sudo_cannot_delete_category() {
+    assert_not_forum_sudo_cannot_update_category(delete_category);
+}
+
+#[test]
+fn not_forum_sudo_cannot_undelete_category() {
+    assert_not_forum_sudo_cannot_update_category(undelete_category);
+}
+
+#[test]
+fn not_forum_sudo_cannot_moderate_thread() {
+    let config = default_genesis_config();
+    let origin = OriginType::Signed(config.forum_sudo);
+
+    build_test_externalities(config).execute_with(|| {
+        let (_, _, thread_id) = create_root_category_and_thread(origin.clone());
+        assert_eq!(
+            moderate_thread(NOT_FORUM_SUDO_ORIGIN, thread_id, good_rationale()),
+            Err(ERROR_ORIGIN_NOT_FORUM_SUDO)
+        );
+    });
+}
+
+#[test]
+fn not_forum_sudo_cannot_moderate_post() {
+    let config = default_genesis_config();
+    let origin = OriginType::Signed(config.forum_sudo);
+
+    build_test_externalities(config).execute_with(|| {
+        let (_, _, _, post_id) = create_root_category_and_thread_and_post(origin.clone());
+        assert_eq!(
+            moderate_post(NOT_FORUM_SUDO_ORIGIN, post_id, good_rationale()),
+            Err(ERROR_ORIGIN_NOT_FORUM_SUDO)
+        );
+    });
+}
+
+// Not a member:
+// -----------------------------------------------------------------------------
+
+#[test]
+fn not_member_cannot_create_thread() {
+    let config = default_genesis_config();
+    let origin = OriginType::Signed(config.forum_sudo);
+
+    build_test_externalities(config).execute_with(|| {
+        CreateThreadFixture {
+            origin: NOT_MEMBER_ORIGIN,
+            category_id: create_root_category(origin),
+            title: good_thread_title(),
+            text: good_thread_text(),
+            result: Err(ERROR_NOT_FORUM_USER),
+        }
+        .call_and_assert();
+    });
+}
+
+#[test]
+fn not_member_cannot_create_post() {
+    let config = default_genesis_config();
+    let origin = OriginType::Signed(config.forum_sudo);
+
+    build_test_externalities(config).execute_with(|| {
+        let (_, _, thread_id) = create_root_category_and_thread(origin);
+        CreatePostFixture {
+            origin: NOT_MEMBER_ORIGIN,
+            thread_id,
+            text: good_post_text(),
+            result: Err(ERROR_NOT_FORUM_USER),
+        }
+        .call_and_assert();
+    });
+}
+
+#[test]
+fn not_member_cannot_edit_post() {
+    let config = default_genesis_config();
+    let origin = OriginType::Signed(config.forum_sudo);
+
+    build_test_externalities(config).execute_with(|| {
+        let (_, _, _, post_id) = create_root_category_and_thread_and_post(origin);
+        assert_err!(
+            TestForumModule::edit_post_text(
+                mock_origin(NOT_MEMBER_ORIGIN),
+                post_id,
+                good_rationale()
+            ),
+            ERROR_NOT_FORUM_USER
+        );
+    });
+}
+
+// Invalid id passed:
+// -----------------------------------------------------------------------------
+
+#[test]
+fn cannot_create_subcategory_with_invalid_parent_category_id() {
+    let config = default_genesis_config();
+    let origin = OriginType::Signed(config.forum_sudo);
+
+    build_test_externalities(config).execute_with(|| {
+        assert_create_category(
+            origin,
+            Some(INVLAID_CATEGORY_ID),
+            Err(ERROR_CATEGORY_DOES_NOT_EXIST),
+        );
+    });
+}
+
+#[test]
+fn cannot_create_thread_with_invalid_category_id() {
+    let config = default_genesis_config();
+
+    build_test_externalities(config).execute_with(|| {
+        CreateThreadFixture {
+            origin: create_forum_member(),
+            category_id: INVLAID_CATEGORY_ID,
+            title: good_thread_title(),
+            text: good_thread_text(),
+            result: Err(ERROR_CATEGORY_DOES_NOT_EXIST),
+        }
+        .call_and_assert();
+    });
+}
+
+#[test]
+fn cannot_create_post_with_invalid_thread_id() {
+    let config = default_genesis_config();
+
+    build_test_externalities(config).execute_with(|| {
+        CreatePostFixture {
+            origin: create_forum_member(),
+            thread_id: INVLAID_THREAD_ID,
+            text: good_post_text(),
+            result: Err(ERROR_THREAD_DOES_NOT_EXIST),
+        }
+        .call_and_assert();
+    });
+}
+
+#[test]
+fn cannot_moderate_thread_with_invalid_id() {
+    let config = default_genesis_config();
+    let origin = OriginType::Signed(config.forum_sudo);
+
+    build_test_externalities(config).execute_with(|| {
+        assert_err!(
+            moderate_thread(origin, INVLAID_THREAD_ID, good_rationale()),
+            ERROR_THREAD_DOES_NOT_EXIST
+        );
+    });
+}
+
+#[test]
+fn cannot_moderate_post_with_invalid_id() {
+    let config = default_genesis_config();
+    let origin = OriginType::Signed(config.forum_sudo);
+
+    build_test_externalities(config).execute_with(|| {
+        assert_err!(
+            moderate_post(origin, INVLAID_POST_ID, good_rationale()),
+            ERROR_POST_DOES_NOT_EXIST
+        );
+    });
+}
+
+// Successfull extrinsics
+// -----------------------------------------------------------------------------
+
+#[test]
+fn archive_then_unarchive_category_successfully() {
+    let config = default_genesis_config();
+    let forum_sudo = OriginType::Signed(config.forum_sudo);
+
+    build_test_externalities(config).execute_with(|| {
+        let category_id = create_root_category(forum_sudo.clone());
+        assert_ok!(archive_category(forum_sudo.clone(), category_id.clone(),));
+        // TODO get category by id and assert archived == true.
+
+        assert_ok!(unarchive_category(forum_sudo, category_id,));
+        // TODO get category by id and assert archived == false.
+    });
+}
+
+#[test]
+fn delete_then_undelete_category_successfully() {
+    let config = default_genesis_config();
+    let forum_sudo = OriginType::Signed(config.forum_sudo);
+
+    build_test_externalities(config).execute_with(|| {
+        let category_id = create_root_category(forum_sudo.clone());
+        assert_ok!(delete_category(forum_sudo.clone(), category_id.clone(),));
+        // TODO get category by id and assert deleted == true.
+
+        assert_ok!(undelete_category(forum_sudo.clone(), category_id.clone(),));
+        // TODO get category by id and assert deleted == false.
+    });
+}
+
+// TODO Consider to fix the logic of the forum module
+// #[test]
+// fn cannot_unarchive_not_archived_category() {
+//     let config = default_genesis_config();
+//     let forum_sudo = OriginType::Signed(config.forum_sudo);
+
+//     build_test_externalities(config).execute_with(|| {
+//         let category_id = create_root_category(forum_sudo.clone());
+
+//         // TODO bug in a logic! it should not be possible. !!!
+
+//         assert_err!(
+//             archive_category(
+//                 forum_sudo.clone(),
+//                 category_id.clone(),
+//             ),
+//             "... TODO expect error ..."
+//         );
+//     });
+// }
+
+// TODO Consider to fix the logic of the forum module
+// #[test]
+// fn cannot_undelete_not_deleted_category() {
+//     let config = default_genesis_config();
+//     let forum_sudo = OriginType::Signed(config.forum_sudo);
+
+//     build_test_externalities(config).execute_with(|| {
+//         let category_id = create_root_category(forum_sudo.clone());
+//         assert_err!(
+//             delete_category(
+//                 forum_sudo.clone(),
+//                 category_id.clone(),
+//             ),
+//             "... TODO expect error ..."
+//         );
+//     });
+// }
+
+// With archived / deleted category, moderated thread
+// -----------------------------------------------------------------------------
+
+#[test]
+fn cannot_create_subcategory_in_archived_category() {
+    let config = default_genesis_config();
+    let forum_sudo = OriginType::Signed(config.forum_sudo);
+
+    build_test_externalities(config).execute_with(|| {
+        let category_id = create_root_category(forum_sudo.clone());
+        assert_ok!(archive_category(forum_sudo.clone(), category_id.clone(),));
+        assert_create_category(
+            forum_sudo,
+            Some(category_id),
+            Err(ERROR_ANCESTOR_CATEGORY_IMMUTABLE),
+        );
+    });
+}
+
+#[test]
+fn cannot_create_subcategory_in_deleted_category() {
+    let config = default_genesis_config();
+    let forum_sudo = OriginType::Signed(config.forum_sudo);
+
+    build_test_externalities(config).execute_with(|| {
+        let category_id = create_root_category(forum_sudo.clone());
+        assert_ok!(delete_category(forum_sudo.clone(), category_id.clone(),));
+        assert_create_category(
+            forum_sudo,
+            Some(category_id),
+            Err(ERROR_ANCESTOR_CATEGORY_IMMUTABLE),
+        );
+    });
+}
+
+#[test]
+fn cannot_create_thread_in_archived_category() {
+    let config = default_genesis_config();
+    let forum_sudo = OriginType::Signed(config.forum_sudo);
+
+    build_test_externalities(config).execute_with(|| {
+        let category_id = create_root_category(forum_sudo.clone());
+        assert_ok!(archive_category(forum_sudo.clone(), category_id.clone(),));
+        assert_create_thread(
+            create_forum_member(),
+            category_id,
+            Err(ERROR_ANCESTOR_CATEGORY_IMMUTABLE),
+        );
+    });
+}
+
+#[test]
+fn cannot_create_thread_in_deleted_category() {
+    let config = default_genesis_config();
+    let forum_sudo = OriginType::Signed(config.forum_sudo);
+
+    build_test_externalities(config).execute_with(|| {
+        let category_id = create_root_category(forum_sudo.clone());
+        assert_ok!(delete_category(forum_sudo.clone(), category_id.clone(),));
+        assert_create_thread(
+            create_forum_member(),
+            category_id,
+            Err(ERROR_ANCESTOR_CATEGORY_IMMUTABLE),
+        );
+    });
+}
+
+#[test]
+fn cannot_create_post_in_thread_of_archived_category() {
+    let config = default_genesis_config();
+    let forum_sudo = OriginType::Signed(config.forum_sudo);
+
+    build_test_externalities(config).execute_with(|| {
+        let category_id = create_root_category(forum_sudo.clone());
+        let thread_id = TestForumModule::next_thread_id();
+        assert_create_thread(create_forum_member(), category_id, Ok(()));
+        assert_ok!(archive_category(forum_sudo.clone(), category_id.clone(),));
+        assert_create_post(
+            create_forum_member(),
+            thread_id,
+            Err(ERROR_ANCESTOR_CATEGORY_IMMUTABLE),
+        );
+    });
+}
+
+#[test]
+fn cannot_create_post_in_thread_of_deleted_category() {
+    let config = default_genesis_config();
+    let forum_sudo = OriginType::Signed(config.forum_sudo);
+
+    build_test_externalities(config).execute_with(|| {
+        let category_id = create_root_category(forum_sudo.clone());
+        let thread_id = TestForumModule::next_thread_id();
+        assert_create_thread(create_forum_member(), category_id, Ok(()));
+        assert_ok!(delete_category(forum_sudo.clone(), category_id.clone(),));
+        assert_create_post(
+            create_forum_member(),
+            thread_id,
+            Err(ERROR_ANCESTOR_CATEGORY_IMMUTABLE),
+        );
+    });
+}
+
+#[test]
+fn cannot_create_post_in_moderated_thread() {
+    let config = default_genesis_config();
+    let forum_sudo = OriginType::Signed(config.forum_sudo);
+
+    build_test_externalities(config).execute_with(|| {
+        let (_, _, thread_id) = create_root_category_and_thread(forum_sudo.clone());
+        assert_ok!(moderate_thread(
+            forum_sudo,
+            thread_id.clone(),
+            good_rationale()
+        ));
+        assert_create_post(
+            create_forum_member(),
+            thread_id,
+            Err(ERROR_THREAD_MODERATED),
+        );
+    });
+}
+
+#[test]
+fn cannot_edit_post_in_moderated_thread() {
+    let config = default_genesis_config();
+    let forum_sudo = OriginType::Signed(config.forum_sudo);
+
+    build_test_externalities(config).execute_with(|| {
+        let (member_origin, _, thread_id, post_id) =
+            create_root_category_and_thread_and_post(forum_sudo.clone());
+        assert_ok!(moderate_thread(forum_sudo, thread_id, good_rationale()));
+        assert_err!(
+            TestForumModule::edit_post_text(mock_origin(member_origin), post_id, good_rationale()),
+            ERROR_THREAD_MODERATED
+        );
+    });
+}
+
+// TODO impl
+// #[test]
+// fn cannot_edit_moderated_post() {}

+ 1 - 2
runtime-modules/hiring/Cargo.toml

@@ -21,9 +21,8 @@ quote = '<=1.0.2'
 
 [dependencies.stake]
 default_features = false
-git = 'https://github.com/joystream/substrate-stake-module'
 package = 'substrate-stake-module'
-tag = 'v1.0.1'
+path = '../stake'
 
 [dependencies.timestamp]
 default_features = false

+ 56 - 0
runtime-modules/recurring-reward/Cargo.toml

@@ -0,0 +1,56 @@
+[package]
+name = 'substrate-recurring-reward-module'
+version = '1.0.1'
+authors = ['Joystream contributors']
+edition = '2018'
+
+[dependencies]
+hex-literal = '0.1.0'
+serde = { version = '1.0', optional = true }
+serde_derive = { version = '1.0', optional = true }
+rstd = { package = 'sr-std', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+runtime-primitives = { package = 'sr-primitives', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+srml-support = { package = 'srml-support', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+srml-support-procedural = { package = 'srml-support-procedural', git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+system = { package = 'srml-system', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+balances = { package = 'srml-balances', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+codec = { package = 'parity-scale-codec', version = '1.0.0', default-features = false, features = ['derive'] }
+# https://users.rust-lang.org/t/failure-derive-compilation-error/39062
+quote = '<=1.0.2'
+
+[dependencies.timestamp]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'srml-timestamp'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dependencies.runtime-io]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'sr-io'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dependencies.minting]
+default_features = false
+package = 'substrate-token-mint-module'
+path = '../token-minting'
+
+[dev-dependencies]
+runtime-io = { package = 'sr-io', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+primitives = { package = 'substrate-primitives', git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+
+[features]
+default = ['std']
+std = [
+	'serde',
+	'serde_derive',
+	'codec/std',
+	'rstd/std',
+	'runtime-io/std',
+	'runtime-primitives/std',
+	'srml-support/std',
+	'system/std',
+  	'balances/std',
+	'timestamp/std',
+	'minting/std',
+]

+ 397 - 0
runtime-modules/recurring-reward/src/lib.rs

@@ -0,0 +1,397 @@
+// Ensure we're `no_std` when compiling for Wasm.
+#![cfg_attr(not(feature = "std"), no_std)]
+use rstd::prelude::*;
+
+use codec::{Codec, Decode, Encode};
+use runtime_primitives::traits::{MaybeSerialize, Member, One, SimpleArithmetic, Zero};
+use srml_support::{decl_module, decl_storage, ensure, Parameter};
+
+use minting::{self, BalanceOf};
+use system;
+
+mod mock;
+mod tests;
+
+pub trait Trait: system::Trait + minting::Trait {
+    type PayoutStatusHandler: PayoutStatusHandler<Self>;
+
+    /// Type of identifier for recipients.
+    type RecipientId: Parameter
+        + Member
+        + SimpleArithmetic
+        + Codec
+        + Default
+        + Copy
+        + MaybeSerialize
+        + PartialEq;
+
+    /// Type for identifier for relationship representing that a recipient recieves recurring reward from a token mint
+    type RewardRelationshipId: Parameter
+        + Member
+        + SimpleArithmetic
+        + Codec
+        + Default
+        + Copy
+        + MaybeSerialize
+        + PartialEq;
+}
+
+/// Handler for aftermath of a payout attempt
+pub trait PayoutStatusHandler<T: Trait> {
+    fn payout_succeeded(
+        id: T::RewardRelationshipId,
+        destination_account: &T::AccountId,
+        amount: BalanceOf<T>,
+    );
+
+    fn payout_failed(
+        id: T::RewardRelationshipId,
+        destination_account: &T::AccountId,
+        amount: BalanceOf<T>,
+    );
+}
+
+/// Makes `()` empty tuple, a PayoutStatusHandler that does nothing.
+impl<T: Trait> PayoutStatusHandler<T> for () {
+    fn payout_succeeded(
+        _id: T::RewardRelationshipId,
+        _destination_account: &T::AccountId,
+        _amount: BalanceOf<T>,
+    ) {
+    }
+
+    fn payout_failed(
+        _id: T::RewardRelationshipId,
+        _destination_account: &T::AccountId,
+        _amount: BalanceOf<T>,
+    ) {
+    }
+}
+
+/// A recipient of recurring rewards
+#[derive(Encode, Decode, Copy, Clone, Debug, Default)]
+pub struct Recipient<Balance> {
+    // stats
+    /// Total payout received by this recipient
+    total_reward_received: Balance,
+
+    /// Total payout missed for this recipient
+    total_reward_missed: Balance,
+}
+
+#[derive(Encode, Decode, Copy, Clone, Debug, Default)]
+pub struct RewardRelationship<AccountId, Balance, BlockNumber, MintId, RecipientId> {
+    /// Identifier for receiver
+    recipient: RecipientId,
+
+    /// Identifier for reward source
+    mint_id: MintId,
+
+    /// Destination account for reward
+    account: AccountId,
+
+    /// The payout amount at the next payout
+    amount_per_payout: Balance,
+
+    /// When set, identifies block when next payout should be processed,
+    /// otherwise there is no pending payout
+    next_payment_at_block: Option<BlockNumber>,
+
+    /// When set, will be the basis for automatically setting next payment,
+    /// otherwise any upcoming payout will be a one off.
+    payout_interval: Option<BlockNumber>,
+
+    // stats
+    /// Total payout received in this relationship
+    total_reward_received: Balance,
+
+    /// Total payout failed in this relationship
+    total_reward_missed: Balance,
+}
+
+impl<AccountId: Clone, Balance: Clone, BlockNumber: Clone, MintId: Clone, RecipientId: Clone>
+    RewardRelationship<AccountId, Balance, BlockNumber, MintId, RecipientId>
+{
+    /// Verifies whether relationship is active
+    pub fn is_active(&self) -> bool {
+        self.next_payment_at_block.is_some()
+    }
+
+    /// Make clone which is activated.
+    pub fn clone_activated(&self, start_at: &BlockNumber) -> Self {
+        Self {
+            next_payment_at_block: Some((*start_at).clone()),
+            ..((*self).clone())
+        }
+    }
+
+    /// Make clone which is deactivated
+    pub fn clone_deactivated(&self) -> Self {
+        Self {
+            next_payment_at_block: None,
+            ..((*self).clone())
+        }
+    }
+}
+
+decl_storage! {
+    trait Store for Module<T: Trait> as RecurringReward {
+        Recipients get(recipients): linked_map T::RecipientId => Recipient<BalanceOf<T>>;
+
+        RecipientsCreated get(recipients_created): T::RecipientId;
+
+        RewardRelationships get(reward_relationships): linked_map T::RewardRelationshipId => RewardRelationship<T::AccountId, BalanceOf<T>, T::BlockNumber, T::MintId, T::RecipientId>;
+
+        RewardRelationshipsCreated get(reward_relationships_created): T::RewardRelationshipId;
+    }
+}
+
+decl_module! {
+    pub struct Module<T: Trait> for enum Call where origin: T::Origin {
+
+        fn on_finalize(now: T::BlockNumber) {
+            Self::do_payouts(now);
+        }
+    }
+}
+
+#[derive(Eq, PartialEq, Debug)]
+pub enum RewardsError {
+    RecipientNotFound,
+    RewardSourceNotFound,
+    NextPaymentNotInFuture,
+    RewardRelationshipNotFound,
+}
+
+impl<T: Trait> Module<T> {
+    /// Adds a new Recipient and returns new recipient identifier.
+    pub fn add_recipient() -> T::RecipientId {
+        let next_id = Self::recipients_created();
+        <RecipientsCreated<T>>::put(next_id + One::one());
+        <Recipients<T>>::insert(&next_id, Recipient::default());
+        next_id
+    }
+
+    /// Adds a new RewardRelationship, for a given source mint, recipient, account.
+    pub fn add_reward_relationship(
+        mint_id: T::MintId,
+        recipient: T::RecipientId,
+        account: T::AccountId,
+        amount_per_payout: BalanceOf<T>,
+        next_payment_at_block: T::BlockNumber,
+        payout_interval: Option<T::BlockNumber>,
+    ) -> Result<T::RewardRelationshipId, RewardsError> {
+        ensure!(
+            <minting::Module<T>>::mint_exists(mint_id),
+            RewardsError::RewardSourceNotFound
+        );
+        ensure!(
+            <Recipients<T>>::exists(recipient),
+            RewardsError::RecipientNotFound
+        );
+        ensure!(
+            next_payment_at_block > <system::Module<T>>::block_number(),
+            RewardsError::NextPaymentNotInFuture
+        );
+
+        let relationship_id = Self::reward_relationships_created();
+        <RewardRelationshipsCreated<T>>::put(relationship_id + One::one());
+        <RewardRelationships<T>>::insert(
+            relationship_id,
+            RewardRelationship {
+                mint_id,
+                recipient,
+                account,
+                amount_per_payout,
+                next_payment_at_block: Some(next_payment_at_block),
+                payout_interval,
+                total_reward_received: Zero::zero(),
+                total_reward_missed: Zero::zero(),
+            },
+        );
+        Ok(relationship_id)
+    }
+
+    /// Removes a relationship from RewardRelashionships and its recipient.
+    pub fn remove_reward_relationship(id: T::RewardRelationshipId) {
+        if <RewardRelationships<T>>::exists(&id) {
+            <Recipients<T>>::remove(<RewardRelationships<T>>::take(&id).recipient);
+        }
+    }
+
+    /// Will attempt to activat a deactivated reward relationship.
+    pub fn try_to_activate_relationship(
+        id: T::RewardRelationshipId,
+        next_payment_at_block: T::BlockNumber,
+    ) -> Result<bool, ()> {
+        // Ensure relationship exists
+        let reward_relationship = Self::ensure_reward_relationship_exists(&id)?;
+
+        let activated = if reward_relationship.is_active() {
+            // Was not activated
+            false
+        } else {
+            // Update as activated
+            let activated_relationship =
+                reward_relationship.clone_activated(&next_payment_at_block);
+
+            RewardRelationships::<T>::insert(id, activated_relationship);
+
+            // We activated
+            true
+        };
+
+        Ok(activated)
+    }
+
+    /// Will attempt to deactivat a activated reward relationship.
+    pub fn try_to_deactivate_relationship(id: T::RewardRelationshipId) -> Result<bool, ()> {
+        // Ensure relationship exists
+        let reward_relationship = Self::ensure_reward_relationship_exists(&id)?;
+
+        let deactivated = if reward_relationship.is_active() {
+            let deactivated_relationship = reward_relationship.clone_deactivated();
+
+            RewardRelationships::<T>::insert(id, deactivated_relationship);
+
+            // Was deactivated
+            true
+        } else {
+            // Was not deactivated
+            false
+        };
+
+        Ok(deactivated)
+    }
+
+    // For reward relationship found with given identifier, new values can be set for
+    // account, payout, block number when next payout will be made and the new interval after
+    // the next scheduled payout. All values are optional, but updating values are combined in this
+    // single method to ensure atomic updates.
+    pub fn set_reward_relationship(
+        id: T::RewardRelationshipId,
+        new_account: Option<T::AccountId>,
+        new_payout: Option<BalanceOf<T>>,
+        new_next_payment_at: Option<Option<T::BlockNumber>>,
+        new_payout_interval: Option<Option<T::BlockNumber>>,
+    ) -> Result<(), RewardsError> {
+        ensure!(
+            <RewardRelationships<T>>::exists(&id),
+            RewardsError::RewardRelationshipNotFound
+        );
+
+        let mut relationship = Self::reward_relationships(&id);
+
+        if let Some(account) = new_account {
+            relationship.account = account;
+        }
+        if let Some(payout) = new_payout {
+            relationship.amount_per_payout = payout;
+        }
+        if let Some(next_payout_at_block) = new_next_payment_at {
+            if let Some(blocknumber) = next_payout_at_block {
+                ensure!(
+                    blocknumber > <system::Module<T>>::block_number(),
+                    RewardsError::NextPaymentNotInFuture
+                );
+            }
+            relationship.next_payment_at_block = next_payout_at_block;
+        }
+        if let Some(payout_interval) = new_payout_interval {
+            relationship.payout_interval = payout_interval;
+        }
+
+        <RewardRelationships<T>>::insert(&id, relationship);
+        Ok(())
+    }
+
+    /*
+    For all relationships where next_payment_at_block is set and matches current block height,
+    a call to pay_reward is made for the suitable amount, recipient and source.
+    The next_payment_in_block is updated based on payout_interval.
+    If the call succeeds, total_reward_received is incremented on both
+    recipient and dependency with amount_per_payout, and a call to T::PayoutStatusHandler is made.
+    Otherwise, analogous steps for failure.
+    */
+    fn do_payouts(now: T::BlockNumber) {
+        for (relationship_id, ref mut relationship) in <RewardRelationships<T>>::enumerate() {
+            assert!(<Recipients<T>>::exists(&relationship.recipient));
+
+            let mut recipient = Self::recipients(relationship.recipient);
+
+            if let Some(next_payment_at_block) = relationship.next_payment_at_block {
+                if next_payment_at_block != now {
+                    continue;
+                }
+
+                // Add the missed payout and try to pay those in addition to scheduled payout?
+                // let payout = relationship.total_reward_missed + relationship.amount_per_payout;
+                let payout = relationship.amount_per_payout;
+
+                // try to make payment
+                if <minting::Module<T>>::transfer_tokens(
+                    relationship.mint_id,
+                    payout,
+                    &relationship.account,
+                )
+                .is_err()
+                {
+                    // add only newly scheduled payout to total missed payout
+                    relationship.total_reward_missed += relationship.amount_per_payout;
+
+                    // update recipient stats
+                    recipient.total_reward_missed += relationship.amount_per_payout;
+
+                    T::PayoutStatusHandler::payout_failed(
+                        relationship_id,
+                        &relationship.account,
+                        payout,
+                    );
+                } else {
+                    // update payout received stats
+                    relationship.total_reward_received += payout;
+                    recipient.total_reward_received += payout;
+
+                    // update missed payout stats
+                    // if relationship.total_reward_missed != Zero::zero() {
+                    //     // update recipient stats
+                    //     recipient.total_reward_missed -= relationship.total_reward_missed;
+
+                    //     // clear missed reward on relationship
+                    //     relationship.total_reward_missed = Zero::zero();
+                    // }
+                    T::PayoutStatusHandler::payout_succeeded(
+                        relationship_id,
+                        &relationship.account,
+                        payout,
+                    );
+                }
+
+                // update next payout blocknumber at interval if set
+                if let Some(payout_interval) = relationship.payout_interval {
+                    relationship.next_payment_at_block = Some(now + payout_interval);
+                } else {
+                    relationship.next_payment_at_block = None;
+                }
+
+                <Recipients<T>>::insert(relationship.recipient, recipient);
+                <RewardRelationships<T>>::insert(relationship_id, relationship);
+            }
+        }
+    }
+}
+
+impl<T: Trait> Module<T> {
+    fn ensure_reward_relationship_exists(
+        id: &T::RewardRelationshipId,
+    ) -> Result<
+        RewardRelationship<T::AccountId, BalanceOf<T>, T::BlockNumber, T::MintId, T::RecipientId>,
+        (),
+    > {
+        ensure!(RewardRelationships::<T>::exists(id), ());
+
+        let relationship = RewardRelationships::<T>::get(id);
+
+        Ok(relationship)
+    }
+}

+ 103 - 0
runtime-modules/recurring-reward/src/mock/mod.rs

@@ -0,0 +1,103 @@
+#![cfg(test)]
+
+// use crate::*;
+use crate::{Module, Trait};
+
+use primitives::H256;
+
+use balances;
+use minting;
+use runtime_primitives::{
+    testing::Header,
+    traits::{BlakeTwo256, IdentityLookup},
+    Perbill,
+};
+use srml_support::{impl_outer_origin, parameter_types};
+
+mod status_handler;
+pub use status_handler::MockStatusHandler;
+
+impl_outer_origin! {
+    pub enum Origin for Test {}
+}
+
+// Workaround for https://github.com/rust-lang/rust/issues/26925 . Remove when sorted.
+#[derive(Clone, PartialEq, Eq, Debug)]
+pub struct Test;
+parameter_types! {
+    pub const BlockHashCount: u64 = 250;
+    pub const MaximumBlockWeight: u32 = 1024;
+    pub const MaximumBlockLength: u32 = 2 * 1024;
+    pub const AvailableBlockRatio: Perbill = Perbill::one();
+    pub const MinimumPeriod: u64 = 5;
+}
+
+impl system::Trait for Test {
+    type Origin = Origin;
+    type Index = u64;
+    type BlockNumber = u64;
+    type Call = ();
+    type Hash = H256;
+    type Hashing = BlakeTwo256;
+    type AccountId = u64;
+    type Lookup = IdentityLookup<Self::AccountId>;
+    type Header = Header;
+    type Event = ();
+    type BlockHashCount = BlockHashCount;
+    type MaximumBlockWeight = MaximumBlockWeight;
+    type MaximumBlockLength = MaximumBlockLength;
+    type AvailableBlockRatio = AvailableBlockRatio;
+    type Version = ();
+}
+
+parameter_types! {
+    pub const ExistentialDeposit: u32 = 0;
+    pub const TransferFee: u32 = 0;
+    pub const CreationFee: u32 = 0;
+    pub const TransactionBaseFee: u32 = 1;
+    pub const TransactionByteFee: u32 = 0;
+    pub const InitialMembersBalance: u64 = 2000;
+}
+
+impl balances::Trait for Test {
+    /// The type for recording an account's balance.
+    type Balance = u64;
+    /// What to do if an account's free balance gets zeroed.
+    type OnFreeBalanceZero = ();
+    /// What to do if a new account is created.
+    type OnNewAccount = ();
+    /// The ubiquitous event type.
+    type Event = ();
+
+    type DustRemoval = ();
+    type TransferPayment = ();
+    type ExistentialDeposit = ExistentialDeposit;
+    type TransferFee = TransferFee;
+    type CreationFee = CreationFee;
+}
+
+impl Trait for Test {
+    type PayoutStatusHandler = MockStatusHandler;
+    type RecipientId = u64;
+    type RewardRelationshipId = u64;
+}
+
+impl minting::Trait for Test {
+    type Currency = Balances;
+    type MintId = u64;
+}
+
+pub fn build_test_externalities() -> runtime_io::TestExternalities {
+    MockStatusHandler::reset();
+
+    let t = system::GenesisConfig::default()
+        .build_storage::<Test>()
+        .unwrap();
+
+    t.into()
+}
+
+pub type System = system::Module<Test>;
+pub type Balances = balances::Module<Test>;
+pub type Rewards = Module<Test>;
+pub type Minting = minting::Module<Test>;

+ 64 - 0
runtime-modules/recurring-reward/src/mock/status_handler.rs

@@ -0,0 +1,64 @@
+#![cfg(test)]
+
+use super::Test;
+use crate::{PayoutStatusHandler, Trait};
+use std::cell::RefCell;
+
+struct StatusHandlerState<T: Trait> {
+    successes: Vec<T::RewardRelationshipId>,
+    failures: Vec<T::RewardRelationshipId>,
+}
+
+impl<T: Trait> StatusHandlerState<T> {
+    pub fn reset(&mut self) {
+        self.successes = vec![];
+        self.failures = vec![];
+    }
+}
+
+impl<T: Trait> Default for StatusHandlerState<T> {
+    fn default() -> Self {
+        Self {
+            successes: vec![],
+            failures: vec![],
+        }
+    }
+}
+
+thread_local!(static STATUS_HANDLER_STATE: RefCell<StatusHandlerState<Test>> = RefCell::new(Default::default()));
+
+pub struct MockStatusHandler {}
+impl MockStatusHandler {
+    pub fn reset() {
+        STATUS_HANDLER_STATE.with(|cell| {
+            cell.borrow_mut().reset();
+        });
+    }
+    pub fn successes() -> usize {
+        let mut value = 0;
+        STATUS_HANDLER_STATE.with(|cell| {
+            value = cell.borrow_mut().successes.len();
+        });
+        value
+    }
+    pub fn failures() -> usize {
+        let mut value = 0;
+        STATUS_HANDLER_STATE.with(|cell| {
+            value = cell.borrow_mut().failures.len();
+        });
+        value
+    }
+}
+impl PayoutStatusHandler<Test> for MockStatusHandler {
+    fn payout_succeeded(id: u64, _destination_account: &u64, _amount: u64) {
+        STATUS_HANDLER_STATE.with(|cell| {
+            cell.borrow_mut().successes.push(id);
+        });
+    }
+
+    fn payout_failed(id: u64, _destination_account: &u64, _amount: u64) {
+        STATUS_HANDLER_STATE.with(|cell| {
+            cell.borrow_mut().failures.push(id);
+        });
+    }
+}

+ 259 - 0
runtime-modules/recurring-reward/src/tests.rs

@@ -0,0 +1,259 @@
+#![cfg(test)]
+
+use super::*;
+use crate::mock::*;
+use srml_support::traits::Currency;
+
+fn create_new_mint_with_capacity(capacity: u64) -> u64 {
+    let mint_id = Minting::add_mint(capacity, None).ok().unwrap();
+    assert!(Minting::mint_exists(mint_id));
+    assert_eq!(Minting::get_mint_capacity(mint_id).ok().unwrap(), capacity);
+    mint_id
+}
+
+#[test]
+fn adding_recipients() {
+    build_test_externalities().execute_with(|| {
+        let next_id = Rewards::recipients_created();
+        assert!(!<Recipients<Test>>::exists(&next_id));
+        let recipient_id = Rewards::add_recipient();
+        assert!(<Recipients<Test>>::exists(&next_id));
+        assert_eq!(recipient_id, next_id);
+        assert_eq!(Rewards::recipients_created(), next_id + 1);
+    });
+}
+
+#[test]
+fn adding_relationships() {
+    build_test_externalities().execute_with(|| {
+        let recipient_account: u64 = 1;
+        let mint_id = create_new_mint_with_capacity(1000000);
+        let recipient_id = Rewards::add_recipient();
+        let interval: u64 = 600;
+        let next_payment_at: u64 = 2222;
+        let payout = 100;
+
+        let next_relationship_id = Rewards::reward_relationships_created();
+        let relationship = Rewards::add_reward_relationship(
+            mint_id,
+            recipient_id,
+            recipient_account,
+            payout,
+            next_payment_at,
+            Some(interval),
+        );
+        assert!(relationship.is_ok());
+        let relationship_id = relationship.ok().unwrap();
+        assert_eq!(relationship_id, next_relationship_id);
+        assert_eq!(
+            Rewards::reward_relationships_created(),
+            next_relationship_id + 1
+        );
+        assert!(<RewardRelationships<Test>>::exists(&relationship_id));
+        let relationship = Rewards::reward_relationships(&relationship_id);
+        assert_eq!(relationship.next_payment_at_block, Some(next_payment_at));
+        assert_eq!(relationship.amount_per_payout, payout);
+        assert_eq!(relationship.mint_id, mint_id);
+        assert_eq!(relationship.account, recipient_account);
+        assert_eq!(relationship.payout_interval, Some(interval));
+
+        // mint doesn't exist
+        assert_eq!(
+            Rewards::add_reward_relationship(
+                111,
+                recipient_id,
+                recipient_account,
+                100,
+                next_payment_at,
+                None,
+            )
+            .expect_err("should fail if mint doesn't exist"),
+            RewardsError::RewardSourceNotFound
+        );
+    });
+}
+
+#[test]
+fn one_off_payout() {
+    build_test_externalities().execute_with(|| {
+        System::set_block_number(10000);
+        let recipient_account: u64 = 1;
+        let _ = Balances::deposit_creating(&recipient_account, 400);
+        let mint_id = create_new_mint_with_capacity(1000000);
+        let recipient_id = Rewards::add_recipient();
+        let payout: u64 = 1000;
+        let next_payout_at: u64 = 12222;
+        let relationship = Rewards::add_reward_relationship(
+            mint_id,
+            recipient_id,
+            recipient_account,
+            payout,
+            next_payout_at,
+            None,
+        );
+        assert!(relationship.is_ok());
+        let relationship_id = relationship.ok().unwrap();
+
+        let relationship = Rewards::reward_relationships(&relationship_id);
+        assert_eq!(relationship.next_payment_at_block, Some(next_payout_at));
+
+        let starting_balance = Balances::free_balance(&recipient_account);
+
+        // try to catch 'off by one' bugs
+        Rewards::do_payouts(next_payout_at - 1);
+        assert_eq!(Balances::free_balance(&recipient_account), starting_balance);
+        Rewards::do_payouts(next_payout_at + 1);
+        assert_eq!(Balances::free_balance(&recipient_account), starting_balance);
+
+        assert_eq!(MockStatusHandler::successes(), 0);
+
+        Rewards::do_payouts(next_payout_at);
+        assert_eq!(
+            Balances::free_balance(&recipient_account),
+            starting_balance + payout
+        );
+        assert_eq!(MockStatusHandler::successes(), 1);
+
+        let relationship = Rewards::reward_relationships(&relationship_id);
+        assert_eq!(relationship.total_reward_received, payout);
+        assert_eq!(relationship.next_payment_at_block, None);
+
+        let recipient = Rewards::recipients(&recipient_id);
+        assert_eq!(recipient.total_reward_received, payout);
+    });
+}
+
+#[test]
+fn recurring_payout() {
+    build_test_externalities().execute_with(|| {
+        System::set_block_number(10000);
+        let recipient_account: u64 = 1;
+        let _ = Balances::deposit_creating(&recipient_account, 400);
+        let mint_id = create_new_mint_with_capacity(1000000);
+        let recipient_id = Rewards::add_recipient();
+        let payout: u64 = 1000;
+        let next_payout_at: u64 = 12222;
+        let interval: u64 = 600;
+        let relationship = Rewards::add_reward_relationship(
+            mint_id,
+            recipient_id,
+            recipient_account,
+            payout,
+            next_payout_at,
+            Some(interval),
+        );
+        assert!(relationship.is_ok());
+        let relationship_id = relationship.ok().unwrap();
+
+        let relationship = Rewards::reward_relationships(&relationship_id);
+        assert_eq!(relationship.next_payment_at_block, Some(next_payout_at));
+
+        let starting_balance = Balances::free_balance(&recipient_account);
+
+        let number_of_payouts = 3;
+        for i in 0..number_of_payouts {
+            Rewards::do_payouts(next_payout_at + interval * i);
+        }
+        assert_eq!(MockStatusHandler::successes(), number_of_payouts as usize);
+
+        assert_eq!(
+            Balances::free_balance(&recipient_account),
+            starting_balance + payout * number_of_payouts
+        );
+
+        let relationship = Rewards::reward_relationships(&relationship_id);
+        assert_eq!(
+            relationship.total_reward_received,
+            payout * number_of_payouts
+        );
+
+        let recipient = Rewards::recipients(&recipient_id);
+        assert_eq!(recipient.total_reward_received, payout * number_of_payouts);
+    });
+}
+
+#[test]
+fn track_missed_payouts() {
+    build_test_externalities().execute_with(|| {
+        System::set_block_number(10000);
+        let recipient_account: u64 = 1;
+        let _ = Balances::deposit_creating(&recipient_account, 400);
+        let mint_id = create_new_mint_with_capacity(0);
+        let recipient_id = Rewards::add_recipient();
+        let payout: u64 = 1000;
+        let next_payout_at: u64 = 12222;
+        let relationship = Rewards::add_reward_relationship(
+            mint_id,
+            recipient_id,
+            recipient_account,
+            payout,
+            next_payout_at,
+            None,
+        );
+        assert!(relationship.is_ok());
+        let relationship_id = relationship.ok().unwrap();
+
+        let relationship = Rewards::reward_relationships(&relationship_id);
+        assert_eq!(relationship.next_payment_at_block, Some(next_payout_at));
+
+        let starting_balance = Balances::free_balance(&recipient_account);
+
+        Rewards::do_payouts(next_payout_at);
+        assert_eq!(Balances::free_balance(&recipient_account), starting_balance);
+
+        assert_eq!(MockStatusHandler::failures(), 1);
+
+        let relationship = Rewards::reward_relationships(&relationship_id);
+        assert_eq!(relationship.total_reward_received, 0);
+        assert_eq!(relationship.total_reward_missed, payout);
+
+        let recipient = Rewards::recipients(&recipient_id);
+        assert_eq!(recipient.total_reward_received, 0);
+        assert_eq!(recipient.total_reward_missed, payout);
+    });
+}
+
+#[test]
+fn activate_and_deactivate_relationship() {
+    build_test_externalities().execute_with(|| {
+        System::set_block_number(10000);
+        let recipient_account: u64 = 1;
+        let _ = Balances::deposit_creating(&recipient_account, 400);
+        let mint_id = create_new_mint_with_capacity(0);
+        let recipient_id = Rewards::add_recipient();
+        let payout: u64 = 1000;
+        let next_payout_at: u64 = 12222;
+
+        // Add relationship
+        let relationship_id = Rewards::add_reward_relationship(
+            mint_id,
+            recipient_id,
+            recipient_account,
+            payout,
+            next_payout_at,
+            None,
+        )
+        .unwrap();
+
+        // The relationship starts out active
+        assert!(Rewards::reward_relationships(&relationship_id).is_active());
+
+        // We are able to deactivate relationship
+        assert!(Rewards::try_to_deactivate_relationship(relationship_id).unwrap());
+
+        // The relationship is no longer active
+        assert!(!Rewards::reward_relationships(&relationship_id).is_active());
+
+        // We cannot deactivate an already deactivated relationship
+        assert!(!Rewards::try_to_deactivate_relationship(relationship_id).unwrap());
+
+        // We are able to activate relationship
+        assert!(Rewards::try_to_activate_relationship(relationship_id, next_payout_at).unwrap());
+
+        // The relationship is now not active
+        assert!(Rewards::reward_relationships(&relationship_id).is_active());
+
+        // We cannot activate an already active relationship
+        assert!(!Rewards::try_to_activate_relationship(relationship_id, next_payout_at).unwrap());
+    });
+}

+ 50 - 0
runtime-modules/stake/Cargo.toml

@@ -0,0 +1,50 @@
+[package]
+name = 'substrate-stake-module'
+version = '1.0.1'
+authors = ['Joystream contributors']
+edition = '2018'
+
+[dependencies]
+hex-literal = '0.1.0'
+serde = { version = '1.0', optional = true }
+serde_derive = { version = '1.0', optional = true }
+rstd = { package = 'sr-std', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+runtime-primitives = { package = 'sr-primitives', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+srml-support = { package = 'srml-support', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+srml-support-procedural = { package = 'srml-support-procedural', git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+system = { package = 'srml-system', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+balances = { package = 'srml-balances', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+codec = { package = 'parity-scale-codec', version = '1.0.0', default-features = false, features = ['derive'] }
+# https://users.rust-lang.org/t/failure-derive-compilation-error/39062
+quote = '<=1.0.2'
+
+[dependencies.timestamp]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'srml-timestamp'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dependencies.runtime-io]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'sr-io'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dev-dependencies]
+runtime-io = { package = 'sr-io', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+primitives = { package = 'substrate-primitives', git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+
+[features]
+default = ['std']
+std = [
+	'serde',
+	'serde_derive',
+	'codec/std',
+	'rstd/std',
+	'runtime-io/std',
+	'runtime-primitives/std',
+	'srml-support/std',
+	'system/std',
+  	'balances/std',
+	'timestamp/std',
+]

+ 158 - 0
runtime-modules/stake/src/errors.rs

@@ -0,0 +1,158 @@
+#[derive(Debug, Eq, PartialEq)]
+pub enum StakeActionError<ErrorType> {
+    StakeNotFound,
+    Error(ErrorType),
+}
+
+#[derive(Debug, Eq, PartialEq)]
+pub enum TransferFromAccountError {
+    InsufficientBalance,
+}
+
+#[derive(Debug, Eq, PartialEq)]
+pub enum StakingError {
+    CannotStakeZero,
+    CannotStakeLessThanMinimumBalance,
+    AlreadyStaked,
+}
+
+#[derive(Debug, Eq, PartialEq)]
+pub enum StakingFromAccountError {
+    StakingError(StakingError),
+    InsufficientBalanceInSourceAccount,
+}
+
+#[derive(Debug, Eq, PartialEq)]
+pub enum IncreasingStakeError {
+    NotStaked,
+    CannotChangeStakeByZero,
+    CannotIncreaseStakeWhileUnstaking,
+}
+
+#[derive(Debug, Eq, PartialEq)]
+pub enum IncreasingStakeFromAccountError {
+    IncreasingStakeError(IncreasingStakeError),
+    InsufficientBalanceInSourceAccount,
+}
+
+#[derive(Debug, Eq, PartialEq)]
+pub enum DecreasingStakeError {
+    NotStaked,
+    CannotChangeStakeByZero,
+    CannotDecreaseStakeWhileOngoingSlahes,
+    InsufficientStake,
+    CannotDecreaseStakeWhileUnstaking,
+}
+
+#[derive(Debug, Eq, PartialEq)]
+pub enum InitiateSlashingError {
+    NotStaked,
+    SlashPeriodShouldBeGreaterThanZero,
+    SlashAmountShouldBeGreaterThanZero,
+}
+
+#[derive(Debug, Eq, PartialEq)]
+pub enum PauseSlashingError {
+    SlashNotFound,
+    NotStaked,
+    AlreadyPaused,
+}
+
+#[derive(Debug, Eq, PartialEq)]
+pub enum ResumeSlashingError {
+    SlashNotFound,
+    NotStaked,
+    NotPaused,
+}
+
+#[derive(Debug, Eq, PartialEq)]
+pub enum CancelSlashingError {
+    SlashNotFound,
+    NotStaked,
+}
+
+#[derive(Debug, Eq, PartialEq)]
+pub enum UnstakingError {
+    NotStaked,
+    AlreadyUnstaking,
+    CannotUnstakeWhileSlashesOngoing,
+}
+
+#[derive(Debug, Eq, PartialEq)]
+pub enum InitiateUnstakingError {
+    UnstakingError(UnstakingError),
+    UnstakingPeriodShouldBeGreaterThanZero,
+}
+
+#[derive(Debug, Eq, PartialEq)]
+pub enum PauseUnstakingError {
+    NotStaked,
+    NotUnstaking,
+    AlreadyPaused,
+}
+
+#[derive(Debug, Eq, PartialEq)]
+pub enum ResumeUnstakingError {
+    NotStaked,
+    NotUnstaking,
+    NotPaused,
+}
+
+impl<ErrorType> From<ErrorType> for StakeActionError<ErrorType> {
+    fn from(e: ErrorType) -> StakeActionError<ErrorType> {
+        StakeActionError::Error(e)
+    }
+}
+
+impl From<StakingError> for StakeActionError<StakingFromAccountError> {
+    fn from(e: StakingError) -> StakeActionError<StakingFromAccountError> {
+        StakeActionError::Error(StakingFromAccountError::StakingError(e))
+    }
+}
+
+impl From<TransferFromAccountError> for StakeActionError<StakingFromAccountError> {
+    fn from(e: TransferFromAccountError) -> StakeActionError<StakingFromAccountError> {
+        match e {
+            TransferFromAccountError::InsufficientBalance => {
+                StakeActionError::Error(StakingFromAccountError::InsufficientBalanceInSourceAccount)
+            }
+        }
+    }
+}
+
+impl From<IncreasingStakeError> for StakeActionError<IncreasingStakeFromAccountError> {
+    fn from(e: IncreasingStakeError) -> StakeActionError<IncreasingStakeFromAccountError> {
+        StakeActionError::Error(IncreasingStakeFromAccountError::IncreasingStakeError(e))
+    }
+}
+
+impl From<TransferFromAccountError> for StakeActionError<IncreasingStakeFromAccountError> {
+    fn from(e: TransferFromAccountError) -> StakeActionError<IncreasingStakeFromAccountError> {
+        match e {
+            TransferFromAccountError::InsufficientBalance => StakeActionError::Error(
+                IncreasingStakeFromAccountError::InsufficientBalanceInSourceAccount,
+            ),
+        }
+    }
+}
+
+impl From<StakeActionError<IncreasingStakeError>>
+    for StakeActionError<IncreasingStakeFromAccountError>
+{
+    fn from(
+        e: StakeActionError<IncreasingStakeError>,
+    ) -> StakeActionError<IncreasingStakeFromAccountError> {
+        match e {
+            StakeActionError::StakeNotFound => StakeActionError::StakeNotFound,
+            StakeActionError::Error(increasing_stake_error) => StakeActionError::Error(
+                IncreasingStakeFromAccountError::IncreasingStakeError(increasing_stake_error),
+            ),
+        }
+    }
+}
+
+impl From<UnstakingError> for StakeActionError<InitiateUnstakingError> {
+    fn from(e: UnstakingError) -> StakeActionError<InitiateUnstakingError> {
+        StakeActionError::Error(InitiateUnstakingError::UnstakingError(e))
+    }
+}

+ 1083 - 0
runtime-modules/stake/src/lib.rs

@@ -0,0 +1,1083 @@
+// Ensure we're `no_std` when compiling for Wasm.
+#![cfg_attr(not(feature = "std"), no_std)]
+
+use rstd::prelude::*;
+
+use codec::{Codec, Decode, Encode};
+use runtime_primitives::traits::{
+    AccountIdConversion, MaybeSerialize, Member, One, SimpleArithmetic, Zero,
+};
+use runtime_primitives::ModuleId;
+use srml_support::traits::{Currency, ExistenceRequirement, Get, Imbalance, WithdrawReasons};
+use srml_support::{decl_module, decl_storage, ensure, Parameter};
+
+use rstd::collections::btree_map::BTreeMap;
+use system;
+
+mod errors;
+pub use errors::*;
+mod macroes;
+mod mock;
+mod tests;
+
+pub type BalanceOf<T> =
+    <<T as Trait>::Currency as Currency<<T as system::Trait>::AccountId>>::Balance;
+
+pub type NegativeImbalance<T> =
+    <<T as Trait>::Currency as Currency<<T as system::Trait>::AccountId>>::NegativeImbalance;
+
+pub trait Trait: system::Trait + Sized {
+    /// The currency that is managed by the module
+    type Currency: Currency<Self::AccountId>;
+
+    /// ModuleId for computing deterministic AccountId for the module
+    type StakePoolId: Get<[u8; 8]>;
+
+    /// Type that will handle various staking events
+    type StakingEventsHandler: StakingEventsHandler<Self>;
+
+    /// The type used as a stake identifier.
+    type StakeId: Parameter
+        + Member
+        + SimpleArithmetic
+        + Codec
+        + Default
+        + Copy
+        + MaybeSerialize
+        + PartialEq;
+
+    /// The type used as slash identifier.
+    type SlashId: Parameter
+        + Member
+        + SimpleArithmetic
+        + Codec
+        + Default
+        + Copy
+        + MaybeSerialize
+        + PartialEq
+        + Ord; //required to be a key in BTreeMap
+}
+
+pub trait StakingEventsHandler<T: Trait> {
+    /// Handler for unstaking event.
+    /// The handler is informed of the amount that was unstaked, and the value removed from stake is passed as a negative imbalance.
+    /// The handler is responsible to consume part or all of the value (for example by moving it into an account). The remainder
+    /// of the value that is not consumed should be returned as a negative imbalance.
+    fn unstaked(
+        id: &T::StakeId,
+        unstaked_amount: BalanceOf<T>,
+        remaining_imbalance: NegativeImbalance<T>,
+    ) -> NegativeImbalance<T>;
+
+    // Handler for slashing event.
+    // NB: actually_slashed can be less than amount of the slash itself if the
+    // claim amount on the stake cannot cover it fully.
+    fn slashed(
+        id: &T::StakeId,
+        slash_id: &T::SlashId,
+        slashed_amount: BalanceOf<T>,
+        remaining_stake: BalanceOf<T>,
+        remaining_imbalance: NegativeImbalance<T>,
+    ) -> NegativeImbalance<T>;
+}
+
+/// Default implementation just destroys the unstaked or slashed value
+impl<T: Trait> StakingEventsHandler<T> for () {
+    fn unstaked(
+        _id: &T::StakeId,
+        _unstaked_amount: BalanceOf<T>,
+        _remaining_imbalance: NegativeImbalance<T>,
+    ) -> NegativeImbalance<T> {
+        NegativeImbalance::<T>::zero()
+    }
+
+    fn slashed(
+        _id: &T::StakeId,
+        _slash_id: &T::SlashId,
+        _slahed_amount: BalanceOf<T>,
+        _remaining_stake: BalanceOf<T>,
+        _remaining_imbalance: NegativeImbalance<T>,
+    ) -> NegativeImbalance<T> {
+        NegativeImbalance::<T>::zero()
+    }
+}
+
+/// Helper implementation so we can chain multiple handlers by grouping handlers in tuple pairs.
+/// For example for three handlers, A, B and C we can set the StakingEventHandler type on the trait to:
+/// type StakingEventHandler = ((A, B), C)
+/// Individual handlers are expected consume in full or in part the negative imbalance and return any unconsumed value.
+/// The unconsumed value is then passed to the next handler in the chain.
+impl<T: Trait, X: StakingEventsHandler<T>, Y: StakingEventsHandler<T>> StakingEventsHandler<T>
+    for (X, Y)
+{
+    fn unstaked(
+        id: &T::StakeId,
+        unstaked_amount: BalanceOf<T>,
+        imbalance: NegativeImbalance<T>,
+    ) -> NegativeImbalance<T> {
+        let unused_imbalance = X::unstaked(id, unstaked_amount, imbalance);
+        Y::unstaked(id, unstaked_amount, unused_imbalance)
+    }
+
+    fn slashed(
+        id: &T::StakeId,
+        slash_id: &T::SlashId,
+        slashed_amount: BalanceOf<T>,
+        remaining_stake: BalanceOf<T>,
+        imbalance: NegativeImbalance<T>,
+    ) -> NegativeImbalance<T> {
+        let unused_imbalance = X::slashed(id, slash_id, slashed_amount, remaining_stake, imbalance);
+        Y::slashed(
+            id,
+            slash_id,
+            slashed_amount,
+            remaining_stake,
+            unused_imbalance,
+        )
+    }
+}
+
+#[derive(Encode, Decode, Copy, Clone, Debug, Default, Eq, PartialEq)]
+pub struct Slash<BlockNumber, Balance> {
+    /// The block where slashing was initiated.
+    pub started_at_block: BlockNumber,
+
+    /// Whether slashing is in active, or conversley paused state.
+    /// Blocks are only counted towards slashing execution delay when active.
+    pub is_active: bool,
+
+    /// The number blocks which must be finalised while in the active period before the slashing can be executed
+    pub blocks_remaining_in_active_period_for_slashing: BlockNumber,
+
+    /// Amount to slash
+    pub slash_amount: Balance,
+}
+
+#[derive(Encode, Decode, Debug, Default, Eq, PartialEq)]
+pub struct UnstakingState<BlockNumber> {
+    /// The block where the unstaking was initiated
+    pub started_at_block: BlockNumber,
+
+    /// Whether unstaking is in active, or conversely paused state.
+    /// Blocks are only counted towards unstaking period when active.
+    pub is_active: bool,
+
+    /// The number blocks which must be finalised while in the active period before the unstaking is finished
+    pub blocks_remaining_in_active_period_for_unstaking: BlockNumber,
+}
+
+#[derive(Encode, Decode, Debug, Eq, PartialEq)]
+pub enum StakedStatus<BlockNumber> {
+    /// Baseline staking status, nothing is happening.
+    Normal,
+
+    /// Unstaking is under way.
+    Unstaking(UnstakingState<BlockNumber>),
+}
+
+impl<BlockNumber> Default for StakedStatus<BlockNumber> {
+    fn default() -> Self {
+        StakedStatus::Normal
+    }
+}
+
+#[derive(Encode, Decode, Debug, Default, Eq, PartialEq)]
+pub struct StakedState<BlockNumber, Balance, SlashId: Ord> {
+    /// Total amount of funds at stake.
+    pub staked_amount: Balance,
+
+    /// Status of the staking.
+    pub staked_status: StakedStatus<BlockNumber>,
+
+    /// SlashId to use for next Slash that is initiated.
+    /// Will be incremented by one after adding a new Slash.
+    pub next_slash_id: SlashId,
+
+    /// All ongoing slashing.
+    pub ongoing_slashes: BTreeMap<SlashId, Slash<BlockNumber, Balance>>,
+}
+
+impl<BlockNumber, Balance, SlashId> StakedState<BlockNumber, Balance, SlashId>
+where
+    BlockNumber: SimpleArithmetic + Copy,
+    Balance: SimpleArithmetic + Copy,
+    SlashId: Ord + Copy,
+{
+    /// Iterates over all ongoing slashes and decrements blocks_remaining_in_active_period_for_slashing of active slashes (advancing the timer).
+    /// Returns true if there was at least one slashe that was active and had its timer advanced.
+    fn advance_slashing_timer(&mut self) -> bool {
+        let mut did_advance_timers = false;
+
+        for (_slash_id, slash) in self.ongoing_slashes.iter_mut() {
+            if slash.is_active
+                && slash.blocks_remaining_in_active_period_for_slashing > Zero::zero()
+            {
+                slash.blocks_remaining_in_active_period_for_slashing -= One::one();
+                did_advance_timers = true;
+            }
+        }
+
+        did_advance_timers
+    }
+
+    /// Returns pair of slash_id and slashes that should be executed
+    fn get_slashes_to_finalize(&mut self) -> Vec<(SlashId, Slash<BlockNumber, Balance>)> {
+        let slashes_to_finalize = self
+            .ongoing_slashes
+            .iter()
+            .filter(|(_, slash)| {
+                slash.blocks_remaining_in_active_period_for_slashing == Zero::zero()
+            })
+            .map(|(slash_id, _)| *slash_id)
+            .collect::<Vec<_>>();
+
+        // remove and return the slashes
+        slashes_to_finalize
+            .iter()
+            .map(|slash_id| {
+                // assert!(self.ongoing_slashes.contains_key(slash_id))
+                (*slash_id, self.ongoing_slashes.remove(slash_id).unwrap())
+            })
+            .collect()
+    }
+
+    /// Executes a Slash. If remaining at stake drops below the minimum_balance, it will slash the entire staked amount.
+    /// Returns the actual slashed amount.
+    fn apply_slash(
+        &mut self,
+        slash: Slash<BlockNumber, Balance>,
+        minimum_balance: Balance,
+    ) -> Balance {
+        // calculate how much to slash
+        let mut slash_amount = if slash.slash_amount > self.staked_amount {
+            self.staked_amount
+        } else {
+            slash.slash_amount
+        };
+
+        // apply the slashing
+        self.staked_amount -= slash_amount;
+
+        // don't leave less than minimum_balance at stake
+        if self.staked_amount < minimum_balance {
+            slash_amount += self.staked_amount;
+            self.staked_amount = Zero::zero();
+        }
+
+        slash_amount
+    }
+
+    /// For all slahes that should be executed, will apply the Slash to the staked amount, and drop it from the ongoing slashes map.
+    /// Returns a vector of the executed slashes outcome: (SlashId, Slashed Amount, Remaining Staked Amount)
+    fn finalize_slashes(&mut self, minimum_balance: Balance) -> Vec<(SlashId, Balance, Balance)> {
+        let mut finalized_slashes: Vec<(SlashId, Balance, Balance)> = vec![];
+
+        for (slash_id, slash) in self.get_slashes_to_finalize().iter() {
+            // apply the slashing and get back actual amount slashed
+            let slashed_amount = self.apply_slash(*slash, minimum_balance);
+
+            finalized_slashes.push((*slash_id, slashed_amount, self.staked_amount));
+        }
+
+        finalized_slashes
+    }
+}
+
+#[derive(Encode, Decode, Debug, Eq, PartialEq)]
+pub enum StakingStatus<BlockNumber, Balance, SlashId: Ord> {
+    NotStaked,
+
+    Staked(StakedState<BlockNumber, Balance, SlashId>),
+}
+
+impl<BlockNumber, Balance, SlashId: Ord> Default for StakingStatus<BlockNumber, Balance, SlashId> {
+    fn default() -> Self {
+        StakingStatus::NotStaked
+    }
+}
+
+#[derive(Encode, Decode, Default, Debug, Eq, PartialEq)]
+pub struct Stake<BlockNumber, Balance, SlashId: Ord> {
+    /// When role was created
+    pub created: BlockNumber,
+
+    /// Status of any possible ongoing staking
+    pub staking_status: StakingStatus<BlockNumber, Balance, SlashId>,
+}
+
+impl<BlockNumber, Balance, SlashId> Stake<BlockNumber, Balance, SlashId>
+where
+    BlockNumber: Copy + SimpleArithmetic,
+    Balance: Copy + SimpleArithmetic,
+    SlashId: Copy + Ord + Zero + One,
+{
+    fn new(created_at: BlockNumber) -> Self {
+        Self {
+            created: created_at,
+            staking_status: StakingStatus::NotStaked,
+        }
+    }
+
+    fn is_not_staked(&self) -> bool {
+        self.staking_status == StakingStatus::NotStaked
+    }
+
+    /// If staking status is Staked and not currently Unstaking it will increase the staked amount by value.
+    /// On success returns new total staked value.
+    /// Increasing stake by zero is an error.
+    fn increase_stake(&mut self, value: Balance) -> Result<Balance, IncreasingStakeError> {
+        ensure!(
+            value > Zero::zero(),
+            IncreasingStakeError::CannotChangeStakeByZero
+        );
+        match self.staking_status {
+            StakingStatus::Staked(ref mut staked_state) => match staked_state.staked_status {
+                StakedStatus::Normal => {
+                    staked_state.staked_amount += value;
+                    Ok(staked_state.staked_amount)
+                }
+                _ => Err(IncreasingStakeError::CannotIncreaseStakeWhileUnstaking),
+            },
+            _ => Err(IncreasingStakeError::NotStaked),
+        }
+    }
+
+    /// If staking status is Staked and not currently Unstaking, and no ongoing slashes exist, it will decrease the amount at stake
+    /// by provided value. If remaining at stake drops below the minimum_balance it will decrease the stake to zero.
+    /// On success returns (the actual amount of stake decreased, the remaining amount at stake).
+    /// Decreasing stake by zero is an error.
+    fn decrease_stake(
+        &mut self,
+        value: Balance,
+        minimum_balance: Balance,
+    ) -> Result<(Balance, Balance), DecreasingStakeError> {
+        // maybe StakeDecrease
+        ensure!(
+            value > Zero::zero(),
+            DecreasingStakeError::CannotChangeStakeByZero
+        );
+        match self.staking_status {
+            StakingStatus::Staked(ref mut staked_state) => match staked_state.staked_status {
+                StakedStatus::Normal => {
+                    // prevent decreasing stake if there are any ongoing slashes (irrespective if active or not)
+                    if !staked_state.ongoing_slashes.is_empty() {
+                        return Err(DecreasingStakeError::CannotDecreaseStakeWhileOngoingSlahes);
+                    }
+
+                    if value > staked_state.staked_amount {
+                        return Err(DecreasingStakeError::InsufficientStake);
+                    }
+
+                    let stake_to_reduce = if staked_state.staked_amount - value < minimum_balance {
+                        // If staked amount would drop below minimum balance, deduct the entire stake
+                        staked_state.staked_amount
+                    } else {
+                        value
+                    };
+
+                    staked_state.staked_amount -= stake_to_reduce;
+
+                    Ok((stake_to_reduce, staked_state.staked_amount))
+                }
+                _ => return Err(DecreasingStakeError::CannotDecreaseStakeWhileUnstaking),
+            },
+            _ => return Err(DecreasingStakeError::NotStaked),
+        }
+    }
+
+    fn start_staking(
+        &mut self,
+        value: Balance,
+        minimum_balance: Balance,
+    ) -> Result<(), StakingError> {
+        ensure!(value > Zero::zero(), StakingError::CannotStakeZero);
+        ensure!(
+            value >= minimum_balance,
+            StakingError::CannotStakeLessThanMinimumBalance
+        );
+        if self.is_not_staked() {
+            self.staking_status = StakingStatus::Staked(StakedState {
+                staked_amount: value,
+                next_slash_id: Zero::zero(),
+                ongoing_slashes: BTreeMap::new(),
+                staked_status: StakedStatus::Normal,
+            });
+            Ok(())
+        } else {
+            Err(StakingError::AlreadyStaked)
+        }
+    }
+
+    fn initiate_slashing(
+        &mut self,
+        slash_amount: Balance,
+        slash_period: BlockNumber,
+        now: BlockNumber,
+    ) -> Result<SlashId, InitiateSlashingError> {
+        ensure!(
+            slash_period > Zero::zero(),
+            InitiateSlashingError::SlashPeriodShouldBeGreaterThanZero
+        );
+        ensure!(
+            slash_amount > Zero::zero(),
+            InitiateSlashingError::SlashAmountShouldBeGreaterThanZero
+        );
+
+        match self.staking_status {
+            StakingStatus::Staked(ref mut staked_state) => {
+                let slash_id = staked_state.next_slash_id;
+                staked_state.next_slash_id = slash_id + One::one();
+
+                staked_state.ongoing_slashes.insert(
+                    slash_id,
+                    Slash {
+                        is_active: true,
+                        blocks_remaining_in_active_period_for_slashing: slash_period,
+                        slash_amount,
+                        started_at_block: now,
+                    },
+                );
+
+                // pause Unstaking if unstaking is active
+                match staked_state.staked_status {
+                    StakedStatus::Unstaking(ref mut unstaking_state) => {
+                        unstaking_state.is_active = false;
+                    }
+                    _ => (),
+                }
+
+                Ok(slash_id)
+            }
+            _ => Err(InitiateSlashingError::NotStaked),
+        }
+    }
+
+    fn pause_slashing(&mut self, slash_id: &SlashId) -> Result<(), PauseSlashingError> {
+        match self.staking_status {
+            StakingStatus::Staked(ref mut staked_state) => {
+                match staked_state.ongoing_slashes.get_mut(slash_id) {
+                    Some(ref mut slash) => {
+                        if slash.is_active {
+                            slash.is_active = false;
+                            Ok(())
+                        } else {
+                            Err(PauseSlashingError::AlreadyPaused)
+                        }
+                    }
+                    _ => Err(PauseSlashingError::SlashNotFound),
+                }
+            }
+            _ => Err(PauseSlashingError::NotStaked),
+        }
+    }
+
+    fn resume_slashing(&mut self, slash_id: &SlashId) -> Result<(), ResumeSlashingError> {
+        match self.staking_status {
+            StakingStatus::Staked(ref mut staked_state) => {
+                match staked_state.ongoing_slashes.get_mut(slash_id) {
+                    Some(ref mut slash) => {
+                        if slash.is_active {
+                            Err(ResumeSlashingError::NotPaused)
+                        } else {
+                            slash.is_active = true;
+                            Ok(())
+                        }
+                    }
+                    _ => Err(ResumeSlashingError::SlashNotFound),
+                }
+            }
+            _ => Err(ResumeSlashingError::NotStaked),
+        }
+    }
+
+    fn cancel_slashing(&mut self, slash_id: &SlashId) -> Result<(), CancelSlashingError> {
+        match self.staking_status {
+            StakingStatus::Staked(ref mut staked_state) => {
+                if staked_state.ongoing_slashes.remove(slash_id).is_none() {
+                    return Err(CancelSlashingError::SlashNotFound);
+                }
+
+                // unpause unstaking on last ongoing slash cancelled
+                if staked_state.ongoing_slashes.is_empty() {
+                    match staked_state.staked_status {
+                        StakedStatus::Unstaking(ref mut unstaking_state) => {
+                            unstaking_state.is_active = true;
+                        }
+                        _ => (),
+                    }
+                }
+
+                Ok(())
+            }
+            _ => Err(CancelSlashingError::NotStaked),
+        }
+    }
+
+    fn unstake(&mut self) -> Result<Balance, UnstakingError> {
+        let staked_amount = match self.staking_status {
+            StakingStatus::Staked(ref staked_state) => {
+                // prevent unstaking if there are any ongonig slashes (irrespective if active or not)
+                if !staked_state.ongoing_slashes.is_empty() {
+                    return Err(UnstakingError::CannotUnstakeWhileSlashesOngoing);
+                }
+                if StakedStatus::Normal != staked_state.staked_status {
+                    return Err(UnstakingError::AlreadyUnstaking);
+                }
+                Ok(staked_state.staked_amount)
+            }
+            _ => Err(UnstakingError::NotStaked),
+        }?;
+
+        self.staking_status = StakingStatus::NotStaked;
+        Ok(staked_amount)
+    }
+
+    fn initiate_unstaking(
+        &mut self,
+        unstaking_period: BlockNumber,
+        now: BlockNumber,
+    ) -> Result<(), InitiateUnstakingError> {
+        ensure!(
+            unstaking_period > Zero::zero(),
+            InitiateUnstakingError::UnstakingPeriodShouldBeGreaterThanZero
+        );
+        match self.staking_status {
+            StakingStatus::Staked(ref mut staked_state) => {
+                // prevent unstaking if there are any ongonig slashes (irrespective if active or not)
+                if !staked_state.ongoing_slashes.is_empty() {
+                    return Err(InitiateUnstakingError::UnstakingError(
+                        UnstakingError::CannotUnstakeWhileSlashesOngoing,
+                    ));
+                }
+
+                if StakedStatus::Normal != staked_state.staked_status {
+                    return Err(InitiateUnstakingError::UnstakingError(
+                        UnstakingError::AlreadyUnstaking,
+                    ));
+                }
+
+                staked_state.staked_status = StakedStatus::Unstaking(UnstakingState {
+                    started_at_block: now,
+                    is_active: true,
+                    blocks_remaining_in_active_period_for_unstaking: unstaking_period,
+                });
+
+                Ok(())
+            }
+            _ => Err(InitiateUnstakingError::UnstakingError(
+                UnstakingError::NotStaked,
+            )),
+        }
+    }
+
+    fn pause_unstaking(&mut self) -> Result<(), PauseUnstakingError> {
+        match self.staking_status {
+            StakingStatus::Staked(ref mut staked_state) => match staked_state.staked_status {
+                StakedStatus::Unstaking(ref mut unstaking_state) => {
+                    if unstaking_state.is_active {
+                        unstaking_state.is_active = false;
+                        Ok(())
+                    } else {
+                        Err(PauseUnstakingError::AlreadyPaused)
+                    }
+                }
+                _ => Err(PauseUnstakingError::NotUnstaking),
+            },
+            _ => Err(PauseUnstakingError::NotStaked),
+        }
+    }
+
+    fn resume_unstaking(&mut self) -> Result<(), ResumeUnstakingError> {
+        match self.staking_status {
+            StakingStatus::Staked(ref mut staked_state) => match staked_state.staked_status {
+                StakedStatus::Unstaking(ref mut unstaking_state) => {
+                    if !unstaking_state.is_active {
+                        unstaking_state.is_active = true;
+                        Ok(())
+                    } else {
+                        Err(ResumeUnstakingError::NotPaused)
+                    }
+                }
+                _ => Err(ResumeUnstakingError::NotUnstaking),
+            },
+            _ => Err(ResumeUnstakingError::NotStaked),
+        }
+    }
+
+    fn finalize_slashing(
+        &mut self,
+        minimum_balance: Balance,
+    ) -> (bool, Vec<(SlashId, Balance, Balance)>) {
+        match self.staking_status {
+            StakingStatus::Staked(ref mut staked_state) => {
+                // tick the slashing timer
+                let did_update = staked_state.advance_slashing_timer();
+
+                // finalize and apply slashes
+                let slashed = staked_state.finalize_slashes(minimum_balance);
+
+                (did_update, slashed)
+            }
+            _ => (false, vec![]),
+        }
+    }
+
+    fn finalize_unstaking(&mut self) -> (bool, Option<Balance>) {
+        let (did_update, unstaked) = match self.staking_status {
+            StakingStatus::Staked(ref mut staked_state) => match staked_state.staked_status {
+                StakedStatus::Unstaking(ref mut unstaking_state) => {
+                    // if all slashes were processed and there are no more active slashes
+                    // resume unstaking
+                    if staked_state.ongoing_slashes.is_empty() {
+                        unstaking_state.is_active = true;
+                    }
+
+                    // tick the unstaking timer
+                    if unstaking_state.is_active
+                        && unstaking_state.blocks_remaining_in_active_period_for_unstaking
+                            > Zero::zero()
+                    {
+                        // tick the unstaking timer
+                        unstaking_state.blocks_remaining_in_active_period_for_unstaking -=
+                            One::one();
+                    }
+
+                    // finalize unstaking
+                    if unstaking_state.blocks_remaining_in_active_period_for_unstaking
+                        == Zero::zero()
+                    {
+                        (true, Some(staked_state.staked_amount))
+                    } else {
+                        (unstaking_state.is_active, None)
+                    }
+                }
+                _ => (false, None),
+            },
+            _ => (false, None),
+        };
+
+        // if unstaking was finalized transition to NotStaked state
+        if unstaked.is_some() {
+            self.staking_status = StakingStatus::NotStaked;
+        }
+
+        (did_update, unstaked)
+    }
+
+    fn finalize_slashing_and_unstaking(
+        &mut self,
+        minimum_balance: Balance,
+    ) -> (bool, Vec<(SlashId, Balance, Balance)>, Option<Balance>) {
+        let (did_update_slashing_timers, slashed) = self.finalize_slashing(minimum_balance);
+
+        let (did_update_unstaking_timer, unstaked) = self.finalize_unstaking();
+
+        (
+            did_update_slashing_timers || did_update_unstaking_timer,
+            slashed,
+            unstaked,
+        )
+    }
+}
+
+decl_storage! {
+    trait Store for Module<T: Trait> as StakePool {
+        /// Maps identifiers to a stake.
+        pub Stakes get(stakes): linked_map T::StakeId => Stake<T::BlockNumber, BalanceOf<T>, T::SlashId>;
+
+        /// Identifier value for next stake, and count of total stakes created (not necessarily the number of current
+        /// stakes in the Stakes map as stakes can be removed.)
+        pub StakesCreated get(stakes_created): T::StakeId;
+    }
+}
+
+decl_module! {
+    pub struct Module<T: Trait> for enum Call where origin: T::Origin {
+        fn on_finalize(_now: T::BlockNumber) {
+            Self::finalize_slashing_and_unstaking();
+        }
+    }
+}
+
+impl<T: Trait> Module<T> {
+    /// The account ID of theis module which holds all the staked balance. (referred to as the stake pool)
+    ///
+    /// This actually does computation. If you need to keep using it, then make sure you cache the
+    /// value and only call this once. Is it deterministic?
+    pub fn stake_pool_account_id() -> T::AccountId {
+        ModuleId(T::StakePoolId::get()).into_account()
+    }
+
+    pub fn stake_pool_balance() -> BalanceOf<T> {
+        T::Currency::free_balance(&Self::stake_pool_account_id())
+    }
+
+    /// Adds a new Stake which is NotStaked, created at given block, into stakes map.
+    pub fn create_stake() -> T::StakeId {
+        let stake_id = Self::stakes_created();
+        <StakesCreated<T>>::put(stake_id + One::one());
+
+        <Stakes<T>>::insert(&stake_id, Stake::new(<system::Module<T>>::block_number()));
+
+        stake_id
+    }
+
+    /// Given that stake with id exists in stakes and is NotStaked, remove from stakes.
+    pub fn remove_stake(stake_id: &T::StakeId) -> Result<(), StakeActionError<StakingError>> {
+        let stake = ensure_stake_exists!(T, stake_id, StakeActionError::StakeNotFound)?;
+
+        ensure!(
+            stake.is_not_staked(),
+            StakeActionError::Error(StakingError::AlreadyStaked)
+        );
+
+        <Stakes<T>>::remove(stake_id);
+
+        Ok(())
+    }
+
+    /// Dry run to see if staking can be initiated for the specified stake id. This should
+    /// be called before stake() to make sure staking is possible before withdrawing funds.
+    pub fn ensure_can_stake(
+        stake_id: &T::StakeId,
+        value: BalanceOf<T>,
+    ) -> Result<(), StakeActionError<StakingError>> {
+        let mut stake = ensure_stake_exists!(T, stake_id, StakeActionError::StakeNotFound)?;
+
+        stake
+            .start_staking(value, T::Currency::minimum_balance())
+            .err()
+            .map_or(Ok(()), |err| Err(StakeActionError::Error(err)))
+    }
+
+    /// Provided the stake exists and is in state NotStaked the value is transferred
+    /// to the module's account, and the corresponding staked_balance is set to this amount in the new Staked state.
+    /// On error, as the negative imbalance is not returned to the caller, it is the caller's responsibility to return the funds
+    /// back to the source (by creating a new positive imbalance)
+    pub fn stake(
+        stake_id: &T::StakeId,
+        imbalance: NegativeImbalance<T>,
+    ) -> Result<(), StakeActionError<StakingError>> {
+        let mut stake = ensure_stake_exists!(T, stake_id, StakeActionError::StakeNotFound)?;
+
+        let value = imbalance.peek();
+
+        stake.start_staking(value, T::Currency::minimum_balance())?;
+
+        <Stakes<T>>::insert(stake_id, stake);
+
+        Self::deposit_funds_into_stake_pool(imbalance);
+
+        Ok(())
+    }
+
+    pub fn stake_from_account(
+        stake_id: &T::StakeId,
+        source_account_id: &T::AccountId,
+        value: BalanceOf<T>,
+    ) -> Result<(), StakeActionError<StakingFromAccountError>> {
+        let mut stake = ensure_stake_exists!(T, stake_id, StakeActionError::StakeNotFound)?;
+
+        stake.start_staking(value, T::Currency::minimum_balance())?;
+
+        // Its important to only do the transfer as the last step to ensure starting staking was possible.
+        Self::transfer_funds_from_account_into_stake_pool(source_account_id, value)?;
+
+        <Stakes<T>>::insert(stake_id, stake);
+
+        Ok(())
+    }
+
+    /// Moves funds from specified account into the module's account
+    fn transfer_funds_from_account_into_stake_pool(
+        source: &T::AccountId,
+        value: BalanceOf<T>,
+    ) -> Result<(), TransferFromAccountError> {
+        // We don't use T::Currency::transfer() to prevent fees being incurred.
+        let negative_imbalance = T::Currency::withdraw(
+            source,
+            value,
+            WithdrawReasons::all(),
+            ExistenceRequirement::AllowDeath,
+        )
+        .map_err(|_err| TransferFromAccountError::InsufficientBalance)?;
+
+        Self::deposit_funds_into_stake_pool(negative_imbalance);
+        Ok(())
+    }
+
+    fn deposit_funds_into_stake_pool(imbalance: NegativeImbalance<T>) {
+        // move the negative imbalance into the stake pool
+        T::Currency::resolve_creating(&Self::stake_pool_account_id(), imbalance);
+    }
+
+    /// Moves funds from the module's account into specified account. Should never fail if used internally.
+    /// Will panic! if value exceeds balance in the pool.
+    fn transfer_funds_from_pool_into_account(destination: &T::AccountId, value: BalanceOf<T>) {
+        let imbalance = Self::withdraw_funds_from_stake_pool(value);
+        T::Currency::resolve_creating(destination, imbalance);
+    }
+
+    /// Withdraws value from the pool and returns a NegativeImbalance.
+    /// As long as it is only called internally when executing slashes and unstaking, it
+    /// should never fail as the pool balance is always in sync with total amount at stake.
+    fn withdraw_funds_from_stake_pool(value: BalanceOf<T>) -> NegativeImbalance<T> {
+        // We don't use T::Currency::transfer() to prevent fees being incurred.
+        T::Currency::withdraw(
+            &Self::stake_pool_account_id(),
+            value,
+            WithdrawReasons::all(),
+            ExistenceRequirement::AllowDeath,
+        )
+        .expect("pool had less than expected funds!")
+    }
+
+    /// Dry run to see if the state of stake allows for increasing stake. This should be called
+    /// to make sure increasing stake is possible before withdrawing funds.
+    pub fn ensure_can_increase_stake(
+        stake_id: &T::StakeId,
+        value: BalanceOf<T>,
+    ) -> Result<(), StakeActionError<IncreasingStakeError>> {
+        let mut stake = ensure_stake_exists!(T, stake_id, StakeActionError::StakeNotFound)?;
+
+        stake
+            .increase_stake(value)
+            .err()
+            .map_or(Ok(()), |err| Err(StakeActionError::Error(err)))
+    }
+
+    /// Provided the stake exists and is in state Staked.Normal, then the amount is transferred to the module's account,
+    /// and the corresponding staked_amount is increased by the value. New value of staked_amount is returned.
+    /// Caller should call check ensure_can_increase_stake() prior to avoid getting back an error. On error, as the negative imbalance
+    /// is not returned to the caller, it is the caller's responsibility to return the funds back to the source (by creating a new positive imbalance)
+    pub fn increase_stake(
+        stake_id: &T::StakeId,
+        imbalance: NegativeImbalance<T>,
+    ) -> Result<BalanceOf<T>, StakeActionError<IncreasingStakeError>> {
+        let mut stake = ensure_stake_exists!(T, stake_id, StakeActionError::StakeNotFound)?;
+
+        let total_staked_amount = stake.increase_stake(imbalance.peek())?;
+        <Stakes<T>>::insert(stake_id, stake);
+
+        Self::deposit_funds_into_stake_pool(imbalance);
+
+        Ok(total_staked_amount)
+    }
+
+    /// Provided the stake exists and is in state Staked.Normal, and the given source account covers the amount,
+    /// then the amount is transferred to the module's account, and the corresponding staked_amount is increased
+    /// by the amount. New value of staked_amount is returned.
+    pub fn increase_stake_from_account(
+        stake_id: &T::StakeId,
+        source_account_id: &T::AccountId,
+        value: BalanceOf<T>,
+    ) -> Result<BalanceOf<T>, StakeActionError<IncreasingStakeFromAccountError>> {
+        // Compiler error when using macro: cannot infer type for `ErrorType`
+        // let mut stake = ensure_stake_exists!(T, stake_id, StakeActionError::StakeNotFound)?;
+        ensure!(
+            <Stakes<T>>::exists(stake_id),
+            StakeActionError::StakeNotFound
+        );
+
+        let mut stake = Self::stakes(stake_id);
+
+        let total_staked_amount = stake.increase_stake(value)?;
+
+        Self::transfer_funds_from_account_into_stake_pool(&source_account_id, value)?;
+
+        <Stakes<T>>::insert(stake_id, stake);
+
+        Ok(total_staked_amount)
+    }
+
+    pub fn ensure_can_decrease_stake(
+        stake_id: &T::StakeId,
+        value: BalanceOf<T>,
+    ) -> Result<(), StakeActionError<DecreasingStakeError>> {
+        let mut stake = ensure_stake_exists!(T, stake_id, StakeActionError::StakeNotFound)?;
+
+        stake
+            .decrease_stake(value, T::Currency::minimum_balance())
+            .err()
+            .map_or(Ok(()), |err| Err(StakeActionError::Error(err)))
+    }
+
+    pub fn decrease_stake(
+        stake_id: &T::StakeId,
+        value: BalanceOf<T>,
+    ) -> Result<(BalanceOf<T>, NegativeImbalance<T>), StakeActionError<DecreasingStakeError>> {
+        let mut stake = ensure_stake_exists!(T, stake_id, StakeActionError::StakeNotFound)?;
+
+        let (deduct_from_pool, staked_amount) =
+            stake.decrease_stake(value, T::Currency::minimum_balance())?;
+
+        <Stakes<T>>::insert(stake_id, stake);
+
+        let imbalance = Self::withdraw_funds_from_stake_pool(deduct_from_pool);
+
+        Ok((staked_amount, imbalance))
+    }
+
+    /// Provided the stake exists and is in state Staked.Normal, and the given stake holds at least the value,
+    /// then the value is transferred from the module's account to the destination_account, and the corresponding
+    /// staked_amount is decreased by the value. New value of staked_amount is returned.
+    pub fn decrease_stake_to_account(
+        stake_id: &T::StakeId,
+        destination_account_id: &T::AccountId,
+        value: BalanceOf<T>,
+    ) -> Result<BalanceOf<T>, StakeActionError<DecreasingStakeError>> {
+        let mut stake = ensure_stake_exists!(T, stake_id, StakeActionError::StakeNotFound)?;
+
+        let (deduct_from_pool, staked_amount) =
+            stake.decrease_stake(value, T::Currency::minimum_balance())?;
+
+        <Stakes<T>>::insert(stake_id, stake);
+
+        Self::transfer_funds_from_pool_into_account(&destination_account_id, deduct_from_pool);
+
+        Ok(staked_amount)
+    }
+
+    /// Initiate a new slashing of a staked stake.
+    pub fn initiate_slashing(
+        stake_id: &T::StakeId,
+        slash_amount: BalanceOf<T>,
+        slash_period: T::BlockNumber,
+    ) -> Result<T::SlashId, StakeActionError<InitiateSlashingError>> {
+        let mut stake = ensure_stake_exists!(T, stake_id, StakeActionError::StakeNotFound)?;
+
+        let slash_id = stake.initiate_slashing(
+            slash_amount,
+            slash_period,
+            <system::Module<T>>::block_number(),
+        )?;
+
+        <Stakes<T>>::insert(stake_id, stake);
+        Ok(slash_id)
+    }
+
+    /// Pause an ongoing slashing
+    pub fn pause_slashing(
+        stake_id: &T::StakeId,
+        slash_id: &T::SlashId,
+    ) -> Result<(), StakeActionError<PauseSlashingError>> {
+        let mut stake = ensure_stake_exists!(T, stake_id, StakeActionError::StakeNotFound)?;
+
+        stake.pause_slashing(slash_id)?;
+
+        <Stakes<T>>::insert(stake_id, stake);
+
+        Ok(())
+    }
+
+    /// Resume a currently paused ongoing slashing.
+    pub fn resume_slashing(
+        stake_id: &T::StakeId,
+        slash_id: &T::SlashId,
+    ) -> Result<(), StakeActionError<ResumeSlashingError>> {
+        let mut stake = ensure_stake_exists!(T, stake_id, StakeActionError::StakeNotFound)?;
+
+        stake.resume_slashing(slash_id)?;
+
+        <Stakes<T>>::insert(stake_id, stake);
+        Ok(())
+    }
+
+    /// Cancel an ongoing slashing (regardless of whether its active or paused).
+    pub fn cancel_slashing(
+        stake_id: &T::StakeId,
+        slash_id: &T::SlashId,
+    ) -> Result<(), StakeActionError<CancelSlashingError>> {
+        let mut stake = ensure_stake_exists!(T, stake_id, StakeActionError::StakeNotFound)?;
+
+        stake.cancel_slashing(slash_id)?;
+
+        <Stakes<T>>::insert(stake_id, stake);
+
+        Ok(())
+    }
+
+    /// Initiate unstaking of a Staked stake.
+    pub fn initiate_unstaking(
+        stake_id: &T::StakeId,
+        unstaking_period: Option<T::BlockNumber>,
+    ) -> Result<(), StakeActionError<InitiateUnstakingError>> {
+        let mut stake = ensure_stake_exists!(T, stake_id, StakeActionError::StakeNotFound)?;
+
+        if let Some(unstaking_period) = unstaking_period {
+            stake.initiate_unstaking(unstaking_period, <system::Module<T>>::block_number())?;
+            <Stakes<T>>::insert(stake_id, stake);
+        } else {
+            let staked_amount = stake.unstake()?;
+            <Stakes<T>>::insert(stake_id, stake);
+
+            let imbalance = Self::withdraw_funds_from_stake_pool(staked_amount);
+            let _ = T::StakingEventsHandler::unstaked(stake_id, staked_amount, imbalance);
+        }
+
+        Ok(())
+    }
+
+    /// Pause an ongoing Unstaking.
+    pub fn pause_unstaking(
+        stake_id: &T::StakeId,
+    ) -> Result<(), StakeActionError<PauseUnstakingError>> {
+        let mut stake = ensure_stake_exists!(T, stake_id, StakeActionError::StakeNotFound)?;
+
+        stake.pause_unstaking()?;
+
+        <Stakes<T>>::insert(stake_id, stake);
+
+        Ok(())
+    }
+
+    /// Resume a currently paused ongoing unstaking.
+    pub fn resume_unstaking(
+        stake_id: &T::StakeId,
+    ) -> Result<(), StakeActionError<ResumeUnstakingError>> {
+        let mut stake = ensure_stake_exists!(T, stake_id, StakeActionError::StakeNotFound)?;
+
+        stake.resume_unstaking()?;
+
+        <Stakes<T>>::insert(stake_id, stake);
+
+        Ok(())
+    }
+
+    /// Handle timers for finalizing unstaking and slashing.
+    /// Finalised unstaking results in the staked_balance in the given stake to removed from the pool, the corresponding
+    /// imbalance is provided to the unstaked() hook in the StakingEventsHandler.
+    /// Finalised slashing results in the staked_balance in the given stake being correspondingly reduced, and the imbalance
+    /// is provided to the slashed() hook in the StakingEventsHandler.
+    fn finalize_slashing_and_unstaking() {
+        for (stake_id, ref mut stake) in <Stakes<T>>::enumerate() {
+            let (updated, slashed, unstaked) =
+                stake.finalize_slashing_and_unstaking(T::Currency::minimum_balance());
+
+            // update the state before making external calls to StakingEventsHandler
+            if updated {
+                <Stakes<T>>::insert(stake_id, stake)
+            }
+
+            for (slash_id, slashed_amount, staked_amount) in slashed.into_iter() {
+                // remove the slashed amount from the pool
+                let imbalance = Self::withdraw_funds_from_stake_pool(slashed_amount);
+
+                let _ = T::StakingEventsHandler::slashed(
+                    &stake_id,
+                    &slash_id,
+                    slashed_amount,
+                    staked_amount,
+                    imbalance,
+                );
+            }
+
+            if let Some(staked_amount) = unstaked {
+                // remove the unstaked amount from the pool
+                let imbalance = Self::withdraw_funds_from_stake_pool(staked_amount);
+
+                let _ = T::StakingEventsHandler::unstaked(&stake_id, staked_amount, imbalance);
+            }
+        }
+    }
+}

+ 19 - 0
runtime-modules/stake/src/macroes.rs

@@ -0,0 +1,19 @@
+#[macro_export]
+macro_rules! ensure_map_has_mapping_with_key {
+    ($map_variable_name:ident , $runtime_trait:tt, $key:expr, $error:expr) => {{
+        if <$map_variable_name<$runtime_trait>>::exists($key) {
+            let value = <$map_variable_name<$runtime_trait>>::get($key);
+
+            Ok(value)
+        } else {
+            Err($error)
+        }
+    }};
+}
+
+#[macro_export]
+macro_rules! ensure_stake_exists {
+    ($runtime_trait:tt, $stake_id:expr, $error:expr) => {{
+        ensure_map_has_mapping_with_key!(Stakes, $runtime_trait, $stake_id, $error)
+    }};
+}

+ 115 - 0
runtime-modules/stake/src/mock.rs

@@ -0,0 +1,115 @@
+#![cfg(test)]
+
+use crate::*;
+
+use primitives::H256;
+
+use crate::{Module, Trait};
+use balances;
+use runtime_primitives::{
+    testing::Header,
+    traits::{BlakeTwo256, IdentityLookup},
+    Perbill,
+};
+use srml_support::{impl_outer_origin, parameter_types};
+
+impl_outer_origin! {
+    pub enum Origin for Test {}
+}
+
+// Workaround for https://github.com/rust-lang/rust/issues/26925 . Remove when sorted.
+#[derive(Clone, PartialEq, Eq, Debug)]
+pub struct Test;
+parameter_types! {
+    pub const BlockHashCount: u64 = 250;
+    pub const MaximumBlockWeight: u32 = 1024;
+    pub const MaximumBlockLength: u32 = 2 * 1024;
+    pub const AvailableBlockRatio: Perbill = Perbill::one();
+    pub const MinimumPeriod: u64 = 5;
+}
+
+impl system::Trait for Test {
+    type Origin = Origin;
+    type Index = u64;
+    type BlockNumber = u64;
+    type Call = ();
+    type Hash = H256;
+    type Hashing = BlakeTwo256;
+    type AccountId = u64;
+    type Lookup = IdentityLookup<Self::AccountId>;
+    type Header = Header;
+    type Event = ();
+    type BlockHashCount = BlockHashCount;
+    type MaximumBlockWeight = MaximumBlockWeight;
+    type MaximumBlockLength = MaximumBlockLength;
+    type AvailableBlockRatio = AvailableBlockRatio;
+    type Version = ();
+}
+
+parameter_types! {
+    pub const ExistentialDeposit: u32 = 500;
+    pub const TransferFee: u32 = 5;
+    pub const CreationFee: u32 = 5;
+    pub const TransactionBaseFee: u32 = 5;
+    pub const TransactionByteFee: u32 = 0;
+    pub const StakePoolId: [u8; 8] = *b"joystake";
+}
+
+impl balances::Trait for Test {
+    /// The type for recording an account's balance.
+    type Balance = u64;
+    /// What to do if an account's free balance gets zeroed.
+    type OnFreeBalanceZero = ();
+    /// What to do if a new account is created.
+    type OnNewAccount = ();
+    /// The ubiquitous event type.
+    type Event = ();
+
+    type DustRemoval = ();
+    type TransferPayment = ();
+    type ExistentialDeposit = ExistentialDeposit;
+    type TransferFee = TransferFee;
+    type CreationFee = CreationFee;
+}
+
+impl Trait for Test {
+    type Currency = Balances;
+    type StakePoolId = StakePoolId;
+    type StakingEventsHandler = ();
+    type StakeId = u64;
+    type SlashId = u64;
+}
+
+pub fn build_test_externalities() -> runtime_io::TestExternalities {
+    let t = system::GenesisConfig::default()
+        .build_storage::<Test>()
+        .unwrap();
+
+    t.into()
+}
+
+pub type System = system::Module<Test>;
+pub type Balances = balances::Module<Test>;
+pub type StakePool = Module<Test>;
+
+// Some helper methods for creating Stake states
+pub mod fixtures {
+    use super::*;
+    pub type OngoingSlashes = BTreeMap<
+        <Test as Trait>::SlashId,
+        Slash<<Test as system::Trait>::BlockNumber, BalanceOf<Test>>,
+    >;
+    // pub enum StakeInState {
+    //     NotStaked,
+    //     StakedNormal(BalanceOf<Test>, OngoingSlashes),
+    //     StakedUnstaking(BalanceOf<Test>, OngoingSlashes, <Test as system::Trait>::BlockNumber),
+    // }
+    // fn get_next_slash_id() -> SlashId {
+    // }
+    // pub fn make_stake(state: StakeInState) -> StakeId {
+    //     let id = StakePool::create_stake();
+    //     <Stakes<Test>>::mutate(id, |stake| {});
+    //     id
+    // }
+    // fn stake_in_state_to_stake(StakeInState) -> StakedState {}
+}

+ 804 - 0
runtime-modules/stake/src/tests.rs

@@ -0,0 +1,804 @@
+#![cfg(test)]
+
+use super::*;
+use crate::mock::*;
+use runtime_primitives::traits::OnFinalize;
+use srml_support::{assert_err, assert_ok};
+
+#[test]
+fn stake_pool_works() {
+    build_test_externalities().execute_with(|| {
+        // using deposit_creating
+        assert_eq!(Balances::total_issuance(), 0);
+        assert_eq!(StakePool::stake_pool_balance(), 0);
+
+        // minimum balance (existential deposit) feature applies to stake pool
+        if Balances::minimum_balance() > 0 {
+            let pos_imbalance = Balances::deposit_creating(
+                &StakePool::stake_pool_account_id(),
+                Balances::minimum_balance() - 1,
+            );
+            assert_eq!(pos_imbalance.peek(), 0);
+            assert_eq!(Balances::total_issuance(), 0);
+            assert_eq!(StakePool::stake_pool_balance(), 0);
+        }
+
+        let starting_pool_balance = Balances::minimum_balance() + 1000;
+        let _ =
+            Balances::deposit_creating(&StakePool::stake_pool_account_id(), starting_pool_balance);
+        assert_eq!(Balances::total_issuance(), starting_pool_balance);
+        assert_eq!(StakePool::stake_pool_balance(), starting_pool_balance);
+
+        let staker_starting_balance = Balances::minimum_balance() + 1000;
+        // using transfer_funds_from_account_into_pool()
+        let _ = Balances::deposit_creating(&1, staker_starting_balance);
+        assert_eq!(
+            Balances::total_issuance(),
+            starting_pool_balance + staker_starting_balance
+        );
+
+        let funds = 100;
+
+        assert_ok!(StakePool::transfer_funds_from_account_into_stake_pool(
+            &1, funds
+        ));
+
+        // total issuance unchanged after movement of funds
+        assert_eq!(
+            Balances::total_issuance(),
+            starting_pool_balance + staker_starting_balance
+        );
+
+        // funds moved into stake pool
+        assert_eq!(
+            StakePool::stake_pool_balance(),
+            starting_pool_balance + funds
+        );
+
+        // no fees were deducted
+        assert_eq!(Balances::free_balance(&1), staker_starting_balance - funds);
+
+        StakePool::transfer_funds_from_pool_into_account(&1, funds);
+
+        assert_eq!(Balances::free_balance(&1), staker_starting_balance);
+        assert_eq!(StakePool::stake_pool_balance(), starting_pool_balance);
+    });
+}
+
+#[test]
+fn create_stake() {
+    build_test_externalities().execute_with(|| {
+        let stake_id = StakePool::create_stake();
+        assert_eq!(stake_id, 0);
+        assert!(<Stakes<Test>>::exists(&stake_id));
+
+        assert_eq!(StakePool::stakes_created(), stake_id + 1);
+
+        // Should be NotStaked
+        let stake = StakePool::stakes(&stake_id);
+        assert_eq!(stake.staking_status, StakingStatus::NotStaked);
+    });
+}
+
+#[test]
+fn remove_stake_in_not_staked_state() {
+    build_test_externalities().execute_with(|| {
+        <Stakes<Test>>::insert(
+            &100,
+            Stake {
+                created: 0,
+                staking_status: StakingStatus::NotStaked,
+            },
+        );
+        assert_ok!(StakePool::remove_stake(&100));
+        assert!(!<Stakes<Test>>::exists(&100));
+
+        // when status is Staked, removing should fail
+        <Stakes<Test>>::insert(
+            &200,
+            Stake {
+                created: 0,
+                staking_status: StakingStatus::Staked(Default::default()),
+            },
+        );
+
+        assert_err!(
+            StakePool::remove_stake(&200),
+            StakeActionError::Error(StakingError::AlreadyStaked)
+        );
+        assert!(<Stakes<Test>>::exists(&200));
+    });
+}
+
+#[test]
+fn enter_staked_state() {
+    build_test_externalities().execute_with(|| {
+        <Stakes<Test>>::insert(
+            &100,
+            Stake {
+                created: 0,
+                staking_status: StakingStatus::NotStaked,
+            },
+        );
+
+        let starting_balance: u64 = Balances::minimum_balance();
+        let staker_account: u64 = 1;
+        let stake_value: u64 = Balances::minimum_balance() + 100;
+
+        let _ = Balances::deposit_creating(&staker_account, starting_balance);
+
+        // can't stake zero
+        assert_err!(
+            StakePool::stake_from_account(&100, &staker_account, 0),
+            StakeActionError::Error(StakingFromAccountError::StakingError(
+                StakingError::CannotStakeZero
+            ))
+        );
+
+        // must stake at least the minimum balance
+        if Balances::minimum_balance() > 0 {
+            assert_err!(
+                StakePool::stake_from_account(
+                    &100,
+                    &staker_account,
+                    Balances::minimum_balance() - 1
+                ),
+                StakeActionError::Error(StakingFromAccountError::StakingError(
+                    StakingError::CannotStakeLessThanMinimumBalance
+                ))
+            );
+        }
+
+        // cannot stake with insufficient funds
+        assert_err!(
+            StakePool::stake_from_account(&100, &staker_account, stake_value),
+            StakeActionError::Error(StakingFromAccountError::InsufficientBalanceInSourceAccount)
+        );
+
+        // deposit exact amount to stake
+        let _ = Balances::deposit_creating(&staker_account, stake_value);
+
+        assert_ok!(StakePool::stake_from_account(
+            &100,
+            &staker_account,
+            stake_value
+        ));
+
+        assert_eq!(Balances::free_balance(&staker_account), starting_balance);
+
+        assert_eq!(StakePool::stake_pool_balance(), stake_value);
+    });
+}
+
+#[test]
+fn increasing_stake() {
+    build_test_externalities().execute_with(|| {
+        let starting_pool_stake = Balances::minimum_balance() + 5000;
+        let _ =
+            Balances::deposit_creating(&StakePool::stake_pool_account_id(), starting_pool_stake);
+
+        let starting_stake = Balances::minimum_balance() + 100;
+        <Stakes<Test>>::insert(
+            &100,
+            Stake {
+                created: 0,
+                staking_status: StakingStatus::Staked(StakedState {
+                    staked_amount: starting_stake,
+                    ongoing_slashes: BTreeMap::new(),
+                    next_slash_id: 0,
+                    staked_status: StakedStatus::Normal,
+                }),
+            },
+        );
+
+        let additional_stake: u64 = 500;
+        let starting_balance: u64 = Balances::minimum_balance() + additional_stake;
+        let staker_account: u64 = 1;
+
+        let _ = Balances::deposit_creating(&staker_account, starting_balance);
+
+        assert_err!(
+            StakePool::increase_stake_from_account(&100, &staker_account, 0),
+            StakeActionError::Error(IncreasingStakeFromAccountError::IncreasingStakeError(
+                IncreasingStakeError::CannotChangeStakeByZero
+            ))
+        );
+
+        let total_staked =
+            StakePool::increase_stake_from_account(&100, &staker_account, additional_stake)
+                .ok()
+                .unwrap();
+        assert_eq!(total_staked, starting_stake + additional_stake);
+
+        assert_eq!(
+            Balances::free_balance(&staker_account),
+            starting_balance - additional_stake
+        );
+
+        assert_eq!(
+            StakePool::stake_pool_balance(),
+            starting_pool_stake + additional_stake
+        );
+
+        assert_eq!(
+            StakePool::stakes(&100),
+            Stake {
+                created: 0,
+                staking_status: StakingStatus::Staked(StakedState {
+                    staked_amount: starting_stake + additional_stake,
+                    ongoing_slashes: BTreeMap::new(),
+                    next_slash_id: 0,
+                    staked_status: StakedStatus::Normal,
+                })
+            }
+        );
+
+        // cannot increase stake if insufficent balance
+        assert!(StakePool::increase_stake_from_account(
+            &100,
+            &staker_account,
+            Balances::free_balance(&staker_account) + 1
+        )
+        .is_err());
+    });
+}
+
+#[test]
+fn decreasing_stake() {
+    build_test_externalities().execute_with(|| {
+        let starting_pool_stake = 5000;
+        let _ =
+            Balances::deposit_creating(&StakePool::stake_pool_account_id(), starting_pool_stake);
+
+        let starting_stake = Balances::minimum_balance() + 2000;
+        <Stakes<Test>>::insert(
+            &100,
+            Stake {
+                created: 0,
+                staking_status: StakingStatus::Staked(StakedState {
+                    staked_amount: starting_stake,
+                    ongoing_slashes: BTreeMap::new(),
+                    next_slash_id: 0,
+                    staked_status: StakedStatus::Normal,
+                }),
+            },
+        );
+
+        let starting_balance: u64 = Balances::minimum_balance();
+        let staker_account: u64 = 1;
+        let decrease_stake_by: u64 = 200;
+
+        let _ = Balances::deposit_creating(&staker_account, starting_balance);
+
+        assert_err!(
+            StakePool::decrease_stake_to_account(&100, &staker_account, 0),
+            StakeActionError::Error(DecreasingStakeError::CannotChangeStakeByZero)
+        );
+
+        let total_staked =
+            StakePool::decrease_stake_to_account(&100, &staker_account, decrease_stake_by)
+                .ok()
+                .unwrap();
+        assert_eq!(total_staked, starting_stake - decrease_stake_by);
+
+        assert_eq!(
+            Balances::free_balance(&staker_account),
+            starting_balance + decrease_stake_by
+        );
+
+        assert_eq!(
+            StakePool::stake_pool_balance(),
+            starting_pool_stake - decrease_stake_by
+        );
+
+        assert_eq!(
+            StakePool::stakes(&100),
+            Stake {
+                created: 0,
+                staking_status: StakingStatus::Staked(StakedState {
+                    staked_amount: starting_stake - decrease_stake_by,
+                    ongoing_slashes: BTreeMap::new(),
+                    next_slash_id: 0,
+                    staked_status: StakedStatus::Normal,
+                })
+            }
+        );
+
+        // cannot unstake more than total at stake
+        assert_err!(
+            StakePool::decrease_stake_to_account(&100, &staker_account, total_staked + 1),
+            StakeActionError::Error(DecreasingStakeError::InsufficientStake)
+        );
+
+        // decreasing stake to value less than minimum_balance should reduce entire stake
+        if Balances::minimum_balance() > 0 {
+            let over_minimum = 50;
+            let staked_amount = Balances::minimum_balance() + over_minimum;
+
+            let _ = Balances::deposit_creating(&StakePool::stake_pool_account_id(), staked_amount);
+            <Stakes<Test>>::insert(
+                &200,
+                Stake {
+                    created: 0,
+                    staking_status: StakingStatus::Staked(StakedState {
+                        staked_amount: staked_amount,
+                        ongoing_slashes: BTreeMap::new(),
+                        next_slash_id: 0,
+                        staked_status: StakedStatus::Normal,
+                    }),
+                },
+            );
+
+            assert_eq!(Balances::free_balance(&2), 0);
+            let starting_pool_balance = StakePool::stake_pool_balance();
+            let remaining_stake = StakePool::decrease_stake_to_account(&200, &2, over_minimum + 1)
+                .ok()
+                .unwrap();
+            assert_eq!(remaining_stake, 0);
+            assert_eq!(Balances::free_balance(&2), staked_amount);
+            assert_eq!(
+                StakePool::stake_pool_balance(),
+                starting_pool_balance - staked_amount
+            );
+        }
+    });
+}
+
+#[test]
+fn initiating_pausing_resuming_cancelling_slashes() {
+    build_test_externalities().execute_with(|| {
+        let staked_amount = Balances::minimum_balance() + 10000;
+        let _ = Balances::deposit_creating(&StakePool::stake_pool_account_id(), staked_amount);
+
+        assert_err!(
+            StakePool::initiate_slashing(&100, 5000, 0),
+            StakeActionError::StakeNotFound
+        );
+
+        let stake_id = StakePool::create_stake();
+        <Stakes<Test>>::insert(
+            &stake_id,
+            Stake {
+                created: System::block_number(),
+                staking_status: StakingStatus::NotStaked,
+            },
+        );
+
+        assert_err!(
+            StakePool::initiate_slashing(&stake_id, 5000, 0),
+            StakeActionError::Error(InitiateSlashingError::SlashPeriodShouldBeGreaterThanZero)
+        );
+
+        assert_err!(
+            StakePool::initiate_slashing(&stake_id, 5000, 1),
+            StakeActionError::Error(InitiateSlashingError::NotStaked)
+        );
+
+        <Stakes<Test>>::insert(
+            &stake_id,
+            Stake {
+                created: System::block_number(),
+                staking_status: StakingStatus::Staked(StakedState {
+                    staked_amount: staked_amount,
+                    ongoing_slashes: BTreeMap::new(),
+                    next_slash_id: 0,
+                    staked_status: StakedStatus::Unstaking(UnstakingState {
+                        started_at_block: 0,
+                        blocks_remaining_in_active_period_for_unstaking: 100,
+                        is_active: true,
+                    }),
+                }),
+            },
+        );
+
+        // assert_err!(StakePool::initiate_slashing(&stake_id, 0, 0), StakingError::ZeroSlashing);
+
+        let mut slash_id = 0;
+        assert!(StakePool::initiate_slashing(&stake_id, 5000, 10).is_ok());
+
+        let mut expected_ongoing_slashes: fixtures::OngoingSlashes = BTreeMap::new();
+
+        expected_ongoing_slashes.insert(
+            slash_id,
+            Slash {
+                started_at_block: System::block_number(),
+                is_active: true,
+                blocks_remaining_in_active_period_for_slashing: 10,
+                slash_amount: 5000,
+            },
+        );
+
+        assert_eq!(
+            StakePool::stakes(&stake_id),
+            Stake {
+                created: System::block_number(),
+                staking_status: StakingStatus::Staked(StakedState {
+                    staked_amount: staked_amount,
+                    ongoing_slashes: expected_ongoing_slashes.clone(),
+                    next_slash_id: slash_id + 1,
+                    staked_status: StakedStatus::Unstaking(UnstakingState {
+                        started_at_block: 0,
+                        blocks_remaining_in_active_period_for_unstaking: 100,
+                        is_active: false,
+                    }),
+                })
+            }
+        );
+
+        assert_err!(
+            StakePool::pause_slashing(&stake_id, &999),
+            StakeActionError::Error(PauseSlashingError::SlashNotFound)
+        );
+        assert_err!(
+            StakePool::pause_slashing(&999, &slash_id),
+            StakeActionError::StakeNotFound
+        );
+
+        assert_ok!(StakePool::pause_slashing(&stake_id, &slash_id));
+        expected_ongoing_slashes.insert(
+            slash_id,
+            Slash {
+                started_at_block: System::block_number(),
+                is_active: false,
+                blocks_remaining_in_active_period_for_slashing: 10,
+                slash_amount: 5000,
+            },
+        );
+        assert_eq!(
+            StakePool::stakes(&stake_id),
+            Stake {
+                created: System::block_number(),
+                staking_status: StakingStatus::Staked(StakedState {
+                    staked_amount: staked_amount,
+                    ongoing_slashes: expected_ongoing_slashes.clone(),
+                    next_slash_id: slash_id + 1,
+                    staked_status: StakedStatus::Unstaking(UnstakingState {
+                        started_at_block: 0,
+                        blocks_remaining_in_active_period_for_unstaking: 100,
+                        is_active: false,
+                    }),
+                })
+            }
+        );
+
+        assert_err!(
+            StakePool::resume_slashing(&stake_id, &999),
+            StakeActionError::Error(ResumeSlashingError::SlashNotFound)
+        );
+        assert_err!(
+            StakePool::resume_slashing(&999, &slash_id),
+            StakeActionError::StakeNotFound
+        );
+
+        assert_ok!(StakePool::resume_slashing(&stake_id, &slash_id));
+        expected_ongoing_slashes.insert(
+            slash_id,
+            Slash {
+                started_at_block: System::block_number(),
+                is_active: true,
+                blocks_remaining_in_active_period_for_slashing: 10,
+                slash_amount: 5000,
+            },
+        );
+        assert_eq!(
+            StakePool::stakes(&stake_id),
+            Stake {
+                created: System::block_number(),
+                staking_status: StakingStatus::Staked(StakedState {
+                    staked_amount: staked_amount,
+                    ongoing_slashes: expected_ongoing_slashes.clone(),
+                    next_slash_id: slash_id + 1,
+                    staked_status: StakedStatus::Unstaking(UnstakingState {
+                        started_at_block: 0,
+                        blocks_remaining_in_active_period_for_unstaking: 100,
+                        is_active: false,
+                    }),
+                })
+            }
+        );
+
+        assert_err!(
+            StakePool::cancel_slashing(&stake_id, &999),
+            StakeActionError::Error(CancelSlashingError::SlashNotFound)
+        );
+        assert_err!(
+            StakePool::cancel_slashing(&999, &slash_id),
+            StakeActionError::StakeNotFound
+        );
+
+        assert_ok!(StakePool::cancel_slashing(&stake_id, &slash_id));
+        assert_eq!(
+            StakePool::stakes(&stake_id),
+            Stake {
+                created: System::block_number(),
+                staking_status: StakingStatus::Staked(StakedState {
+                    staked_amount: staked_amount,
+                    ongoing_slashes: BTreeMap::new(),
+                    next_slash_id: slash_id + 1,
+                    staked_status: StakedStatus::Unstaking(UnstakingState {
+                        started_at_block: 0,
+                        blocks_remaining_in_active_period_for_unstaking: 100,
+                        is_active: true,
+                    }),
+                })
+            }
+        );
+
+        expected_ongoing_slashes = BTreeMap::new();
+        let slashing_amount = 5000;
+        slash_id += 1;
+        assert!(StakePool::initiate_slashing(&stake_id, slashing_amount, 2).is_ok());
+
+        StakePool::on_finalize(System::block_number());
+
+        expected_ongoing_slashes.insert(
+            slash_id,
+            Slash {
+                started_at_block: System::block_number(),
+                is_active: true,
+                blocks_remaining_in_active_period_for_slashing: 1,
+                slash_amount: slashing_amount,
+            },
+        );
+        assert_eq!(
+            StakePool::stakes(&stake_id),
+            Stake {
+                created: System::block_number(),
+                staking_status: StakingStatus::Staked(StakedState {
+                    staked_amount: staked_amount,
+                    ongoing_slashes: expected_ongoing_slashes.clone(),
+                    next_slash_id: slash_id + 1,
+                    staked_status: StakedStatus::Unstaking(UnstakingState {
+                        started_at_block: 0,
+                        blocks_remaining_in_active_period_for_unstaking: 100,
+                        is_active: false,
+                    }),
+                })
+            }
+        );
+
+        StakePool::on_finalize(System::block_number());
+        assert_eq!(
+            StakePool::stakes(&stake_id),
+            Stake {
+                created: System::block_number(),
+                staking_status: StakingStatus::Staked(StakedState {
+                    staked_amount: staked_amount - slashing_amount,
+                    ongoing_slashes: BTreeMap::new(),
+                    next_slash_id: slash_id + 1,
+                    staked_status: StakedStatus::Unstaking(UnstakingState {
+                        started_at_block: 0,
+                        blocks_remaining_in_active_period_for_unstaking: 99,
+                        is_active: true
+                    })
+                })
+            }
+        );
+
+        assert_eq!(
+            StakePool::stake_pool_balance(),
+            staked_amount - slashing_amount
+        );
+    });
+}
+
+#[test]
+fn initiating_pausing_resuming_unstaking() {
+    build_test_externalities().execute_with(|| {
+        let staked_amount = Balances::minimum_balance() + 10000;
+        let starting_stake_fund_balance = Balances::minimum_balance() + 3333;
+
+        let _ = Balances::deposit_creating(
+            &StakePool::stake_pool_account_id(),
+            starting_stake_fund_balance + staked_amount,
+        );
+
+        assert_err!(
+            StakePool::initiate_unstaking(&100, Some(1)),
+            StakeActionError::StakeNotFound
+        );
+
+        let stake_id = StakePool::create_stake();
+        <Stakes<Test>>::insert(
+            &stake_id,
+            Stake {
+                created: System::block_number(),
+                staking_status: StakingStatus::NotStaked,
+            },
+        );
+
+        assert_err!(
+            StakePool::initiate_unstaking(&stake_id, Some(0)),
+            StakeActionError::Error(InitiateUnstakingError::UnstakingPeriodShouldBeGreaterThanZero)
+        );
+
+        assert_err!(
+            StakePool::initiate_unstaking(&stake_id, Some(1)),
+            StakeActionError::Error(InitiateUnstakingError::UnstakingError(
+                UnstakingError::NotStaked
+            ))
+        );
+
+        let mut ongoing_slashes = BTreeMap::new();
+        ongoing_slashes.insert(
+            1,
+            Slash {
+                started_at_block: System::block_number(),
+                is_active: true,
+                blocks_remaining_in_active_period_for_slashing: 100,
+                slash_amount: 100,
+            },
+        );
+
+        <Stakes<Test>>::insert(
+            &stake_id,
+            Stake {
+                created: System::block_number(),
+                staking_status: StakingStatus::Staked(StakedState {
+                    staked_amount,
+                    ongoing_slashes,
+                    next_slash_id: 2,
+                    staked_status: StakedStatus::Normal,
+                }),
+            },
+        );
+
+        assert_err!(
+            StakePool::initiate_unstaking(&stake_id, Some(1)),
+            StakeActionError::Error(InitiateUnstakingError::UnstakingError(
+                UnstakingError::CannotUnstakeWhileSlashesOngoing
+            ))
+        );
+
+        assert_ok!(StakePool::cancel_slashing(&stake_id, &1));
+
+        assert_ok!(StakePool::initiate_unstaking(&stake_id, Some(2)));
+
+        assert_eq!(
+            StakePool::stakes(&stake_id),
+            Stake {
+                created: System::block_number(),
+                staking_status: StakingStatus::Staked(StakedState {
+                    staked_amount,
+                    ongoing_slashes: BTreeMap::new(),
+                    next_slash_id: 2,
+                    staked_status: StakedStatus::Unstaking(UnstakingState {
+                        started_at_block: System::block_number(),
+                        blocks_remaining_in_active_period_for_unstaking: 2,
+                        is_active: true
+                    })
+                })
+            }
+        );
+
+        StakePool::on_finalize(System::block_number());
+        assert_eq!(
+            StakePool::stakes(&stake_id),
+            Stake {
+                created: System::block_number(),
+                staking_status: StakingStatus::Staked(StakedState {
+                    staked_amount,
+                    ongoing_slashes: BTreeMap::new(),
+                    next_slash_id: 2,
+                    staked_status: StakedStatus::Unstaking(UnstakingState {
+                        started_at_block: System::block_number(),
+                        blocks_remaining_in_active_period_for_unstaking: 1,
+                        is_active: true
+                    })
+                })
+            }
+        );
+
+        StakePool::finalize_slashing_and_unstaking();
+        assert_eq!(
+            StakePool::stakes(&stake_id),
+            Stake {
+                created: System::block_number(),
+                staking_status: StakingStatus::NotStaked
+            }
+        );
+
+        assert_eq!(StakePool::stake_pool_balance(), starting_stake_fund_balance);
+
+        // unstaked amount is destroyed by StakingEventsHandler
+        assert_eq!(Balances::total_issuance(), starting_stake_fund_balance);
+    });
+}
+
+#[test]
+fn unstake() {
+    build_test_externalities().execute_with(|| {
+        assert_err!(
+            StakePool::initiate_unstaking(&0, None),
+            StakeActionError::StakeNotFound
+        );
+
+        let staked_amount = Balances::minimum_balance() + 10000;
+        let starting_stake_fund_balance = Balances::minimum_balance() + 3333;
+
+        let _ = Balances::deposit_creating(
+            &StakePool::stake_pool_account_id(),
+            starting_stake_fund_balance + staked_amount,
+        );
+
+        let stake_id = StakePool::create_stake();
+
+        assert_err!(
+            StakePool::initiate_unstaking(&stake_id, None),
+            StakeActionError::Error(InitiateUnstakingError::UnstakingError(
+                UnstakingError::NotStaked
+            ))
+        );
+
+        let mut ongoing_slashes = BTreeMap::new();
+        ongoing_slashes.insert(
+            1,
+            Slash {
+                started_at_block: System::block_number(),
+                is_active: true,
+                blocks_remaining_in_active_period_for_slashing: 100,
+                slash_amount: 100,
+            },
+        );
+
+        <Stakes<Test>>::insert(
+            &stake_id,
+            Stake {
+                created: System::block_number(),
+                staking_status: StakingStatus::Staked(StakedState {
+                    staked_amount,
+                    ongoing_slashes,
+                    next_slash_id: 0,
+                    staked_status: StakedStatus::Normal,
+                }),
+            },
+        );
+
+        assert_err!(
+            StakePool::initiate_unstaking(&stake_id, None),
+            StakeActionError::Error(InitiateUnstakingError::UnstakingError(
+                UnstakingError::CannotUnstakeWhileSlashesOngoing
+            ))
+        );
+
+        <Stakes<Test>>::insert(
+            &stake_id,
+            Stake {
+                created: System::block_number(),
+                staking_status: StakingStatus::Staked(StakedState {
+                    staked_amount,
+                    ongoing_slashes: BTreeMap::new(),
+                    next_slash_id: 0,
+                    staked_status: StakedStatus::Unstaking(UnstakingState {
+                        started_at_block: 0,
+                        blocks_remaining_in_active_period_for_unstaking: 100,
+                        is_active: true,
+                    }),
+                }),
+            },
+        );
+
+        assert_err!(
+            StakePool::initiate_unstaking(&stake_id, None),
+            StakeActionError::Error(InitiateUnstakingError::UnstakingError(
+                UnstakingError::AlreadyUnstaking
+            ))
+        );
+
+        <Stakes<Test>>::insert(
+            &stake_id,
+            Stake {
+                created: System::block_number(),
+                staking_status: StakingStatus::Staked(StakedState {
+                    staked_amount,
+                    ongoing_slashes: BTreeMap::new(),
+                    next_slash_id: 0,
+                    staked_status: StakedStatus::Normal,
+                }),
+            },
+        );
+
+        assert_ok!(StakePool::initiate_unstaking(&stake_id, None));
+        assert_eq!(StakePool::stake_pool_balance(), starting_stake_fund_balance);
+    });
+}

+ 50 - 0
runtime-modules/token-minting/Cargo.toml

@@ -0,0 +1,50 @@
+[package]
+name = 'substrate-token-mint-module'
+version = '1.0.1'
+authors = ['Joystream contributors']
+edition = '2018'
+
+[dependencies]
+hex-literal = '0.1.0'
+serde = { version = '1.0.101', optional = true}
+serde_derive = { version = '1.0.101', optional = true }
+rstd = { package = 'sr-std', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+runtime-primitives = { package = 'sr-primitives', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+srml-support = { package = 'srml-support', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+srml-support-procedural = { package = 'srml-support-procedural', git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+system = { package = 'srml-system', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+balances = { package = 'srml-balances', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+codec = { package = 'parity-scale-codec', version = '1.0.0', default-features = false, features = ['derive'] }
+# https://users.rust-lang.org/t/failure-derive-compilation-error/39062
+quote = '<=1.0.2'
+
+[dependencies.timestamp]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'srml-timestamp'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dependencies.runtime-io]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'sr-io'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dev-dependencies]
+runtime-io = { package = 'sr-io', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+primitives = { package = 'substrate-primitives', git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+
+[features]
+default = ['std']
+std = [
+	'serde',
+	'serde_derive',
+	'codec/std',
+	'rstd/std',
+	'runtime-io/std',
+	'runtime-primitives/std',
+	'srml-support/std',
+	'system/std',
+  	'balances/std',
+	'timestamp/std',
+]

+ 260 - 0
runtime-modules/token-minting/src/lib.rs

@@ -0,0 +1,260 @@
+// Ensure we're `no_std` when compiling for Wasm.
+#![cfg_attr(not(feature = "std"), no_std)]
+
+#[cfg(feature = "std")]
+use rstd::prelude::*;
+
+use codec::{Codec, Decode, Encode};
+use runtime_primitives::traits::{MaybeSerialize, Member, One, SimpleArithmetic, Zero};
+use srml_support::traits::Currency;
+use srml_support::{decl_module, decl_storage, ensure, Parameter};
+
+mod mint;
+mod mock;
+mod tests;
+
+pub use mint::*;
+
+use system;
+
+pub trait Trait: system::Trait {
+    /// The currency to mint.
+    type Currency: Currency<Self::AccountId>;
+
+    /// The type used as a mint identifier.
+    type MintId: Parameter
+        + Member
+        + SimpleArithmetic
+        + Codec
+        + Default
+        + Copy
+        + MaybeSerialize
+        + PartialEq;
+}
+
+pub type BalanceOf<T> =
+    <<T as Trait>::Currency as Currency<<T as system::Trait>::AccountId>>::Balance;
+
+#[derive(PartialEq, Eq, Debug)]
+pub enum GeneralError {
+    MintNotFound,
+    NextAdjustmentInPast,
+}
+
+/// Errors that can arise from attempt to mint and transfer tokens from a mint to
+/// an account.
+#[derive(PartialEq, Eq, Debug)]
+pub enum TransferError {
+    MintNotFound,
+    NotEnoughCapacity,
+}
+
+/// Errors that can arise from attempt to transfer capacity between mints.
+#[derive(PartialEq, Eq, Debug)]
+pub enum CapacityTransferError {
+    SourceMintNotFound,
+    DestinationMintNotFound,
+    NotEnoughCapacity,
+}
+
+impl From<MintingError> for CapacityTransferError {
+    fn from(err: MintingError) -> CapacityTransferError {
+        match err {
+            MintingError::NotEnoughCapacity => CapacityTransferError::NotEnoughCapacity,
+        }
+    }
+}
+
+impl From<MintingError> for TransferError {
+    fn from(err: MintingError) -> TransferError {
+        match err {
+            MintingError::NotEnoughCapacity => TransferError::NotEnoughCapacity,
+        }
+    }
+}
+
+#[derive(Encode, Decode, Copy, Clone, Debug, Eq, PartialEq)]
+pub enum Adjustment<Balance: Zero, BlockNumber> {
+    // First adjustment will be after AdjustOnInterval.block_interval
+    Interval(AdjustOnInterval<Balance, BlockNumber>),
+    // First Adjustment will be at absolute blocknumber
+    IntervalAfterFirstAdjustmentAbsolute(AdjustOnInterval<Balance, BlockNumber>, BlockNumber),
+    // First Adjustment will be after a specified number of blocks
+    IntervalAfterFirstAdjustmentRelative(AdjustOnInterval<Balance, BlockNumber>, BlockNumber),
+}
+
+decl_storage! {
+    trait Store for Module<T: Trait> as TokenMint {
+        /// Mints
+        pub Mints get(mints) : linked_map T::MintId => Mint<BalanceOf<T>, T::BlockNumber>;
+
+        /// The number of mints created.
+        pub MintsCreated get(mints_created): T::MintId;
+    }
+}
+
+decl_module! {
+    pub struct Module<T: Trait> for enum Call where origin: T::Origin {
+        fn on_finalize(now: T::BlockNumber) {
+            Self::update_mints(now);
+        }
+    }
+}
+
+impl<T: Trait> Module<T> {
+    fn update_mints(now: T::BlockNumber) {
+        // Are we reading value from storage twice?
+        for (mint_id, ref mut mint) in <Mints<T>>::enumerate() {
+            if mint.maybe_do_capacity_adjustment(now) {
+                <Mints<T>>::insert(&mint_id, mint);
+            }
+        }
+    }
+
+    /// Adds a new mint with given settings to mints, and returns new MintId.
+    pub fn add_mint(
+        initial_capacity: BalanceOf<T>,
+        adjustment: Option<Adjustment<BalanceOf<T>, T::BlockNumber>>,
+    ) -> Result<T::MintId, GeneralError> {
+        let now = <system::Module<T>>::block_number();
+
+        // Ensure the next adjustment if set, is in the future
+        if let Some(adjustment) = adjustment {
+            match adjustment {
+                Adjustment::IntervalAfterFirstAdjustmentAbsolute(_, first_adjustment_in) => {
+                    ensure!(
+                        first_adjustment_in > now,
+                        GeneralError::NextAdjustmentInPast
+                    );
+                }
+                _ => (),
+            }
+        }
+
+        // Determine next adjutment
+        let next_adjustment = adjustment.map(|adjustment| match adjustment {
+            Adjustment::Interval(adjust_on_interval) => NextAdjustment {
+                at_block: now + adjust_on_interval.block_interval,
+                adjustment: adjust_on_interval,
+            },
+            Adjustment::IntervalAfterFirstAdjustmentAbsolute(
+                adjust_on_interval,
+                first_adjustment_at,
+            ) => NextAdjustment {
+                adjustment: adjust_on_interval,
+                at_block: first_adjustment_at,
+            },
+            Adjustment::IntervalAfterFirstAdjustmentRelative(
+                adjust_on_interval,
+                first_adjustment_after,
+            ) => NextAdjustment {
+                adjustment: adjust_on_interval,
+                at_block: now + first_adjustment_after,
+            },
+        });
+
+        // get next mint_id and increment total number of mints created
+        let mint_id = Self::mints_created();
+        <MintsCreated<T>>::put(mint_id + One::one());
+
+        <Mints<T>>::insert(mint_id, Mint::new(initial_capacity, next_adjustment, now));
+
+        Ok(mint_id)
+    }
+
+    /// Removes a mint. Passing a non existent mint has no side effects.
+    pub fn remove_mint(mint_id: T::MintId) {
+        <Mints<T>>::remove(&mint_id);
+    }
+
+    /// Tries to transfer exact requested amount from mint to a recipient account id.
+    /// Returns error if amount exceeds mint capacity or the specified mint doesn't exist.
+    /// Transfering amount of zero has no side effects. Return nothing on success.
+    pub fn transfer_tokens(
+        mint_id: T::MintId,
+        requested_amount: BalanceOf<T>,
+        recipient: &T::AccountId,
+    ) -> Result<(), TransferError> {
+        if requested_amount == Zero::zero() {
+            return Ok(());
+        }
+
+        ensure!(<Mints<T>>::exists(&mint_id), TransferError::MintNotFound);
+
+        let mut mint = Self::mints(&mint_id);
+
+        // Try minting
+        mint.mint_tokens(requested_amount)?;
+
+        <Mints<T>>::insert(&mint_id, mint);
+
+        // Deposit into recipient account
+        T::Currency::deposit_creating(recipient, requested_amount);
+
+        Ok(())
+    }
+
+    /// Provided mint exists, sets its capacity to specied value, return error otherwise.
+    pub fn set_mint_capacity(
+        mint_id: T::MintId,
+        capacity: BalanceOf<T>,
+    ) -> Result<(), GeneralError> {
+        ensure!(<Mints<T>>::exists(&mint_id), GeneralError::MintNotFound);
+
+        <Mints<T>>::mutate(&mint_id, |mint| {
+            mint.set_capacity(capacity);
+        });
+
+        Ok(())
+    }
+
+    /// Provided source and destination mints exist, will attempt to transfer capacity from the source mint
+    /// to the destination mint. Will return errors on non-existence of
+    /// mints or capacity_to_transfer exceeds the source mint's capacity.
+    pub fn transfer_capacity(
+        source: T::MintId,
+        destination: T::MintId,
+        capacity_to_transfer: BalanceOf<T>,
+    ) -> Result<(), CapacityTransferError> {
+        ensure!(
+            <Mints<T>>::exists(&source),
+            CapacityTransferError::SourceMintNotFound
+        );
+        ensure!(
+            <Mints<T>>::exists(&destination),
+            CapacityTransferError::DestinationMintNotFound
+        );
+
+        <Mints<T>>::mutate(&source, |source_mint| {
+            <Mints<T>>::mutate(&destination, |destination_mint| {
+                source_mint.transfer_capacity_to(destination_mint, capacity_to_transfer)
+            })
+        })?;
+
+        Ok(())
+    }
+
+    /// Returns a mint's capacity if it exists, error otherwise.
+    pub fn get_mint_capacity(mint_id: T::MintId) -> Result<BalanceOf<T>, GeneralError> {
+        ensure!(<Mints<T>>::exists(&mint_id), GeneralError::MintNotFound);
+        let mint = Self::mints(&mint_id);
+
+        Ok(mint.capacity())
+    }
+
+    /// Returns a mint's adjustment policy if it exists, error otherwise.
+    pub fn get_mint_next_adjustment(
+        mint_id: T::MintId,
+    ) -> Result<Option<NextAdjustment<BalanceOf<T>, T::BlockNumber>>, GeneralError> {
+        ensure!(<Mints<T>>::exists(&mint_id), GeneralError::MintNotFound);
+
+        let mint = Self::mints(&mint_id);
+
+        Ok(mint.next_adjustment())
+    }
+
+    /// Returns true if a mint exists.
+    pub fn mint_exists(mint_id: T::MintId) -> bool {
+        <Mints<T>>::exists(&mint_id)
+    }
+}

+ 153 - 0
runtime-modules/token-minting/src/mint.rs

@@ -0,0 +1,153 @@
+use codec::{Decode, Encode};
+use runtime_primitives::traits::{SimpleArithmetic, Zero};
+use srml_support::ensure;
+
+#[derive(Encode, Decode, Copy, Clone, Debug, Eq, PartialEq)]
+pub enum AdjustCapacityBy<Balance: Zero> {
+    /// Set capacity of mint to specific value
+    Setting(Balance),
+    /// Add to the capacity of the mint
+    Adding(Balance),
+    /// Reduce capacity of the mint
+    Reducing(Balance),
+}
+
+#[derive(Encode, Decode, Copy, Clone, Debug, Eq, PartialEq)]
+pub struct AdjustOnInterval<Balance: Zero, BlockNumber> {
+    pub block_interval: BlockNumber,
+    pub adjustment_type: AdjustCapacityBy<Balance>,
+}
+
+#[derive(Encode, Decode, Copy, Clone, Debug, Eq, PartialEq)]
+pub struct NextAdjustment<Balance: Zero, BlockNumber> {
+    pub adjustment: AdjustOnInterval<Balance, BlockNumber>,
+    pub at_block: BlockNumber,
+}
+
+#[derive(Encode, Decode, Default, Copy, Clone)]
+// Note we don't use TokenMint<T: Trait> it breaks the Default derivation macro with error T doesn't impl Default
+// Which requires manually implementing Default trait.
+// We want Default trait on TokenMint so we can use it as value in StorageMap without needing to wrap it in an Option
+pub struct Mint<Balance, BlockNumber>
+where
+    Balance: Copy + SimpleArithmetic + Zero,
+    BlockNumber: Copy + SimpleArithmetic,
+{
+    capacity: Balance,
+
+    // Whether there is an upcoming block where an adjustment to the mint will be made
+    // When this is not set, the mint is effectively paused.
+    next_adjustment: Option<NextAdjustment<Balance, BlockNumber>>,
+
+    created_at: BlockNumber,
+
+    total_minted: Balance,
+}
+
+#[derive(PartialEq, Eq, Debug)]
+pub enum MintingError {
+    NotEnoughCapacity,
+}
+
+impl<Balance, BlockNumber> Mint<Balance, BlockNumber>
+where
+    Balance: Copy + SimpleArithmetic + Zero,
+    BlockNumber: Copy + SimpleArithmetic,
+{
+    pub fn new(
+        initial_capacity: Balance,
+        next_adjustment: Option<NextAdjustment<Balance, BlockNumber>>,
+        now: BlockNumber,
+    ) -> Self {
+        Mint {
+            capacity: initial_capacity,
+            created_at: now,
+            total_minted: Zero::zero(),
+            next_adjustment: next_adjustment,
+        }
+    }
+
+    pub fn mint_tokens(&mut self, requested_amount: Balance) -> Result<(), MintingError> {
+        ensure!(
+            self.capacity >= requested_amount,
+            MintingError::NotEnoughCapacity
+        );
+        self.capacity -= requested_amount;
+        self.total_minted += requested_amount;
+        Ok(())
+    }
+
+    pub fn set_capacity(&mut self, new_capacity: Balance) {
+        self.capacity = new_capacity;
+    }
+
+    pub fn capacity(&self) -> Balance {
+        self.capacity
+    }
+
+    pub fn can_mint(&self, amount: Balance) -> bool {
+        self.capacity >= amount
+    }
+
+    pub fn created_at(&self) -> BlockNumber {
+        self.created_at
+    }
+
+    pub fn total_minted(&self) -> Balance {
+        self.total_minted
+    }
+
+    pub fn transfer_capacity_to(
+        &mut self,
+        destination: &mut Self,
+        capacity_to_transfer: Balance,
+    ) -> Result<(), MintingError> {
+        ensure!(
+            self.capacity >= capacity_to_transfer,
+            MintingError::NotEnoughCapacity
+        );
+        self.capacity -= capacity_to_transfer;
+        destination.capacity += capacity_to_transfer;
+        Ok(())
+    }
+
+    pub fn next_adjustment(&self) -> Option<NextAdjustment<Balance, BlockNumber>> {
+        self.next_adjustment
+    }
+
+    pub fn maybe_do_capacity_adjustment(&mut self, now: BlockNumber) -> bool {
+        self.next_adjustment.map_or(false, |next_adjustment| {
+            if now != next_adjustment.at_block {
+                false
+            } else {
+                // update mint capacity
+                self.capacity = Self::adjusted_capacity(
+                    self.capacity,
+                    next_adjustment.adjustment.adjustment_type,
+                );
+
+                // set next adjustment
+                self.next_adjustment = Some(NextAdjustment {
+                    adjustment: next_adjustment.adjustment,
+                    at_block: now + next_adjustment.adjustment.block_interval,
+                });
+
+                true
+            }
+        })
+    }
+
+    fn adjusted_capacity(capacity: Balance, adjustment_type: AdjustCapacityBy<Balance>) -> Balance {
+        match adjustment_type {
+            AdjustCapacityBy::Adding(amount) => capacity + amount,
+            AdjustCapacityBy::Setting(amount) => amount,
+            AdjustCapacityBy::Reducing(amount) => {
+                if amount > capacity {
+                    Zero::zero()
+                } else {
+                    capacity - amount
+                }
+            }
+        }
+    }
+}

+ 90 - 0
runtime-modules/token-minting/src/mock.rs

@@ -0,0 +1,90 @@
+#![cfg(test)]
+
+use crate::*;
+
+use primitives::H256;
+
+use crate::{Module, Trait};
+use balances;
+use runtime_primitives::{
+    testing::Header,
+    traits::{BlakeTwo256, IdentityLookup},
+    Perbill,
+};
+use srml_support::{impl_outer_origin, parameter_types};
+
+impl_outer_origin! {
+    pub enum Origin for Test {}
+}
+
+// Workaround for https://github.com/rust-lang/rust/issues/26925 . Remove when sorted.
+#[derive(Clone, PartialEq, Eq, Debug)]
+pub struct Test;
+parameter_types! {
+    pub const BlockHashCount: u64 = 250;
+    pub const MaximumBlockWeight: u32 = 1024;
+    pub const MaximumBlockLength: u32 = 2 * 1024;
+    pub const AvailableBlockRatio: Perbill = Perbill::one();
+    pub const MinimumPeriod: u64 = 5;
+}
+
+impl system::Trait for Test {
+    type Origin = Origin;
+    type Index = u64;
+    type BlockNumber = u64;
+    type Call = ();
+    type Hash = H256;
+    type Hashing = BlakeTwo256;
+    type AccountId = u64;
+    type Lookup = IdentityLookup<Self::AccountId>;
+    type Header = Header;
+    type Event = ();
+    type BlockHashCount = BlockHashCount;
+    type MaximumBlockWeight = MaximumBlockWeight;
+    type MaximumBlockLength = MaximumBlockLength;
+    type AvailableBlockRatio = AvailableBlockRatio;
+    type Version = ();
+}
+
+parameter_types! {
+    pub const ExistentialDeposit: u32 = 0;
+    pub const TransferFee: u32 = 0;
+    pub const CreationFee: u32 = 0;
+    pub const TransactionBaseFee: u32 = 1;
+    pub const TransactionByteFee: u32 = 0;
+    pub const InitialMembersBalance: u64 = 2000;
+}
+
+impl balances::Trait for Test {
+    /// The type for recording an account's balance.
+    type Balance = u64;
+    /// What to do if an account's free balance gets zeroed.
+    type OnFreeBalanceZero = ();
+    /// What to do if a new account is created.
+    type OnNewAccount = ();
+    /// The ubiquitous event type.
+    type Event = ();
+
+    type DustRemoval = ();
+    type TransferPayment = ();
+    type ExistentialDeposit = ExistentialDeposit;
+    type TransferFee = TransferFee;
+    type CreationFee = CreationFee;
+}
+
+impl Trait for Test {
+    type Currency = Balances;
+    type MintId = u64;
+}
+
+pub fn build_test_externalities() -> runtime_io::TestExternalities {
+    let t = system::GenesisConfig::default()
+        .build_storage::<Test>()
+        .unwrap();
+
+    t.into()
+}
+
+pub type System = system::Module<Test>;
+pub type Balances = balances::Module<Test>;
+pub type Minting = Module<Test>;

+ 202 - 0
runtime-modules/token-minting/src/tests.rs

@@ -0,0 +1,202 @@
+#![cfg(test)]
+
+use super::*;
+use crate::mock::*;
+
+#[test]
+fn adding_and_removing_mints() {
+    build_test_externalities().execute_with(|| {
+        System::set_block_number(1);
+        let capacity: u64 = 5000;
+        let adjustment_amount: u64 = 500;
+
+        let adjustment = AdjustOnInterval {
+            adjustment_type: AdjustCapacityBy::Adding(adjustment_amount),
+            block_interval: 100,
+        };
+
+        let mint_id = Minting::add_mint(capacity, Some(Adjustment::Interval(adjustment)))
+            .ok()
+            .unwrap();
+        assert!(Minting::mint_exists(mint_id));
+
+        assert_eq!(Minting::get_mint_capacity(mint_id).ok().unwrap(), capacity);
+
+        assert_eq!(
+            Minting::get_mint_next_adjustment(mint_id),
+            Ok(Some(NextAdjustment {
+                adjustment,
+                at_block: 1 + 100,
+            }))
+        );
+
+        Minting::remove_mint(mint_id);
+        assert!(!Minting::mint_exists(mint_id));
+    });
+}
+
+#[test]
+fn minting() {
+    build_test_externalities().execute_with(|| {
+        let capacity: u64 = 5000;
+
+        let mint_id = Minting::add_mint(capacity, None).ok().unwrap();
+
+        assert!(Minting::transfer_tokens(mint_id, 1000, &1).is_ok());
+
+        assert_eq!(Balances::free_balance(&1), 1000);
+
+        assert_eq!(Minting::get_mint_capacity(mint_id).ok().unwrap(), 4000);
+    });
+}
+
+#[test]
+fn minting_exact() {
+    build_test_externalities().execute_with(|| {
+        let capacity: u64 = 1000;
+
+        let mint_id = Minting::add_mint(capacity, None).ok().unwrap();
+
+        assert_eq!(
+            Minting::transfer_tokens(mint_id, 2000, &1),
+            Err(TransferError::NotEnoughCapacity)
+        );
+    });
+}
+
+#[test]
+fn adjustment_adding() {
+    build_test_externalities().execute_with(|| {
+        System::set_block_number(0);
+        let capacity: u64 = 5000;
+        let adjustment_amount: u64 = 500;
+
+        let adjustment = AdjustOnInterval {
+            adjustment_type: AdjustCapacityBy::Adding(adjustment_amount),
+            block_interval: 100,
+        };
+
+        let mint_id = Minting::add_mint(capacity, Some(Adjustment::Interval(adjustment)))
+            .ok()
+            .unwrap();
+
+        Minting::update_mints(100);
+        assert_eq!(
+            Minting::get_mint_capacity(mint_id).ok().unwrap(),
+            capacity + (adjustment_amount * 1)
+        );
+
+        // no adjustments should happen
+        Minting::update_mints(100);
+        Minting::update_mints(140);
+        Minting::update_mints(199);
+
+        Minting::update_mints(200);
+        assert_eq!(
+            Minting::get_mint_capacity(mint_id).ok().unwrap(),
+            capacity + (adjustment_amount * 2)
+        );
+    });
+}
+
+#[test]
+fn adjustment_reducing() {
+    build_test_externalities().execute_with(|| {
+        System::set_block_number(0);
+        let capacity: u64 = 5000;
+        let adjustment_amount: u64 = 500;
+
+        let adjustment = AdjustOnInterval {
+            adjustment_type: AdjustCapacityBy::Reducing(adjustment_amount),
+            block_interval: 100,
+        };
+
+        let mint_id = Minting::add_mint(capacity, Some(Adjustment::Interval(adjustment)))
+            .ok()
+            .unwrap();
+
+        Minting::update_mints(100);
+        assert_eq!(
+            Minting::get_mint_capacity(mint_id).ok().unwrap(),
+            capacity - adjustment_amount
+        );
+
+        assert_eq!(
+            Minting::get_mint_capacity(mint_id).ok().unwrap(),
+            capacity - (adjustment_amount * 1)
+        );
+
+        // no adjustments should happen
+        Minting::update_mints(100);
+        Minting::update_mints(140);
+        Minting::update_mints(199);
+
+        Minting::update_mints(200);
+        assert_eq!(
+            Minting::get_mint_capacity(mint_id).ok().unwrap(),
+            capacity - (adjustment_amount * 2)
+        );
+    });
+}
+
+#[test]
+fn adjustment_setting() {
+    build_test_externalities().execute_with(|| {
+        System::set_block_number(0);
+        let capacity: u64 = 2000;
+        let setting_amount: u64 = 5000;
+
+        let adjustment = AdjustOnInterval {
+            adjustment_type: AdjustCapacityBy::Setting(setting_amount),
+            block_interval: 100,
+        };
+
+        let mint_id = Minting::add_mint(capacity, Some(Adjustment::Interval(adjustment)))
+            .ok()
+            .unwrap();
+
+        Minting::update_mints(100);
+        assert_eq!(
+            Minting::get_mint_capacity(mint_id).ok().unwrap(),
+            setting_amount
+        );
+    });
+}
+
+#[test]
+fn adjustment_first_interval() {
+    build_test_externalities().execute_with(|| {
+        System::set_block_number(0);
+        let capacity: u64 = 2000;
+        let amount: u64 = 500;
+
+        let adjustment = AdjustOnInterval {
+            adjustment_type: AdjustCapacityBy::Adding(amount),
+            block_interval: 100,
+        };
+
+        let mint_id = Minting::add_mint(
+            capacity,
+            Some(Adjustment::IntervalAfterFirstAdjustmentAbsolute(
+                adjustment, 1000,
+            )),
+        )
+        .ok()
+        .unwrap();
+
+        Minting::update_mints(100);
+        assert_eq!(Minting::get_mint_capacity(mint_id).ok().unwrap(), capacity);
+
+        Minting::update_mints(1000);
+        assert_eq!(
+            Minting::get_mint_capacity(mint_id).ok().unwrap(),
+            capacity + amount
+        );
+
+        Minting::update_mints(1100);
+        assert_eq!(
+            Minting::get_mint_capacity(mint_id).ok().unwrap(),
+            capacity + 2 * amount
+        );
+    });
+}

+ 44 - 0
runtime-modules/versioned-store-permissions/Cargo.toml

@@ -0,0 +1,44 @@
+[package]
+name = 'substrate-versioned-store-permissions-module'
+version = '1.0.1'
+authors = ['Joystream contributors']
+edition = '2018'
+
+[dependencies]
+hex-literal = '0.1.0'
+serde = { version = '1.0', optional = true }
+serde_derive = { version = '1.0', optional = true }
+codec = { package = 'parity-scale-codec', version = '1.0.0', default-features = false, features = ['derive'] }
+rstd = { package = 'sr-std', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+runtime-primitives = { package = 'sr-primitives', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+srml-support = { package = 'srml-support', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+srml-support-procedural = { package = 'srml-support-procedural', git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+system = { package = 'srml-system', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+timestamp = { package = 'srml-timestamp', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+runtime-io = { package = 'sr-io', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+# https://users.rust-lang.org/t/failure-derive-compilation-error/39062
+quote = '<=1.0.2'
+
+[dependencies.versioned-store]
+default_features = false
+package ='substrate-versioned-store'
+path = '../versioned-store'
+
+[dev-dependencies]
+runtime-io = { package = 'sr-io', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+primitives = { package = 'substrate-primitives', git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+
+[features]
+default = ['std']
+std = [
+	'serde',
+	'serde_derive',
+	'codec/std',
+	'rstd/std',
+	'runtime-io/std',
+	'runtime-primitives/std',
+	'srml-support/std',
+	'system/std',
+	'timestamp/std',
+	'versioned-store/std',
+]

+ 28 - 0
runtime-modules/versioned-store-permissions/src/constraint.rs

@@ -0,0 +1,28 @@
+use codec::{Decode, Encode};
+use rstd::collections::btree_set::BTreeSet;
+
+/// Reference to a specific property of a specific class.
+#[derive(Encode, Decode, Eq, PartialEq, Ord, PartialOrd, Clone, Debug)]
+pub struct PropertyOfClass<ClassId, PropertyIndex> {
+    pub class_id: ClassId,
+    pub property_index: PropertyIndex,
+}
+
+/// The type of constraint imposed on referencing a class via class property of type "Internal".
+#[derive(Encode, Decode, Eq, PartialEq, Clone, Debug)]
+pub enum ReferenceConstraint<ClassId: Ord, PropertyIndex: Ord> {
+    /// No property can reference the class.
+    NoReferencingAllowed,
+
+    /// Any property of any class may reference the class.
+    NoConstraint,
+
+    /// Only a set of properties of specific classes can reference the class.
+    Restricted(BTreeSet<PropertyOfClass<ClassId, PropertyIndex>>),
+}
+
+impl<ClassId: Ord, PropertyIndex: Ord> Default for ReferenceConstraint<ClassId, PropertyIndex> {
+    fn default() -> Self {
+        ReferenceConstraint::NoReferencingAllowed
+    }
+}

+ 57 - 0
runtime-modules/versioned-store-permissions/src/credentials.rs

@@ -0,0 +1,57 @@
+use codec::{Decode, Encode};
+use rstd::collections::btree_set::BTreeSet;
+use rstd::prelude::*;
+
+#[derive(Encode, Decode, Eq, PartialEq, Clone, Debug)]
+pub struct CredentialSet<Credential>(BTreeSet<Credential>);
+
+impl<Credential> From<Vec<Credential>> for CredentialSet<Credential>
+where
+    Credential: Ord,
+{
+    fn from(v: Vec<Credential>) -> CredentialSet<Credential> {
+        let mut set = CredentialSet(BTreeSet::new());
+        for credential in v.into_iter() {
+            set.insert(credential);
+        }
+        set
+    }
+}
+
+/// Default CredentialSet set is just an empty set.
+impl<Credential: Ord> Default for CredentialSet<Credential> {
+    fn default() -> Self {
+        CredentialSet(BTreeSet::new())
+    }
+}
+
+impl<Credential: Ord> CredentialSet<Credential> {
+    pub fn new() -> Self {
+        Self(BTreeSet::new())
+    }
+
+    pub fn insert(&mut self, value: Credential) -> bool {
+        self.0.insert(value)
+    }
+
+    pub fn contains(&self, value: &Credential) -> bool {
+        self.0.contains(value)
+    }
+
+    pub fn is_empty(&self) -> bool {
+        self.0.is_empty()
+    }
+}
+
+/// Type, derived from dispatchable call, identifies the caller
+#[derive(Encode, Decode, Eq, PartialEq, Ord, PartialOrd, Clone, Debug)]
+pub enum AccessLevel<Credential> {
+    /// ROOT origin
+    System,
+    /// Caller identified as the entity maintainer
+    EntityMaintainer, // Maybe enclose EntityId?
+    /// Verified Credential
+    Credential(Credential),
+    /// In cases where a signed extrinsic doesn't provide a Credential
+    Unspecified,
+}

+ 646 - 0
runtime-modules/versioned-store-permissions/src/lib.rs

@@ -0,0 +1,646 @@
+// Ensure we're `no_std` when compiling for Wasm.
+#![cfg_attr(not(feature = "std"), no_std)]
+
+use codec::Codec;
+use rstd::collections::btree_map::BTreeMap;
+use rstd::prelude::*;
+use runtime_primitives::traits::{MaybeSerialize, Member, SimpleArithmetic};
+use srml_support::{decl_module, decl_storage, dispatch, ensure, Parameter};
+use system;
+
+// EntityId, ClassId -> should be configured on versioned_store::Trait
+pub use versioned_store::{ClassId, ClassPropertyValue, EntityId, Property, PropertyValue};
+
+mod constraint;
+mod credentials;
+mod mock;
+mod operations;
+mod permissions;
+mod tests;
+
+pub use constraint::*;
+pub use credentials::*;
+pub use operations::*;
+pub use permissions::*;
+
+/// Trait for checking if an account has specified Credential
+pub trait CredentialChecker<T: Trait> {
+    fn account_has_credential(account: &T::AccountId, credential: T::Credential) -> bool;
+}
+
+/// An implementation where no account has any credential. Effectively
+/// only the system will be able to perform any action on the versioned store.
+impl<T: Trait> CredentialChecker<T> for () {
+    fn account_has_credential(_account: &T::AccountId, _credential: T::Credential) -> bool {
+        false
+    }
+}
+
+/// An implementation that calls into multiple checkers. This allows for multiple modules
+/// to maintain AccountId to Credential mappings.
+impl<T: Trait, X: CredentialChecker<T>, Y: CredentialChecker<T>> CredentialChecker<T> for (X, Y) {
+    fn account_has_credential(account: &T::AccountId, group: T::Credential) -> bool {
+        X::account_has_credential(account, group) || Y::account_has_credential(account, group)
+    }
+}
+
+/// Trait for externally checking if an account can create new classes in the versioned store.
+pub trait CreateClassPermissionsChecker<T: Trait> {
+    fn account_can_create_class_permissions(account: &T::AccountId) -> bool;
+}
+
+/// An implementation that does not permit any account to create classes. Effectively
+/// only the system can create classes.
+impl<T: Trait> CreateClassPermissionsChecker<T> for () {
+    fn account_can_create_class_permissions(_account: &T::AccountId) -> bool {
+        false
+    }
+}
+
+pub type ClassPermissionsType<T> =
+    ClassPermissions<ClassId, <T as Trait>::Credential, u16, <T as system::Trait>::BlockNumber>;
+
+pub trait Trait: system::Trait + versioned_store::Trait {
+    // type Event: ...
+    // Do we need Events?
+
+    /// Type that represents an actor or group of actors in the system.
+    type Credential: Parameter
+        + Member
+        + SimpleArithmetic
+        + Codec
+        + Default
+        + Copy
+        + Clone
+        + MaybeSerialize
+        + Eq
+        + PartialEq
+        + Ord;
+
+    /// External type for checking if an account has specified credential.
+    type CredentialChecker: CredentialChecker<Self>;
+
+    /// External type used to check if an account has permission to create new Classes.
+    type CreateClassPermissionsChecker: CreateClassPermissionsChecker<Self>;
+}
+
+decl_storage! {
+    trait Store for Module<T: Trait> as VersionedStorePermissions {
+      /// ClassPermissions of corresponding Classes in the versioned store
+      pub ClassPermissionsByClassId get(class_permissions_by_class_id): linked_map ClassId => ClassPermissionsType<T>;
+
+      /// Owner of an entity in the versioned store. If it is None then it is owned by the system.
+      pub EntityMaintainerByEntityId get(entity_maintainer_by_entity_id): linked_map EntityId => Option<T::Credential>;
+    }
+}
+
+decl_module! {
+    pub struct Module<T: Trait> for enum Call where origin: T::Origin {
+
+        /// Sets the admins for a class
+        fn set_class_admins(
+            origin,
+            class_id: ClassId,
+            admins: CredentialSet<T::Credential>
+        ) -> dispatch::Result {
+            let raw_origin = Self::ensure_root_or_signed(origin)?;
+
+            Self::mutate_class_permissions(
+                &raw_origin,
+                None,
+                Self::is_system, // root origin
+                class_id,
+                |class_permissions| {
+                    class_permissions.admins = admins;
+                    Ok(())
+                }
+            )
+        }
+
+        // Methods for updating concrete permissions
+
+        fn set_class_entity_permissions(
+            origin,
+            with_credential: Option<T::Credential>,
+            class_id: ClassId,
+            entity_permissions: EntityPermissions<T::Credential>
+        ) -> dispatch::Result {
+            let raw_origin = Self::ensure_root_or_signed(origin)?;
+
+            Self::mutate_class_permissions(
+                &raw_origin,
+                with_credential,
+                ClassPermissions::is_admin,
+                class_id,
+                |class_permissions| {
+                    class_permissions.entity_permissions = entity_permissions;
+                    Ok(())
+                }
+            )
+        }
+
+        fn set_class_entities_can_be_created(
+            origin,
+            with_credential: Option<T::Credential>,
+            class_id: ClassId,
+            can_be_created: bool
+        ) -> dispatch::Result {
+            let raw_origin = Self::ensure_root_or_signed(origin)?;
+
+            Self::mutate_class_permissions(
+                &raw_origin,
+                with_credential,
+                ClassPermissions::is_admin,
+                class_id,
+                |class_permissions| {
+                    class_permissions.entities_can_be_created = can_be_created;
+                    Ok(())
+                }
+            )
+        }
+
+        fn set_class_add_schemas_set(
+            origin,
+            with_credential: Option<T::Credential>,
+            class_id: ClassId,
+            credential_set: CredentialSet<T::Credential>
+        ) -> dispatch::Result {
+            let raw_origin = Self::ensure_root_or_signed(origin)?;
+
+            Self::mutate_class_permissions(
+                &raw_origin,
+                with_credential,
+                ClassPermissions::is_admin,
+                class_id,
+                |class_permissions| {
+                    class_permissions.add_schemas = credential_set;
+                    Ok(())
+                }
+            )
+        }
+
+        fn set_class_create_entities_set(
+            origin,
+            with_credential: Option<T::Credential>,
+            class_id: ClassId,
+            credential_set: CredentialSet<T::Credential>
+        ) -> dispatch::Result {
+            let raw_origin = Self::ensure_root_or_signed(origin)?;
+
+            Self::mutate_class_permissions(
+                &raw_origin,
+                with_credential,
+                ClassPermissions::is_admin,
+                class_id,
+                |class_permissions| {
+                    class_permissions.create_entities = credential_set;
+                    Ok(())
+                }
+            )
+        }
+
+        fn set_class_reference_constraint(
+            origin,
+            with_credential: Option<T::Credential>,
+            class_id: ClassId,
+            constraint: ReferenceConstraint<ClassId, u16>
+        ) -> dispatch::Result {
+            let raw_origin = Self::ensure_root_or_signed(origin)?;
+
+            Self::mutate_class_permissions(
+                &raw_origin,
+                with_credential,
+                ClassPermissions::is_admin,
+                class_id,
+                |class_permissions| {
+                    class_permissions.reference_constraint = constraint;
+                    Ok(())
+                }
+            )
+        }
+
+        // Setting a new maintainer for an entity may require having additional constraints.
+        // So for now it is disabled.
+        // pub fn set_entity_maintainer(
+        //     origin,
+        //     entity_id: EntityId,
+        //     new_maintainer: Option<T::Credential>
+        // ) -> dispatch::Result {
+        //     ensure_root(origin)?;
+
+        //     // ensure entity exists in the versioned store
+        //     let _ = Self::get_class_id_by_entity_id(entity_id)?;
+
+        //     <EntityMaintainerByEntityId<T>>::mutate(entity_id, |maintainer| {
+        //         *maintainer = new_maintainer;
+        //     });
+
+        //     Ok(())
+        // }
+
+        // Permissioned proxy calls to versioned store
+
+        pub fn create_class(
+            origin,
+            name: Vec<u8>,
+            description: Vec<u8>,
+            class_permissions: ClassPermissionsType<T>
+        ) -> dispatch::Result {
+            let raw_origin = Self::ensure_root_or_signed(origin)?;
+
+            let can_create_class = match raw_origin {
+                system::RawOrigin::Root => true,
+                system::RawOrigin::Signed(sender) => {
+                    T::CreateClassPermissionsChecker::account_can_create_class_permissions(&sender)
+                },
+                _ => false
+            };
+
+            if can_create_class {
+                let class_id = <versioned_store::Module<T>>::create_class(name, description)?;
+
+                // is there a need to assert class_id is unique?
+
+                <ClassPermissionsByClassId<T>>::insert(&class_id, class_permissions);
+
+                Ok(())
+            } else {
+                Err("NotPermittedToCreateClass")
+            }
+        }
+
+        pub fn create_class_with_default_permissions(
+            origin,
+            name: Vec<u8>,
+            description: Vec<u8>
+        ) -> dispatch::Result {
+            Self::create_class(origin, name, description, ClassPermissions::default())
+        }
+
+        pub fn add_class_schema(
+            origin,
+            with_credential: Option<T::Credential>,
+            class_id: ClassId,
+            existing_properties: Vec<u16>,
+            new_properties: Vec<Property>
+        ) -> dispatch::Result {
+            let raw_origin = Self::ensure_root_or_signed(origin)?;
+
+            Self::if_class_permissions_satisfied(
+                &raw_origin,
+                with_credential,
+                None,
+                ClassPermissions::can_add_class_schema,
+                class_id,
+                |_class_permissions, _access_level| {
+                    // If a new property points at another class,
+                    // at this point we don't enforce anything about reference constraints
+                    // because of the chicken and egg problem. Instead enforcement is done
+                    // at the time of creating an entity.
+                    let _schema_index = <versioned_store::Module<T>>::add_class_schema(class_id, existing_properties, new_properties)?;
+                    Ok(())
+                }
+            )
+        }
+
+        /// Creates a new entity of type class_id. The maintainer is set to be either None if the origin is root, or the provided credential
+        /// associated with signer.
+        pub fn create_entity(
+            origin,
+            with_credential: Option<T::Credential>,
+            class_id: ClassId
+        ) -> dispatch::Result {
+            let raw_origin = Self::ensure_root_or_signed(origin)?;
+            let _entity_id = Self::do_create_entity(&raw_origin, with_credential, class_id)?;
+            Ok(())
+        }
+
+        pub fn add_schema_support_to_entity(
+            origin,
+            with_credential: Option<T::Credential>,
+            as_entity_maintainer: bool,
+            entity_id: EntityId,
+            schema_id: u16, // Do not type alias u16!! - u16,
+            property_values: Vec<ClassPropertyValue>
+        ) -> dispatch::Result {
+            let raw_origin = Self::ensure_root_or_signed(origin)?;
+            Self::do_add_schema_support_to_entity(&raw_origin, with_credential, as_entity_maintainer, entity_id, schema_id, property_values)
+        }
+
+        pub fn update_entity_property_values(
+            origin,
+            with_credential: Option<T::Credential>,
+            as_entity_maintainer: bool,
+            entity_id: EntityId,
+            property_values: Vec<ClassPropertyValue>
+        ) -> dispatch::Result {
+            let raw_origin = Self::ensure_root_or_signed(origin)?;
+            Self::do_update_entity_property_values(&raw_origin, with_credential, as_entity_maintainer, entity_id, property_values)
+        }
+
+        pub fn transaction(origin, operations: Vec<Operation<T::Credential>>) -> dispatch::Result {
+            // This map holds the EntityId of the entity created as a result of executing a CreateEntity Operation
+            // keyed by the indexed of the operation, in the operations vector.
+            let mut entity_created_in_operation: BTreeMap<usize, EntityId> = BTreeMap::new();
+
+            let raw_origin = Self::ensure_root_or_signed(origin)?;
+
+            for (op_index, operation) in operations.into_iter().enumerate() {
+                match operation.operation_type {
+                    OperationType::CreateEntity(create_entity_operation) => {
+                        let entity_id = Self::do_create_entity(&raw_origin, operation.with_credential, create_entity_operation.class_id)?;
+                        entity_created_in_operation.insert(op_index, entity_id);
+                    },
+                    OperationType::UpdatePropertyValues(update_property_values_operation) => {
+                        let entity_id = operations::parametrized_entity_to_entity_id(&entity_created_in_operation, update_property_values_operation.entity_id)?;
+                        let property_values = operations::parametrized_property_values_to_property_values(&entity_created_in_operation, update_property_values_operation.new_parametrized_property_values)?;
+                        Self::do_update_entity_property_values(&raw_origin, operation.with_credential, operation.as_entity_maintainer, entity_id, property_values)?;
+                    },
+                    OperationType::AddSchemaSupportToEntity(add_schema_support_to_entity_operation) => {
+                        let entity_id = operations::parametrized_entity_to_entity_id(&entity_created_in_operation, add_schema_support_to_entity_operation.entity_id)?;
+                        let schema_id = add_schema_support_to_entity_operation.schema_id;
+                        let property_values = operations::parametrized_property_values_to_property_values(&entity_created_in_operation, add_schema_support_to_entity_operation.parametrized_property_values)?;
+                        Self::do_add_schema_support_to_entity(&raw_origin, operation.with_credential, operation.as_entity_maintainer, entity_id, schema_id, property_values)?;
+                    }
+                }
+            }
+
+            Ok(())
+        }
+    }
+}
+
+impl<T: Trait> Module<T> {
+    fn ensure_root_or_signed(
+        origin: T::Origin,
+    ) -> Result<system::RawOrigin<T::AccountId>, &'static str> {
+        match origin.into() {
+            Ok(system::RawOrigin::Root) => Ok(system::RawOrigin::Root),
+            Ok(system::RawOrigin::Signed(account_id)) => Ok(system::RawOrigin::Signed(account_id)),
+            _ => Err("BadOrigin:ExpectedRootOrSigned"),
+        }
+    }
+
+    fn do_create_entity(
+        raw_origin: &system::RawOrigin<T::AccountId>,
+        with_credential: Option<T::Credential>,
+        class_id: ClassId,
+    ) -> Result<EntityId, &'static str> {
+        Self::if_class_permissions_satisfied(
+            raw_origin,
+            with_credential,
+            None,
+            ClassPermissions::can_create_entity,
+            class_id,
+            |_class_permissions, access_level| {
+                let entity_id = <versioned_store::Module<T>>::create_entity(class_id)?;
+
+                // Note: mutating value to None is equivalient to removing the value from storage map
+                <EntityMaintainerByEntityId<T>>::mutate(
+                    entity_id,
+                    |maintainer| match access_level {
+                        AccessLevel::System => *maintainer = None,
+                        AccessLevel::Credential(credential) => *maintainer = Some(*credential),
+                        _ => *maintainer = None,
+                    },
+                );
+
+                Ok(entity_id)
+            },
+        )
+    }
+
+    fn do_update_entity_property_values(
+        raw_origin: &system::RawOrigin<T::AccountId>,
+        with_credential: Option<T::Credential>,
+        as_entity_maintainer: bool,
+        entity_id: EntityId,
+        property_values: Vec<ClassPropertyValue>,
+    ) -> dispatch::Result {
+        let class_id = Self::get_class_id_by_entity_id(entity_id)?;
+
+        Self::ensure_internal_property_values_permitted(class_id, &property_values)?;
+
+        let as_entity_maintainer = if as_entity_maintainer {
+            Some(entity_id)
+        } else {
+            None
+        };
+
+        Self::if_class_permissions_satisfied(
+            raw_origin,
+            with_credential,
+            as_entity_maintainer,
+            ClassPermissions::can_update_entity,
+            class_id,
+            |_class_permissions, _access_level| {
+                <versioned_store::Module<T>>::update_entity_property_values(
+                    entity_id,
+                    property_values,
+                )
+            },
+        )
+    }
+
+    fn do_add_schema_support_to_entity(
+        raw_origin: &system::RawOrigin<T::AccountId>,
+        with_credential: Option<T::Credential>,
+        as_entity_maintainer: bool,
+        entity_id: EntityId,
+        schema_id: u16,
+        property_values: Vec<ClassPropertyValue>,
+    ) -> dispatch::Result {
+        // class id of the entity being updated
+        let class_id = Self::get_class_id_by_entity_id(entity_id)?;
+
+        Self::ensure_internal_property_values_permitted(class_id, &property_values)?;
+
+        let as_entity_maintainer = if as_entity_maintainer {
+            Some(entity_id)
+        } else {
+            None
+        };
+
+        Self::if_class_permissions_satisfied(
+            raw_origin,
+            with_credential,
+            as_entity_maintainer,
+            ClassPermissions::can_update_entity,
+            class_id,
+            |_class_permissions, _access_level| {
+                <versioned_store::Module<T>>::add_schema_support_to_entity(
+                    entity_id,
+                    schema_id,
+                    property_values,
+                )
+            },
+        )
+    }
+
+    /// Derives the AccessLevel the caller is attempting to act with.
+    /// It expects only signed or root origin.
+    fn derive_access_level(
+        raw_origin: &system::RawOrigin<T::AccountId>,
+        with_credential: Option<T::Credential>,
+        as_entity_maintainer: Option<EntityId>,
+    ) -> Result<AccessLevel<T::Credential>, &'static str> {
+        match raw_origin {
+            system::RawOrigin::Root => Ok(AccessLevel::System),
+            system::RawOrigin::Signed(account_id) => {
+                if let Some(credential) = with_credential {
+                    if T::CredentialChecker::account_has_credential(&account_id, credential) {
+                        if let Some(entity_id) = as_entity_maintainer {
+                            // is entity maintained by system
+                            ensure!(
+                                <EntityMaintainerByEntityId<T>>::exists(entity_id),
+                                "NotEnityMaintainer"
+                            );
+                            // ensure entity maintainer matches
+                            match Self::entity_maintainer_by_entity_id(entity_id) {
+                                Some(maintainer_credential)
+                                    if credential == maintainer_credential =>
+                                {
+                                    Ok(AccessLevel::EntityMaintainer)
+                                }
+                                _ => Err("NotEnityMaintainer"),
+                            }
+                        } else {
+                            Ok(AccessLevel::Credential(credential))
+                        }
+                    } else {
+                        Err("OriginCannotActWithRequestedCredential")
+                    }
+                } else {
+                    Ok(AccessLevel::Unspecified)
+                }
+            }
+            _ => Err("BadOrigin:ExpectedRootOrSigned"),
+        }
+    }
+
+    /// Returns the stored class permissions if exist, error otherwise.
+    fn ensure_class_permissions(
+        class_id: ClassId,
+    ) -> Result<ClassPermissionsType<T>, &'static str> {
+        ensure!(
+            <ClassPermissionsByClassId<T>>::exists(class_id),
+            "ClassPermissionsNotFoundByClassId"
+        );
+        Ok(Self::class_permissions_by_class_id(class_id))
+    }
+
+    /// Derives the access level of the caller.
+    /// If the predicate passes, the mutate method is invoked.
+    fn mutate_class_permissions<Predicate, Mutate>(
+        raw_origin: &system::RawOrigin<T::AccountId>,
+        with_credential: Option<T::Credential>,
+        // predicate to test
+        predicate: Predicate,
+        // class permissions to perform mutation on if it exists
+        class_id: ClassId,
+        // actual mutation to apply.
+        mutate: Mutate,
+    ) -> dispatch::Result
+    where
+        Predicate:
+            FnOnce(&ClassPermissionsType<T>, &AccessLevel<T::Credential>) -> dispatch::Result,
+        Mutate: FnOnce(&mut ClassPermissionsType<T>) -> dispatch::Result,
+    {
+        let access_level = Self::derive_access_level(raw_origin, with_credential, None)?;
+        let mut class_permissions = Self::ensure_class_permissions(class_id)?;
+
+        predicate(&class_permissions, &access_level)?;
+        mutate(&mut class_permissions)?;
+        class_permissions.last_permissions_update = <system::Module<T>>::block_number();
+        <ClassPermissionsByClassId<T>>::insert(class_id, class_permissions);
+        Ok(())
+    }
+
+    fn is_system(
+        _: &ClassPermissionsType<T>,
+        access_level: &AccessLevel<T::Credential>,
+    ) -> dispatch::Result {
+        if *access_level == AccessLevel::System {
+            Ok(())
+        } else {
+            Err("NotRootOrigin")
+        }
+    }
+
+    /// Derives the access level of the caller.
+    /// If the peridcate passes the callback is invoked. Returns result of the callback
+    /// or error from failed predicate.
+    fn if_class_permissions_satisfied<Predicate, Callback, R>(
+        raw_origin: &system::RawOrigin<T::AccountId>,
+        with_credential: Option<T::Credential>,
+        as_entity_maintainer: Option<EntityId>,
+        // predicate to test
+        predicate: Predicate,
+        // class permissions to test
+        class_id: ClassId,
+        // callback to invoke if predicate passes
+        callback: Callback,
+    ) -> Result<R, &'static str>
+    where
+        Predicate:
+            FnOnce(&ClassPermissionsType<T>, &AccessLevel<T::Credential>) -> dispatch::Result,
+        Callback: FnOnce(
+            &ClassPermissionsType<T>,
+            &AccessLevel<T::Credential>,
+        ) -> Result<R, &'static str>,
+    {
+        let access_level =
+            Self::derive_access_level(raw_origin, with_credential, as_entity_maintainer)?;
+        let class_permissions = Self::ensure_class_permissions(class_id)?;
+
+        predicate(&class_permissions, &access_level)?;
+        callback(&class_permissions, &access_level)
+    }
+
+    fn get_class_id_by_entity_id(entity_id: EntityId) -> Result<ClassId, &'static str> {
+        // use a utility method on versioned_store module
+        ensure!(
+            versioned_store::EntityById::exists(entity_id),
+            "EntityNotFound"
+        );
+        let entity = <versioned_store::Module<T>>::entity_by_id(entity_id);
+        Ok(entity.class_id)
+    }
+
+    // Ensures property_values of type Internal that point to a class,
+    // the target entity and class exists and constraint allows it.
+    fn ensure_internal_property_values_permitted(
+        source_class_id: ClassId,
+        property_values: &[ClassPropertyValue],
+    ) -> dispatch::Result {
+        for property_value in property_values.iter() {
+            if let PropertyValue::Internal(ref target_entity_id) = property_value.value {
+                // get the class permissions for target class
+                let target_class_id = Self::get_class_id_by_entity_id(*target_entity_id)?;
+                // assert class permissions exists for target class
+                let class_permissions = Self::class_permissions_by_class_id(target_class_id);
+
+                // ensure internal reference is permitted
+                match class_permissions.reference_constraint {
+                    ReferenceConstraint::NoConstraint => Ok(()),
+                    ReferenceConstraint::NoReferencingAllowed => {
+                        Err("EntityCannotReferenceTargetEntity")
+                    }
+                    ReferenceConstraint::Restricted(permitted_properties) => {
+                        if permitted_properties.contains(&PropertyOfClass {
+                            class_id: source_class_id,
+                            property_index: property_value.in_class_index,
+                        }) {
+                            Ok(())
+                        } else {
+                            Err("EntityCannotReferenceTargetEntity")
+                        }
+                    }
+                }?;
+            }
+        }
+
+        // if we reach here all Internal properties have passed the constraint check
+        Ok(())
+    }
+}

+ 164 - 0
runtime-modules/versioned-store-permissions/src/mock.rs

@@ -0,0 +1,164 @@
+#![cfg(test)]
+
+use crate::*;
+use crate::{Module, Trait};
+
+use primitives::H256;
+use runtime_primitives::{
+    testing::Header,
+    traits::{BlakeTwo256, IdentityLookup},
+    Perbill,
+};
+use srml_support::{impl_outer_origin, parameter_types};
+use versioned_store::InputValidationLengthConstraint;
+
+impl_outer_origin! {
+    pub enum Origin for Runtime {}
+}
+
+// Workaround for https://github.com/rust-lang/rust/issues/26925 . Remove when sorted.
+#[derive(Clone, PartialEq, Eq, Debug)]
+pub struct Runtime;
+parameter_types! {
+    pub const BlockHashCount: u64 = 250;
+    pub const MaximumBlockWeight: u32 = 1024;
+    pub const MaximumBlockLength: u32 = 2 * 1024;
+    pub const AvailableBlockRatio: Perbill = Perbill::one();
+    pub const MinimumPeriod: u64 = 5;
+}
+
+impl system::Trait for Runtime {
+    type Origin = Origin;
+    type Index = u64;
+    type BlockNumber = u64;
+    type Call = ();
+    type Hash = H256;
+    type Hashing = BlakeTwo256;
+    type AccountId = u64;
+    type Lookup = IdentityLookup<Self::AccountId>;
+    type Header = Header;
+    type Event = ();
+    type BlockHashCount = BlockHashCount;
+    type MaximumBlockWeight = MaximumBlockWeight;
+    type MaximumBlockLength = MaximumBlockLength;
+    type AvailableBlockRatio = AvailableBlockRatio;
+    type Version = ();
+}
+
+impl timestamp::Trait for Runtime {
+    type Moment = u64;
+    type OnTimestampSet = ();
+    type MinimumPeriod = MinimumPeriod;
+}
+
+impl versioned_store::Trait for Runtime {
+    type Event = ();
+}
+
+impl Trait for Runtime {
+    type Credential = u64;
+    type CredentialChecker = MockCredentialChecker;
+    type CreateClassPermissionsChecker = MockCreateClassPermissionsChecker;
+}
+
+pub const MEMBER_ONE_WITH_CREDENTIAL_ZERO: u64 = 100;
+pub const MEMBER_TWO_WITH_CREDENTIAL_ZERO: u64 = 101;
+pub const MEMBER_ONE_WITH_CREDENTIAL_ONE: u64 = 102;
+pub const MEMBER_TWO_WITH_CREDENTIAL_ONE: u64 = 103;
+
+pub const PRINCIPAL_GROUP_MEMBERS: [[u64; 2]; 2] = [
+    [
+        MEMBER_ONE_WITH_CREDENTIAL_ZERO,
+        MEMBER_TWO_WITH_CREDENTIAL_ZERO,
+    ],
+    [
+        MEMBER_ONE_WITH_CREDENTIAL_ONE,
+        MEMBER_TWO_WITH_CREDENTIAL_ONE,
+    ],
+];
+
+pub struct MockCredentialChecker {}
+
+impl CredentialChecker<Runtime> for MockCredentialChecker {
+    fn account_has_credential(
+        account_id: &<Runtime as system::Trait>::AccountId,
+        credential_id: <Runtime as Trait>::Credential,
+    ) -> bool {
+        if (credential_id as usize) < PRINCIPAL_GROUP_MEMBERS.len() {
+            PRINCIPAL_GROUP_MEMBERS[credential_id as usize]
+                .iter()
+                .any(|id| *id == *account_id)
+        } else {
+            false
+        }
+    }
+}
+
+pub const CLASS_PERMISSIONS_CREATOR1: u64 = 200;
+pub const CLASS_PERMISSIONS_CREATOR2: u64 = 300;
+pub const UNAUTHORIZED_CLASS_PERMISSIONS_CREATOR: u64 = 50;
+
+const CLASS_PERMISSIONS_CREATORS: [u64; 2] =
+    [CLASS_PERMISSIONS_CREATOR1, CLASS_PERMISSIONS_CREATOR2];
+
+pub struct MockCreateClassPermissionsChecker {}
+
+impl CreateClassPermissionsChecker<Runtime> for MockCreateClassPermissionsChecker {
+    fn account_can_create_class_permissions(
+        account_id: &<Runtime as system::Trait>::AccountId,
+    ) -> bool {
+        CLASS_PERMISSIONS_CREATORS
+            .iter()
+            .any(|id| *id == *account_id)
+    }
+}
+
+// This function basically just builds a genesis storage key/value store according to
+// our desired mockup.
+
+fn default_versioned_store_genesis_config() -> versioned_store::GenesisConfig {
+    versioned_store::GenesisConfig {
+        class_by_id: vec![],
+        entity_by_id: vec![],
+        next_class_id: 1,
+        next_entity_id: 1,
+        property_name_constraint: InputValidationLengthConstraint {
+            min: 1,
+            max_min_diff: 49,
+        },
+        property_description_constraint: InputValidationLengthConstraint {
+            min: 0,
+            max_min_diff: 500,
+        },
+        class_name_constraint: InputValidationLengthConstraint {
+            min: 1,
+            max_min_diff: 49,
+        },
+        class_description_constraint: InputValidationLengthConstraint {
+            min: 0,
+            max_min_diff: 500,
+        },
+    }
+}
+
+fn build_test_externalities(
+    config: versioned_store::GenesisConfig,
+) -> runtime_io::TestExternalities {
+    let mut t = system::GenesisConfig::default()
+        .build_storage::<Runtime>()
+        .unwrap();
+
+    config.assimilate_storage(&mut t).unwrap();
+
+    t.into()
+}
+
+pub fn with_test_externalities<R, F: FnOnce() -> R>(f: F) -> R {
+    let versioned_store_config = default_versioned_store_genesis_config();
+    build_test_externalities(versioned_store_config).execute_with(f)
+}
+
+// pub type System = system::Module;
+
+/// Export module on a test runtime
+pub type Permissions = Module<Runtime>;

+ 135 - 0
runtime-modules/versioned-store-permissions/src/operations.rs

@@ -0,0 +1,135 @@
+use codec::{Decode, Encode};
+use rstd::collections::btree_map::BTreeMap;
+use rstd::prelude::*;
+use versioned_store::{ClassId, ClassPropertyValue, EntityId, PropertyValue};
+
+#[derive(Encode, Decode, Eq, PartialEq, Clone, Debug)]
+pub enum ParametrizedPropertyValue {
+    /// Same fields as normal PropertyValue
+    PropertyValue(PropertyValue),
+
+    /// This is the index of an operation creating an entity in the transaction/batch operations
+    InternalEntityJustAdded(u32), // should really be usize but it doesn't have Encode/Decode support
+
+    /// Vector of mix of Entities already existing and just added in a recent operation
+    InternalEntityVec(Vec<ParameterizedEntity>),
+}
+
+#[derive(Encode, Decode, Eq, PartialEq, Clone, Debug)]
+pub enum ParameterizedEntity {
+    InternalEntityJustAdded(u32),
+    ExistingEntity(EntityId),
+}
+
+#[derive(Encode, Decode, Eq, PartialEq, Clone, Debug)]
+pub struct ParametrizedClassPropertyValue {
+    /// Index is into properties vector of class.
+    pub in_class_index: u16,
+
+    /// Value of property with index `in_class_index` in a given class.
+    pub value: ParametrizedPropertyValue,
+}
+
+#[derive(Encode, Decode, Eq, PartialEq, Clone, Debug)]
+pub struct CreateEntityOperation {
+    pub class_id: ClassId,
+}
+
+#[derive(Encode, Decode, Eq, PartialEq, Clone, Debug)]
+pub struct UpdatePropertyValuesOperation {
+    pub entity_id: ParameterizedEntity,
+    pub new_parametrized_property_values: Vec<ParametrizedClassPropertyValue>,
+}
+
+#[derive(Encode, Decode, Eq, PartialEq, Clone, Debug)]
+pub struct AddSchemaSupportToEntityOperation {
+    pub entity_id: ParameterizedEntity,
+    pub schema_id: u16,
+    pub parametrized_property_values: Vec<ParametrizedClassPropertyValue>,
+}
+
+#[derive(Encode, Decode, Eq, PartialEq, Clone, Debug)]
+pub enum OperationType {
+    CreateEntity(CreateEntityOperation),
+    UpdatePropertyValues(UpdatePropertyValuesOperation),
+    AddSchemaSupportToEntity(AddSchemaSupportToEntityOperation),
+}
+
+#[derive(Encode, Decode, Eq, PartialEq, Clone, Debug)]
+pub struct Operation<Credential> {
+    pub with_credential: Option<Credential>,
+    pub as_entity_maintainer: bool,
+    pub operation_type: OperationType,
+}
+
+pub fn parametrized_entity_to_entity_id(
+    created_entities: &BTreeMap<usize, EntityId>,
+    entity: ParameterizedEntity,
+) -> Result<EntityId, &'static str> {
+    match entity {
+        ParameterizedEntity::ExistingEntity(entity_id) => Ok(entity_id),
+        ParameterizedEntity::InternalEntityJustAdded(op_index_u32) => {
+            let op_index = op_index_u32 as usize;
+            if created_entities.contains_key(&op_index) {
+                let entity_id = created_entities.get(&op_index).unwrap();
+                Ok(*entity_id)
+            } else {
+                Err("EntityNotCreatedByOperation")
+            }
+        }
+    }
+}
+
+pub fn parametrized_property_values_to_property_values(
+    created_entities: &BTreeMap<usize, EntityId>,
+    parametrized_property_values: Vec<ParametrizedClassPropertyValue>,
+) -> Result<Vec<ClassPropertyValue>, &'static str> {
+    let mut class_property_values: Vec<ClassPropertyValue> = vec![];
+
+    for parametrized_class_property_value in parametrized_property_values.into_iter() {
+        let property_value = match parametrized_class_property_value.value {
+            ParametrizedPropertyValue::PropertyValue(value) => value,
+            ParametrizedPropertyValue::InternalEntityJustAdded(
+                entity_created_in_operation_index,
+            ) => {
+                // Verify that referenced entity was indeed created created
+                let op_index = entity_created_in_operation_index as usize;
+                if created_entities.contains_key(&op_index) {
+                    let entity_id = created_entities.get(&op_index).unwrap();
+                    PropertyValue::Internal(*entity_id)
+                } else {
+                    return Err("EntityNotCreatedByOperation");
+                }
+            }
+            ParametrizedPropertyValue::InternalEntityVec(parametrized_entities) => {
+                let mut entities: Vec<EntityId> = vec![];
+
+                for parametrized_entity in parametrized_entities.into_iter() {
+                    match parametrized_entity {
+                        ParameterizedEntity::ExistingEntity(id) => entities.push(id),
+                        ParameterizedEntity::InternalEntityJustAdded(
+                            entity_created_in_operation_index,
+                        ) => {
+                            let op_index = entity_created_in_operation_index as usize;
+                            if created_entities.contains_key(&op_index) {
+                                let entity_id = created_entities.get(&op_index).unwrap();
+                                entities.push(*entity_id);
+                            } else {
+                                return Err("EntityNotCreatedByOperation");
+                            }
+                        }
+                    }
+                }
+
+                PropertyValue::InternalVec(entities)
+            }
+        };
+
+        class_property_values.push(ClassPropertyValue {
+            in_class_index: parametrized_class_property_value.in_class_index,
+            value: property_value,
+        });
+    }
+
+    Ok(class_property_values)
+}

+ 154 - 0
runtime-modules/versioned-store-permissions/src/permissions.rs

@@ -0,0 +1,154 @@
+use codec::{Decode, Encode};
+use srml_support::dispatch;
+
+use crate::constraint::*;
+use crate::credentials::*;
+
+/// Permissions for an instance of a Class in the versioned store.
+#[derive(Encode, Decode, Default, Eq, PartialEq, Clone, Debug)]
+pub struct ClassPermissions<ClassId, Credential, PropertyIndex, BlockNumber>
+where
+    ClassId: Ord,
+    Credential: Ord + Clone,
+    PropertyIndex: Ord,
+{
+    // concrete permissions
+    /// Permissions that are applied to entities of this class, define who in addition to
+    /// root origin can update entities of this class.
+    pub entity_permissions: EntityPermissions<Credential>,
+
+    /// Wether new entities of this class be created or not. Is not enforced for root origin.
+    pub entities_can_be_created: bool,
+
+    /// Who can add new schemas in the versioned store for this class
+    pub add_schemas: CredentialSet<Credential>,
+
+    /// Who can create new entities in the versioned store of this class
+    pub create_entities: CredentialSet<Credential>,
+
+    /// The type of constraint on referencing the class from other entities.
+    pub reference_constraint: ReferenceConstraint<ClassId, PropertyIndex>,
+
+    /// Who (in addition to root origin) can update all concrete permissions.
+    /// The admins can only be set by the root origin, "System".
+    pub admins: CredentialSet<Credential>,
+
+    // Block where permissions were changed
+    pub last_permissions_update: BlockNumber,
+}
+
+impl<ClassId, Credential, PropertyIndex, BlockNumber>
+    ClassPermissions<ClassId, Credential, PropertyIndex, BlockNumber>
+where
+    ClassId: Ord,
+    Credential: Ord + Clone,
+    PropertyIndex: Ord,
+{
+    /// Returns Ok if access_level is root origin or credential is in admins set, Err otherwise
+    pub fn is_admin(
+        class_permissions: &Self,
+        access_level: &AccessLevel<Credential>,
+    ) -> dispatch::Result {
+        match access_level {
+            AccessLevel::System => Ok(()),
+            AccessLevel::Credential(credential) => {
+                if class_permissions.admins.contains(credential) {
+                    Ok(())
+                } else {
+                    Err("NotInAdminsSet")
+                }
+            }
+            AccessLevel::Unspecified => Err("UnspecifiedActor"),
+            AccessLevel::EntityMaintainer => Err("AccessLevel::EntityMaintainer-UsedOutOfPlace"),
+        }
+    }
+
+    pub fn can_add_class_schema(
+        class_permissions: &Self,
+        access_level: &AccessLevel<Credential>,
+    ) -> dispatch::Result {
+        match access_level {
+            AccessLevel::System => Ok(()),
+            AccessLevel::Credential(credential) => {
+                if class_permissions.add_schemas.contains(credential) {
+                    Ok(())
+                } else {
+                    Err("NotInAddSchemasSet")
+                }
+            }
+            AccessLevel::Unspecified => Err("UnspecifiedActor"),
+            AccessLevel::EntityMaintainer => Err("AccessLevel::EntityMaintainer-UsedOutOfPlace"),
+        }
+    }
+
+    pub fn can_create_entity(
+        class_permissions: &Self,
+        access_level: &AccessLevel<Credential>,
+    ) -> dispatch::Result {
+        match access_level {
+            AccessLevel::System => Ok(()),
+            AccessLevel::Credential(credential) => {
+                if !class_permissions.entities_can_be_created {
+                    Err("EntitiesCannotBeCreated")
+                } else if class_permissions.create_entities.contains(credential) {
+                    Ok(())
+                } else {
+                    Err("NotInCreateEntitiesSet")
+                }
+            }
+            AccessLevel::Unspecified => Err("UnspecifiedActor"),
+            AccessLevel::EntityMaintainer => Err("AccessLevel::EntityMaintainer-UsedOutOfPlace"),
+        }
+    }
+
+    pub fn can_update_entity(
+        class_permissions: &Self,
+        access_level: &AccessLevel<Credential>,
+    ) -> dispatch::Result {
+        match access_level {
+            AccessLevel::System => Ok(()),
+            AccessLevel::Credential(credential) => {
+                if class_permissions
+                    .entity_permissions
+                    .update
+                    .contains(credential)
+                {
+                    Ok(())
+                } else {
+                    Err("CredentialNotInEntityPermissionsUpdateSet")
+                }
+            }
+            AccessLevel::EntityMaintainer => {
+                if class_permissions
+                    .entity_permissions
+                    .maintainer_has_all_permissions
+                {
+                    Ok(())
+                } else {
+                    Err("MaintainerNotGivenAllPermissions")
+                }
+            }
+            _ => Err("UnknownActor"),
+        }
+    }
+}
+
+#[derive(Encode, Decode, Clone, Debug, Eq, PartialEq)]
+pub struct EntityPermissions<Credential>
+where
+    Credential: Ord,
+{
+    // Principals permitted to update any entity of the class which this permission is associated with.
+    pub update: CredentialSet<Credential>,
+    /// Wether the designated maintainer (if set) of an entity has permission to update it.
+    pub maintainer_has_all_permissions: bool,
+}
+
+impl<Credential: Ord> Default for EntityPermissions<Credential> {
+    fn default() -> Self {
+        EntityPermissions {
+            maintainer_has_all_permissions: true,
+            update: CredentialSet::new(),
+        }
+    }
+}

+ 665 - 0
runtime-modules/versioned-store-permissions/src/tests.rs

@@ -0,0 +1,665 @@
+#![cfg(test)]
+
+use super::*;
+use crate::mock::*;
+use rstd::collections::btree_set::BTreeSet;
+use versioned_store::PropertyType;
+
+use srml_support::{assert_err, assert_ok};
+
+fn simple_test_schema() -> Vec<Property> {
+    vec![Property {
+        prop_type: PropertyType::Int64,
+        required: false,
+        name: b"field1".to_vec(),
+        description: b"Description field1".to_vec(),
+    }]
+}
+
+fn simple_test_entity_property_values() -> Vec<ClassPropertyValue> {
+    vec![ClassPropertyValue {
+        in_class_index: 0,
+        value: PropertyValue::Int64(1337),
+    }]
+}
+
+fn create_simple_class(permissions: ClassPermissionsType<Runtime>) -> ClassId {
+    let class_id = <versioned_store::Module<Runtime>>::next_class_id();
+    assert_ok!(Permissions::create_class(
+        Origin::signed(CLASS_PERMISSIONS_CREATOR1),
+        b"class_name_1".to_vec(),
+        b"class_description_1".to_vec(),
+        permissions
+    ));
+    class_id
+}
+
+fn create_simple_class_with_default_permissions() -> ClassId {
+    create_simple_class(Default::default())
+}
+
+fn class_permissions_minimal() -> ClassPermissionsType<Runtime> {
+    ClassPermissions {
+        // remove special permissions for entity maintainers
+        entity_permissions: EntityPermissions {
+            maintainer_has_all_permissions: false,
+            ..Default::default()
+        },
+        ..Default::default()
+    }
+}
+
+fn class_permissions_minimal_with_admins(
+    admins: Vec<<Runtime as Trait>::Credential>,
+) -> ClassPermissionsType<Runtime> {
+    ClassPermissions {
+        admins: admins.into(),
+        ..class_permissions_minimal()
+    }
+}
+
+fn next_entity_id() -> EntityId {
+    <versioned_store::Module<Runtime>>::next_entity_id()
+}
+
+#[test]
+fn create_class_then_entity_with_default_class_permissions() {
+    with_test_externalities(|| {
+        // Only authorized accounts can create classes
+        assert_err!(
+            Permissions::create_class_with_default_permissions(
+                Origin::signed(UNAUTHORIZED_CLASS_PERMISSIONS_CREATOR),
+                b"class_name".to_vec(),
+                b"class_description".to_vec(),
+            ),
+            "NotPermittedToCreateClass"
+        );
+
+        let class_id = create_simple_class_with_default_permissions();
+
+        assert!(<ClassPermissionsByClassId<Runtime>>::exists(class_id));
+
+        // default class permissions have empty add_schema acl
+        assert_err!(
+            Permissions::add_class_schema(
+                Origin::signed(MEMBER_ONE_WITH_CREDENTIAL_ZERO),
+                Some(0),
+                class_id,
+                vec![],
+                simple_test_schema()
+            ),
+            "NotInAddSchemasSet"
+        );
+
+        // give members of GROUP_ZERO permission to add schemas
+        let add_schema_set = CredentialSet::from(vec![0]);
+        assert_ok!(Permissions::set_class_add_schemas_set(
+            Origin::ROOT,
+            None,
+            class_id,
+            add_schema_set
+        ));
+
+        // successfully add a new schema
+        assert_ok!(Permissions::add_class_schema(
+            Origin::signed(MEMBER_ONE_WITH_CREDENTIAL_ZERO),
+            Some(0),
+            class_id,
+            vec![],
+            simple_test_schema()
+        ));
+
+        // System can always create entities (provided class exists) bypassing any permissions
+        let entity_id_1 = next_entity_id();
+        assert_ok!(Permissions::create_entity(Origin::ROOT, None, class_id,));
+        // entities created by system are "un-owned"
+        assert!(!<EntityMaintainerByEntityId<Runtime>>::exists(entity_id_1));
+        assert_eq!(
+            Permissions::entity_maintainer_by_entity_id(entity_id_1),
+            None
+        );
+
+        // default permissions have empty create_entities set and by default no entities can be created
+        assert_err!(
+            Permissions::create_entity(
+                Origin::signed(MEMBER_ONE_WITH_CREDENTIAL_ONE),
+                Some(1),
+                class_id,
+            ),
+            "EntitiesCannotBeCreated"
+        );
+
+        assert_ok!(Permissions::set_class_entities_can_be_created(
+            Origin::ROOT,
+            None,
+            class_id,
+            true
+        ));
+
+        assert_err!(
+            Permissions::create_entity(
+                Origin::signed(MEMBER_ONE_WITH_CREDENTIAL_ONE),
+                Some(1),
+                class_id,
+            ),
+            "NotInCreateEntitiesSet"
+        );
+
+        // give members of GROUP_ONE permission to create entities
+        let create_entities_set = CredentialSet::from(vec![1]);
+        assert_ok!(Permissions::set_class_create_entities_set(
+            Origin::ROOT,
+            None,
+            class_id,
+            create_entities_set
+        ));
+
+        let entity_id_2 = next_entity_id();
+        assert_ok!(Permissions::create_entity(
+            Origin::signed(MEMBER_ONE_WITH_CREDENTIAL_ONE),
+            Some(1),
+            class_id,
+        ));
+        assert!(<EntityMaintainerByEntityId<Runtime>>::exists(entity_id_2));
+        assert_eq!(
+            Permissions::entity_maintainer_by_entity_id(entity_id_2),
+            Some(1)
+        );
+
+        // Updating entity must be authorized
+        assert_err!(
+            Permissions::add_schema_support_to_entity(
+                Origin::signed(MEMBER_ONE_WITH_CREDENTIAL_ZERO),
+                Some(0),
+                false, // not claiming to be entity maintainer
+                entity_id_2,
+                0, // first schema created
+                simple_test_entity_property_values()
+            ),
+            "CredentialNotInEntityPermissionsUpdateSet"
+        );
+
+        // default permissions give entity maintainer permission to update and delete
+        assert_ok!(Permissions::add_schema_support_to_entity(
+            Origin::signed(MEMBER_ONE_WITH_CREDENTIAL_ONE),
+            Some(1),
+            true, // we are claiming to be the entity maintainer
+            entity_id_2,
+            0,
+            simple_test_entity_property_values()
+        ));
+        assert_ok!(Permissions::update_entity_property_values(
+            Origin::signed(MEMBER_ONE_WITH_CREDENTIAL_ONE),
+            Some(1),
+            true, // we are claiming to be the entity maintainer
+            entity_id_2,
+            simple_test_entity_property_values()
+        ));
+    })
+}
+
+#[test]
+fn class_permissions_set_admins() {
+    with_test_externalities(|| {
+        // create a class where all permission sets are empty
+        let class_id = create_simple_class(class_permissions_minimal());
+        let class_permissions = Permissions::class_permissions_by_class_id(class_id);
+
+        assert!(class_permissions.admins.is_empty());
+
+        let credential_set = CredentialSet::from(vec![1]);
+
+        // only root should be able to set admins
+        assert_err!(
+            Permissions::set_class_admins(Origin::signed(1), class_id, credential_set.clone()),
+            "NotRootOrigin"
+        );
+        assert_err!(
+            Permissions::set_class_admins(
+                Origin::NONE, //unsigned inherent?
+                class_id,
+                credential_set.clone()
+            ),
+            "BadOrigin:ExpectedRootOrSigned"
+        );
+
+        // root origin can set admins
+        assert_ok!(Permissions::set_class_admins(
+            Origin::ROOT,
+            class_id,
+            credential_set.clone()
+        ));
+
+        let class_permissions = Permissions::class_permissions_by_class_id(class_id);
+        assert_eq!(class_permissions.admins, credential_set);
+    })
+}
+
+#[test]
+fn class_permissions_set_add_schemas_set() {
+    with_test_externalities(|| {
+        const ADMIN_ACCOUNT: u64 = MEMBER_ONE_WITH_CREDENTIAL_ZERO;
+        // create a class where all permission sets are empty
+        let class_id = create_simple_class(class_permissions_minimal_with_admins(vec![0]));
+        let class_permissions = Permissions::class_permissions_by_class_id(class_id);
+
+        assert!(class_permissions.add_schemas.is_empty());
+
+        let credential_set1 = CredentialSet::from(vec![1, 2]);
+        let credential_set2 = CredentialSet::from(vec![3, 4]);
+
+        // root
+        assert_ok!(Permissions::set_class_add_schemas_set(
+            Origin::ROOT,
+            None,
+            class_id,
+            credential_set1.clone()
+        ));
+        let class_permissions = Permissions::class_permissions_by_class_id(class_id);
+        assert_eq!(class_permissions.add_schemas, credential_set1);
+
+        // admins
+        assert_ok!(Permissions::set_class_add_schemas_set(
+            Origin::signed(ADMIN_ACCOUNT),
+            Some(0),
+            class_id,
+            credential_set2.clone()
+        ));
+        let class_permissions = Permissions::class_permissions_by_class_id(class_id);
+        assert_eq!(class_permissions.add_schemas, credential_set2);
+
+        // non-admins
+        assert_err!(
+            Permissions::set_class_add_schemas_set(
+                Origin::signed(MEMBER_ONE_WITH_CREDENTIAL_ONE),
+                Some(1),
+                class_id,
+                credential_set2.clone()
+            ),
+            "NotInAdminsSet"
+        );
+    })
+}
+
+#[test]
+fn class_permissions_set_class_create_entities_set() {
+    with_test_externalities(|| {
+        const ADMIN_ACCOUNT: u64 = MEMBER_ONE_WITH_CREDENTIAL_ZERO;
+        // create a class where all permission sets are empty
+        let class_id = create_simple_class(class_permissions_minimal_with_admins(vec![0]));
+        let class_permissions = Permissions::class_permissions_by_class_id(class_id);
+
+        assert!(class_permissions.create_entities.is_empty());
+
+        let credential_set1 = CredentialSet::from(vec![1, 2]);
+        let credential_set2 = CredentialSet::from(vec![3, 4]);
+
+        // root
+        assert_ok!(Permissions::set_class_create_entities_set(
+            Origin::ROOT,
+            None,
+            class_id,
+            credential_set1.clone()
+        ));
+        let class_permissions = Permissions::class_permissions_by_class_id(class_id);
+        assert_eq!(class_permissions.create_entities, credential_set1);
+
+        // admins
+        assert_ok!(Permissions::set_class_create_entities_set(
+            Origin::signed(ADMIN_ACCOUNT),
+            Some(0),
+            class_id,
+            credential_set2.clone()
+        ));
+        let class_permissions = Permissions::class_permissions_by_class_id(class_id);
+        assert_eq!(class_permissions.create_entities, credential_set2);
+
+        // non-admins
+        assert_err!(
+            Permissions::set_class_create_entities_set(
+                Origin::signed(MEMBER_ONE_WITH_CREDENTIAL_ONE),
+                Some(1),
+                class_id,
+                credential_set2.clone()
+            ),
+            "NotInAdminsSet"
+        );
+    })
+}
+
+#[test]
+fn class_permissions_set_class_entities_can_be_created() {
+    with_test_externalities(|| {
+        const ADMIN_ACCOUNT: u64 = MEMBER_ONE_WITH_CREDENTIAL_ZERO;
+        // create a class where all permission sets are empty
+        let class_id = create_simple_class(class_permissions_minimal_with_admins(vec![0]));
+        let class_permissions = Permissions::class_permissions_by_class_id(class_id);
+
+        assert_eq!(class_permissions.entities_can_be_created, false);
+
+        // root
+        assert_ok!(Permissions::set_class_entities_can_be_created(
+            Origin::ROOT,
+            None,
+            class_id,
+            true
+        ));
+        let class_permissions = Permissions::class_permissions_by_class_id(class_id);
+        assert_eq!(class_permissions.entities_can_be_created, true);
+
+        // admins
+        assert_ok!(Permissions::set_class_entities_can_be_created(
+            Origin::signed(ADMIN_ACCOUNT),
+            Some(0),
+            class_id,
+            false
+        ));
+        let class_permissions = Permissions::class_permissions_by_class_id(class_id);
+        assert_eq!(class_permissions.entities_can_be_created, false);
+
+        // non-admins
+        assert_err!(
+            Permissions::set_class_entities_can_be_created(
+                Origin::signed(MEMBER_ONE_WITH_CREDENTIAL_ONE),
+                Some(1),
+                class_id,
+                true
+            ),
+            "NotInAdminsSet"
+        );
+    })
+}
+
+#[test]
+fn class_permissions_set_class_entity_permissions() {
+    with_test_externalities(|| {
+        const ADMIN_ACCOUNT: u64 = MEMBER_ONE_WITH_CREDENTIAL_ZERO;
+        // create a class where all permission sets are empty
+        let class_id = create_simple_class(class_permissions_minimal_with_admins(vec![0]));
+        let class_permissions = Permissions::class_permissions_by_class_id(class_id);
+
+        assert!(class_permissions.entity_permissions.update.is_empty());
+
+        let entity_permissions1 = EntityPermissions {
+            update: CredentialSet::from(vec![1]),
+            maintainer_has_all_permissions: true,
+        };
+
+        //root
+        assert_ok!(Permissions::set_class_entity_permissions(
+            Origin::ROOT,
+            None,
+            class_id,
+            entity_permissions1.clone()
+        ));
+        let class_permissions = Permissions::class_permissions_by_class_id(class_id);
+        assert_eq!(class_permissions.entity_permissions, entity_permissions1);
+
+        let entity_permissions2 = EntityPermissions {
+            update: CredentialSet::from(vec![4]),
+            maintainer_has_all_permissions: true,
+        };
+        //admins
+        assert_ok!(Permissions::set_class_entity_permissions(
+            Origin::signed(ADMIN_ACCOUNT),
+            Some(0),
+            class_id,
+            entity_permissions2.clone()
+        ));
+        let class_permissions = Permissions::class_permissions_by_class_id(class_id);
+        assert_eq!(class_permissions.entity_permissions, entity_permissions2);
+
+        // non admins
+        assert_err!(
+            Permissions::set_class_entity_permissions(
+                Origin::signed(MEMBER_ONE_WITH_CREDENTIAL_ONE),
+                Some(1),
+                class_id,
+                entity_permissions2.clone()
+            ),
+            "NotInAdminsSet"
+        );
+    })
+}
+
+#[test]
+fn class_permissions_set_class_reference_constraint() {
+    with_test_externalities(|| {
+        const ADMIN_ACCOUNT: u64 = MEMBER_ONE_WITH_CREDENTIAL_ZERO;
+        // create a class where all permission sets are empty
+        let class_id = create_simple_class(class_permissions_minimal_with_admins(vec![0]));
+        let class_permissions = Permissions::class_permissions_by_class_id(class_id);
+
+        assert_eq!(class_permissions.reference_constraint, Default::default());
+
+        let mut constraints_set = BTreeSet::new();
+        constraints_set.insert(PropertyOfClass {
+            class_id: 1,
+            property_index: 0,
+        });
+        let reference_constraint1 = ReferenceConstraint::Restricted(constraints_set);
+
+        //root
+        assert_ok!(Permissions::set_class_reference_constraint(
+            Origin::ROOT,
+            None,
+            class_id,
+            reference_constraint1.clone()
+        ));
+        let class_permissions = Permissions::class_permissions_by_class_id(class_id);
+        assert_eq!(
+            class_permissions.reference_constraint,
+            reference_constraint1
+        );
+
+        let mut constraints_set = BTreeSet::new();
+        constraints_set.insert(PropertyOfClass {
+            class_id: 2,
+            property_index: 2,
+        });
+        let reference_constraint2 = ReferenceConstraint::Restricted(constraints_set);
+
+        //admins
+        assert_ok!(Permissions::set_class_reference_constraint(
+            Origin::signed(ADMIN_ACCOUNT),
+            Some(0),
+            class_id,
+            reference_constraint2.clone()
+        ));
+        let class_permissions = Permissions::class_permissions_by_class_id(class_id);
+        assert_eq!(
+            class_permissions.reference_constraint,
+            reference_constraint2
+        );
+
+        // non admins
+        assert_err!(
+            Permissions::set_class_reference_constraint(
+                Origin::signed(MEMBER_ONE_WITH_CREDENTIAL_ONE),
+                Some(1),
+                class_id,
+                reference_constraint2.clone()
+            ),
+            "NotInAdminsSet"
+        );
+    })
+}
+
+#[test]
+fn batch_transaction_simple() {
+    with_test_externalities(|| {
+        const CREDENTIAL_ONE: u64 = 1;
+
+        let new_class_id = create_simple_class(ClassPermissions {
+            entities_can_be_created: true,
+            create_entities: vec![CREDENTIAL_ONE].into(),
+            reference_constraint: ReferenceConstraint::NoConstraint,
+            ..Default::default()
+        });
+
+        let new_properties = vec![Property {
+            prop_type: PropertyType::Internal(new_class_id),
+            required: true,
+            name: b"entity".to_vec(),
+            description: b"another entity of same class".to_vec(),
+        }];
+
+        assert_ok!(Permissions::add_class_schema(
+            Origin::ROOT,
+            None,
+            new_class_id,
+            vec![],
+            new_properties
+        ));
+
+        let operations = vec![
+            Operation {
+                with_credential: Some(CREDENTIAL_ONE),
+                as_entity_maintainer: false,
+                operation_type: OperationType::CreateEntity(CreateEntityOperation {
+                    class_id: new_class_id,
+                }),
+            },
+            Operation {
+                with_credential: Some(CREDENTIAL_ONE),
+                as_entity_maintainer: true, // in prior operation CREDENTIAL_ONE became the maintainer
+                operation_type: OperationType::AddSchemaSupportToEntity(
+                    AddSchemaSupportToEntityOperation {
+                        entity_id: ParameterizedEntity::InternalEntityJustAdded(0), // index 0 (prior operation)
+                        schema_id: 0,
+                        parametrized_property_values: vec![ParametrizedClassPropertyValue {
+                            in_class_index: 0,
+                            value: ParametrizedPropertyValue::InternalEntityJustAdded(0),
+                        }],
+                    },
+                ),
+            },
+            Operation {
+                with_credential: Some(CREDENTIAL_ONE),
+                as_entity_maintainer: false,
+                operation_type: OperationType::CreateEntity(CreateEntityOperation {
+                    class_id: new_class_id,
+                }),
+            },
+            Operation {
+                with_credential: Some(CREDENTIAL_ONE),
+                as_entity_maintainer: true, // in prior operation CREDENTIAL_ONE became the maintainer
+                operation_type: OperationType::UpdatePropertyValues(
+                    UpdatePropertyValuesOperation {
+                        entity_id: ParameterizedEntity::InternalEntityJustAdded(0), // index 0 (prior operation)
+                        new_parametrized_property_values: vec![ParametrizedClassPropertyValue {
+                            in_class_index: 0,
+                            value: ParametrizedPropertyValue::InternalEntityJustAdded(2),
+                        }],
+                    },
+                ),
+            },
+        ];
+
+        let entity_id = next_entity_id();
+
+        assert_ok!(Permissions::transaction(
+            Origin::signed(MEMBER_ONE_WITH_CREDENTIAL_ONE),
+            operations
+        ));
+
+        // two entities created
+        assert!(versioned_store::EntityById::exists(entity_id));
+        assert!(versioned_store::EntityById::exists(entity_id + 1));
+    })
+}
+
+#[test]
+fn batch_transaction_vector_of_entities() {
+    with_test_externalities(|| {
+        const CREDENTIAL_ONE: u64 = 1;
+
+        let new_class_id = create_simple_class(ClassPermissions {
+            entities_can_be_created: true,
+            create_entities: vec![CREDENTIAL_ONE].into(),
+            reference_constraint: ReferenceConstraint::NoConstraint,
+            ..Default::default()
+        });
+
+        let new_properties = vec![Property {
+            prop_type: PropertyType::InternalVec(10, new_class_id),
+            required: true,
+            name: b"entities".to_vec(),
+            description: b"vector of entities of same class".to_vec(),
+        }];
+
+        assert_ok!(Permissions::add_class_schema(
+            Origin::ROOT,
+            None,
+            new_class_id,
+            vec![],
+            new_properties
+        ));
+
+        let operations = vec![
+            Operation {
+                with_credential: Some(CREDENTIAL_ONE),
+                as_entity_maintainer: false,
+                operation_type: OperationType::CreateEntity(CreateEntityOperation {
+                    class_id: new_class_id,
+                }),
+            },
+            Operation {
+                with_credential: Some(CREDENTIAL_ONE),
+                as_entity_maintainer: false,
+                operation_type: OperationType::CreateEntity(CreateEntityOperation {
+                    class_id: new_class_id,
+                }),
+            },
+            Operation {
+                with_credential: Some(CREDENTIAL_ONE),
+                as_entity_maintainer: false,
+                operation_type: OperationType::CreateEntity(CreateEntityOperation {
+                    class_id: new_class_id,
+                }),
+            },
+            Operation {
+                with_credential: Some(CREDENTIAL_ONE),
+                as_entity_maintainer: true, // in prior operation CREDENTIAL_ONE became the maintainer
+                operation_type: OperationType::AddSchemaSupportToEntity(
+                    AddSchemaSupportToEntityOperation {
+                        entity_id: ParameterizedEntity::InternalEntityJustAdded(0),
+                        schema_id: 0,
+                        parametrized_property_values: vec![ParametrizedClassPropertyValue {
+                            in_class_index: 0,
+                            value: ParametrizedPropertyValue::InternalEntityVec(vec![
+                                ParameterizedEntity::InternalEntityJustAdded(1),
+                                ParameterizedEntity::InternalEntityJustAdded(2),
+                            ]),
+                        }],
+                    },
+                ),
+            },
+        ];
+
+        let entity_id = next_entity_id();
+
+        assert_ok!(Permissions::transaction(
+            Origin::signed(MEMBER_ONE_WITH_CREDENTIAL_ONE),
+            operations
+        ));
+
+        // three entities created
+        assert!(versioned_store::EntityById::exists(entity_id));
+        assert!(versioned_store::EntityById::exists(entity_id + 1));
+        assert!(versioned_store::EntityById::exists(entity_id + 2));
+
+        assert_eq!(
+            versioned_store::EntityById::get(entity_id),
+            versioned_store::Entity {
+                class_id: new_class_id,
+                id: entity_id,
+                in_class_schema_indexes: vec![0],
+                values: vec![ClassPropertyValue {
+                    in_class_index: 0,
+                    value: PropertyValue::InternalVec(vec![entity_id + 1, entity_id + 2,])
+                }]
+            }
+        );
+    })
+}

+ 50 - 0
runtime-modules/versioned-store/Cargo.toml

@@ -0,0 +1,50 @@
+[package]
+name = 'substrate-versioned-store'
+version = '1.0.1'
+authors = ['Joystream contributors']
+edition = '2018'
+
+[dependencies]
+hex-literal = '0.1.0'
+serde = { version = '1.0', optional = true }
+serde_derive = { version = '1.0', optional = true }
+rstd = { package = 'sr-std', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+runtime-primitives = { package = 'sr-primitives', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+srml-support = { package = 'srml-support', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+srml-support-procedural = { package = 'srml-support-procedural', git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+system = { package = 'srml-system', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+balances = { package = 'srml-balances', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+codec = { package = 'parity-scale-codec', version = '1.0.0', default-features = false, features = ['derive'] }
+# https://users.rust-lang.org/t/failure-derive-compilation-error/39062
+quote = '<=1.0.2'
+
+[dependencies.timestamp]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'srml-timestamp'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dependencies.runtime-io]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'sr-io'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dev-dependencies]
+runtime-io = { package = 'sr-io', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+primitives = { package = 'substrate-primitives', git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+
+[features]
+default = ['std']
+std = [
+	'serde',
+	'serde_derive',
+	'codec/std',
+	'rstd/std',
+	'runtime-io/std',
+	'runtime-primitives/std',
+	'srml-support/std',
+	'system/std',
+	'balances/std',
+	'timestamp/std',
+]

+ 528 - 0
runtime-modules/versioned-store/src/example.rs

@@ -0,0 +1,528 @@
+#![cfg(test)]
+
+use super::*;
+use crate::mock::*;
+
+use srml_support::assert_ok;
+
+/// This example uses Class, Properties, Schema and Entity structures
+/// to describe the Staked podcast channel and its second episode.
+/// See https://staked.libsyn.com/rss
+
+#[test]
+fn create_podcast_class_schema() {
+    with_test_externalities(|| {
+        fn common_text_prop() -> PropertyType {
+            PropertyType::Text(200)
+        }
+
+        fn long_text_prop() -> PropertyType {
+            PropertyType::Text(4000)
+        }
+
+        // Channel props:
+        // ------------------------------------------
+
+        let channel_props = vec![
+            // 0
+            Property {
+                prop_type: common_text_prop(),
+                required: true,
+                name: b"atom:link".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 1
+            Property {
+                prop_type: common_text_prop(),
+                required: true,
+                name: b"title".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 2
+            Property {
+                prop_type: common_text_prop(),
+                required: false,
+                name: b"pubDate".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 3
+            Property {
+                prop_type: common_text_prop(),
+                required: false,
+                name: b"lastBuildDate".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 4
+            Property {
+                prop_type: common_text_prop(),
+                required: false,
+                name: b"generator".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 5
+            Property {
+                prop_type: common_text_prop(),
+                required: false,
+                name: b"link".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 6
+            // Example: en-us
+            Property {
+                prop_type: PropertyType::Text(5),
+                required: false,
+                name: b"language".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 7
+            Property {
+                prop_type: common_text_prop(),
+                required: false,
+                name: b"copyright".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 8
+            Property {
+                prop_type: common_text_prop(),
+                required: false,
+                name: b"docs".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 9
+            Property {
+                prop_type: common_text_prop(),
+                required: false,
+                name: b"managingEditor".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 10
+            Property {
+                prop_type: common_text_prop(),
+                required: false,
+                name: b"image/url".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 11
+            Property {
+                prop_type: common_text_prop(),
+                required: false,
+                name: b"image/title".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 12
+            Property {
+                prop_type: common_text_prop(),
+                required: false,
+                name: b"image/link".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 13
+            Property {
+                prop_type: common_text_prop(),
+                required: false,
+                name: b"itunes:summary".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 14
+            // TODO this could be Internal prop.
+            Property {
+                prop_type: common_text_prop(),
+                required: false,
+                name: b"itunes:author".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 15
+            // TODO make this as a text vec?
+            Property {
+                prop_type: common_text_prop(),
+                required: false,
+                name: b"itunes:keywords".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 16
+            Property {
+                prop_type: PropertyType::TextVec(10, 100),
+                required: false,
+                name: b"itunes:category".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 17
+            Property {
+                prop_type: common_text_prop(),
+                required: false,
+                name: b"itunes:image".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 18
+            Property {
+                prop_type: common_text_prop(),
+                required: false,
+                name: b"itunes:explicit".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 19
+            Property {
+                prop_type: common_text_prop(),
+                required: false,
+                name: b"itunes:owner/itunes:name".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 20
+            Property {
+                prop_type: common_text_prop(),
+                required: false,
+                name: b"itunes:owner/itunes:email".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 21
+            Property {
+                prop_type: PropertyType::Text(4000),
+                required: false,
+                name: b"description".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 22
+            Property {
+                prop_type: common_text_prop(),
+                required: false,
+                name: b"itunes:subtitle".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 23
+            Property {
+                prop_type: common_text_prop(),
+                required: false,
+                name: b"itunes:type".to_vec(),
+                description: b"".to_vec(),
+            },
+        ];
+
+        // Episode props
+        // ------------------------------------------
+
+        let episode_props = vec![
+            // 0
+            Property {
+                prop_type: common_text_prop(),
+                required: false,
+                name: b"title".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 1
+            Property {
+                prop_type: common_text_prop(),
+                required: false,
+                name: b"itunes:title".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 2
+            Property {
+                prop_type: common_text_prop(),
+                required: false,
+                name: b"pubDate".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 3
+            Property {
+                prop_type: common_text_prop(),
+                required: false,
+                name: b"guid".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 4
+            Property {
+                prop_type: common_text_prop(),
+                required: false,
+                name: b"link".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 5
+            Property {
+                prop_type: common_text_prop(),
+                required: false,
+                name: b"itunes:image".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 6
+            Property {
+                prop_type: long_text_prop(),
+                required: false,
+                name: b"description".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 7
+            Property {
+                prop_type: long_text_prop(),
+                required: false,
+                name: b"content:encoded".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 8
+            Property {
+                prop_type: PropertyType::Text(50),
+                required: false,
+                name: b"enclosure/length".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 9
+            Property {
+                prop_type: PropertyType::Text(50),
+                required: false,
+                name: b"enclosure/type".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 10
+            Property {
+                prop_type: common_text_prop(),
+                required: false,
+                name: b"enclosure/url".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 11
+            Property {
+                prop_type: common_text_prop(),
+                required: false,
+                name: b"itunes:duration".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 12
+            Property {
+                prop_type: common_text_prop(),
+                required: false,
+                name: b"itunes:explicit".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 13
+            // TODO make this as a text vec?
+            Property {
+                prop_type: common_text_prop(),
+                required: false,
+                name: b"itunes:keywords".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 14
+            Property {
+                prop_type: common_text_prop(),
+                required: false,
+                name: b"itunes:subtitle".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 15
+            Property {
+                prop_type: long_text_prop(),
+                required: false,
+                name: b"itunes:summary".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 16
+            Property {
+                prop_type: PropertyType::Uint16,
+                required: false,
+                name: b"itunes:season".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 17
+            Property {
+                prop_type: PropertyType::Uint16,
+                required: false,
+                name: b"itunes:episode".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 18
+            Property {
+                prop_type: common_text_prop(),
+                required: false,
+                name: b"itunes:episodeType".to_vec(),
+                description: b"".to_vec(),
+            },
+            // 19
+            // TODO this could be Internal prop.
+            Property {
+                prop_type: common_text_prop(),
+                required: false,
+                name: b"itunes:author".to_vec(),
+                description: b"".to_vec(),
+            },
+        ];
+
+        // Channel
+
+        let channel_class_id = TestModule::next_class_id();
+        assert_ok!(
+            TestModule::create_class(b"PodcastChannel".to_vec(), b"A podcast channel".to_vec(),),
+            channel_class_id
+        );
+
+        let channel_schema_id: u16 = 0;
+
+        assert_ok!(
+            TestModule::add_class_schema(channel_class_id, vec![], channel_props,),
+            channel_schema_id
+        );
+
+        // Episodes:
+
+        let episode_class_id = TestModule::next_class_id();
+        assert_ok!(
+            TestModule::create_class(b"PodcastEpisode".to_vec(), b"A podcast episode".to_vec(),),
+            episode_class_id
+        );
+
+        let episode_schema_id: u16 = 0;
+
+        assert_ok!(
+            TestModule::add_class_schema(episode_class_id, vec![], episode_props,),
+            episode_schema_id
+        );
+
+        let mut p = PropHelper::new();
+        let channel_entity_id = TestModule::next_entity_id();
+
+        assert_ok!(
+            TestModule::create_entity(channel_class_id,),
+            channel_entity_id
+        );
+
+        assert_ok!(
+            TestModule::add_schema_support_to_entity(
+                channel_entity_id,
+                channel_schema_id,
+                vec![
+                    // 0
+                    p.next_text_value(b"https://staked.libsyn.com/rss".to_vec()),
+                    // 1
+                    p.next_text_value(b"Staked".to_vec()),
+                    // 2
+                    p.next_text_value(b"Wed, 15 May 2019 20:36:20 +0000".to_vec()),
+                    // 3
+                    p.next_text_value(b"Fri, 23 Aug 2019 11:26:24 +0000".to_vec()),
+                    // 4
+                    p.next_text_value(b"Libsyn WebEngine 2.0".to_vec()),
+                    // 5
+                    p.next_text_value(b"https://twitter.com/staked_podcast".to_vec()),
+                    // 6
+                    p.next_text_value(b"en".to_vec()),
+                    // 7
+                    p.next_value(PropertyValue::None),
+                    // 8
+                    p.next_text_value(b"https://twitter.com/staked_podcast".to_vec()),
+                    // 9
+                    p.next_text_value(b"staked@jsgenesis.com (staked@jsgenesis.com)".to_vec()),
+                    // 10
+                    p.next_text_value(b"https://ssl-static.libsyn.com/p/assets/2/d/2/5/2d25eb5fa72739f7/iTunes_Cover.png".to_vec()),
+                    // 11
+                    p.next_text_value(b"Staked".to_vec()),
+                    // 12
+                    p.next_text_value(b"https://twitter.com/staked_podcast".to_vec()),
+                    // 13
+                    p.next_text_value(b"Exploring crypto and blockchain governance.".to_vec()),
+                    // 14
+                    p.next_text_value(b"Staked".to_vec()),
+                    // 15
+                    p.next_text_value(b"crypto,blockchain,governance,staking,bitcoin,ethereum".to_vec()),
+                    // 16
+                    p.next_value(PropertyValue::TextVec(vec![
+                        b"Technology".to_vec(), 
+                        b"Software How-To".to_vec()
+                    ])),
+                    // 17
+                    p.next_text_value(b"https://ssl-static.libsyn.com/p/assets/2/d/2/5/2d25eb5fa72739f7/iTunes_Cover.png".to_vec()),
+                    // 18
+                    p.next_text_value(b"yes".to_vec()),
+                    // 19
+                    p.next_text_value(b"Martin Wessel-Berg".to_vec()),
+                    // 20
+                    p.next_text_value(b"staked@jsgenesis.com".to_vec()),
+                    // 21
+                    p.next_text_value(b"Exploring crypto and blockchain governance.".to_vec()),
+                    // 22
+                    p.next_text_value(b"Exploring crypto and blockchain governance.".to_vec()),
+                    // 23
+                    p.next_text_value(b"episodic".to_vec()),
+                ]
+            )
+        );
+
+        let episode_2_summary = b"<p>In July 2017, the SEC published a report following their <a href=\"https://www.sec.gov/litigation/investreport/34-81207.pdf\">investigation of the DAO</a>. This was significant as it was the first actionable statement from the SEC, giving some insight as to how they interpret this new asset class in light of existing securities laws.</p> <p>Staked is brought to you by Joystream - A user governed media platform.</p>".to_vec();
+
+        p = PropHelper::new();
+        let episode_2_entity_id = TestModule::next_entity_id();
+
+        assert_ok!(
+            TestModule::create_entity(episode_class_id,),
+            episode_2_entity_id
+        );
+
+        assert_ok!(
+            TestModule::add_schema_support_to_entity(
+                episode_2_entity_id,
+                episode_schema_id,
+                vec![
+                    // 0
+                    p.next_text_value(b"Implications of the DAO Report for Crypto Governance".to_vec()),
+                    // 1
+                    p.next_text_value(b"Implications of the DAO Report for Crypto Governance".to_vec()),
+                    // 2
+                    p.next_text_value(b"Wed, 13 Mar 2019 11:20:33 +0000".to_vec()),
+                    // 3
+                    p.next_text_value(b"1bf862ba81ab4ee797526d98e09ad301".to_vec()),
+                    // 4
+                    p.next_text_value(b"http://staked.libsyn.com/implications-of-the-dao-report-for-crypto-governance".to_vec()),
+                    // 5
+                    p.next_text_value(b"https://ssl-static.libsyn.com/p/assets/2/d/2/5/2d25eb5fa72739f7/iTunes_Cover.png".to_vec()),
+                    // 6
+                    p.next_text_value(episode_2_summary.clone()),
+                    // 7
+                    p.next_text_value(episode_2_summary.clone()),
+                    // 8
+                    p.next_text_value(b"87444374".to_vec()),
+                    // 9
+                    p.next_text_value(b"audio/mpeg".to_vec()),
+                    // 10
+                    p.next_text_value(b"https://traffic.libsyn.com/secure/staked/Staked_-_Ep._2_final_cut.mp3?dest-id=1097396".to_vec()),
+                    // 11
+                    p.next_text_value(b"36:27".to_vec()),
+                    // 12
+                    p.next_text_value(b"yes".to_vec()),
+                    // 13
+                    p.next_text_value(b"governance,crypto,sec,securities,dao,bitcoin,blockchain,ethereum".to_vec()),
+                    // 14
+                    p.next_text_value(b"Part I in a series exploring decentralized governance and securities law".to_vec()),
+                    // 15
+                    p.next_text_value(episode_2_summary),
+                    // 16
+                    p.next_value(PropertyValue::Uint16(1)),
+                    // 17
+                    p.next_value(PropertyValue::Uint16(2)),
+                    // 18
+                    p.next_text_value(b"full".to_vec()),
+                    // 19
+                    p.next_text_value(b"Staked".to_vec()),
+                ]
+            )
+        );
+    })
+}
+
+struct PropHelper {
+    prop_idx: u16,
+}
+
+impl PropHelper {
+    fn new() -> PropHelper {
+        PropHelper { prop_idx: 0 }
+    }
+
+    fn next_value(&mut self, value: PropertyValue) -> ClassPropertyValue {
+        let value = ClassPropertyValue {
+            in_class_index: self.prop_idx,
+            value: value,
+        };
+        self.prop_idx += 1;
+        value
+    }
+
+    fn next_text_value(&mut self, text: Vec<u8>) -> ClassPropertyValue {
+        self.next_value(PropertyValue::Text(text))
+    }
+}

+ 809 - 0
runtime-modules/versioned-store/src/lib.rs

@@ -0,0 +1,809 @@
+// Copyright 2019 Jsgenesis.
+//
+// This is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Substrate. If not, see <http://www.gnu.org/licenses/>.
+
+// Ensure we're `no_std` when compiling for Wasm.
+#![cfg_attr(not(feature = "std"), no_std)]
+
+#[cfg(feature = "std")]
+use serde_derive::{Deserialize, Serialize};
+
+use codec::{Decode, Encode};
+use rstd::collections::btree_set::BTreeSet;
+use rstd::prelude::*;
+use srml_support::{decl_event, decl_module, decl_storage, dispatch, ensure};
+use system;
+
+mod example;
+mod mock;
+mod tests;
+
+// Validation errors
+// --------------------------------------
+
+const ERROR_PROPERTY_NAME_TOO_SHORT: &str = "Property name is too short";
+const ERROR_PROPERTY_NAME_TOO_LONG: &str = "Property name is too long";
+const ERROR_PROPERTY_DESCRIPTION_TOO_SHORT: &str = "Property description is too long";
+const ERROR_PROPERTY_DESCRIPTION_TOO_LONG: &str = "Property description is too long";
+
+const ERROR_CLASS_NAME_TOO_SHORT: &str = "Class name is too short";
+const ERROR_CLASS_NAME_TOO_LONG: &str = "Class name is too long";
+const ERROR_CLASS_DESCRIPTION_TOO_SHORT: &str = "Class description is too long";
+const ERROR_CLASS_DESCRIPTION_TOO_LONG: &str = "Class description is too long";
+
+// Main logic errors
+// --------------------------------------
+
+const ERROR_CLASS_NOT_FOUND: &str = "Class was not found by id";
+const ERROR_UNKNOWN_CLASS_SCHEMA_ID: &str = "Unknown class schema id";
+const ERROR_CLASS_SCHEMA_REFERS_UNKNOWN_PROP_INDEX: &str =
+    "New class schema refers to an unknown property index";
+const ERROR_CLASS_SCHEMA_REFERS_UNKNOWN_INTERNAL_ID: &str =
+    "New class schema refers to an unknown internal class id";
+const ERROR_NO_PROPS_IN_CLASS_SCHEMA: &str =
+    "Cannot add a class schema with an empty list of properties";
+const ERROR_ENTITY_NOT_FOUND: &str = "Entity was not found by id";
+// const ERROR_ENTITY_ALREADY_DELETED: &str = "Entity is already deleted";
+const ERROR_SCHEMA_ALREADY_ADDED_TO_ENTITY: &str =
+    "Cannot add a schema that is already added to this entity";
+const ERROR_PROP_VALUE_DONT_MATCH_TYPE: &str =
+    "Some of the provided property values don't match the expected property type";
+const ERROR_PROP_NAME_NOT_UNIQUE_IN_CLASS: &str = "Property name is not unique within its class";
+const ERROR_MISSING_REQUIRED_PROP: &str =
+    "Some required property was not found when adding schema support to entity";
+const ERROR_UNKNOWN_ENTITY_PROP_ID: &str = "Some of the provided property ids cannot be found on the current list of propery values of this entity";
+const ERROR_TEXT_PROP_IS_TOO_LONG: &str = "Text propery is too long";
+const ERROR_VEC_PROP_IS_TOO_LONG: &str = "Vector propery is too long";
+const ERROR_INTERNAL_RPOP_DOES_NOT_MATCH_ITS_CLASS: &str =
+    "Internal property does not match its class";
+
+/// Length constraint for input validation
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
+#[derive(Encode, Decode, Default, Clone, PartialEq, Eq, Debug)]
+pub struct InputValidationLengthConstraint {
+    /// Minimum length
+    pub min: u16,
+
+    /// Difference between minimum length and max length.
+    /// While having max would have been more direct, this
+    /// way makes max < min unrepresentable semantically,
+    /// which is safer.
+    pub max_min_diff: u16,
+}
+
+impl InputValidationLengthConstraint {
+    /// Helper for computing max
+    pub fn max(&self) -> u16 {
+        self.min + self.max_min_diff
+    }
+
+    pub fn ensure_valid(
+        &self,
+        len: usize,
+        too_short_msg: &'static str,
+        too_long_msg: &'static str,
+    ) -> Result<(), &'static str> {
+        let length = len as u16;
+        if length < self.min {
+            Err(too_short_msg)
+        } else if length > self.max() {
+            Err(too_long_msg)
+        } else {
+            Ok(())
+        }
+    }
+}
+
+pub type ClassId = u64;
+pub type EntityId = u64;
+
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
+#[derive(Encode, Decode, Default, Clone, PartialEq, Eq, Debug)]
+pub struct Class {
+    pub id: ClassId,
+
+    /// All properties that have been used on this class across different class schemas.
+    /// Unlikely to be more than roughly 20 properties per class, often less.
+    /// For Person, think "height", "weight", etc.
+    pub properties: Vec<Property>,
+
+    /// All scehmas that are available for this class, think v0.0 Person, v.1.0 Person, etc.
+    pub schemas: Vec<ClassSchema>,
+
+    pub name: Vec<u8>,
+    pub description: Vec<u8>,
+}
+
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
+#[derive(Encode, Decode, Default, Clone, PartialEq, Eq, Debug)]
+pub struct Entity {
+    pub id: EntityId,
+
+    /// The class id of this entity.
+    pub class_id: ClassId,
+
+    /// What schemas under which this entity of a class is available, think
+    /// v.2.0 Person schema for John, v3.0 Person schema for John
+    /// Unlikely to be more than roughly 20ish, assuming schemas for a given class eventually stableize, or that very old schema are eventually removed.
+    pub in_class_schema_indexes: Vec<u16>, // indices of schema in corresponding class
+
+    /// Values for properties on class that are used by some schema used by this entity!
+    /// Length is no more than Class.properties.
+    pub values: Vec<ClassPropertyValue>,
+    // pub deleted: bool,
+}
+
+/// A schema defines what properties describe an entity
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
+#[derive(Encode, Decode, Default, Clone, PartialEq, Eq, Debug)]
+pub struct ClassSchema {
+    /// Indices into properties vector for the corresponding class.
+    pub properties: Vec<u16>,
+}
+
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
+#[derive(Encode, Decode, Default, Clone, PartialEq, Eq, Debug)]
+pub struct Property {
+    pub prop_type: PropertyType,
+    pub required: bool,
+    pub name: Vec<u8>,
+    pub description: Vec<u8>,
+}
+
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
+#[derive(Encode, Decode, Clone, PartialEq, Eq, Debug)]
+pub enum PropertyType {
+    None,
+
+    // Single value:
+    Bool,
+    Uint16,
+    Uint32,
+    Uint64,
+    Int16,
+    Int32,
+    Int64,
+    Text(u16),
+    Internal(ClassId),
+
+    // Vector of values.
+    // The first u16 value is the max length of this vector.
+    BoolVec(u16),
+    Uint16Vec(u16),
+    Uint32Vec(u16),
+    Uint64Vec(u16),
+    Int16Vec(u16),
+    Int32Vec(u16),
+    Int64Vec(u16),
+
+    /// The first u16 value is the max length of this vector.
+    /// The second u16 value is the max length of every text item in this vector.
+    TextVec(u16, u16),
+
+    /// The first u16 value is the max length of this vector.
+    /// The second ClassId value tells that an every element of this vector
+    /// should be of a specific ClassId.
+    InternalVec(u16, ClassId),
+    // External(ExternalProperty),
+    // ExternalVec(u16, ExternalProperty),
+}
+
+impl Default for PropertyType {
+    fn default() -> Self {
+        PropertyType::None
+    }
+}
+
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
+#[derive(Encode, Decode, Clone, PartialEq, Eq, Debug)]
+pub enum PropertyValue {
+    None,
+
+    // Single value:
+    Bool(bool),
+    Uint16(u16),
+    Uint32(u32),
+    Uint64(u64),
+    Int16(i16),
+    Int32(i32),
+    Int64(i64),
+    Text(Vec<u8>),
+    Internal(EntityId),
+
+    // Vector of values:
+    BoolVec(Vec<bool>),
+    Uint16Vec(Vec<u16>),
+    Uint32Vec(Vec<u32>),
+    Uint64Vec(Vec<u64>),
+    Int16Vec(Vec<i16>),
+    Int32Vec(Vec<i32>),
+    Int64Vec(Vec<i64>),
+    TextVec(Vec<Vec<u8>>),
+    InternalVec(Vec<EntityId>),
+    // External(ExternalPropertyType),
+    // ExternalVec(Vec<ExternalPropertyType>),
+}
+
+impl Default for PropertyValue {
+    fn default() -> Self {
+        PropertyValue::None
+    }
+}
+
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
+#[derive(Encode, Decode, Default, Clone, PartialEq, Eq, Debug)]
+pub struct ClassPropertyValue {
+    /// Index is into properties vector of class.
+    pub in_class_index: u16,
+
+    /// Value of property with index `in_class_index` in a given class.
+    pub value: PropertyValue,
+}
+
+pub trait Trait: system::Trait + Sized {
+    type Event: From<Event<Self>> + Into<<Self as system::Trait>::Event>;
+}
+
+decl_storage! {
+
+    trait Store for Module<T: Trait> as VersionedStore {
+
+        pub ClassById get(class_by_id) config(): map ClassId => Class;
+
+        pub EntityById get(entity_by_id) config(): map EntityId => Entity;
+
+        pub NextClassId get(next_class_id) config(): ClassId;
+
+        pub NextEntityId get(next_entity_id) config(): EntityId;
+
+        pub PropertyNameConstraint get(property_name_constraint)
+            config(): InputValidationLengthConstraint;
+
+        pub PropertyDescriptionConstraint get(property_description_constraint)
+            config(): InputValidationLengthConstraint;
+
+        pub ClassNameConstraint get(class_name_constraint)
+            config(): InputValidationLengthConstraint;
+
+        pub ClassDescriptionConstraint get(class_description_constraint)
+            config(): InputValidationLengthConstraint;
+    }
+}
+
+decl_event!(
+    pub enum Event<T>
+    where
+        <T as system::Trait>::AccountId,
+    {
+        ClassCreated(ClassId),
+        ClassSchemaAdded(ClassId, u16),
+
+        EntityCreated(EntityId),
+        // EntityDeleted(EntityId),
+        EntityPropertiesUpdated(EntityId),
+        EntitySchemaAdded(EntityId, u16),
+
+        /// This is a fake event that uses AccountId type just to make Rust compiler happy to compile this module.
+        FixCompilation(AccountId),
+    }
+);
+
+decl_module! {
+    pub struct Module<T: Trait> for enum Call where origin: T::Origin {
+        fn deposit_event() = default;
+    }
+}
+
+// Shortcuts for faster readability of match expression:
+use PropertyType as PT;
+use PropertyValue as PV;
+
+impl<T: Trait> Module<T> {
+    /// Returns an id of a newly added class.
+    pub fn create_class(name: Vec<u8>, description: Vec<u8>) -> Result<ClassId, &'static str> {
+        Self::ensure_class_name_is_valid(&name)?;
+
+        Self::ensure_class_description_is_valid(&description)?;
+
+        let class_id = NextClassId::get();
+
+        let new_class = Class {
+            id: class_id,
+            properties: vec![],
+            schemas: vec![],
+            name,
+            description,
+        };
+
+        // Save newly created class:
+        ClassById::insert(class_id, new_class);
+
+        // Increment the next class id:
+        NextClassId::mutate(|n| *n += 1);
+
+        Self::deposit_event(RawEvent::ClassCreated(class_id));
+        Ok(class_id)
+    }
+
+    /// Returns an index of a newly added class schema on success.
+    pub fn add_class_schema(
+        class_id: ClassId,
+        existing_properties: Vec<u16>,
+        new_properties: Vec<Property>,
+    ) -> Result<u16, &'static str> {
+        Self::ensure_known_class_id(class_id)?;
+
+        let non_empty_schema = !existing_properties.is_empty() || !new_properties.is_empty();
+
+        ensure!(non_empty_schema, ERROR_NO_PROPS_IN_CLASS_SCHEMA);
+
+        let class = ClassById::get(class_id);
+
+        // TODO Use BTreeSet for prop unique names when switched to Substrate 2.
+        // There is no support for BTreeSet in Substrate 1 runtime.
+        // use rstd::collections::btree_set::BTreeSet;
+        let mut unique_prop_names = BTreeSet::new();
+        for prop in class.properties.iter() {
+            unique_prop_names.insert(prop.name.clone());
+        }
+
+        for prop in new_properties.iter() {
+            Self::ensure_property_name_is_valid(&prop.name)?;
+            Self::ensure_property_description_is_valid(&prop.description)?;
+
+            // Check that the name of a new property is unique within its class.
+            ensure!(
+                !unique_prop_names.contains(&prop.name),
+                ERROR_PROP_NAME_NOT_UNIQUE_IN_CLASS
+            );
+            unique_prop_names.insert(prop.name.clone());
+        }
+
+        // Check that existing props are valid indices of class properties vector:
+        let has_unknown_props = existing_properties
+            .iter()
+            .any(|&prop_id| prop_id >= class.properties.len() as u16);
+        ensure!(
+            !has_unknown_props,
+            ERROR_CLASS_SCHEMA_REFERS_UNKNOWN_PROP_INDEX
+        );
+
+        // Check validity of Internal(ClassId) for new_properties.
+        let has_unknown_internal_id = new_properties.iter().any(|prop| match prop.prop_type {
+            PropertyType::Internal(other_class_id) => !ClassById::exists(other_class_id),
+            _ => false,
+        });
+        ensure!(
+            !has_unknown_internal_id,
+            ERROR_CLASS_SCHEMA_REFERS_UNKNOWN_INTERNAL_ID
+        );
+
+        // Use the current length of schemas in this class as an index
+        // for the next schema that will be sent in a result of this function.
+        let schema_idx = class.schemas.len() as u16;
+
+        let mut schema = ClassSchema {
+            properties: existing_properties,
+        };
+
+        let mut updated_class_props = class.properties;
+        new_properties.into_iter().for_each(|prop| {
+            let prop_id = updated_class_props.len() as u16;
+            updated_class_props.push(prop);
+            schema.properties.push(prop_id);
+        });
+
+        ClassById::mutate(class_id, |class| {
+            class.properties = updated_class_props;
+            class.schemas.push(schema);
+        });
+
+        Self::deposit_event(RawEvent::ClassSchemaAdded(class_id, schema_idx));
+        Ok(schema_idx)
+    }
+
+    pub fn create_entity(class_id: ClassId) -> Result<EntityId, &'static str> {
+        Self::ensure_known_class_id(class_id)?;
+
+        let entity_id = NextEntityId::get();
+
+        let new_entity = Entity {
+            id: entity_id,
+            class_id,
+            in_class_schema_indexes: vec![],
+            values: vec![],
+            // deleted: false,
+        };
+
+        // Save newly created entity:
+        EntityById::insert(entity_id, new_entity);
+
+        // Increment the next entity id:
+        NextEntityId::mutate(|n| *n += 1);
+
+        Self::deposit_event(RawEvent::EntityCreated(entity_id));
+        Ok(entity_id)
+    }
+
+    pub fn add_schema_support_to_entity(
+        entity_id: EntityId,
+        schema_id: u16,
+        property_values: Vec<ClassPropertyValue>,
+    ) -> dispatch::Result {
+        Self::ensure_known_entity_id(entity_id)?;
+
+        let (entity, class) = Self::get_entity_and_class(entity_id);
+
+        // Check that schema_id is a valid index of class schemas vector:
+        let known_schema_id = schema_id < class.schemas.len() as u16;
+        ensure!(known_schema_id, ERROR_UNKNOWN_CLASS_SCHEMA_ID);
+
+        // Check that schema id is not yet added to this entity:
+        let schema_not_added = entity
+            .in_class_schema_indexes
+            .iter()
+            .position(|x| *x == schema_id)
+            .is_none();
+        ensure!(schema_not_added, ERROR_SCHEMA_ALREADY_ADDED_TO_ENTITY);
+
+        let class_schema_opt = class.schemas.get(schema_id as usize);
+        let schema_prop_ids = class_schema_opt.unwrap().properties.clone();
+
+        let current_entity_values = entity.values.clone();
+        let mut appended_entity_values = entity.values;
+
+        for &prop_id in schema_prop_ids.iter() {
+            let prop_already_added = current_entity_values
+                .iter()
+                .any(|prop| prop.in_class_index == prop_id);
+
+            if prop_already_added {
+                // A property is already added to the entity and cannot be updated
+                // while adding a schema support to this entity.
+                continue;
+            }
+
+            let class_prop = class.properties.get(prop_id as usize).unwrap();
+
+            // If a value was not povided for the property of this schema:
+            match property_values
+                .iter()
+                .find(|prop| prop.in_class_index == prop_id)
+            {
+                Some(new_prop) => {
+                    let ClassPropertyValue {
+                        in_class_index: new_id,
+                        value: new_value,
+                    } = new_prop;
+
+                    Self::ensure_property_value_is_valid(new_value.clone(), class_prop.clone())?;
+
+                    appended_entity_values.push(ClassPropertyValue {
+                        in_class_index: *new_id,
+                        value: new_value.clone(),
+                    });
+                }
+                None => {
+                    // All required prop values should be are provided
+                    if class_prop.required {
+                        return Err(ERROR_MISSING_REQUIRED_PROP);
+                    }
+                    // Add all missing non required schema prop values as PropertyValue::None
+                    else {
+                        appended_entity_values.push(ClassPropertyValue {
+                            in_class_index: prop_id,
+                            value: PropertyValue::None,
+                        });
+                    }
+                }
+            }
+        }
+
+        EntityById::mutate(entity_id, |entity| {
+            // Add a new schema to the list of schemas supported by this entity.
+            entity.in_class_schema_indexes.push(schema_id);
+
+            // Update entity values only if new properties have been added.
+            if appended_entity_values.len() > entity.values.len() {
+                entity.values = appended_entity_values;
+            }
+        });
+
+        Self::deposit_event(RawEvent::EntitySchemaAdded(entity_id, schema_id));
+        Ok(())
+    }
+
+    pub fn update_entity_property_values(
+        entity_id: EntityId,
+        new_property_values: Vec<ClassPropertyValue>,
+    ) -> dispatch::Result {
+        Self::ensure_known_entity_id(entity_id)?;
+
+        let (entity, class) = Self::get_entity_and_class(entity_id);
+
+        // Get current property values of an entity as a mutable vector,
+        // so we can update them if new values provided present in new_property_values.
+        let mut updated_values = entity.values;
+        let mut updates_count = 0;
+
+        // Iterate over a vector of new values and update corresponding properties
+        // of this entity if new values are valid.
+        for new_prop_value in new_property_values.iter() {
+            let ClassPropertyValue {
+                in_class_index: id,
+                value: new_value,
+            } = new_prop_value;
+
+            // Try to find a current property value in the entity
+            // by matching its id to the id of a property with an updated value.
+            if let Some(current_prop_value) = updated_values
+                .iter_mut()
+                .find(|prop| *id == prop.in_class_index)
+            {
+                let ClassPropertyValue {
+                    in_class_index: valid_id,
+                    value: current_value,
+                } = current_prop_value;
+
+                // Get class-level information about this property
+                let class_prop = class.properties.get(*valid_id as usize).unwrap();
+
+                // Validate a new property value against the type of this property
+                // and check any additional constraints like the length of a vector
+                // if it's a vector property or the length of a text if it's a text property.
+                Self::ensure_property_value_is_valid(new_value.clone(), class_prop.clone())?;
+
+                // Update a current prop value in a mutable vector, if a new value is valid.
+                *current_value = new_value.clone();
+                updates_count += 1;
+            } else {
+                // Throw an error if a property was not found on entity
+                // by an in-class index of a property update.
+                return Err(ERROR_UNKNOWN_ENTITY_PROP_ID);
+            }
+        }
+
+        // If at least one of the entity property values should be update:
+        if updates_count > 0 {
+            EntityById::mutate(entity_id, |entity| {
+                entity.values = updated_values;
+            });
+            Self::deposit_event(RawEvent::EntityPropertiesUpdated(entity_id));
+        }
+
+        Ok(())
+    }
+
+    // Commented out for now <- requested by Bedeho.
+    // pub fn delete_entity(entity_id: EntityId) -> dispatch::Result {
+    //     Self::ensure_known_entity_id(entity_id)?;
+
+    //     let is_deleted = EntityById::get(entity_id).deleted;
+    //     ensure!(!is_deleted, ERROR_ENTITY_ALREADY_DELETED);
+
+    //     EntityById::mutate(entity_id, |x| {
+    //         x.deleted = true;
+    //     });
+
+    //     Self::deposit_event(RawEvent::EntityDeleted(entity_id));
+    //     Ok(())
+    // }
+
+    // Helper functions:
+    // ----------------------------------------------------------------
+
+    pub fn ensure_known_class_id(class_id: ClassId) -> dispatch::Result {
+        ensure!(ClassById::exists(class_id), ERROR_CLASS_NOT_FOUND);
+        Ok(())
+    }
+
+    pub fn ensure_known_entity_id(entity_id: EntityId) -> dispatch::Result {
+        ensure!(EntityById::exists(entity_id), ERROR_ENTITY_NOT_FOUND);
+        Ok(())
+    }
+
+    pub fn ensure_valid_internal_prop(value: PropertyValue, prop: Property) -> dispatch::Result {
+        match (value, prop.prop_type) {
+            (PV::Internal(entity_id), PT::Internal(class_id)) => {
+                Self::ensure_known_class_id(class_id)?;
+                Self::ensure_known_entity_id(entity_id)?;
+                let entity = Self::entity_by_id(entity_id);
+                ensure!(
+                    entity.class_id == class_id,
+                    ERROR_INTERNAL_RPOP_DOES_NOT_MATCH_ITS_CLASS
+                );
+                Ok(())
+            }
+            _ => Ok(()),
+        }
+    }
+
+    pub fn is_unknown_internal_entity_id(id: PropertyValue) -> bool {
+        if let PropertyValue::Internal(entity_id) = id {
+            !EntityById::exists(entity_id)
+        } else {
+            false
+        }
+    }
+
+    pub fn get_entity_and_class(entity_id: EntityId) -> (Entity, Class) {
+        let entity = EntityById::get(entity_id);
+        let class = ClassById::get(entity.class_id);
+        (entity, class)
+    }
+
+    pub fn ensure_property_value_is_valid(
+        value: PropertyValue,
+        prop: Property,
+    ) -> dispatch::Result {
+        Self::ensure_prop_value_matches_its_type(value.clone(), prop.clone())?;
+        Self::ensure_valid_internal_prop(value.clone(), prop.clone())?;
+        Self::validate_max_len_if_text_prop(value.clone(), prop.clone())?;
+        Self::validate_max_len_if_vec_prop(value.clone(), prop.clone())?;
+        Ok(())
+    }
+
+    pub fn validate_max_len_if_text_prop(value: PropertyValue, prop: Property) -> dispatch::Result {
+        match (value, prop.prop_type) {
+            (PV::Text(text), PT::Text(max_len)) => Self::validate_max_len_of_text(text, max_len),
+            _ => Ok(()),
+        }
+    }
+
+    pub fn validate_max_len_of_text(text: Vec<u8>, max_len: u16) -> dispatch::Result {
+        if text.len() <= max_len as usize {
+            Ok(())
+        } else {
+            Err(ERROR_TEXT_PROP_IS_TOO_LONG)
+        }
+    }
+
+    #[rustfmt::skip]
+    pub fn validate_max_len_if_vec_prop(
+        value: PropertyValue,
+        prop: Property,
+    ) -> dispatch::Result {
+
+        fn validate_vec_len<T>(vec: Vec<T>, max_len: u16) -> bool {
+            vec.len() <= max_len as usize
+        }
+
+        fn validate_vec_len_ref<T>(vec: &Vec<T>, max_len: u16) -> bool {
+            vec.len() <= max_len as usize
+        }
+
+        let is_valid_len = match (value, prop.prop_type) {
+            (PV::BoolVec(vec),     PT::BoolVec(max_len))   => validate_vec_len(vec, max_len),
+            (PV::Uint16Vec(vec),   PT::Uint16Vec(max_len)) => validate_vec_len(vec, max_len),
+            (PV::Uint32Vec(vec),   PT::Uint32Vec(max_len)) => validate_vec_len(vec, max_len),
+            (PV::Uint64Vec(vec),   PT::Uint64Vec(max_len)) => validate_vec_len(vec, max_len),
+            (PV::Int16Vec(vec),    PT::Int16Vec(max_len))  => validate_vec_len(vec, max_len),
+            (PV::Int32Vec(vec),    PT::Int32Vec(max_len))  => validate_vec_len(vec, max_len),
+            (PV::Int64Vec(vec),    PT::Int64Vec(max_len))  => validate_vec_len(vec, max_len),
+
+            (PV::TextVec(vec),     PT::TextVec(vec_max_len, text_max_len)) => {
+                if validate_vec_len_ref(&vec, vec_max_len) {
+                    for text_item in vec.iter() {
+                        Self::validate_max_len_of_text(text_item.clone(), text_max_len)?;
+                    }
+                    true
+                } else {
+                    false
+                }
+            },
+
+            (PV::InternalVec(vec), PT::InternalVec(vec_max_len, class_id)) => {
+                Self::ensure_known_class_id(class_id)?;
+                if validate_vec_len_ref(&vec, vec_max_len) {
+                    for entity_id in vec.iter() {
+                        Self::ensure_known_entity_id(entity_id.clone())?;
+                        let entity = Self::entity_by_id(entity_id);
+                        ensure!(entity.class_id == class_id, ERROR_INTERNAL_RPOP_DOES_NOT_MATCH_ITS_CLASS);
+                    }
+                    true
+                } else {
+                    false
+                }
+            },
+
+            _ => true
+        };
+
+        if is_valid_len {
+            Ok(())
+        } else {
+            Err(ERROR_VEC_PROP_IS_TOO_LONG)
+        }
+    }
+
+    pub fn ensure_prop_value_matches_its_type(
+        value: PropertyValue,
+        prop: Property,
+    ) -> dispatch::Result {
+        if Self::does_prop_value_match_type(value, prop) {
+            Ok(())
+        } else {
+            Err(ERROR_PROP_VALUE_DONT_MATCH_TYPE)
+        }
+    }
+
+    #[rustfmt::skip]
+    pub fn does_prop_value_match_type(
+        value: PropertyValue,
+        prop: Property,
+    ) -> bool {
+
+        // A non required property can be updated to None:
+        if !prop.required && value == PV::None {
+            return true
+        }
+
+        match (value, prop.prop_type) {
+            (PV::None,        PT::None) |
+
+            // Single values
+            (PV::Bool(_),     PT::Bool) |
+            (PV::Uint16(_),   PT::Uint16) |
+            (PV::Uint32(_),   PT::Uint32) |
+            (PV::Uint64(_),   PT::Uint64) |
+            (PV::Int16(_),    PT::Int16) |
+            (PV::Int32(_),    PT::Int32) |
+            (PV::Int64(_),    PT::Int64) |
+            (PV::Text(_),     PT::Text(_)) |
+            (PV::Internal(_), PT::Internal(_)) |
+
+            // Vectors:
+            (PV::BoolVec(_),     PT::BoolVec(_)) |
+            (PV::Uint16Vec(_),   PT::Uint16Vec(_)) |
+            (PV::Uint32Vec(_),   PT::Uint32Vec(_)) |
+            (PV::Uint64Vec(_),   PT::Uint64Vec(_)) |
+            (PV::Int16Vec(_),    PT::Int16Vec(_)) |
+            (PV::Int32Vec(_),    PT::Int32Vec(_)) |
+            (PV::Int64Vec(_),    PT::Int64Vec(_)) |
+            (PV::TextVec(_),     PT::TextVec(_, _)) |
+            (PV::InternalVec(_), PT::InternalVec(_, _)) => true,
+
+            // (PV::External(_), PT::External(_)) => true,
+            // (PV::ExternalVec(_), PT::ExternalVec(_, _)) => true,
+            _ => false,
+        }
+    }
+
+    pub fn ensure_property_name_is_valid(text: &Vec<u8>) -> dispatch::Result {
+        PropertyNameConstraint::get().ensure_valid(
+            text.len(),
+            ERROR_PROPERTY_NAME_TOO_SHORT,
+            ERROR_PROPERTY_NAME_TOO_LONG,
+        )
+    }
+
+    pub fn ensure_property_description_is_valid(text: &Vec<u8>) -> dispatch::Result {
+        PropertyDescriptionConstraint::get().ensure_valid(
+            text.len(),
+            ERROR_PROPERTY_DESCRIPTION_TOO_SHORT,
+            ERROR_PROPERTY_DESCRIPTION_TOO_LONG,
+        )
+    }
+
+    pub fn ensure_class_name_is_valid(text: &Vec<u8>) -> dispatch::Result {
+        ClassNameConstraint::get().ensure_valid(
+            text.len(),
+            ERROR_CLASS_NAME_TOO_SHORT,
+            ERROR_CLASS_NAME_TOO_LONG,
+        )
+    }
+
+    pub fn ensure_class_description_is_valid(text: &Vec<u8>) -> dispatch::Result {
+        ClassDescriptionConstraint::get().ensure_valid(
+            text.len(),
+            ERROR_CLASS_DESCRIPTION_TOO_SHORT,
+            ERROR_CLASS_DESCRIPTION_TOO_LONG,
+        )
+    }
+}

+ 259 - 0
runtime-modules/versioned-store/src/mock.rs

@@ -0,0 +1,259 @@
+#![cfg(test)]
+
+use crate::*;
+use crate::{GenesisConfig, Module, Trait};
+
+use primitives::H256;
+use runtime_primitives::{
+    testing::Header,
+    traits::{BlakeTwo256, IdentityLookup},
+    Perbill,
+};
+use srml_support::{assert_err, assert_ok, impl_outer_origin, parameter_types};
+
+impl_outer_origin! {
+    pub enum Origin for Runtime {}
+}
+
+// Workaround for https://github.com/rust-lang/rust/issues/26925 . Remove when sorted.
+#[derive(Clone, PartialEq, Eq, Debug)]
+pub struct Runtime;
+parameter_types! {
+    pub const BlockHashCount: u64 = 250;
+    pub const MaximumBlockWeight: u32 = 1024;
+    pub const MaximumBlockLength: u32 = 2 * 1024;
+    pub const AvailableBlockRatio: Perbill = Perbill::one();
+    pub const MinimumPeriod: u64 = 5;
+}
+
+impl system::Trait for Runtime {
+    type Origin = Origin;
+    type Index = u64;
+    type BlockNumber = u64;
+    type Call = ();
+    type Hash = H256;
+    type Hashing = BlakeTwo256;
+    type AccountId = u64;
+    type Lookup = IdentityLookup<Self::AccountId>;
+    type Header = Header;
+    type Event = ();
+    type BlockHashCount = BlockHashCount;
+    type MaximumBlockWeight = MaximumBlockWeight;
+    type MaximumBlockLength = MaximumBlockLength;
+    type AvailableBlockRatio = AvailableBlockRatio;
+    type Version = ();
+}
+
+impl timestamp::Trait for Runtime {
+    type Moment = u64;
+    type OnTimestampSet = ();
+    type MinimumPeriod = MinimumPeriod;
+}
+
+impl Trait for Runtime {
+    type Event = ();
+}
+
+pub const UNKNOWN_CLASS_ID: ClassId = 111;
+
+pub const UNKNOWN_ENTITY_ID: EntityId = 222;
+
+pub const UNKNOWN_PROP_ID: u16 = 333;
+
+// pub const UNKNOWN_SCHEMA_ID: u16 = 444;
+
+pub const SCHEMA_ID_0: u16 = 0;
+pub const SCHEMA_ID_1: u16 = 1;
+
+// pub fn generate_text(len: usize) -> Vec<u8> {
+//     vec![b'x'; len]
+// }
+
+pub fn good_class_name() -> Vec<u8> {
+    b"Name of a class".to_vec()
+}
+
+pub fn good_class_description() -> Vec<u8> {
+    b"Description of a class".to_vec()
+}
+
+impl Property {
+    fn required(&self) -> Property {
+        let mut new_self = self.clone();
+        new_self.required = true;
+        new_self
+    }
+}
+
+pub fn good_prop_bool() -> Property {
+    Property {
+        prop_type: PropertyType::Bool,
+        required: false,
+        name: b"Name of a bool property".to_vec(),
+        description: b"Description of a bool property".to_vec(),
+    }
+}
+
+pub fn good_prop_u32() -> Property {
+    Property {
+        prop_type: PropertyType::Uint32,
+        required: false,
+        name: b"Name of a u32 property".to_vec(),
+        description: b"Description of a u32 property".to_vec(),
+    }
+}
+
+pub fn good_prop_text() -> Property {
+    Property {
+        prop_type: PropertyType::Text(20),
+        required: false,
+        name: b"Name of a text property".to_vec(),
+        description: b"Description of a text property".to_vec(),
+    }
+}
+
+pub fn new_internal_class_prop(class_id: ClassId) -> Property {
+    Property {
+        prop_type: PropertyType::Internal(class_id),
+        required: false,
+        name: b"Name of a internal property".to_vec(),
+        description: b"Description of a internal property".to_vec(),
+    }
+}
+
+pub fn good_props() -> Vec<Property> {
+    vec![good_prop_bool(), good_prop_u32()]
+}
+
+pub fn good_prop_ids() -> Vec<u16> {
+    vec![0, 1]
+}
+
+pub fn create_class() -> ClassId {
+    let class_id = TestModule::next_class_id();
+    assert_ok!(
+        TestModule::create_class(good_class_name(), good_class_description(),),
+        class_id
+    );
+    class_id
+}
+
+pub fn bool_prop_value() -> ClassPropertyValue {
+    ClassPropertyValue {
+        in_class_index: 0,
+        value: PropertyValue::Bool(true),
+    }
+}
+
+pub fn prop_value(index: u16, value: PropertyValue) -> ClassPropertyValue {
+    ClassPropertyValue {
+        in_class_index: index,
+        value: value,
+    }
+}
+
+pub fn create_class_with_schema_and_entity() -> (ClassId, u16, EntityId) {
+    let class_id = create_class();
+    if let Ok(schema_id) = TestModule::add_class_schema(
+        class_id,
+        vec![],
+        vec![
+            good_prop_bool().required(),
+            good_prop_u32(),
+            new_internal_class_prop(class_id),
+        ],
+    ) {
+        let entity_id = create_entity_of_class(class_id);
+        (class_id, schema_id, entity_id)
+    } else {
+        panic!("This should not happen")
+    }
+}
+
+pub const PROP_ID_BOOL: u16 = 0;
+pub const PROP_ID_U32: u16 = 1;
+pub const PROP_ID_INTERNAL: u16 = 2;
+
+pub fn create_entity_with_schema_support() -> EntityId {
+    let (_, schema_id, entity_id) = create_class_with_schema_and_entity();
+    assert_ok!(TestModule::add_schema_support_to_entity(
+        entity_id,
+        schema_id,
+        vec![prop_value(PROP_ID_BOOL, PropertyValue::Bool(true))]
+    ));
+    entity_id
+}
+
+pub fn create_entity_of_class(class_id: ClassId) -> EntityId {
+    let entity_id = TestModule::next_entity_id();
+    assert_ok!(TestModule::create_entity(class_id,), entity_id);
+    entity_id
+}
+
+pub fn assert_class_props(class_id: ClassId, expected_props: Vec<Property>) {
+    let class = TestModule::class_by_id(class_id);
+    assert_eq!(class.properties, expected_props);
+}
+
+pub fn assert_class_schemas(class_id: ClassId, expected_schema_prop_ids: Vec<Vec<u16>>) {
+    let class = TestModule::class_by_id(class_id);
+    let schemas: Vec<_> = expected_schema_prop_ids
+        .iter()
+        .map(|prop_ids| ClassSchema {
+            properties: prop_ids.clone(),
+        })
+        .collect();
+    assert_eq!(class.schemas, schemas);
+}
+
+pub fn assert_entity_not_found(result: dispatch::Result) {
+    assert_err!(result, ERROR_ENTITY_NOT_FOUND);
+}
+
+// This function basically just builds a genesis storage key/value store according to
+// our desired mockup.
+
+pub fn default_genesis_config() -> GenesisConfig {
+    GenesisConfig {
+        class_by_id: vec![],
+        entity_by_id: vec![],
+        next_class_id: 1,
+        next_entity_id: 1,
+        property_name_constraint: InputValidationLengthConstraint {
+            min: 1,
+            max_min_diff: 49,
+        },
+        property_description_constraint: InputValidationLengthConstraint {
+            min: 0,
+            max_min_diff: 500,
+        },
+        class_name_constraint: InputValidationLengthConstraint {
+            min: 1,
+            max_min_diff: 49,
+        },
+        class_description_constraint: InputValidationLengthConstraint {
+            min: 0,
+            max_min_diff: 500,
+        },
+    }
+}
+
+fn build_test_externalities(config: GenesisConfig) -> runtime_io::TestExternalities {
+    let mut t = system::GenesisConfig::default()
+        .build_storage::<Runtime>()
+        .unwrap();
+
+    config.assimilate_storage(&mut t).unwrap();
+
+    t.into()
+}
+
+pub fn with_test_externalities<R, F: FnOnce() -> R>(f: F) -> R {
+    let config = default_genesis_config();
+    build_test_externalities(config).execute_with(f)
+}
+
+// pub type System = system::Module;
+
+/// Export module on a test runtime
+pub type TestModule = Module<Runtime>;

+ 503 - 0
runtime-modules/versioned-store/src/tests.rs

@@ -0,0 +1,503 @@
+#![cfg(test)]
+
+use super::*;
+use crate::mock::*;
+
+use srml_support::{assert_err, assert_ok};
+
+// Create class
+// --------------------------------------
+
+#[test]
+fn create_class_successfully() {
+    with_test_externalities(|| {
+        let class_id = TestModule::next_class_id();
+        assert_ok!(
+            TestModule::create_class(good_class_name(), good_class_description(),),
+            class_id
+        );
+        assert_eq!(TestModule::next_class_id(), class_id + 1);
+    })
+}
+
+#[test]
+fn cannot_create_class_with_empty_name() {
+    with_test_externalities(|| {
+        let empty_name = vec![];
+        assert_err!(
+            TestModule::create_class(empty_name, good_class_description(),),
+            ERROR_CLASS_NAME_TOO_SHORT
+        );
+    })
+}
+
+#[test]
+fn create_class_with_empty_description() {
+    with_test_externalities(|| {
+        let empty_description = vec![];
+        assert_eq!(
+            TestModule::create_class(good_class_name(), empty_description,),
+            Ok(1)
+        );
+    })
+}
+
+// Add class schema
+// --------------------------------------
+
+#[test]
+fn cannot_add_schema_to_unknown_class() {
+    with_test_externalities(|| {
+        assert_err!(
+            TestModule::add_class_schema(UNKNOWN_CLASS_ID, good_prop_ids(), good_props()),
+            ERROR_CLASS_NOT_FOUND
+        );
+    })
+}
+
+#[test]
+fn cannot_add_class_schema_when_no_props_passed() {
+    with_test_externalities(|| {
+        let class_id = create_class();
+        assert_err!(
+            TestModule::add_class_schema(class_id, vec![], vec![]),
+            ERROR_NO_PROPS_IN_CLASS_SCHEMA
+        );
+    })
+}
+
+#[test]
+fn cannot_add_class_schema_when_it_refers_unknown_prop_index_and_class_has_no_props() {
+    with_test_externalities(|| {
+        let class_id = create_class();
+        assert_err!(
+            TestModule::add_class_schema(class_id, vec![UNKNOWN_PROP_ID], vec![]),
+            ERROR_CLASS_SCHEMA_REFERS_UNKNOWN_PROP_INDEX
+        );
+    })
+}
+
+#[test]
+fn cannot_add_class_schema_when_it_refers_unknown_prop_index() {
+    with_test_externalities(|| {
+        let class_id = create_class();
+
+        assert_eq!(
+            TestModule::add_class_schema(class_id, vec![], good_props()),
+            Ok(SCHEMA_ID_0)
+        );
+
+        // Try to add a new schema that is based on one valid prop ids
+        // plus another prop id is unknown on this class.
+        assert_err!(
+            TestModule::add_class_schema(class_id, vec![0, UNKNOWN_PROP_ID], vec![]),
+            ERROR_CLASS_SCHEMA_REFERS_UNKNOWN_PROP_INDEX
+        );
+
+        // Verify that class props and schemas remain unchanged:
+        assert_class_props(class_id, good_props());
+        assert_class_schemas(class_id, vec![good_prop_ids()]);
+    })
+}
+
+#[test]
+fn cannot_add_class_schema_when_it_refers_unknown_internal_id() {
+    with_test_externalities(|| {
+        let class_id = create_class();
+        let bad_internal_prop = new_internal_class_prop(UNKNOWN_CLASS_ID);
+
+        assert_err!(
+            TestModule::add_class_schema(
+                class_id,
+                vec![],
+                vec![good_prop_bool(), bad_internal_prop]
+            ),
+            ERROR_CLASS_SCHEMA_REFERS_UNKNOWN_INTERNAL_ID
+        );
+    })
+}
+
+#[test]
+fn should_add_class_schema_with_internal_class_prop() {
+    with_test_externalities(|| {
+        let class_id = create_class();
+        let internal_class_prop = new_internal_class_prop(class_id);
+
+        // Add first schema with new props.
+        // No other props on the class at this time.
+        assert_eq!(
+            TestModule::add_class_schema(class_id, vec![], vec![internal_class_prop.clone()]),
+            Ok(SCHEMA_ID_0)
+        );
+
+        assert_class_props(class_id, vec![internal_class_prop]);
+        assert_class_schemas(class_id, vec![vec![SCHEMA_ID_0]]);
+    })
+}
+
+#[test]
+fn should_add_class_schema_when_only_new_props_passed() {
+    with_test_externalities(|| {
+        let class_id = create_class();
+
+        // Add first schema with new props.
+        // No other props on the class at this time.
+        assert_eq!(
+            TestModule::add_class_schema(class_id, vec![], good_props()),
+            Ok(SCHEMA_ID_0)
+        );
+
+        assert_class_props(class_id, good_props());
+        assert_class_schemas(class_id, vec![good_prop_ids()]);
+    })
+}
+
+#[test]
+fn should_add_class_schema_when_only_prop_ids_passed() {
+    with_test_externalities(|| {
+        let class_id = create_class();
+
+        // Add first schema with new props.
+        // No other props on the class at this time.
+        assert_eq!(
+            TestModule::add_class_schema(class_id, vec![], good_props()),
+            Ok(SCHEMA_ID_0)
+        );
+
+        // Add a new schema that is based solely on the props ids
+        // of the previously added schema.
+        assert_eq!(
+            TestModule::add_class_schema(class_id, good_prop_ids(), vec![]),
+            Ok(SCHEMA_ID_1)
+        );
+    })
+}
+
+#[test]
+fn cannot_add_class_schema_when_new_props_have_duplicate_names() {
+    with_test_externalities(|| {
+        let class_id = create_class();
+
+        // Add first schema with new props.
+        // No other props on the class at this time.
+        assert_eq!(
+            TestModule::add_class_schema(class_id, vec![], good_props()),
+            Ok(SCHEMA_ID_0)
+        );
+
+        // Add a new schema with not unique property names:
+        assert_err!(
+            TestModule::add_class_schema(class_id, vec![], good_props()),
+            ERROR_PROP_NAME_NOT_UNIQUE_IN_CLASS
+        );
+    })
+}
+
+#[test]
+fn should_add_class_schema_when_both_prop_ids_and_new_props_passed() {
+    with_test_externalities(|| {
+        let class_id = create_class();
+
+        // Add first schema with new props.
+        // No other props on the class at this time.
+        assert_eq!(
+            TestModule::add_class_schema(class_id, vec![], vec![good_prop_bool(), good_prop_u32()]),
+            Ok(SCHEMA_ID_0)
+        );
+
+        // Add a new schema that is based on some prop ids
+        // added with previous schema plus some new props,
+        // introduced by this new schema.
+        assert_eq!(
+            TestModule::add_class_schema(class_id, vec![1], vec![good_prop_text()]),
+            Ok(SCHEMA_ID_1)
+        );
+
+        assert_class_props(
+            class_id,
+            vec![good_prop_bool(), good_prop_u32(), good_prop_text()],
+        );
+
+        assert_class_schemas(class_id, vec![vec![0, 1], vec![1, 2]]);
+    })
+}
+
+// Create entity
+// --------------------------------------
+
+#[test]
+fn create_entity_successfully() {
+    with_test_externalities(|| {
+        let class_id = create_class();
+        let entity_id_1 = TestModule::next_entity_id();
+        assert_ok!(TestModule::create_entity(class_id,), entity_id_1);
+        // TODO assert entity from storage
+        assert_eq!(TestModule::next_entity_id(), entity_id_1 + 1);
+    })
+}
+
+#[test]
+fn cannot_create_entity_with_unknown_class_id() {
+    with_test_externalities(|| {
+        assert_err!(
+            TestModule::create_entity(UNKNOWN_CLASS_ID,),
+            ERROR_CLASS_NOT_FOUND
+        );
+    })
+}
+
+// Add schema support to entity
+// --------------------------------------
+
+#[test]
+fn cannot_add_schema_to_entity_when_entity_not_found() {
+    with_test_externalities(|| {
+        assert_entity_not_found(TestModule::add_schema_support_to_entity(
+            UNKNOWN_ENTITY_ID,
+            1,
+            vec![],
+        ));
+    })
+}
+
+#[test]
+fn cannot_add_schema_to_entity_when_schema_already_added_to_entity() {
+    with_test_externalities(|| {
+        let (_, schema_id, entity_id) = create_class_with_schema_and_entity();
+
+        // Firstly we just add support for a valid class schema.
+        assert_ok!(TestModule::add_schema_support_to_entity(
+            entity_id,
+            schema_id,
+            vec![bool_prop_value()]
+        ));
+
+        // Secondly we try to add support for the same schema.
+        assert_err!(
+            TestModule::add_schema_support_to_entity(entity_id, schema_id, vec![]),
+            ERROR_SCHEMA_ALREADY_ADDED_TO_ENTITY
+        );
+    })
+}
+
+#[test]
+fn cannot_add_schema_to_entity_when_schema_id_is_unknown() {
+    with_test_externalities(|| {
+        let (_, schema_id, entity_id) = create_class_with_schema_and_entity();
+        let unknown_schema_id = schema_id + 1;
+        assert_err!(
+            TestModule::add_schema_support_to_entity(
+                entity_id,
+                unknown_schema_id,
+                vec![prop_value(0, PropertyValue::None)]
+            ),
+            ERROR_UNKNOWN_CLASS_SCHEMA_ID
+        );
+    })
+}
+
+#[test]
+fn cannot_add_schema_to_entity_when_prop_value_dont_match_type() {
+    with_test_externalities(|| {
+        let (_, schema_id, entity_id) = create_class_with_schema_and_entity();
+        assert_err!(
+            TestModule::add_schema_support_to_entity(
+                entity_id,
+                schema_id,
+                vec![
+                    bool_prop_value(),
+                    prop_value(PROP_ID_U32, PropertyValue::Bool(true))
+                ]
+            ),
+            ERROR_PROP_VALUE_DONT_MATCH_TYPE
+        );
+    })
+}
+
+#[test]
+fn cannot_add_schema_to_entity_when_unknown_internal_entity_id() {
+    with_test_externalities(|| {
+        let (_, schema_id, entity_id) = create_class_with_schema_and_entity();
+        assert_err!(
+            TestModule::add_schema_support_to_entity(
+                entity_id,
+                schema_id,
+                vec![
+                    bool_prop_value(),
+                    prop_value(PROP_ID_INTERNAL, PropertyValue::Internal(UNKNOWN_ENTITY_ID))
+                ]
+            ),
+            ERROR_ENTITY_NOT_FOUND
+        );
+    })
+}
+
+#[test]
+fn cannot_add_schema_to_entity_when_missing_required_prop() {
+    with_test_externalities(|| {
+        let (_, schema_id, entity_id) = create_class_with_schema_and_entity();
+        assert_err!(
+            TestModule::add_schema_support_to_entity(
+                entity_id,
+                schema_id,
+                vec![prop_value(PROP_ID_U32, PropertyValue::Uint32(456))]
+            ),
+            ERROR_MISSING_REQUIRED_PROP
+        );
+    })
+}
+
+#[test]
+fn should_add_schema_to_entity_when_some_optional_props_provided() {
+    with_test_externalities(|| {
+        let (_, schema_id, entity_id) = create_class_with_schema_and_entity();
+        assert_ok!(TestModule::add_schema_support_to_entity(
+            entity_id,
+            schema_id,
+            vec![
+                bool_prop_value(),
+                prop_value(PROP_ID_U32, PropertyValue::Uint32(123)),
+                // Note that an optional internal prop is not provided here.
+            ]
+        ));
+
+        let entity = TestModule::entity_by_id(entity_id);
+        assert_eq!(entity.in_class_schema_indexes, [SCHEMA_ID_0]);
+        assert_eq!(
+            entity.values,
+            vec![
+                bool_prop_value(),
+                prop_value(PROP_ID_U32, PropertyValue::Uint32(123)),
+                prop_value(PROP_ID_INTERNAL, PropertyValue::None),
+            ]
+        );
+    })
+}
+
+// Update entity properties
+// --------------------------------------
+
+#[test]
+fn cannot_update_entity_props_when_entity_not_found() {
+    with_test_externalities(|| {
+        assert_entity_not_found(TestModule::update_entity_property_values(
+            UNKNOWN_ENTITY_ID,
+            vec![],
+        ));
+    })
+}
+
+#[test]
+fn cannot_update_entity_props_when_prop_value_dont_match_type() {
+    with_test_externalities(|| {
+        let entity_id = create_entity_with_schema_support();
+        assert_err!(
+            TestModule::update_entity_property_values(
+                entity_id,
+                vec![prop_value(PROP_ID_BOOL, PropertyValue::Uint32(1))]
+            ),
+            ERROR_PROP_VALUE_DONT_MATCH_TYPE
+        );
+    })
+}
+
+#[test]
+fn cannot_update_entity_props_when_unknown_internal_entity_id() {
+    with_test_externalities(|| {
+        let entity_id = create_entity_with_schema_support();
+        assert_err!(
+            TestModule::update_entity_property_values(
+                entity_id,
+                vec![prop_value(
+                    PROP_ID_INTERNAL,
+                    PropertyValue::Internal(UNKNOWN_ENTITY_ID)
+                )]
+            ),
+            ERROR_ENTITY_NOT_FOUND
+        );
+    })
+}
+
+#[test]
+fn cannot_update_entity_props_when_unknown_entity_prop_id() {
+    with_test_externalities(|| {
+        let entity_id = create_entity_with_schema_support();
+        assert_err!(
+            TestModule::update_entity_property_values(
+                entity_id,
+                vec![prop_value(UNKNOWN_PROP_ID, PropertyValue::Bool(true))]
+            ),
+            ERROR_UNKNOWN_ENTITY_PROP_ID
+        );
+    })
+}
+
+#[test]
+fn update_entity_props_successfully() {
+    with_test_externalities(|| {
+        let entity_id = create_entity_with_schema_support();
+        assert_eq!(
+            TestModule::entity_by_id(entity_id).values,
+            vec![
+                prop_value(PROP_ID_BOOL, PropertyValue::Bool(true)),
+                prop_value(PROP_ID_U32, PropertyValue::None),
+                prop_value(PROP_ID_INTERNAL, PropertyValue::None),
+            ]
+        );
+        assert_ok!(TestModule::update_entity_property_values(
+            entity_id,
+            vec![
+                prop_value(PROP_ID_BOOL, PropertyValue::Bool(false)),
+                prop_value(PROP_ID_U32, PropertyValue::Uint32(123)),
+                prop_value(PROP_ID_INTERNAL, PropertyValue::Internal(entity_id)),
+            ]
+        ));
+        assert_eq!(
+            TestModule::entity_by_id(entity_id).values,
+            vec![
+                prop_value(PROP_ID_BOOL, PropertyValue::Bool(false)),
+                prop_value(PROP_ID_U32, PropertyValue::Uint32(123)),
+                prop_value(PROP_ID_INTERNAL, PropertyValue::Internal(entity_id)),
+            ]
+        );
+    })
+}
+
+// TODO test text max len
+
+// TODO test vec max len
+
+// Delete entity
+// --------------------------------------
+
+// #[test]
+// fn delete_entity_successfully() {
+//     with_test_externalities(|| {
+//         let entity_id = create_entity();
+//         assert_ok!(
+//             TestModule::delete_entity(entity_id),
+//             ()
+//         );
+//     })
+// }
+
+// #[test]
+// fn cannot_delete_entity_when_entity_not_found() {
+//     with_test_externalities(|| {
+//         assert_entity_not_found(
+//             TestModule::delete_entity(UNKNOWN_ENTITY_ID)
+//         );
+//     })
+// }
+
+// #[test]
+// fn cannot_delete_already_deleted_entity() {
+//     with_test_externalities(|| {
+//         let entity_id = create_entity();
+//         let _ok = TestModule::delete_entity(entity_id);
+//         assert_err!(
+//             TestModule::delete_entity(entity_id),
+//             ERROR_ENTITY_ALREADY_DELETED
+//         );
+//     })
+// }