Browse Source

runtime: storage-v2: Add data size fee for uploading.

- add new extrinsic `update_data_size_fee`
- add storage field `DataObjectPerMegabyteFee`
- refactor static bags (remove separate council and working groups storage fields)
- slash data size fee on uploading
Shamil Gadelshin 3 years ago
parent
commit
43b9fa44d0

+ 69 - 36
runtime-modules/storage-v2/src/lib.rs

@@ -5,20 +5,16 @@
 #![cfg_attr(not(feature = "std"), no_std)]
 #![warn(missing_docs)]
 
-// TODO:How to create a dynamic bag? -
-//- new methods to be called from another modules: a method create dynamic bag.
-
+// TODO: Remove static and dynamic storage helpers.
 // TODO: Remove old Storage pallet.
 // TODO: authentication_key
 // TODO: Check dynamic bag existence.
 // TODO: use StorageBucket.accepting_new_bags
-// TODO: merge council and WG storage bags.
 // TODO: add dynamic bag creation policy.
 // TODO: add module comment
-// TODO: add benchmarks
-// TODO: adjust constants
 // TODO: max storage buckets for bags? do we need that?
 // TODO: make public methods as root extrinsics to enable storage-node dev mode.
+// TODO: make public methods "weight-ready".
 
 /// TODO: convert to variable
 pub const MAX_OBJECT_SIZE_LIMIT: u64 = 100;
@@ -39,7 +35,7 @@ use frame_support::traits::{Currency, ExistenceRequirement, Get};
 use frame_support::{decl_error, decl_event, decl_module, decl_storage, ensure, Parameter};
 #[cfg(feature = "std")]
 use serde::{Deserialize, Serialize};
-use sp_arithmetic::traits::{BaseArithmetic, One};
+use sp_arithmetic::traits::{BaseArithmetic, One, Zero};
 use sp_runtime::traits::{AccountIdConversion, MaybeSerialize, Member, Saturating};
 use sp_runtime::{ModuleId, SaturatedConversion};
 use sp_std::collections::btree_map::BTreeMap;
@@ -640,12 +636,9 @@ decl_storage! {
         /// Defines whether all new uploads blocked
         pub UploadingBlocked get(fn uploading_blocked): bool;
 
-        /// Council bag.
-        pub CouncilBag get(fn council_bag): StaticBag<T>;
-
-        /// Working group bag storage map.
-        pub WorkingGroupBags get(fn working_group_bag): map hasher(blake2_128_concat)
-            WorkingGroup => StaticBag<T>;
+        /// Working groups' and council's bags storage map.
+        pub StaticBags get(fn static_bag): map hasher(blake2_128_concat)
+            StaticBagId => StaticBag<T>;
 
         /// Dynamic bag storage map.
         pub DynamicBags get (fn dynamic_bag_by_id): map hasher(blake2_128_concat)
@@ -669,6 +662,9 @@ decl_storage! {
 
         /// Blacklist collection counter.
         pub CurrentBlacklistSize get (fn current_blacklist_size): u64;
+
+        /// Size based pricing of new objects uploaded.
+        pub DataObjectPerMegabyteFee get (fn data_object_per_mega_byte_fee): BalanceOf<T>;
     }
 }
 
@@ -743,11 +739,16 @@ decl_event! {
         /// - storage bucket ID
         StorageBucketOperatorRemoved(StorageBucketId),
 
-        /// Emits on changing the global uploading block status.
+        /// Emits on changing the size-based pricing of new objects uploaded.
         /// Params
         /// - new status
         UploadingBlockStatusUpdated(bool),
 
+        /// Emits on changing the size-based pricing of new objects uploaded.
+        /// Params
+        /// - new data size fee
+        DataObjectPerMegabyteFeeUpdated(Balance),
+
         /// Emits on moving data objects between bags.
         /// Params
         /// - source bag ID
@@ -957,6 +958,20 @@ decl_module! {
             Self::deposit_event(RawEvent::UploadingBlockStatusUpdated(new_status));
         }
 
+        /// Updates size-based pricing of new objects uploaded.
+        #[weight = 10_000_000] // TODO: adjust weight
+        pub fn update_data_size_fee(origin, new_data_size_fee: BalanceOf<T>) {
+            T::ensure_working_group_leader_origin(origin)?;
+
+            //
+            // == MUTATION SAFE ==
+            //
+
+            DataObjectPerMegabyteFee::<T>::put(new_data_size_fee);
+
+            Self::deposit_event(RawEvent::DataObjectPerMegabyteFeeUpdated(new_data_size_fee));
+        }
+
         /// Add and remove hashes to the current blacklist.
         #[weight = 10_000_000] // TODO: adjust weight
         pub fn update_blacklist(
@@ -1304,6 +1319,11 @@ impl<T: Trait> DataObjectStorage<T> for Module<T> {
             data.total_deletion_prize,
         )?;
 
+        Self::slash_data_size_fee(
+            &params.deletion_prize_source_account_id,
+            voucher_update.objects_total_size,
+        );
+
         <NextDataObjectId<T>>::put(data.next_data_object_id);
 
         BagManager::<T>::append_data_objects(&params.bag_id, &data.data_objects_map);
@@ -1605,22 +1625,9 @@ impl<T: Trait> Module<T> {
         }
     }
 
-    // Get static bag by its ID from the storage.
-    pub(crate) fn static_bag(bag_id: &StaticBagId) -> StaticBag<T> {
-        match bag_id {
-            StaticBagId::Council => Self::council_bag(),
-            StaticBagId::WorkingGroup(working_group) => Self::working_group_bag(working_group),
-        }
-    }
-
     // Save static bag to the storage.
     fn save_static_bag(bag_id: &StaticBagId, bag: StaticBag<T>) {
-        match bag_id {
-            StaticBagId::Council => CouncilBag::<T>::put(bag),
-            StaticBagId::WorkingGroup(working_group) => {
-                <WorkingGroupBags<T>>::insert(working_group, bag)
-            }
-        }
+        <StaticBags<T>>::insert(bag_id, bag)
     }
 
     // Create data objects from the creation data.
@@ -1936,19 +1943,20 @@ impl<T: Trait> Module<T> {
             );
         }
 
+        let new_objects_total_size: u64 =
+            params.object_creation_list.iter().map(|obj| obj.size).sum();
+
         let total_deletion_prize: BalanceOf<T> =
             new_objects_number.saturated_into::<BalanceOf<T>>() * T::DataObjectDeletionPrize::get();
+
+        let size_fee = Self::calculate_data_storage_fee(new_objects_total_size);
+
         let usable_balance =
             Balances::<T>::usable_balance(&params.deletion_prize_source_account_id);
 
-        // Check account balance to satisfy deletion prize.
-        ensure!(
-            usable_balance >= total_deletion_prize,
-            Error::<T>::InsufficientBalance
-        );
-
-        let new_objects_total_size: u64 =
-            params.object_creation_list.iter().map(|obj| obj.size).sum();
+        // Check account balance to satisfy deletion prize and storage fee.
+        let total_fee = total_deletion_prize + size_fee;
+        ensure!(usable_balance >= total_fee, Error::<T>::InsufficientBalance);
 
         let voucher_update = VoucherUpdate {
             objects_number: new_objects_number,
@@ -2031,4 +2039,29 @@ impl<T: Trait> Module<T> {
             Self::change_deletion_prize_for_dynamic_bag(dynamic_bag_id, deletion_prize, operation);
         }
     }
+
+    // Calculate data storage fee based on size. Fee-value uses megabytes as measure value.
+    // Data size will be rounded to nearest greater MB integer.
+    pub(crate) fn calculate_data_storage_fee(bytes: u64) -> BalanceOf<T> {
+        let mb_fee = Self::data_object_per_mega_byte_fee();
+
+        const ONE_MB: u64 = 1_048_576;
+
+        let mut megabytes = bytes / ONE_MB;
+
+        if bytes % ONE_MB > 0 {
+            megabytes += 1; // rounding to the nearest greater integer
+        }
+
+        mb_fee.saturating_mul(megabytes.saturated_into())
+    }
+
+    // Slash data size fee if fee value is set to non-zero.
+    fn slash_data_size_fee(account_id: &T::AccountId, bytes: u64) {
+        let fee = Self::calculate_data_storage_fee(bytes);
+
+        if fee != Zero::zero() {
+            let _ = Balances::<T>::slash(account_id, fee);
+        }
+    }
 }

+ 36 - 0
runtime-modules/storage-v2/src/tests/fixtures.rs

@@ -856,3 +856,39 @@ impl RemoveStorageBucketOperatorFixture {
         }
     }
 }
+
+pub struct UpdateDataObjectPerMegabyteFeeFixture {
+    origin: RawOrigin<u64>,
+    new_fee: u64,
+}
+
+impl UpdateDataObjectPerMegabyteFeeFixture {
+    pub fn default() -> Self {
+        Self {
+            origin: RawOrigin::Signed(WG_LEADER_ACCOUNT_ID),
+            new_fee: 0,
+        }
+    }
+
+    pub fn with_origin(self, origin: RawOrigin<u64>) -> Self {
+        Self { origin, ..self }
+    }
+
+    pub fn with_new_fee(self, new_fee: u64) -> Self {
+        Self { new_fee, ..self }
+    }
+
+    pub fn call_and_assert(&self, expected_result: DispatchResult) {
+        let old_fee = Storage::data_object_per_mega_byte_fee();
+
+        let actual_result = Storage::update_data_size_fee(self.origin.clone().into(), self.new_fee);
+
+        assert_eq!(actual_result, expected_result);
+
+        if actual_result.is_ok() {
+            assert_eq!(Storage::data_object_per_mega_byte_fee(), self.new_fee);
+        } else {
+            assert_eq!(old_fee, Storage::data_object_per_mega_byte_fee());
+        }
+    }
+}

+ 189 - 66
runtime-modules/storage-v2/src/tests/mod.rs

@@ -14,9 +14,10 @@ use sp_std::iter::FromIterator;
 use common::working_group::WorkingGroup;
 
 use crate::{
-    AssignedDataObject, BagId, DataObject, DataObjectCreationParameters, DynamicBagId, Error,
-    ModuleAccount, ObjectsInBagParams, RawEvent, StaticBagId, StorageBucketOperatorStatus,
-    StorageTreasury, UpdateStorageBucketForBagsParams, UploadParameters, Voucher,
+    AssignedDataObject, BagId, DataObject, DataObjectCreationParameters, DataObjectStorage,
+    DynamicBagId, Error, ModuleAccount, ObjectsInBagParams, RawEvent, StaticBagId,
+    StorageBucketOperatorStatus, StorageTreasury, UpdateStorageBucketForBagsParams,
+    UploadParameters, Voucher,
 };
 
 use mocks::{
@@ -283,7 +284,7 @@ fn update_storage_buckets_for_bags_succeeded() {
             .with_params(params.clone())
             .call_and_assert(Ok(()));
 
-        let bag = Storage::council_bag();
+        let bag = Storage::static_bag(StaticBagId::Council);
         assert_eq!(bag.stored_by, buckets);
 
         EventFixture::assert_last_crate_event(RawEvent::StorageBucketsUpdatedForBags(params));
@@ -338,7 +339,7 @@ fn update_storage_buckets_for_bags_succeeded_with_voucher_usage() {
             .with_params(params.clone())
             .call_and_assert(Ok(()));
 
-        let bag = Storage::council_bag();
+        let bag = Storage::static_bag(StaticBagId::Council);
         assert_eq!(bag.stored_by, new_buckets);
 
         //// Check vouchers
@@ -576,7 +577,7 @@ fn upload_succeeded() {
 
         // check bag content
         let data_object_id = 0u64;
-        let bag = Storage::council_bag();
+        let bag = Storage::static_bag(StaticBagId::Council);
         assert_eq!(
             bag.objects.iter().collect::<Vec<_>>(),
             vec![(
@@ -607,78 +608,38 @@ fn upload_succeeded() {
 }
 
 #[test]
-fn deletion_prize_changed_event_fired() {
+fn upload_succeeded_with_data_size_fee() {
     build_test_externalities().execute_with(|| {
-        let starting_block = 1;
-        run_to_block(starting_block);
-
         let initial_balance = 1000;
         increase_account_balance(&DEFAULT_MEMBER_ACCOUNT_ID, initial_balance);
 
-        let dynamic_bag = DynamicBagId::<Test>::Member(DEFAULT_MEMBER_ID);
+        let data_size_fee = 100;
 
-        let upload_params = UploadParameters::<Test> {
-            bag_id: BagId::<Test>::DynamicBag(dynamic_bag.clone()),
-            authentication_key: Vec::new(),
-            deletion_prize_source_account_id: DEFAULT_MEMBER_ACCOUNT_ID,
-            object_creation_list: create_single_data_object(),
-        };
-
-        UploadFixture::default()
-            .with_params(upload_params.clone())
+        UpdateDataObjectPerMegabyteFeeFixture::default()
+            .with_origin(RawOrigin::Signed(WG_LEADER_ACCOUNT_ID))
+            .with_new_fee(data_size_fee)
             .call_and_assert(Ok(()));
 
-        EventFixture::contains_crate_event(RawEvent::DeletionPrizeChanged(
-            dynamic_bag,
-            DataObjectDeletionPrize::get(),
-        ));
-    });
-}
-
-#[test]
-fn storage_bucket_voucher_changed_event_fired() {
-    build_test_externalities().execute_with(|| {
-        let starting_block = 1;
-        run_to_block(starting_block);
-
-        let dynamic_bag = DynamicBagId::<Test>::Member(DEFAULT_MEMBER_ID);
-
-        let bag_id = BagId::<Test>::DynamicBag(dynamic_bag.clone());
-        let objects_limit = 1;
-        let size_limit = 100;
-
-        let bucket_id = create_storage_bucket_and_assign_to_bag(
-            bag_id.clone(),
-            None,
-            objects_limit,
-            size_limit,
-        );
-
-        let object_creation_list = create_single_data_object();
-
-        let initial_balance = 1000;
-        increase_account_balance(&DEFAULT_MEMBER_ACCOUNT_ID, initial_balance);
-
         let upload_params = UploadParameters::<Test> {
-            bag_id: bag_id.clone(),
+            bag_id: BagId::<Test>::StaticBag(StaticBagId::Council),
             authentication_key: Vec::new(),
             deletion_prize_source_account_id: DEFAULT_MEMBER_ACCOUNT_ID,
-            object_creation_list: object_creation_list.clone(),
+            object_creation_list: create_single_data_object(),
         };
 
         UploadFixture::default()
             .with_params(upload_params.clone())
             .call_and_assert(Ok(()));
 
-        EventFixture::contains_crate_event(RawEvent::VoucherChanged(
-            bucket_id,
-            Voucher {
-                objects_limit,
-                size_limit,
-                objects_used: 1,
-                size_used: object_creation_list[0].size,
-            },
-        ));
+        // check balances
+        assert_eq!(
+            Balances::usable_balance(&DEFAULT_MEMBER_ACCOUNT_ID),
+            initial_balance - DataObjectDeletionPrize::get() - data_size_fee
+        );
+        assert_eq!(
+            Balances::usable_balance(&<StorageTreasury<Test>>::module_account_id()),
+            DataObjectDeletionPrize::get()
+        );
     });
 }
 
@@ -851,7 +812,7 @@ fn upload_succeeded_with_non_empty_bag() {
             .with_params(upload_params2.clone())
             .call_and_assert(Ok(()));
 
-        let bag = Storage::council_bag();
+        let bag = Storage::static_bag(StaticBagId::Council);
         assert_eq!(bag.objects.len(), 4);
     });
 }
@@ -940,6 +901,34 @@ fn upload_fails_with_insufficient_balance_for_deletion_prize() {
     });
 }
 
+#[test]
+fn upload_fails_with_insufficient_balance_for_data_size_fee() {
+    build_test_externalities().execute_with(|| {
+        increase_account_balance(&DEFAULT_MEMBER_ACCOUNT_ID, DataObjectDeletionPrize::get());
+
+        let upload_params = UploadParameters::<Test> {
+            bag_id: BagId::<Test>::StaticBag(StaticBagId::Council),
+            authentication_key: Vec::new(),
+            deletion_prize_source_account_id: DEFAULT_MEMBER_ACCOUNT_ID,
+            object_creation_list: create_single_data_object(),
+        };
+
+        // Check that balance is sufficient for the deletion prize.
+        assert_eq!(Storage::can_upload_data_objects(&upload_params), Ok(()));
+
+        let data_size_fee = 1000;
+
+        UpdateDataObjectPerMegabyteFeeFixture::default()
+            .with_origin(RawOrigin::Signed(WG_LEADER_ACCOUNT_ID))
+            .with_new_fee(data_size_fee)
+            .call_and_assert(Ok(()));
+
+        UploadFixture::default()
+            .with_params(upload_params)
+            .call_and_assert(Err(Error::<Test>::InsufficientBalance.into()));
+    });
+}
+
 #[test]
 fn upload_failed_with_blocked_uploading() {
     build_test_externalities().execute_with(|| {
@@ -1144,7 +1133,7 @@ fn accept_pending_data_objects_succeeded() {
             assigned_data_objects: objects,
         };
 
-        let bag = Storage::council_bag();
+        let bag = Storage::static_bag(StaticBagId::Council);
         // Check `accepted` flag for the fist data object in the bag.
         assert_eq!(bag.objects.iter().collect::<Vec<_>>()[0].1.accepted, false);
 
@@ -1155,7 +1144,7 @@ fn accept_pending_data_objects_succeeded() {
             .with_params(accept_params.clone())
             .call_and_assert(Ok(()));
 
-        let bag = Storage::council_bag();
+        let bag = Storage::static_bag(StaticBagId::Council);
         // Check `accepted` flag for the fist data object in the bag.
         assert_eq!(bag.objects.iter().collect::<Vec<_>>()[0].1.accepted, true);
 
@@ -1972,7 +1961,7 @@ fn delete_data_objects_succeeded_with_voucher_usage() {
             .with_data_object_ids(data_object_ids.clone())
             .call_and_assert(Ok(()));
 
-        let bag = Storage::council_bag();
+        let bag = Storage::static_bag(StaticBagId::Council);
         assert!(!bag.objects.contains_key(&data_object_id));
 
         //// Post-check voucher
@@ -2586,3 +2575,137 @@ fn remove_storage_bucket_operator_fails_with_missing_storage_provider() {
             .call_and_assert(Err(Error::<Test>::StorageProviderMustBeSet.into()));
     });
 }
+
+#[test]
+fn update_data_size_fee_succeeded() {
+    build_test_externalities().execute_with(|| {
+        let starting_block = 1;
+        run_to_block(starting_block);
+
+        let new_fee = 1000;
+
+        UpdateDataObjectPerMegabyteFeeFixture::default()
+            .with_origin(RawOrigin::Signed(WG_LEADER_ACCOUNT_ID))
+            .with_new_fee(new_fee)
+            .call_and_assert(Ok(()));
+
+        EventFixture::assert_last_crate_event(RawEvent::DataObjectPerMegabyteFeeUpdated(new_fee));
+    });
+}
+
+#[test]
+fn update_data_size_fee_fails_with_non_leader_origin() {
+    build_test_externalities().execute_with(|| {
+        let non_leader_id = 1;
+
+        UpdateDataObjectPerMegabyteFeeFixture::default()
+            .with_origin(RawOrigin::Signed(non_leader_id))
+            .call_and_assert(Err(DispatchError::BadOrigin));
+    });
+}
+
+#[test]
+fn data_size_fee_calculation_works_properly() {
+    build_test_externalities().execute_with(|| {
+        const ONE_MB: u64 = 1_048_576;
+
+        // Fee set to zero.
+        assert_eq!(Storage::calculate_data_storage_fee(ONE_MB), 0);
+
+        let data_size_fee = 1000;
+
+        UpdateDataObjectPerMegabyteFeeFixture::default()
+            .with_origin(RawOrigin::Signed(WG_LEADER_ACCOUNT_ID))
+            .with_new_fee(data_size_fee)
+            .call_and_assert(Ok(()));
+
+        // Fee set.
+        assert_eq!(Storage::calculate_data_storage_fee(ONE_MB), data_size_fee);
+        assert_eq!(
+            Storage::calculate_data_storage_fee(2 * ONE_MB),
+            2 * data_size_fee
+        );
+
+        // Rounding works correctly.
+        assert_eq!(
+            Storage::calculate_data_storage_fee(ONE_MB + 1),
+            2 * data_size_fee
+        );
+    });
+}
+
+#[test]
+fn deletion_prize_changed_event_fired() {
+    build_test_externalities().execute_with(|| {
+        let starting_block = 1;
+        run_to_block(starting_block);
+
+        let initial_balance = 1000;
+        increase_account_balance(&DEFAULT_MEMBER_ACCOUNT_ID, initial_balance);
+
+        let dynamic_bag = DynamicBagId::<Test>::Member(DEFAULT_MEMBER_ID);
+
+        let upload_params = UploadParameters::<Test> {
+            bag_id: BagId::<Test>::DynamicBag(dynamic_bag.clone()),
+            authentication_key: Vec::new(),
+            deletion_prize_source_account_id: DEFAULT_MEMBER_ACCOUNT_ID,
+            object_creation_list: create_single_data_object(),
+        };
+
+        UploadFixture::default()
+            .with_params(upload_params.clone())
+            .call_and_assert(Ok(()));
+
+        EventFixture::contains_crate_event(RawEvent::DeletionPrizeChanged(
+            dynamic_bag,
+            DataObjectDeletionPrize::get(),
+        ));
+    });
+}
+
+#[test]
+fn storage_bucket_voucher_changed_event_fired() {
+    build_test_externalities().execute_with(|| {
+        let starting_block = 1;
+        run_to_block(starting_block);
+
+        let dynamic_bag = DynamicBagId::<Test>::Member(DEFAULT_MEMBER_ID);
+
+        let bag_id = BagId::<Test>::DynamicBag(dynamic_bag.clone());
+        let objects_limit = 1;
+        let size_limit = 100;
+
+        let bucket_id = create_storage_bucket_and_assign_to_bag(
+            bag_id.clone(),
+            None,
+            objects_limit,
+            size_limit,
+        );
+
+        let object_creation_list = create_single_data_object();
+
+        let initial_balance = 1000;
+        increase_account_balance(&DEFAULT_MEMBER_ACCOUNT_ID, initial_balance);
+
+        let upload_params = UploadParameters::<Test> {
+            bag_id: bag_id.clone(),
+            authentication_key: Vec::new(),
+            deletion_prize_source_account_id: DEFAULT_MEMBER_ACCOUNT_ID,
+            object_creation_list: object_creation_list.clone(),
+        };
+
+        UploadFixture::default()
+            .with_params(upload_params.clone())
+            .call_and_assert(Ok(()));
+
+        EventFixture::contains_crate_event(RawEvent::VoucherChanged(
+            bucket_id,
+            Voucher {
+                objects_limit,
+                size_limit,
+                objects_used: 1,
+                size_used: object_creation_list[0].size,
+            },
+        ));
+    });
+}