From 71168c5b17c0ddf67cfd5abdadd0940e8a732c94 Mon Sep 17 00:00:00 2001 From: ekovalev Date: Thu, 5 Oct 2023 13:10:59 +0200 Subject: [PATCH] feat(vara): Maintain total supply consistency in offchain election provider (#3389) --- Cargo.lock | 2 + pallets/staking-rewards/Cargo.toml | 2 + pallets/staking-rewards/src/lib.rs | 8 +- pallets/staking-rewards/src/mock.rs | 134 +++++++++++++++++++++++++-- pallets/staking-rewards/src/tests.rs | 115 +++++++++++++++++++++++ runtime/vara/src/lib.rs | 4 +- 6 files changed, 252 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 076ee790ae9..53cb6a03082 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7689,6 +7689,7 @@ dependencies = [ "pallet-authorship", "pallet-bags-list", "pallet-balances", + "pallet-election-provider-multi-phase", "pallet-session", "pallet-staking", "pallet-staking-reward-fn", @@ -7703,6 +7704,7 @@ dependencies = [ "sp-authority-discovery", "sp-core 7.0.0 (git+https://github.com/gear-tech/substrate.git?branch=gear-polkadot-v0.9.41-canary-no-sandbox)", "sp-io 7.0.0 (git+https://github.com/gear-tech/substrate.git?branch=gear-polkadot-v0.9.41-canary-no-sandbox)", + "sp-npos-elections", "sp-runtime 7.0.0 (git+https://github.com/gear-tech/substrate.git?branch=gear-polkadot-v0.9.41-canary-no-sandbox)", "sp-std 5.0.0 (git+https://github.com/gear-tech/substrate.git?branch=gear-polkadot-v0.9.41-canary-no-sandbox)", ] diff --git a/pallets/staking-rewards/Cargo.toml b/pallets/staking-rewards/Cargo.toml index 567659a9853..3dd27074128 100644 --- a/pallets/staking-rewards/Cargo.toml +++ b/pallets/staking-rewards/Cargo.toml @@ -33,6 +33,7 @@ pallet-staking-reward-fn.workspace = true sp-core = { workspace = true, features = ["std"] } sp-io = { workspace = true, features = ["std"] } sp-authority-discovery = { workspace = true, features = ["std"] } +sp-npos-elections = { workspace = true, features = ["std"] } frame-election-provider-support = { workspace = true, features = ["std"] } pallet-treasury = { workspace = true, features = ["std"] } pallet-authorship = { workspace = true, features = ["std"] } @@ -41,6 +42,7 @@ pallet-session = { workspace = true, features = ["std"] } pallet-sudo = { workspace = true, features = ["std"] } pallet-bags-list = { workspace = true, features = ["std"] } pallet-utility = { workspace = true, features = ["std"] } +pallet-election-provider-multi-phase = { workspace = true, features = ["std"] } frame-executive = { workspace = true, features = ["std"] } env_logger.workspace = true diff --git a/pallets/staking-rewards/src/lib.rs b/pallets/staking-rewards/src/lib.rs index 039a0ee60df..66fd46e05fb 100644 --- a/pallets/staking-rewards/src/lib.rs +++ b/pallets/staking-rewards/src/lib.rs @@ -428,9 +428,11 @@ impl OnUnbalanced> for Pallet { /// A type to be plugged into the Staking pallet as the `RewardRemainder` associated type. /// -/// Implements the `OnUnbalanced` trait in a way that would try to offset -/// the input negative imbalance against the staking rewards pool so that the total -/// token supply is not affected by the rewards-in-excess that are sent to Treasury. +/// Implements the `OnUnbalanced` trait in a way that would try to burn +/// the amount equivalent to that provided in the input `NegativeImbalance` from the rewards +/// pool in order to keep the token total supply intact. It is assumed that the subsequent +/// `OnUnbalanced` handler (e.g. Treasury) would `resolve` the imbalance and not drop it - +/// otherwise the the total supply will decrease. pub struct RewardsStash(sp_std::marker::PhantomData<(T, U)>); impl OnUnbalanced> for RewardsStash diff --git a/pallets/staking-rewards/src/mock.rs b/pallets/staking-rewards/src/mock.rs index e5037ced27b..e5ca6aad641 100644 --- a/pallets/staking-rewards/src/mock.rs +++ b/pallets/staking-rewards/src/mock.rs @@ -17,16 +17,20 @@ // along with this program. If not, see . use crate as pallet_gear_staking_rewards; -use frame_election_provider_support::{onchain, SequentialPhragmen, VoteWeight}; +use frame_election_provider_support::{ + onchain, ElectionDataProvider, SequentialPhragmen, VoteWeight, +}; use frame_support::{ construct_runtime, parameter_types, traits::{ - ConstU32, Contains, FindAuthor, GenesisBuild, OnFinalize, OnInitialize, U128CurrencyToVote, + ConstU32, Contains, Currency, Everything, FindAuthor, GenesisBuild, NeverEnsureOrigin, + OnFinalize, OnInitialize, U128CurrencyToVote, }, - weights::constants::RocksDbWeight, + weights::{constants::RocksDbWeight, Weight}, PalletId, }; use frame_system::{self as system, pallet_prelude::BlockNumberFor, EnsureRoot}; +use pallet_election_provider_multi_phase::{self as multi_phase}; use pallet_session::historical::{self as pallet_session_historical}; use sp_core::{crypto::key_types, H256}; use sp_runtime::{ @@ -95,6 +99,7 @@ construct_runtime!( BagsList: pallet_bags_list::::{Pallet, Event}, Sudo: pallet_sudo::{Pallet, Call, Storage, Config, Event}, Utility: pallet_utility::{Pallet, Call, Event}, + ElectionProviderMultiPhase: multi_phase::{Pallet, Call, Event}, } ); @@ -117,7 +122,7 @@ parameter_types! { } impl system::Config for Test { - type BaseCallFilter = frame_support::traits::Everything; + type BaseCallFilter = Everything; type BlockWeights = (); type BlockLength = (); type DbWeight = RocksDbWeight; @@ -301,6 +306,16 @@ parameter_types! { pub const MaxOnChainElectableTargets: u16 = 100; } +frame_election_provider_support::generate_solution_type!( + #[compact] + pub struct TestNposSolution::< + VoterIndex = u32, + TargetIndex = u16, + Accuracy = sp_runtime::PerU16, + MaxVoters = ConstU32::<2_000>, + >(16) +); + pub struct OnChainSeqPhragmen; impl onchain::Config for OnChainSeqPhragmen { type System = Test; @@ -414,7 +429,93 @@ impl pallet_treasury::Config for Test { type SpendFunds = (); type WeightInfo = (); type MaxApprovals = MaxApprovals; - type SpendOrigin = frame_support::traits::NeverEnsureOrigin; + type SpendOrigin = NeverEnsureOrigin; +} + +parameter_types! { + // phase durations. 1/4 of the last session for each. + pub static SignedPhase: u64 = SESSION_DURATION / 4; + pub static UnsignedPhase: u64 = SESSION_DURATION / 4; + + // signed config + pub static SignedRewardBase: Balance = 50 * UNITS; + pub static SignedDepositBase: Balance = 50 * UNITS; + pub static SignedDepositByte: Balance = 0; + pub static SignedMaxSubmissions: u32 = 5; + pub static SignedMaxRefunds: u32 = 2; + pub BetterUnsignedThreshold: Perbill = Perbill::zero(); + pub SignedMaxWeight: Weight = Weight::from_parts(u64::MAX, u64::MAX); + + // miner configs + pub static MinerTxPriority: u64 = 100; + pub static MinerMaxWeight: Weight = Weight::from_parts(u64::MAX, u64::MAX); + pub static MinerMaxLength: u32 = 256; +} + +impl multi_phase::MinerConfig for Test { + type AccountId = AccountId; + type MaxLength = MinerMaxLength; + type MaxWeight = MinerMaxWeight; + type MaxVotesPerVoter = ::MaxVotesPerVoter; + type MaxWinners = MaxActiveValidators; + type Solution = TestNposSolution; + + fn solution_weight(v: u32, t: u32, a: u32, d: u32) -> Weight { + <::WeightInfo as multi_phase::WeightInfo>::submit_unsigned( + v, t, a, d, + ) + } +} + +pub struct TestBenchmarkingConfig; +impl multi_phase::BenchmarkingConfig for TestBenchmarkingConfig { + const VOTERS: [u32; 2] = [1000, 2000]; + const TARGETS: [u32; 2] = [500, 1000]; + const ACTIVE_VOTERS: [u32; 2] = [500, 800]; + const DESIRED_TARGETS: [u32; 2] = [200, 400]; + const SNAPSHOT_MAXIMUM_VOTERS: u32 = 1000; + const MINER_MAXIMUM_VOTERS: u32 = 1000; + const MAXIMUM_TARGETS: u32 = 300; +} + +impl multi_phase::Config for Test { + type RuntimeEvent = RuntimeEvent; + type Currency = Balances; + type EstimateCallFee = ConstU32<1_000>; + type SignedPhase = SignedPhase; + type UnsignedPhase = UnsignedPhase; + type BetterUnsignedThreshold = BetterUnsignedThreshold; + type BetterSignedThreshold = (); + type OffchainRepeat = OffchainRepeat; + type MinerTxPriority = MinerTxPriority; + type SignedRewardBase = SignedRewardBase; + type SignedDepositBase = SignedDepositBase; + type SignedDepositByte = (); + type SignedDepositWeight = (); + type SignedMaxWeight = SignedMaxWeight; + type SignedMaxSubmissions = SignedMaxSubmissions; + type SignedMaxRefunds = SignedMaxRefunds; + type SlashHandler = Treasury; + type RewardHandler = StakingRewards; + type DataProvider = Staking; + type Fallback = onchain::OnChainExecution; + type GovernanceFallback = onchain::OnChainExecution; + type ForceOrigin = frame_system::EnsureRoot; + type MaxElectableTargets = MaxElectableTargets; + type MaxWinners = MaxActiveValidators; + type MaxElectingVoters = MaxElectingVoters; + type WeightInfo = (); + type BenchmarkingConfig = TestBenchmarkingConfig; + type MinerConfig = Self; + type Solver = SequentialPhragmen, ()>; +} + +impl frame_system::offchain::SendTransactionTypes for Test +where + RuntimeCall: From, +{ + type OverarchingCall = RuntimeCall; + type Extrinsic = TestXt; } pub type ValidatorAccountId = ( @@ -573,9 +674,7 @@ impl ExtBuilder { if total_supply < self.total_supply { // Mint the difference to SIGNER user let diff = self.total_supply.saturating_sub(total_supply); - let _ = >::deposit_creating( - &SIGNER, diff, - ); + let _ = >::deposit_creating(&SIGNER, diff); } }); ext @@ -607,11 +706,30 @@ pub(crate) fn run_for_n_blocks(n: u64) { } } +pub fn run_to_unsigned() { + while !matches!( + ElectionProviderMultiPhase::current_phase(), + multi_phase::Phase::Unsigned(_) + ) { + run_to_block(System::block_number() + 1); + } +} + +pub fn run_to_signed() { + while !matches!( + ElectionProviderMultiPhase::current_phase(), + multi_phase::Phase::Signed + ) { + run_to_block(System::block_number() + 1); + } +} + // Run on_initialize hooks in order as they appear in AllPalletsWithSystem. pub(crate) fn on_initialize(new_block_number: BlockNumberFor) { Timestamp::set_timestamp(new_block_number.saturating_mul(MILLISECS_PER_BLOCK)); Authorship::on_initialize(new_block_number); Session::on_initialize(new_block_number); + ElectionProviderMultiPhase::on_initialize(new_block_number); } // Run on_finalize hooks (in pallets reverse order, as they appear in AllPalletsWithSystem) diff --git a/pallets/staking-rewards/src/tests.rs b/pallets/staking-rewards/src/tests.rs index e1f1a230ff7..e2942765929 100644 --- a/pallets/staking-rewards/src/tests.rs +++ b/pallets/staking-rewards/src/tests.rs @@ -1234,6 +1234,121 @@ fn empty_rewards_pool_causes_inflation() { }); } +#[test] +fn election_solution_rewards_add_up() { + use pallet_election_provider_multi_phase::{Config as MPConfig, RawSolution}; + use sp_npos_elections::ElectionScore; + + let (target_inflation, ideal_stake, pool_balance, non_stakeable) = sensible_defaults(); + // Solutions submitters + let accounts = (0_u64..5).map(|i| 100 + i).collect::>(); + let mut ext = ExtBuilder::default() + .initial_authorities(vec![ + (VAL_1_STASH, VAL_1_CONTROLLER, VAL_1_AUTH_ID), + (VAL_2_STASH, VAL_2_CONTROLLER, VAL_2_AUTH_ID), + (VAL_3_STASH, VAL_3_CONTROLLER, VAL_3_AUTH_ID), + ]) + .stash(VALIDATOR_STAKE) + .endowment(ENDOWMENT) + .endowed_accounts(accounts) + .total_supply(INITIAL_TOTAL_TOKEN_SUPPLY) + .non_stakeable(non_stakeable) + .pool_balance(pool_balance) + .ideal_stake(ideal_stake) + .target_inflation(target_inflation) + .build(); + ext.execute_with(|| { + // Initial chain state + let (initial_total_issuance, _, initial_treasury_balance, initial_rewards_pool_balance) = + chain_state(); + assert_eq!(initial_rewards_pool_balance, pool_balance); + + // Running chain until the signing phase begins + run_to_signed(); + assert!(ElectionProviderMultiPhase::current_phase().is_signed()); + assert_eq!(::SignedMaxRefunds::get(), 2_u32); + assert!(::SignedMaxSubmissions::get() > 3_u32); + + // Submit 3 election solutions candidates: + // 2 good solutions and 1 incorrect one (with higher score, so that it is going to run + // through feasibility check as the best candidate but eventually be rejected and slashed). + let good_solution = RawSolution { + solution: TestNposSolution { + votes1: vec![(0, 0), (1, 1), (2, 2)], + ..Default::default() + }, + score: ElectionScore { + minimal_stake: VALIDATOR_STAKE, + sum_stake: 3 * VALIDATOR_STAKE, + sum_stake_squared: 3 * VALIDATOR_STAKE * VALIDATOR_STAKE, + }, + round: 1, + }; + let bad_solution = RawSolution { + solution: TestNposSolution { + votes1: vec![(0, 0), (1, 1), (2, 2)], + ..Default::default() + }, + score: ElectionScore { + minimal_stake: VALIDATOR_STAKE + 100_u128, + sum_stake: 3 * VALIDATOR_STAKE, + sum_stake_squared: 3 * VALIDATOR_STAKE * VALIDATOR_STAKE, + }, + round: 1, + }; + let solutions = vec![good_solution.clone(), bad_solution, good_solution]; + for (i, s) in solutions.into_iter().enumerate() { + let account = 100_u64 + i as u64; + assert_ok!(ElectionProviderMultiPhase::submit( + RuntimeOrigin::signed(account), + Box::new(s) + )); + assert_eq!( + Balances::free_balance(account), + ENDOWMENT - ::SignedDepositBase::get() + ); + } + + run_to_unsigned(); + + // Measure current stats + let (total_issuance, _, treasury_balance, rewards_pool_balance) = chain_state(); + + // Check all balances consistency: + // 1. `total_issuance` hasn't change despite rewards having been minted + assert_eq!(total_issuance, initial_total_issuance); + // 2. the account whose solution was accepted got reward + tx fee rebate + assert_eq!( + Balances::free_balance(102), + ENDOWMENT + + ::SignedRewardBase::get() + + <::EstimateCallFee as Get>::get() as u128 + ); + // 3. the account whose solution was rejected got slashed and lost the deposit and fee + assert_eq!( + Balances::free_balance(101), + ENDOWMENT - ::SignedDepositBase::get() + ); + // 4. the third account got deposit unreserved and tx fee returned + assert_eq!( + Balances::free_balance(100), + ENDOWMENT + <::EstimateCallFee as Get>::get() as u128 + ); + // 5. the slashed deposit went to `Treasury` + assert_eq!( + treasury_balance, + initial_treasury_balance + ::SignedDepositBase::get() + ); + // 6. the rewards offset pool's balanced decreased to compensate for reward and rebates. + assert_eq!( + rewards_pool_balance, + initial_rewards_pool_balance + - ::SignedRewardBase::get() + - <::EstimateCallFee as Get>::get() as u128 * 2 + ); + }); +} + fn sensible_defaults() -> (Perquintill, Perquintill, u128, Perquintill) { ( Perquintill::from_rational(578_u64, 10_000_u64), diff --git a/runtime/vara/src/lib.rs b/runtime/vara/src/lib.rs index 6b8e1038a0f..e3d33b36656 100644 --- a/runtime/vara/src/lib.rs +++ b/runtime/vara/src/lib.rs @@ -555,8 +555,8 @@ impl pallet_election_provider_multi_phase::Config for Runtime { type SignedMaxRefunds = ConstU32<3>; type SignedDepositWeight = (); type SignedMaxWeight = MinerMaxWeight; - type SlashHandler = (); // burn slashes - type RewardHandler = (); // nothing to do upon rewards + type SlashHandler = Treasury; + type RewardHandler = StakingRewards; type DataProvider = Staking; type Fallback = onchain::OnChainExecution; type GovernanceFallback = onchain::OnChainExecution;