From 39dff77c28dda75b0951a3ce7cde9ab40f21468c Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Wed, 30 Aug 2023 16:11:40 +0200 Subject: [PATCH] Abstract sparse vector type --- Cargo.lock | 1 + pallets/dapp-staking-v3/Cargo.toml | 3 + pallets/dapp-staking-v3/src/lib.rs | 12 +- .../dapp-staking-v3/src/test/testing_utils.rs | 2 +- .../dapp-staking-v3/src/test/tests_types.rs | 115 +++---- pallets/dapp-staking-v3/src/types.rs | 309 +++++++++++------- 6 files changed, 255 insertions(+), 187 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c6c80e5d77..536ec19859 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7109,6 +7109,7 @@ dependencies = [ name = "pallet-dapp-staking-v3" version = "0.0.1-alpha" dependencies = [ + "astar-primitives", "frame-support", "frame-system", "num-traits", diff --git a/pallets/dapp-staking-v3/Cargo.toml b/pallets/dapp-staking-v3/Cargo.toml index 27f927fe6e..acc457ac8f 100644 --- a/pallets/dapp-staking-v3/Cargo.toml +++ b/pallets/dapp-staking-v3/Cargo.toml @@ -21,6 +21,8 @@ sp-io = { workspace = true } sp-runtime = { workspace = true } sp-std = { workspace = true } +astar-primitives = { workspace = true } + [dev-dependencies] pallet-balances = { workspace = true } @@ -39,4 +41,5 @@ std = [ "frame-support/std", "frame-system/std", "pallet-balances/std", + "astar-primitives/std", ] diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index b2ed5d17e4..27441cbaad 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -47,6 +47,8 @@ use frame_support::{ use frame_system::pallet_prelude::*; use sp_runtime::traits::{BadOrigin, Saturating, Zero}; +use astar_primitives::Balance; + use crate::types::*; pub use pallet::*; @@ -74,7 +76,11 @@ pub mod pallet { type RuntimeEvent: From> + IsType<::RuntimeEvent>; /// Currency used for staking. - type Currency: LockableCurrency; + type Currency: LockableCurrency< + Self::AccountId, + Moment = Self::BlockNumber, + Balance = Balance, + >; /// Describes smart contract in the context required by dApp staking. type SmartContract: Parameter + Member + MaxEncodedLen; @@ -97,7 +103,7 @@ pub mod pallet { /// Minimum amount an account has to lock in dApp staking in order to participate. #[pallet::constant] - type MinimumLockedAmount: Get>; + type MinimumLockedAmount: Get; /// Amount of blocks that need to pass before unlocking chunks can be claimed by the owner. #[pallet::constant] @@ -196,7 +202,7 @@ pub mod pallet { /// General information about the current era. #[pallet::storage] - pub type CurrentEraInfo = StorageValue<_, EraInfo>, ValueQuery>; + pub type CurrentEraInfo = StorageValue<_, EraInfo, ValueQuery>; #[pallet::call] impl Pallet { diff --git a/pallets/dapp-staking-v3/src/test/testing_utils.rs b/pallets/dapp-staking-v3/src/test/testing_utils.rs index 25d52490bf..063b11b185 100644 --- a/pallets/dapp-staking-v3/src/test/testing_utils.rs +++ b/pallets/dapp-staking-v3/src/test/testing_utils.rs @@ -32,7 +32,7 @@ use std::collections::HashMap; pub(crate) struct MemorySnapshot { active_protocol_state: ProtocolState>, next_dapp_id: DAppId, - current_era_info: EraInfo>, + current_era_info: EraInfo, integrated_dapps: HashMap< ::SmartContract, DAppInfo<::AccountId>, diff --git a/pallets/dapp-staking-v3/src/test/tests_types.rs b/pallets/dapp-staking-v3/src/test/tests_types.rs index dfdda1036e..24833ff767 100644 --- a/pallets/dapp-staking-v3/src/test/tests_types.rs +++ b/pallets/dapp-staking-v3/src/test/tests_types.rs @@ -47,7 +47,7 @@ fn protocol_state_default() { fn account_ledger_default() { get_u32_type!(LockedDummy, 5); get_u32_type!(UnlockingDummy, 5); - let acc_ledger = AccountLedger::::default(); + let acc_ledger = AccountLedger::::default(); assert!(acc_ledger.is_empty()); assert!(acc_ledger.active_locked_amount().is_zero()); @@ -59,8 +59,7 @@ fn account_ledger_default() { fn account_ledger_add_lock_amount_works() { get_u32_type!(LockedDummy, 5); get_u32_type!(UnlockingDummy, 5); - let mut acc_ledger = - AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); // First step, sanity checks let first_era = 1; @@ -76,10 +75,10 @@ fn account_ledger_add_lock_amount_works() { assert_eq!(acc_ledger.total_locked_amount(), init_amount); assert_eq!(acc_ledger.lock_era(), first_era); assert!(!acc_ledger.is_empty()); - assert_eq!(acc_ledger.locked.len(), 1); + assert_eq!(acc_ledger.locked.0.len(), 1); assert_eq!( acc_ledger.latest_locked_chunk(), - Some(&LockedChunk:: { + Some(&LockedChunk { amount: init_amount, era: first_era, }) @@ -91,7 +90,7 @@ fn account_ledger_add_lock_amount_works() { assert_eq!(acc_ledger.active_locked_amount(), init_amount + addition); assert_eq!(acc_ledger.total_locked_amount(), init_amount + addition); assert_eq!(acc_ledger.lock_era(), first_era); - assert_eq!(acc_ledger.locked.len(), 1); + assert_eq!(acc_ledger.locked.0.len(), 1); // Add up to storage limit for i in 2..=LockedDummy::get() { @@ -101,7 +100,7 @@ fn account_ledger_add_lock_amount_works() { init_amount + addition * i as u128 ); assert_eq!(acc_ledger.lock_era(), first_era + i); - assert_eq!(acc_ledger.locked.len(), i as usize); + assert_eq!(acc_ledger.locked.0.len(), i as usize); } // Any further additions should fail due to exhausting bounded storage capacity @@ -116,14 +115,13 @@ fn account_ledger_add_lock_amount_works() { fn account_ledger_subtract_lock_amount_basic_usage_works() { get_u32_type!(LockedDummy, 5); get_u32_type!(UnlockingDummy, 5); - let mut acc_ledger = - AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); // Sanity check scenario // Cannot reduce if there is nothing locked, should be a noop assert!(acc_ledger.subtract_lock_amount(0, 1).is_ok()); assert!(acc_ledger.subtract_lock_amount(10, 1).is_ok()); - assert!(acc_ledger.locked.len().is_zero()); + assert!(acc_ledger.locked.0.len().is_zero()); assert!(acc_ledger.is_empty()); // First basic scenario @@ -137,7 +135,7 @@ fn account_ledger_subtract_lock_amount_basic_usage_works() { assert!(acc_ledger .subtract_lock_amount(unlock_amount, first_era) .is_ok()); - assert_eq!(acc_ledger.locked.len(), 1); + assert_eq!(acc_ledger.locked.0.len(), 1); assert_eq!( acc_ledger.total_locked_amount(), first_lock_amount - unlock_amount @@ -157,23 +155,23 @@ fn account_ledger_subtract_lock_amount_basic_usage_works() { .add_lock_amount(second_lock_amount - first_lock_amount, second_era) .is_ok()); assert_eq!(acc_ledger.active_locked_amount(), second_lock_amount); - assert_eq!(acc_ledger.locked.len(), 2); + assert_eq!(acc_ledger.locked.0.len(), 2); // Subtract from the first era and verify state is as expected assert!(acc_ledger .subtract_lock_amount(unlock_amount, first_era) .is_ok()); - assert_eq!(acc_ledger.locked.len(), 2); + assert_eq!(acc_ledger.locked.0.len(), 2); assert_eq!( acc_ledger.active_locked_amount(), second_lock_amount - unlock_amount ); assert_eq!( - acc_ledger.locked[0].amount, + acc_ledger.locked.0[0].amount, first_lock_amount - unlock_amount ); assert_eq!( - acc_ledger.locked[1].amount, + acc_ledger.locked.0[1].amount, second_lock_amount - unlock_amount ); @@ -182,17 +180,17 @@ fn account_ledger_subtract_lock_amount_basic_usage_works() { assert!(acc_ledger .subtract_lock_amount(unlock_amount, second_era) .is_ok()); - assert_eq!(acc_ledger.locked.len(), 2); + assert_eq!(acc_ledger.locked.0.len(), 2); assert_eq!( acc_ledger.active_locked_amount(), second_lock_amount - unlock_amount * 2 ); assert_eq!( - acc_ledger.locked[0].amount, + acc_ledger.locked.0[0].amount, first_lock_amount - unlock_amount ); assert_eq!( - acc_ledger.locked[1].amount, + acc_ledger.locked.0[1].amount, second_lock_amount - unlock_amount * 2 ); } @@ -201,8 +199,7 @@ fn account_ledger_subtract_lock_amount_basic_usage_works() { fn account_ledger_subtract_lock_amount_overflow_fails() { get_u32_type!(LockedDummy, 5); get_u32_type!(UnlockingDummy, 5); - let mut acc_ledger = - AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); let first_lock_amount = 17 * 19; let era = 1; @@ -210,7 +207,7 @@ fn account_ledger_subtract_lock_amount_overflow_fails() { assert!(acc_ledger.add_lock_amount(first_lock_amount, era).is_ok()); for idx in 1..=LockedDummy::get() { assert!(acc_ledger.subtract_lock_amount(unlock_amount, idx).is_ok()); - assert_eq!(acc_ledger.locked.len(), idx as usize); + assert_eq!(acc_ledger.locked.0.len(), idx as usize); assert_eq!( acc_ledger.active_locked_amount(), first_lock_amount - unlock_amount * idx as u128 @@ -218,20 +215,20 @@ fn account_ledger_subtract_lock_amount_overflow_fails() { } // Updating existing lock should still work - let locked_snapshot = acc_ledger.locked.clone(); + let locked_snapshot = acc_ledger.locked.0.clone(); for i in 1..10 { assert!(acc_ledger .subtract_lock_amount(unlock_amount, LockedDummy::get()) .is_ok()); - assert_eq!(acc_ledger.locked.len(), LockedDummy::get() as usize); + assert_eq!(acc_ledger.locked.0.len(), LockedDummy::get() as usize); let last_idx = LockedDummy::get() as usize - 1; assert_eq!( - &acc_ledger.locked[0..last_idx], + &acc_ledger.locked.0[0..last_idx], &locked_snapshot[0..last_idx] ); assert_eq!( - acc_ledger.locked[last_idx].amount as u128 + unlock_amount * i, + acc_ledger.locked.0[last_idx].amount as u128 + unlock_amount * i, locked_snapshot[last_idx].amount ); } @@ -248,8 +245,7 @@ fn account_ledger_subtract_lock_amount_overflow_fails() { fn account_ledger_subtract_lock_amount_advanced_example_works() { get_u32_type!(LockedDummy, 5); get_u32_type!(UnlockingDummy, 5); - let mut acc_ledger = - AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); // Prepare an example where we have two non-consecutive entries, and we unlock in the era right before the second entry. // This covers a scenario where user has called `lock` in the current era, @@ -266,7 +262,7 @@ fn account_ledger_subtract_lock_amount_advanced_example_works() { assert!(acc_ledger .add_lock_amount(second_lock_amount, second_era) .is_ok()); - assert_eq!(acc_ledger.locked.len(), 2); + assert_eq!(acc_ledger.locked.0.len(), 2); assert!(acc_ledger .subtract_lock_amount(unlock_amount, unlock_era) @@ -277,26 +273,25 @@ fn account_ledger_subtract_lock_amount_advanced_example_works() { ); // Check entries in more detail - assert_eq!(acc_ledger.locked.len(), 3); - assert_eq!(acc_ledger.locked[0].amount, first_lock_amount,); + assert_eq!(acc_ledger.locked.0.len(), 3); + assert_eq!(acc_ledger.locked.0[0].amount, first_lock_amount,); assert_eq!( - acc_ledger.locked[2].amount, + acc_ledger.locked.0[2].amount, first_lock_amount + second_lock_amount - unlock_amount ); // Verify the new entry is as expected assert_eq!( - acc_ledger.locked[1].amount, + acc_ledger.locked.0[1].amount, first_lock_amount - unlock_amount ); - assert_eq!(acc_ledger.locked[1].era, unlock_era); + assert_eq!(acc_ledger.locked.0[1].era, unlock_era); } #[test] fn account_ledger_subtract_lock_amount_with_only_one_locked_chunk() { get_u32_type!(LockedDummy, 5); get_u32_type!(UnlockingDummy, 5); - let mut acc_ledger = - AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); // Scenario: user locks for era 2 while era 1 is active, immediately followed by unlock call. // Locked amount should be updated for the next era, but active locked amount should be unchanged (zero). @@ -309,9 +304,9 @@ fn account_ledger_subtract_lock_amount_with_only_one_locked_chunk() { .subtract_lock_amount(unlock_amount, unlock_era) .is_ok()); - assert_eq!(acc_ledger.locked.len(), 1); + assert_eq!(acc_ledger.locked.0.len(), 1); assert_eq!( - acc_ledger.locked[0], + acc_ledger.locked.0[0], LockedChunk { amount: lock_amount - unlock_amount, era: lock_era, @@ -323,8 +318,7 @@ fn account_ledger_subtract_lock_amount_with_only_one_locked_chunk() { fn account_ledger_subtract_lock_amount_correct_zero_cleanup() { get_u32_type!(LockedDummy, 5); get_u32_type!(UnlockingDummy, 5); - let mut acc_ledger = - AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); // Ensure that zero entries are cleaned up correctly when required. // There are a couple of distinct scenarios: @@ -339,14 +333,14 @@ fn account_ledger_subtract_lock_amount_correct_zero_cleanup() { assert!(acc_ledger .subtract_lock_amount(lock_amount, lock_era) .is_ok()); - assert!(acc_ledger.locked.is_empty()); + assert!(acc_ledger.locked.0.is_empty()); // 1st scenario (B) - only one zero entry, unlock is in the previous era assert!(acc_ledger.add_lock_amount(lock_amount, lock_era).is_ok()); assert!(acc_ledger .subtract_lock_amount(lock_amount, lock_era - 1) .is_ok()); - assert!(acc_ledger.locked.is_empty()); + assert!(acc_ledger.locked.0.is_empty()); // 2nd scenario - last entry is zero let first_lock_era = 3; @@ -362,7 +356,7 @@ fn account_ledger_subtract_lock_amount_correct_zero_cleanup() { assert!(acc_ledger .subtract_lock_amount(acc_ledger.active_locked_amount(), unlock_era) .is_ok()); - assert_eq!(acc_ledger.locked.len(), 3); + assert_eq!(acc_ledger.locked.0.len(), 3); assert!(acc_ledger.active_locked_amount().is_zero()); } @@ -370,8 +364,7 @@ fn account_ledger_subtract_lock_amount_correct_zero_cleanup() { fn account_ledger_subtract_lock_amount_zero_entry_between_two_non_zero() { get_u32_type!(LockedDummy, 5); get_u32_type!(UnlockingDummy, 5); - let mut acc_ledger = - AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); let (first_lock_amount, second_lock_amount, third_lock_amount) = (17, 23, 29); let (first_lock_era, second_lock_era, third_lock_era) = (1, 3, 7); @@ -386,7 +379,7 @@ fn account_ledger_subtract_lock_amount_zero_entry_between_two_non_zero() { assert!(acc_ledger .add_lock_amount(third_lock_amount, third_lock_era) .is_ok()); - assert_eq!(acc_ledger.locked.len(), 3); + assert_eq!(acc_ledger.locked.0.len(), 3); // Unlock everything for the era right before the latest chunk era // This should result in scenario like: @@ -394,31 +387,31 @@ fn account_ledger_subtract_lock_amount_zero_entry_between_two_non_zero() { assert!(acc_ledger .subtract_lock_amount(first_lock_amount + second_lock_amount, third_lock_era - 1) .is_ok()); - assert_eq!(acc_ledger.locked.len(), 4); + assert_eq!(acc_ledger.locked.0.len(), 4); assert_eq!(acc_ledger.active_locked_amount(), third_lock_amount); assert_eq!( - acc_ledger.locked[0], + acc_ledger.locked.0[0], LockedChunk { amount: first_lock_amount, era: first_lock_era } ); assert_eq!( - acc_ledger.locked[1], + acc_ledger.locked.0[1], LockedChunk { amount: first_lock_amount + second_lock_amount, era: second_lock_era } ); assert_eq!( - acc_ledger.locked[2], + acc_ledger.locked.0[2], LockedChunk { amount: 0, era: third_lock_era - 1 } ); assert_eq!( - acc_ledger.locked[3], + acc_ledger.locked.0[3], LockedChunk { amount: third_lock_amount, era: third_lock_era @@ -430,8 +423,7 @@ fn account_ledger_subtract_lock_amount_zero_entry_between_two_non_zero() { fn account_ledger_subtract_lock_amount_consecutive_zeroes_merged() { get_u32_type!(LockedDummy, 5); get_u32_type!(UnlockingDummy, 5); - let mut acc_ledger = - AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); // Prepare scenario with 3 locked chunks, where the middle one is zero let lock_amount = 61; @@ -439,23 +431,22 @@ fn account_ledger_subtract_lock_amount_consecutive_zeroes_merged() { assert!(acc_ledger.add_lock_amount(lock_amount, 2).is_ok()); assert!(acc_ledger.subtract_lock_amount(lock_amount, 5).is_ok()); assert!(acc_ledger.add_lock_amount(lock_amount, last_era).is_ok()); - let second_chunk = acc_ledger.locked[1]; + let second_chunk = acc_ledger.locked.0[1]; // Unlock everything in the era right before the latest chunk era, but that chunk should not persist // [61, 0, 61] --> [61, 0, 0, 61] shouldn't happen since the 2nd zero is redundant. assert!(acc_ledger .subtract_lock_amount(lock_amount, last_era - 1) .is_ok()); - assert_eq!(acc_ledger.locked.len(), 3); - assert_eq!(acc_ledger.locked[1], second_chunk); + assert_eq!(acc_ledger.locked.0.len(), 3); + assert_eq!(acc_ledger.locked.0[1], second_chunk); } #[test] fn account_ledger_add_unlocking_chunk_works() { get_u32_type!(LockedDummy, 5); get_u32_type!(UnlockingDummy, 5); - let mut acc_ledger = - AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); // Sanity check scenario // Cannot reduce if there is nothing locked, should be a noop @@ -518,8 +509,7 @@ fn account_ledger_add_unlocking_chunk_works() { fn active_stake_works() { get_u32_type!(LockedDummy, 5); get_u32_type!(UnlockingDummy, 5); - let mut acc_ledger = - AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); // Sanity check assert!(acc_ledger.active_stake(0).is_zero()); @@ -540,8 +530,7 @@ fn active_stake_works() { fn unlockable_amount_works() { get_u32_type!(LockedDummy, 5); get_u32_type!(UnlockingDummy, 5); - let mut acc_ledger = - AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); // Sanity check scenario assert!(acc_ledger.unlockable_amount(0).is_zero()); @@ -569,7 +558,7 @@ fn unlockable_amount_works() { assert_eq!(acc_ledger.unlockable_amount(stake_period + 2), lock_amount); // Absurd example, for the sake of completeness - staked without any lock - acc_ledger.locked = Default::default(); + acc_ledger.locked.0 = Default::default(); assert!(acc_ledger.unlockable_amount(stake_period).is_zero()); assert!(acc_ledger.unlockable_amount(stake_period - 2).is_zero()); assert!(acc_ledger.unlockable_amount(stake_period + 1).is_zero()); @@ -577,7 +566,7 @@ fn unlockable_amount_works() { #[test] fn era_info_manipulation_works() { - let mut era_info = EraInfo::::default(); + let mut era_info = EraInfo::default(); // Sanity check assert!(era_info.total_locked.is_zero()); diff --git a/pallets/dapp-staking-v3/src/types.rs b/pallets/dapp-staking-v3/src/types.rs index 71874f2a58..3e31b45f13 100644 --- a/pallets/dapp-staking-v3/src/types.rs +++ b/pallets/dapp-staking-v3/src/types.rs @@ -19,7 +19,12 @@ use frame_support::{pallet_prelude::*, traits::Currency, BoundedVec}; use frame_system::pallet_prelude::*; use parity_scale_codec::{Decode, Encode}; -use sp_runtime::traits::{AtLeast32BitUnsigned, Zero}; +use sp_runtime::{ + traits::{AtLeast32BitUnsigned, Zero}, + Saturating, +}; + +use astar_primitives::Balance; use crate::pallet::Config; @@ -31,7 +36,6 @@ pub type BalanceOf = /// Convenience type for `AccountLedger` usage. pub type AccountLedgerFor = AccountLedger< - BalanceOf, BlockNumberFor, ::MaxLockedChunks, ::MaxUnlockingChunks, @@ -125,17 +129,14 @@ pub struct DAppInfo { /// How much was locked in a specific era #[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)] -pub struct LockedChunk { +pub struct LockedChunk { #[codec(compact)] pub amount: Balance, #[codec(compact)] pub era: EraNumber, } -impl Default for LockedChunk -where - Balance: AtLeast32BitUnsigned + MaxEncodedLen + Copy, -{ +impl Default for LockedChunk { fn default() -> Self { Self { amount: Balance::zero(), @@ -144,21 +145,39 @@ where } } +impl AmountEraPair for LockedChunk { + fn get_amount(&self) -> Balance { + self.amount + } + + fn get_era(&self) -> EraNumber { + self.era + } + + fn set_era(&mut self, era: EraNumber) { + self.era = era; + } + + fn saturating_accrue(&mut self, increase: Balance) { + self.amount.saturating_accrue(increase); + } + + fn saturating_reduce(&mut self, reduction: Balance) { + self.amount.saturating_reduce(reduction); + } +} + /// How much was unlocked in some block. #[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)] -pub struct UnlockingChunk< - Balance: AtLeast32BitUnsigned + MaxEncodedLen + Copy, - BlockNumber: AtLeast32BitUnsigned + MaxEncodedLen + Copy, -> { +pub struct UnlockingChunk { #[codec(compact)] pub amount: Balance, #[codec(compact)] pub unlock_block: BlockNumber, } -impl Default for UnlockingChunk +impl Default for UnlockingChunk where - Balance: AtLeast32BitUnsigned + MaxEncodedLen + Copy, BlockNumber: AtLeast32BitUnsigned + MaxEncodedLen + Copy, { fn default() -> Self { @@ -171,17 +190,14 @@ where /// Information about how much was staked in a specific period. #[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)] -pub struct StakeInfo { +pub struct StakeInfo { #[codec(compact)] pub amount: Balance, #[codec(compact)] pub period: PeriodNumber, } -impl Default for StakeInfo -where - Balance: AtLeast32BitUnsigned + MaxEncodedLen + Copy, -{ +impl Default for StakeInfo { fn default() -> Self { Self { amount: Balance::zero(), @@ -190,56 +206,188 @@ where } } +/// Trait for types that can be used as a pair of amount & era. +pub trait AmountEraPair: MaxEncodedLen + Default + Copy { + /// Balance amount used somehow during the accompanied era. + fn get_amount(&self) -> Balance; + /// Era acting as timestamp for the accompanied amount. + fn get_era(&self) -> EraNumber; + // Sets the era to the specified value. + fn set_era(&mut self, era: EraNumber); + /// Increase the total amount by the specified increase, saturating at the maximum value. + fn saturating_accrue(&mut self, increase: Balance); + /// Reduce the total amount by the specified reduction, saturating at the minumum value. + fn saturating_reduce(&mut self, reduction: Balance); +} + +/// Helper struct for easier manipulation of sparse pairs. +/// +/// The struct guarantes the following: +/// ----------------------------------- +/// 1. The vector is always sorted by era, in ascending order. +/// 2. There are no two consecutive zero chunks. +/// 3. There are no two chunks with the same era. +/// 4. The vector is always bounded by the specified maximum length. +/// +#[derive(Encode, Decode, MaxEncodedLen, Clone, Debug, PartialEq, Eq, TypeInfo)] +#[scale_info(skip_type_params(ML))] +// TODO: should I use `EncodeLike`? +pub struct SparseBoundedAmountEraVec>(pub BoundedVec); + +impl SparseBoundedAmountEraVec +where + P: AmountEraPair, + ML: Get, +{ + // TODO: maybe add a custom error type? + // Could be useful to know what exactly went wrong. + + // TODO2: write (or reuse) custom tests for this implementation. + + /// Places the specified pair into the vector, in an appropriate place. + /// + /// If entry for the specified era already exists, it's updated. + /// + /// If entry for the specified era doesn't exist, it's created and insertion is attempted. + /// + /// In case vector has no more capacity, error is returned, and whole operation is a noop. + pub fn add_amount(&mut self, amount: Balance, era: EraNumber) -> Result<(), ()> { + if amount.is_zero() { + return Ok(()); + } + + let mut chunk = if let Some(&chunk) = self.0.last() { + chunk + } else { + P::default() + }; + + chunk.saturating_accrue(amount); + + if chunk.get_era() == era && !self.0.is_empty() { + if let Some(last) = self.0.last_mut() { + *last = chunk; + } + } else { + chunk.set_era(era); + self.0.try_push(chunk).map_err(|_| ())?; + } + + Ok(()) + } + + /// Subtracts the specified amount of the total locked amount, if possible. + /// + /// If entry for the specified era already exists, it's updated. + /// + /// If entry for the specified era doesn't exist, it's created and insertion is attempted. + /// In case vector has no more capacity, error is returned, and whole operation is a noop. + pub fn subtract_amount(&mut self, amount: Balance, era: EraNumber) -> Result<(), ()> { + if amount.is_zero() || self.0.is_empty() { + return Ok(()); + } + // TODO: this method can surely be optimized (avoid too many iters) but focus on that later, + // when it's all working fine, and we have good test coverage. + + // Find the most relevant locked chunk for the specified era + let index = if let Some(index) = self.0.iter().rposition(|&chunk| chunk.get_era() <= era) { + index + } else { + // Covers scenario when there's only 1 chunk for the next era, and remove it if it's zero. + self.0 + .iter_mut() + .for_each(|chunk| chunk.saturating_reduce(amount)); + self.0.retain(|chunk| !chunk.get_amount().is_zero()); + return Ok(()); + }; + + // Update existing or insert a new chunk + let mut inner = self.0.clone().into_inner(); + let relevant_chunk_index = if inner[index].get_era() == era { + inner[index].saturating_reduce(amount); + index + } else { + let mut chunk = inner[index]; + chunk.saturating_reduce(amount); + chunk.set_era(era); + + inner.insert(index + 1, chunk); + index + 1 + }; + + // Update all chunks after the relevant one, and remove eligible zero chunks + inner[relevant_chunk_index + 1..] + .iter_mut() + .for_each(|chunk| chunk.saturating_reduce(amount)); + + // Merge all consecutive zero chunks + let mut i = relevant_chunk_index; + while i < inner.len() - 1 { + if inner[i].get_amount().is_zero() && inner[i + 1].get_amount().is_zero() { + inner.remove(i + 1); + } else { + i += 1; + } + } + + // Cleanup if only one zero chunk exists + if inner.len() == 1 && inner[0].get_amount().is_zero() { + inner.pop(); + } + + // Update `locked` to the new vector + self.0 = BoundedVec::try_from(inner).map_err(|_| ())?; + + Ok(()) + } +} + /// General info about user's stakes #[derive(Encode, Decode, MaxEncodedLen, Clone, Debug, PartialEq, Eq, TypeInfo)] #[scale_info(skip_type_params(LockedLen, UnlockingLen))] pub struct AccountLedger< - Balance: AtLeast32BitUnsigned + MaxEncodedLen + Copy, BlockNumber: AtLeast32BitUnsigned + MaxEncodedLen + Copy, LockedLen: Get, UnlockingLen: Get, > { /// How much was staked in each era - pub locked: BoundedVec, LockedLen>, + pub locked: SparseBoundedAmountEraVec, /// How much started unlocking on a certain block - pub unlocking: BoundedVec, UnlockingLen>, + pub unlocking: BoundedVec, UnlockingLen>, /// How much user had staked in some period - pub staked: StakeInfo, + pub staked: StakeInfo, } -impl Default - for AccountLedger +impl Default + for AccountLedger where - Balance: AtLeast32BitUnsigned + MaxEncodedLen + Copy, BlockNumber: AtLeast32BitUnsigned + MaxEncodedLen + Copy, LockedLen: Get, UnlockingLen: Get, { fn default() -> Self { Self { - locked: BoundedVec::, LockedLen>::default(), - unlocking: BoundedVec::, UnlockingLen>::default(), - staked: StakeInfo::::default(), + locked: SparseBoundedAmountEraVec(BoundedVec::::default()), + unlocking: BoundedVec::, UnlockingLen>::default(), + staked: StakeInfo::default(), } } } -impl - AccountLedger +impl AccountLedger where - Balance: AtLeast32BitUnsigned + MaxEncodedLen + Copy, BlockNumber: AtLeast32BitUnsigned + MaxEncodedLen + Copy, LockedLen: Get, UnlockingLen: Get, { /// Empty if no locked/unlocking/staked info exists. pub fn is_empty(&self) -> bool { - self.locked.is_empty() && self.unlocking.is_empty() && self.staked.amount.is_zero() + self.locked.0.is_empty() && self.unlocking.is_empty() && self.staked.amount.is_zero() } /// Returns latest locked chunk if it exists, `None` otherwise - pub fn latest_locked_chunk(&self) -> Option<&LockedChunk> { - self.locked.last() + pub fn latest_locked_chunk(&self) -> Option<&LockedChunk> { + self.locked.0.last() } /// Returns active locked amount. @@ -290,28 +438,7 @@ where /// If entry for the specified era doesn't exist, it's created and insertion is attempted. /// In case vector has no more capacity, error is returned, and whole operation is a noop. pub fn add_lock_amount(&mut self, amount: Balance, era: EraNumber) -> Result<(), ()> { - if amount.is_zero() { - return Ok(()); - } - - let mut locked_chunk = if let Some(&locked_chunk) = self.locked.last() { - locked_chunk - } else { - LockedChunk::default() - }; - - locked_chunk.amount.saturating_accrue(amount); - - if locked_chunk.era == era && !self.locked.is_empty() { - if let Some(last) = self.locked.last_mut() { - *last = locked_chunk; - } - } else { - locked_chunk.era = era; - self.locked.try_push(locked_chunk).map_err(|_| ())?; - } - - Ok(()) + self.locked.add_amount(amount, era) } /// Subtracts the specified amount of the total locked amount, if possible. @@ -321,62 +448,7 @@ where /// If entry for the specified era doesn't exist, it's created and insertion is attempted. /// In case vector has no more capacity, error is returned, and whole operation is a noop. pub fn subtract_lock_amount(&mut self, amount: Balance, era: EraNumber) -> Result<(), ()> { - if amount.is_zero() || self.locked.is_empty() { - return Ok(()); - } - // TODO: this method can surely be optimized (avoid too many iters) but focus on that later, - // when it's all working fine, and we have good test coverage. - - // Find the most relevant locked chunk for the specified era - let index = if let Some(index) = self.locked.iter().rposition(|&chunk| chunk.era <= era) { - index - } else { - // Covers scenario when there's only 1 chunk for the next era, and remove it if it's zero. - self.locked - .iter_mut() - .for_each(|chunk| chunk.amount.saturating_reduce(amount)); - self.locked.retain(|chunk| !chunk.amount.is_zero()); - return Ok(()); - }; - - // Update existing or insert a new chunk - let mut inner = self.locked.clone().into_inner(); - let relevant_chunk_index = if inner[index].era == era { - inner[index].amount.saturating_reduce(amount); - index - } else { - let mut chunk = inner[index]; - chunk.amount.saturating_reduce(amount); - chunk.era = era; - - inner.insert(index + 1, chunk); - index + 1 - }; - - // Update all chunks after the relevant one, and remove eligible zero chunks - inner[relevant_chunk_index + 1..] - .iter_mut() - .for_each(|chunk| chunk.amount.saturating_reduce(amount)); - - // Merge all consecutive zero chunks - let mut i = relevant_chunk_index; - while i < inner.len() - 1 { - if inner[i].amount.is_zero() && inner[i + 1].amount.is_zero() { - inner.remove(i + 1); - } else { - i += 1; - } - } - - // Cleanup if only one zero chunk exists - if inner.len() == 1 && inner[0].amount.is_zero() { - inner.pop(); - } - - // Update `locked` to the new vector - self.locked = BoundedVec::try_from(inner).map_err(|_| ())?; - - Ok(()) + self.locked.subtract_amount(amount, era) } /// Adds the specified amount to the unlocking chunks. @@ -423,9 +495,9 @@ where } } -/// Rewards pool for lock participants & dApps +/// Rewards pool for stakers & dApps #[derive(Encode, Decode, MaxEncodedLen, Copy, Clone, Debug, PartialEq, Eq, TypeInfo, Default)] -pub struct RewardInfo { +pub struct RewardInfo { /// Rewards pool for accounts which have locked funds in dApp staking #[codec(compact)] pub participants: Balance, @@ -436,9 +508,9 @@ pub struct RewardInfo { /// Info about current era, including the rewards, how much is locked, unlocking, etc. #[derive(Encode, Decode, MaxEncodedLen, Copy, Clone, Debug, PartialEq, Eq, TypeInfo, Default)] -pub struct EraInfo { +pub struct EraInfo { /// Info about era rewards - pub rewards: RewardInfo, + pub rewards: RewardInfo, /// How much balance is considered to be locked in the current era. /// This value influences the reward distribution. #[codec(compact)] @@ -452,10 +524,7 @@ pub struct EraInfo { pub unlocking: Balance, } -impl EraInfo -where - Balance: AtLeast32BitUnsigned + MaxEncodedLen + Copy, -{ +impl EraInfo { /// Update with the new amount that has just been locked. pub fn add_locked(&mut self, amount: Balance) { self.total_locked.saturating_accrue(amount);