Browse Source

Merge pull request #2148 from conectado/blog-module

Add blog module to olympia
Bedeho Mender 4 years ago
parent
commit
cf69439184

+ 22 - 0
Cargo.lock

@@ -2351,6 +2351,7 @@ dependencies = [
  "pallet-authorship",
  "pallet-babe",
  "pallet-balances",
+ "pallet-blog",
  "pallet-common",
  "pallet-constitution",
  "pallet-content-directory",
@@ -3734,6 +3735,26 @@ dependencies = [
  "sp-std",
 ]
 
+[[package]]
+name = "pallet-blog"
+version = "3.0.0"
+dependencies = [
+ "frame-benchmarking",
+ "frame-support",
+ "frame-system",
+ "pallet-balances",
+ "pallet-common",
+ "pallet-membership",
+ "pallet-staking-handler",
+ "pallet-timestamp",
+ "parity-scale-codec",
+ "sp-arithmetic",
+ "sp-core",
+ "sp-io",
+ "sp-runtime",
+ "sp-std",
+]
+
 [[package]]
 name = "pallet-common"
 version = "4.0.0"
@@ -3961,6 +3982,7 @@ dependencies = [
  "frame-support",
  "frame-system",
  "pallet-balances",
+ "pallet-blog",
  "pallet-common",
  "pallet-constitution",
  "pallet-council",

+ 1 - 0
Cargo.toml

@@ -16,6 +16,7 @@ members = [
 	"runtime-modules/content-directory",
 	"runtime-modules/constitution",
 	"runtime-modules/staking-handler",
+    "runtime-modules/blog",
 	"node",
 	"utils/chain-spec-builder/"
 ]

+ 2 - 0
runtime-modules/blog/.gitattributes

@@ -0,0 +1,2 @@
+# Auto detect text files and perform LF normalization
+* text=auto

+ 3 - 0
runtime-modules/blog/.gitignore

@@ -0,0 +1,3 @@
+/target
+**/*.rs.bk
+*.lock

+ 40 - 0
runtime-modules/blog/Cargo.toml

@@ -0,0 +1,40 @@
+[package]
+name = 'pallet-blog'
+version = '3.0.0'
+authors = ['Joystream contributors']
+edition = '2018'
+
+[dependencies]
+sp-std = { package = 'sp-std', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'a200cdb93c6af5763b9c7bf313fa708764ac88ca'}
+sp-runtime = { package = 'sp-runtime', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'a200cdb93c6af5763b9c7bf313fa708764ac88ca'}
+frame-support = { package = 'frame-support', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'a200cdb93c6af5763b9c7bf313fa708764ac88ca'}
+frame-system = { package = 'frame-system', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'a200cdb93c6af5763b9c7bf313fa708764ac88ca'}
+codec = { package = 'parity-scale-codec', version = '1.0.0', default-features = false, features = ['derive'] }
+sp-arithmetic = { package = 'sp-arithmetic', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'a200cdb93c6af5763b9c7bf313fa708764ac88ca'}
+common = { package = 'pallet-common', default-features = false, path = '../common'}
+
+#Benchmark dependencies
+frame-benchmarking = { package = 'frame-benchmarking', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'a200cdb93c6af5763b9c7bf313fa708764ac88ca', optional = true}
+membership = { package = 'pallet-membership', default-features = false, path = '../membership', optional = true}
+balances = { package = 'pallet-balances', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'a200cdb93c6af5763b9c7bf313fa708764ac88ca', optional = true}
+
+[dev-dependencies]
+sp-io = { package = 'sp-io', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'a200cdb93c6af5763b9c7bf313fa708764ac88ca'}
+sp-core = { package = 'sp-core', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'a200cdb93c6af5763b9c7bf313fa708764ac88ca'}
+balances = { package = 'pallet-balances', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'a200cdb93c6af5763b9c7bf313fa708764ac88ca'}
+membership = { package = 'pallet-membership', default-features = false, path = '../membership' }
+staking-handler = { package = 'pallet-staking-handler', default-features = false, path = '../staking-handler' }
+pallet-timestamp = { package = 'pallet-timestamp', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'a200cdb93c6af5763b9c7bf313fa708764ac88ca'}
+
+[features]
+default = ['std']
+runtime-benchmarks = ['frame-benchmarking', 'membership', 'balances']
+std = [
+	'codec/std',
+	'sp-std/std',
+	'frame-support/std',
+	'sp-runtime/std',
+	'frame-system/std',
+    'common/std',
+    'sp-arithmetic/std'
+]

+ 2 - 0
runtime-modules/blog/README.md

@@ -0,0 +1,2 @@
+# substrate-blog-module
+A reusable blog module for Substrate.

+ 344 - 0
runtime-modules/blog/src/benchmarking.rs

@@ -0,0 +1,344 @@
+#![cfg(feature = "runtime-benchmarks")]
+use super::*;
+use balances::Module as Balances;
+use frame_benchmarking::{account, benchmarks_instance, Zero};
+use frame_support::traits::Currency;
+use frame_system::Module as System;
+use frame_system::{EventRecord, RawOrigin};
+use membership::Module as Membership;
+use sp_runtime::traits::Bounded;
+use sp_std::convert::TryInto;
+use Module as Blog;
+
+const MAX_BYTES: u32 = 16384;
+const SEED: u32 = 0;
+
+fn assert_last_event<T: Trait<I>, I: Instance>(generic_event: <T as Trait<I>>::Event) {
+    let events = System::<T>::events();
+    let system_event: <T as frame_system::Trait>::Event = generic_event.into();
+
+    assert!(!events.is_empty(), "There are no events in event queue");
+
+    // compare to the last event record
+    let EventRecord { event, .. } = &events[events.len() - 1];
+    assert_eq!(event, &system_event);
+}
+
+fn get_byte(num: u32, byte_number: u8) -> u8 {
+    ((num & (0xff << (8 * byte_number))) >> 8 * byte_number) as u8
+}
+
+fn member_funded_account<T: Trait<I> + membership::Trait + balances::Trait, I: Instance>(
+    name: &'static str,
+    id: u32,
+) -> (T::AccountId, T::MemberId) {
+    let account_id = account::<T::AccountId>(name, id, SEED);
+    let handle = handle_from_id::<T>(id);
+
+    let _ = Balances::<T>::make_free_balance_be(
+        &account_id,
+        <T as balances::Trait>::Balance::max_value(),
+    );
+
+    let params = membership::BuyMembershipParameters {
+        root_account: account_id.clone(),
+        controller_account: account_id.clone(),
+        name: None,
+        handle: Some(handle),
+        avatar_uri: None,
+        about: None,
+        referrer_id: None,
+    };
+
+    Membership::<T>::buy_membership(RawOrigin::Signed(account_id.clone()).into(), params).unwrap();
+
+    let member_id = T::MemberId::from(id.try_into().unwrap());
+    Membership::<T>::add_staking_account_candidate(
+        RawOrigin::Signed(account_id.clone()).into(),
+        member_id.clone(),
+    )
+    .unwrap();
+    Membership::<T>::confirm_staking_account(
+        RawOrigin::Signed(account_id.clone()).into(),
+        member_id.clone(),
+        account_id.clone(),
+    )
+    .unwrap();
+
+    (account_id, member_id)
+}
+
+// Method to generate a distintic valid handle
+// for a membership. For each index.
+fn handle_from_id<T: membership::Trait>(id: u32) -> Vec<u8> {
+    let min_handle_length = 1;
+
+    let mut handle = vec![];
+
+    for i in 0..4 {
+        handle.push(get_byte(id, i));
+    }
+
+    while handle.len() < (min_handle_length as usize) {
+        handle.push(0u8);
+    }
+
+    handle
+}
+
+fn generate_post<T: Trait<I>, I: Instance>() -> PostId {
+    assert_eq!(Blog::<T, I>::post_count(), 0);
+
+    Blog::<T, I>::create_post(RawOrigin::Root.into(), vec![0u8], vec![0u8]).unwrap();
+
+    let post_id = 0;
+
+    assert_eq!(Blog::<T, I>::post_count(), 1);
+
+    assert_eq!(
+        Blog::<T, I>::post_by_id(post_id),
+        Post::<T, I>::new(&vec![0u8], &vec![0u8])
+    );
+
+    post_id
+}
+
+fn generate_reply<T: Trait<I>, I: Instance>(
+    creator_id: T::AccountId,
+    participant_id: ParticipantId<T>,
+    post_id: PostId,
+) -> T::ReplyId {
+    let creator_origin = RawOrigin::Signed(creator_id);
+    Blog::<T, I>::create_reply(
+        creator_origin.clone().into(),
+        participant_id,
+        post_id,
+        None,
+        vec![0u8],
+    )
+    .unwrap();
+
+    assert_eq!(
+        Blog::<T, I>::reply_by_id(post_id, T::ReplyId::zero()),
+        Reply::<T, I>::new(vec![0u8], participant_id, ParentId::Post(post_id))
+    );
+
+    T::ReplyId::zero()
+}
+
+benchmarks_instance! {
+    where_clause { where T: balances::Trait, T: membership::Trait }
+
+    _ {}
+
+    create_post {
+        let t in 0 .. MAX_BYTES;
+        let b in 0 .. MAX_BYTES;
+        assert_eq!(Blog::<T, I>::post_count(), 0);
+        let title = vec![0u8; t.try_into().unwrap()];
+        let body = vec![0u8; b.try_into().unwrap()];
+        let post_id = Blog::<T, I>::post_count();
+
+    }:_(RawOrigin::Root, title.clone(), body.clone())
+    verify {
+        assert_eq!(Blog::<T, I>::post_count(), post_id + 1);
+
+        assert_eq!(
+            Blog::<T, I>::post_by_id(post_id),
+            Post::<T, I>::new(&title, &body)
+        );
+
+        assert_last_event::<T, I>(RawEvent::PostCreated(
+                0,
+                title,
+                body
+            ).into());
+    }
+
+    lock_post {
+        let post_id = generate_post::<T, I>();
+    }: _(RawOrigin::Root, post_id)
+    verify {
+        assert!(Blog::<T, I>::post_by_id(post_id).is_locked());
+        assert_last_event::<T, I>(RawEvent::PostLocked(post_id).into());
+    }
+
+    unlock_post {
+        let post_id = generate_post::<T, I>();
+        Blog::<T, I>::lock_post(RawOrigin::Root.into(), post_id).unwrap();
+        assert!(Blog::<T, I>::post_by_id(post_id).is_locked());
+    }: _(RawOrigin::Root, post_id)
+    verify {
+        assert!(!Blog::<T, I>::post_by_id(post_id).is_locked());
+        assert_last_event::<T, I>(RawEvent::PostUnlocked(post_id).into());
+    }
+
+    edit_post {
+        let t in 0 .. MAX_BYTES;
+        let b in 0 .. MAX_BYTES;
+
+        let post_id = generate_post::<T, I>();
+        let title = Some(vec![1u8; t.try_into().unwrap()]);
+        let body = Some(vec![1u8; b.try_into().unwrap()]);
+    }: _(RawOrigin::Root, post_id, title.clone(), body.clone())
+    verify {
+        assert_eq!(
+            Blog::<T, I>::post_by_id(post_id),
+            Post::<T, I>::new(&vec![1u8; t.try_into().unwrap()], &vec![1u8; b.try_into().unwrap()])
+        );
+        assert_last_event::<T, I>(RawEvent::PostEdited(post_id, title, body).into());
+    }
+
+    create_reply_to_post {
+        let t in 0 .. MAX_BYTES;
+
+        let post_id = generate_post::<T, I>();
+        let (account_id, participant_id) = member_funded_account::<T, I>("caller", 0);
+        let origin = RawOrigin::Signed(account_id);
+        let text = vec![0u8; t.try_into().unwrap()];
+    }: create_reply(origin.clone(), participant_id, post_id, None, text.clone())
+    verify {
+        let mut expected_post = Post::<T, I>::new(&vec![0u8], &vec![0u8]);
+        expected_post.increment_replies_counter();
+        assert_eq!(Blog::<T, I>::post_by_id(post_id), expected_post);
+        assert_eq!(
+            Blog::<T, I>::reply_by_id(post_id, T::ReplyId::zero()),
+            Reply::<T, I>::new(
+                text.clone(),
+                participant_id,
+                ParentId::Post(post_id)
+            )
+        );
+
+        assert_last_event::<T, I>(
+            RawEvent::ReplyCreated(
+                participant_id,
+                post_id,
+                Zero::zero(),
+                text
+            ).into()
+        );
+    }
+
+    create_reply_to_reply {
+        let t in 0 .. MAX_BYTES;
+
+        let post_id = generate_post::<T, I>();
+        let (account_id, participant_id) = member_funded_account::<T, I>("caller", 0);
+        let reply_id = generate_reply::<T, I>(account_id.clone(), participant_id, post_id.clone());
+        let origin = RawOrigin::Signed(account_id);
+        let mut expected_post = Post::<T, I>::new(&vec![0u8], &vec![0u8]);
+        expected_post.increment_replies_counter();
+        assert_eq!(Blog::<T, I>::post_by_id(post_id), expected_post);
+        let text = vec![0u8; t.try_into().unwrap()];
+    }: create_reply(origin.clone(), participant_id, post_id, Some(reply_id), text.clone())
+    verify {
+        expected_post.increment_replies_counter();
+        assert_eq!(Blog::<T, I>::post_by_id(post_id), expected_post);
+        assert_eq!(
+            Blog::<T, I>::reply_by_id(post_id, T::ReplyId::one()),
+            Reply::<T, I>::new(
+                text.clone(),
+                participant_id,
+                ParentId::Reply(reply_id)
+            )
+        );
+
+        assert_last_event::<T, I>(
+            RawEvent::DirectReplyCreated(
+                participant_id,
+                post_id,
+                reply_id,
+                One::one(),
+                text
+            ).into()
+        );
+    }
+
+    edit_reply {
+        let t in 0 .. MAX_BYTES;
+
+        let post_id = generate_post::<T, I>();
+        let (account_id, participant_id) = member_funded_account::<T, I>("caller", 0);
+        let reply_id = generate_reply::<T, I>(account_id.clone(), participant_id, post_id.clone());
+        let origin = RawOrigin::Signed(account_id);
+        let updated_text = vec![1u8; t.try_into().unwrap()];
+    }: _(origin.clone(), participant_id, post_id, reply_id, updated_text.clone())
+    verify {
+        assert_eq!(
+            Blog::<T, I>::reply_by_id(post_id, reply_id).text_hash,
+            T::Hashing::hash(&updated_text)
+        );
+        assert_eq!(
+            Blog::<T, I>::reply_by_id(post_id, reply_id),
+            Reply::<T, I>::new(
+                updated_text.clone(),
+                participant_id,
+                ParentId::Post(post_id)
+            )
+        );
+
+        assert_last_event::<T, I>(RawEvent::ReplyEdited(
+                participant_id,
+                post_id,
+                reply_id,
+                updated_text
+            ).into());
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::mock::{ExtBuilder, Runtime};
+    use frame_support::assert_ok;
+
+    #[test]
+    fn test_create_post() {
+        ExtBuilder::default().build().execute_with(|| {
+            assert_ok!(test_benchmark_create_post::<Runtime>());
+        })
+    }
+
+    #[test]
+    fn test_lock_post() {
+        ExtBuilder::default().build().execute_with(|| {
+            assert_ok!(test_benchmark_lock_post::<Runtime>());
+        })
+    }
+
+    #[test]
+    fn test_unlock_post() {
+        ExtBuilder::default().build().execute_with(|| {
+            assert_ok!(test_benchmark_unlock_post::<Runtime>());
+        })
+    }
+
+    #[test]
+    fn test_edit_post() {
+        ExtBuilder::default().build().execute_with(|| {
+            assert_ok!(test_benchmark_edit_post::<Runtime>());
+        })
+    }
+
+    #[test]
+    fn test_create_reply_to_post() {
+        ExtBuilder::default().build().execute_with(|| {
+            assert_ok!(test_benchmark_create_reply_to_post::<Runtime>());
+        })
+    }
+
+    #[test]
+    fn test_create_reply_to_reply() {
+        ExtBuilder::default().build().execute_with(|| {
+            assert_ok!(test_benchmark_create_reply_to_reply::<Runtime>());
+        })
+    }
+
+    #[test]
+    fn test_edit_reply() {
+        ExtBuilder::default().build().execute_with(|| {
+            assert_ok!(test_benchmark_edit_reply::<Runtime>());
+        })
+    }
+}

+ 34 - 0
runtime-modules/blog/src/errors.rs

@@ -0,0 +1,34 @@
+use crate::{Instance, Module, Trait};
+use frame_support::decl_error;
+
+decl_error! {
+    /// Blog module predefined errors
+    pub enum Error for Module<T: Trait<I>, I: Instance> {
+        /// A non-owner is trying to do a privilegeded action.
+        BlogOwnershipError,
+
+        /// A non-member is trying to participate
+        MembershipError,
+
+        /// Post do not exists.
+        PostNotFound,
+
+        /// Post is locked for modifications.
+        PostLockedError,
+
+        /// Reply do no exists.
+        ReplyNotFound,
+
+        /// A non-owner of a reply is trying to do a privileged action.
+        ReplyOwnershipError,
+
+        /// Number of posts exceeds limits.
+        PostLimitReached,
+
+        /// Number of maximum replies reached
+        RepliesLimitReached,
+
+        /// Reaction doesn't exists
+        InvalidReactionIndex,
+    }
+}

+ 712 - 0
runtime-modules/blog/src/lib.rs

@@ -0,0 +1,712 @@
+//! # Blog Module
+//!
+//!
+//! The Blog module provides functionality for handling blogs
+//!
+//! - [`timestamp::Trait`](./trait.Trait.html)
+//! - [`Call`](./enum.Call.html)
+//! - [`Module`](./struct.Module.html)
+//!
+//! ## Overview
+//!
+//! The blog module provides functions for:
+//!
+//! - Creation and editing of posts, associated with given blog
+//! - Posts locking/unlocking
+//! - Creation and editing of replies, associated with given post
+//!
+//! ### Terminology
+//!
+//! - **Lock:** A forbiddance of mutation of any associated information related to a given post.
+//!
+//! ## Interface
+//! The posts creation/edition/locking/unlocking are done through proposals
+//! To reply to posts you need to be a member
+//!
+//! ## Supported extrinsics
+//!
+//! - [create_post](./struct.Module.html#method.create_post)
+//! - [lock_post](./struct.Module.html#method.lock_post)
+//! - [unlock_post](./struct.Module.html#method.unlock_post)
+//! - [edit_post](./struct.Module.html#method.edit_post)
+//! - [create_reply](./struct.Module.html#method.create_reply)
+//! - [edit_reply](./struct.Module.html#method.create_reply)
+
+#![cfg_attr(not(feature = "std"), no_std)]
+
+use codec::{Codec, Decode, Encode};
+use common::origin::MemberOriginValidator;
+use errors::Error;
+pub use frame_support::dispatch::{DispatchError, DispatchResult};
+use frame_support::weights::Weight;
+use frame_support::{
+    decl_event, decl_module, decl_storage, ensure, traits::Get, Parameter, StorageDoubleMap,
+};
+use sp_arithmetic::traits::{BaseArithmetic, One};
+use sp_runtime::traits::{Hash, MaybeSerialize, Member};
+use sp_runtime::SaturatedConversion;
+use sp_std::prelude::*;
+
+mod benchmarking;
+mod errors;
+mod mock;
+mod tests;
+
+// Type for maximum number of posts/replies
+type MaxNumber = u64;
+
+/// Type for post IDs
+pub type PostId = u64;
+
+/// Blogger participant ID alias for the member of the system.
+pub type ParticipantId<T> = common::MemberId<T>;
+
+/// blog WeightInfo.
+/// Note: This was auto generated through the benchmark CLI using the `--weight-trait` flag
+pub trait WeightInfo {
+    fn create_post(t: u32, b: u32) -> Weight;
+    fn lock_post() -> Weight;
+    fn unlock_post() -> Weight;
+    fn edit_post(t: u32, b: u32) -> Weight;
+    fn create_reply_to_post(t: u32) -> Weight;
+    fn create_reply_to_reply(t: u32) -> Weight;
+    fn edit_reply(t: u32) -> Weight;
+}
+
+// The pallet's configuration trait.
+pub trait Trait<I: Instance = DefaultInstance>: frame_system::Trait + common::Trait {
+    /// Origin from which participant must come.
+    type ParticipantEnsureOrigin: MemberOriginValidator<
+        Self::Origin,
+        ParticipantId<Self>,
+        Self::AccountId,
+    >;
+
+    /// The overarching event type.
+    type Event: From<Event<Self, I>> + Into<<Self as frame_system::Trait>::Event>;
+
+    /// The maximum number of posts in a blog.
+    type PostsMaxNumber: Get<MaxNumber>;
+
+    /// The maximum number of replies to a post.
+    type RepliesMaxNumber: Get<MaxNumber>;
+
+    /// Type of identifier for replies.
+    type ReplyId: Parameter
+        + Member
+        + BaseArithmetic
+        + Codec
+        + Default
+        + Copy
+        + MaybeSerialize
+        + PartialEq
+        + From<u64>
+        + Into<u64>;
+
+    /// Weight information for extrinsics in this pallet.
+    type WeightInfo: WeightInfo;
+}
+
+/// Type, representing blog related post structure
+#[derive(Encode, Decode, Clone)]
+pub struct Post<T: Trait<I>, I: Instance> {
+    /// Locking status
+    locked: bool,
+    title_hash: T::Hash,
+    body_hash: T::Hash,
+    /// Overall replies counter, associated with post
+    replies_count: T::ReplyId,
+}
+
+// Note: we derive it by hand because the derive isn't working because of a Rust problem
+// where the generic parameters need to comply with the bounds instead of the associated traits
+// see: https://github.com/rust-lang/rust/issues/26925
+impl<T: Trait<I>, I: Instance> sp_std::fmt::Debug for Post<T, I> {
+    fn fmt(&self, f: &mut sp_std::fmt::Formatter<'_>) -> sp_std::fmt::Result {
+        f.debug_struct("Post")
+            .field("locked", &self.locked)
+            .field("title_hash", &self.title_hash)
+            .field("body_hash", &self.body_hash)
+            .field("replies_count", &self.replies_count)
+            .finish()
+    }
+}
+
+// Note: we derive it by hand because the derive isn't working because of a Rust problem
+// where the generic parameters need to comply with the bounds instead of the associated traits
+// see: https://github.com/rust-lang/rust/issues/26925
+impl<T: Trait<I>, I: Instance> PartialEq for Post<T, I> {
+    fn eq(&self, other: &Post<T, I>) -> bool {
+        self.locked == other.locked
+            && self.title_hash == other.title_hash
+            && self.body_hash == other.body_hash
+            && self.replies_count == other.replies_count
+    }
+}
+
+/// Default Post
+// Note: we derive it by hand because the derive isn't working because of a Rust problem
+// where the generic parameters need to comply with the bounds instead of the associated traits
+// see: https://github.com/rust-lang/rust/issues/26925
+impl<T: Trait<I>, I: Instance> Default for Post<T, I> {
+    fn default() -> Self {
+        Post {
+            locked: Default::default(),
+            title_hash: Default::default(),
+            body_hash: Default::default(),
+            replies_count: Default::default(),
+        }
+    }
+}
+
+impl<T: Trait<I>, I: Instance> Post<T, I> {
+    /// Create a new post with given title and body
+    pub fn new(title: &[u8], body: &[u8]) -> Self {
+        Self {
+            // Post default locking status
+            locked: false,
+            title_hash: T::Hashing::hash(title),
+            body_hash: T::Hashing::hash(body),
+            // Set replies count of newly created post to zero
+            replies_count: T::ReplyId::default(),
+        }
+    }
+
+    /// Make all data, associated with this post immutable
+    fn lock(&mut self) {
+        self.locked = true;
+    }
+
+    /// Inverse to lock
+    fn unlock(&mut self) {
+        self.locked = false;
+    }
+
+    /// Get current locking status
+    pub fn is_locked(&self) -> bool {
+        self.locked
+    }
+
+    /// Get overall replies count, associated with this post
+    fn replies_count(&self) -> T::ReplyId {
+        self.replies_count
+    }
+
+    /// Increase replies counter, associated with given post by 1
+    fn increment_replies_counter(&mut self) {
+        self.replies_count += T::ReplyId::one()
+    }
+
+    /// Update post title and body, if Option::Some(_)
+    fn update(&mut self, new_title: &Option<Vec<u8>>, new_body: &Option<Vec<u8>>) {
+        if let Some(ref new_title) = new_title {
+            self.title_hash = T::Hashing::hash(new_title)
+        }
+        if let Some(ref new_body) = new_body {
+            self.body_hash = T::Hashing::hash(new_body)
+        }
+    }
+}
+
+/// Enum variant, representing either reply or post id
+#[derive(Encode, Decode, Clone, PartialEq, Debug)]
+pub enum ParentId<ReplyId, PostId: Default> {
+    Reply(ReplyId),
+    Post(PostId),
+}
+
+/// Default parent representation
+impl<ReplyId, PostId: Default> Default for ParentId<ReplyId, PostId> {
+    fn default() -> Self {
+        ParentId::Post(PostId::default())
+    }
+}
+
+/// Type, representing either root post reply or direct reply to reply
+#[derive(Encode, Decode, Clone)]
+pub struct Reply<T: Trait<I>, I: Instance> {
+    /// Reply text hash
+    text_hash: T::Hash,
+    /// Participant id, associated with a reply owner
+    owner: ParticipantId<T>,
+    /// Reply`s parent id
+    parent_id: ParentId<T::ReplyId, PostId>,
+}
+
+// Note: we derive it by hand because the derive isn't working because of a Rust problem
+// where the generic parameters need to comply with the bounds instead of the associated traits
+// see: https://github.com/rust-lang/rust/issues/26925
+impl<T: Trait<I>, I: Instance> sp_std::fmt::Debug for Reply<T, I> {
+    fn fmt(&self, f: &mut sp_std::fmt::Formatter<'_>) -> sp_std::fmt::Result {
+        f.debug_struct("Reply")
+            .field("text_hash", &self.text_hash)
+            .field("owner", &self.owner)
+            .field("parent_id", &self.parent_id)
+            .finish()
+    }
+}
+
+/// Reply comparator
+// Note: we derive it by hand because the derive isn't working because of a Rust problem
+// where the generic parameters need to comply with the bounds instead of the associated traits
+// see: https://github.com/rust-lang/rust/issues/26925
+impl<T: Trait<I>, I: Instance> PartialEq for Reply<T, I> {
+    fn eq(&self, other: &Reply<T, I>) -> bool {
+        self.text_hash == other.text_hash
+            && self.owner == other.owner
+            && self.parent_id == other.parent_id
+    }
+}
+
+/// Default Reply
+// Note: we derive it by hand because the derive isn't working because of a Rust problem
+// where the generic parameters need to comply with the bounds instead of the associated traits
+// see: https://github.com/rust-lang/rust/issues/26925
+impl<T: Trait<I>, I: Instance> Default for Reply<T, I> {
+    fn default() -> Self {
+        Reply {
+            text_hash: Default::default(),
+            owner: Default::default(),
+            parent_id: Default::default(),
+        }
+    }
+}
+
+impl<T: Trait<I>, I: Instance> Reply<T, I> {
+    /// Create new reply with given text and owner id
+    fn new(
+        text: Vec<u8>,
+        owner: ParticipantId<T>,
+        parent_id: ParentId<T::ReplyId, PostId>,
+    ) -> Self {
+        Self {
+            text_hash: T::Hashing::hash(&text),
+            owner,
+            parent_id,
+        }
+    }
+
+    /// Check if account_id is reply owner
+    fn is_owner(&self, account_id: &ParticipantId<T>) -> bool {
+        self.owner == *account_id
+    }
+
+    /// Update reply`s text
+    fn update(&mut self, new_text: Vec<u8>) {
+        self.text_hash = T::Hashing::hash(&new_text)
+    }
+}
+
+// Blog`s pallet storage items.
+decl_storage! {
+    trait Store for Module<T: Trait<I>, I: Instance=DefaultInstance> as BlogModule {
+
+        /// Maps, representing id => item relationship for blogs, posts and replies related structures
+
+        /// Post count
+        PostCount get(fn post_count): PostId;
+
+        /// Post by unique blog and post identificators
+        PostById get(fn post_by_id): map hasher(blake2_128_concat) PostId => Post<T, I>;
+
+        /// Reply by unique blog, post and reply identificators
+        ReplyById get (fn reply_by_id): double_map hasher(blake2_128_concat) PostId, hasher(blake2_128_concat) T::ReplyId => Reply<T, I>;
+
+    }
+}
+
+// Blog`s pallet dispatchable functions.
+decl_module! {
+    pub struct Module<T: Trait<I>, I: Instance=DefaultInstance> for enum Call where origin: T::Origin {
+
+        /// Setup events
+        fn deposit_event() = default;
+
+        /// Predefined errors
+        type Error = Error<T, I>;
+
+        /// Blog owner can create posts, related to a given blog, if related blog is unlocked
+        ///
+        /// <weight>
+        ///
+        /// ## Weight
+        /// `O (T + B)` where:
+        /// - `T` is the length of the title
+        /// - `B` is the length of the body
+        /// - DB:
+        ///    - O(1) doesn't depend on the state or parameters
+        /// # </weight>
+        #[weight = T::WeightInfo::create_post(
+                title.len().saturated_into(),
+                body.len().saturated_into()
+            )]
+        pub fn create_post(origin, title: Vec<u8>, body: Vec<u8>) -> DispatchResult  {
+
+            // Ensure blog -> owner relation exists
+            Self::ensure_blog_ownership(origin)?;
+
+            // Check security/configuration constraints
+
+            let posts_count = Self::ensure_posts_limit_not_reached()?;
+
+            //
+            // == MUTATION SAFE ==
+            //
+
+            let post_count = <PostCount<I>>::get();
+            <PostCount<I>>::put(post_count + 1);
+
+            // New post creation
+            let post = Post::new(&title, &body);
+            <PostById<T, I>>::insert(posts_count, post);
+
+            // Trigger event
+            Self::deposit_event(RawEvent::PostCreated(posts_count, title, body));
+            Ok(())
+        }
+
+        /// Blog owner can lock posts, related to a given blog,
+        /// making post immutable to any actions (replies creation, post editing, etc.)
+        ///
+        /// <weight>
+        ///
+        /// ## Weight
+        /// `O (1)` doesn't depends on the state or parameters
+        /// - DB:
+        ///    - O(1) doesn't depend on the state or parameters
+        /// # </weight>
+        #[weight = T::WeightInfo::lock_post()]
+        pub fn lock_post(origin, post_id: PostId) -> DispatchResult {
+
+            // Ensure blog -> owner relation exists
+            Self::ensure_blog_ownership(origin)?;
+
+            // Ensure post with given id exists
+            Self::ensure_post_exists(post_id)?;
+
+            //
+            // == MUTATION SAFE ==
+            //
+
+            // Update post lock status, associated with given id
+            <PostById<T, I>>::mutate(post_id, |inner_post| inner_post.lock());
+
+            // Trigger event
+            Self::deposit_event(RawEvent::PostLocked(post_id));
+            Ok(())
+        }
+
+        /// Blog owner can unlock posts, related to a given blog,
+        /// making post accesible to previously forbidden actions
+        ///
+        /// <weight>
+        ///
+        /// ## Weight
+        /// `O (1)` doesn't depends on the state or parameters
+        /// - DB:
+        ///    - O(1) doesn't depend on the state or parameters
+        /// # </weight>
+        #[weight = T::WeightInfo::unlock_post()]
+        pub fn unlock_post(origin, post_id: PostId) -> DispatchResult {
+
+            // Ensure blog -> owner relation exists
+            Self::ensure_blog_ownership(origin)?;
+
+            // Ensure post with given id exists
+            Self::ensure_post_exists(post_id)?;
+
+            //
+            // == MUTATION SAFE ==
+            //
+
+            // Update post lock status, associated with given id
+            <PostById<T, I>>::mutate(post_id, |inner_post| inner_post.unlock());
+
+            // Trigger event
+            Self::deposit_event(RawEvent::PostUnlocked(post_id));
+            Ok(())
+        }
+
+        /// Blog owner can edit post, related to a given blog (if unlocked)
+        /// with a new title and/or body
+        /// <weight>
+        ///
+        /// ## Weight
+        /// `O (T + B)` where:
+        /// - `T` is the length of the `new_title`
+        /// - `B` is the length of the `new_body`
+        /// - DB:
+        ///    - O(1) doesn't depend on the state or parameters
+        /// # </weight>
+        #[weight = Module::<T, I>::edit_post_weight(&new_title, &new_body)]
+        pub fn edit_post(
+            origin,
+            post_id: PostId,
+            new_title: Option<Vec<u8>>,
+            new_body: Option<Vec<u8>>
+        ) -> DispatchResult {
+            // Ensure blog -> owner relation exists
+            Self::ensure_blog_ownership(origin)?;
+
+            // Ensure post with given id exists
+            let post = Self::ensure_post_exists(post_id)?;
+
+            // Ensure post unlocked, so mutations can be performed
+            Self::ensure_post_unlocked(&post)?;
+
+            // == MUTATION SAFE ==
+            //
+
+            // Update post with new text
+            <PostById<T, I>>::mutate(
+                post_id,
+                |inner_post| inner_post.update(&new_title, &new_body)
+            );
+
+            // Trigger event
+            Self::deposit_event(RawEvent::PostEdited(post_id, new_title, new_body));
+            Ok(())
+        }
+
+        /// Create either root post reply or direct reply to reply
+        /// (Only accessible, if related blog and post are unlocked)
+        /// <weight>
+        ///
+        /// ## Weight
+        /// `O (T)` where:
+        /// - `T` is the length of the `text`
+        /// - DB:
+        ///    - O(1) doesn't depend on the state or parameters
+        /// # </weight>
+        #[weight = Module::<T, I>::create_reply_weight(text.len())]
+        pub fn create_reply(
+            origin,
+            participant_id: ParticipantId<T>,
+            post_id: PostId,
+            reply_id: Option<T::ReplyId>,
+            text: Vec<u8>
+        ) -> DispatchResult {
+            Self::ensure_valid_participant(origin, participant_id)?;
+
+            // Ensure post with given id exists
+            let post = Self::ensure_post_exists(post_id)?;
+
+            // Ensure post unlocked, so mutations can be performed
+            Self::ensure_post_unlocked(&post)?;
+
+            // Ensure root replies limit not reached
+            Self::ensure_replies_limit_not_reached(&post)?;
+
+            // New reply creation
+            let reply = if let Some(reply_id) = reply_id {
+                // Check parent reply existance in case of direct reply
+                Self::ensure_reply_exists(post_id, reply_id)?;
+                Reply::<T, I>::new(text.clone(), participant_id, ParentId::Reply(reply_id))
+            } else {
+                Reply::<T, I>::new(text.clone(), participant_id, ParentId::Post(post_id))
+            };
+
+            //
+            // == MUTATION SAFE ==
+            //
+
+            // Update runtime storage with new reply
+            let post_replies_count = post.replies_count();
+            <ReplyById<T, I>>::insert(post_id, post_replies_count, reply);
+
+            // Increment replies counter, associated with given post
+            <PostById<T, I>>::mutate(post_id, |inner_post| inner_post.increment_replies_counter());
+
+            if let Some(reply_id) = reply_id {
+                // Trigger event
+                Self::deposit_event(RawEvent::DirectReplyCreated(participant_id, post_id, reply_id, post_replies_count, text));
+            } else {
+                // Trigger event
+                Self::deposit_event(RawEvent::ReplyCreated(participant_id, post_id, post_replies_count, text));
+            }
+            Ok(())
+        }
+
+        /// Reply owner can edit reply with a new text
+        /// (Only accessible, if related blog and post are unlocked)
+        ///
+        /// <weight>
+        ///
+        /// ## Weight
+        /// `O (T)` where:
+        /// - `T` is the length of the `new_text`
+        /// - DB:
+        ///    - O(1) doesn't depend on the state or parameters
+        /// # </weight>
+        #[weight = T::WeightInfo::edit_reply(new_text.len().saturated_into())]
+        pub fn edit_reply(
+            origin,
+            participant_id: ParticipantId<T>,
+            post_id: PostId,
+            reply_id: T::ReplyId,
+            new_text: Vec<u8>
+        ) -> DispatchResult {
+            Self::ensure_valid_participant(origin, participant_id)?;
+
+            // Ensure post with given id exists
+            let post = Self::ensure_post_exists(post_id)?;
+
+            // Ensure post unlocked, so mutations can be performed
+            Self::ensure_post_unlocked(&post)?;
+
+            // Ensure reply with given id exists
+            let reply = Self::ensure_reply_exists(post_id, reply_id)?;
+
+            // Ensure reply -> owner relation exists
+            Self::ensure_reply_ownership(&reply, &participant_id)?;
+
+            //
+            // == MUTATION SAFE ==
+            //
+
+            // Update reply with new text
+            <ReplyById<T, I>>::mutate(
+                post_id,
+                reply_id,
+                |inner_reply| inner_reply.update(new_text.clone())
+            );
+
+            // Trigger event
+            Self::deposit_event(RawEvent::ReplyEdited(participant_id, post_id, reply_id, new_text));
+            Ok(())
+        }
+
+    }
+}
+
+impl<T: Trait<I>, I: Instance> Module<T, I> {
+    // edit_post_weight
+    fn edit_post_weight(title: &Option<Vec<u8>>, body: &Option<Vec<u8>>) -> Weight {
+        let title_len: u32 = title.as_ref().map_or(0, |t| t.len().saturated_into());
+        let body_len: u32 = body.as_ref().map_or(0, |b| b.len().saturated_into());
+
+        T::WeightInfo::edit_post(title_len, body_len)
+    }
+
+    // calculate create_reply weight
+    fn create_reply_weight(text_len: usize) -> Weight {
+        let text_len: u32 = text_len.saturated_into();
+        T::WeightInfo::create_reply_to_post(text_len)
+            .max(T::WeightInfo::create_reply_to_reply(text_len))
+    }
+
+    // Get participant id from origin
+    fn ensure_valid_participant(
+        origin: T::Origin,
+        participant_id: ParticipantId<T>,
+    ) -> Result<(), DispatchError> {
+        let account_id = frame_system::ensure_signed(origin)?;
+        ensure!(
+            T::ParticipantEnsureOrigin::is_member_controller_account(&participant_id, &account_id),
+            Error::<T, I>::MembershipError
+        );
+        Ok(())
+    }
+
+    fn ensure_post_exists(post_id: PostId) -> Result<Post<T, I>, DispatchError> {
+        ensure!(
+            <PostById<T, I>>::contains_key(post_id),
+            Error::<T, I>::PostNotFound
+        );
+        Ok(Self::post_by_id(post_id))
+    }
+
+    fn ensure_reply_exists(
+        post_id: PostId,
+        reply_id: T::ReplyId,
+    ) -> Result<Reply<T, I>, DispatchError> {
+        ensure!(
+            <ReplyById<T, I>>::contains_key(post_id, reply_id),
+            Error::<T, I>::ReplyNotFound
+        );
+        Ok(Self::reply_by_id(post_id, reply_id))
+    }
+
+    fn ensure_blog_ownership(blog_owner: T::Origin) -> Result<(), DispatchError> {
+        ensure!(
+            frame_system::ensure_root(blog_owner).is_ok(),
+            Error::<T, I>::BlogOwnershipError
+        );
+
+        Ok(())
+    }
+
+    fn ensure_reply_ownership(
+        reply: &Reply<T, I>,
+        reply_owner: &ParticipantId<T>,
+    ) -> Result<(), DispatchError> {
+        ensure!(
+            reply.is_owner(reply_owner),
+            Error::<T, I>::ReplyOwnershipError
+        );
+        Ok(())
+    }
+
+    fn ensure_post_unlocked(post: &Post<T, I>) -> Result<(), DispatchError> {
+        ensure!(!post.is_locked(), Error::<T, I>::PostLockedError);
+        Ok(())
+    }
+
+    fn ensure_posts_limit_not_reached() -> Result<PostId, DispatchError> {
+        // Get posts count, associated with given blog
+        let posts_count = Self::post_count();
+
+        ensure!(
+            posts_count < T::PostsMaxNumber::get(),
+            Error::<T, I>::PostLimitReached
+        );
+
+        Ok(posts_count)
+    }
+
+    fn ensure_replies_limit_not_reached(post: &Post<T, I>) -> Result<(), DispatchError> {
+        // Get replies count, associated with given post
+        let root_replies_count = post.replies_count();
+
+        ensure!(
+            root_replies_count < T::RepliesMaxNumber::get().into(),
+            Error::<T, I>::RepliesLimitReached
+        );
+
+        Ok(())
+    }
+}
+
+decl_event!(
+    pub enum Event<T, I = DefaultInstance>
+    where
+        ParticipantId = ParticipantId<T>,
+        PostId = PostId,
+        ReplyId = <T as Trait<I>>::ReplyId,
+        Title = Vec<u8>,
+        Text = Vec<u8>,
+        UpdatedTitle = Option<Vec<u8>>,
+        UpdatedBody = Option<Vec<u8>>,
+    {
+        /// A post was created
+        PostCreated(PostId, Title, Text),
+
+        /// A post was locked
+        PostLocked(PostId),
+
+        /// A post was unlocked
+        PostUnlocked(PostId),
+
+        /// A post was edited
+        PostEdited(PostId, UpdatedTitle, UpdatedBody),
+
+        /// A reply to a post was created
+        ReplyCreated(ParticipantId, PostId, ReplyId, Text),
+
+        /// A reply to a reply was created
+        DirectReplyCreated(ParticipantId, PostId, ReplyId, ReplyId, Text),
+
+        /// A reply was edited
+        ReplyEdited(ParticipantId, PostId, ReplyId, Text),
+    }
+);

+ 431 - 0
runtime-modules/blog/src/mock.rs

@@ -0,0 +1,431 @@
+#![cfg(test)]
+
+use crate::*;
+use frame_support::traits::{LockIdentifier, OnFinalize, OnInitialize};
+use frame_support::weights::Weight;
+use frame_support::{impl_outer_event, impl_outer_origin, parameter_types};
+use sp_core::H256;
+use sp_io::TestExternalities;
+use sp_runtime::{
+    testing::Header,
+    traits::{BlakeTwo256, IdentityLookup},
+    DispatchResult, Perbill,
+};
+
+pub(crate) const FIRST_OWNER_ORIGIN: u64 = 0;
+pub(crate) const FIRST_OWNER_PARTICIPANT_ID: u64 = 0;
+pub(crate) const SECOND_OWNER_ORIGIN: u64 = 2;
+pub(crate) const SECOND_OWNER_PARTICIPANT_ID: u64 = 2;
+pub(crate) const BAD_MEMBER_ID: u64 = 100000;
+
+impl_outer_origin! {
+    pub enum Origin for Runtime {}
+}
+
+#[derive(Clone, Default, PartialEq, Eq, Debug)]
+pub struct Runtime;
+
+parameter_types! {
+    pub const ExistentialDeposit: u32 = 0;
+}
+
+impl balances::Trait for Runtime {
+    type Balance = u64;
+    type DustRemoval = ();
+    type Event = TestEvent;
+    type ExistentialDeposit = ExistentialDeposit;
+    type AccountStore = System;
+    type WeightInfo = ();
+    type MaxLocks = ();
+}
+
+parameter_types! {
+    pub const BlockHashCount: u64 = 250;
+    pub const MaximumBlockWeight: u32 = 1024;
+    pub const MaximumBlockLength: u32 = 2 * 1024;
+    pub const AvailableBlockRatio: Perbill = Perbill::one();
+}
+
+// First, implement the system pallet's configuration trait for `Runtime`
+impl frame_system::Trait for Runtime {
+    type BaseCallFilter = ();
+    type Origin = Origin;
+    type Call = ();
+    type Index = u64;
+    type BlockNumber = u64;
+    type Hash = H256;
+    type Hashing = BlakeTwo256;
+    type AccountId = u64;
+    type Lookup = IdentityLookup<Self::AccountId>;
+    type Header = Header;
+    type Event = TestEvent;
+    type BlockHashCount = BlockHashCount;
+    type MaximumBlockWeight = MaximumBlockWeight;
+    type DbWeight = ();
+    type BlockExecutionWeight = ();
+    type ExtrinsicBaseWeight = ();
+    type MaximumExtrinsicWeight = ();
+    type MaximumBlockLength = MaximumBlockLength;
+    type AvailableBlockRatio = AvailableBlockRatio;
+    type Version = ();
+    type PalletInfo = ();
+    type AccountData = balances::AccountData<u64>;
+    type OnNewAccount = ();
+    type OnKilledAccount = ();
+    type SystemWeightInfo = ();
+}
+
+impl_outer_event! {
+    pub enum TestEvent for Runtime {
+        crate DefaultInstance <T>,
+        frame_system<T>,
+        balances<T>,
+        membership<T>,
+    }
+}
+
+parameter_types! {
+    pub const DefaultMembershipPrice: u64 = 100;
+    pub const DefaultInitialInvitationBalance: u64 = 100;
+    pub const InviteMemberLockId: [u8; 8] = [9; 8];
+    pub const MinimumPeriod: u64 = 5;
+}
+
+impl membership::Trait for Runtime {
+    type Event = TestEvent;
+    type DefaultMembershipPrice = DefaultMembershipPrice;
+    type DefaultInitialInvitationBalance = DefaultInitialInvitationBalance;
+    type WorkingGroup = ();
+    type WeightInfo = Weights;
+    type InvitedMemberStakingHandler = staking_handler::StakingManager<Self, InviteMemberLockId>;
+}
+
+impl pallet_timestamp::Trait for Runtime {
+    type Moment = u64;
+    type OnTimestampSet = ();
+    type MinimumPeriod = MinimumPeriod;
+    type WeightInfo = ();
+}
+
+impl staking_handler::LockComparator<u64> for Runtime {
+    fn are_locks_conflicting(
+        _new_lock: &LockIdentifier,
+        _existing_locks: &[LockIdentifier],
+    ) -> bool {
+        false
+    }
+}
+
+impl common::working_group::WorkingGroupBudgetHandler<Runtime> for () {
+    fn get_budget() -> u64 {
+        unimplemented!();
+    }
+
+    fn set_budget(_: u64) {
+        unimplemented!()
+    }
+}
+
+impl common::working_group::WorkingGroupAuthenticator<Runtime> for () {
+    fn ensure_worker_origin(
+        _origin: <Runtime as frame_system::Trait>::Origin,
+        _worker_id: &<Runtime as common::Trait>::ActorId,
+    ) -> DispatchResult {
+        unimplemented!()
+    }
+
+    fn ensure_leader_origin(_origin: <Runtime as frame_system::Trait>::Origin) -> DispatchResult {
+        unimplemented!()
+    }
+
+    fn get_leader_member_id() -> Option<<Runtime as common::Trait>::MemberId> {
+        unimplemented!()
+    }
+
+    fn is_leader_account_id(_: &<Runtime as frame_system::Trait>::AccountId) -> bool {
+        unimplemented!();
+    }
+
+    fn is_worker_account_id(
+        _: &<Runtime as frame_system::Trait>::AccountId,
+        _worker_id: &<Runtime as common::Trait>::ActorId,
+    ) -> bool {
+        unimplemented!();
+    }
+}
+
+pub struct Weights;
+impl membership::WeightInfo for Weights {
+    fn buy_membership_without_referrer(_: u32, _: u32, _: u32, _: u32) -> Weight {
+        unimplemented!()
+    }
+    fn buy_membership_with_referrer(_: u32, _: u32, _: u32, _: u32) -> Weight {
+        unimplemented!()
+    }
+    fn update_profile(_: u32) -> Weight {
+        unimplemented!()
+    }
+    fn update_accounts_none() -> Weight {
+        unimplemented!()
+    }
+    fn update_accounts_root() -> Weight {
+        unimplemented!()
+    }
+    fn update_accounts_controller() -> Weight {
+        unimplemented!()
+    }
+    fn update_accounts_both() -> Weight {
+        unimplemented!()
+    }
+    fn set_referral_cut() -> Weight {
+        unimplemented!()
+    }
+    fn transfer_invites() -> Weight {
+        unimplemented!()
+    }
+    fn invite_member(_: u32, _: u32, _: u32, _: u32) -> Weight {
+        unimplemented!()
+    }
+    fn set_membership_price() -> Weight {
+        unimplemented!()
+    }
+    fn update_profile_verification() -> Weight {
+        unimplemented!()
+    }
+    fn set_leader_invitation_quota() -> Weight {
+        unimplemented!()
+    }
+    fn set_initial_invitation_balance() -> Weight {
+        unimplemented!()
+    }
+    fn set_initial_invitation_count() -> Weight {
+        unimplemented!()
+    }
+    fn add_staking_account_candidate() -> Weight {
+        unimplemented!()
+    }
+    fn confirm_staking_account() -> Weight {
+        unimplemented!()
+    }
+    fn remove_staking_account() -> Weight {
+        unimplemented!()
+    }
+}
+
+parameter_types! {
+    pub const PostsMaxNumber: u64 = 20;
+    pub const RepliesMaxNumber: u64 = 100;
+}
+
+impl Trait for Runtime {
+    type Event = TestEvent;
+
+    type PostsMaxNumber = PostsMaxNumber;
+    type RepliesMaxNumber = RepliesMaxNumber;
+    type ParticipantEnsureOrigin = MockEnsureParticipant;
+    type WeightInfo = ();
+
+    type ReplyId = u64;
+}
+
+impl WeightInfo for () {
+    fn create_post(_: u32, _: u32) -> Weight {
+        unimplemented!()
+    }
+    fn lock_post() -> Weight {
+        unimplemented!()
+    }
+    fn unlock_post() -> Weight {
+        unimplemented!()
+    }
+    fn edit_post(_: u32, _: u32) -> Weight {
+        unimplemented!()
+    }
+    fn create_reply_to_post(_: u32) -> Weight {
+        unimplemented!()
+    }
+    fn create_reply_to_reply(_: u32) -> Weight {
+        unimplemented!()
+    }
+    fn edit_reply(_: u32) -> Weight {
+        unimplemented!()
+    }
+}
+
+pub struct MockEnsureParticipant;
+impl
+    MemberOriginValidator<
+        Origin,
+        ParticipantId<Runtime>,
+        <Runtime as frame_system::Trait>::AccountId,
+    > for MockEnsureParticipant
+{
+    fn is_member_controller_account(
+        member_id: &ParticipantId<Runtime>,
+        _: &<Runtime as frame_system::Trait>::AccountId,
+    ) -> bool {
+        *member_id != BAD_MEMBER_ID
+    }
+
+    fn ensure_member_controller_account_origin(
+        _: Origin,
+        _: ParticipantId<Runtime>,
+    ) -> Result<<Runtime as frame_system::Trait>::AccountId, DispatchError> {
+        unimplemented!();
+    }
+}
+
+impl common::Trait for Runtime {
+    type MemberId = u64;
+    type ActorId = u64;
+}
+
+#[derive(Default)]
+pub struct ExtBuilder;
+
+impl ExtBuilder {
+    fn run_to_block(n: u64) {
+        while System::block_number() < n {
+            <System as OnFinalize<u64>>::on_finalize(System::block_number());
+            <crate::Module<Runtime> as OnFinalize<u64>>::on_finalize(System::block_number());
+            System::set_block_number(System::block_number() + 1);
+            <System as OnInitialize<u64>>::on_initialize(System::block_number());
+            <crate::Module<Runtime> as OnInitialize<u64>>::on_initialize(System::block_number());
+        }
+    }
+
+    pub fn build(self) -> TestExternalities {
+        let t = frame_system::GenesisConfig::default()
+            .build_storage::<Runtime>()
+            .unwrap();
+
+        let mut result: TestExternalities = t.into();
+
+        // Make sure we are not in block 0 where no events are emitted - see https://substrate.dev/recipes/2-appetizers/4-events.html#emitting-events
+        result.execute_with(|| Self::run_to_block(1));
+
+        result
+    }
+}
+
+// Assign back to type variables so we can make dispatched calls of these modules later.
+pub type System = frame_system::Module<Runtime>;
+pub type TestBlogModule = Module<Runtime>;
+
+pub fn generate_text(len: usize) -> Vec<u8> {
+    vec![b'x'; len]
+}
+
+type RawTestEvent = RawEvent<
+    ParticipantId<Runtime>,
+    PostId,
+    <Runtime as Trait>::ReplyId,
+    Vec<u8>,
+    Vec<u8>,
+    Option<Vec<u8>>,
+    Option<Vec<u8>>,
+    DefaultInstance,
+>;
+
+pub fn get_test_event(raw_event: RawTestEvent) -> TestEvent {
+    TestEvent::crate_DefaultInstance(raw_event)
+}
+
+// Posts
+pub fn post_count() -> u64 {
+    TestBlogModule::post_count()
+}
+
+pub fn post_by_id(post_id: PostId) -> Option<Post<Runtime, DefaultInstance>> {
+    match TestBlogModule::post_by_id(post_id) {
+        post if post != Post::<Runtime, DefaultInstance>::default() => Some(post),
+        _ => None,
+    }
+}
+
+pub fn get_post(locked: bool) -> Post<Runtime, DefaultInstance> {
+    let title = generate_text(10);
+    let body = generate_text(100);
+    let mut post = Post::new(&title, &body);
+    if locked {
+        post.lock()
+    }
+    post
+}
+
+pub(crate) fn generate_post() -> (Vec<u8>, Vec<u8>) {
+    (generate_text(10), generate_text(100))
+}
+
+pub fn create_post(origin: Origin) -> DispatchResult {
+    let (title, body) = generate_post();
+    TestBlogModule::create_post(origin, title, body)
+}
+
+pub fn lock_post(origin: Origin, post_id: PostId) -> DispatchResult {
+    TestBlogModule::lock_post(origin, post_id)
+}
+
+pub fn unlock_post(origin: Origin, post_id: PostId) -> DispatchResult {
+    TestBlogModule::unlock_post(origin, post_id)
+}
+
+pub fn edit_post(origin: Origin, post_id: PostId) -> DispatchResult {
+    let (title, body) = generate_post();
+    TestBlogModule::edit_post(origin, post_id, Some(title), Some(body))
+}
+
+// Replies
+pub fn reply_by_id(
+    post_id: PostId,
+    reply_id: <Runtime as Trait>::ReplyId,
+) -> Option<Reply<Runtime, DefaultInstance>> {
+    match TestBlogModule::reply_by_id(post_id, reply_id) {
+        reply if reply != Reply::<Runtime, DefaultInstance>::default() => Some(reply),
+        _ => None,
+    }
+}
+
+pub fn get_reply_text() -> Vec<u8> {
+    generate_text(100)
+}
+
+pub fn get_reply(
+    owner: <Runtime as frame_system::Trait>::AccountId,
+    parent_id: ParentId<<Runtime as Trait>::ReplyId, PostId>,
+) -> Reply<Runtime, DefaultInstance> {
+    let reply_text = get_reply_text();
+    Reply::new(reply_text, owner, parent_id)
+}
+
+pub fn create_reply(
+    origin_id: u64,
+    participant_id: u64,
+    post_id: PostId,
+    reply_id: Option<<Runtime as Trait>::ReplyId>,
+) -> DispatchResult {
+    let reply = get_reply_text();
+    TestBlogModule::create_reply(
+        Origin::signed(origin_id),
+        participant_id,
+        post_id,
+        reply_id,
+        reply,
+    )
+}
+
+pub fn edit_reply(
+    origin_id: u64,
+    participant_id: u64,
+    post_id: PostId,
+    reply_id: <Runtime as Trait>::ReplyId,
+) -> DispatchResult {
+    let reply = get_reply_text();
+    TestBlogModule::edit_reply(
+        Origin::signed(origin_id),
+        participant_id,
+        post_id,
+        reply_id,
+        reply,
+    )
+}

+ 829 - 0
runtime-modules/blog/src/tests.rs

@@ -0,0 +1,829 @@
+#![cfg(test)]
+
+use crate::mock::*;
+use crate::*;
+use frame_support::assert_ok;
+use frame_system::ensure_signed;
+
+//Blog, post or reply id
+const FIRST_ID: u64 = 0;
+const SECOND_ID: u64 = 1;
+
+fn assert_event_success(tested_event: TestEvent, number_of_events_after_call: usize) {
+    // Ensure  runtime events length is equal to expected number of events after call
+    assert_eq!(System::events().len(), number_of_events_after_call);
+
+    // Ensure  last emitted event is equal to expected one
+    assert!(matches!(
+            System::events()
+                .iter()
+                .last(),
+            Some(last_event) if last_event.event == tested_event
+    ));
+}
+
+fn assert_failure(
+    call_result: DispatchResult,
+    expected_error: errors::Error<Runtime, DefaultInstance>,
+    number_of_events_before_call: usize,
+) {
+    // Ensure  call result is equal to expected error
+    assert_eq!(
+        call_result,
+        sp_std::result::Result::Err(expected_error.into())
+    );
+
+    // Ensure  no other events emitted after call
+    assert_eq!(System::events().len(), number_of_events_before_call);
+}
+
+fn ensure_replies_equality(
+    reply: Option<Reply<Runtime, DefaultInstance>>,
+    reply_owner_id: <Runtime as frame_system::Trait>::AccountId,
+    parent: ParentId<<Runtime as Trait>::ReplyId, PostId>,
+) {
+    // Ensure  stored reply is equal to expected one
+    assert!(matches!(
+        reply,
+        Some(reply) if reply == get_reply(reply_owner_id, parent)
+    ));
+}
+
+fn ensure_posts_equality(post: Option<Post<Runtime, DefaultInstance>>, locked: bool) {
+    // Ensure  stored post is equal to expected one
+    assert!(matches!(
+        post,
+        Some(post) if post == get_post(locked)
+    ));
+}
+
+// Posts
+#[test]
+fn post_creation_success() {
+    ExtBuilder::default().build().execute_with(|| {
+        // Events number before tested call
+        let number_of_events_before_call = System::events().len();
+
+        // Create post
+        assert_ok!(create_post(Origin::root()));
+
+        // Check related state after extrinsic performed
+
+        // Posts storage updated succesfully
+        let post = post_by_id(FIRST_ID);
+
+        ensure_posts_equality(post, false);
+
+        // Post counter, related to given blog updated succesfully
+        assert_eq!(post_count(), 1);
+
+        // Event checked
+        let post_created_event = get_test_event(RawEvent::PostCreated(
+            FIRST_ID,
+            generate_post().0,
+            generate_post().1,
+        ));
+        assert_event_success(post_created_event, number_of_events_before_call + 1)
+    })
+}
+
+#[test]
+fn post_creation_blog_ownership_error() {
+    ExtBuilder::default().build().execute_with(|| {
+        // Events number before tested call
+        let number_of_events_before_call = System::events().len();
+
+        let create_result = create_post(Origin::signed(SECOND_OWNER_ORIGIN));
+
+        // Check if related runtime storage left unchanged
+        // assert!(post_storage_unchanged(FIRST_ID, FIRST_ID));
+
+        // Failure checked
+        assert_failure(
+            create_result,
+            Error::BlogOwnershipError,
+            number_of_events_before_call,
+        );
+    })
+}
+
+#[test]
+fn post_creation_limit_reached() {
+    ExtBuilder::default().build().execute_with(|| {
+        loop {
+            // Events number before tested call
+            let number_of_events_before_call = System::events().len();
+
+            if let Err(create_post_err) = create_post(Origin::root()) {
+                // Post counter & post max number contraint equality checked
+                assert_eq!(post_count(), PostsMaxNumber::get());
+
+                // Last post creation, before limit reached, failure checked
+                assert_failure(
+                    Err(create_post_err),
+                    Error::PostLimitReached,
+                    number_of_events_before_call,
+                );
+                break;
+            }
+        }
+    })
+}
+
+#[test]
+fn post_locking_success() {
+    ExtBuilder::default().build().execute_with(|| {
+        create_post(Origin::root()).unwrap();
+
+        let post = post_by_id(FIRST_ID).unwrap();
+
+        // Check default post locking status right after creation
+        assert_eq!(post.is_locked(), false);
+
+        // Events number before tested call
+        let number_of_events_before_call = System::events().len();
+
+        assert_ok!(lock_post(Origin::root(), FIRST_ID));
+
+        // Check related state after extrinsic performed
+
+        let post = post_by_id(FIRST_ID).unwrap();
+
+        assert_eq!(post.is_locked(), true);
+
+        let post_locked_event = get_test_event(RawEvent::PostLocked(FIRST_ID));
+
+        // Event checked
+        assert_event_success(post_locked_event, number_of_events_before_call + 1)
+    })
+}
+
+#[test]
+fn post_locking_post_not_found() {
+    ExtBuilder::default().build().execute_with(|| {
+        // Events number before tested call
+        let number_of_events_before_call = System::events().len();
+
+        let lock_result = lock_post(Origin::root(), FIRST_ID);
+
+        // Failure checked
+        assert_failure(
+            lock_result,
+            Error::PostNotFound,
+            number_of_events_before_call,
+        );
+    })
+}
+
+#[test]
+fn post_locking_ownership_error() {
+    ExtBuilder::default().build().execute_with(|| {
+        create_post(Origin::root()).unwrap();
+
+        // Events number before tested call
+        let number_of_events_before_call = System::events().len();
+
+        let lock_result = lock_post(Origin::signed(SECOND_OWNER_ORIGIN), FIRST_ID);
+
+        // Check related state after extrinsic performed
+
+        let post = post_by_id(FIRST_ID).unwrap();
+
+        // Remain unlocked
+        assert_eq!(post.is_locked(), false);
+
+        // Failure checked
+        assert_failure(
+            lock_result,
+            Error::BlogOwnershipError,
+            number_of_events_before_call,
+        );
+    })
+}
+
+#[test]
+fn post_unlocking_success() {
+    ExtBuilder::default().build().execute_with(|| {
+        create_post(Origin::root()).unwrap();
+
+        // Lock post firstly
+        lock_post(Origin::root(), FIRST_ID).unwrap();
+
+        // Check related state before extrinsic performed
+        let post = post_by_id(FIRST_ID).unwrap();
+
+        assert_eq!(post.is_locked(), true);
+
+        // Events number before tested call
+        let number_of_events_before_call = System::events().len();
+
+        assert_ok!(unlock_post(Origin::root(), FIRST_ID));
+
+        // Check related state after extrinsic performed
+        let post = post_by_id(FIRST_ID).unwrap();
+
+        assert_eq!(post.is_locked(), false);
+
+        let post_unlocked_event = get_test_event(RawEvent::PostUnlocked(FIRST_ID));
+
+        // Event checked
+        assert_event_success(post_unlocked_event, number_of_events_before_call + 1)
+    })
+}
+
+#[test]
+fn post_unlocking_owner_not_found() {
+    ExtBuilder::default().build().execute_with(|| {
+        create_post(Origin::root()).unwrap();
+
+        // Lock post firstly
+        lock_post(Origin::root(), FIRST_ID).unwrap();
+
+        // Events number before tested call
+        let number_of_events_before_call = System::events().len();
+
+        let unlock_result = unlock_post(Origin::signed(SECOND_OWNER_ORIGIN), FIRST_ID);
+
+        // Check related state after extrinsic performed
+        let post = post_by_id(FIRST_ID).unwrap();
+
+        // Remain locked
+        assert_eq!(post.is_locked(), true);
+
+        // Failure checked
+        assert_failure(
+            unlock_result,
+            Error::BlogOwnershipError,
+            number_of_events_before_call,
+        );
+    })
+}
+
+#[test]
+fn post_unlocking_post_not_found() {
+    ExtBuilder::default().build().execute_with(|| {
+        // Events number before tested call
+        let number_of_events_before_call = System::events().len();
+
+        // Try to unlock not existing post
+        let unlock_result = unlock_post(Origin::root(), FIRST_ID);
+
+        // Failure checked
+        assert_failure(
+            unlock_result,
+            Error::PostNotFound,
+            number_of_events_before_call,
+        );
+    })
+}
+
+#[test]
+fn post_unlocking_ownership_error() {
+    ExtBuilder::default().build().execute_with(|| {
+        create_post(Origin::root()).unwrap();
+
+        // Lock post firstly
+        lock_post(Origin::root(), FIRST_ID).unwrap();
+
+        // Events number before tested call
+        let number_of_events_before_call = System::events().len();
+
+        let unlock_result = unlock_post(Origin::signed(SECOND_OWNER_ORIGIN), FIRST_ID);
+
+        // Check related state after extrinsic performed
+        let post = post_by_id(FIRST_ID).unwrap();
+
+        // Remain locked
+        assert_eq!(post.is_locked(), true);
+
+        // Failure checked
+        assert_failure(
+            unlock_result,
+            Error::BlogOwnershipError,
+            number_of_events_before_call,
+        );
+    })
+}
+
+#[test]
+fn post_editing_success() {
+    ExtBuilder::default().build().execute_with(|| {
+        // Create blog for future posts
+        create_post(Origin::root()).unwrap();
+
+        // Events number before tested call
+        let number_of_events_before_call = System::events().len();
+
+        assert_ok!(edit_post(Origin::root(), FIRST_ID));
+
+        // Post after editing checked
+        let post_after_editing = post_by_id(FIRST_ID);
+
+        ensure_posts_equality(post_after_editing, false);
+
+        let post_edited_event = TestEvent::crate_DefaultInstance(RawEvent::PostEdited(
+            FIRST_ID,
+            Some(generate_post().0),
+            Some(generate_post().1),
+        ));
+
+        // Event checked
+        assert_event_success(post_edited_event, number_of_events_before_call + 1)
+    })
+}
+
+#[test]
+fn post_editing_ownership_error() {
+    ExtBuilder::default().build().execute_with(|| {
+        create_post(Origin::root()).unwrap();
+
+        // Events number before tested call
+        let number_of_events_before_call = System::events().len();
+
+        let edit_result = edit_post(Origin::signed(SECOND_OWNER_ORIGIN), FIRST_ID);
+
+        // Remain unedited
+        let post = post_by_id(FIRST_ID);
+
+        // Compare with default unedited post
+        ensure_posts_equality(post, false);
+
+        // Failure checked
+        assert_failure(
+            edit_result,
+            Error::BlogOwnershipError,
+            number_of_events_before_call,
+        );
+    })
+}
+
+#[test]
+fn post_editing_post_not_found() {
+    ExtBuilder::default().build().execute_with(|| {
+        // Events number before tested call
+        let number_of_events_before_call = System::events().len();
+
+        // Try to unlock not existing post
+        let edit_result = edit_post(Origin::root(), FIRST_ID);
+
+        // Failure checked
+        assert_failure(
+            edit_result,
+            Error::PostNotFound,
+            number_of_events_before_call,
+        );
+    })
+}
+
+#[test]
+fn post_editing_post_locked_error() {
+    ExtBuilder::default().build().execute_with(|| {
+        create_post(Origin::root()).unwrap();
+
+        // Lock post to make all related data immutable
+        lock_post(Origin::root(), FIRST_ID).unwrap();
+
+        // Events number before tested call
+        let number_of_events_before_call = System::events().len();
+
+        let edit_result = edit_post(Origin::root(), FIRST_ID);
+
+        // Remain unedited
+        let post = post_by_id(FIRST_ID);
+
+        // Compare with default unedited locked post
+        ensure_posts_equality(post, true);
+
+        // Failure checked
+        assert_failure(
+            edit_result,
+            Error::PostLockedError,
+            number_of_events_before_call,
+        );
+    })
+}
+
+// Replies
+#[test]
+fn reply_creation_success() {
+    ExtBuilder::default().build().execute_with(|| {
+        // Create post for future replies
+        create_post(Origin::root()).unwrap();
+
+        let reply_owner_id = ensure_signed(Origin::signed(SECOND_OWNER_ORIGIN)).unwrap();
+
+        // Events number before tested call
+        let number_of_events_before_call = System::events().len();
+
+        assert_ok!(create_reply(
+            SECOND_OWNER_ORIGIN,
+            SECOND_OWNER_PARTICIPANT_ID,
+            FIRST_ID,
+            None
+        ));
+
+        // Check reply related state after extrinsic performed
+
+        let post = post_by_id(FIRST_ID).unwrap();
+
+        // Replies related storage updated succesfully
+        let reply = reply_by_id(FIRST_ID, FIRST_ID);
+
+        ensure_replies_equality(reply, reply_owner_id, ParentId::Post(FIRST_ID));
+
+        // Overall post replies count
+        assert_eq!(post.replies_count(), 1);
+
+        // Root replies counter updated
+        assert_eq!(post.replies_count(), 1);
+
+        // Event checked
+        let reply_created_event = get_test_event(RawEvent::ReplyCreated(
+            reply_owner_id,
+            FIRST_ID,
+            FIRST_ID,
+            get_reply_text(),
+        ));
+        assert_event_success(reply_created_event, number_of_events_before_call + 1)
+    })
+}
+
+#[test]
+fn direct_reply_creation_success() {
+    ExtBuilder::default().build().execute_with(|| {
+        // Create post for future replies
+        create_post(Origin::root()).unwrap();
+        let direct_reply_owner_id = ensure_signed(Origin::signed(SECOND_OWNER_ORIGIN)).unwrap();
+
+        assert_ok!(create_reply(
+            FIRST_OWNER_ORIGIN,
+            FIRST_OWNER_PARTICIPANT_ID,
+            FIRST_ID,
+            None
+        ));
+
+        // Events number before tested call
+        let number_of_events_before_call = System::events().len();
+
+        // Create reply for direct replying
+        assert_ok!(create_reply(
+            SECOND_OWNER_ORIGIN,
+            SECOND_OWNER_PARTICIPANT_ID,
+            FIRST_ID,
+            Some(FIRST_ID)
+        ));
+
+        // Check reply related state after extrinsic performed
+
+        let post = post_by_id(FIRST_ID).unwrap();
+
+        // Replies related storage updated succesfully
+        reply_by_id(FIRST_ID, FIRST_ID).expect("Reply not found");
+
+        // Overall post replies count
+        assert_eq!(post.replies_count(), 2);
+
+        // Event checked
+        let reply_created_event = get_test_event(RawEvent::DirectReplyCreated(
+            direct_reply_owner_id,
+            FIRST_ID,
+            FIRST_ID,
+            SECOND_ID,
+            get_reply_text(),
+        ));
+        assert_event_success(reply_created_event, number_of_events_before_call + 1)
+    })
+}
+
+#[test]
+fn reply_creation_post_locked_error() {
+    ExtBuilder::default().build().execute_with(|| {
+        create_post(Origin::root()).unwrap();
+
+        // Lock post to make all related data immutable
+        lock_post(Origin::root(), FIRST_ID).unwrap();
+
+        // Events number before tested call
+        let number_of_events_before_call = System::events().len();
+
+        let reply_creation_result = create_reply(
+            SECOND_OWNER_ORIGIN,
+            SECOND_OWNER_PARTICIPANT_ID,
+            FIRST_ID,
+            None,
+        );
+
+        // Check if related replies storage left unchanged
+        assert!(replies_storage_unchanged(FIRST_ID, FIRST_ID));
+
+        // Failure checked
+        assert_failure(
+            reply_creation_result,
+            Error::PostLockedError,
+            number_of_events_before_call,
+        );
+    })
+}
+
+#[test]
+fn reply_creation_post_not_found() {
+    ExtBuilder::default().build().execute_with(|| {
+        // Events number before tested call
+        let number_of_events_before_call = System::events().len();
+
+        let reply_creation_result = create_reply(
+            SECOND_OWNER_ORIGIN,
+            SECOND_OWNER_PARTICIPANT_ID,
+            FIRST_ID,
+            None,
+        );
+
+        // Check if related replies storage left unchanged
+        assert!(replies_storage_unchanged(FIRST_ID, FIRST_ID));
+
+        // Failure checked
+        assert_failure(
+            reply_creation_result,
+            Error::PostNotFound,
+            number_of_events_before_call,
+        );
+    })
+}
+
+#[test]
+fn reply_creation_limit_reached() {
+    ExtBuilder::default().build().execute_with(|| {
+        // Create post for future replies
+        create_post(Origin::root()).unwrap();
+        loop {
+            // Events number before tested call
+            let number_of_events_before_call = System::events().len();
+            if let Err(create_reply_err) = create_reply(
+                FIRST_OWNER_ORIGIN,
+                FIRST_OWNER_PARTICIPANT_ID,
+                FIRST_ID,
+                None,
+            ) {
+                let post = post_by_id(FIRST_ID).unwrap();
+
+                // Root post replies counter & reply root max number contraint equality checked
+                assert_eq!(post.replies_count(), RepliesMaxNumber::get());
+
+                // Last reply creation, before limit reached, failure checked
+                assert_failure(
+                    Err(create_reply_err),
+                    Error::RepliesLimitReached,
+                    number_of_events_before_call,
+                );
+                break;
+            }
+        }
+    })
+}
+
+#[test]
+fn direct_reply_creation_reply_not_found() {
+    ExtBuilder::default().build().execute_with(|| {
+        // Create post for future replies
+        create_post(Origin::root()).unwrap();
+
+        // Events number before tested call
+        let number_of_events_before_call = System::events().len();
+
+        // Attempt to create direct reply for nonexistent reply
+        let reply_creation_result = create_reply(
+            SECOND_OWNER_ORIGIN,
+            SECOND_OWNER_PARTICIPANT_ID,
+            FIRST_ID,
+            Some(FIRST_ID),
+        );
+
+        // Check if related runtime storage left unchanged
+        assert!(replies_storage_unchanged(FIRST_ID, SECOND_ID));
+
+        // Failure checked
+        assert_failure(
+            reply_creation_result,
+            Error::ReplyNotFound,
+            number_of_events_before_call,
+        );
+    })
+}
+
+#[test]
+fn reply_editing_success() {
+    ExtBuilder::default().build().execute_with(|| {
+        // Create post for future replies
+        create_post(Origin::root()).unwrap();
+
+        let reply_owner_id = ensure_signed(Origin::signed(SECOND_OWNER_ORIGIN)).unwrap();
+
+        create_reply(
+            SECOND_OWNER_ORIGIN,
+            SECOND_OWNER_PARTICIPANT_ID,
+            FIRST_ID,
+            None,
+        )
+        .unwrap();
+
+        // Events number before tested call
+        let number_of_events_before_call = System::events().len();
+
+        edit_reply(
+            SECOND_OWNER_ORIGIN,
+            SECOND_OWNER_PARTICIPANT_ID,
+            FIRST_ID,
+            FIRST_ID,
+        )
+        .unwrap();
+
+        // Reply after editing checked
+        let reply = reply_by_id(FIRST_ID, FIRST_ID);
+
+        ensure_replies_equality(reply, reply_owner_id, ParentId::Post(FIRST_ID));
+
+        // Event checked
+        let reply_edited_event = get_test_event(RawEvent::ReplyEdited(
+            SECOND_OWNER_PARTICIPANT_ID,
+            FIRST_ID,
+            FIRST_ID,
+            get_reply_text(),
+        ));
+        assert_event_success(reply_edited_event, number_of_events_before_call + 1)
+    })
+}
+
+#[test]
+fn reply_editing_post_locked_error() {
+    ExtBuilder::default().build().execute_with(|| {
+        // Create post for future replies
+        create_post(Origin::root()).unwrap();
+
+        let reply_owner_id = ensure_signed(Origin::signed(SECOND_OWNER_ORIGIN)).unwrap();
+
+        create_reply(
+            SECOND_OWNER_ORIGIN,
+            SECOND_OWNER_PARTICIPANT_ID,
+            FIRST_ID,
+            None,
+        )
+        .unwrap();
+
+        // Lock blog to make all related data immutable
+        lock_post(Origin::root(), FIRST_ID).unwrap();
+
+        // Events number before tested call
+        let number_of_events_before_call = System::events().len();
+
+        let reply_editing_result = edit_reply(
+            SECOND_OWNER_ORIGIN,
+            SECOND_OWNER_PARTICIPANT_ID,
+            FIRST_ID,
+            FIRST_ID,
+        );
+
+        // Reply after editing checked
+        let reply = reply_by_id(FIRST_ID, FIRST_ID);
+
+        // Compare with default unedited reply
+        ensure_replies_equality(reply, reply_owner_id, ParentId::Post(FIRST_ID));
+
+        // Failure checked
+        assert_failure(
+            reply_editing_result,
+            Error::PostLockedError,
+            number_of_events_before_call,
+        );
+    })
+}
+
+#[test]
+fn reply_editing_not_found() {
+    ExtBuilder::default().build().execute_with(|| {
+        // Create post for future replies
+        create_post(Origin::root()).unwrap();
+
+        // Events number before tested call
+        let number_of_events_before_call = System::events().len();
+
+        let reply_editing_result = edit_reply(
+            SECOND_OWNER_ORIGIN,
+            SECOND_OWNER_PARTICIPANT_ID,
+            FIRST_ID,
+            FIRST_ID,
+        );
+
+        // Failure checked
+        assert_failure(
+            reply_editing_result,
+            Error::ReplyNotFound,
+            number_of_events_before_call,
+        );
+    })
+}
+
+#[test]
+fn reply_editing_ownership_error() {
+    ExtBuilder::default().build().execute_with(|| {
+        // Create post for future replies
+        create_post(Origin::root()).unwrap();
+
+        let reply_owner_id = ensure_signed(Origin::signed(SECOND_OWNER_ORIGIN)).unwrap();
+
+        create_reply(
+            SECOND_OWNER_ORIGIN,
+            SECOND_OWNER_PARTICIPANT_ID,
+            FIRST_ID,
+            None,
+        )
+        .unwrap();
+
+        // Events number before tested call
+        let number_of_events_before_call = System::events().len();
+
+        let reply_editing_result = edit_reply(
+            FIRST_OWNER_ORIGIN,
+            FIRST_OWNER_PARTICIPANT_ID,
+            FIRST_ID,
+            FIRST_ID,
+        );
+
+        // Reply after editing checked
+        let reply = reply_by_id(FIRST_ID, FIRST_ID);
+
+        // Compare with default unedited reply
+        ensure_replies_equality(reply, reply_owner_id, ParentId::Post(FIRST_ID));
+
+        // Failure checked
+        assert_failure(
+            reply_editing_result,
+            Error::ReplyOwnershipError,
+            number_of_events_before_call,
+        );
+    })
+}
+
+#[test]
+fn reply_participant_error() {
+    ExtBuilder::default().build().execute_with(|| {
+        // Create post for future replies
+        create_post(Origin::root()).unwrap();
+
+        let number_of_events_before_call = System::events().len();
+
+        let reply_result = create_reply(SECOND_OWNER_ORIGIN, BAD_MEMBER_ID, FIRST_ID, None);
+
+        // Failure checked
+        assert_failure(
+            reply_result,
+            Error::MembershipError,
+            number_of_events_before_call,
+        );
+    })
+}
+
+#[test]
+fn reply_editing_participant_error() {
+    ExtBuilder::default().build().execute_with(|| {
+        // Create post for future replies
+        create_post(Origin::root()).unwrap();
+
+        let reply_owner_id = ensure_signed(Origin::signed(SECOND_OWNER_ORIGIN)).unwrap();
+
+        create_reply(
+            SECOND_OWNER_ORIGIN,
+            SECOND_OWNER_PARTICIPANT_ID,
+            FIRST_ID,
+            None,
+        )
+        .unwrap();
+
+        // Events number before tested call
+        let number_of_events_before_call = System::events().len();
+
+        let reply_editing_result =
+            edit_reply(FIRST_OWNER_ORIGIN, BAD_MEMBER_ID, FIRST_ID, FIRST_ID);
+
+        // Reply after editing checked
+        let reply = reply_by_id(FIRST_ID, FIRST_ID);
+
+        // Compare with default unedited reply
+        ensure_replies_equality(reply, reply_owner_id, ParentId::Post(FIRST_ID));
+
+        // Failure checked
+        assert_failure(
+            reply_editing_result,
+            Error::MembershipError,
+            number_of_events_before_call,
+        );
+    })
+}
+
+fn replies_storage_unchanged(post_id: PostId, reply_id: <Runtime as Trait>::ReplyId) -> bool {
+    match post_by_id(post_id) {
+        Some(post) if post.replies_count() == 0 && reply_by_id(post_id, reply_id).is_none() => true,
+        Some(_) => false,
+        None if reply_by_id(post_id, reply_id).is_none() => true,
+        None => false,
+    }
+}

+ 1 - 0
runtime-modules/proposals/codex/Cargo.toml

@@ -22,6 +22,7 @@ proposals-discussion = { package = 'pallet-proposals-discussion', default-featur
 constitution = { package = 'pallet-constitution', default-features = false, path = '../../constitution'}
 membership = { package = 'pallet-membership', default-features = false, path = '../../membership'}
 council = { package = 'pallet-council', default-features = false, path = '../../council'}
+blog = { package = 'pallet-blog', default-features = false, path = '../../blog'}
 
 # Benchmarking dependencies
 frame-benchmarking = { package = 'frame-benchmarking', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'a200cdb93c6af5763b9c7bf313fa708764ac88ca', optional = true}

+ 221 - 34
runtime-modules/proposals/codex/src/benchmarking.rs

@@ -212,7 +212,8 @@ benchmarks! {
         let t in ...;
         let d in ...;
 
-        let (account_id, member_id, general_proposal_paramters) = create_proposal_parameters::<T>(t, d);
+        let (account_id, member_id, general_proposal_paramters) =
+            create_proposal_parameters::<T>(t, d);
 
         let proposal_details = ProposalDetails::Signal(vec![0u8; i.try_into().unwrap()]);
     }: create_proposal(
@@ -234,7 +235,8 @@ benchmarks! {
         let t in ...;
         let d in ...;
 
-        let (account_id, member_id, general_proposal_paramters) = create_proposal_parameters::<T>(t, d);
+        let (account_id, member_id, general_proposal_paramters) =
+            create_proposal_parameters::<T>(t, d);
 
         let proposal_details = ProposalDetails::RuntimeUpgrade(vec![0u8; i.try_into().unwrap()]);
     }: create_proposal(
@@ -256,9 +258,13 @@ benchmarks! {
         let t in ...;
         let d in ...;
 
-        let (account_id, member_id, general_proposal_paramters) = create_proposal_parameters::<T>(t, d);
+        let (account_id, member_id, general_proposal_paramters) =
+            create_proposal_parameters::<T>(t, d);
 
-        council::Module::<T>::set_budget(RawOrigin::Root.into(), council::Balance::<T>::max_value()).unwrap();
+        council::Module::<T>::set_budget(
+            RawOrigin::Root.into(),
+            council::Balance::<T>::max_value()
+        ).unwrap();
 
         let mut funding_requests =
             Vec::<common::FundingRequestParameters<council::Balance::<T>, T::AccountId>>::new();
@@ -289,7 +295,8 @@ benchmarks! {
         let t in ...;
         let d in ...;
 
-        let (account_id, member_id, general_proposal_paramters) = create_proposal_parameters::<T>(t, d);
+        let (account_id, member_id, general_proposal_paramters) =
+            create_proposal_parameters::<T>(t, d);
 
         let proposal_details = ProposalDetails::SetMaxValidatorCount(MAX_VALIDATOR_COUNT);
     }: create_proposal(
@@ -311,13 +318,15 @@ benchmarks! {
         let t in ...;
         let d in ...;
 
-        let (account_id, member_id, general_proposal_paramters) = create_proposal_parameters::<T>(t, d);
+        let (account_id, member_id, general_proposal_paramters) =
+            create_proposal_parameters::<T>(t, d);
 
-        let proposal_details = ProposalDetails::CreateWorkingGroupLeadOpening(CreateOpeningParameters {
-            description: vec![0u8; i.try_into().unwrap()],
-            stake_policy: None,
-            reward_per_block: None,
-            group: WorkingGroup::Forum,
+        let proposal_details = ProposalDetails::CreateWorkingGroupLeadOpening(
+            CreateOpeningParameters {
+                description: vec![0u8; i.try_into().unwrap()],
+                stake_policy: None,
+                reward_per_block: None,
+                group: WorkingGroup::Forum,
         });
     }: create_proposal(
         RawOrigin::Signed(account_id.clone()),
@@ -337,7 +346,8 @@ benchmarks! {
         let t in ...;
         let d in ...;
 
-        let (account_id, member_id, general_proposal_paramters) = create_proposal_parameters::<T>(t, d);
+        let (account_id, member_id, general_proposal_paramters) =
+            create_proposal_parameters::<T>(t, d);
 
         let proposal_details = ProposalDetails::FillWorkingGroupLeadOpening(FillOpeningParameters {
             opening_id: working_group::OpeningId::zero(),
@@ -362,7 +372,8 @@ benchmarks! {
         let t in ...;
         let d in ...;
 
-        let (account_id, member_id, general_proposal_paramters) = create_proposal_parameters::<T>(t, d);
+        let (account_id, member_id, general_proposal_paramters) =
+            create_proposal_parameters::<T>(t, d);
 
         let proposal_details = ProposalDetails::UpdateWorkingGroupBudget(
             One::one(),
@@ -387,7 +398,8 @@ benchmarks! {
         let t in ...;
         let d in ...;
 
-        let (account_id, member_id, general_proposal_paramters) = create_proposal_parameters::<T>(t, d);
+        let (account_id, member_id, general_proposal_paramters) =
+            create_proposal_parameters::<T>(t, d);
 
         let proposal_details = ProposalDetails::DecreaseWorkingGroupLeadStake(
             working_group::WorkerId::<T>::zero(),
@@ -412,7 +424,8 @@ benchmarks! {
         let t in ...;
         let d in ...;
 
-        let (account_id, member_id, general_proposal_paramters) = create_proposal_parameters::<T>(t, d);
+        let (account_id, member_id, general_proposal_paramters) =
+            create_proposal_parameters::<T>(t, d);
 
         let proposal_details = ProposalDetails::SlashWorkingGroupLead(
             working_group::WorkerId::<T>::zero(),
@@ -437,7 +450,8 @@ benchmarks! {
         let t in ...;
         let d in ...;
 
-        let (account_id, member_id, general_proposal_paramters) = create_proposal_parameters::<T>(t, d);
+        let (account_id, member_id, general_proposal_paramters) =
+            create_proposal_parameters::<T>(t, d);
 
         let proposal_details = ProposalDetails::SetWorkingGroupLeadReward(
             working_group::WorkerId::<T>::zero(),
@@ -462,7 +476,8 @@ benchmarks! {
         let t in ...;
         let d in ...;
 
-        let (account_id, member_id, general_proposal_paramters) = create_proposal_parameters::<T>(t, d);
+        let (account_id, member_id, general_proposal_paramters) =
+            create_proposal_parameters::<T>(t, d);
 
         let proposal_details = ProposalDetails::TerminateWorkingGroupLead(
             TerminateRoleParameters {
@@ -490,9 +505,11 @@ benchmarks! {
         let t in ...;
         let d in ...;
 
-        let (account_id, member_id, general_proposal_paramters) = create_proposal_parameters::<T>(t, d);
+        let (account_id, member_id, general_proposal_paramters) =
+            create_proposal_parameters::<T>(t, d);
 
-        let proposal_details = ProposalDetails::AmendConstitution(vec![0u8; i.try_into().unwrap()]);
+        let proposal_details =
+            ProposalDetails::AmendConstitution(vec![0u8; i.try_into().unwrap()]);
     }: create_proposal(
         RawOrigin::Signed(account_id.clone()),
         general_proposal_paramters.clone(),
@@ -511,7 +528,8 @@ benchmarks! {
         let t in ...;
         let d in ...;
 
-        let (account_id, member_id, general_proposal_paramters) = create_proposal_parameters::<T>(t, d);
+        let (account_id, member_id, general_proposal_paramters) =
+            create_proposal_parameters::<T>(t, d);
 
         let proposal_details = ProposalDetails::CancelWorkingGroupLeadOpening(
             working_group::OpeningId::zero(),
@@ -534,7 +552,8 @@ benchmarks! {
         let t in ...;
         let d in ...;
 
-        let (account_id, member_id, general_proposal_parameters) = create_proposal_parameters::<T>(t, d);
+        let (account_id, member_id, general_proposal_parameters) =
+            create_proposal_parameters::<T>(t, d);
 
         let proposal_details = ProposalDetails::SetMembershipPrice(BalanceOf::<T>::one());
     }: create_proposal(
@@ -555,7 +574,8 @@ benchmarks! {
         let t in ...;
         let d in ...;
 
-        let (account_id, member_id, general_proposal_paramters) = create_proposal_parameters::<T>(t, d);
+        let (account_id, member_id, general_proposal_paramters) =
+            create_proposal_parameters::<T>(t, d);
 
         let proposal_details = ProposalDetails::SetCouncilBudgetIncrement(BalanceOf::<T>::one());
     }: create_proposal(
@@ -576,7 +596,8 @@ benchmarks! {
         let t in ...;
         let d in ...;
 
-        let (account_id, member_id, general_proposal_paramters) = create_proposal_parameters::<T>(t, d);
+        let (account_id, member_id, general_proposal_paramters) =
+            create_proposal_parameters::<T>(t, d);
 
         let proposal_details = ProposalDetails::SetCouncilorReward(BalanceOf::<T>::one());
     }: create_proposal(
@@ -597,7 +618,8 @@ benchmarks! {
         let t in ...;
         let d in ...;
 
-        let (account_id, member_id, general_proposal_paramters) = create_proposal_parameters::<T>(t, d);
+        let (account_id, member_id, general_proposal_paramters) =
+            create_proposal_parameters::<T>(t, d);
 
         let proposal_details = ProposalDetails::SetInitialInvitationBalance(BalanceOf::<T>::one());
     }: create_proposal(
@@ -618,7 +640,8 @@ benchmarks! {
         let t in ...;
         let d in ...;
 
-        let (account_id, member_id, general_proposal_paramters) = create_proposal_parameters::<T>(t, d);
+        let (account_id, member_id, general_proposal_paramters) =
+            create_proposal_parameters::<T>(t, d);
 
         let proposal_details = ProposalDetails::SetInitialInvitationCount(One::one());
     }: create_proposal(
@@ -639,7 +662,8 @@ benchmarks! {
         let t in ...;
         let d in ...;
 
-        let (account_id, member_id, general_proposal_paramters) = create_proposal_parameters::<T>(t, d);
+        let (account_id, member_id, general_proposal_paramters) =
+            create_proposal_parameters::<T>(t, d);
 
         let proposal_details = ProposalDetails::SetMembershipLeadInvitationQuota(One::one());
     }: create_proposal(
@@ -660,7 +684,8 @@ benchmarks! {
         let t in ...;
         let d in ...;
 
-        let (account_id, member_id, general_proposal_paramters) = create_proposal_parameters::<T>(t, d);
+        let (account_id, member_id, general_proposal_paramters) =
+            create_proposal_parameters::<T>(t, d);
 
         let proposal_details = ProposalDetails::SetReferralCut(One::one());
     }: create_proposal(
@@ -677,37 +702,161 @@ benchmarks! {
         );
     }
 
+    create_proposal_create_blog_post {
+        let t in ...;
+        let d in ...;
+        let h in 1 .. MAX_BYTES;
+        let b in 1 .. MAX_BYTES;
+
+        let (account_id, member_id, general_proposal_paramters) =
+            create_proposal_parameters::<T>(t, d);
+
+        let proposal_details = ProposalDetails::CreateBlogPost(
+                vec![0; h.try_into().unwrap()],
+                vec![0; b.try_into().unwrap()],
+            );
+    }: create_proposal(
+        RawOrigin::Signed(account_id.clone()),
+        general_proposal_paramters.clone(),
+        proposal_details.clone()
+    )
+    verify {
+        create_proposal_verify::<T>(
+            account_id,
+            member_id,
+            general_proposal_paramters,
+            proposal_details
+        );
+    }
+
+    create_proposal_edit_blog_post {
+        let t in ...;
+        let d in ...;
+        let h in 1 .. MAX_BYTES;
+        let b in 1 .. MAX_BYTES;
+
+        let (account_id, member_id, general_proposal_paramters) =
+            create_proposal_parameters::<T>(t, d);
+
+        let proposal_details = ProposalDetails::EditBlogPost(
+                0,
+                Some(vec![0; h.try_into().unwrap()]),
+                Some(vec![0; b.try_into().unwrap()]),
+            );
+    }: create_proposal(
+        RawOrigin::Signed(account_id.clone()),
+        general_proposal_paramters.clone(),
+        proposal_details.clone()
+    )
+    verify {
+        create_proposal_verify::<T>(
+            account_id,
+            member_id,
+            general_proposal_paramters,
+            proposal_details
+        );
+    }
+
+    create_proposal_lock_blog_post {
+        let t in ...;
+        let d in ...;
+
+        let (account_id, member_id, general_proposal_paramters) =
+            create_proposal_parameters::<T>(t, d);
+
+        let proposal_details = ProposalDetails::LockBlogPost(0);
+    }: create_proposal(
+        RawOrigin::Signed(account_id.clone()),
+        general_proposal_paramters.clone(),
+        proposal_details.clone()
+    )
+    verify {
+        create_proposal_verify::<T>(
+            account_id,
+            member_id,
+            general_proposal_paramters,
+            proposal_details
+        );
+    }
+
+    create_proposal_unlock_blog_post {
+        let t in ...;
+        let d in ...;
+
+        let (account_id, member_id, general_proposal_paramters) =
+            create_proposal_parameters::<T>(t, d);
+
+        let proposal_details = ProposalDetails::UnlockBlogPost(0);
+    }: create_proposal(
+        RawOrigin::Signed(account_id.clone()),
+        general_proposal_paramters.clone(),
+        proposal_details.clone()
+    )
+    verify {
+        create_proposal_verify::<T>(
+            account_id,
+            member_id,
+            general_proposal_paramters,
+            proposal_details
+        );
+    }
+
     update_working_group_budget_positive_forum {
         set_wg_and_council_budget::<T>(100, WorkingGroup::Forum);
-    }: update_working_group_budget(RawOrigin::Root, WorkingGroup::Forum, One::one(), BalanceKind::Positive)
+    }: update_working_group_budget(
+        RawOrigin::Root,
+        WorkingGroup::Forum,
+        One::one(),
+        BalanceKind::Positive
+    )
     verify {
         assert_new_budgets::<T>(99, 101, WorkingGroup::Forum);
     }
 
     update_working_group_budget_negative_forum {
         set_wg_and_council_budget::<T>(100, WorkingGroup::Forum);
-    }: update_working_group_budget(RawOrigin::Root, WorkingGroup::Forum, One::one(), BalanceKind::Negative)
+    }: update_working_group_budget(
+        RawOrigin::Root,
+        WorkingGroup::Forum,
+        One::one(),
+        BalanceKind::Negative
+    )
     verify{
         assert_new_budgets::<T>(101, 99, WorkingGroup::Forum);
     }
 
     update_working_group_budget_positive_storage {
         set_wg_and_council_budget::<T>(100, WorkingGroup::Storage);
-    }: update_working_group_budget(RawOrigin::Root, WorkingGroup::Storage, One::one(), BalanceKind::Positive)
+    }: update_working_group_budget(
+        RawOrigin::Root,
+        WorkingGroup::Storage,
+        One::one(),
+        BalanceKind::Positive
+    )
     verify {
         assert_new_budgets::<T>(99, 101, WorkingGroup::Storage);
     }
 
     update_working_group_budget_negative_storage {
         set_wg_and_council_budget::<T>(100, WorkingGroup::Storage);
-    }: update_working_group_budget(RawOrigin::Root, WorkingGroup::Storage, One::one(), BalanceKind::Negative)
+    }: update_working_group_budget(
+        RawOrigin::Root,
+        WorkingGroup::Storage,
+        One::one(),
+        BalanceKind::Negative
+    )
     verify {
         assert_new_budgets::<T>(101, 99, WorkingGroup::Storage);
     }
 
     update_working_group_budget_positive_content {
         set_wg_and_council_budget::<T>(100, WorkingGroup::Content);
-    }: update_working_group_budget(RawOrigin::Root, WorkingGroup::Content, One::one(), BalanceKind::Positive)
+    }: update_working_group_budget(
+        RawOrigin::Root,
+        WorkingGroup::Content,
+        One::one(),
+        BalanceKind::Positive
+    )
     verify {
         assert_new_budgets::<T>(99, 101, WorkingGroup::Content);
     }
@@ -722,14 +871,24 @@ benchmarks! {
 
     update_working_group_budget_positive_membership {
         set_wg_and_council_budget::<T>(100, WorkingGroup::Membership);
-    }: update_working_group_budget(RawOrigin::Root, WorkingGroup::Membership, One::one(), BalanceKind::Positive)
+    }: update_working_group_budget(
+        RawOrigin::Root,
+        WorkingGroup::Membership,
+        One::one(),
+        BalanceKind::Positive
+    )
     verify {
         assert_new_budgets::<T>(99, 101, WorkingGroup::Membership);
     }
 
     update_working_group_budget_negative_membership {
         set_wg_and_council_budget::<T>(100, WorkingGroup::Membership);
-    }: update_working_group_budget(RawOrigin::Root, WorkingGroup::Membership, One::one(), BalanceKind::Negative)
+    }: update_working_group_budget(
+        RawOrigin::Root,
+        WorkingGroup::Membership,
+        One::one(),
+        BalanceKind::Negative
+    )
     verify {
         assert_new_budgets::<T>(101, 99, WorkingGroup::Membership);
     }
@@ -944,4 +1103,32 @@ mod tests {
             assert_ok!(test_benchmark_update_working_group_budget_negative_membership::<Test>());
         });
     }
+
+    #[test]
+    fn test_create_blog_post() {
+        initial_test_ext().execute_with(|| {
+            assert_ok!(test_benchmark_create_proposal_create_blog_post::<Test>());
+        });
+    }
+
+    #[test]
+    fn test_edit_blog_post() {
+        initial_test_ext().execute_with(|| {
+            assert_ok!(test_benchmark_create_proposal_edit_blog_post::<Test>());
+        });
+    }
+
+    #[test]
+    fn test_lock_blog_post() {
+        initial_test_ext().execute_with(|| {
+            assert_ok!(test_benchmark_create_proposal_lock_blog_post::<Test>());
+        });
+    }
+
+    #[test]
+    fn test_unlock_blog_post() {
+        initial_test_ext().execute_with(|| {
+            assert_ok!(test_benchmark_create_proposal_unlock_blog_post::<Test>());
+        });
+    }
 }

+ 113 - 41
runtime-modules/proposals/codex/src/lib.rs

@@ -90,25 +90,29 @@ const MAX_FUNDING_REQUEST_ACCOUNTS: usize = 100;
 pub trait WeightInfo {
     fn execute_signal_proposal(i: u32) -> Weight;
     fn create_proposal_signal(i: u32, t: u32, d: u32) -> Weight;
-    fn create_proposal_runtime_upgrade(i: u32, t: u32) -> Weight;
-    fn create_proposal_funding_request() -> Weight;
-    fn create_proposal_set_max_validator_count() -> Weight;
-    fn create_proposal_create_working_group_lead_opening(i: u32, t: u32) -> Weight;
+    fn create_proposal_runtime_upgrade(i: u32, t: u32, d: u32) -> Weight;
+    fn create_proposal_funding_request(i: u32) -> Weight;
+    fn create_proposal_set_max_validator_count(d: u32) -> Weight;
+    fn create_proposal_create_working_group_lead_opening(i: u32) -> Weight;
     fn create_proposal_fill_working_group_lead_opening() -> Weight;
-    fn create_proposal_update_working_group_budget(t: u32, d: u32) -> Weight;
-    fn create_proposal_decrease_working_group_lead_stake(d: u32) -> Weight;
-    fn create_proposal_slash_working_group_lead() -> Weight;
-    fn create_proposal_set_working_group_lead_reward(t: u32) -> Weight;
-    fn create_proposal_terminate_working_group_lead(t: u32) -> Weight;
-    fn create_proposal_amend_constitution(i: u32, t: u32) -> Weight;
-    fn create_proposal_cancel_working_group_lead_opening(d: u32) -> Weight;
-    fn create_proposal_set_membership_price(t: u32, d: u32) -> Weight;
-    fn create_proposal_set_council_budget_increment(t: u32, d: u32) -> Weight;
-    fn create_proposal_set_councilor_reward(d: u32) -> Weight;
-    fn create_proposal_set_initial_invitation_balance(d: u32) -> Weight;
-    fn create_proposal_set_initial_invitation_count(t: u32, d: u32) -> Weight;
-    fn create_proposal_set_membership_lead_invitation_quota() -> Weight;
+    fn create_proposal_update_working_group_budget(d: u32) -> Weight;
+    fn create_proposal_decrease_working_group_lead_stake(t: u32, d: u32) -> Weight;
+    fn create_proposal_slash_working_group_lead(t: u32, d: u32) -> Weight;
+    fn create_proposal_set_working_group_lead_reward(d: u32) -> Weight;
+    fn create_proposal_terminate_working_group_lead() -> Weight;
+    fn create_proposal_amend_constitution(i: u32, t: u32, d: u32) -> Weight;
+    fn create_proposal_cancel_working_group_lead_opening(t: u32, d: u32) -> Weight;
+    fn create_proposal_set_membership_price() -> Weight;
+    fn create_proposal_set_council_budget_increment() -> Weight;
+    fn create_proposal_set_councilor_reward(t: u32) -> Weight;
+    fn create_proposal_set_initial_invitation_balance(t: u32, d: u32) -> Weight;
+    fn create_proposal_set_initial_invitation_count() -> Weight;
+    fn create_proposal_set_membership_lead_invitation_quota(t: u32) -> Weight;
     fn create_proposal_set_referral_cut(t: u32) -> Weight;
+    fn create_proposal_create_blog_post(t: u32, d: u32, h: u32, b: u32) -> Weight;
+    fn create_proposal_edit_blog_post(t: u32, d: u32, h: u32, b: u32) -> Weight;
+    fn create_proposal_lock_blog_post(t: u32) -> Weight;
+    fn create_proposal_unlock_blog_post() -> Weight;
     fn update_working_group_budget_positive_forum() -> Weight;
     fn update_working_group_budget_negative_forum() -> Weight;
     fn update_working_group_budget_positive_storage() -> Weight;
@@ -244,6 +248,22 @@ pub trait Trait:
         ProposalParameters<Self::BlockNumber, BalanceOf<Self>>,
     >;
 
+    /// `Create Blog Post` proposal parameters
+    type CreateBlogPostProposalParameters: Get<
+        ProposalParameters<Self::BlockNumber, BalanceOf<Self>>,
+    >;
+
+    /// `Edit Blog Post` proposal parameters
+    type EditBlogPostProoposalParamters: Get<ProposalParameters<Self::BlockNumber, BalanceOf<Self>>>;
+
+    /// `Lock Blog Post` proposal parameters
+    type LockBlogPostProposalParameters: Get<ProposalParameters<Self::BlockNumber, BalanceOf<Self>>>;
+
+    /// `Unlock Blog Post` proposal parameters
+    type UnlockBlogPostProposalParameters: Get<
+        ProposalParameters<Self::BlockNumber, BalanceOf<Self>>,
+    >;
+
     /// Gets the budget of the given WorkingGroup
     fn get_working_group_budget(working_group: WorkingGroup) -> BalanceOf<Self>;
 
@@ -451,6 +471,19 @@ decl_module! {
         const SetReferralCutProposalParameters:
             ProposalParameters<T::BlockNumber, BalanceOf<T>> = T::SetReferralCutProposalParameters::get();
 
+        const CreateBlogPostProposalParameters:
+            ProposalParameters<T::BlockNumber, BalanceOf<T>> = T::CreateBlogPostProposalParameters::get();
+
+        const EditBlogPostProoposalParamters:
+            ProposalParameters<T::BlockNumber, BalanceOf<T>> = T::EditBlogPostProoposalParamters::get();
+
+        const LockBlogPostProposalParameters:
+            ProposalParameters<T::BlockNumber, BalanceOf<T>> = T::LockBlogPostProposalParameters::get();
+
+        const UnlockBlogPostProposalParameters:
+            ProposalParameters<T::BlockNumber, BalanceOf<T>> = T::UnlockBlogPostProposalParameters::get();
+
+
         /// Create a proposal, the type of proposal depends on the `proposal_details` variant
         ///
         /// <weight>
@@ -725,6 +758,18 @@ impl<T: Trait> Module<T> {
             ProposalDetails::SetReferralCut(..) => {
                 // Note: No checks for this proposal for now
             }
+            ProposalDetails::CreateBlogPost(..) => {
+                // Note: No checks for this proposal for now
+            }
+            ProposalDetails::EditBlogPost(..) => {
+                // Note: No checks for this proposal for now
+            }
+            ProposalDetails::LockBlogPost(..) => {
+                // Note: No checks for this proposal for now
+            }
+            ProposalDetails::UnlockBlogPost(..) => {
+                // Note: No checks for this proposal for now
+            }
         }
 
         Ok(())
@@ -785,6 +830,10 @@ impl<T: Trait> Module<T> {
                 T::SetMembershipLeadInvitationQuotaProposalParameters::get()
             }
             ProposalDetails::SetReferralCut(..) => T::SetReferralCutProposalParameters::get(),
+            ProposalDetails::CreateBlogPost(..) => T::CreateBlogPostProposalParameters::get(),
+            ProposalDetails::EditBlogPost(..) => T::EditBlogPostProoposalParamters::get(),
+            ProposalDetails::LockBlogPost(..) => T::LockBlogPostProposalParameters::get(),
+            ProposalDetails::UnlockBlogPost(..) => T::UnlockBlogPostProposalParameters::get(),
         }
     }
 
@@ -842,18 +891,20 @@ impl<T: Trait> Module<T> {
                 WeightInfoCodex::<T>::create_proposal_runtime_upgrade(
                     blob.len().saturated_into(),
                     title_length.saturated_into(),
+                    description_length.saturated_into(),
                 )
             }
-            ProposalDetails::FundingRequest(..) => {
-                WeightInfoCodex::<T>::create_proposal_funding_request()
+            ProposalDetails::FundingRequest(params) => {
+                WeightInfoCodex::<T>::create_proposal_funding_request(params.len().saturated_into())
             }
             ProposalDetails::SetMaxValidatorCount(..) => {
-                WeightInfoCodex::<T>::create_proposal_set_max_validator_count()
+                WeightInfoCodex::<T>::create_proposal_set_max_validator_count(
+                    description_length.saturated_into(),
+                )
             }
             ProposalDetails::CreateWorkingGroupLeadOpening(opening_params) => {
                 WeightInfoCodex::<T>::create_proposal_create_working_group_lead_opening(
                     opening_params.description.len().saturated_into(),
-                    title_length.saturated_into(),
                 )
             }
             ProposalDetails::FillWorkingGroupLeadOpening(..) => {
@@ -861,75 +912,96 @@ impl<T: Trait> Module<T> {
             }
             ProposalDetails::UpdateWorkingGroupBudget(..) => {
                 WeightInfoCodex::<T>::create_proposal_update_working_group_budget(
-                    title_length.saturated_into(),
                     description_length.saturated_into(),
                 )
             }
             ProposalDetails::DecreaseWorkingGroupLeadStake(..) => {
                 WeightInfoCodex::<T>::create_proposal_decrease_working_group_lead_stake(
+                    title_length.saturated_into(),
                     description_length.saturated_into(),
                 )
             }
             ProposalDetails::SlashWorkingGroupLead(..) => {
-                WeightInfoCodex::<T>::create_proposal_slash_working_group_lead()
+                WeightInfoCodex::<T>::create_proposal_slash_working_group_lead(
+                    title_length.saturated_into(),
+                    description_length.saturated_into(),
+                )
             }
             ProposalDetails::SetWorkingGroupLeadReward(..) => {
                 WeightInfoCodex::<T>::create_proposal_set_working_group_lead_reward(
-                    title_length.saturated_into(),
+                    description_length.saturated_into(),
                 )
             }
             ProposalDetails::TerminateWorkingGroupLead(..) => {
-                WeightInfoCodex::<T>::create_proposal_terminate_working_group_lead(
-                    title_length.saturated_into(),
-                )
+                WeightInfoCodex::<T>::create_proposal_terminate_working_group_lead()
             }
             ProposalDetails::AmendConstitution(new_constitution) => {
                 WeightInfoCodex::<T>::create_proposal_amend_constitution(
                     new_constitution.len().saturated_into(),
                     title_length.saturated_into(),
+                    description_length.saturated_into(),
                 )
             }
             ProposalDetails::SetMembershipPrice(..) => {
-                WeightInfoCodex::<T>::create_proposal_set_membership_price(
-                    title_length.saturated_into(),
-                    description_length.saturated_into(),
-                )
+                WeightInfoCodex::<T>::create_proposal_set_membership_price()
             }
             ProposalDetails::CancelWorkingGroupLeadOpening(..) => {
                 WeightInfoCodex::<T>::create_proposal_cancel_working_group_lead_opening(
+                    title_length.saturated_into(),
                     description_length.saturated_into(),
                 )
             }
             ProposalDetails::SetCouncilBudgetIncrement(..) => {
-                WeightInfoCodex::<T>::create_proposal_set_council_budget_increment(
-                    title_length.saturated_into(),
-                    description_length.saturated_into(),
-                )
+                WeightInfoCodex::<T>::create_proposal_set_council_budget_increment()
             }
             ProposalDetails::SetCouncilorReward(..) => {
                 WeightInfoCodex::<T>::create_proposal_set_councilor_reward(
-                    description_length.saturated_into(),
+                    title_length.saturated_into(),
                 )
             }
             ProposalDetails::SetInitialInvitationBalance(..) => {
                 WeightInfoCodex::<T>::create_proposal_set_initial_invitation_balance(
+                    title_length.saturated_into(),
                     description_length.saturated_into(),
                 )
             }
             ProposalDetails::SetInitialInvitationCount(..) => {
-                WeightInfoCodex::<T>::create_proposal_set_initial_invitation_count(
-                    title_length.saturated_into(),
-                    description_length.saturated_into(),
-                )
+                WeightInfoCodex::<T>::create_proposal_set_initial_invitation_count()
             }
             ProposalDetails::SetMembershipLeadInvitationQuota(..) => {
-                WeightInfoCodex::<T>::create_proposal_set_membership_lead_invitation_quota()
+                WeightInfoCodex::<T>::create_proposal_set_membership_lead_invitation_quota(
+                    title_length.saturated_into(),
+                )
             }
             ProposalDetails::SetReferralCut(..) => {
                 WeightInfoCodex::<T>::create_proposal_set_referral_cut(
                     title_length.saturated_into(),
                 )
             }
+            ProposalDetails::CreateBlogPost(header, body) => {
+                WeightInfoCodex::<T>::create_proposal_create_blog_post(
+                    title_length.saturated_into(),
+                    description_length.saturated_into(),
+                    header.len().saturated_into(),
+                    body.len().saturated_into(),
+                )
+            }
+            ProposalDetails::EditBlogPost(_, header, body) => {
+                let header_len = header.as_ref().map_or(0, |h| h.len());
+                let body_len = body.as_ref().map_or(0, |b| b.len());
+                WeightInfoCodex::<T>::create_proposal_edit_blog_post(
+                    title_length.saturated_into(),
+                    description_length.saturated_into(),
+                    header_len.saturated_into(),
+                    body_len.saturated_into(),
+                )
+            }
+            ProposalDetails::LockBlogPost(..) => {
+                WeightInfoCodex::<T>::create_proposal_lock_blog_post(title_length.saturated_into())
+            }
+            ProposalDetails::UnlockBlogPost(..) => {
+                WeightInfoCodex::<T>::create_proposal_unlock_blog_post().saturated_into()
+            }
         }
     }
 }

+ 31 - 15
runtime-modules/proposals/codex/src/tests/mock.rs

@@ -591,6 +591,10 @@ impl crate::Trait for Test {
     type SetInvitationCountProposalParameters = DefaultProposalParameters;
     type SetMembershipLeadInvitationQuotaProposalParameters = DefaultProposalParameters;
     type SetReferralCutProposalParameters = DefaultProposalParameters;
+    type CreateBlogPostProposalParameters = DefaultProposalParameters;
+    type EditBlogPostProoposalParamters = DefaultProposalParameters;
+    type LockBlogPostProposalParameters = DefaultProposalParameters;
+    type UnlockBlogPostProposalParameters = DefaultProposalParameters;
 
     fn get_working_group_budget(working_group: WorkingGroup) -> BalanceOf<Test> {
         call_wg!(working_group<Test>, get_budget)
@@ -799,63 +803,75 @@ impl crate::WeightInfo for () {
     fn create_proposal_signal(_: u32, _: u32, _: u32) -> Weight {
         0
     }
-    fn create_proposal_runtime_upgrade(_: u32, _: u32) -> Weight {
+    fn create_proposal_runtime_upgrade(_: u32, _: u32, _: u32) -> Weight {
         0
     }
-    fn create_proposal_funding_request() -> Weight {
+    fn create_proposal_funding_request(_: u32) -> Weight {
         0
     }
-    fn create_proposal_set_max_validator_count() -> Weight {
+    fn create_proposal_set_max_validator_count(_: u32) -> Weight {
         0
     }
-    fn create_proposal_create_working_group_lead_opening(_: u32, _: u32) -> Weight {
+    fn create_proposal_create_working_group_lead_opening(_: u32) -> Weight {
         0
     }
     fn create_proposal_fill_working_group_lead_opening() -> Weight {
         0
     }
-    fn create_proposal_update_working_group_budget(_: u32, _: u32) -> Weight {
+    fn create_proposal_update_working_group_budget(_: u32) -> Weight {
         0
     }
-    fn create_proposal_decrease_working_group_lead_stake(_: u32) -> Weight {
+    fn create_proposal_decrease_working_group_lead_stake(_: u32, _: u32) -> Weight {
         0
     }
-    fn create_proposal_slash_working_group_lead() -> Weight {
+    fn create_proposal_slash_working_group_lead(_: u32, _: u32) -> Weight {
         0
     }
     fn create_proposal_set_working_group_lead_reward(_: u32) -> Weight {
         0
     }
-    fn create_proposal_terminate_working_group_lead(_: u32) -> Weight {
+    fn create_proposal_terminate_working_group_lead() -> Weight {
         0
     }
-    fn create_proposal_amend_constitution(_: u32, _: u32) -> Weight {
+    fn create_proposal_amend_constitution(_: u32, _: u32, _: u32) -> Weight {
         0
     }
-    fn create_proposal_cancel_working_group_lead_opening(_: u32) -> Weight {
+    fn create_proposal_cancel_working_group_lead_opening(_: u32, _: u32) -> Weight {
         0
     }
-    fn create_proposal_set_membership_price(_: u32, _: u32) -> Weight {
+    fn create_proposal_set_membership_price() -> Weight {
         0
     }
-    fn create_proposal_set_council_budget_increment(_: u32, _: u32) -> Weight {
+    fn create_proposal_set_council_budget_increment() -> Weight {
         0
     }
     fn create_proposal_set_councilor_reward(_: u32) -> Weight {
         0
     }
-    fn create_proposal_set_initial_invitation_balance(_: u32) -> Weight {
+    fn create_proposal_set_initial_invitation_balance(_: u32, _: u32) -> Weight {
         0
     }
-    fn create_proposal_set_initial_invitation_count(_: u32, _: u32) -> Weight {
+    fn create_proposal_set_initial_invitation_count() -> Weight {
         0
     }
-    fn create_proposal_set_membership_lead_invitation_quota() -> Weight {
+    fn create_proposal_set_membership_lead_invitation_quota(_: u32) -> Weight {
         0
     }
     fn create_proposal_set_referral_cut(_: u32) -> Weight {
         0
     }
+    fn create_proposal_create_blog_post(_: u32, _: u32, _: u32, _: u32) -> Weight {
+        0
+    }
+    fn create_proposal_edit_blog_post(_: u32, _: u32, _: u32, _: u32) -> Weight {
+        0
+    }
+    fn create_proposal_lock_blog_post(_: u32) -> Weight {
+        0
+    }
+    fn create_proposal_unlock_blog_post() -> Weight {
+        0
+    }
     fn update_working_group_budget_positive_forum() -> Weight {
         0
     }

+ 212 - 0
runtime-modules/proposals/codex/src/tests/mod.rs

@@ -674,6 +674,218 @@ fn create_set_max_validator_count_proposal_common_checks_succeed() {
     });
 }
 
+#[test]
+fn create_create_blog_post_proposal_common_checks_succeed() {
+    initial_test_ext().execute_with(|| {
+        increase_total_balance_issuance_using_account_id(1, 500000);
+
+        let general_proposal_parameters_no_staking = GeneralProposalParameters::<Test> {
+            member_id: 1,
+            title: b"title".to_vec(),
+            description: b"body".to_vec(),
+            staking_account_id: None,
+            exact_execution_block: None,
+        };
+
+        let general_proposal_parameters = GeneralProposalParameters::<Test> {
+            member_id: 1,
+            title: b"title".to_vec(),
+            description: b"body".to_vec(),
+            staking_account_id: Some(1),
+            exact_execution_block: None,
+        };
+
+        let proposal_details = ProposalDetails::CreateBlogPost(vec![0u8], vec![0u8]);
+
+        let proposal_fixture = ProposalTestFixture {
+            general_proposal_parameters: general_proposal_parameters.clone(),
+            proposal_details: proposal_details.clone(),
+            insufficient_rights_call: || {
+                ProposalCodex::create_proposal(
+                    RawOrigin::None.into(),
+                    general_proposal_parameters_no_staking.clone(),
+                    proposal_details.clone(),
+                )
+            },
+            empty_stake_call: || {
+                ProposalCodex::create_proposal(
+                    RawOrigin::Signed(1).into(),
+                    general_proposal_parameters_no_staking.clone(),
+                    proposal_details.clone(),
+                )
+            },
+            successful_call: || {
+                ProposalCodex::create_proposal(
+                    RawOrigin::Signed(1).into(),
+                    general_proposal_parameters.clone(),
+                    proposal_details.clone(),
+                )
+            },
+            proposal_parameters: <Test as crate::Trait>::CreateBlogPostProposalParameters::get(),
+        };
+        proposal_fixture.check_all();
+    });
+}
+
+#[test]
+fn create_edit_blog_post_proposal_common_checks_succeed() {
+    initial_test_ext().execute_with(|| {
+        increase_total_balance_issuance_using_account_id(1, 500000);
+
+        let general_proposal_parameters_no_staking = GeneralProposalParameters::<Test> {
+            member_id: 1,
+            title: b"title".to_vec(),
+            description: b"body".to_vec(),
+            staking_account_id: None,
+            exact_execution_block: None,
+        };
+
+        let general_proposal_parameters = GeneralProposalParameters::<Test> {
+            member_id: 1,
+            title: b"title".to_vec(),
+            description: b"body".to_vec(),
+            staking_account_id: Some(1),
+            exact_execution_block: None,
+        };
+
+        let proposal_details = ProposalDetails::EditBlogPost(0, Some(vec![0u8]), Some(vec![0u8]));
+
+        let proposal_fixture = ProposalTestFixture {
+            general_proposal_parameters: general_proposal_parameters.clone(),
+            proposal_details: proposal_details.clone(),
+            insufficient_rights_call: || {
+                ProposalCodex::create_proposal(
+                    RawOrigin::None.into(),
+                    general_proposal_parameters_no_staking.clone(),
+                    proposal_details.clone(),
+                )
+            },
+            empty_stake_call: || {
+                ProposalCodex::create_proposal(
+                    RawOrigin::Signed(1).into(),
+                    general_proposal_parameters_no_staking.clone(),
+                    proposal_details.clone(),
+                )
+            },
+            successful_call: || {
+                ProposalCodex::create_proposal(
+                    RawOrigin::Signed(1).into(),
+                    general_proposal_parameters.clone(),
+                    proposal_details.clone(),
+                )
+            },
+            proposal_parameters: <Test as crate::Trait>::EditBlogPostProoposalParamters::get(),
+        };
+        proposal_fixture.check_all();
+    });
+}
+
+#[test]
+fn create_lock_blog_post_proposal_common_checks_succeed() {
+    initial_test_ext().execute_with(|| {
+        increase_total_balance_issuance_using_account_id(1, 500000);
+
+        let general_proposal_parameters_no_staking = GeneralProposalParameters::<Test> {
+            member_id: 1,
+            title: b"title".to_vec(),
+            description: b"body".to_vec(),
+            staking_account_id: None,
+            exact_execution_block: None,
+        };
+
+        let general_proposal_parameters = GeneralProposalParameters::<Test> {
+            member_id: 1,
+            title: b"title".to_vec(),
+            description: b"body".to_vec(),
+            staking_account_id: Some(1),
+            exact_execution_block: None,
+        };
+
+        let proposal_details = ProposalDetails::LockBlogPost(0);
+
+        let proposal_fixture = ProposalTestFixture {
+            general_proposal_parameters: general_proposal_parameters.clone(),
+            proposal_details: proposal_details.clone(),
+            insufficient_rights_call: || {
+                ProposalCodex::create_proposal(
+                    RawOrigin::None.into(),
+                    general_proposal_parameters_no_staking.clone(),
+                    proposal_details.clone(),
+                )
+            },
+            empty_stake_call: || {
+                ProposalCodex::create_proposal(
+                    RawOrigin::Signed(1).into(),
+                    general_proposal_parameters_no_staking.clone(),
+                    proposal_details.clone(),
+                )
+            },
+            successful_call: || {
+                ProposalCodex::create_proposal(
+                    RawOrigin::Signed(1).into(),
+                    general_proposal_parameters.clone(),
+                    proposal_details.clone(),
+                )
+            },
+            proposal_parameters: <Test as crate::Trait>::LockBlogPostProposalParameters::get(),
+        };
+        proposal_fixture.check_all();
+    });
+}
+
+#[test]
+fn create_unlock_blog_post_proposal_common_checks_succeed() {
+    initial_test_ext().execute_with(|| {
+        increase_total_balance_issuance_using_account_id(1, 500000);
+
+        let general_proposal_parameters_no_staking = GeneralProposalParameters::<Test> {
+            member_id: 1,
+            title: b"title".to_vec(),
+            description: b"body".to_vec(),
+            staking_account_id: None,
+            exact_execution_block: None,
+        };
+
+        let general_proposal_parameters = GeneralProposalParameters::<Test> {
+            member_id: 1,
+            title: b"title".to_vec(),
+            description: b"body".to_vec(),
+            staking_account_id: Some(1),
+            exact_execution_block: None,
+        };
+
+        let proposal_details = ProposalDetails::UnlockBlogPost(0);
+
+        let proposal_fixture = ProposalTestFixture {
+            general_proposal_parameters: general_proposal_parameters.clone(),
+            proposal_details: proposal_details.clone(),
+            insufficient_rights_call: || {
+                ProposalCodex::create_proposal(
+                    RawOrigin::None.into(),
+                    general_proposal_parameters_no_staking.clone(),
+                    proposal_details.clone(),
+                )
+            },
+            empty_stake_call: || {
+                ProposalCodex::create_proposal(
+                    RawOrigin::Signed(1).into(),
+                    general_proposal_parameters_no_staking.clone(),
+                    proposal_details.clone(),
+                )
+            },
+            successful_call: || {
+                ProposalCodex::create_proposal(
+                    RawOrigin::Signed(1).into(),
+                    general_proposal_parameters.clone(),
+                    proposal_details.clone(),
+                )
+            },
+            proposal_parameters: <Test as crate::Trait>::UnlockBlogPostProposalParameters::get(),
+        };
+        proposal_fixture.check_all();
+    });
+}
+
 #[test]
 fn create_set_max_validator_count_proposal_failed_with_invalid_validator_count() {
     initial_test_ext().execute_with(|| {

+ 16 - 3
runtime-modules/proposals/codex/src/types.rs

@@ -23,6 +23,7 @@ pub type ProposalDetailsOf<T> = ProposalDetails<
     <T as frame_system::Trait>::AccountId,
     working_group::WorkerId<T>,
     working_group::OpeningId,
+    blog::PostId,
 >;
 
 /// Kind of Balance for `Update Working Group Budget`.
@@ -38,7 +39,7 @@ pub enum BalanceKind {
 /// Proposal details provide voters the information required for the perceived voting.
 #[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
 #[derive(Encode, Decode, Clone, PartialEq, Debug, Eq)]
-pub enum ProposalDetails<Balance, BlockNumber, AccountId, WorkerId, OpeningId> {
+pub enum ProposalDetails<Balance, BlockNumber, AccountId, WorkerId, OpeningId, PostId> {
     /// The signal of the `Signal` proposal
     Signal(Vec<u8>),
 
@@ -103,10 +104,22 @@ pub enum ProposalDetails<Balance, BlockNumber, AccountId, WorkerId, OpeningId> {
 
     /// `Set Referral Cut` proposal
     SetReferralCut(Balance),
+
+    /// `Create Blog Post` proposal
+    CreateBlogPost(Vec<u8>, Vec<u8>),
+
+    /// `Edit Blog Post` proposal
+    EditBlogPost(PostId, Option<Vec<u8>>, Option<Vec<u8>>),
+
+    /// `Lock Blog Post` proposal
+    LockBlogPost(PostId),
+
+    /// `Unlock Blog Post` proposal
+    UnlockBlogPost(PostId),
 }
 
-impl<Balance, BlockNumber, AccountId, WorkerId, OpeningId> Default
-    for ProposalDetails<Balance, BlockNumber, AccountId, WorkerId, OpeningId>
+impl<Balance, BlockNumber, AccountId, WorkerId, OpeningId, ProposalId> Default
+    for ProposalDetails<Balance, BlockNumber, AccountId, WorkerId, OpeningId, ProposalId>
 {
     fn default() -> Self {
         ProposalDetails::Signal(b"invalid proposal details".to_vec())

+ 3 - 0
runtime/Cargo.toml

@@ -78,6 +78,7 @@ proposals-codex = { package = 'pallet-proposals-codex', default-features = false
 content-directory = { package = 'pallet-content-directory', default-features = false, path = '../runtime-modules/content-directory' }
 pallet_constitution = { package = 'pallet-constitution', default-features = false, path = '../runtime-modules/constitution' }
 staking-handler = { package = 'pallet-staking-handler', default-features = false, path = '../runtime-modules/staking-handler'}
+blog = { package = 'pallet-blog', default-features = false, path = '../runtime-modules/blog'}
 
 [dev-dependencies]
 sp-io = { package = 'sp-io', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'a200cdb93c6af5763b9c7bf313fa708764ac88ca'}
@@ -151,6 +152,7 @@ std = [
     'content-directory/std',
     'pallet_constitution/std',
     'staking-handler/std',
+    'blog/std',
 ]
 runtime-benchmarks = [
     "frame-system/runtime-benchmarks",
@@ -173,6 +175,7 @@ runtime-benchmarks = [
     "membership/runtime-benchmarks",
     "council/runtime-benchmarks",
     "referendum/runtime-benchmarks",
+    "blog/runtime-benchmarks",
     "hex-literal",
 ]
 

+ 10 - 0
runtime/src/integration/proposals/proposal_encoder.rs

@@ -125,6 +125,16 @@ impl ProposalEncoder<Runtime> for ExtrinsicProposalEncoder {
             ProposalDetails::SetReferralCut(new_referral_cut) => {
                 Call::Members(membership::Call::set_referral_cut(new_referral_cut))
             }
+            ProposalDetails::CreateBlogPost(title, body) => {
+                Call::Blog(blog::Call::create_post(title, body))
+            }
+            ProposalDetails::EditBlogPost(post_id, title, body) => {
+                Call::Blog(blog::Call::edit_post(post_id, title, body))
+            }
+            ProposalDetails::LockBlogPost(post_id) => Call::Blog(blog::Call::lock_post(post_id)),
+            ProposalDetails::UnlockBlogPost(post_id) => {
+                Call::Blog(blog::Call::unlock_post(post_id))
+            }
         };
 
         call.encode()

+ 22 - 0
runtime/src/lib.rs

@@ -861,6 +861,10 @@ impl proposals_codex::Trait for Runtime {
     type SetMembershipLeadInvitationQuotaProposalParameters =
         SetMembershipLeadInvitationQuotaProposalParameters;
     type SetReferralCutProposalParameters = SetReferralCutProposalParameters;
+    type CreateBlogPostProposalParameters = CreateBlogPostProposalParameters;
+    type EditBlogPostProoposalParamters = EditBlogPostProoposalParamters;
+    type LockBlogPostProposalParameters = LockBlogPostProposalParameters;
+    type UnlockBlogPostProposalParameters = UnlockBlogPostProposalParameters;
     type WeightInfo = weights::proposals_codex::WeightInfo;
     fn get_working_group_budget(working_group: WorkingGroup) -> Balance {
         call_wg!(working_group, get_budget)
@@ -875,6 +879,23 @@ impl pallet_constitution::Trait for Runtime {
     type WeightInfo = weights::pallet_constitution::WeightInfo;
 }
 
+parameter_types! {
+    pub const PostsMaxNumber: u64 = 20;
+    pub const RepliesMaxNumber: u64 = 100;
+}
+
+pub type BlogInstance = blog::Instance1;
+impl blog::Trait<BlogInstance> for Runtime {
+    type Event = Event;
+
+    type PostsMaxNumber = PostsMaxNumber;
+    type RepliesMaxNumber = RepliesMaxNumber;
+    type ParticipantEnsureOrigin = Members;
+    type WeightInfo = weights::blog::WeightInfo;
+
+    type ReplyId = u64;
+}
+
 /// Forum identifier for category
 pub type CategoryId = u64;
 
@@ -927,6 +948,7 @@ construct_runtime!(
         Forum: forum::{Module, Call, Storage, Event<T>, Config<T>},
         ContentDirectory: content_directory::{Module, Call, Storage, Event<T>, Config<T>},
         Constitution: pallet_constitution::{Module, Call, Storage, Event},
+        Blog: blog::<Instance1>::{Module, Call, Storage, Event<T>},
         // --- Storage
         DataObjectTypeRegistry: data_object_type_registry::{Module, Call, Storage, Event<T>, Config<T>},
         DataDirectory: data_directory::{Module, Call, Storage, Event<T>},

+ 60 - 0
runtime/src/proposals_configuration/defaults.rs

@@ -287,3 +287,63 @@ pub(crate) fn set_invitation_count_proposal() -> ProposalParameters<BlockNumber,
         constitutionality: 1,
     }
 }
+
+// TODO: decide on paramaters
+// Proposal parameters for the 'Create Blog Post' proposal
+pub(crate) fn create_blog_post_proposal() -> ProposalParameters<BlockNumber, Balance> {
+    ProposalParameters {
+        voting_period: 72000,
+        grace_period: 43200,
+        approval_quorum_percentage: 60,
+        approval_threshold_percentage: 75,
+        slashing_quorum_percentage: 60,
+        slashing_threshold_percentage: 80,
+        required_stake: Some(50_000),
+        constitutionality: 1,
+    }
+}
+
+// TODO: decide on paramaters
+// Proposal parameters for the 'Edit Blog Post' proposal
+pub(crate) fn edit_blog_post_proposal() -> ProposalParameters<BlockNumber, Balance> {
+    ProposalParameters {
+        voting_period: 72000,
+        grace_period: 43200,
+        approval_quorum_percentage: 60,
+        approval_threshold_percentage: 75,
+        slashing_quorum_percentage: 60,
+        slashing_threshold_percentage: 80,
+        required_stake: Some(50_000),
+        constitutionality: 1,
+    }
+}
+
+// TODO: decide on paramaters
+// Proposal parameters for the 'Lock Blog Post' proposal
+pub(crate) fn lock_blog_post_proposal() -> ProposalParameters<BlockNumber, Balance> {
+    ProposalParameters {
+        voting_period: 72000,
+        grace_period: 43200,
+        approval_quorum_percentage: 60,
+        approval_threshold_percentage: 75,
+        slashing_quorum_percentage: 60,
+        slashing_threshold_percentage: 80,
+        required_stake: Some(50_000),
+        constitutionality: 1,
+    }
+}
+
+// TODO: decide on paramaters
+// Proposal parameters for the 'Unlock Blog Post' proposal
+pub(crate) fn unlock_blog_post_proposal() -> ProposalParameters<BlockNumber, Balance> {
+    ProposalParameters {
+        voting_period: 72000,
+        grace_period: 43200,
+        approval_quorum_percentage: 60,
+        approval_threshold_percentage: 75,
+        slashing_quorum_percentage: 60,
+        slashing_threshold_percentage: 80,
+        required_stake: Some(50_000),
+        constitutionality: 1,
+    }
+}

+ 21 - 1
runtime/src/proposals_configuration/mod.rs

@@ -53,6 +53,14 @@ parameter_types! {
         ALL_PROPOSALS_PARAMETERS.set_referral_cut_proposal;
     pub SetInvitationCountProposalParameters: ProposalParameters<BlockNumber, Balance> =
         ALL_PROPOSALS_PARAMETERS.set_invitation_count_proposal;
+    pub CreateBlogPostProposalParameters: ProposalParameters<BlockNumber, Balance> =
+        ALL_PROPOSALS_PARAMETERS.create_blog_post_proposal;
+    pub EditBlogPostProoposalParamters: ProposalParameters<BlockNumber, Balance> =
+        ALL_PROPOSALS_PARAMETERS.edit_blog_post_proposal;
+    pub LockBlogPostProposalParameters: ProposalParameters<BlockNumber, Balance> =
+        ALL_PROPOSALS_PARAMETERS.lock_blog_post_proposal;
+    pub UnlockBlogPostProposalParameters: ProposalParameters<BlockNumber, Balance> =
+        ALL_PROPOSALS_PARAMETERS.unlock_blog_post_proposal;
 }
 
 ///////////
@@ -78,6 +86,10 @@ struct AllProposalsParameters {
     pub set_membership_lead_invitation_quota_proposal: ProposalParameters<BlockNumber, Balance>,
     pub set_referral_cut_proposal: ProposalParameters<BlockNumber, Balance>,
     pub set_invitation_count_proposal: ProposalParameters<BlockNumber, Balance>,
+    pub create_blog_post_proposal: ProposalParameters<BlockNumber, Balance>,
+    pub edit_blog_post_proposal: ProposalParameters<BlockNumber, Balance>,
+    pub lock_blog_post_proposal: ProposalParameters<BlockNumber, Balance>,
+    pub unlock_blog_post_proposal: ProposalParameters<BlockNumber, Balance>,
 }
 
 // to initialize parameters only once.
@@ -173,7 +185,11 @@ fn convert_json_object_to_proposal_parameters(
             set_membership_lead_invitation_quota_proposal
         );
         init_proposal_parameter_object!(params, jo.clone(), set_referral_cut_proposal);
-        init_proposal_parameter_object!(params, jo, set_invitation_count_proposal);
+        init_proposal_parameter_object!(params, jo.clone(), set_invitation_count_proposal);
+        init_proposal_parameter_object!(params, jo.clone(), create_blog_post_proposal);
+        init_proposal_parameter_object!(params, jo.clone(), edit_blog_post_proposal);
+        init_proposal_parameter_object!(params, jo.clone(), lock_blog_post_proposal);
+        init_proposal_parameter_object!(params, jo, unlock_blog_post_proposal);
     }
 
     params
@@ -304,5 +320,9 @@ fn default_parameters() -> AllProposalsParameters {
             defaults::set_membership_lead_invitation_quota_proposal(),
         set_referral_cut_proposal: defaults::set_referral_cut_proposal(),
         set_invitation_count_proposal: defaults::set_invitation_count_proposal(),
+        create_blog_post_proposal: defaults::create_blog_post_proposal(),
+        edit_blog_post_proposal: defaults::edit_blog_post_proposal(),
+        lock_blog_post_proposal: defaults::lock_blog_post_proposal(),
+        unlock_blog_post_proposal: defaults::unlock_blog_post_proposal(),
     }
 }

+ 40 - 0
runtime/src/proposals_configuration/sample_proposal_parameters.json

@@ -199,5 +199,45 @@
         "slashing_threshold_percentage": 6,
         "required_stake": 7,
         "constitutionality": 8
+    },
+    "create_blog_post_proposal": {
+        "voting_period": 1,
+        "grace_period": 2,
+        "approval_quorum_percentage": 3,
+        "approval_threshold_percentage": 4,
+        "slashing_quorum_percentage": 5,
+        "slashing_threshold_percentage": 6,
+        "required_stake": 7,
+        "constitutionality": 8
+    },
+    "edit_blog_post_proposal": {
+        "voting_period": 1,
+        "grace_period": 2,
+        "approval_quorum_percentage": 3,
+        "approval_threshold_percentage": 4,
+        "slashing_quorum_percentage": 5,
+        "slashing_threshold_percentage": 6,
+        "required_stake": 7,
+        "constitutionality": 8
+    },
+    "lock_blog_post_proposal": {
+        "voting_period": 1,
+        "grace_period": 2,
+        "approval_quorum_percentage": 3,
+        "approval_threshold_percentage": 4,
+        "slashing_quorum_percentage": 5,
+        "slashing_threshold_percentage": 6,
+        "required_stake": 7,
+        "constitutionality": 8
+    },
+    "unlock_blog_post_proposal": {
+        "voting_period": 1,
+        "grace_period": 2,
+        "approval_quorum_percentage": 3,
+        "approval_threshold_percentage": 4,
+        "slashing_quorum_percentage": 5,
+        "slashing_threshold_percentage": 6,
+        "required_stake": 7,
+        "constitutionality": 8
     }
 }

+ 36 - 0
runtime/src/proposals_configuration/tests.rs

@@ -192,3 +192,39 @@ fn proposal_parameters_are_initialized_set_invitation_count() {
 
     assert_eq!(default_proposal_parameters(), actual_params);
 }
+
+// Enable during the conditional compilation tests.
+#[test]
+#[ignore]
+fn proposal_parameters_are_initialized_create_blog_post_proposal() {
+    let actual_params = super::CreateBlogPostProposalParameters::get();
+
+    assert_eq!(default_proposal_parameters(), actual_params);
+}
+
+// Enable during the conditional compilation tests.
+#[test]
+#[ignore]
+fn proposal_parameters_are_initialized_edit_blog_post_proposal() {
+    let actual_params = super::EditBlogPostProoposalParamters::get();
+
+    assert_eq!(default_proposal_parameters(), actual_params);
+}
+
+// Enable during the conditional compilation tests.
+#[test]
+#[ignore]
+fn proposal_parameters_are_initialized_lock_blog_post_proposal() {
+    let actual_params = super::LockBlogPostProposalParameters::get();
+
+    assert_eq!(default_proposal_parameters(), actual_params);
+}
+
+// Enable during the conditional compilation tests.
+#[test]
+#[ignore]
+fn proposal_parameters_are_initialized_unlock_blog_post_proposal() {
+    let actual_params = super::UnlockBlogPostProposalParameters::get();
+
+    assert_eq!(default_proposal_parameters(), actual_params);
+}

+ 2 - 0
runtime/src/runtime_api.rs

@@ -294,6 +294,7 @@ impl_runtime_apis! {
             use crate::ImOnline;
             use crate::Council;
             use crate::Referendum;
+            use crate::Blog;
 
 
             // Trying to add benchmarks directly to the Session Pallet caused cyclic dependency issues.
@@ -383,6 +384,7 @@ impl_runtime_apis! {
             add_benchmark!(params, batches, working_group, ContentDirectoryWorkingGroup);
             add_benchmark!(params, batches, referendum, Referendum);
             add_benchmark!(params, batches, council, Council);
+            add_benchmark!(params, batches, blog, Blog);
 
             if batches.is_empty() { return Err("Benchmark not found for this pallet.".into()) }
             Ok(batches)

+ 150 - 1
runtime/src/tests/proposals_integration/mod.rs

@@ -5,7 +5,7 @@
 mod working_group_proposals;
 
 use crate::tests::run_to_block;
-use crate::{MembershipWorkingGroupInstance, ProposalCancellationFee, Runtime};
+use crate::{BlogInstance, MembershipWorkingGroupInstance, ProposalCancellationFee, Runtime};
 use codec::Encode;
 use proposals_codex::{GeneralProposalParameters, ProposalDetails};
 use proposals_engine::{
@@ -35,6 +35,7 @@ pub type ProposalCodex = proposals_codex::Module<Runtime>;
 pub type Council = council::Module<Runtime>;
 pub type Membership = membership::Module<Runtime>;
 pub type MembershipWorkingGroup = working_group::Module<Runtime, MembershipWorkingGroupInstance>;
+pub type Blog = blog::Module<Runtime, BlogInstance>;
 
 fn setup_members(count: u8) {
     for i in 0..count {
@@ -578,6 +579,154 @@ fn funding_request_proposal_execution_succeeds() {
     });
 }
 
+#[test]
+fn create_blog_post_proposal_execution_succeeds() {
+    initial_test_ext().execute_with(|| {
+        let member_id = 10;
+        let account_id: [u8; 32] = [member_id; 32];
+        let council_budget = 5_000_000;
+
+        assert!(Council::set_budget(RawOrigin::Root.into(), council_budget).is_ok());
+
+        let codex_extrinsic_test_fixture = CodexProposalTestFixture::default_for_call(|| {
+            let general_proposal_parameters = GeneralProposalParameters::<Runtime> {
+                member_id: member_id.into(),
+                title: b"title".to_vec(),
+                description: b"body".to_vec(),
+                staking_account_id: Some(account_id.into()),
+                exact_execution_block: None,
+            };
+
+            ProposalCodex::create_proposal(
+                RawOrigin::Signed(account_id.clone().into()).into(),
+                general_proposal_parameters,
+                ProposalDetails::CreateBlogPost(vec![0u8], vec![0u8]),
+            )
+        })
+        .with_member_id(member_id as u64);
+
+        assert_eq!(Blog::post_count(), 0);
+
+        codex_extrinsic_test_fixture.call_extrinsic_and_assert();
+        run_to_block(86410);
+
+        assert_eq!(Blog::post_count(), 1);
+    });
+}
+
+#[test]
+fn edit_blog_post_proposal_execution_succeeds() {
+    initial_test_ext().execute_with(|| {
+        let member_id = 10;
+        let account_id: [u8; 32] = [member_id; 32];
+        let council_budget = 5_000_000;
+
+        assert!(Council::set_budget(RawOrigin::Root.into(), council_budget).is_ok());
+
+        let post_id = Blog::post_count();
+        Blog::create_post(RawOrigin::Root.into(), vec![0u8], vec![0u8]).unwrap();
+
+        let codex_extrinsic_test_fixture = CodexProposalTestFixture::default_for_call(|| {
+            let general_proposal_parameters = GeneralProposalParameters::<Runtime> {
+                member_id: member_id.into(),
+                title: b"title".to_vec(),
+                description: b"body".to_vec(),
+                staking_account_id: Some(account_id.into()),
+                exact_execution_block: None,
+            };
+
+            ProposalCodex::create_proposal(
+                RawOrigin::Signed(account_id.clone().into()).into(),
+                general_proposal_parameters,
+                ProposalDetails::EditBlogPost(post_id, Some(vec![1u8]), None),
+            )
+        })
+        .with_member_id(member_id as u64);
+
+        codex_extrinsic_test_fixture.call_extrinsic_and_assert();
+        run_to_block(86410);
+
+        assert!(Blog::post_by_id(post_id) == blog::Post::new(&vec![1u8], &vec![0u8]));
+    });
+}
+
+#[test]
+fn lock_blog_post_proposal_execution_succeeds() {
+    initial_test_ext().execute_with(|| {
+        let member_id = 10;
+        let account_id: [u8; 32] = [member_id; 32];
+        let council_budget = 5_000_000;
+
+        assert!(Council::set_budget(RawOrigin::Root.into(), council_budget).is_ok());
+
+        let post_id = Blog::post_count();
+        Blog::create_post(RawOrigin::Root.into(), vec![0u8], vec![0u8]).unwrap();
+
+        let codex_extrinsic_test_fixture = CodexProposalTestFixture::default_for_call(|| {
+            let general_proposal_parameters = GeneralProposalParameters::<Runtime> {
+                member_id: member_id.into(),
+                title: b"title".to_vec(),
+                description: b"body".to_vec(),
+                staking_account_id: Some(account_id.into()),
+                exact_execution_block: None,
+            };
+
+            ProposalCodex::create_proposal(
+                RawOrigin::Signed(account_id.clone().into()).into(),
+                general_proposal_parameters,
+                ProposalDetails::LockBlogPost(post_id),
+            )
+        })
+        .with_member_id(member_id as u64);
+
+        assert_eq!(Blog::post_by_id(post_id).is_locked(), false);
+
+        codex_extrinsic_test_fixture.call_extrinsic_and_assert();
+        run_to_block(86410);
+
+        assert_eq!(Blog::post_by_id(post_id).is_locked(), true);
+    });
+}
+
+#[test]
+fn unlock_blog_post_proposal_execution_succeeds() {
+    initial_test_ext().execute_with(|| {
+        let member_id = 10;
+        let account_id: [u8; 32] = [member_id; 32];
+        let council_budget = 5_000_000;
+
+        assert!(Council::set_budget(RawOrigin::Root.into(), council_budget).is_ok());
+
+        let post_id = Blog::post_count();
+        Blog::create_post(RawOrigin::Root.into(), vec![0u8], vec![0u8]).unwrap();
+        Blog::lock_post(RawOrigin::Root.into(), post_id).unwrap();
+
+        let codex_extrinsic_test_fixture = CodexProposalTestFixture::default_for_call(|| {
+            let general_proposal_parameters = GeneralProposalParameters::<Runtime> {
+                member_id: member_id.into(),
+                title: b"title".to_vec(),
+                description: b"body".to_vec(),
+                staking_account_id: Some(account_id.into()),
+                exact_execution_block: None,
+            };
+
+            ProposalCodex::create_proposal(
+                RawOrigin::Signed(account_id.clone().into()).into(),
+                general_proposal_parameters,
+                ProposalDetails::UnlockBlogPost(post_id),
+            )
+        })
+        .with_member_id(member_id as u64);
+
+        assert_eq!(Blog::post_by_id(0).is_locked(), true);
+
+        codex_extrinsic_test_fixture.call_extrinsic_and_assert();
+        run_to_block(86410);
+
+        assert_eq!(Blog::post_by_id(0).is_locked(), false);
+    });
+}
+
 #[test]
 fn set_validator_count_proposal_execution_succeeds() {
     initial_test_ext().execute_with(|| {

+ 52 - 0
runtime/src/weights/blog.rs

@@ -0,0 +1,52 @@
+//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 2.0.0
+
+#![allow(unused_parens)]
+#![allow(unused_imports)]
+
+use frame_support::weights::{constants::RocksDbWeight as DbWeight, Weight};
+
+pub struct WeightInfo;
+impl blog::WeightInfo for WeightInfo {
+    fn create_post(t: u32, b: u32) -> Weight {
+        (355_638_000 as Weight)
+            .saturating_add((151_000 as Weight).saturating_mul(t as Weight))
+            .saturating_add((189_000 as Weight).saturating_mul(b as Weight))
+            .saturating_add(DbWeight::get().reads(1 as Weight))
+            .saturating_add(DbWeight::get().writes(2 as Weight))
+    }
+    fn lock_post() -> Weight {
+        (205_726_000 as Weight)
+            .saturating_add(DbWeight::get().reads(1 as Weight))
+            .saturating_add(DbWeight::get().writes(1 as Weight))
+    }
+    fn unlock_post() -> Weight {
+        (200_216_000 as Weight)
+            .saturating_add(DbWeight::get().reads(1 as Weight))
+            .saturating_add(DbWeight::get().writes(1 as Weight))
+    }
+    fn edit_post(t: u32, b: u32) -> Weight {
+        (497_924_000 as Weight)
+            .saturating_add((142_000 as Weight).saturating_mul(t as Weight))
+            .saturating_add((197_000 as Weight).saturating_mul(b as Weight))
+            .saturating_add(DbWeight::get().reads(1 as Weight))
+            .saturating_add(DbWeight::get().writes(1 as Weight))
+    }
+    fn create_reply_to_post(t: u32) -> Weight {
+        (354_299_000 as Weight)
+            .saturating_add((245_000 as Weight).saturating_mul(t as Weight))
+            .saturating_add(DbWeight::get().reads(2 as Weight))
+            .saturating_add(DbWeight::get().writes(2 as Weight))
+    }
+    fn create_reply_to_reply(t: u32) -> Weight {
+        (407_973_000 as Weight)
+            .saturating_add((245_000 as Weight).saturating_mul(t as Weight))
+            .saturating_add(DbWeight::get().reads(3 as Weight))
+            .saturating_add(DbWeight::get().writes(2 as Weight))
+    }
+    fn edit_reply(t: u32) -> Weight {
+        (317_997_000 as Weight)
+            .saturating_add((252_000 as Weight).saturating_mul(t as Weight))
+            .saturating_add(DbWeight::get().reads(3 as Weight))
+            .saturating_add(DbWeight::get().writes(1 as Weight))
+    }
+}

+ 1 - 0
runtime/src/weights/mod.rs

@@ -24,6 +24,7 @@ pub mod pallet_timestamp;
 pub mod pallet_utility;
 
 // Joystream pallets
+pub mod blog;
 pub mod council;
 pub mod forum;
 pub mod membership;

+ 112 - 82
runtime/src/weights/proposals_codex.rs

@@ -8,184 +8,214 @@ use frame_support::weights::{constants::RocksDbWeight as DbWeight, Weight};
 pub struct WeightInfo;
 impl proposals_codex::WeightInfo for WeightInfo {
     fn execute_signal_proposal(i: u32) -> Weight {
-        (16_042_000 as Weight).saturating_add((41_000 as Weight).saturating_mul(i as Weight))
+        (14_205_000 as Weight).saturating_add((36_000 as Weight).saturating_mul(i as Weight))
     }
     fn create_proposal_signal(i: u32, t: u32, d: u32) -> Weight {
-        (0 as Weight)
-            .saturating_add((296_000 as Weight).saturating_mul(i as Weight))
-            .saturating_add((10_652_000 as Weight).saturating_mul(t as Weight))
-            .saturating_add((14_000 as Weight).saturating_mul(d as Weight))
+        (381_500_000 as Weight)
+            .saturating_add((280_000 as Weight).saturating_mul(i as Weight))
+            .saturating_add((4_369_000 as Weight).saturating_mul(t as Weight))
+            .saturating_add((56_000 as Weight).saturating_mul(d as Weight))
             .saturating_add(DbWeight::get().reads(6 as Weight))
             .saturating_add(DbWeight::get().writes(10 as Weight))
     }
-    // WARNING! Some components were not used: ["d"]
-    fn create_proposal_runtime_upgrade(i: u32, t: u32) -> Weight {
-        (0 as Weight)
-            .saturating_add((370_000 as Weight).saturating_mul(i as Weight))
-            .saturating_add((34_716_000 as Weight).saturating_mul(t as Weight))
+    fn create_proposal_runtime_upgrade(i: u32, t: u32, d: u32) -> Weight {
+        (417_621_000 as Weight)
+            .saturating_add((282_000 as Weight).saturating_mul(i as Weight))
+            .saturating_add((2_246_000 as Weight).saturating_mul(t as Weight))
+            .saturating_add((47_000 as Weight).saturating_mul(d as Weight))
             .saturating_add(DbWeight::get().reads(6 as Weight))
             .saturating_add(DbWeight::get().writes(10 as Weight))
     }
     // WARNING! Some components were not used: ["t", "d"]
-    fn create_proposal_funding_request() -> Weight {
-        (748_303_000 as Weight)
-            .saturating_add(DbWeight::get().reads(7 as Weight))
+    fn create_proposal_funding_request(i: u32) -> Weight {
+        (1_073_701_000 as Weight)
+            .saturating_add((20_213_000 as Weight).saturating_mul(i as Weight))
+            .saturating_add(DbWeight::get().reads(6 as Weight))
             .saturating_add(DbWeight::get().writes(10 as Weight))
     }
-    // WARNING! Some components were not used: ["t", "d"]
-    fn create_proposal_set_max_validator_count() -> Weight {
-        (731_233_000 as Weight)
+    // WARNING! Some components were not used: ["t"]
+    fn create_proposal_set_max_validator_count(d: u32) -> Weight {
+        (680_435_000 as Weight)
+            .saturating_add((1_000 as Weight).saturating_mul(d as Weight))
             .saturating_add(DbWeight::get().reads(7 as Weight))
             .saturating_add(DbWeight::get().writes(10 as Weight))
     }
-    // WARNING! Some components were not used: ["d"]
-    fn create_proposal_create_working_group_lead_opening(i: u32, t: u32) -> Weight {
-        (0 as Weight)
-            .saturating_add((485_000 as Weight).saturating_mul(i as Weight))
-            .saturating_add((23_755_000 as Weight).saturating_mul(t as Weight))
+    // WARNING! Some components were not used: ["t", "d"]
+    fn create_proposal_create_working_group_lead_opening(i: u32) -> Weight {
+        (1_658_073_000 as Weight)
+            .saturating_add((401_000 as Weight).saturating_mul(i as Weight))
             .saturating_add(DbWeight::get().reads(6 as Weight))
             .saturating_add(DbWeight::get().writes(10 as Weight))
     }
     // WARNING! Some components were not used: ["t", "d"]
     fn create_proposal_fill_working_group_lead_opening() -> Weight {
-        (999_000_000 as Weight)
+        (688_697_000 as Weight)
             .saturating_add(DbWeight::get().reads(6 as Weight))
             .saturating_add(DbWeight::get().writes(10 as Weight))
     }
-    fn create_proposal_update_working_group_budget(t: u32, d: u32) -> Weight {
-        (420_054_000 as Weight)
-            .saturating_add((5_853_000 as Weight).saturating_mul(t as Weight))
-            .saturating_add((76_000 as Weight).saturating_mul(d as Weight))
+    // WARNING! Some components were not used: ["t"]
+    fn create_proposal_update_working_group_budget(d: u32) -> Weight {
+        (677_232_000 as Weight)
+            .saturating_add((1_000 as Weight).saturating_mul(d as Weight))
+            .saturating_add(DbWeight::get().reads(6 as Weight))
+            .saturating_add(DbWeight::get().writes(10 as Weight))
+    }
+    fn create_proposal_decrease_working_group_lead_stake(t: u32, d: u32) -> Weight {
+        (673_696_000 as Weight)
+            .saturating_add((57_000 as Weight).saturating_mul(t as Weight))
+            .saturating_add((1_000 as Weight).saturating_mul(d as Weight))
+            .saturating_add(DbWeight::get().reads(6 as Weight))
+            .saturating_add(DbWeight::get().writes(10 as Weight))
+    }
+    fn create_proposal_slash_working_group_lead(t: u32, d: u32) -> Weight {
+        (665_717_000 as Weight)
+            .saturating_add((416_000 as Weight).saturating_mul(t as Weight))
+            .saturating_add((2_000 as Weight).saturating_mul(d as Weight))
             .saturating_add(DbWeight::get().reads(6 as Weight))
             .saturating_add(DbWeight::get().writes(10 as Weight))
     }
     // WARNING! Some components were not used: ["t"]
-    fn create_proposal_decrease_working_group_lead_stake(d: u32) -> Weight {
-        (778_605_000 as Weight)
-            .saturating_add((12_000 as Weight).saturating_mul(d as Weight))
+    fn create_proposal_set_working_group_lead_reward(d: u32) -> Weight {
+        (681_995_000 as Weight)
+            .saturating_add((10_000 as Weight).saturating_mul(d as Weight))
             .saturating_add(DbWeight::get().reads(6 as Weight))
             .saturating_add(DbWeight::get().writes(10 as Weight))
     }
     // WARNING! Some components were not used: ["t", "d"]
-    fn create_proposal_slash_working_group_lead() -> Weight {
-        (758_609_000 as Weight)
+    fn create_proposal_terminate_working_group_lead() -> Weight {
+        (672_205_000 as Weight)
             .saturating_add(DbWeight::get().reads(6 as Weight))
             .saturating_add(DbWeight::get().writes(10 as Weight))
     }
-    // WARNING! Some components were not used: ["d"]
-    fn create_proposal_set_working_group_lead_reward(t: u32) -> Weight {
-        (641_272_000 as Weight)
-            .saturating_add((53_000 as Weight).saturating_mul(t as Weight))
+    fn create_proposal_amend_constitution(i: u32, t: u32, d: u32) -> Weight {
+        (415_625_000 as Weight)
+            .saturating_add((298_000 as Weight).saturating_mul(i as Weight))
+            .saturating_add((3_229_000 as Weight).saturating_mul(t as Weight))
+            .saturating_add((34_000 as Weight).saturating_mul(d as Weight))
             .saturating_add(DbWeight::get().reads(6 as Weight))
             .saturating_add(DbWeight::get().writes(10 as Weight))
     }
-    // WARNING! Some components were not used: ["d"]
-    fn create_proposal_terminate_working_group_lead(t: u32) -> Weight {
-        (628_976_000 as Weight)
-            .saturating_add((1_434_000 as Weight).saturating_mul(t as Weight))
+    fn create_proposal_cancel_working_group_lead_opening(t: u32, d: u32) -> Weight {
+        (650_522_000 as Weight)
+            .saturating_add((406_000 as Weight).saturating_mul(t as Weight))
+            .saturating_add((5_000 as Weight).saturating_mul(d as Weight))
             .saturating_add(DbWeight::get().reads(6 as Weight))
             .saturating_add(DbWeight::get().writes(10 as Weight))
     }
-    // WARNING! Some components were not used: ["d"]
-    fn create_proposal_amend_constitution(i: u32, t: u32) -> Weight {
-        (301_477_000 as Weight)
-            .saturating_add((328_000 as Weight).saturating_mul(i as Weight))
-            .saturating_add((5_947_000 as Weight).saturating_mul(t as Weight))
+    // WARNING! Some components were not used: ["t", "d"]
+    fn create_proposal_set_membership_price() -> Weight {
+        (688_555_000 as Weight)
             .saturating_add(DbWeight::get().reads(6 as Weight))
             .saturating_add(DbWeight::get().writes(10 as Weight))
     }
-    // WARNING! Some components were not used: ["t"]
-    fn create_proposal_cancel_working_group_lead_opening(d: u32) -> Weight {
-        (717_016_000 as Weight)
-            .saturating_add((4_000 as Weight).saturating_mul(d as Weight))
+    // WARNING! Some components were not used: ["t", "d"]
+    fn create_proposal_set_council_budget_increment() -> Weight {
+        (677_145_000 as Weight)
             .saturating_add(DbWeight::get().reads(6 as Weight))
             .saturating_add(DbWeight::get().writes(10 as Weight))
     }
-    fn create_proposal_set_membership_price(t: u32, d: u32) -> Weight {
-        (648_039_000 as Weight)
-            .saturating_add((350_000 as Weight).saturating_mul(t as Weight))
-            .saturating_add((3_000 as Weight).saturating_mul(d as Weight))
+    // WARNING! Some components were not used: ["d"]
+    fn create_proposal_set_councilor_reward(t: u32) -> Weight {
+        (691_635_000 as Weight)
+            .saturating_add((538_000 as Weight).saturating_mul(t as Weight))
             .saturating_add(DbWeight::get().reads(6 as Weight))
             .saturating_add(DbWeight::get().writes(10 as Weight))
     }
-    fn create_proposal_set_council_budget_increment(t: u32, d: u32) -> Weight {
-        (544_120_000 as Weight)
-            .saturating_add((1_986_000 as Weight).saturating_mul(t as Weight))
-            .saturating_add((22_000 as Weight).saturating_mul(d as Weight))
+    fn create_proposal_set_initial_invitation_balance(t: u32, d: u32) -> Weight {
+        (647_455_000 as Weight)
+            .saturating_add((605_000 as Weight).saturating_mul(t as Weight))
+            .saturating_add((6_000 as Weight).saturating_mul(d as Weight))
             .saturating_add(DbWeight::get().reads(6 as Weight))
             .saturating_add(DbWeight::get().writes(10 as Weight))
     }
-    // WARNING! Some components were not used: ["t"]
-    fn create_proposal_set_councilor_reward(d: u32) -> Weight {
-        (679_828_000 as Weight)
-            .saturating_add((8_000 as Weight).saturating_mul(d as Weight))
+    // WARNING! Some components were not used: ["t", "d"]
+    fn create_proposal_set_initial_invitation_count() -> Weight {
+        (708_072_000 as Weight)
             .saturating_add(DbWeight::get().reads(6 as Weight))
             .saturating_add(DbWeight::get().writes(10 as Weight))
     }
-    // WARNING! Some components were not used: ["t"]
-    fn create_proposal_set_initial_invitation_balance(d: u32) -> Weight {
-        (647_942_000 as Weight)
-            .saturating_add((7_000 as Weight).saturating_mul(d as Weight))
+    // WARNING! Some components were not used: ["d"]
+    fn create_proposal_set_membership_lead_invitation_quota(t: u32) -> Weight {
+        (729_756_000 as Weight)
+            .saturating_add((462_000 as Weight).saturating_mul(t as Weight))
             .saturating_add(DbWeight::get().reads(6 as Weight))
             .saturating_add(DbWeight::get().writes(10 as Weight))
     }
-    fn create_proposal_set_initial_invitation_count(t: u32, d: u32) -> Weight {
-        (622_013_000 as Weight)
-            .saturating_add((2_003_000 as Weight).saturating_mul(t as Weight))
-            .saturating_add((7_000 as Weight).saturating_mul(d as Weight))
+    // WARNING! Some components were not used: ["d"]
+    fn create_proposal_set_referral_cut(t: u32) -> Weight {
+        (669_708_000 as Weight)
+            .saturating_add((263_000 as Weight).saturating_mul(t as Weight))
             .saturating_add(DbWeight::get().reads(6 as Weight))
             .saturating_add(DbWeight::get().writes(10 as Weight))
     }
-    // WARNING! Some components were not used: ["t", "d"]
-    fn create_proposal_set_membership_lead_invitation_quota() -> Weight {
-        (688_083_000 as Weight)
+    fn create_proposal_create_blog_post(t: u32, d: u32, h: u32, b: u32) -> Weight {
+        (0 as Weight)
+            .saturating_add((26_565_000 as Weight).saturating_mul(t as Weight))
+            .saturating_add((473_000 as Weight).saturating_mul(d as Weight))
+            .saturating_add((389_000 as Weight).saturating_mul(h as Weight))
+            .saturating_add((392_000 as Weight).saturating_mul(b as Weight))
+            .saturating_add(DbWeight::get().reads(6 as Weight))
+            .saturating_add(DbWeight::get().writes(10 as Weight))
+    }
+    fn create_proposal_edit_blog_post(t: u32, d: u32, h: u32, b: u32) -> Weight {
+        (0 as Weight)
+            .saturating_add((51_663_000 as Weight).saturating_mul(t as Weight))
+            .saturating_add((415_000 as Weight).saturating_mul(d as Weight))
+            .saturating_add((381_000 as Weight).saturating_mul(h as Weight))
+            .saturating_add((408_000 as Weight).saturating_mul(b as Weight))
             .saturating_add(DbWeight::get().reads(6 as Weight))
             .saturating_add(DbWeight::get().writes(10 as Weight))
     }
     // WARNING! Some components were not used: ["d"]
-    fn create_proposal_set_referral_cut(t: u32) -> Weight {
-        (638_572_000 as Weight)
-            .saturating_add((353_000 as Weight).saturating_mul(t as Weight))
+    fn create_proposal_lock_blog_post(t: u32) -> Weight {
+        (732_383_000 as Weight)
+            .saturating_add((13_000 as Weight).saturating_mul(t as Weight))
+            .saturating_add(DbWeight::get().reads(6 as Weight))
+            .saturating_add(DbWeight::get().writes(10 as Weight))
+    }
+    // WARNING! Some components were not used: ["t", "d"]
+    fn create_proposal_unlock_blog_post() -> Weight {
+        (670_647_000 as Weight)
             .saturating_add(DbWeight::get().reads(6 as Weight))
             .saturating_add(DbWeight::get().writes(10 as Weight))
     }
     fn update_working_group_budget_positive_forum() -> Weight {
-        (101_370_000 as Weight)
+        (102_611_000 as Weight)
             .saturating_add(DbWeight::get().reads(2 as Weight))
             .saturating_add(DbWeight::get().writes(2 as Weight))
     }
     fn update_working_group_budget_negative_forum() -> Weight {
-        (101_227_000 as Weight)
+        (102_707_000 as Weight)
             .saturating_add(DbWeight::get().reads(2 as Weight))
             .saturating_add(DbWeight::get().writes(2 as Weight))
     }
     fn update_working_group_budget_positive_storage() -> Weight {
-        (101_047_000 as Weight)
+        (102_791_000 as Weight)
             .saturating_add(DbWeight::get().reads(2 as Weight))
             .saturating_add(DbWeight::get().writes(2 as Weight))
     }
     fn update_working_group_budget_negative_storage() -> Weight {
-        (101_188_000 as Weight)
+        (102_502_000 as Weight)
             .saturating_add(DbWeight::get().reads(2 as Weight))
             .saturating_add(DbWeight::get().writes(2 as Weight))
     }
     fn update_working_group_budget_positive_content() -> Weight {
-        (101_790_000 as Weight)
+        (103_375_000 as Weight)
             .saturating_add(DbWeight::get().reads(2 as Weight))
             .saturating_add(DbWeight::get().writes(2 as Weight))
     }
     fn update_working_group_budget_negative_content() -> Weight {
-        (101_742_000 as Weight)
+        (102_741_000 as Weight)
             .saturating_add(DbWeight::get().reads(2 as Weight))
             .saturating_add(DbWeight::get().writes(2 as Weight))
     }
     fn update_working_group_budget_positive_membership() -> Weight {
-        (100_238_000 as Weight)
+        (103_449_000 as Weight)
             .saturating_add(DbWeight::get().reads(2 as Weight))
             .saturating_add(DbWeight::get().writes(2 as Weight))
     }
     fn update_working_group_budget_negative_membership() -> Weight {
-        (100_460_000 as Weight)
+        (102_274_000 as Weight)
             .saturating_add(DbWeight::get().reads(2 as Weight))
             .saturating_add(DbWeight::get().writes(2 as Weight))
     }

+ 2 - 1
scripts/generate-weights.sh

@@ -53,4 +53,5 @@ benchmark working_group
 benchmark council
 benchmark referendum
 benchmark forum
-benchmark membership
+benchmark membership
+benchmark blog