diff --git a/aleph-client/src/lib.rs b/aleph-client/src/lib.rs index 0eec7e4d01..79cdbb3fc9 100644 --- a/aleph-client/src/lib.rs +++ b/aleph-client/src/lib.rs @@ -13,18 +13,20 @@ pub use multisig::{ }; pub use rpc::{rotate_keys, rotate_keys_raw_result, state_query_storage_at}; pub use session::{ - change_next_era_reserved_validators, change_validators, get_current as get_current_session, - set_keys, wait_for as wait_for_session, Keys as SessionKeys, + change_next_era_reserved_validators, change_validators, get_current_session, get_session, + get_session_period, set_keys, wait_for as wait_for_session, + wait_for_at_least as wait_for_at_least_session, Keys as SessionKeys, }; use sp_core::{sr25519, storage::StorageKey, Pair, H256}; use sp_runtime::{generic::Header as GenericHeader, traits::BlakeTwo256}; pub use staking::{ batch_bond as staking_batch_bond, batch_nominate as staking_batch_nominate, bond as staking_bond, bonded as staking_bonded, force_new_era as staking_force_new_era, - get_current_era, get_payout_for_era, ledger as staking_ledger, - multi_bond as staking_multi_bond, nominate as staking_nominate, payout_stakers, - payout_stakers_and_assert_locked_balance, set_staking_limits as staking_set_staking_limits, - validate as staking_validate, wait_for_full_era_completion, wait_for_next_era, StakingLedger, + get_current_era, get_era, get_era_reward_points, get_exposure, get_payout_for_era, + get_sessions_per_era, ledger as staking_ledger, multi_bond as staking_multi_bond, + nominate as staking_nominate, payout_stakers, payout_stakers_and_assert_locked_balance, + set_staking_limits as staking_set_staking_limits, validate as staking_validate, + wait_for_full_era_completion, wait_for_next_era, RewardPoint, StakingLedger, }; pub use substrate_api_client; use substrate_api_client::{ @@ -308,3 +310,13 @@ pub fn get_storage_key(pallet: &str, call: &str) -> String { let storage_key = StorageKey(bytes.into()); hex::encode(storage_key.0) } + +pub fn get_block_hash(connection: &C, block_number: u32) -> H256 { + connection + .as_connection() + .get_block_hash(Some(block_number)) + .expect("API call should have succeeded.") + .unwrap_or_else(|| { + panic!("Failed to obtain block hash for block {}.", block_number); + }) +} diff --git a/aleph-client/src/session.rs b/aleph-client/src/session.rs index 59acd77ab4..c3f5a1f00c 100644 --- a/aleph-client/src/session.rs +++ b/aleph-client/src/session.rs @@ -1,6 +1,7 @@ use codec::{Decode, Encode}; use log::info; -use sp_core::Pair; +use primitives::SessionIndex; +use sp_core::{Pair, H256}; use substrate_api_client::{ compose_call, compose_extrinsic, AccountId, ExtrinsicParams, FromHexString, XtStatus, }; @@ -85,33 +86,57 @@ pub fn set_keys(connection: &SignedConnection, new_keys: Keys, status: XtStatus) send_xt(connection, xt, Some("set_keys"), status); } -/// Get the number of the current session. -pub fn get_current(connection: &C) -> u32 { +pub fn get_current_session(connection: &C) -> SessionIndex { + get_session(connection, None) +} + +pub fn get_session(connection: &C, block_hash: Option) -> SessionIndex { connection .as_connection() - .get_storage_value("Session", "CurrentIndex", None) + .get_storage_value("Session", "CurrentIndex", block_hash) .unwrap() .unwrap_or(0) } -pub fn wait_for( +pub fn wait_for_predicate bool>( connection: &C, - session_index: u32, + session_predicate: P, ) -> anyhow::Result { - info!(target: "aleph-client", "Waiting for session {}", session_index); + info!(target: "aleph-client", "Waiting for session"); #[derive(Debug, Decode, Clone)] struct NewSessionEvent { - session_index: u32, + session_index: SessionIndex, } - wait_for_event( + let result = wait_for_event( connection, ("Session", "NewSession"), |e: NewSessionEvent| { info!(target: "aleph-client", "New session {}", e.session_index); - e.session_index == session_index + session_predicate(e.session_index) }, )?; - Ok(session_index) + Ok(result.session_index) +} + +pub fn wait_for( + connection: &C, + session_index: SessionIndex, +) -> anyhow::Result { + wait_for_predicate(connection, |session_ix| session_ix == session_index) +} + +pub fn wait_for_at_least( + connection: &C, + session_index: SessionIndex, +) -> anyhow::Result { + wait_for_predicate(connection, |session_ix| session_ix >= session_index) +} + +pub fn get_session_period(connection: &C) -> u32 { + connection + .as_connection() + .get_constant("Elections", "SessionPeriod") + .expect("Failed to decode SessionPeriod extrinsic!") } diff --git a/aleph-client/src/staking.rs b/aleph-client/src/staking.rs index fbfaea5973..5649dfae3d 100644 --- a/aleph-client/src/staking.rs +++ b/aleph-client/src/staking.rs @@ -1,9 +1,13 @@ +use std::collections::BTreeMap; + use codec::{Compact, Decode, Encode}; use frame_support::BoundedVec; -use log::info; -use pallet_staking::{MaxUnlockingChunks, RewardDestination, UnlockChunk, ValidatorPrefs}; +use pallet_staking::{ + Exposure, MaxUnlockingChunks, RewardDestination, UnlockChunk, ValidatorPrefs, +}; +use primitives::EraIndex; use rayon::prelude::*; -use sp_core::Pair; +use sp_core::{Pair, H256}; use sp_runtime::Perbill; use substrate_api_client::{ compose_call, compose_extrinsic, AccountId, Balance, ExtrinsicParams, GenericAddress, XtStatus, @@ -97,33 +101,21 @@ pub fn force_new_era(connection: &RootConnection, status: XtStatus) { send_xt(connection, xt, Some("force_new_era"), status); } -pub fn get_current_era(connection: &C) -> u32 { - let current_era = connection - .as_connection() - .get_storage_value("Staking", "ActiveEra", None) - .expect("Failed to decode ActiveEra extrinsic!") - .expect("ActiveEra is empty in the storage!"); - info!(target: "aleph-client", "Current era is {}", current_era); - current_era -} - -pub fn wait_for_full_era_completion( - connection: &C, -) -> anyhow::Result { +pub fn wait_for_full_era_completion(connection: &C) -> anyhow::Result { // staking works in such a way, that when we request a controller to be a validator in era N, // then the changes are applied in the era N+1 (so the new validator is receiving points in N+1), // so that we need N+1 to finish in order to claim the reward in era N+2 for the N+1 era wait_for_era_completion(connection, get_current_era(connection) + 2) } -pub fn wait_for_next_era(connection: &C) -> anyhow::Result { +pub fn wait_for_next_era(connection: &C) -> anyhow::Result { wait_for_era_completion(connection, get_current_era(connection) + 1) } fn wait_for_era_completion( connection: &C, - next_era_index: u32, -) -> anyhow::Result { + next_era_index: EraIndex, +) -> anyhow::Result { let sessions_per_era: u32 = connection .as_connection() .get_constant("Staking", "SessionsPerEra") @@ -133,6 +125,25 @@ fn wait_for_era_completion( Ok(next_era_index) } +pub fn get_sessions_per_era(connection: &C) -> u32 { + connection + .as_connection() + .get_constant("Staking", "SessionsPerEra") + .expect("Failed to decode SessionsPerEra extrinsic!") +} + +pub fn get_era(connection: &C, block: Option) -> EraIndex { + connection + .as_connection() + .get_storage_value("Staking", "ActiveEra", block) + .expect("Failed to decode ActiveEra extrinsic!") + .expect("ActiveEra is empty in the storage!") +} + +pub fn get_current_era(connection: &C) -> EraIndex { + get_era(connection, None) +} + pub fn payout_stakers( stash_connection: &SignedConnection, stash_account: &AccountId, @@ -294,10 +305,53 @@ pub fn ledger(connection: &C, controller: &KeyPair) -> Option< .unwrap_or_else(|_| panic!("Failed to obtain Ledger for account id {}", account_id)) } -pub fn get_payout_for_era(connection: &C, era: u32) -> u128 { +pub fn get_payout_for_era(connection: &C, era: EraIndex) -> u128 { connection .as_connection() .get_storage_map("Staking", "ErasValidatorReward", era, None) .expect("Failed to decode ErasValidatorReward") .expect("ErasValidatoReward is empty in the storage") } + +pub fn get_exposure( + connection: &C, + era: EraIndex, + account_id: &AccountId, + block_hash: Option, +) -> Exposure { + connection + .as_connection() + .get_storage_double_map("Staking", "ErasStakers", era, account_id, block_hash) + .expect("Failed to decode ErasStakers extrinsic!") + .unwrap_or_else(|| panic!("Failed to obtain ErasStakers for era {}.", era)) +} + +pub type RewardPoint = u32; + +/// Helper to decode reward points for an era without the need to fill in a generic parameter. +/// Reward points of an era. Used to split era total payout between validators. +/// +/// This points will be used to reward validators and their respective nominators. +#[derive(Clone, Decode, Default)] +pub struct EraRewardPoints { + /// Total number of points. Equals the sum of reward points for each validator. + pub total: RewardPoint, + /// The reward points earned by a given validator. + pub individual: BTreeMap, +} + +pub fn get_era_reward_points( + connection: &C, + era: EraIndex, + block_hash: Option, +) -> Option { + connection + .as_connection() + .get_storage_map("Staking", "ErasRewardPoints", era, block_hash) + .unwrap_or_else(|e| { + panic!( + "Failed to obtain ErasRewardPoints for era {} at block {:?}: {}", + era, block_hash, e + ) + }) +} diff --git a/e2e-tests/Cargo.lock b/e2e-tests/Cargo.lock index b7341cc8a2..daf3d91937 100644 --- a/e2e-tests/Cargo.lock +++ b/e2e-tests/Cargo.lock @@ -104,6 +104,7 @@ dependencies = [ "frame-system", "log", "pallet-balances", + "pallet-elections", "pallet-staking", "parity-scale-codec", "primitives", @@ -1782,6 +1783,25 @@ dependencies = [ "sp-std", ] +[[package]] +name = "pallet-elections" +version = "0.3.0" +dependencies = [ + "frame-election-provider-support", + "frame-support", + "frame-system", + "pallet-authorship", + "pallet-balances", + "pallet-session", + "pallet-staking", + "parity-scale-codec", + "primitives", + "scale-info", + "sp-core", + "sp-staking", + "sp-std", +] + [[package]] name = "pallet-multisig" version = "4.0.0-dev" diff --git a/e2e-tests/Cargo.toml b/e2e-tests/Cargo.toml index 246bca1c23..29f124f5c8 100644 --- a/e2e-tests/Cargo.toml +++ b/e2e-tests/Cargo.toml @@ -23,6 +23,7 @@ pallet-staking = { git = "https://github.com/Cardinal-Cryptography/substrate.git pallet-balances = { git = "https://github.com/Cardinal-Cryptography/substrate.git", branch = "aleph-v0.9.23", default-features = false } aleph_client = { path = "../aleph-client" } +pallet-elections = { path = "../pallets/elections" } primitives = { path = "../primitives", features = ["short_session"], default-features = false } [features] diff --git a/e2e-tests/src/accounts.rs b/e2e-tests/src/accounts.rs index 641341c250..407431d1fd 100644 --- a/e2e-tests/src/accounts.rs +++ b/e2e-tests/src/accounts.rs @@ -2,12 +2,16 @@ use aleph_client::{keypair_from_string, KeyPair}; use crate::config::Config; +fn get_validator_seed(seed: u32) -> String { + format!("//{}", seed) +} + // this should be extracted to common code pub fn get_validators_seeds(config: &Config) -> Vec { match config.validators_seeds { Some(ref seeds) => seeds.clone(), None => (0..config.validators_count) - .map(|seed| format!("//{}", seed)) + .map(get_validator_seed) .collect(), } } @@ -27,3 +31,27 @@ pub fn accounts_seeds_to_keys(seeds: &[String]) -> Vec { pub fn get_sudo_key(config: &Config) -> KeyPair { keypair_from_string(&config.sudo_seed) } + +pub struct NodeKeys { + pub validator: KeyPair, + pub controller: KeyPair, + pub stash: KeyPair, +} + +impl From for NodeKeys { + fn from(seed: String) -> Self { + Self { + validator: keypair_from_string(&seed), + controller: keypair_from_string(&get_validators_controller_seed(&seed)), + stash: keypair_from_string(&get_validators_stash_seed(&seed)), + } + } +} + +fn get_validators_controller_seed(seed: &str) -> String { + format!("{}//Controller", seed) +} + +fn get_validators_stash_seed(seed: &str) -> String { + format!("{}//stash", seed) +} diff --git a/e2e-tests/src/cases.rs b/e2e-tests/src/cases.rs index a8ae97e2b4..fb5bed58a8 100644 --- a/e2e-tests/src/cases.rs +++ b/e2e-tests/src/cases.rs @@ -2,7 +2,7 @@ use crate::{ config::Config, test::{ batch_transactions as test_batch_transactions, change_validators as test_change_validators, - channeling_fee_and_tip as test_channeling_fee_and_tip, + channeling_fee_and_tip as test_channeling_fee_and_tip, disable_node as test_disable_node, era_payouts_calculated_correctly as test_era_payout, era_validators as test_era_validators, fee_calculation as test_fee_calculation, finalization as test_finalization, staking_era_payouts as test_staking_era_payouts, @@ -21,6 +21,7 @@ pub type PossibleTestCases = Vec<(&'static str, TestCase)>; pub fn possible_test_cases() -> PossibleTestCases { vec![ ("finalization", test_finalization as TestCase), + ("reward_points_disable_node", test_disable_node as TestCase), ("token_transfer", test_token_transfer as TestCase), ( "channeling_fee_and_tip", diff --git a/e2e-tests/src/config.rs b/e2e-tests/src/config.rs index 11f2e93a44..bdc1a951e3 100644 --- a/e2e-tests/src/config.rs +++ b/e2e-tests/src/config.rs @@ -1,5 +1,8 @@ +use aleph_client::RootConnection; use clap::Parser; +use crate::accounts::{get_sudo_key, get_validators_seeds, NodeKeys}; + #[derive(Debug, Parser, Clone)] #[clap(version = "1.0")] pub struct Config { @@ -24,3 +27,20 @@ pub struct Config { #[clap(long, default_value = "//Alice")] pub sudo_seed: String, } + +impl Config { + /// Returns keys associated with the node represented by this Config (first of the validators_seeds). + /// Panics if Config is invalid. + pub fn node_keys(&self) -> NodeKeys { + let validator_seed = get_validators_seeds(self) + .into_iter() + .next() + .expect("we should have a seed for at least one validator"); + NodeKeys::from(validator_seed) + } + + pub fn create_root_connection(&self) -> RootConnection { + let sudo_keypair = get_sudo_key(self); + RootConnection::new(&self.node, sudo_keypair) + } +} diff --git a/e2e-tests/src/lib.rs b/e2e-tests/src/lib.rs index 088e39adbe..f58f47f0e4 100644 --- a/e2e-tests/src/lib.rs +++ b/e2e-tests/src/lib.rs @@ -4,5 +4,6 @@ pub use config::Config; mod accounts; mod cases; mod config; +mod rewards; mod test; mod transfer; diff --git a/e2e-tests/src/rewards.rs b/e2e-tests/src/rewards.rs new file mode 100644 index 0000000000..e20506f57d --- /dev/null +++ b/e2e-tests/src/rewards.rs @@ -0,0 +1,239 @@ +use std::collections::HashMap; + +use aleph_client::{ + get_block_hash, get_current_session, get_era_reward_points, get_exposure, get_session_period, + rotate_keys, set_keys, wait_for_at_least_session, wait_for_finalized_block, AnyConnection, + RewardPoint, SessionKeys, SignedConnection, +}; +use log::info; +use pallet_elections::LENIENT_THRESHOLD; +use pallet_staking::Exposure; +use primitives::{EraIndex, SessionIndex}; +use sp_core::H256; +use sp_runtime::Perquintill; +use substrate_api_client::{AccountId, XtStatus}; + +/// Changes session_keys used by a given `controller` to some `zero`/invalid value, +/// making it impossible to create new legal blocks. +pub fn set_invalid_keys_for_validator( + controller_connection: &SignedConnection, +) -> anyhow::Result<()> { + const ZERO_SESSION_KEYS: SessionKeys = SessionKeys { + aura: [0; 32], + aleph: [0; 32], + }; + + set_keys(controller_connection, ZERO_SESSION_KEYS, XtStatus::InBlock); + // wait until our node is forced to use new keys, i.e. current session + 2 + let current_session = get_current_session(controller_connection); + wait_for_at_least_session(controller_connection, current_session + 2)?; + + Ok(()) +} + +/// Rotates session_keys of a given `controller`, making it able to rejoin the `consensus`. +pub fn reset_validator_keys(controller_connection: &SignedConnection) -> anyhow::Result<()> { + let validator_keys = + rotate_keys(controller_connection).expect("Failed to retrieve keys from chain"); + set_keys(controller_connection, validator_keys, XtStatus::InBlock); + + // wait until our node is forced to use new keys, i.e. current session + 2 + let current_session = get_current_session(controller_connection); + wait_for_at_least_session(controller_connection, current_session + 2)?; + + Ok(()) +} + +pub fn download_exposure( + connection: &SignedConnection, + era: EraIndex, + account_id: &AccountId, + beginning_of_session_block_hash: H256, +) -> u128 { + let exposure: Exposure = get_exposure( + connection, + era, + account_id, + Some(beginning_of_session_block_hash), + ); + info!( + "Validator {} has own exposure of {} and total of {}.", + account_id, exposure.own, exposure.total + ); + exposure.others.iter().for_each(|individual_exposure| { + info!( + "Validator {} has nominator {} exposure {}.", + account_id, individual_exposure.who, individual_exposure.value + ) + }); + exposure.total +} + +fn check_rewards( + validator_reward_points: HashMap, + retrieved_reward_points: HashMap, + max_relative_difference: f64, +) -> anyhow::Result<()> { + let our_sum: f64 = validator_reward_points + .iter() + .map(|(_, reward)| reward) + .sum(); + let retrieved_sum: u32 = retrieved_reward_points + .iter() + .map(|(_, reward)| reward) + .sum(); + + for (account, reward) in validator_reward_points { + let retrieved_reward = *retrieved_reward_points.get(&account).unwrap_or_else(|| { + panic!( + "missing account={} in retrieved collection of reward points", + account + ) + }); + + let reward_ratio = reward / our_sum; + let retrieved_ratio = retrieved_reward as f64 / retrieved_sum as f64; + + info!( + "{} reward_ratio: {}; retrieved_ratio: {}.", + account, reward_ratio, retrieved_ratio + ); + assert!((reward_ratio - retrieved_ratio).abs() <= max_relative_difference); + } + + Ok(()) +} + +fn get_node_performance( + connection: &SignedConnection, + account_id: AccountId, + before_end_of_session_block_hash: H256, + blocks_to_produce_per_session: u32, +) -> f64 { + let block_count: u32 = connection + .as_connection() + .get_storage_map( + "Elections", + "SessionValidatorBlockCount", + account_id.clone(), + Some(before_end_of_session_block_hash), + ) + .expect("Failed to decode SessionValidatorBlockCount extrinsic!") + .unwrap_or(0); + info!( + "Block count for validator {} is {:?}, block hash is {}.", + account_id, block_count, before_end_of_session_block_hash + ); + let performance = block_count as f64 / blocks_to_produce_per_session as f64; + info!("validator {}, performance {:?}.", account_id, performance); + let lenient_performance = match Perquintill::from_float(performance) >= LENIENT_THRESHOLD + && blocks_to_produce_per_session >= block_count + { + true => 1.0, + false => performance, + }; + info!( + "Validator {}, lenient performance {:?}.", + account_id, lenient_performance + ); + lenient_performance +} + +pub fn check_points( + connection: &SignedConnection, + session: SessionIndex, + era: EraIndex, + members: impl IntoIterator + Clone, + members_bench: impl IntoIterator + Clone, + max_relative_difference: f64, +) -> anyhow::Result<()> { + let session_period = get_session_period(connection); + + info!("Era: {} | session: {}.", era, session); + + let beggining_of_session_block = session * session_period; + let end_of_session_block = beggining_of_session_block + session_period; + info!("Waiting for block: {}.", end_of_session_block); + wait_for_finalized_block(connection, end_of_session_block)?; + + let beggining_of_session_block_hash = get_block_hash(connection, beggining_of_session_block); + let end_of_session_block_hash = get_block_hash(connection, end_of_session_block); + let before_end_of_session_block_hash = get_block_hash(connection, end_of_session_block - 1); + info!("End-of-session block hash: {}.", end_of_session_block_hash); + + let members_per_session: u32 = connection + .as_connection() + .get_storage_value( + "Elections", + "CommitteeSize", + Some(beggining_of_session_block_hash), + ) + .expect("Failed to decode CommitteeSize extrinsic!") + .unwrap_or_else(|| panic!("Failed to obtain CommitteeSize for session {}.", session)); + + info!("Members per session: {}.", members_per_session); + + let blocks_to_produce_per_session = session_period / members_per_session; + info!( + "Blocks to produce per session: {} - session period {}.", + blocks_to_produce_per_session, session_period + ); + + // get points stored by the Staking pallet + let validator_reward_points_current_era = + get_era_reward_points(connection, era, Some(end_of_session_block_hash)) + .unwrap_or_default() + .individual; + + let validator_reward_points_previous_session = + get_era_reward_points(connection, era, Some(beggining_of_session_block_hash)) + .unwrap_or_default() + .individual; + + let validator_reward_points_current_session: HashMap = + validator_reward_points_current_era + .into_iter() + .map(|(account_id, reward_points)| { + let reward_points_previous_session = validator_reward_points_previous_session + .get(&account_id) + .unwrap_or(&0); + let reward_points_current = reward_points - reward_points_previous_session; + + info!( + "In session {} validator {} accumulated {}.", + session, account_id, reward_points + ); + (account_id, reward_points_current) + }) + .collect(); + + let members_uptime = members.into_iter().map(|account_id| { + ( + account_id.clone(), + get_node_performance( + connection, + account_id, + before_end_of_session_block_hash, + blocks_to_produce_per_session, + ), + ) + }); + + let members_bench_uptime = members_bench + .into_iter() + .map(|account_id| (account_id, 1.0)); + + let mut reward_points: HashMap<_, _> = members_uptime.chain(members_bench_uptime).collect(); + let members_count = reward_points.len() as f64; + for (account_id, reward_points) in reward_points.iter_mut() { + let exposure = + download_exposure(connection, era, account_id, beggining_of_session_block_hash); + *reward_points *= exposure as f64 / members_count; + } + + check_rewards( + reward_points, + validator_reward_points_current_session, + max_relative_difference, + ) +} diff --git a/e2e-tests/src/test/mod.rs b/e2e-tests/src/test/mod.rs index feec301c1c..6746200c91 100644 --- a/e2e-tests/src/test/mod.rs +++ b/e2e-tests/src/test/mod.rs @@ -2,6 +2,7 @@ pub use era_payout::era_payouts_calculated_correctly; pub use era_validators::era_validators; pub use fee::fee_calculation; pub use finalization::finalization; +pub use rewards::disable_node; pub use staking::{staking_era_payouts, staking_new_validator}; pub use transfer::token_transfer; pub use treasury::{channeling_fee_and_tip, treasury_access}; @@ -13,6 +14,7 @@ mod era_payout; mod era_validators; mod fee; mod finalization; +mod rewards; mod staking; mod transfer; mod treasury; diff --git a/e2e-tests/src/test/rewards.rs b/e2e-tests/src/test/rewards.rs new file mode 100644 index 0000000000..2ff6fd7164 --- /dev/null +++ b/e2e-tests/src/test/rewards.rs @@ -0,0 +1,111 @@ +use aleph_client::{ + account_from_keypair, change_validators, get_sessions_per_era, wait_for_full_era_completion, + wait_for_next_era, KeyPair, SignedConnection, +}; +use log::info; +use primitives::SessionIndex; +use substrate_api_client::{AccountId, XtStatus}; + +use crate::{ + accounts::get_validators_keys, + rewards::{check_points, reset_validator_keys, set_invalid_keys_for_validator}, + Config, +}; + +fn get_reserved_members(config: &Config) -> Vec { + get_validators_keys(config)[0..2].to_vec() +} + +fn get_non_reserved_members(config: &Config) -> Vec { + get_validators_keys(config)[2..].to_vec() +} + +fn get_non_reserved_members_for_session(config: &Config, session: SessionIndex) -> Vec { + // Test assumption + const FREE_SEATS: u32 = 2; + + let mut non_reserved = vec![]; + + let non_reserved_nodes_order_from_runtime = get_non_reserved_members(config); + let non_reserved_nodes_order_from_runtime_len = non_reserved_nodes_order_from_runtime.len(); + + for i in (FREE_SEATS * session)..(FREE_SEATS * (session + 1)) { + non_reserved.push( + non_reserved_nodes_order_from_runtime + [i as usize % non_reserved_nodes_order_from_runtime_len] + .clone(), + ); + } + + non_reserved.iter().map(account_from_keypair).collect() +} + +pub fn disable_node(config: &Config) -> anyhow::Result<()> { + const MAX_DIFFERENCE: f64 = 0.05; + const VALIDATORS_PER_SESSION: u32 = 4; + + let root_connection = config.create_root_connection(); + + let sessions_per_era = get_sessions_per_era(&root_connection); + + let reserved_members: Vec<_> = get_reserved_members(config) + .iter() + .map(account_from_keypair) + .collect(); + let non_reserved_members: Vec<_> = get_non_reserved_members(config) + .iter() + .map(account_from_keypair) + .collect(); + + change_validators( + &root_connection, + Some(reserved_members.clone()), + Some(non_reserved_members.clone()), + Some(VALIDATORS_PER_SESSION), + XtStatus::Finalized, + ); + + let era = wait_for_next_era(&root_connection)?; + let start_session = era * sessions_per_era; + + let controller_connection = SignedConnection::new(&config.node, config.node_keys().controller); + // this should `disable` this node by setting invalid session_keys + set_invalid_keys_for_validator(&controller_connection)?; + // this should `re-enable` this node, i.e. by means of the `rotate keys` procedure + reset_validator_keys(&controller_connection)?; + + let era = wait_for_full_era_completion(&root_connection)?; + + let end_session = era * sessions_per_era; + + info!( + "Checking rewards for sessions {}..{}.", + start_session, end_session + ); + + for session in start_session..end_session { + let non_reserved_for_session = get_non_reserved_members_for_session(config, session); + let non_reserved_bench = non_reserved_members + .iter() + .filter(|account_id| !non_reserved_for_session.contains(*account_id)) + .cloned(); + + let members = reserved_members + .iter() + .chain(non_reserved_for_session.iter()) + .cloned(); + let members_bench: Vec<_> = non_reserved_bench.collect(); + + let era = session / sessions_per_era; + check_points( + &controller_connection, + session, + era, + members, + members_bench, + MAX_DIFFERENCE, + )?; + } + + Ok(()) +} diff --git a/pallets/elections/src/impls.rs b/pallets/elections/src/impls.rs index 2a2318f724..d6691fe14c 100644 --- a/pallets/elections/src/impls.rs +++ b/pallets/elections/src/impls.rs @@ -11,7 +11,7 @@ use crate::{ }; const MAX_REWARD: u32 = 1_000_000_000; -const LENIENT_THRESHOLD: Perquintill = Perquintill::from_percent(90); +pub const LENIENT_THRESHOLD: Perquintill = Perquintill::from_percent(90); /// We assume that block `B` ends session nr `S`, and current era index is `E`. /// diff --git a/pallets/elections/src/lib.rs b/pallets/elections/src/lib.rs index 00a54b6c49..a26a369d10 100644 --- a/pallets/elections/src/lib.rs +++ b/pallets/elections/src/lib.rs @@ -20,7 +20,7 @@ mod traits; use codec::{Decode, Encode}; use frame_support::traits::StorageVersion; -pub use impls::compute_validator_scaled_total_rewards; +pub use impls::{compute_validator_scaled_total_rewards, LENIENT_THRESHOLD}; pub use pallet::*; use scale_info::TypeInfo; use sp_std::{ diff --git a/primitives/src/lib.rs b/primitives/src/lib.rs index f868671dd6..6620861d3a 100644 --- a/primitives/src/lib.rs +++ b/primitives/src/lib.rs @@ -3,7 +3,7 @@ use codec::{Decode, Encode}; use sp_core::crypto::KeyTypeId; use sp_runtime::ConsensusEngineId; -pub use sp_staking::SessionIndex; +pub use sp_staking::{EraIndex, SessionIndex}; use sp_std::vec::Vec; pub const KEY_TYPE: KeyTypeId = KeyTypeId(*b"alp0");