diff --git a/polkadot/runtime/parachains/src/configuration.rs b/polkadot/runtime/parachains/src/configuration.rs index 32a71975789dc9d8e3c82d331467b7d20785ae63..8337be8c29584becb4aa7891e6be713c967bb16f 100644 --- a/polkadot/runtime/parachains/src/configuration.rs +++ b/polkadot/runtime/parachains/src/configuration.rs @@ -26,6 +26,9 @@ use primitives::v1::{Balance, SessionIndex, MAX_CODE_SIZE, MAX_HEAD_DATA_SIZE, M use sp_runtime::traits::Zero; use sp_std::prelude::*; +#[cfg(test)] +mod tests; + #[cfg(feature = "runtime-benchmarks")] mod benchmarking; @@ -1301,565 +1304,3 @@ impl<T: Config> Pallet<T> { Ok(()) } } - -#[cfg(test)] -mod tests { - use super::*; - use crate::mock::{new_test_ext, Configuration, Origin, ParasShared, Test}; - use frame_support::{assert_err, assert_ok}; - - fn on_new_session( - session_index: SessionIndex, - ) -> (HostConfiguration<u32>, HostConfiguration<u32>) { - ParasShared::set_session_index(session_index); - let SessionChangeOutcome { prev_config, new_config } = - Configuration::initializer_on_new_session(&session_index); - let new_config = new_config.unwrap_or_else(|| prev_config.clone()); - (prev_config, new_config) - } - - #[test] - fn default_is_consistent() { - new_test_ext(Default::default()).execute_with(|| { - Configuration::config().panic_if_not_consistent(); - }); - } - - #[test] - fn scheduled_session_is_two_sessions_from_now() { - new_test_ext(Default::default()).execute_with(|| { - // The logic here is really tested only with scheduled_session = 2. It should work - // with other values, but that should receive a more rigorious testing. - on_new_session(1); - assert_eq!(Configuration::scheduled_session(), 3); - }); - } - - #[test] - fn initializer_on_new_session() { - new_test_ext(Default::default()).execute_with(|| { - let (prev_config, new_config) = on_new_session(1); - assert_eq!(prev_config, new_config); - assert_ok!(Configuration::set_validation_upgrade_delay(Origin::root(), 100)); - - let (prev_config, new_config) = on_new_session(2); - assert_eq!(prev_config, new_config); - - let (prev_config, new_config) = on_new_session(3); - assert_eq!(prev_config, HostConfiguration::default()); - assert_eq!( - new_config, - HostConfiguration { validation_upgrade_delay: 100, ..prev_config } - ); - }); - } - - #[test] - fn config_changes_after_2_session_boundary() { - new_test_ext(Default::default()).execute_with(|| { - let old_config = Configuration::config(); - let mut config = old_config.clone(); - config.validation_upgrade_delay = 100; - assert!(old_config != config); - - assert_ok!(Configuration::set_validation_upgrade_delay(Origin::root(), 100)); - - // Verify that the current configuration has not changed and that there is a scheduled - // change for the SESSION_DELAY sessions in advance. - assert_eq!(Configuration::config(), old_config); - assert_eq!(<Configuration as Store>::PendingConfigs::get(), vec![(2, config.clone())]); - - on_new_session(1); - - // One session has passed, we should be still waiting for the pending configuration. - assert_eq!(Configuration::config(), old_config); - assert_eq!(<Configuration as Store>::PendingConfigs::get(), vec![(2, config.clone())]); - - on_new_session(2); - - assert_eq!(Configuration::config(), config); - assert_eq!(<Configuration as Store>::PendingConfigs::get(), vec![]); - }) - } - - #[test] - fn consecutive_changes_within_one_session() { - new_test_ext(Default::default()).execute_with(|| { - let old_config = Configuration::config(); - let mut config = old_config.clone(); - config.validation_upgrade_delay = 100; - config.validation_upgrade_cooldown = 100; - assert!(old_config != config); - - assert_ok!(Configuration::set_validation_upgrade_delay(Origin::root(), 100)); - assert_ok!(Configuration::set_validation_upgrade_cooldown(Origin::root(), 100)); - assert_eq!(Configuration::config(), old_config); - assert_eq!(<Configuration as Store>::PendingConfigs::get(), vec![(2, config.clone())]); - - on_new_session(1); - - assert_eq!(Configuration::config(), old_config); - assert_eq!(<Configuration as Store>::PendingConfigs::get(), vec![(2, config.clone())]); - - on_new_session(2); - - assert_eq!(Configuration::config(), config); - assert_eq!(<Configuration as Store>::PendingConfigs::get(), vec![]); - }); - } - - #[test] - fn pending_next_session_but_we_upgrade_once_more() { - new_test_ext(Default::default()).execute_with(|| { - let initial_config = Configuration::config(); - let intermediate_config = - HostConfiguration { validation_upgrade_delay: 100, ..initial_config.clone() }; - let final_config = HostConfiguration { - validation_upgrade_delay: 100, - validation_upgrade_cooldown: 99, - ..initial_config.clone() - }; - - assert_ok!(Configuration::set_validation_upgrade_delay(Origin::root(), 100)); - assert_eq!(Configuration::config(), initial_config); - assert_eq!( - <Configuration as Store>::PendingConfigs::get(), - vec![(2, intermediate_config.clone())] - ); - - on_new_session(1); - - // We are still waiting until the pending configuration is applied and we add another - // update. - assert_ok!(Configuration::set_validation_upgrade_cooldown(Origin::root(), 99)); - - // This should result in yet another configiguration change scheduled. - assert_eq!(Configuration::config(), initial_config); - assert_eq!( - <Configuration as Store>::PendingConfigs::get(), - vec![(2, intermediate_config.clone()), (3, final_config.clone())] - ); - - on_new_session(2); - - assert_eq!(Configuration::config(), intermediate_config); - assert_eq!( - <Configuration as Store>::PendingConfigs::get(), - vec![(3, final_config.clone())] - ); - - on_new_session(3); - - assert_eq!(Configuration::config(), final_config); - assert_eq!(<Configuration as Store>::PendingConfigs::get(), vec![]); - }); - } - - #[test] - fn scheduled_session_config_update_while_next_session_pending() { - new_test_ext(Default::default()).execute_with(|| { - let initial_config = Configuration::config(); - let intermediate_config = - HostConfiguration { validation_upgrade_delay: 100, ..initial_config.clone() }; - let final_config = HostConfiguration { - validation_upgrade_delay: 100, - validation_upgrade_cooldown: 99, - code_retention_period: 98, - ..initial_config.clone() - }; - - assert_ok!(Configuration::set_validation_upgrade_delay(Origin::root(), 100)); - assert_eq!(Configuration::config(), initial_config); - assert_eq!( - <Configuration as Store>::PendingConfigs::get(), - vec![(2, intermediate_config.clone())] - ); - - on_new_session(1); - - // The second call should fall into the case where we already have a pending config - // update for the scheduled_session, but we want to update it once more. - assert_ok!(Configuration::set_validation_upgrade_cooldown(Origin::root(), 99)); - assert_ok!(Configuration::set_code_retention_period(Origin::root(), 98)); - - // This should result in yet another configiguration change scheduled. - assert_eq!(Configuration::config(), initial_config); - assert_eq!( - <Configuration as Store>::PendingConfigs::get(), - vec![(2, intermediate_config.clone()), (3, final_config.clone())] - ); - - on_new_session(2); - - assert_eq!(Configuration::config(), intermediate_config); - assert_eq!( - <Configuration as Store>::PendingConfigs::get(), - vec![(3, final_config.clone())] - ); - - on_new_session(3); - - assert_eq!(Configuration::config(), final_config); - assert_eq!(<Configuration as Store>::PendingConfigs::get(), vec![]); - }); - } - - #[test] - fn invariants() { - new_test_ext(Default::default()).execute_with(|| { - assert_err!( - Configuration::set_max_code_size(Origin::root(), MAX_CODE_SIZE + 1), - Error::<Test>::InvalidNewValue - ); - - assert_err!( - Configuration::set_max_pov_size(Origin::root(), MAX_POV_SIZE + 1), - Error::<Test>::InvalidNewValue - ); - - assert_err!( - Configuration::set_max_head_data_size(Origin::root(), MAX_HEAD_DATA_SIZE + 1), - Error::<Test>::InvalidNewValue - ); - - assert_err!( - Configuration::set_chain_availability_period(Origin::root(), 0), - Error::<Test>::InvalidNewValue - ); - assert_err!( - Configuration::set_thread_availability_period(Origin::root(), 0), - Error::<Test>::InvalidNewValue - ); - assert_err!( - Configuration::set_no_show_slots(Origin::root(), 0), - Error::<Test>::InvalidNewValue - ); - - <Configuration as Store>::ActiveConfig::put(HostConfiguration { - chain_availability_period: 10, - thread_availability_period: 8, - minimum_validation_upgrade_delay: 11, - ..Default::default() - }); - assert_err!( - Configuration::set_chain_availability_period(Origin::root(), 12), - Error::<Test>::InvalidNewValue - ); - assert_err!( - Configuration::set_thread_availability_period(Origin::root(), 12), - Error::<Test>::InvalidNewValue - ); - assert_err!( - Configuration::set_minimum_validation_upgrade_delay(Origin::root(), 9), - Error::<Test>::InvalidNewValue - ); - - assert_err!( - Configuration::set_validation_upgrade_delay(Origin::root(), 0), - Error::<Test>::InvalidNewValue - ); - }); - } - - #[test] - fn consistency_bypass_works() { - new_test_ext(Default::default()).execute_with(|| { - assert_err!( - Configuration::set_max_code_size(Origin::root(), MAX_CODE_SIZE + 1), - Error::<Test>::InvalidNewValue - ); - - assert_ok!(Configuration::set_bypass_consistency_check(Origin::root(), true)); - assert_ok!(Configuration::set_max_code_size(Origin::root(), MAX_CODE_SIZE + 1)); - - assert_eq!( - Configuration::config().max_code_size, - HostConfiguration::<u32>::default().max_code_size - ); - - on_new_session(1); - on_new_session(2); - - assert_eq!(Configuration::config().max_code_size, MAX_CODE_SIZE + 1); - }); - } - - #[test] - fn setting_pending_config_members() { - new_test_ext(Default::default()).execute_with(|| { - let new_config = HostConfiguration { - validation_upgrade_cooldown: 100, - validation_upgrade_delay: 10, - code_retention_period: 5, - max_code_size: 100_000, - max_pov_size: 1024, - max_head_data_size: 1_000, - parathread_cores: 2, - parathread_retries: 5, - group_rotation_frequency: 20, - chain_availability_period: 10, - thread_availability_period: 8, - scheduling_lookahead: 3, - max_validators_per_core: None, - max_validators: None, - dispute_period: 239, - dispute_post_conclusion_acceptance_period: 10, - dispute_max_spam_slots: 2, - dispute_conclusion_by_time_out_period: 512, - no_show_slots: 240, - n_delay_tranches: 241, - zeroth_delay_tranche_width: 242, - needed_approvals: 242, - relay_vrf_modulo_samples: 243, - max_upward_queue_count: 1337, - max_upward_queue_size: 228, - max_downward_message_size: 2048, - ump_service_total_weight: 20000, - max_upward_message_size: 448, - max_upward_message_num_per_candidate: 5, - hrmp_sender_deposit: 22, - hrmp_recipient_deposit: 4905, - hrmp_channel_max_capacity: 3921, - hrmp_channel_max_total_size: 7687, - hrmp_max_parachain_inbound_channels: 37, - hrmp_max_parathread_inbound_channels: 19, - hrmp_channel_max_message_size: 8192, - hrmp_max_parachain_outbound_channels: 10, - hrmp_max_parathread_outbound_channels: 20, - hrmp_max_message_num_per_candidate: 20, - ump_max_individual_weight: 909, - pvf_checking_enabled: true, - pvf_voting_ttl: 3, - minimum_validation_upgrade_delay: 20, - }; - - assert!(<Configuration as Store>::PendingConfig::get(shared::SESSION_DELAY).is_none()); - - Configuration::set_validation_upgrade_cooldown( - Origin::root(), - new_config.validation_upgrade_cooldown, - ) - .unwrap(); - Configuration::set_validation_upgrade_delay( - Origin::root(), - new_config.validation_upgrade_delay, - ) - .unwrap(); - Configuration::set_code_retention_period( - Origin::root(), - new_config.code_retention_period, - ) - .unwrap(); - Configuration::set_max_code_size(Origin::root(), new_config.max_code_size).unwrap(); - Configuration::set_max_pov_size(Origin::root(), new_config.max_pov_size).unwrap(); - Configuration::set_max_head_data_size(Origin::root(), new_config.max_head_data_size) - .unwrap(); - Configuration::set_parathread_cores(Origin::root(), new_config.parathread_cores) - .unwrap(); - Configuration::set_parathread_retries(Origin::root(), new_config.parathread_retries) - .unwrap(); - Configuration::set_group_rotation_frequency( - Origin::root(), - new_config.group_rotation_frequency, - ) - .unwrap(); - // This comes out of order to satisfy the validity criteria for the chain and thread - // availability periods. - Configuration::set_minimum_validation_upgrade_delay( - Origin::root(), - new_config.minimum_validation_upgrade_delay, - ) - .unwrap(); - Configuration::set_chain_availability_period( - Origin::root(), - new_config.chain_availability_period, - ) - .unwrap(); - Configuration::set_thread_availability_period( - Origin::root(), - new_config.thread_availability_period, - ) - .unwrap(); - Configuration::set_scheduling_lookahead( - Origin::root(), - new_config.scheduling_lookahead, - ) - .unwrap(); - Configuration::set_max_validators_per_core( - Origin::root(), - new_config.max_validators_per_core, - ) - .unwrap(); - Configuration::set_max_validators(Origin::root(), new_config.max_validators).unwrap(); - Configuration::set_dispute_period(Origin::root(), new_config.dispute_period).unwrap(); - Configuration::set_dispute_post_conclusion_acceptance_period( - Origin::root(), - new_config.dispute_post_conclusion_acceptance_period, - ) - .unwrap(); - Configuration::set_dispute_max_spam_slots( - Origin::root(), - new_config.dispute_max_spam_slots, - ) - .unwrap(); - Configuration::set_dispute_conclusion_by_time_out_period( - Origin::root(), - new_config.dispute_conclusion_by_time_out_period, - ) - .unwrap(); - Configuration::set_no_show_slots(Origin::root(), new_config.no_show_slots).unwrap(); - Configuration::set_n_delay_tranches(Origin::root(), new_config.n_delay_tranches) - .unwrap(); - Configuration::set_zeroth_delay_tranche_width( - Origin::root(), - new_config.zeroth_delay_tranche_width, - ) - .unwrap(); - Configuration::set_needed_approvals(Origin::root(), new_config.needed_approvals) - .unwrap(); - Configuration::set_relay_vrf_modulo_samples( - Origin::root(), - new_config.relay_vrf_modulo_samples, - ) - .unwrap(); - Configuration::set_max_upward_queue_count( - Origin::root(), - new_config.max_upward_queue_count, - ) - .unwrap(); - Configuration::set_max_upward_queue_size( - Origin::root(), - new_config.max_upward_queue_size, - ) - .unwrap(); - Configuration::set_max_downward_message_size( - Origin::root(), - new_config.max_downward_message_size, - ) - .unwrap(); - Configuration::set_ump_service_total_weight( - Origin::root(), - new_config.ump_service_total_weight, - ) - .unwrap(); - Configuration::set_max_upward_message_size( - Origin::root(), - new_config.max_upward_message_size, - ) - .unwrap(); - Configuration::set_max_upward_message_num_per_candidate( - Origin::root(), - new_config.max_upward_message_num_per_candidate, - ) - .unwrap(); - Configuration::set_hrmp_sender_deposit(Origin::root(), new_config.hrmp_sender_deposit) - .unwrap(); - Configuration::set_hrmp_recipient_deposit( - Origin::root(), - new_config.hrmp_recipient_deposit, - ) - .unwrap(); - Configuration::set_hrmp_channel_max_capacity( - Origin::root(), - new_config.hrmp_channel_max_capacity, - ) - .unwrap(); - Configuration::set_hrmp_channel_max_total_size( - Origin::root(), - new_config.hrmp_channel_max_total_size, - ) - .unwrap(); - Configuration::set_hrmp_max_parachain_inbound_channels( - Origin::root(), - new_config.hrmp_max_parachain_inbound_channels, - ) - .unwrap(); - Configuration::set_hrmp_max_parathread_inbound_channels( - Origin::root(), - new_config.hrmp_max_parathread_inbound_channels, - ) - .unwrap(); - Configuration::set_hrmp_channel_max_message_size( - Origin::root(), - new_config.hrmp_channel_max_message_size, - ) - .unwrap(); - Configuration::set_hrmp_max_parachain_outbound_channels( - Origin::root(), - new_config.hrmp_max_parachain_outbound_channels, - ) - .unwrap(); - Configuration::set_hrmp_max_parathread_outbound_channels( - Origin::root(), - new_config.hrmp_max_parathread_outbound_channels, - ) - .unwrap(); - Configuration::set_hrmp_max_message_num_per_candidate( - Origin::root(), - new_config.hrmp_max_message_num_per_candidate, - ) - .unwrap(); - Configuration::set_ump_max_individual_weight( - Origin::root(), - new_config.ump_max_individual_weight, - ) - .unwrap(); - Configuration::set_pvf_checking_enabled( - Origin::root(), - new_config.pvf_checking_enabled, - ) - .unwrap(); - Configuration::set_pvf_voting_ttl(Origin::root(), new_config.pvf_voting_ttl).unwrap(); - - assert_eq!( - <Configuration as Store>::PendingConfigs::get(), - vec![(shared::SESSION_DELAY, new_config)], - ); - }) - } - - #[test] - fn non_root_cannot_set_config() { - new_test_ext(Default::default()).execute_with(|| { - assert!(Configuration::set_validation_upgrade_delay(Origin::signed(1), 100).is_err()); - }); - } - - #[test] - fn verify_externally_accessible() { - // This test verifies that the value can be accessed through the well known keys and the - // host configuration decodes into the abridged version. - - use primitives::v1::{well_known_keys, AbridgedHostConfiguration}; - - new_test_ext(Default::default()).execute_with(|| { - let ground_truth = HostConfiguration::default(); - - // Make sure that the configuration is stored in the storage. - <Configuration as Store>::ActiveConfig::put(ground_truth.clone()); - - // Extract the active config via the well known key. - let raw_active_config = sp_io::storage::get(well_known_keys::ACTIVE_CONFIG) - .expect("config must be present in storage under ACTIVE_CONFIG"); - let abridged_config = AbridgedHostConfiguration::decode(&mut &raw_active_config[..]) - .expect("HostConfiguration must be decodable into AbridgedHostConfiguration"); - - assert_eq!( - abridged_config, - AbridgedHostConfiguration { - max_code_size: ground_truth.max_code_size, - max_head_data_size: ground_truth.max_head_data_size, - max_upward_queue_count: ground_truth.max_upward_queue_count, - max_upward_queue_size: ground_truth.max_upward_queue_size, - max_upward_message_size: ground_truth.max_upward_message_size, - max_upward_message_num_per_candidate: ground_truth - .max_upward_message_num_per_candidate, - hrmp_max_message_num_per_candidate: ground_truth - .hrmp_max_message_num_per_candidate, - validation_upgrade_cooldown: ground_truth.validation_upgrade_cooldown, - validation_upgrade_delay: ground_truth.validation_upgrade_delay, - }, - ); - }); - } -} diff --git a/polkadot/runtime/parachains/src/configuration/tests.rs b/polkadot/runtime/parachains/src/configuration/tests.rs new file mode 100644 index 0000000000000000000000000000000000000000..8750560e4a9961e321590f7114c05cd145c9ab0b --- /dev/null +++ b/polkadot/runtime/parachains/src/configuration/tests.rs @@ -0,0 +1,553 @@ +// Copyright 2020 Parity Technologies (UK) Ltd. +// This file is part of Polkadot. + +// Polkadot is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Polkadot is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Polkadot. If not, see <http://www.gnu.org/licenses/>. + +use super::*; +use crate::mock::{new_test_ext, Configuration, Origin, ParasShared, Test}; +use frame_support::{assert_err, assert_ok}; + +fn on_new_session(session_index: SessionIndex) -> (HostConfiguration<u32>, HostConfiguration<u32>) { + ParasShared::set_session_index(session_index); + let SessionChangeOutcome { prev_config, new_config } = + Configuration::initializer_on_new_session(&session_index); + let new_config = new_config.unwrap_or_else(|| prev_config.clone()); + (prev_config, new_config) +} + +#[test] +fn default_is_consistent() { + new_test_ext(Default::default()).execute_with(|| { + Configuration::config().panic_if_not_consistent(); + }); +} + +#[test] +fn scheduled_session_is_two_sessions_from_now() { + new_test_ext(Default::default()).execute_with(|| { + // The logic here is really tested only with scheduled_session = 2. It should work + // with other values, but that should receive a more rigorious testing. + on_new_session(1); + assert_eq!(Configuration::scheduled_session(), 3); + }); +} + +#[test] +fn initializer_on_new_session() { + new_test_ext(Default::default()).execute_with(|| { + let (prev_config, new_config) = on_new_session(1); + assert_eq!(prev_config, new_config); + assert_ok!(Configuration::set_validation_upgrade_delay(Origin::root(), 100)); + + let (prev_config, new_config) = on_new_session(2); + assert_eq!(prev_config, new_config); + + let (prev_config, new_config) = on_new_session(3); + assert_eq!(prev_config, HostConfiguration::default()); + assert_eq!(new_config, HostConfiguration { validation_upgrade_delay: 100, ..prev_config }); + }); +} + +#[test] +fn config_changes_after_2_session_boundary() { + new_test_ext(Default::default()).execute_with(|| { + let old_config = Configuration::config(); + let mut config = old_config.clone(); + config.validation_upgrade_delay = 100; + assert!(old_config != config); + + assert_ok!(Configuration::set_validation_upgrade_delay(Origin::root(), 100)); + + // Verify that the current configuration has not changed and that there is a scheduled + // change for the SESSION_DELAY sessions in advance. + assert_eq!(Configuration::config(), old_config); + assert_eq!(<Configuration as Store>::PendingConfigs::get(), vec![(2, config.clone())]); + + on_new_session(1); + + // One session has passed, we should be still waiting for the pending configuration. + assert_eq!(Configuration::config(), old_config); + assert_eq!(<Configuration as Store>::PendingConfigs::get(), vec![(2, config.clone())]); + + on_new_session(2); + + assert_eq!(Configuration::config(), config); + assert_eq!(<Configuration as Store>::PendingConfigs::get(), vec![]); + }) +} + +#[test] +fn consecutive_changes_within_one_session() { + new_test_ext(Default::default()).execute_with(|| { + let old_config = Configuration::config(); + let mut config = old_config.clone(); + config.validation_upgrade_delay = 100; + config.validation_upgrade_cooldown = 100; + assert!(old_config != config); + + assert_ok!(Configuration::set_validation_upgrade_delay(Origin::root(), 100)); + assert_ok!(Configuration::set_validation_upgrade_cooldown(Origin::root(), 100)); + assert_eq!(Configuration::config(), old_config); + assert_eq!(<Configuration as Store>::PendingConfigs::get(), vec![(2, config.clone())]); + + on_new_session(1); + + assert_eq!(Configuration::config(), old_config); + assert_eq!(<Configuration as Store>::PendingConfigs::get(), vec![(2, config.clone())]); + + on_new_session(2); + + assert_eq!(Configuration::config(), config); + assert_eq!(<Configuration as Store>::PendingConfigs::get(), vec![]); + }); +} + +#[test] +fn pending_next_session_but_we_upgrade_once_more() { + new_test_ext(Default::default()).execute_with(|| { + let initial_config = Configuration::config(); + let intermediate_config = + HostConfiguration { validation_upgrade_delay: 100, ..initial_config.clone() }; + let final_config = HostConfiguration { + validation_upgrade_delay: 100, + validation_upgrade_cooldown: 99, + ..initial_config.clone() + }; + + assert_ok!(Configuration::set_validation_upgrade_delay(Origin::root(), 100)); + assert_eq!(Configuration::config(), initial_config); + assert_eq!( + <Configuration as Store>::PendingConfigs::get(), + vec![(2, intermediate_config.clone())] + ); + + on_new_session(1); + + // We are still waiting until the pending configuration is applied and we add another + // update. + assert_ok!(Configuration::set_validation_upgrade_cooldown(Origin::root(), 99)); + + // This should result in yet another configiguration change scheduled. + assert_eq!(Configuration::config(), initial_config); + assert_eq!( + <Configuration as Store>::PendingConfigs::get(), + vec![(2, intermediate_config.clone()), (3, final_config.clone())] + ); + + on_new_session(2); + + assert_eq!(Configuration::config(), intermediate_config); + assert_eq!( + <Configuration as Store>::PendingConfigs::get(), + vec![(3, final_config.clone())] + ); + + on_new_session(3); + + assert_eq!(Configuration::config(), final_config); + assert_eq!(<Configuration as Store>::PendingConfigs::get(), vec![]); + }); +} + +#[test] +fn scheduled_session_config_update_while_next_session_pending() { + new_test_ext(Default::default()).execute_with(|| { + let initial_config = Configuration::config(); + let intermediate_config = + HostConfiguration { validation_upgrade_delay: 100, ..initial_config.clone() }; + let final_config = HostConfiguration { + validation_upgrade_delay: 100, + validation_upgrade_cooldown: 99, + code_retention_period: 98, + ..initial_config.clone() + }; + + assert_ok!(Configuration::set_validation_upgrade_delay(Origin::root(), 100)); + assert_eq!(Configuration::config(), initial_config); + assert_eq!( + <Configuration as Store>::PendingConfigs::get(), + vec![(2, intermediate_config.clone())] + ); + + on_new_session(1); + + // The second call should fall into the case where we already have a pending config + // update for the scheduled_session, but we want to update it once more. + assert_ok!(Configuration::set_validation_upgrade_cooldown(Origin::root(), 99)); + assert_ok!(Configuration::set_code_retention_period(Origin::root(), 98)); + + // This should result in yet another configiguration change scheduled. + assert_eq!(Configuration::config(), initial_config); + assert_eq!( + <Configuration as Store>::PendingConfigs::get(), + vec![(2, intermediate_config.clone()), (3, final_config.clone())] + ); + + on_new_session(2); + + assert_eq!(Configuration::config(), intermediate_config); + assert_eq!( + <Configuration as Store>::PendingConfigs::get(), + vec![(3, final_config.clone())] + ); + + on_new_session(3); + + assert_eq!(Configuration::config(), final_config); + assert_eq!(<Configuration as Store>::PendingConfigs::get(), vec![]); + }); +} + +#[test] +fn invariants() { + new_test_ext(Default::default()).execute_with(|| { + assert_err!( + Configuration::set_max_code_size(Origin::root(), MAX_CODE_SIZE + 1), + Error::<Test>::InvalidNewValue + ); + + assert_err!( + Configuration::set_max_pov_size(Origin::root(), MAX_POV_SIZE + 1), + Error::<Test>::InvalidNewValue + ); + + assert_err!( + Configuration::set_max_head_data_size(Origin::root(), MAX_HEAD_DATA_SIZE + 1), + Error::<Test>::InvalidNewValue + ); + + assert_err!( + Configuration::set_chain_availability_period(Origin::root(), 0), + Error::<Test>::InvalidNewValue + ); + assert_err!( + Configuration::set_thread_availability_period(Origin::root(), 0), + Error::<Test>::InvalidNewValue + ); + assert_err!( + Configuration::set_no_show_slots(Origin::root(), 0), + Error::<Test>::InvalidNewValue + ); + + <Configuration as Store>::ActiveConfig::put(HostConfiguration { + chain_availability_period: 10, + thread_availability_period: 8, + minimum_validation_upgrade_delay: 11, + ..Default::default() + }); + assert_err!( + Configuration::set_chain_availability_period(Origin::root(), 12), + Error::<Test>::InvalidNewValue + ); + assert_err!( + Configuration::set_thread_availability_period(Origin::root(), 12), + Error::<Test>::InvalidNewValue + ); + assert_err!( + Configuration::set_minimum_validation_upgrade_delay(Origin::root(), 9), + Error::<Test>::InvalidNewValue + ); + + assert_err!( + Configuration::set_validation_upgrade_delay(Origin::root(), 0), + Error::<Test>::InvalidNewValue + ); + }); +} + +#[test] +fn consistency_bypass_works() { + new_test_ext(Default::default()).execute_with(|| { + assert_err!( + Configuration::set_max_code_size(Origin::root(), MAX_CODE_SIZE + 1), + Error::<Test>::InvalidNewValue + ); + + assert_ok!(Configuration::set_bypass_consistency_check(Origin::root(), true)); + assert_ok!(Configuration::set_max_code_size(Origin::root(), MAX_CODE_SIZE + 1)); + + assert_eq!( + Configuration::config().max_code_size, + HostConfiguration::<u32>::default().max_code_size + ); + + on_new_session(1); + on_new_session(2); + + assert_eq!(Configuration::config().max_code_size, MAX_CODE_SIZE + 1); + }); +} + +#[test] +fn setting_pending_config_members() { + new_test_ext(Default::default()).execute_with(|| { + let new_config = HostConfiguration { + validation_upgrade_cooldown: 100, + validation_upgrade_delay: 10, + code_retention_period: 5, + max_code_size: 100_000, + max_pov_size: 1024, + max_head_data_size: 1_000, + parathread_cores: 2, + parathread_retries: 5, + group_rotation_frequency: 20, + chain_availability_period: 10, + thread_availability_period: 8, + scheduling_lookahead: 3, + max_validators_per_core: None, + max_validators: None, + dispute_period: 239, + dispute_post_conclusion_acceptance_period: 10, + dispute_max_spam_slots: 2, + dispute_conclusion_by_time_out_period: 512, + no_show_slots: 240, + n_delay_tranches: 241, + zeroth_delay_tranche_width: 242, + needed_approvals: 242, + relay_vrf_modulo_samples: 243, + max_upward_queue_count: 1337, + max_upward_queue_size: 228, + max_downward_message_size: 2048, + ump_service_total_weight: 20000, + max_upward_message_size: 448, + max_upward_message_num_per_candidate: 5, + hrmp_sender_deposit: 22, + hrmp_recipient_deposit: 4905, + hrmp_channel_max_capacity: 3921, + hrmp_channel_max_total_size: 7687, + hrmp_max_parachain_inbound_channels: 37, + hrmp_max_parathread_inbound_channels: 19, + hrmp_channel_max_message_size: 8192, + hrmp_max_parachain_outbound_channels: 10, + hrmp_max_parathread_outbound_channels: 20, + hrmp_max_message_num_per_candidate: 20, + ump_max_individual_weight: 909, + pvf_checking_enabled: true, + pvf_voting_ttl: 3, + minimum_validation_upgrade_delay: 20, + }; + + assert!(<Configuration as Store>::PendingConfig::get(shared::SESSION_DELAY).is_none()); + + Configuration::set_validation_upgrade_cooldown( + Origin::root(), + new_config.validation_upgrade_cooldown, + ) + .unwrap(); + Configuration::set_validation_upgrade_delay( + Origin::root(), + new_config.validation_upgrade_delay, + ) + .unwrap(); + Configuration::set_code_retention_period(Origin::root(), new_config.code_retention_period) + .unwrap(); + Configuration::set_max_code_size(Origin::root(), new_config.max_code_size).unwrap(); + Configuration::set_max_pov_size(Origin::root(), new_config.max_pov_size).unwrap(); + Configuration::set_max_head_data_size(Origin::root(), new_config.max_head_data_size) + .unwrap(); + Configuration::set_parathread_cores(Origin::root(), new_config.parathread_cores).unwrap(); + Configuration::set_parathread_retries(Origin::root(), new_config.parathread_retries) + .unwrap(); + Configuration::set_group_rotation_frequency( + Origin::root(), + new_config.group_rotation_frequency, + ) + .unwrap(); + // This comes out of order to satisfy the validity criteria for the chain and thread + // availability periods. + Configuration::set_minimum_validation_upgrade_delay( + Origin::root(), + new_config.minimum_validation_upgrade_delay, + ) + .unwrap(); + Configuration::set_chain_availability_period( + Origin::root(), + new_config.chain_availability_period, + ) + .unwrap(); + Configuration::set_thread_availability_period( + Origin::root(), + new_config.thread_availability_period, + ) + .unwrap(); + Configuration::set_scheduling_lookahead(Origin::root(), new_config.scheduling_lookahead) + .unwrap(); + Configuration::set_max_validators_per_core( + Origin::root(), + new_config.max_validators_per_core, + ) + .unwrap(); + Configuration::set_max_validators(Origin::root(), new_config.max_validators).unwrap(); + Configuration::set_dispute_period(Origin::root(), new_config.dispute_period).unwrap(); + Configuration::set_dispute_post_conclusion_acceptance_period( + Origin::root(), + new_config.dispute_post_conclusion_acceptance_period, + ) + .unwrap(); + Configuration::set_dispute_max_spam_slots( + Origin::root(), + new_config.dispute_max_spam_slots, + ) + .unwrap(); + Configuration::set_dispute_conclusion_by_time_out_period( + Origin::root(), + new_config.dispute_conclusion_by_time_out_period, + ) + .unwrap(); + Configuration::set_no_show_slots(Origin::root(), new_config.no_show_slots).unwrap(); + Configuration::set_n_delay_tranches(Origin::root(), new_config.n_delay_tranches).unwrap(); + Configuration::set_zeroth_delay_tranche_width( + Origin::root(), + new_config.zeroth_delay_tranche_width, + ) + .unwrap(); + Configuration::set_needed_approvals(Origin::root(), new_config.needed_approvals).unwrap(); + Configuration::set_relay_vrf_modulo_samples( + Origin::root(), + new_config.relay_vrf_modulo_samples, + ) + .unwrap(); + Configuration::set_max_upward_queue_count( + Origin::root(), + new_config.max_upward_queue_count, + ) + .unwrap(); + Configuration::set_max_upward_queue_size(Origin::root(), new_config.max_upward_queue_size) + .unwrap(); + Configuration::set_max_downward_message_size( + Origin::root(), + new_config.max_downward_message_size, + ) + .unwrap(); + Configuration::set_ump_service_total_weight( + Origin::root(), + new_config.ump_service_total_weight, + ) + .unwrap(); + Configuration::set_max_upward_message_size( + Origin::root(), + new_config.max_upward_message_size, + ) + .unwrap(); + Configuration::set_max_upward_message_num_per_candidate( + Origin::root(), + new_config.max_upward_message_num_per_candidate, + ) + .unwrap(); + Configuration::set_hrmp_sender_deposit(Origin::root(), new_config.hrmp_sender_deposit) + .unwrap(); + Configuration::set_hrmp_recipient_deposit( + Origin::root(), + new_config.hrmp_recipient_deposit, + ) + .unwrap(); + Configuration::set_hrmp_channel_max_capacity( + Origin::root(), + new_config.hrmp_channel_max_capacity, + ) + .unwrap(); + Configuration::set_hrmp_channel_max_total_size( + Origin::root(), + new_config.hrmp_channel_max_total_size, + ) + .unwrap(); + Configuration::set_hrmp_max_parachain_inbound_channels( + Origin::root(), + new_config.hrmp_max_parachain_inbound_channels, + ) + .unwrap(); + Configuration::set_hrmp_max_parathread_inbound_channels( + Origin::root(), + new_config.hrmp_max_parathread_inbound_channels, + ) + .unwrap(); + Configuration::set_hrmp_channel_max_message_size( + Origin::root(), + new_config.hrmp_channel_max_message_size, + ) + .unwrap(); + Configuration::set_hrmp_max_parachain_outbound_channels( + Origin::root(), + new_config.hrmp_max_parachain_outbound_channels, + ) + .unwrap(); + Configuration::set_hrmp_max_parathread_outbound_channels( + Origin::root(), + new_config.hrmp_max_parathread_outbound_channels, + ) + .unwrap(); + Configuration::set_hrmp_max_message_num_per_candidate( + Origin::root(), + new_config.hrmp_max_message_num_per_candidate, + ) + .unwrap(); + Configuration::set_ump_max_individual_weight( + Origin::root(), + new_config.ump_max_individual_weight, + ) + .unwrap(); + Configuration::set_pvf_checking_enabled(Origin::root(), new_config.pvf_checking_enabled) + .unwrap(); + Configuration::set_pvf_voting_ttl(Origin::root(), new_config.pvf_voting_ttl).unwrap(); + + assert_eq!( + <Configuration as Store>::PendingConfigs::get(), + vec![(shared::SESSION_DELAY, new_config)], + ); + }) +} + +#[test] +fn non_root_cannot_set_config() { + new_test_ext(Default::default()).execute_with(|| { + assert!(Configuration::set_validation_upgrade_delay(Origin::signed(1), 100).is_err()); + }); +} + +#[test] +fn verify_externally_accessible() { + // This test verifies that the value can be accessed through the well known keys and the + // host configuration decodes into the abridged version. + + use primitives::v1::{well_known_keys, AbridgedHostConfiguration}; + + new_test_ext(Default::default()).execute_with(|| { + let ground_truth = HostConfiguration::default(); + + // Make sure that the configuration is stored in the storage. + <Configuration as Store>::ActiveConfig::put(ground_truth.clone()); + + // Extract the active config via the well known key. + let raw_active_config = sp_io::storage::get(well_known_keys::ACTIVE_CONFIG) + .expect("config must be present in storage under ACTIVE_CONFIG"); + let abridged_config = AbridgedHostConfiguration::decode(&mut &raw_active_config[..]) + .expect("HostConfiguration must be decodable into AbridgedHostConfiguration"); + + assert_eq!( + abridged_config, + AbridgedHostConfiguration { + max_code_size: ground_truth.max_code_size, + max_head_data_size: ground_truth.max_head_data_size, + max_upward_queue_count: ground_truth.max_upward_queue_count, + max_upward_queue_size: ground_truth.max_upward_queue_size, + max_upward_message_size: ground_truth.max_upward_message_size, + max_upward_message_num_per_candidate: ground_truth + .max_upward_message_num_per_candidate, + hrmp_max_message_num_per_candidate: ground_truth.hrmp_max_message_num_per_candidate, + validation_upgrade_cooldown: ground_truth.validation_upgrade_cooldown, + validation_upgrade_delay: ground_truth.validation_upgrade_delay, + }, + ); + }); +} diff --git a/polkadot/runtime/parachains/src/disputes.rs b/polkadot/runtime/parachains/src/disputes.rs index 2b8662ea41fdcdaa8550e5f3bdbb98d80630f669..b723d4a4ab1310a5a476f96a0414dbedef298a28 100644 --- a/polkadot/runtime/parachains/src/disputes.rs +++ b/polkadot/runtime/parachains/src/disputes.rs @@ -35,6 +35,13 @@ use sp_runtime::{ }; use sp_std::{cmp::Ordering, prelude::*}; +#[cfg(test)] +#[allow(unused_imports)] +pub(crate) use self::tests::run_to_block; + +#[cfg(test)] +mod tests; + #[cfg(feature = "runtime-benchmarks")] mod benchmarking; @@ -1312,2477 +1319,3 @@ fn check_signature( Err(()) } } - -#[cfg(test)] -#[allow(unused_imports)] -pub(crate) use self::tests::run_to_block; - -#[cfg(test)] -mod tests { - use super::*; - use crate::{ - configuration::HostConfiguration, - disputes::DisputesHandler, - mock::{ - new_test_ext, AccountId, AllPalletsWithSystem, Initializer, MockGenesisConfig, System, - Test, PUNISH_VALIDATORS_AGAINST, PUNISH_VALIDATORS_FOR, PUNISH_VALIDATORS_INCONCLUSIVE, - REWARD_VALIDATORS, - }, - }; - use frame_support::{ - assert_err, assert_noop, assert_ok, - traits::{OnFinalize, OnInitialize}, - }; - use primitives::v1::BlockNumber; - use sp_core::{crypto::CryptoType, Pair}; - - /// Filtering updates the spam slots, as such update them. - fn update_spam_slots(stmts: MultiDisputeStatementSet) -> CheckedMultiDisputeStatementSet { - let config = <configuration::Pallet<Test>>::config(); - let max_spam_slots = config.dispute_max_spam_slots; - let post_conclusion_acceptance_period = config.dispute_post_conclusion_acceptance_period; - - stmts - .into_iter() - .filter_map(|set| { - // updates spam slots implicitly - let filter = Pallet::<Test>::filter_dispute_data( - &set, - post_conclusion_acceptance_period, - max_spam_slots, - VerifyDisputeSignatures::Skip, - ); - filter.filter_statement_set(set) - }) - .collect::<Vec<_>>() - } - - // All arguments for `initializer::on_new_session` - type NewSession<'a> = ( - bool, - SessionIndex, - Vec<(&'a AccountId, ValidatorId)>, - Option<Vec<(&'a AccountId, ValidatorId)>>, - ); - - // Run to specific block, while calling disputes pallet hooks manually, because disputes is not - // integrated in initializer yet. - pub(crate) fn run_to_block<'a>( - to: BlockNumber, - new_session: impl Fn(BlockNumber) -> Option<NewSession<'a>>, - ) { - while System::block_number() < to { - let b = System::block_number(); - if b != 0 { - // circumvent requirement to have bitfields and headers in block for testing purposes - crate::paras_inherent::Included::<Test>::set(Some(())); - - AllPalletsWithSystem::on_finalize(b); - System::finalize(); - } - - System::reset_events(); - System::initialize(&(b + 1), &Default::default(), &Default::default()); - AllPalletsWithSystem::on_initialize(b + 1); - - if let Some(new_session) = new_session(b + 1) { - Initializer::test_trigger_on_new_session( - new_session.0, - new_session.1, - new_session.2.into_iter(), - new_session.3.map(|q| q.into_iter()), - ); - } - } - } - - #[test] - fn test_contains_duplicates_in_sorted_iter() { - // We here use the implicit ascending sorting and builtin equality of integers - let v = vec![1, 2, 3, 5, 5, 8]; - assert_eq!(true, contains_duplicates_in_sorted_iter(&v, |a, b| a == b)); - - let v = vec![1, 2, 3, 4]; - assert_eq!(false, contains_duplicates_in_sorted_iter(&v, |a, b| a == b)); - } - - #[test] - fn test_dispute_state_flag_from_state() { - assert_eq!( - DisputeStateFlags::from_state(&DisputeState { - validators_for: bitvec![BitOrderLsb0, u8; 0, 0, 0, 0, 0, 0, 0, 0], - validators_against: bitvec![BitOrderLsb0, u8; 0, 0, 0, 0, 0, 0, 0, 0], - start: 0, - concluded_at: None, - }), - DisputeStateFlags::default(), - ); - - assert_eq!( - DisputeStateFlags::from_state(&DisputeState { - validators_for: bitvec![BitOrderLsb0, u8; 1, 1, 1, 1, 1, 0, 0], - validators_against: bitvec![BitOrderLsb0, u8; 0, 0, 0, 0, 0, 0, 0], - start: 0, - concluded_at: None, - }), - DisputeStateFlags::FOR_SUPERMAJORITY | DisputeStateFlags::CONFIRMED, - ); - - assert_eq!( - DisputeStateFlags::from_state(&DisputeState { - validators_for: bitvec![BitOrderLsb0, u8; 0, 0, 0, 0, 0, 0, 0], - validators_against: bitvec![BitOrderLsb0, u8; 1, 1, 1, 1, 1, 0, 0], - start: 0, - concluded_at: None, - }), - DisputeStateFlags::AGAINST_SUPERMAJORITY | DisputeStateFlags::CONFIRMED, - ); - } - - #[test] - fn test_import_new_participant_spam_inc() { - let mut importer = DisputeStateImporter::new( - DisputeState { - validators_for: bitvec![BitOrderLsb0, u8; 1, 0, 0, 0, 0, 0, 0, 0], - validators_against: bitvec![BitOrderLsb0, u8; 0, 0, 0, 0, 0, 0, 0, 0], - start: 0, - concluded_at: None, - }, - 0, - ); - - assert_err!( - importer.import(ValidatorIndex(9), true), - VoteImportError::ValidatorIndexOutOfBounds, - ); - - assert_err!(importer.import(ValidatorIndex(0), true), VoteImportError::DuplicateStatement); - assert_ok!(importer.import(ValidatorIndex(0), false)); - - assert_ok!(importer.import(ValidatorIndex(2), true)); - assert_err!(importer.import(ValidatorIndex(2), true), VoteImportError::DuplicateStatement); - - assert_ok!(importer.import(ValidatorIndex(2), false)); - assert_err!(importer.import(ValidatorIndex(2), false), VoteImportError::DuplicateStatement); - - let summary = importer.finish(); - assert_eq!(summary.new_flags, DisputeStateFlags::default()); - assert_eq!( - summary.state, - DisputeState { - validators_for: bitvec![BitOrderLsb0, u8; 1, 0, 1, 0, 0, 0, 0, 0], - validators_against: bitvec![BitOrderLsb0, u8; 1, 0, 1, 0, 0, 0, 0, 0], - start: 0, - concluded_at: None, - }, - ); - assert_eq!(summary.spam_slot_changes, vec![(ValidatorIndex(2), SpamSlotChange::Inc)]); - assert!(summary.slash_for.is_empty()); - assert!(summary.slash_against.is_empty()); - assert_eq!(summary.new_participants, bitvec![BitOrderLsb0, u8; 0, 0, 1, 0, 0, 0, 0, 0]); - } - - #[test] - fn test_import_prev_participant_spam_dec_confirmed() { - let mut importer = DisputeStateImporter::new( - DisputeState { - validators_for: bitvec![BitOrderLsb0, u8; 1, 0, 0, 0, 0, 0, 0, 0], - validators_against: bitvec![BitOrderLsb0, u8; 0, 1, 0, 0, 0, 0, 0, 0], - start: 0, - concluded_at: None, - }, - 0, - ); - - assert_ok!(importer.import(ValidatorIndex(2), true)); - - let summary = importer.finish(); - assert_eq!( - summary.state, - DisputeState { - validators_for: bitvec![BitOrderLsb0, u8; 1, 0, 1, 0, 0, 0, 0, 0], - validators_against: bitvec![BitOrderLsb0, u8; 0, 1, 0, 0, 0, 0, 0, 0], - start: 0, - concluded_at: None, - }, - ); - assert_eq!( - summary.spam_slot_changes, - vec![ - (ValidatorIndex(0), SpamSlotChange::Dec), - (ValidatorIndex(1), SpamSlotChange::Dec), - ], - ); - assert!(summary.slash_for.is_empty()); - assert!(summary.slash_against.is_empty()); - assert_eq!(summary.new_participants, bitvec![BitOrderLsb0, u8; 0, 0, 1, 0, 0, 0, 0, 0]); - assert_eq!(summary.new_flags, DisputeStateFlags::CONFIRMED); - } - - #[test] - fn test_import_prev_participant_spam_dec_confirmed_slash_for() { - let mut importer = DisputeStateImporter::new( - DisputeState { - validators_for: bitvec![BitOrderLsb0, u8; 1, 0, 0, 0, 0, 0, 0, 0], - validators_against: bitvec![BitOrderLsb0, u8; 0, 1, 0, 0, 0, 0, 0, 0], - start: 0, - concluded_at: None, - }, - 0, - ); - - assert_ok!(importer.import(ValidatorIndex(2), true)); - assert_ok!(importer.import(ValidatorIndex(2), false)); - assert_ok!(importer.import(ValidatorIndex(3), false)); - assert_ok!(importer.import(ValidatorIndex(4), false)); - assert_ok!(importer.import(ValidatorIndex(5), false)); - assert_ok!(importer.import(ValidatorIndex(6), false)); - - let summary = importer.finish(); - assert_eq!( - summary.state, - DisputeState { - validators_for: bitvec![BitOrderLsb0, u8; 1, 0, 1, 0, 0, 0, 0, 0], - validators_against: bitvec![BitOrderLsb0, u8; 0, 1, 1, 1, 1, 1, 1, 0], - start: 0, - concluded_at: Some(0), - }, - ); - assert_eq!( - summary.spam_slot_changes, - vec![ - (ValidatorIndex(0), SpamSlotChange::Dec), - (ValidatorIndex(1), SpamSlotChange::Dec), - ], - ); - assert_eq!(summary.slash_for, vec![ValidatorIndex(0), ValidatorIndex(2)]); - assert!(summary.slash_against.is_empty()); - assert_eq!(summary.new_participants, bitvec![BitOrderLsb0, u8; 0, 0, 1, 1, 1, 1, 1, 0]); - assert_eq!( - summary.new_flags, - DisputeStateFlags::CONFIRMED | DisputeStateFlags::AGAINST_SUPERMAJORITY, - ); - } - - #[test] - fn test_import_slash_against() { - let mut importer = DisputeStateImporter::new( - DisputeState { - validators_for: bitvec![BitOrderLsb0, u8; 1, 0, 1, 0, 0, 0, 0, 0], - validators_against: bitvec![BitOrderLsb0, u8; 0, 1, 0, 0, 0, 0, 0, 0], - start: 0, - concluded_at: None, - }, - 0, - ); - - assert_ok!(importer.import(ValidatorIndex(3), true)); - assert_ok!(importer.import(ValidatorIndex(4), true)); - assert_ok!(importer.import(ValidatorIndex(5), false)); - assert_ok!(importer.import(ValidatorIndex(6), true)); - assert_ok!(importer.import(ValidatorIndex(7), true)); - - let summary = importer.finish(); - assert_eq!( - summary.state, - DisputeState { - validators_for: bitvec![BitOrderLsb0, u8; 1, 0, 1, 1, 1, 0, 1, 1], - validators_against: bitvec![BitOrderLsb0, u8; 0, 1, 0, 0, 0, 1, 0, 0], - start: 0, - concluded_at: Some(0), - }, - ); - assert!(summary.spam_slot_changes.is_empty()); - assert!(summary.slash_for.is_empty()); - assert_eq!(summary.slash_against, vec![ValidatorIndex(1), ValidatorIndex(5)]); - assert_eq!(summary.new_participants, bitvec![BitOrderLsb0, u8; 0, 0, 0, 1, 1, 1, 1, 1]); - assert_eq!(summary.new_flags, DisputeStateFlags::FOR_SUPERMAJORITY); - } - - // Test that punish_inconclusive is correctly called. - #[test] - fn test_initializer_initialize() { - let dispute_conclusion_by_time_out_period = 3; - let start = 10; - - let mock_genesis_config = MockGenesisConfig { - configuration: crate::configuration::GenesisConfig { - config: HostConfiguration { - dispute_conclusion_by_time_out_period, - ..Default::default() - }, - ..Default::default() - }, - ..Default::default() - }; - - new_test_ext(mock_genesis_config).execute_with(|| { - // We need 6 validators for the byzantine threshold to be 2 - let v0 = <ValidatorId as CryptoType>::Pair::generate().0; - let v1 = <ValidatorId as CryptoType>::Pair::generate().0; - let v2 = <ValidatorId as CryptoType>::Pair::generate().0; - let v3 = <ValidatorId as CryptoType>::Pair::generate().0; - let v4 = <ValidatorId as CryptoType>::Pair::generate().0; - let v5 = <ValidatorId as CryptoType>::Pair::generate().0; - let v6 = <ValidatorId as CryptoType>::Pair::generate().0; - - run_to_block(start, |b| { - // a new session at each block - Some(( - true, - b, - vec![ - (&0, v0.public()), - (&1, v1.public()), - (&2, v2.public()), - (&3, v3.public()), - (&4, v4.public()), - (&5, v5.public()), - (&6, v6.public()), - ], - Some(vec![ - (&0, v0.public()), - (&1, v1.public()), - (&2, v2.public()), - (&3, v3.public()), - (&4, v4.public()), - (&5, v5.public()), - (&6, v6.public()), - ]), - )) - }); - - let candidate_hash = CandidateHash(sp_core::H256::repeat_byte(1)); - - // v0 votes for 3, v6 against. - let stmts = vec![DisputeStatementSet { - candidate_hash: candidate_hash.clone(), - session: start - 1, - statements: vec![ - ( - DisputeStatement::Valid(ValidDisputeStatementKind::Explicit), - ValidatorIndex(0), - v0.sign( - &ExplicitDisputeStatement { - valid: true, - candidate_hash: candidate_hash.clone(), - session: start - 1, - } - .signing_payload(), - ), - ), - ( - DisputeStatement::Invalid(InvalidDisputeStatementKind::Explicit), - ValidatorIndex(6), - v2.sign( - &ExplicitDisputeStatement { - valid: false, - candidate_hash: candidate_hash.clone(), - session: start - 1, - } - .signing_payload(), - ), - ), - ], - }]; - - let stmts = update_spam_slots(stmts); - assert_eq!(SpamSlots::<Test>::get(start - 1), Some(vec![1, 0, 0, 0, 0, 0, 1])); - - assert_ok!( - Pallet::<Test>::process_checked_multi_dispute_data(stmts), - vec![(9, candidate_hash.clone())], - ); - - // Run to timeout period - run_to_block(start + dispute_conclusion_by_time_out_period, |_| None); - assert_eq!(SpamSlots::<Test>::get(start - 1), Some(vec![1, 0, 0, 0, 0, 0, 1])); - - // Run to timeout + 1 in order to executive on_finalize(timeout) - run_to_block(start + dispute_conclusion_by_time_out_period + 1, |_| None); - assert_eq!(SpamSlots::<Test>::get(start - 1), Some(vec![0, 0, 0, 0, 0, 0, 0])); - assert_eq!( - PUNISH_VALIDATORS_INCONCLUSIVE.with(|r| r.borrow()[0].clone()), - (9, vec![ValidatorIndex(0), ValidatorIndex(6)]), - ); - }); - } - - // Test pruning works - #[test] - fn test_initializer_on_new_session() { - let dispute_period = 3; - - let mock_genesis_config = MockGenesisConfig { - configuration: crate::configuration::GenesisConfig { - config: HostConfiguration { dispute_period, ..Default::default() }, - ..Default::default() - }, - ..Default::default() - }; - - new_test_ext(mock_genesis_config).execute_with(|| { - let v0 = <ValidatorId as CryptoType>::Pair::generate().0; - - let candidate_hash = CandidateHash(sp_core::H256::repeat_byte(1)); - Pallet::<Test>::note_included(0, candidate_hash.clone(), 0); - Pallet::<Test>::note_included(1, candidate_hash.clone(), 1); - Pallet::<Test>::note_included(2, candidate_hash.clone(), 2); - Pallet::<Test>::note_included(3, candidate_hash.clone(), 3); - Pallet::<Test>::note_included(4, candidate_hash.clone(), 4); - Pallet::<Test>::note_included(5, candidate_hash.clone(), 5); - Pallet::<Test>::note_included(6, candidate_hash.clone(), 5); - - run_to_block(7, |b| { - // a new session at each block - Some((true, b, vec![(&0, v0.public())], Some(vec![(&0, v0.public())]))) - }); - - // current session is 7, - // we keep for dispute_period + 1 session and we remove in on_finalize - // thus we keep info for session 3, 4, 5, 6, 7. - assert_eq!(Included::<Test>::iter_prefix(0).count(), 0); - assert_eq!(Included::<Test>::iter_prefix(1).count(), 0); - assert_eq!(Included::<Test>::iter_prefix(2).count(), 0); - assert_eq!(Included::<Test>::iter_prefix(3).count(), 1); - assert_eq!(Included::<Test>::iter_prefix(4).count(), 1); - assert_eq!(Included::<Test>::iter_prefix(5).count(), 1); - assert_eq!(Included::<Test>::iter_prefix(6).count(), 1); - }); - } - - #[test] - fn test_provide_data_duplicate_error() { - new_test_ext(Default::default()).execute_with(|| { - let candidate_hash_1 = CandidateHash(sp_core::H256::repeat_byte(1)); - let candidate_hash_2 = CandidateHash(sp_core::H256::repeat_byte(2)); - - let mut stmts = vec![ - DisputeStatementSet { - candidate_hash: candidate_hash_2, - session: 2, - statements: vec![], - }, - DisputeStatementSet { - candidate_hash: candidate_hash_1, - session: 1, - statements: vec![], - }, - DisputeStatementSet { - candidate_hash: candidate_hash_2, - session: 2, - statements: vec![], - }, - ]; - - assert!(Pallet::<Test>::deduplicate_and_sort_dispute_data(&mut stmts).is_err()); - assert_eq!(stmts.len(), 2); - }) - } - - #[test] - fn test_provide_multi_dispute_is_providing() { - new_test_ext(Default::default()).execute_with(|| { - let v0 = <ValidatorId as CryptoType>::Pair::generate().0; - let v1 = <ValidatorId as CryptoType>::Pair::generate().0; - - run_to_block(3, |b| { - // a new session at each block - if b == 1 { - Some(( - true, - b, - vec![(&0, v0.public()), (&1, v1.public())], - Some(vec![(&0, v0.public()), (&1, v1.public())]), - )) - } else { - Some((true, b, vec![(&1, v1.public())], Some(vec![(&1, v1.public())]))) - } - }); - - let candidate_hash = CandidateHash(sp_core::H256::repeat_byte(1)); - let stmts = vec![DisputeStatementSet { - candidate_hash: candidate_hash.clone(), - session: 1, - statements: vec![ - ( - DisputeStatement::Valid(ValidDisputeStatementKind::Explicit), - ValidatorIndex(0), - v0.sign( - &ExplicitDisputeStatement { - valid: true, - candidate_hash: candidate_hash.clone(), - session: 1, - } - .signing_payload(), - ), - ), - ( - DisputeStatement::Invalid(InvalidDisputeStatementKind::Explicit), - ValidatorIndex(1), - v1.sign( - &ExplicitDisputeStatement { - valid: false, - candidate_hash: candidate_hash.clone(), - session: 1, - } - .signing_payload(), - ), - ), - ], - }]; - - assert_ok!( - Pallet::<Test>::process_checked_multi_dispute_data( - stmts - .into_iter() - .map(CheckedDisputeStatementSet::unchecked_from_unchecked) - .collect() - ), - vec![(1, candidate_hash.clone())], - ); - }) - } - - #[test] - fn test_freeze_on_note_included() { - new_test_ext(Default::default()).execute_with(|| { - let v0 = <ValidatorId as CryptoType>::Pair::generate().0; - let v1 = <ValidatorId as CryptoType>::Pair::generate().0; - - run_to_block(6, |b| { - // a new session at each block - Some(( - true, - b, - vec![(&0, v0.public()), (&1, v1.public())], - Some(vec![(&0, v0.public()), (&1, v1.public())]), - )) - }); - - let candidate_hash = CandidateHash(sp_core::H256::repeat_byte(1)); - - // v0 votes for 3 - let stmts = vec![DisputeStatementSet { - candidate_hash: candidate_hash.clone(), - session: 3, - statements: vec![ - ( - DisputeStatement::Invalid(InvalidDisputeStatementKind::Explicit), - ValidatorIndex(0), - v0.sign( - &ExplicitDisputeStatement { - valid: false, - candidate_hash: candidate_hash.clone(), - session: 3, - } - .signing_payload(), - ), - ), - ( - DisputeStatement::Invalid(InvalidDisputeStatementKind::Explicit), - ValidatorIndex(1), - v1.sign( - &ExplicitDisputeStatement { - valid: false, - candidate_hash: candidate_hash.clone(), - session: 3, - } - .signing_payload(), - ), - ), - ( - DisputeStatement::Valid(ValidDisputeStatementKind::Explicit), - ValidatorIndex(1), - v1.sign( - &ExplicitDisputeStatement { - valid: true, - candidate_hash: candidate_hash.clone(), - session: 3, - } - .signing_payload(), - ), - ), - ], - }]; - assert!(Pallet::<Test>::process_checked_multi_dispute_data( - stmts - .into_iter() - .map(CheckedDisputeStatementSet::unchecked_from_unchecked) - .collect() - ) - .is_ok()); - - Pallet::<Test>::note_included(3, candidate_hash.clone(), 3); - assert_eq!(Frozen::<Test>::get(), Some(2)); - }); - } - - #[test] - fn test_freeze_provided_against_supermajority_for_included() { - new_test_ext(Default::default()).execute_with(|| { - let v0 = <ValidatorId as CryptoType>::Pair::generate().0; - let v1 = <ValidatorId as CryptoType>::Pair::generate().0; - - run_to_block(6, |b| { - // a new session at each block - Some(( - true, - b, - vec![(&0, v0.public()), (&1, v1.public())], - Some(vec![(&0, v0.public()), (&1, v1.public())]), - )) - }); - - let candidate_hash = CandidateHash(sp_core::H256::repeat_byte(1)); - - // v0 votes for 3 - let stmts = vec![DisputeStatementSet { - candidate_hash: candidate_hash.clone(), - session: 3, - statements: vec![ - ( - DisputeStatement::Invalid(InvalidDisputeStatementKind::Explicit), - ValidatorIndex(0), - v0.sign( - &ExplicitDisputeStatement { - valid: false, - candidate_hash: candidate_hash.clone(), - session: 3, - } - .signing_payload(), - ), - ), - ( - DisputeStatement::Invalid(InvalidDisputeStatementKind::Explicit), - ValidatorIndex(1), - v1.sign( - &ExplicitDisputeStatement { - valid: false, - candidate_hash: candidate_hash.clone(), - session: 3, - } - .signing_payload(), - ), - ), - ( - DisputeStatement::Valid(ValidDisputeStatementKind::Explicit), - ValidatorIndex(1), - v1.sign( - &ExplicitDisputeStatement { - valid: true, - candidate_hash: candidate_hash.clone(), - session: 3, - } - .signing_payload(), - ), - ), - ], - }]; - - Pallet::<Test>::note_included(3, candidate_hash.clone(), 3); - assert!(Pallet::<Test>::process_checked_multi_dispute_data( - stmts - .into_iter() - .map(CheckedDisputeStatementSet::unchecked_from_unchecked) - .collect() - ) - .is_ok()); - assert_eq!(Frozen::<Test>::get(), Some(2)); - }); - } - - // tests for: - // * provide_multi_dispute: with success scenario - // * disputes: correctness of datas - // * could_be_invalid: correctness of datas - // * note_included: decrement spam correctly - // * spam slots: correctly incremented and decremented - // * ensure rewards and punishment are correctly called. - #[test] - fn test_provide_multi_dispute_success_and_other() { - new_test_ext(Default::default()).execute_with(|| { - // 7 validators needed for byzantine threshold of 2. - let v0 = <ValidatorId as CryptoType>::Pair::generate().0; - let v1 = <ValidatorId as CryptoType>::Pair::generate().0; - let v2 = <ValidatorId as CryptoType>::Pair::generate().0; - let v3 = <ValidatorId as CryptoType>::Pair::generate().0; - let v4 = <ValidatorId as CryptoType>::Pair::generate().0; - let v5 = <ValidatorId as CryptoType>::Pair::generate().0; - let v6 = <ValidatorId as CryptoType>::Pair::generate().0; - - // v0 -> 0 - // v1 -> 3 - // v2 -> 6 - // v3 -> 5 - // v4 -> 1 - // v5 -> 4 - // v6 -> 2 - - run_to_block(6, |b| { - // a new session at each block - Some(( - true, - b, - vec![ - (&0, v0.public()), - (&1, v1.public()), - (&2, v2.public()), - (&3, v3.public()), - (&4, v4.public()), - (&5, v5.public()), - (&6, v6.public()), - ], - Some(vec![ - (&0, v0.public()), - (&1, v1.public()), - (&2, v2.public()), - (&3, v3.public()), - (&4, v4.public()), - (&5, v5.public()), - (&6, v6.public()), - ]), - )) - }); - - let candidate_hash = CandidateHash(sp_core::H256::repeat_byte(1)); - - // v0 votes for 3, v6 votes against - let stmts = vec![DisputeStatementSet { - candidate_hash: candidate_hash.clone(), - session: 3, - statements: vec![ - ( - DisputeStatement::Valid(ValidDisputeStatementKind::Explicit), - ValidatorIndex(0), - v0.sign( - &ExplicitDisputeStatement { - valid: true, - candidate_hash: candidate_hash.clone(), - session: 3, - } - .signing_payload(), - ), - ), - ( - DisputeStatement::Invalid(InvalidDisputeStatementKind::Explicit), - ValidatorIndex(2), - v6.sign( - &ExplicitDisputeStatement { - valid: false, - candidate_hash: candidate_hash.clone(), - session: 3, - } - .signing_payload(), - ), - ), - ], - }]; - - let stmts = update_spam_slots(stmts); - assert_eq!(SpamSlots::<Test>::get(3), Some(vec![1, 0, 1, 0, 0, 0, 0])); - - assert_ok!( - Pallet::<Test>::process_checked_multi_dispute_data(stmts), - vec![(3, candidate_hash.clone())], - ); - - // v1 votes for 4 and for 3, v6 votes against 4. - let stmts = vec![ - DisputeStatementSet { - candidate_hash: candidate_hash.clone(), - session: 4, - statements: vec![ - ( - DisputeStatement::Valid(ValidDisputeStatementKind::Explicit), - ValidatorIndex(3), - v1.sign( - &ExplicitDisputeStatement { - valid: true, - candidate_hash: candidate_hash.clone(), - session: 4, - } - .signing_payload(), - ), - ), - ( - DisputeStatement::Invalid(InvalidDisputeStatementKind::Explicit), - ValidatorIndex(2), - v6.sign( - &ExplicitDisputeStatement { - valid: false, - candidate_hash: candidate_hash.clone(), - session: 4, - } - .signing_payload(), - ), - ), - ], - }, - DisputeStatementSet { - candidate_hash: candidate_hash.clone(), - session: 3, - statements: vec![( - DisputeStatement::Valid(ValidDisputeStatementKind::Explicit), - ValidatorIndex(3), - v1.sign( - &ExplicitDisputeStatement { - valid: true, - candidate_hash: candidate_hash.clone(), - session: 3, - } - .signing_payload(), - ), - )], - }, - ]; - - let stmts = update_spam_slots(stmts); - - assert_ok!( - Pallet::<Test>::process_checked_multi_dispute_data(stmts), - vec![(4, candidate_hash.clone())], - ); - assert_eq!(SpamSlots::<Test>::get(3), Some(vec![0, 0, 0, 0, 0, 0, 0])); // Confirmed as no longer spam - assert_eq!(SpamSlots::<Test>::get(4), Some(vec![0, 0, 1, 1, 0, 0, 0])); - - // v3 votes against 3 and for 5, v6 votes against 5. - let stmts = vec![ - DisputeStatementSet { - candidate_hash: candidate_hash.clone(), - session: 3, - statements: vec![( - DisputeStatement::Invalid(InvalidDisputeStatementKind::Explicit), - ValidatorIndex(5), - v3.sign( - &ExplicitDisputeStatement { - valid: false, - candidate_hash: candidate_hash.clone(), - session: 3, - } - .signing_payload(), - ), - )], - }, - DisputeStatementSet { - candidate_hash: candidate_hash.clone(), - session: 5, - statements: vec![ - ( - DisputeStatement::Valid(ValidDisputeStatementKind::Explicit), - ValidatorIndex(5), - v3.sign( - &ExplicitDisputeStatement { - valid: true, - candidate_hash: candidate_hash.clone(), - session: 5, - } - .signing_payload(), - ), - ), - ( - DisputeStatement::Invalid(InvalidDisputeStatementKind::Explicit), - ValidatorIndex(2), - v6.sign( - &ExplicitDisputeStatement { - valid: false, - candidate_hash: candidate_hash.clone(), - session: 5, - } - .signing_payload(), - ), - ), - ], - }, - ]; - - let stmts = update_spam_slots(stmts); - assert_ok!( - Pallet::<Test>::process_checked_multi_dispute_data(stmts), - vec![(5, candidate_hash.clone())], - ); - assert_eq!(SpamSlots::<Test>::get(3), Some(vec![0, 0, 0, 0, 0, 0, 0])); - assert_eq!(SpamSlots::<Test>::get(4), Some(vec![0, 0, 1, 1, 0, 0, 0])); - assert_eq!(SpamSlots::<Test>::get(5), Some(vec![0, 0, 1, 0, 0, 1, 0])); - - // v2 votes for 3 and against 5 - let stmts = vec![ - DisputeStatementSet { - candidate_hash: candidate_hash.clone(), - session: 3, - statements: vec![( - DisputeStatement::Valid(ValidDisputeStatementKind::Explicit), - ValidatorIndex(6), - v2.sign( - &ExplicitDisputeStatement { - valid: true, - candidate_hash: candidate_hash.clone(), - session: 3, - } - .signing_payload(), - ), - )], - }, - DisputeStatementSet { - candidate_hash: candidate_hash.clone(), - session: 5, - statements: vec![( - DisputeStatement::Invalid(InvalidDisputeStatementKind::Explicit), - ValidatorIndex(6), - v2.sign( - &ExplicitDisputeStatement { - valid: false, - candidate_hash: candidate_hash.clone(), - session: 5, - } - .signing_payload(), - ), - )], - }, - ]; - let stmts = update_spam_slots(stmts); - assert_ok!(Pallet::<Test>::process_checked_multi_dispute_data(stmts), vec![]); - assert_eq!(SpamSlots::<Test>::get(3), Some(vec![0, 0, 0, 0, 0, 0, 0])); - assert_eq!(SpamSlots::<Test>::get(4), Some(vec![0, 0, 1, 1, 0, 0, 0])); - assert_eq!(SpamSlots::<Test>::get(5), Some(vec![0, 0, 0, 0, 0, 0, 0])); - - let stmts = vec![ - // 0, 4, and 5 vote against 5 - DisputeStatementSet { - candidate_hash: candidate_hash.clone(), - session: 5, - statements: vec![ - ( - DisputeStatement::Invalid(InvalidDisputeStatementKind::Explicit), - ValidatorIndex(0), - v0.sign( - &ExplicitDisputeStatement { - valid: false, - candidate_hash: candidate_hash.clone(), - session: 5, - } - .signing_payload(), - ), - ), - ( - DisputeStatement::Invalid(InvalidDisputeStatementKind::Explicit), - ValidatorIndex(1), - v4.sign( - &ExplicitDisputeStatement { - valid: false, - candidate_hash: candidate_hash.clone(), - session: 5, - } - .signing_payload(), - ), - ), - ( - DisputeStatement::Invalid(InvalidDisputeStatementKind::Explicit), - ValidatorIndex(4), - v5.sign( - &ExplicitDisputeStatement { - valid: false, - candidate_hash: candidate_hash.clone(), - session: 5, - } - .signing_payload(), - ), - ), - ], - }, - // 4 and 5 vote for 3 - DisputeStatementSet { - candidate_hash: candidate_hash.clone(), - session: 3, - statements: vec![ - ( - DisputeStatement::Valid(ValidDisputeStatementKind::Explicit), - ValidatorIndex(1), - v4.sign( - &ExplicitDisputeStatement { - valid: true, - candidate_hash: candidate_hash.clone(), - session: 3, - } - .signing_payload(), - ), - ), - ( - DisputeStatement::Valid(ValidDisputeStatementKind::Explicit), - ValidatorIndex(4), - v5.sign( - &ExplicitDisputeStatement { - valid: true, - candidate_hash: candidate_hash.clone(), - session: 3, - } - .signing_payload(), - ), - ), - ], - }, - ]; - let stmts = update_spam_slots(stmts); - assert_ok!(Pallet::<Test>::process_checked_multi_dispute_data(stmts), vec![]); - - assert_eq!( - Pallet::<Test>::disputes(), - vec![ - ( - 5, - candidate_hash.clone(), - DisputeState { - validators_for: bitvec![BitOrderLsb0, u8; 0, 0, 0, 0, 0, 1, 0], - validators_against: bitvec![BitOrderLsb0, u8; 1, 1, 1, 0, 1, 0, 1], - start: 6, - concluded_at: Some(6), // 5 vote against - } - ), - ( - 3, - candidate_hash.clone(), - DisputeState { - validators_for: bitvec![BitOrderLsb0, u8; 1, 1, 0, 1, 1, 0, 1], - validators_against: bitvec![BitOrderLsb0, u8; 0, 0, 1, 0, 0, 1, 0], - start: 6, - concluded_at: Some(6), // 5 vote for - } - ), - ( - 4, - candidate_hash.clone(), - DisputeState { - validators_for: bitvec![BitOrderLsb0, u8; 0, 0, 0, 1, 0, 0, 0], - validators_against: bitvec![BitOrderLsb0, u8; 0, 0, 1, 0, 0, 0, 0], - start: 6, - concluded_at: None, - } - ), - ] - ); - - assert!(!Pallet::<Test>::concluded_invalid(3, candidate_hash.clone())); - assert!(!Pallet::<Test>::concluded_invalid(4, candidate_hash.clone())); - assert!(Pallet::<Test>::concluded_invalid(5, candidate_hash.clone())); - - // Ensure inclusion removes spam slots - assert_eq!(SpamSlots::<Test>::get(4), Some(vec![0, 0, 1, 1, 0, 0, 0])); - Pallet::<Test>::note_included(4, candidate_hash.clone(), 4); - assert_eq!(SpamSlots::<Test>::get(4), Some(vec![0, 0, 0, 0, 0, 0, 0])); - - // Ensure the `reward_validator` function was correctly called - assert_eq!( - REWARD_VALIDATORS.with(|r| r.borrow().clone()), - vec![ - (3, vec![ValidatorIndex(0), ValidatorIndex(2)]), - (4, vec![ValidatorIndex(2), ValidatorIndex(3)]), - (3, vec![ValidatorIndex(3)]), - (3, vec![ValidatorIndex(5)]), - (5, vec![ValidatorIndex(2), ValidatorIndex(5)]), - (3, vec![ValidatorIndex(6)]), - (5, vec![ValidatorIndex(6)]), - (5, vec![ValidatorIndex(0), ValidatorIndex(1), ValidatorIndex(4)]), - (3, vec![ValidatorIndex(1), ValidatorIndex(4)]), - ], - ); - - // Ensure punishment against is called - assert_eq!( - PUNISH_VALIDATORS_AGAINST.with(|r| r.borrow().clone()), - vec![ - (3, vec![]), - (4, vec![]), - (3, vec![]), - (3, vec![]), - (5, vec![]), - (3, vec![]), - (5, vec![]), - (5, vec![]), - (3, vec![ValidatorIndex(2), ValidatorIndex(5)]), - ], - ); - - // Ensure punishment for is called - assert_eq!( - PUNISH_VALIDATORS_FOR.with(|r| r.borrow().clone()), - vec![ - (3, vec![]), - (4, vec![]), - (3, vec![]), - (3, vec![]), - (5, vec![]), - (3, vec![]), - (5, vec![]), - (5, vec![ValidatorIndex(5)]), - (3, vec![]), - ], - ); - }) - } - - #[test] - fn test_revert_and_freeze() { - new_test_ext(Default::default()).execute_with(|| { - // events are ignored for genesis block - System::set_block_number(1); - - Frozen::<Test>::put(Some(0)); - assert_noop!( - { - Pallet::<Test>::revert_and_freeze(0); - Result::<(), ()>::Err(()) // Just a small trick in order to use `assert_noop`. - }, - (), - ); - - Frozen::<Test>::kill(); - Pallet::<Test>::revert_and_freeze(0); - - assert_eq!(Frozen::<Test>::get(), Some(0)); - assert_eq!(System::digest().logs[0], ConsensusLog::Revert(1).into()); - System::assert_has_event(Event::Revert(1).into()); - }) - } - - #[test] - fn test_revert_and_freeze_merges() { - new_test_ext(Default::default()).execute_with(|| { - Frozen::<Test>::put(Some(10)); - assert_noop!( - { - Pallet::<Test>::revert_and_freeze(10); - Result::<(), ()>::Err(()) // Just a small trick in order to use `assert_noop`. - }, - (), - ); - - Pallet::<Test>::revert_and_freeze(8); - assert_eq!(Frozen::<Test>::get(), Some(8)); - }) - } - - #[test] - fn test_has_supermajority_against() { - assert_eq!( - has_supermajority_against(&DisputeState { - validators_for: bitvec![BitOrderLsb0, u8; 1, 1, 0, 0, 0, 0, 0, 0], - validators_against: bitvec![BitOrderLsb0, u8; 1, 1, 1, 1, 1, 0, 0, 0], - start: 0, - concluded_at: None, - }), - false, - ); - - assert_eq!( - has_supermajority_against(&DisputeState { - validators_for: bitvec![BitOrderLsb0, u8; 1, 1, 0, 0, 0, 0, 0, 0], - validators_against: bitvec![BitOrderLsb0, u8; 1, 1, 1, 1, 1, 1, 0, 0], - start: 0, - concluded_at: None, - }), - true, - ); - } - - #[test] - fn test_decrement_spam() { - let original_spam_slots = vec![0, 1, 2, 3, 4, 5, 6, 7]; - - // Test confirm is no-op - let mut spam_slots = original_spam_slots.clone(); - let dispute_state_confirm = DisputeState { - validators_for: bitvec![BitOrderLsb0, u8; 1, 1, 0, 0, 0, 0, 0, 0], - validators_against: bitvec![BitOrderLsb0, u8; 1, 0, 1, 0, 0, 0, 0, 0], - start: 0, - concluded_at: None, - }; - assert_eq!( - DisputeStateFlags::from_state(&dispute_state_confirm), - DisputeStateFlags::CONFIRMED - ); - assert_eq!( - decrement_spam(spam_slots.as_mut(), &dispute_state_confirm), - bitvec![BitOrderLsb0, u8; 1, 1, 1, 0, 0, 0, 0, 0], - ); - assert_eq!(spam_slots, original_spam_slots); - - // Test not confirm is decreasing spam - let mut spam_slots = original_spam_slots.clone(); - let dispute_state_no_confirm = DisputeState { - validators_for: bitvec![BitOrderLsb0, u8; 1, 0, 0, 0, 0, 0, 0, 0], - validators_against: bitvec![BitOrderLsb0, u8; 1, 0, 1, 0, 0, 0, 0, 0], - start: 0, - concluded_at: None, - }; - assert_eq!( - DisputeStateFlags::from_state(&dispute_state_no_confirm), - DisputeStateFlags::default() - ); - assert_eq!( - decrement_spam(spam_slots.as_mut(), &dispute_state_no_confirm), - bitvec![BitOrderLsb0, u8; 1, 0, 1, 0, 0, 0, 0, 0], - ); - assert_eq!(spam_slots, vec![0, 1, 1, 3, 4, 5, 6, 7]); - } - - #[test] - fn test_check_signature() { - let validator_id = <ValidatorId as CryptoType>::Pair::generate().0; - let wrong_validator_id = <ValidatorId as CryptoType>::Pair::generate().0; - - let session = 0; - let wrong_session = 1; - let candidate_hash = CandidateHash(sp_core::H256::repeat_byte(1)); - let wrong_candidate_hash = CandidateHash(sp_core::H256::repeat_byte(2)); - let inclusion_parent = sp_core::H256::repeat_byte(3); - let wrong_inclusion_parent = sp_core::H256::repeat_byte(4); - - let statement_1 = DisputeStatement::Valid(ValidDisputeStatementKind::Explicit); - let statement_2 = DisputeStatement::Valid(ValidDisputeStatementKind::BackingSeconded( - inclusion_parent.clone(), - )); - let wrong_statement_2 = DisputeStatement::Valid( - ValidDisputeStatementKind::BackingSeconded(wrong_inclusion_parent.clone()), - ); - let statement_3 = DisputeStatement::Valid(ValidDisputeStatementKind::BackingValid( - inclusion_parent.clone(), - )); - let wrong_statement_3 = DisputeStatement::Valid(ValidDisputeStatementKind::BackingValid( - wrong_inclusion_parent.clone(), - )); - let statement_4 = DisputeStatement::Valid(ValidDisputeStatementKind::ApprovalChecking); - let statement_5 = DisputeStatement::Invalid(InvalidDisputeStatementKind::Explicit); - - let signed_1 = validator_id.sign( - &ExplicitDisputeStatement { - valid: true, - candidate_hash: candidate_hash.clone(), - session, - } - .signing_payload(), - ); - let signed_2 = - validator_id.sign(&CompactStatement::Seconded(candidate_hash.clone()).signing_payload( - &SigningContext { session_index: session, parent_hash: inclusion_parent.clone() }, - )); - let signed_3 = - validator_id.sign(&CompactStatement::Valid(candidate_hash.clone()).signing_payload( - &SigningContext { session_index: session, parent_hash: inclusion_parent.clone() }, - )); - let signed_4 = - validator_id.sign(&ApprovalVote(candidate_hash.clone()).signing_payload(session)); - let signed_5 = validator_id.sign( - &ExplicitDisputeStatement { - valid: false, - candidate_hash: candidate_hash.clone(), - session, - } - .signing_payload(), - ); - - assert!(check_signature( - &validator_id.public(), - candidate_hash, - session, - &statement_1, - &signed_1 - ) - .is_ok()); - assert!(check_signature( - &wrong_validator_id.public(), - candidate_hash, - session, - &statement_1, - &signed_1 - ) - .is_err()); - assert!(check_signature( - &validator_id.public(), - wrong_candidate_hash, - session, - &statement_1, - &signed_1 - ) - .is_err()); - assert!(check_signature( - &validator_id.public(), - candidate_hash, - wrong_session, - &statement_1, - &signed_1 - ) - .is_err()); - assert!(check_signature( - &validator_id.public(), - candidate_hash, - session, - &statement_2, - &signed_1 - ) - .is_err()); - assert!(check_signature( - &validator_id.public(), - candidate_hash, - session, - &statement_3, - &signed_1 - ) - .is_err()); - assert!(check_signature( - &validator_id.public(), - candidate_hash, - session, - &statement_4, - &signed_1 - ) - .is_err()); - assert!(check_signature( - &validator_id.public(), - candidate_hash, - session, - &statement_5, - &signed_1 - ) - .is_err()); - - assert!(check_signature( - &validator_id.public(), - candidate_hash, - session, - &statement_2, - &signed_2 - ) - .is_ok()); - assert!(check_signature( - &wrong_validator_id.public(), - candidate_hash, - session, - &statement_2, - &signed_2 - ) - .is_err()); - assert!(check_signature( - &validator_id.public(), - wrong_candidate_hash, - session, - &statement_2, - &signed_2 - ) - .is_err()); - assert!(check_signature( - &validator_id.public(), - candidate_hash, - wrong_session, - &statement_2, - &signed_2 - ) - .is_err()); - assert!(check_signature( - &validator_id.public(), - candidate_hash, - session, - &wrong_statement_2, - &signed_2 - ) - .is_err()); - assert!(check_signature( - &validator_id.public(), - candidate_hash, - session, - &statement_1, - &signed_2 - ) - .is_err()); - assert!(check_signature( - &validator_id.public(), - candidate_hash, - session, - &statement_3, - &signed_2 - ) - .is_err()); - assert!(check_signature( - &validator_id.public(), - candidate_hash, - session, - &statement_4, - &signed_2 - ) - .is_err()); - assert!(check_signature( - &validator_id.public(), - candidate_hash, - session, - &statement_5, - &signed_2 - ) - .is_err()); - - assert!(check_signature( - &validator_id.public(), - candidate_hash, - session, - &statement_3, - &signed_3 - ) - .is_ok()); - assert!(check_signature( - &wrong_validator_id.public(), - candidate_hash, - session, - &statement_3, - &signed_3 - ) - .is_err()); - assert!(check_signature( - &validator_id.public(), - wrong_candidate_hash, - session, - &statement_3, - &signed_3 - ) - .is_err()); - assert!(check_signature( - &validator_id.public(), - candidate_hash, - wrong_session, - &statement_3, - &signed_3 - ) - .is_err()); - assert!(check_signature( - &validator_id.public(), - candidate_hash, - session, - &wrong_statement_3, - &signed_3 - ) - .is_err()); - assert!(check_signature( - &validator_id.public(), - candidate_hash, - session, - &statement_1, - &signed_3 - ) - .is_err()); - assert!(check_signature( - &validator_id.public(), - candidate_hash, - session, - &statement_2, - &signed_3 - ) - .is_err()); - assert!(check_signature( - &validator_id.public(), - candidate_hash, - session, - &statement_4, - &signed_3 - ) - .is_err()); - assert!(check_signature( - &validator_id.public(), - candidate_hash, - session, - &statement_5, - &signed_3 - ) - .is_err()); - - assert!(check_signature( - &validator_id.public(), - candidate_hash, - session, - &statement_4, - &signed_4 - ) - .is_ok()); - assert!(check_signature( - &wrong_validator_id.public(), - candidate_hash, - session, - &statement_4, - &signed_4 - ) - .is_err()); - assert!(check_signature( - &validator_id.public(), - wrong_candidate_hash, - session, - &statement_4, - &signed_4 - ) - .is_err()); - assert!(check_signature( - &validator_id.public(), - candidate_hash, - wrong_session, - &statement_4, - &signed_4 - ) - .is_err()); - assert!(check_signature( - &validator_id.public(), - candidate_hash, - session, - &statement_1, - &signed_4 - ) - .is_err()); - assert!(check_signature( - &validator_id.public(), - candidate_hash, - session, - &statement_2, - &signed_4 - ) - .is_err()); - assert!(check_signature( - &validator_id.public(), - candidate_hash, - session, - &statement_3, - &signed_4 - ) - .is_err()); - assert!(check_signature( - &validator_id.public(), - candidate_hash, - session, - &statement_5, - &signed_4 - ) - .is_err()); - - assert!(check_signature( - &validator_id.public(), - candidate_hash, - session, - &statement_5, - &signed_5 - ) - .is_ok()); - assert!(check_signature( - &wrong_validator_id.public(), - candidate_hash, - session, - &statement_5, - &signed_5 - ) - .is_err()); - assert!(check_signature( - &validator_id.public(), - wrong_candidate_hash, - session, - &statement_5, - &signed_5 - ) - .is_err()); - assert!(check_signature( - &validator_id.public(), - candidate_hash, - wrong_session, - &statement_5, - &signed_5 - ) - .is_err()); - assert!(check_signature( - &validator_id.public(), - candidate_hash, - session, - &statement_1, - &signed_5 - ) - .is_err()); - assert!(check_signature( - &validator_id.public(), - candidate_hash, - session, - &statement_2, - &signed_5 - ) - .is_err()); - assert!(check_signature( - &validator_id.public(), - candidate_hash, - session, - &statement_3, - &signed_5 - ) - .is_err()); - assert!(check_signature( - &validator_id.public(), - candidate_hash, - session, - &statement_4, - &signed_5 - ) - .is_err()); - } - - #[test] - fn deduplication_and_sorting_works() { - new_test_ext(Default::default()).execute_with(|| { - let v0 = <ValidatorId as CryptoType>::Pair::generate().0; - let v1 = <ValidatorId as CryptoType>::Pair::generate().0; - let v2 = <ValidatorId as CryptoType>::Pair::generate().0; - let v3 = <ValidatorId as CryptoType>::Pair::generate().0; - - run_to_block(3, |b| { - // a new session at each block - Some(( - true, - b, - vec![ - (&0, v0.public()), - (&1, v1.public()), - (&2, v2.public()), - (&3, v3.public()), - ], - Some(vec![ - (&0, v0.public()), - (&1, v1.public()), - (&2, v2.public()), - (&3, v3.public()), - ]), - )) - }); - - let candidate_hash_a = CandidateHash(sp_core::H256::repeat_byte(1)); - let candidate_hash_b = CandidateHash(sp_core::H256::repeat_byte(2)); - let candidate_hash_c = CandidateHash(sp_core::H256::repeat_byte(3)); - - let create_explicit_statement = |vidx: ValidatorIndex, - validator: &<ValidatorId as CryptoType>::Pair, - c_hash: &CandidateHash, - valid, - session| { - let payload = - ExplicitDisputeStatement { valid, candidate_hash: c_hash.clone(), session } - .signing_payload(); - let sig = validator.sign(&payload); - (DisputeStatement::Valid(ValidDisputeStatementKind::Explicit), vidx, sig.clone()) - }; - - let explicit_triple_a = - create_explicit_statement(ValidatorIndex(0), &v0, &candidate_hash_a, true, 1); - let explicit_triple_a_bad = - create_explicit_statement(ValidatorIndex(1), &v1, &candidate_hash_a, false, 1); - - let explicit_triple_b = - create_explicit_statement(ValidatorIndex(0), &v0, &candidate_hash_b, true, 2); - let explicit_triple_b_bad = - create_explicit_statement(ValidatorIndex(1), &v1, &candidate_hash_b, false, 2); - - let explicit_triple_c = - create_explicit_statement(ValidatorIndex(0), &v0, &candidate_hash_c, true, 2); - let explicit_triple_c_bad = - create_explicit_statement(ValidatorIndex(1), &v1, &candidate_hash_c, false, 2); - - let mut disputes = vec![ - DisputeStatementSet { - candidate_hash: candidate_hash_b.clone(), - session: 2, - statements: vec![explicit_triple_b.clone(), explicit_triple_b_bad.clone()], - }, - // same session as above - DisputeStatementSet { - candidate_hash: candidate_hash_c.clone(), - session: 2, - statements: vec![explicit_triple_c, explicit_triple_c_bad], - }, - // the duplicate set - DisputeStatementSet { - candidate_hash: candidate_hash_b.clone(), - session: 2, - statements: vec![explicit_triple_b.clone(), explicit_triple_b_bad.clone()], - }, - DisputeStatementSet { - candidate_hash: candidate_hash_a.clone(), - session: 1, - statements: vec![explicit_triple_a, explicit_triple_a_bad], - }, - ]; - - let disputes_orig = disputes.clone(); - - <Pallet<Test> as DisputesHandler< - <Test as frame_system::Config>::BlockNumber, - >>::deduplicate_and_sort_dispute_data(&mut disputes).unwrap_err(); - - // assert ordering of local only disputes, and at the same time, and being free of duplicates - assert_eq!(disputes_orig.len(), disputes.len() + 1); - - let are_these_equal = |a: &DisputeStatementSet, b: &DisputeStatementSet| { - use core::cmp::Ordering; - // we only have local disputes here, so sorting of those adheres to the - // simplified sorting logic - let cmp = - a.session.cmp(&b.session).then_with(|| a.candidate_hash.cmp(&b.candidate_hash)); - assert_ne!(cmp, Ordering::Greater); - cmp == Ordering::Equal - }; - - assert_eq!(false, contains_duplicates_in_sorted_iter(&disputes, are_these_equal)); - }) - } - - fn apply_filter_all<T: Config, I: IntoIterator<Item = DisputeStatementSet>>( - sets: I, - ) -> Vec<CheckedDisputeStatementSet> { - let config = <configuration::Pallet<T>>::config(); - let max_spam_slots = config.dispute_max_spam_slots; - let post_conclusion_acceptance_period = config.dispute_post_conclusion_acceptance_period; - - let mut acc = Vec::<CheckedDisputeStatementSet>::new(); - for dispute_statement in sets { - if let Some(checked) = - <Pallet<T> as DisputesHandler<<T>::BlockNumber>>::filter_dispute_data( - dispute_statement, - max_spam_slots, - post_conclusion_acceptance_period, - VerifyDisputeSignatures::Yes, - ) { - acc.push(checked); - } - } - acc - } - - #[test] - fn filter_removes_duplicates_within_set() { - new_test_ext(Default::default()).execute_with(|| { - let v0 = <ValidatorId as CryptoType>::Pair::generate().0; - let v1 = <ValidatorId as CryptoType>::Pair::generate().0; - - run_to_block(3, |b| { - // a new session at each block - Some(( - true, - b, - vec![(&0, v0.public()), (&1, v1.public())], - Some(vec![(&0, v0.public()), (&1, v1.public())]), - )) - }); - - let candidate_hash = CandidateHash(sp_core::H256::repeat_byte(1)); - - let payload = ExplicitDisputeStatement { - valid: true, - candidate_hash: candidate_hash.clone(), - session: 1, - } - .signing_payload(); - - let payload_against = ExplicitDisputeStatement { - valid: false, - candidate_hash: candidate_hash.clone(), - session: 1, - } - .signing_payload(); - - let sig_a = v0.sign(&payload); - let sig_b = v0.sign(&payload); - let sig_c = v0.sign(&payload); - let sig_d = v1.sign(&payload_against); - - let statements = DisputeStatementSet { - candidate_hash: candidate_hash.clone(), - session: 1, - statements: vec![ - ( - DisputeStatement::Valid(ValidDisputeStatementKind::Explicit), - ValidatorIndex(0), - sig_a.clone(), - ), - ( - DisputeStatement::Valid(ValidDisputeStatementKind::Explicit), - ValidatorIndex(0), - sig_b, - ), - ( - DisputeStatement::Valid(ValidDisputeStatementKind::Explicit), - ValidatorIndex(0), - sig_c, - ), - ( - DisputeStatement::Invalid(InvalidDisputeStatementKind::Explicit), - ValidatorIndex(1), - sig_d.clone(), - ), - ], - }; - - let max_spam_slots = 10; - let post_conclusion_acceptance_period = 10; - let statements = <Pallet<Test> as DisputesHandler< - <Test as frame_system::Config>::BlockNumber, - >>::filter_dispute_data( - statements, - max_spam_slots, - post_conclusion_acceptance_period, - VerifyDisputeSignatures::Yes, - ); - - assert_eq!( - statements, - Some(CheckedDisputeStatementSet::unchecked_from_unchecked(DisputeStatementSet { - candidate_hash: candidate_hash.clone(), - session: 1, - statements: vec![ - ( - DisputeStatement::Valid(ValidDisputeStatementKind::Explicit), - ValidatorIndex(0), - sig_a, - ), - ( - DisputeStatement::Invalid(InvalidDisputeStatementKind::Explicit), - ValidatorIndex(1), - sig_d, - ), - ] - })) - ); - }) - } - - #[test] - fn filter_bad_signatures_correctly_detects_single_sided() { - new_test_ext(Default::default()).execute_with(|| { - let v0 = <ValidatorId as CryptoType>::Pair::generate().0; - let v1 = <ValidatorId as CryptoType>::Pair::generate().0; - let v2 = <ValidatorId as CryptoType>::Pair::generate().0; - let v3 = <ValidatorId as CryptoType>::Pair::generate().0; - - run_to_block(3, |b| { - // a new session at each block - Some(( - true, - b, - vec![ - (&0, v0.public()), - (&1, v1.public()), - (&2, v2.public()), - (&3, v3.public()), - ], - Some(vec![ - (&0, v0.public()), - (&1, v1.public()), - (&2, v2.public()), - (&3, v3.public()), - ]), - )) - }); - - let candidate_hash_a = CandidateHash(sp_core::H256::repeat_byte(1)); - - let payload = |c_hash: &CandidateHash, valid| { - ExplicitDisputeStatement { valid, candidate_hash: c_hash.clone(), session: 1 } - .signing_payload() - }; - - let payload_a = payload(&candidate_hash_a, true); - let payload_a_bad = payload(&candidate_hash_a, false); - - let sig_0 = v0.sign(&payload_a); - let sig_1 = v1.sign(&payload_a_bad); - - let statements = vec![DisputeStatementSet { - candidate_hash: candidate_hash_a.clone(), - session: 1, - statements: vec![ - ( - DisputeStatement::Valid(ValidDisputeStatementKind::Explicit), - ValidatorIndex(0), - sig_0.clone(), - ), - ( - DisputeStatement::Invalid(InvalidDisputeStatementKind::Explicit), - ValidatorIndex(2), - sig_1.clone(), - ), - ], - }]; - - let statements = apply_filter_all::<Test, _>(statements); - - assert!(statements.is_empty()); - }) - } - - #[test] - fn filter_correctly_accounts_spam_slots() { - let dispute_max_spam_slots = 2; - - let mock_genesis_config = MockGenesisConfig { - configuration: crate::configuration::GenesisConfig { - config: HostConfiguration { dispute_max_spam_slots, ..Default::default() }, - ..Default::default() - }, - ..Default::default() - }; - - new_test_ext(mock_genesis_config).execute_with(|| { - // We need 7 validators for the byzantine threshold to be 2 - let v0 = <ValidatorId as CryptoType>::Pair::generate().0; - let v1 = <ValidatorId as CryptoType>::Pair::generate().0; - let v2 = <ValidatorId as CryptoType>::Pair::generate().0; - let v3 = <ValidatorId as CryptoType>::Pair::generate().0; - let v4 = <ValidatorId as CryptoType>::Pair::generate().0; - let v5 = <ValidatorId as CryptoType>::Pair::generate().0; - let v6 = <ValidatorId as CryptoType>::Pair::generate().0; - - run_to_block(3, |b| { - // a new session at each block - Some(( - true, - b, - vec![ - (&0, v0.public()), - (&1, v1.public()), - (&2, v2.public()), - (&3, v3.public()), - (&4, v4.public()), - (&5, v5.public()), - (&6, v6.public()), - ], - Some(vec![ - (&0, v0.public()), - (&1, v1.public()), - (&2, v2.public()), - (&3, v3.public()), - (&4, v4.public()), - (&5, v5.public()), - (&6, v6.public()), - ]), - )) - }); - - let candidate_hash_a = CandidateHash(sp_core::H256::repeat_byte(1)); - let candidate_hash_b = CandidateHash(sp_core::H256::repeat_byte(2)); - let candidate_hash_c = CandidateHash(sp_core::H256::repeat_byte(3)); - - let payload = |c_hash: &CandidateHash, valid| { - ExplicitDisputeStatement { valid, candidate_hash: c_hash.clone(), session: 1 } - .signing_payload() - }; - - let payload_a = payload(&candidate_hash_a, true); - let payload_b = payload(&candidate_hash_b, true); - let payload_c = payload(&candidate_hash_c, true); - - let payload_a_bad = payload(&candidate_hash_a, false); - let payload_b_bad = payload(&candidate_hash_b, false); - let payload_c_bad = payload(&candidate_hash_c, false); - - let sig_0a = v0.sign(&payload_a); - let sig_0b = v0.sign(&payload_b); - let sig_0c = v0.sign(&payload_c); - - let sig_1b = v1.sign(&payload_b); - - let sig_2a = v2.sign(&payload_a_bad); - let sig_2b = v2.sign(&payload_b_bad); - let sig_2c = v2.sign(&payload_c_bad); - - let statements = vec![ - // validators 0 and 2 get 1 spam slot from this. - DisputeStatementSet { - candidate_hash: candidate_hash_a.clone(), - session: 1, - statements: vec![ - ( - DisputeStatement::Valid(ValidDisputeStatementKind::Explicit), - ValidatorIndex(0), - sig_0a.clone(), - ), - ( - DisputeStatement::Invalid(InvalidDisputeStatementKind::Explicit), - ValidatorIndex(6), - sig_2a.clone(), - ), - ], - }, - // Validators 0, 2, and 3 get no spam slots for this - DisputeStatementSet { - candidate_hash: candidate_hash_b.clone(), - session: 1, - statements: vec![ - ( - DisputeStatement::Valid(ValidDisputeStatementKind::Explicit), - ValidatorIndex(0), - sig_0b.clone(), - ), - ( - DisputeStatement::Valid(ValidDisputeStatementKind::Explicit), - ValidatorIndex(3), - sig_1b.clone(), - ), - ( - DisputeStatement::Invalid(InvalidDisputeStatementKind::Explicit), - ValidatorIndex(6), - sig_2b.clone(), - ), - ], - }, - // Validators 0 and 2 get an extra spam slot for this. - DisputeStatementSet { - candidate_hash: candidate_hash_c.clone(), - session: 1, - statements: vec![ - ( - DisputeStatement::Valid(ValidDisputeStatementKind::Explicit), - ValidatorIndex(0), - sig_0c.clone(), - ), - ( - DisputeStatement::Invalid(InvalidDisputeStatementKind::Explicit), - ValidatorIndex(6), - sig_2c.clone(), - ), - ], - }, - ]; - - let old_statements = statements - .clone() - .into_iter() - .map(CheckedDisputeStatementSet::unchecked_from_unchecked) - .collect::<Vec<_>>(); - let statements = apply_filter_all::<Test, _>(statements); - - assert_eq!(statements, old_statements); - }) - } - - #[test] - fn filter_removes_session_out_of_bounds() { - new_test_ext(Default::default()).execute_with(|| { - let v0 = <ValidatorId as CryptoType>::Pair::generate().0; - - run_to_block(3, |b| { - // a new session at each block - Some((true, b, vec![(&0, v0.public())], Some(vec![(&0, v0.public())]))) - }); - - let candidate_hash = CandidateHash(sp_core::H256::repeat_byte(1)); - - let payload = ExplicitDisputeStatement { - valid: true, - candidate_hash: candidate_hash.clone(), - session: 1, - } - .signing_payload(); - - let sig_a = v0.sign(&payload); - - let statements = vec![DisputeStatementSet { - candidate_hash: candidate_hash.clone(), - session: 100, - statements: vec![( - DisputeStatement::Valid(ValidDisputeStatementKind::Explicit), - ValidatorIndex(0), - sig_a, - )], - }]; - - let statements = apply_filter_all::<Test, _>(statements); - - assert!(statements.is_empty()); - }) - } - - #[test] - fn filter_removes_concluded_ancient() { - let dispute_post_conclusion_acceptance_period = 2; - - let mock_genesis_config = MockGenesisConfig { - configuration: crate::configuration::GenesisConfig { - config: HostConfiguration { - dispute_post_conclusion_acceptance_period, - ..Default::default() - }, - ..Default::default() - }, - ..Default::default() - }; - - new_test_ext(mock_genesis_config).execute_with(|| { - let v0 = <ValidatorId as CryptoType>::Pair::generate().0; - - run_to_block(3, |b| { - // a new session at each block - Some((true, b, vec![(&0, v0.public())], Some(vec![(&0, v0.public())]))) - }); - - let candidate_hash_a = CandidateHash(sp_core::H256::repeat_byte(1)); - let candidate_hash_b = CandidateHash(sp_core::H256::repeat_byte(2)); - - <Disputes<Test>>::insert( - &1, - &candidate_hash_a, - DisputeState { - validators_for: bitvec![BitOrderLsb0, u8; 0; 4], - validators_against: bitvec![BitOrderLsb0, u8; 1; 4], - start: 0, - concluded_at: Some(0), - }, - ); - - <Disputes<Test>>::insert( - &1, - &candidate_hash_b, - DisputeState { - validators_for: bitvec![BitOrderLsb0, u8; 0; 4], - validators_against: bitvec![BitOrderLsb0, u8; 1; 4], - start: 0, - concluded_at: Some(1), - }, - ); - - let payload_a = ExplicitDisputeStatement { - valid: true, - candidate_hash: candidate_hash_a.clone(), - session: 1, - } - .signing_payload(); - - let payload_b = ExplicitDisputeStatement { - valid: true, - candidate_hash: candidate_hash_b.clone(), - session: 1, - } - .signing_payload(); - - let sig_a = v0.sign(&payload_a); - let sig_b = v0.sign(&payload_b); - - let statements = vec![ - DisputeStatementSet { - candidate_hash: candidate_hash_a.clone(), - session: 1, - statements: vec![( - DisputeStatement::Valid(ValidDisputeStatementKind::Explicit), - ValidatorIndex(0), - sig_a, - )], - }, - DisputeStatementSet { - candidate_hash: candidate_hash_b.clone(), - session: 1, - statements: vec![( - DisputeStatement::Valid(ValidDisputeStatementKind::Explicit), - ValidatorIndex(0), - sig_b.clone(), - )], - }, - ]; - - let statements = apply_filter_all::<Test, _>(statements); - - assert_eq!( - statements, - vec![CheckedDisputeStatementSet::unchecked_from_unchecked(DisputeStatementSet { - candidate_hash: candidate_hash_b.clone(), - session: 1, - statements: vec![( - DisputeStatement::Valid(ValidDisputeStatementKind::Explicit), - ValidatorIndex(0), - sig_b, - ),] - })] - ); - }) - } - - #[test] - fn filter_removes_duplicate_statements_sets() { - new_test_ext(Default::default()).execute_with(|| { - let v0 = <ValidatorId as CryptoType>::Pair::generate().0; - let v1 = <ValidatorId as CryptoType>::Pair::generate().0; - - run_to_block(3, |b| { - // a new session at each block - Some(( - true, - b, - vec![(&0, v0.public()), (&1, v1.public())], - Some(vec![(&0, v0.public()), (&1, v1.public())]), - )) - }); - - let candidate_hash_a = CandidateHash(sp_core::H256::repeat_byte(1)); - - let payload = ExplicitDisputeStatement { - valid: true, - candidate_hash: candidate_hash_a.clone(), - session: 1, - } - .signing_payload(); - - let payload_against = ExplicitDisputeStatement { - valid: false, - candidate_hash: candidate_hash_a.clone(), - session: 1, - } - .signing_payload(); - - let sig_a = v0.sign(&payload); - let sig_a_against = v1.sign(&payload_against); - - let statements = vec![ - ( - DisputeStatement::Valid(ValidDisputeStatementKind::Explicit), - ValidatorIndex(0), - sig_a.clone(), - ), - ( - DisputeStatement::Invalid(InvalidDisputeStatementKind::Explicit), - ValidatorIndex(1), - sig_a_against.clone(), - ), - ]; - - let mut sets = vec![ - DisputeStatementSet { - candidate_hash: candidate_hash_a.clone(), - session: 1, - statements: statements.clone(), - }, - DisputeStatementSet { - candidate_hash: candidate_hash_a.clone(), - session: 1, - statements: statements.clone(), - }, - ]; - - // `Err(())` indicates presence of duplicates - assert!(<Pallet::<Test> as DisputesHandler< - <Test as frame_system::Config>::BlockNumber, - >>::deduplicate_and_sort_dispute_data(&mut sets) - .is_err()); - - assert_eq!( - sets, - vec![DisputeStatementSet { - candidate_hash: candidate_hash_a.clone(), - session: 1, - statements, - }] - ); - }) - } - - #[test] - fn assure_no_duplicate_statements_sets_are_fine() { - new_test_ext(Default::default()).execute_with(|| { - let v0 = <ValidatorId as CryptoType>::Pair::generate().0; - let v1 = <ValidatorId as CryptoType>::Pair::generate().0; - - run_to_block(3, |b| { - // a new session at each block - Some(( - true, - b, - vec![(&0, v0.public()), (&1, v1.public())], - Some(vec![(&0, v0.public()), (&1, v1.public())]), - )) - }); - - let candidate_hash_a = CandidateHash(sp_core::H256::repeat_byte(1)); - - let payload = ExplicitDisputeStatement { - valid: true, - candidate_hash: candidate_hash_a.clone(), - session: 1, - } - .signing_payload(); - - let payload_against = ExplicitDisputeStatement { - valid: false, - candidate_hash: candidate_hash_a.clone(), - session: 1, - } - .signing_payload(); - - let sig_a = v0.sign(&payload); - let sig_a_against = v1.sign(&payload_against); - - let statements = vec![ - ( - DisputeStatement::Valid(ValidDisputeStatementKind::Explicit), - ValidatorIndex(0), - sig_a.clone(), - ), - ( - DisputeStatement::Invalid(InvalidDisputeStatementKind::Explicit), - ValidatorIndex(1), - sig_a_against.clone(), - ), - ]; - - let sets = vec![ - DisputeStatementSet { - candidate_hash: candidate_hash_a.clone(), - session: 1, - statements: statements.clone(), - }, - DisputeStatementSet { - candidate_hash: candidate_hash_a.clone(), - session: 2, - statements: statements.clone(), - }, - ]; - - // `Err(())` indicates presence of duplicates - assert!(<Pallet::<Test> as DisputesHandler< - <Test as frame_system::Config>::BlockNumber, - >>::assure_deduplicated_and_sorted(&sets) - .is_ok()); - }) - } - - #[test] - fn assure_detects_duplicate_statements_sets() { - new_test_ext(Default::default()).execute_with(|| { - let v0 = <ValidatorId as CryptoType>::Pair::generate().0; - let v1 = <ValidatorId as CryptoType>::Pair::generate().0; - - run_to_block(3, |b| { - // a new session at each block - Some(( - true, - b, - vec![(&0, v0.public()), (&1, v1.public())], - Some(vec![(&0, v0.public()), (&1, v1.public())]), - )) - }); - - let candidate_hash_a = CandidateHash(sp_core::H256::repeat_byte(1)); - - let payload = ExplicitDisputeStatement { - valid: true, - candidate_hash: candidate_hash_a.clone(), - session: 1, - } - .signing_payload(); - - let payload_against = ExplicitDisputeStatement { - valid: false, - candidate_hash: candidate_hash_a.clone(), - session: 1, - } - .signing_payload(); - - let sig_a = v0.sign(&payload); - let sig_a_against = v1.sign(&payload_against); - - let statements = vec![ - ( - DisputeStatement::Valid(ValidDisputeStatementKind::Explicit), - ValidatorIndex(0), - sig_a.clone(), - ), - ( - DisputeStatement::Invalid(InvalidDisputeStatementKind::Explicit), - ValidatorIndex(1), - sig_a_against.clone(), - ), - ]; - - let sets = vec![ - DisputeStatementSet { - candidate_hash: candidate_hash_a.clone(), - session: 1, - statements: statements.clone(), - }, - DisputeStatementSet { - candidate_hash: candidate_hash_a.clone(), - session: 1, - statements: statements.clone(), - }, - ]; - - // `Err(())` indicates presence of duplicates - assert!(<Pallet::<Test> as DisputesHandler< - <Test as frame_system::Config>::BlockNumber, - >>::assure_deduplicated_and_sorted(&sets) - .is_err()); - }) - } - - #[test] - fn filter_ignores_single_sided() { - new_test_ext(Default::default()).execute_with(|| { - let v0 = <ValidatorId as CryptoType>::Pair::generate().0; - - run_to_block(3, |b| { - // a new session at each block - Some((true, b, vec![(&0, v0.public())], Some(vec![(&0, v0.public())]))) - }); - - let candidate_hash_a = CandidateHash(sp_core::H256::repeat_byte(1)); - - let payload = ExplicitDisputeStatement { - valid: true, - candidate_hash: candidate_hash_a.clone(), - session: 1, - } - .signing_payload(); - - let sig_a = v0.sign(&payload); - - let statements = vec![DisputeStatementSet { - candidate_hash: candidate_hash_a.clone(), - session: 1, - statements: vec![( - DisputeStatement::Valid(ValidDisputeStatementKind::Explicit), - ValidatorIndex(0), - sig_a.clone(), - )], - }]; - - let statements = apply_filter_all::<Test, _>(statements); - - assert!(statements.is_empty()); - }) - } - - #[test] - fn import_ignores_single_sided() { - new_test_ext(Default::default()).execute_with(|| { - let v0 = <ValidatorId as CryptoType>::Pair::generate().0; - - run_to_block(3, |b| { - // a new session at each block - Some((true, b, vec![(&0, v0.public())], Some(vec![(&0, v0.public())]))) - }); - - let candidate_hash_a = CandidateHash(sp_core::H256::repeat_byte(1)); - - let payload = ExplicitDisputeStatement { - valid: true, - candidate_hash: candidate_hash_a.clone(), - session: 1, - } - .signing_payload(); - - let sig_a = v0.sign(&payload); - - let statements = vec![DisputeStatementSet { - candidate_hash: candidate_hash_a.clone(), - session: 1, - statements: vec![( - DisputeStatement::Valid(ValidDisputeStatementKind::Explicit), - ValidatorIndex(0), - sig_a.clone(), - )], - }]; - - let statements = apply_filter_all::<Test, _>(statements); - assert!(statements.is_empty()); - }) - } -} diff --git a/polkadot/runtime/parachains/src/disputes/tests.rs b/polkadot/runtime/parachains/src/disputes/tests.rs new file mode 100644 index 0000000000000000000000000000000000000000..5a00cfa8f4bc3b165491bc9e1f7f0349a9620c60 --- /dev/null +++ b/polkadot/runtime/parachains/src/disputes/tests.rs @@ -0,0 +1,2453 @@ +// Copyright 2020 Parity Technologies (UK) Ltd. +// This file is part of Polkadot. + +// Polkadot is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Polkadot is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Polkadot. If not, see <http://www.gnu.org/licenses/>. + +use super::*; +use crate::{ + configuration::HostConfiguration, + disputes::DisputesHandler, + mock::{ + new_test_ext, AccountId, AllPalletsWithSystem, Initializer, MockGenesisConfig, System, + Test, PUNISH_VALIDATORS_AGAINST, PUNISH_VALIDATORS_FOR, PUNISH_VALIDATORS_INCONCLUSIVE, + REWARD_VALIDATORS, + }, +}; +use frame_support::{ + assert_err, assert_noop, assert_ok, + traits::{OnFinalize, OnInitialize}, +}; +use primitives::v1::BlockNumber; +use sp_core::{crypto::CryptoType, Pair}; + +/// Filtering updates the spam slots, as such update them. +fn update_spam_slots(stmts: MultiDisputeStatementSet) -> CheckedMultiDisputeStatementSet { + let config = <configuration::Pallet<Test>>::config(); + let max_spam_slots = config.dispute_max_spam_slots; + let post_conclusion_acceptance_period = config.dispute_post_conclusion_acceptance_period; + + stmts + .into_iter() + .filter_map(|set| { + // updates spam slots implicitly + let filter = Pallet::<Test>::filter_dispute_data( + &set, + post_conclusion_acceptance_period, + max_spam_slots, + VerifyDisputeSignatures::Skip, + ); + filter.filter_statement_set(set) + }) + .collect::<Vec<_>>() +} + +// All arguments for `initializer::on_new_session` +type NewSession<'a> = ( + bool, + SessionIndex, + Vec<(&'a AccountId, ValidatorId)>, + Option<Vec<(&'a AccountId, ValidatorId)>>, +); + +// Run to specific block, while calling disputes pallet hooks manually, because disputes is not +// integrated in initializer yet. +pub(crate) fn run_to_block<'a>( + to: BlockNumber, + new_session: impl Fn(BlockNumber) -> Option<NewSession<'a>>, +) { + while System::block_number() < to { + let b = System::block_number(); + if b != 0 { + // circumvent requirement to have bitfields and headers in block for testing purposes + crate::paras_inherent::Included::<Test>::set(Some(())); + + AllPalletsWithSystem::on_finalize(b); + System::finalize(); + } + + System::reset_events(); + System::initialize(&(b + 1), &Default::default(), &Default::default()); + AllPalletsWithSystem::on_initialize(b + 1); + + if let Some(new_session) = new_session(b + 1) { + Initializer::test_trigger_on_new_session( + new_session.0, + new_session.1, + new_session.2.into_iter(), + new_session.3.map(|q| q.into_iter()), + ); + } + } +} + +#[test] +fn test_contains_duplicates_in_sorted_iter() { + // We here use the implicit ascending sorting and builtin equality of integers + let v = vec![1, 2, 3, 5, 5, 8]; + assert_eq!(true, contains_duplicates_in_sorted_iter(&v, |a, b| a == b)); + + let v = vec![1, 2, 3, 4]; + assert_eq!(false, contains_duplicates_in_sorted_iter(&v, |a, b| a == b)); +} + +#[test] +fn test_dispute_state_flag_from_state() { + assert_eq!( + DisputeStateFlags::from_state(&DisputeState { + validators_for: bitvec![BitOrderLsb0, u8; 0, 0, 0, 0, 0, 0, 0, 0], + validators_against: bitvec![BitOrderLsb0, u8; 0, 0, 0, 0, 0, 0, 0, 0], + start: 0, + concluded_at: None, + }), + DisputeStateFlags::default(), + ); + + assert_eq!( + DisputeStateFlags::from_state(&DisputeState { + validators_for: bitvec![BitOrderLsb0, u8; 1, 1, 1, 1, 1, 0, 0], + validators_against: bitvec![BitOrderLsb0, u8; 0, 0, 0, 0, 0, 0, 0], + start: 0, + concluded_at: None, + }), + DisputeStateFlags::FOR_SUPERMAJORITY | DisputeStateFlags::CONFIRMED, + ); + + assert_eq!( + DisputeStateFlags::from_state(&DisputeState { + validators_for: bitvec![BitOrderLsb0, u8; 0, 0, 0, 0, 0, 0, 0], + validators_against: bitvec![BitOrderLsb0, u8; 1, 1, 1, 1, 1, 0, 0], + start: 0, + concluded_at: None, + }), + DisputeStateFlags::AGAINST_SUPERMAJORITY | DisputeStateFlags::CONFIRMED, + ); +} + +#[test] +fn test_import_new_participant_spam_inc() { + let mut importer = DisputeStateImporter::new( + DisputeState { + validators_for: bitvec![BitOrderLsb0, u8; 1, 0, 0, 0, 0, 0, 0, 0], + validators_against: bitvec![BitOrderLsb0, u8; 0, 0, 0, 0, 0, 0, 0, 0], + start: 0, + concluded_at: None, + }, + 0, + ); + + assert_err!( + importer.import(ValidatorIndex(9), true), + VoteImportError::ValidatorIndexOutOfBounds, + ); + + assert_err!(importer.import(ValidatorIndex(0), true), VoteImportError::DuplicateStatement); + assert_ok!(importer.import(ValidatorIndex(0), false)); + + assert_ok!(importer.import(ValidatorIndex(2), true)); + assert_err!(importer.import(ValidatorIndex(2), true), VoteImportError::DuplicateStatement); + + assert_ok!(importer.import(ValidatorIndex(2), false)); + assert_err!(importer.import(ValidatorIndex(2), false), VoteImportError::DuplicateStatement); + + let summary = importer.finish(); + assert_eq!(summary.new_flags, DisputeStateFlags::default()); + assert_eq!( + summary.state, + DisputeState { + validators_for: bitvec![BitOrderLsb0, u8; 1, 0, 1, 0, 0, 0, 0, 0], + validators_against: bitvec![BitOrderLsb0, u8; 1, 0, 1, 0, 0, 0, 0, 0], + start: 0, + concluded_at: None, + }, + ); + assert_eq!(summary.spam_slot_changes, vec![(ValidatorIndex(2), SpamSlotChange::Inc)]); + assert!(summary.slash_for.is_empty()); + assert!(summary.slash_against.is_empty()); + assert_eq!(summary.new_participants, bitvec![BitOrderLsb0, u8; 0, 0, 1, 0, 0, 0, 0, 0]); +} + +#[test] +fn test_import_prev_participant_spam_dec_confirmed() { + let mut importer = DisputeStateImporter::new( + DisputeState { + validators_for: bitvec![BitOrderLsb0, u8; 1, 0, 0, 0, 0, 0, 0, 0], + validators_against: bitvec![BitOrderLsb0, u8; 0, 1, 0, 0, 0, 0, 0, 0], + start: 0, + concluded_at: None, + }, + 0, + ); + + assert_ok!(importer.import(ValidatorIndex(2), true)); + + let summary = importer.finish(); + assert_eq!( + summary.state, + DisputeState { + validators_for: bitvec![BitOrderLsb0, u8; 1, 0, 1, 0, 0, 0, 0, 0], + validators_against: bitvec![BitOrderLsb0, u8; 0, 1, 0, 0, 0, 0, 0, 0], + start: 0, + concluded_at: None, + }, + ); + assert_eq!( + summary.spam_slot_changes, + vec![(ValidatorIndex(0), SpamSlotChange::Dec), (ValidatorIndex(1), SpamSlotChange::Dec),], + ); + assert!(summary.slash_for.is_empty()); + assert!(summary.slash_against.is_empty()); + assert_eq!(summary.new_participants, bitvec![BitOrderLsb0, u8; 0, 0, 1, 0, 0, 0, 0, 0]); + assert_eq!(summary.new_flags, DisputeStateFlags::CONFIRMED); +} + +#[test] +fn test_import_prev_participant_spam_dec_confirmed_slash_for() { + let mut importer = DisputeStateImporter::new( + DisputeState { + validators_for: bitvec![BitOrderLsb0, u8; 1, 0, 0, 0, 0, 0, 0, 0], + validators_against: bitvec![BitOrderLsb0, u8; 0, 1, 0, 0, 0, 0, 0, 0], + start: 0, + concluded_at: None, + }, + 0, + ); + + assert_ok!(importer.import(ValidatorIndex(2), true)); + assert_ok!(importer.import(ValidatorIndex(2), false)); + assert_ok!(importer.import(ValidatorIndex(3), false)); + assert_ok!(importer.import(ValidatorIndex(4), false)); + assert_ok!(importer.import(ValidatorIndex(5), false)); + assert_ok!(importer.import(ValidatorIndex(6), false)); + + let summary = importer.finish(); + assert_eq!( + summary.state, + DisputeState { + validators_for: bitvec![BitOrderLsb0, u8; 1, 0, 1, 0, 0, 0, 0, 0], + validators_against: bitvec![BitOrderLsb0, u8; 0, 1, 1, 1, 1, 1, 1, 0], + start: 0, + concluded_at: Some(0), + }, + ); + assert_eq!( + summary.spam_slot_changes, + vec![(ValidatorIndex(0), SpamSlotChange::Dec), (ValidatorIndex(1), SpamSlotChange::Dec),], + ); + assert_eq!(summary.slash_for, vec![ValidatorIndex(0), ValidatorIndex(2)]); + assert!(summary.slash_against.is_empty()); + assert_eq!(summary.new_participants, bitvec![BitOrderLsb0, u8; 0, 0, 1, 1, 1, 1, 1, 0]); + assert_eq!( + summary.new_flags, + DisputeStateFlags::CONFIRMED | DisputeStateFlags::AGAINST_SUPERMAJORITY, + ); +} + +#[test] +fn test_import_slash_against() { + let mut importer = DisputeStateImporter::new( + DisputeState { + validators_for: bitvec![BitOrderLsb0, u8; 1, 0, 1, 0, 0, 0, 0, 0], + validators_against: bitvec![BitOrderLsb0, u8; 0, 1, 0, 0, 0, 0, 0, 0], + start: 0, + concluded_at: None, + }, + 0, + ); + + assert_ok!(importer.import(ValidatorIndex(3), true)); + assert_ok!(importer.import(ValidatorIndex(4), true)); + assert_ok!(importer.import(ValidatorIndex(5), false)); + assert_ok!(importer.import(ValidatorIndex(6), true)); + assert_ok!(importer.import(ValidatorIndex(7), true)); + + let summary = importer.finish(); + assert_eq!( + summary.state, + DisputeState { + validators_for: bitvec![BitOrderLsb0, u8; 1, 0, 1, 1, 1, 0, 1, 1], + validators_against: bitvec![BitOrderLsb0, u8; 0, 1, 0, 0, 0, 1, 0, 0], + start: 0, + concluded_at: Some(0), + }, + ); + assert!(summary.spam_slot_changes.is_empty()); + assert!(summary.slash_for.is_empty()); + assert_eq!(summary.slash_against, vec![ValidatorIndex(1), ValidatorIndex(5)]); + assert_eq!(summary.new_participants, bitvec![BitOrderLsb0, u8; 0, 0, 0, 1, 1, 1, 1, 1]); + assert_eq!(summary.new_flags, DisputeStateFlags::FOR_SUPERMAJORITY); +} + +// Test that punish_inconclusive is correctly called. +#[test] +fn test_initializer_initialize() { + let dispute_conclusion_by_time_out_period = 3; + let start = 10; + + let mock_genesis_config = MockGenesisConfig { + configuration: crate::configuration::GenesisConfig { + config: HostConfiguration { + dispute_conclusion_by_time_out_period, + ..Default::default() + }, + ..Default::default() + }, + ..Default::default() + }; + + new_test_ext(mock_genesis_config).execute_with(|| { + // We need 6 validators for the byzantine threshold to be 2 + let v0 = <ValidatorId as CryptoType>::Pair::generate().0; + let v1 = <ValidatorId as CryptoType>::Pair::generate().0; + let v2 = <ValidatorId as CryptoType>::Pair::generate().0; + let v3 = <ValidatorId as CryptoType>::Pair::generate().0; + let v4 = <ValidatorId as CryptoType>::Pair::generate().0; + let v5 = <ValidatorId as CryptoType>::Pair::generate().0; + let v6 = <ValidatorId as CryptoType>::Pair::generate().0; + + run_to_block(start, |b| { + // a new session at each block + Some(( + true, + b, + vec![ + (&0, v0.public()), + (&1, v1.public()), + (&2, v2.public()), + (&3, v3.public()), + (&4, v4.public()), + (&5, v5.public()), + (&6, v6.public()), + ], + Some(vec![ + (&0, v0.public()), + (&1, v1.public()), + (&2, v2.public()), + (&3, v3.public()), + (&4, v4.public()), + (&5, v5.public()), + (&6, v6.public()), + ]), + )) + }); + + let candidate_hash = CandidateHash(sp_core::H256::repeat_byte(1)); + + // v0 votes for 3, v6 against. + let stmts = vec![DisputeStatementSet { + candidate_hash: candidate_hash.clone(), + session: start - 1, + statements: vec![ + ( + DisputeStatement::Valid(ValidDisputeStatementKind::Explicit), + ValidatorIndex(0), + v0.sign( + &ExplicitDisputeStatement { + valid: true, + candidate_hash: candidate_hash.clone(), + session: start - 1, + } + .signing_payload(), + ), + ), + ( + DisputeStatement::Invalid(InvalidDisputeStatementKind::Explicit), + ValidatorIndex(6), + v2.sign( + &ExplicitDisputeStatement { + valid: false, + candidate_hash: candidate_hash.clone(), + session: start - 1, + } + .signing_payload(), + ), + ), + ], + }]; + + let stmts = update_spam_slots(stmts); + assert_eq!(SpamSlots::<Test>::get(start - 1), Some(vec![1, 0, 0, 0, 0, 0, 1])); + + assert_ok!( + Pallet::<Test>::process_checked_multi_dispute_data(stmts), + vec![(9, candidate_hash.clone())], + ); + + // Run to timeout period + run_to_block(start + dispute_conclusion_by_time_out_period, |_| None); + assert_eq!(SpamSlots::<Test>::get(start - 1), Some(vec![1, 0, 0, 0, 0, 0, 1])); + + // Run to timeout + 1 in order to executive on_finalize(timeout) + run_to_block(start + dispute_conclusion_by_time_out_period + 1, |_| None); + assert_eq!(SpamSlots::<Test>::get(start - 1), Some(vec![0, 0, 0, 0, 0, 0, 0])); + assert_eq!( + PUNISH_VALIDATORS_INCONCLUSIVE.with(|r| r.borrow()[0].clone()), + (9, vec![ValidatorIndex(0), ValidatorIndex(6)]), + ); + }); +} + +// Test pruning works +#[test] +fn test_initializer_on_new_session() { + let dispute_period = 3; + + let mock_genesis_config = MockGenesisConfig { + configuration: crate::configuration::GenesisConfig { + config: HostConfiguration { dispute_period, ..Default::default() }, + ..Default::default() + }, + ..Default::default() + }; + + new_test_ext(mock_genesis_config).execute_with(|| { + let v0 = <ValidatorId as CryptoType>::Pair::generate().0; + + let candidate_hash = CandidateHash(sp_core::H256::repeat_byte(1)); + Pallet::<Test>::note_included(0, candidate_hash.clone(), 0); + Pallet::<Test>::note_included(1, candidate_hash.clone(), 1); + Pallet::<Test>::note_included(2, candidate_hash.clone(), 2); + Pallet::<Test>::note_included(3, candidate_hash.clone(), 3); + Pallet::<Test>::note_included(4, candidate_hash.clone(), 4); + Pallet::<Test>::note_included(5, candidate_hash.clone(), 5); + Pallet::<Test>::note_included(6, candidate_hash.clone(), 5); + + run_to_block(7, |b| { + // a new session at each block + Some((true, b, vec![(&0, v0.public())], Some(vec![(&0, v0.public())]))) + }); + + // current session is 7, + // we keep for dispute_period + 1 session and we remove in on_finalize + // thus we keep info for session 3, 4, 5, 6, 7. + assert_eq!(Included::<Test>::iter_prefix(0).count(), 0); + assert_eq!(Included::<Test>::iter_prefix(1).count(), 0); + assert_eq!(Included::<Test>::iter_prefix(2).count(), 0); + assert_eq!(Included::<Test>::iter_prefix(3).count(), 1); + assert_eq!(Included::<Test>::iter_prefix(4).count(), 1); + assert_eq!(Included::<Test>::iter_prefix(5).count(), 1); + assert_eq!(Included::<Test>::iter_prefix(6).count(), 1); + }); +} + +#[test] +fn test_provide_data_duplicate_error() { + new_test_ext(Default::default()).execute_with(|| { + let candidate_hash_1 = CandidateHash(sp_core::H256::repeat_byte(1)); + let candidate_hash_2 = CandidateHash(sp_core::H256::repeat_byte(2)); + + let mut stmts = vec![ + DisputeStatementSet { + candidate_hash: candidate_hash_2, + session: 2, + statements: vec![], + }, + DisputeStatementSet { + candidate_hash: candidate_hash_1, + session: 1, + statements: vec![], + }, + DisputeStatementSet { + candidate_hash: candidate_hash_2, + session: 2, + statements: vec![], + }, + ]; + + assert!(Pallet::<Test>::deduplicate_and_sort_dispute_data(&mut stmts).is_err()); + assert_eq!(stmts.len(), 2); + }) +} + +#[test] +fn test_provide_multi_dispute_is_providing() { + new_test_ext(Default::default()).execute_with(|| { + let v0 = <ValidatorId as CryptoType>::Pair::generate().0; + let v1 = <ValidatorId as CryptoType>::Pair::generate().0; + + run_to_block(3, |b| { + // a new session at each block + if b == 1 { + Some(( + true, + b, + vec![(&0, v0.public()), (&1, v1.public())], + Some(vec![(&0, v0.public()), (&1, v1.public())]), + )) + } else { + Some((true, b, vec![(&1, v1.public())], Some(vec![(&1, v1.public())]))) + } + }); + + let candidate_hash = CandidateHash(sp_core::H256::repeat_byte(1)); + let stmts = vec![DisputeStatementSet { + candidate_hash: candidate_hash.clone(), + session: 1, + statements: vec![ + ( + DisputeStatement::Valid(ValidDisputeStatementKind::Explicit), + ValidatorIndex(0), + v0.sign( + &ExplicitDisputeStatement { + valid: true, + candidate_hash: candidate_hash.clone(), + session: 1, + } + .signing_payload(), + ), + ), + ( + DisputeStatement::Invalid(InvalidDisputeStatementKind::Explicit), + ValidatorIndex(1), + v1.sign( + &ExplicitDisputeStatement { + valid: false, + candidate_hash: candidate_hash.clone(), + session: 1, + } + .signing_payload(), + ), + ), + ], + }]; + + assert_ok!( + Pallet::<Test>::process_checked_multi_dispute_data( + stmts + .into_iter() + .map(CheckedDisputeStatementSet::unchecked_from_unchecked) + .collect() + ), + vec![(1, candidate_hash.clone())], + ); + }) +} + +#[test] +fn test_freeze_on_note_included() { + new_test_ext(Default::default()).execute_with(|| { + let v0 = <ValidatorId as CryptoType>::Pair::generate().0; + let v1 = <ValidatorId as CryptoType>::Pair::generate().0; + + run_to_block(6, |b| { + // a new session at each block + Some(( + true, + b, + vec![(&0, v0.public()), (&1, v1.public())], + Some(vec![(&0, v0.public()), (&1, v1.public())]), + )) + }); + + let candidate_hash = CandidateHash(sp_core::H256::repeat_byte(1)); + + // v0 votes for 3 + let stmts = vec![DisputeStatementSet { + candidate_hash: candidate_hash.clone(), + session: 3, + statements: vec![ + ( + DisputeStatement::Invalid(InvalidDisputeStatementKind::Explicit), + ValidatorIndex(0), + v0.sign( + &ExplicitDisputeStatement { + valid: false, + candidate_hash: candidate_hash.clone(), + session: 3, + } + .signing_payload(), + ), + ), + ( + DisputeStatement::Invalid(InvalidDisputeStatementKind::Explicit), + ValidatorIndex(1), + v1.sign( + &ExplicitDisputeStatement { + valid: false, + candidate_hash: candidate_hash.clone(), + session: 3, + } + .signing_payload(), + ), + ), + ( + DisputeStatement::Valid(ValidDisputeStatementKind::Explicit), + ValidatorIndex(1), + v1.sign( + &ExplicitDisputeStatement { + valid: true, + candidate_hash: candidate_hash.clone(), + session: 3, + } + .signing_payload(), + ), + ), + ], + }]; + assert!(Pallet::<Test>::process_checked_multi_dispute_data( + stmts + .into_iter() + .map(CheckedDisputeStatementSet::unchecked_from_unchecked) + .collect() + ) + .is_ok()); + + Pallet::<Test>::note_included(3, candidate_hash.clone(), 3); + assert_eq!(Frozen::<Test>::get(), Some(2)); + }); +} + +#[test] +fn test_freeze_provided_against_supermajority_for_included() { + new_test_ext(Default::default()).execute_with(|| { + let v0 = <ValidatorId as CryptoType>::Pair::generate().0; + let v1 = <ValidatorId as CryptoType>::Pair::generate().0; + + run_to_block(6, |b| { + // a new session at each block + Some(( + true, + b, + vec![(&0, v0.public()), (&1, v1.public())], + Some(vec![(&0, v0.public()), (&1, v1.public())]), + )) + }); + + let candidate_hash = CandidateHash(sp_core::H256::repeat_byte(1)); + + // v0 votes for 3 + let stmts = vec![DisputeStatementSet { + candidate_hash: candidate_hash.clone(), + session: 3, + statements: vec![ + ( + DisputeStatement::Invalid(InvalidDisputeStatementKind::Explicit), + ValidatorIndex(0), + v0.sign( + &ExplicitDisputeStatement { + valid: false, + candidate_hash: candidate_hash.clone(), + session: 3, + } + .signing_payload(), + ), + ), + ( + DisputeStatement::Invalid(InvalidDisputeStatementKind::Explicit), + ValidatorIndex(1), + v1.sign( + &ExplicitDisputeStatement { + valid: false, + candidate_hash: candidate_hash.clone(), + session: 3, + } + .signing_payload(), + ), + ), + ( + DisputeStatement::Valid(ValidDisputeStatementKind::Explicit), + ValidatorIndex(1), + v1.sign( + &ExplicitDisputeStatement { + valid: true, + candidate_hash: candidate_hash.clone(), + session: 3, + } + .signing_payload(), + ), + ), + ], + }]; + + Pallet::<Test>::note_included(3, candidate_hash.clone(), 3); + assert!(Pallet::<Test>::process_checked_multi_dispute_data( + stmts + .into_iter() + .map(CheckedDisputeStatementSet::unchecked_from_unchecked) + .collect() + ) + .is_ok()); + assert_eq!(Frozen::<Test>::get(), Some(2)); + }); +} + +// tests for: +// * provide_multi_dispute: with success scenario +// * disputes: correctness of datas +// * could_be_invalid: correctness of datas +// * note_included: decrement spam correctly +// * spam slots: correctly incremented and decremented +// * ensure rewards and punishment are correctly called. +#[test] +fn test_provide_multi_dispute_success_and_other() { + new_test_ext(Default::default()).execute_with(|| { + // 7 validators needed for byzantine threshold of 2. + let v0 = <ValidatorId as CryptoType>::Pair::generate().0; + let v1 = <ValidatorId as CryptoType>::Pair::generate().0; + let v2 = <ValidatorId as CryptoType>::Pair::generate().0; + let v3 = <ValidatorId as CryptoType>::Pair::generate().0; + let v4 = <ValidatorId as CryptoType>::Pair::generate().0; + let v5 = <ValidatorId as CryptoType>::Pair::generate().0; + let v6 = <ValidatorId as CryptoType>::Pair::generate().0; + + // v0 -> 0 + // v1 -> 3 + // v2 -> 6 + // v3 -> 5 + // v4 -> 1 + // v5 -> 4 + // v6 -> 2 + + run_to_block(6, |b| { + // a new session at each block + Some(( + true, + b, + vec![ + (&0, v0.public()), + (&1, v1.public()), + (&2, v2.public()), + (&3, v3.public()), + (&4, v4.public()), + (&5, v5.public()), + (&6, v6.public()), + ], + Some(vec![ + (&0, v0.public()), + (&1, v1.public()), + (&2, v2.public()), + (&3, v3.public()), + (&4, v4.public()), + (&5, v5.public()), + (&6, v6.public()), + ]), + )) + }); + + let candidate_hash = CandidateHash(sp_core::H256::repeat_byte(1)); + + // v0 votes for 3, v6 votes against + let stmts = vec![DisputeStatementSet { + candidate_hash: candidate_hash.clone(), + session: 3, + statements: vec![ + ( + DisputeStatement::Valid(ValidDisputeStatementKind::Explicit), + ValidatorIndex(0), + v0.sign( + &ExplicitDisputeStatement { + valid: true, + candidate_hash: candidate_hash.clone(), + session: 3, + } + .signing_payload(), + ), + ), + ( + DisputeStatement::Invalid(InvalidDisputeStatementKind::Explicit), + ValidatorIndex(2), + v6.sign( + &ExplicitDisputeStatement { + valid: false, + candidate_hash: candidate_hash.clone(), + session: 3, + } + .signing_payload(), + ), + ), + ], + }]; + + let stmts = update_spam_slots(stmts); + assert_eq!(SpamSlots::<Test>::get(3), Some(vec![1, 0, 1, 0, 0, 0, 0])); + + assert_ok!( + Pallet::<Test>::process_checked_multi_dispute_data(stmts), + vec![(3, candidate_hash.clone())], + ); + + // v1 votes for 4 and for 3, v6 votes against 4. + let stmts = vec![ + DisputeStatementSet { + candidate_hash: candidate_hash.clone(), + session: 4, + statements: vec![ + ( + DisputeStatement::Valid(ValidDisputeStatementKind::Explicit), + ValidatorIndex(3), + v1.sign( + &ExplicitDisputeStatement { + valid: true, + candidate_hash: candidate_hash.clone(), + session: 4, + } + .signing_payload(), + ), + ), + ( + DisputeStatement::Invalid(InvalidDisputeStatementKind::Explicit), + ValidatorIndex(2), + v6.sign( + &ExplicitDisputeStatement { + valid: false, + candidate_hash: candidate_hash.clone(), + session: 4, + } + .signing_payload(), + ), + ), + ], + }, + DisputeStatementSet { + candidate_hash: candidate_hash.clone(), + session: 3, + statements: vec![( + DisputeStatement::Valid(ValidDisputeStatementKind::Explicit), + ValidatorIndex(3), + v1.sign( + &ExplicitDisputeStatement { + valid: true, + candidate_hash: candidate_hash.clone(), + session: 3, + } + .signing_payload(), + ), + )], + }, + ]; + + let stmts = update_spam_slots(stmts); + + assert_ok!( + Pallet::<Test>::process_checked_multi_dispute_data(stmts), + vec![(4, candidate_hash.clone())], + ); + assert_eq!(SpamSlots::<Test>::get(3), Some(vec![0, 0, 0, 0, 0, 0, 0])); // Confirmed as no longer spam + assert_eq!(SpamSlots::<Test>::get(4), Some(vec![0, 0, 1, 1, 0, 0, 0])); + + // v3 votes against 3 and for 5, v6 votes against 5. + let stmts = vec![ + DisputeStatementSet { + candidate_hash: candidate_hash.clone(), + session: 3, + statements: vec![( + DisputeStatement::Invalid(InvalidDisputeStatementKind::Explicit), + ValidatorIndex(5), + v3.sign( + &ExplicitDisputeStatement { + valid: false, + candidate_hash: candidate_hash.clone(), + session: 3, + } + .signing_payload(), + ), + )], + }, + DisputeStatementSet { + candidate_hash: candidate_hash.clone(), + session: 5, + statements: vec![ + ( + DisputeStatement::Valid(ValidDisputeStatementKind::Explicit), + ValidatorIndex(5), + v3.sign( + &ExplicitDisputeStatement { + valid: true, + candidate_hash: candidate_hash.clone(), + session: 5, + } + .signing_payload(), + ), + ), + ( + DisputeStatement::Invalid(InvalidDisputeStatementKind::Explicit), + ValidatorIndex(2), + v6.sign( + &ExplicitDisputeStatement { + valid: false, + candidate_hash: candidate_hash.clone(), + session: 5, + } + .signing_payload(), + ), + ), + ], + }, + ]; + + let stmts = update_spam_slots(stmts); + assert_ok!( + Pallet::<Test>::process_checked_multi_dispute_data(stmts), + vec![(5, candidate_hash.clone())], + ); + assert_eq!(SpamSlots::<Test>::get(3), Some(vec![0, 0, 0, 0, 0, 0, 0])); + assert_eq!(SpamSlots::<Test>::get(4), Some(vec![0, 0, 1, 1, 0, 0, 0])); + assert_eq!(SpamSlots::<Test>::get(5), Some(vec![0, 0, 1, 0, 0, 1, 0])); + + // v2 votes for 3 and against 5 + let stmts = vec![ + DisputeStatementSet { + candidate_hash: candidate_hash.clone(), + session: 3, + statements: vec![( + DisputeStatement::Valid(ValidDisputeStatementKind::Explicit), + ValidatorIndex(6), + v2.sign( + &ExplicitDisputeStatement { + valid: true, + candidate_hash: candidate_hash.clone(), + session: 3, + } + .signing_payload(), + ), + )], + }, + DisputeStatementSet { + candidate_hash: candidate_hash.clone(), + session: 5, + statements: vec![( + DisputeStatement::Invalid(InvalidDisputeStatementKind::Explicit), + ValidatorIndex(6), + v2.sign( + &ExplicitDisputeStatement { + valid: false, + candidate_hash: candidate_hash.clone(), + session: 5, + } + .signing_payload(), + ), + )], + }, + ]; + let stmts = update_spam_slots(stmts); + assert_ok!(Pallet::<Test>::process_checked_multi_dispute_data(stmts), vec![]); + assert_eq!(SpamSlots::<Test>::get(3), Some(vec![0, 0, 0, 0, 0, 0, 0])); + assert_eq!(SpamSlots::<Test>::get(4), Some(vec![0, 0, 1, 1, 0, 0, 0])); + assert_eq!(SpamSlots::<Test>::get(5), Some(vec![0, 0, 0, 0, 0, 0, 0])); + + let stmts = vec![ + // 0, 4, and 5 vote against 5 + DisputeStatementSet { + candidate_hash: candidate_hash.clone(), + session: 5, + statements: vec![ + ( + DisputeStatement::Invalid(InvalidDisputeStatementKind::Explicit), + ValidatorIndex(0), + v0.sign( + &ExplicitDisputeStatement { + valid: false, + candidate_hash: candidate_hash.clone(), + session: 5, + } + .signing_payload(), + ), + ), + ( + DisputeStatement::Invalid(InvalidDisputeStatementKind::Explicit), + ValidatorIndex(1), + v4.sign( + &ExplicitDisputeStatement { + valid: false, + candidate_hash: candidate_hash.clone(), + session: 5, + } + .signing_payload(), + ), + ), + ( + DisputeStatement::Invalid(InvalidDisputeStatementKind::Explicit), + ValidatorIndex(4), + v5.sign( + &ExplicitDisputeStatement { + valid: false, + candidate_hash: candidate_hash.clone(), + session: 5, + } + .signing_payload(), + ), + ), + ], + }, + // 4 and 5 vote for 3 + DisputeStatementSet { + candidate_hash: candidate_hash.clone(), + session: 3, + statements: vec![ + ( + DisputeStatement::Valid(ValidDisputeStatementKind::Explicit), + ValidatorIndex(1), + v4.sign( + &ExplicitDisputeStatement { + valid: true, + candidate_hash: candidate_hash.clone(), + session: 3, + } + .signing_payload(), + ), + ), + ( + DisputeStatement::Valid(ValidDisputeStatementKind::Explicit), + ValidatorIndex(4), + v5.sign( + &ExplicitDisputeStatement { + valid: true, + candidate_hash: candidate_hash.clone(), + session: 3, + } + .signing_payload(), + ), + ), + ], + }, + ]; + let stmts = update_spam_slots(stmts); + assert_ok!(Pallet::<Test>::process_checked_multi_dispute_data(stmts), vec![]); + + assert_eq!( + Pallet::<Test>::disputes(), + vec![ + ( + 5, + candidate_hash.clone(), + DisputeState { + validators_for: bitvec![BitOrderLsb0, u8; 0, 0, 0, 0, 0, 1, 0], + validators_against: bitvec![BitOrderLsb0, u8; 1, 1, 1, 0, 1, 0, 1], + start: 6, + concluded_at: Some(6), // 5 vote against + } + ), + ( + 3, + candidate_hash.clone(), + DisputeState { + validators_for: bitvec![BitOrderLsb0, u8; 1, 1, 0, 1, 1, 0, 1], + validators_against: bitvec![BitOrderLsb0, u8; 0, 0, 1, 0, 0, 1, 0], + start: 6, + concluded_at: Some(6), // 5 vote for + } + ), + ( + 4, + candidate_hash.clone(), + DisputeState { + validators_for: bitvec![BitOrderLsb0, u8; 0, 0, 0, 1, 0, 0, 0], + validators_against: bitvec![BitOrderLsb0, u8; 0, 0, 1, 0, 0, 0, 0], + start: 6, + concluded_at: None, + } + ), + ] + ); + + assert!(!Pallet::<Test>::concluded_invalid(3, candidate_hash.clone())); + assert!(!Pallet::<Test>::concluded_invalid(4, candidate_hash.clone())); + assert!(Pallet::<Test>::concluded_invalid(5, candidate_hash.clone())); + + // Ensure inclusion removes spam slots + assert_eq!(SpamSlots::<Test>::get(4), Some(vec![0, 0, 1, 1, 0, 0, 0])); + Pallet::<Test>::note_included(4, candidate_hash.clone(), 4); + assert_eq!(SpamSlots::<Test>::get(4), Some(vec![0, 0, 0, 0, 0, 0, 0])); + + // Ensure the `reward_validator` function was correctly called + assert_eq!( + REWARD_VALIDATORS.with(|r| r.borrow().clone()), + vec![ + (3, vec![ValidatorIndex(0), ValidatorIndex(2)]), + (4, vec![ValidatorIndex(2), ValidatorIndex(3)]), + (3, vec![ValidatorIndex(3)]), + (3, vec![ValidatorIndex(5)]), + (5, vec![ValidatorIndex(2), ValidatorIndex(5)]), + (3, vec![ValidatorIndex(6)]), + (5, vec![ValidatorIndex(6)]), + (5, vec![ValidatorIndex(0), ValidatorIndex(1), ValidatorIndex(4)]), + (3, vec![ValidatorIndex(1), ValidatorIndex(4)]), + ], + ); + + // Ensure punishment against is called + assert_eq!( + PUNISH_VALIDATORS_AGAINST.with(|r| r.borrow().clone()), + vec![ + (3, vec![]), + (4, vec![]), + (3, vec![]), + (3, vec![]), + (5, vec![]), + (3, vec![]), + (5, vec![]), + (5, vec![]), + (3, vec![ValidatorIndex(2), ValidatorIndex(5)]), + ], + ); + + // Ensure punishment for is called + assert_eq!( + PUNISH_VALIDATORS_FOR.with(|r| r.borrow().clone()), + vec![ + (3, vec![]), + (4, vec![]), + (3, vec![]), + (3, vec![]), + (5, vec![]), + (3, vec![]), + (5, vec![]), + (5, vec![ValidatorIndex(5)]), + (3, vec![]), + ], + ); + }) +} + +#[test] +fn test_revert_and_freeze() { + new_test_ext(Default::default()).execute_with(|| { + // events are ignored for genesis block + System::set_block_number(1); + + Frozen::<Test>::put(Some(0)); + assert_noop!( + { + Pallet::<Test>::revert_and_freeze(0); + Result::<(), ()>::Err(()) // Just a small trick in order to use `assert_noop`. + }, + (), + ); + + Frozen::<Test>::kill(); + Pallet::<Test>::revert_and_freeze(0); + + assert_eq!(Frozen::<Test>::get(), Some(0)); + assert_eq!(System::digest().logs[0], ConsensusLog::Revert(1).into()); + System::assert_has_event(Event::Revert(1).into()); + }) +} + +#[test] +fn test_revert_and_freeze_merges() { + new_test_ext(Default::default()).execute_with(|| { + Frozen::<Test>::put(Some(10)); + assert_noop!( + { + Pallet::<Test>::revert_and_freeze(10); + Result::<(), ()>::Err(()) // Just a small trick in order to use `assert_noop`. + }, + (), + ); + + Pallet::<Test>::revert_and_freeze(8); + assert_eq!(Frozen::<Test>::get(), Some(8)); + }) +} + +#[test] +fn test_has_supermajority_against() { + assert_eq!( + has_supermajority_against(&DisputeState { + validators_for: bitvec![BitOrderLsb0, u8; 1, 1, 0, 0, 0, 0, 0, 0], + validators_against: bitvec![BitOrderLsb0, u8; 1, 1, 1, 1, 1, 0, 0, 0], + start: 0, + concluded_at: None, + }), + false, + ); + + assert_eq!( + has_supermajority_against(&DisputeState { + validators_for: bitvec![BitOrderLsb0, u8; 1, 1, 0, 0, 0, 0, 0, 0], + validators_against: bitvec![BitOrderLsb0, u8; 1, 1, 1, 1, 1, 1, 0, 0], + start: 0, + concluded_at: None, + }), + true, + ); +} + +#[test] +fn test_decrement_spam() { + let original_spam_slots = vec![0, 1, 2, 3, 4, 5, 6, 7]; + + // Test confirm is no-op + let mut spam_slots = original_spam_slots.clone(); + let dispute_state_confirm = DisputeState { + validators_for: bitvec![BitOrderLsb0, u8; 1, 1, 0, 0, 0, 0, 0, 0], + validators_against: bitvec![BitOrderLsb0, u8; 1, 0, 1, 0, 0, 0, 0, 0], + start: 0, + concluded_at: None, + }; + assert_eq!(DisputeStateFlags::from_state(&dispute_state_confirm), DisputeStateFlags::CONFIRMED); + assert_eq!( + decrement_spam(spam_slots.as_mut(), &dispute_state_confirm), + bitvec![BitOrderLsb0, u8; 1, 1, 1, 0, 0, 0, 0, 0], + ); + assert_eq!(spam_slots, original_spam_slots); + + // Test not confirm is decreasing spam + let mut spam_slots = original_spam_slots.clone(); + let dispute_state_no_confirm = DisputeState { + validators_for: bitvec![BitOrderLsb0, u8; 1, 0, 0, 0, 0, 0, 0, 0], + validators_against: bitvec![BitOrderLsb0, u8; 1, 0, 1, 0, 0, 0, 0, 0], + start: 0, + concluded_at: None, + }; + assert_eq!( + DisputeStateFlags::from_state(&dispute_state_no_confirm), + DisputeStateFlags::default() + ); + assert_eq!( + decrement_spam(spam_slots.as_mut(), &dispute_state_no_confirm), + bitvec![BitOrderLsb0, u8; 1, 0, 1, 0, 0, 0, 0, 0], + ); + assert_eq!(spam_slots, vec![0, 1, 1, 3, 4, 5, 6, 7]); +} + +#[test] +fn test_check_signature() { + let validator_id = <ValidatorId as CryptoType>::Pair::generate().0; + let wrong_validator_id = <ValidatorId as CryptoType>::Pair::generate().0; + + let session = 0; + let wrong_session = 1; + let candidate_hash = CandidateHash(sp_core::H256::repeat_byte(1)); + let wrong_candidate_hash = CandidateHash(sp_core::H256::repeat_byte(2)); + let inclusion_parent = sp_core::H256::repeat_byte(3); + let wrong_inclusion_parent = sp_core::H256::repeat_byte(4); + + let statement_1 = DisputeStatement::Valid(ValidDisputeStatementKind::Explicit); + let statement_2 = DisputeStatement::Valid(ValidDisputeStatementKind::BackingSeconded( + inclusion_parent.clone(), + )); + let wrong_statement_2 = DisputeStatement::Valid(ValidDisputeStatementKind::BackingSeconded( + wrong_inclusion_parent.clone(), + )); + let statement_3 = + DisputeStatement::Valid(ValidDisputeStatementKind::BackingValid(inclusion_parent.clone())); + let wrong_statement_3 = DisputeStatement::Valid(ValidDisputeStatementKind::BackingValid( + wrong_inclusion_parent.clone(), + )); + let statement_4 = DisputeStatement::Valid(ValidDisputeStatementKind::ApprovalChecking); + let statement_5 = DisputeStatement::Invalid(InvalidDisputeStatementKind::Explicit); + + let signed_1 = validator_id.sign( + &ExplicitDisputeStatement { valid: true, candidate_hash: candidate_hash.clone(), session } + .signing_payload(), + ); + let signed_2 = + validator_id.sign(&CompactStatement::Seconded(candidate_hash.clone()).signing_payload( + &SigningContext { session_index: session, parent_hash: inclusion_parent.clone() }, + )); + let signed_3 = + validator_id.sign(&CompactStatement::Valid(candidate_hash.clone()).signing_payload( + &SigningContext { session_index: session, parent_hash: inclusion_parent.clone() }, + )); + let signed_4 = + validator_id.sign(&ApprovalVote(candidate_hash.clone()).signing_payload(session)); + let signed_5 = validator_id.sign( + &ExplicitDisputeStatement { valid: false, candidate_hash: candidate_hash.clone(), session } + .signing_payload(), + ); + + assert!(check_signature( + &validator_id.public(), + candidate_hash, + session, + &statement_1, + &signed_1 + ) + .is_ok()); + assert!(check_signature( + &wrong_validator_id.public(), + candidate_hash, + session, + &statement_1, + &signed_1 + ) + .is_err()); + assert!(check_signature( + &validator_id.public(), + wrong_candidate_hash, + session, + &statement_1, + &signed_1 + ) + .is_err()); + assert!(check_signature( + &validator_id.public(), + candidate_hash, + wrong_session, + &statement_1, + &signed_1 + ) + .is_err()); + assert!(check_signature( + &validator_id.public(), + candidate_hash, + session, + &statement_2, + &signed_1 + ) + .is_err()); + assert!(check_signature( + &validator_id.public(), + candidate_hash, + session, + &statement_3, + &signed_1 + ) + .is_err()); + assert!(check_signature( + &validator_id.public(), + candidate_hash, + session, + &statement_4, + &signed_1 + ) + .is_err()); + assert!(check_signature( + &validator_id.public(), + candidate_hash, + session, + &statement_5, + &signed_1 + ) + .is_err()); + + assert!(check_signature( + &validator_id.public(), + candidate_hash, + session, + &statement_2, + &signed_2 + ) + .is_ok()); + assert!(check_signature( + &wrong_validator_id.public(), + candidate_hash, + session, + &statement_2, + &signed_2 + ) + .is_err()); + assert!(check_signature( + &validator_id.public(), + wrong_candidate_hash, + session, + &statement_2, + &signed_2 + ) + .is_err()); + assert!(check_signature( + &validator_id.public(), + candidate_hash, + wrong_session, + &statement_2, + &signed_2 + ) + .is_err()); + assert!(check_signature( + &validator_id.public(), + candidate_hash, + session, + &wrong_statement_2, + &signed_2 + ) + .is_err()); + assert!(check_signature( + &validator_id.public(), + candidate_hash, + session, + &statement_1, + &signed_2 + ) + .is_err()); + assert!(check_signature( + &validator_id.public(), + candidate_hash, + session, + &statement_3, + &signed_2 + ) + .is_err()); + assert!(check_signature( + &validator_id.public(), + candidate_hash, + session, + &statement_4, + &signed_2 + ) + .is_err()); + assert!(check_signature( + &validator_id.public(), + candidate_hash, + session, + &statement_5, + &signed_2 + ) + .is_err()); + + assert!(check_signature( + &validator_id.public(), + candidate_hash, + session, + &statement_3, + &signed_3 + ) + .is_ok()); + assert!(check_signature( + &wrong_validator_id.public(), + candidate_hash, + session, + &statement_3, + &signed_3 + ) + .is_err()); + assert!(check_signature( + &validator_id.public(), + wrong_candidate_hash, + session, + &statement_3, + &signed_3 + ) + .is_err()); + assert!(check_signature( + &validator_id.public(), + candidate_hash, + wrong_session, + &statement_3, + &signed_3 + ) + .is_err()); + assert!(check_signature( + &validator_id.public(), + candidate_hash, + session, + &wrong_statement_3, + &signed_3 + ) + .is_err()); + assert!(check_signature( + &validator_id.public(), + candidate_hash, + session, + &statement_1, + &signed_3 + ) + .is_err()); + assert!(check_signature( + &validator_id.public(), + candidate_hash, + session, + &statement_2, + &signed_3 + ) + .is_err()); + assert!(check_signature( + &validator_id.public(), + candidate_hash, + session, + &statement_4, + &signed_3 + ) + .is_err()); + assert!(check_signature( + &validator_id.public(), + candidate_hash, + session, + &statement_5, + &signed_3 + ) + .is_err()); + + assert!(check_signature( + &validator_id.public(), + candidate_hash, + session, + &statement_4, + &signed_4 + ) + .is_ok()); + assert!(check_signature( + &wrong_validator_id.public(), + candidate_hash, + session, + &statement_4, + &signed_4 + ) + .is_err()); + assert!(check_signature( + &validator_id.public(), + wrong_candidate_hash, + session, + &statement_4, + &signed_4 + ) + .is_err()); + assert!(check_signature( + &validator_id.public(), + candidate_hash, + wrong_session, + &statement_4, + &signed_4 + ) + .is_err()); + assert!(check_signature( + &validator_id.public(), + candidate_hash, + session, + &statement_1, + &signed_4 + ) + .is_err()); + assert!(check_signature( + &validator_id.public(), + candidate_hash, + session, + &statement_2, + &signed_4 + ) + .is_err()); + assert!(check_signature( + &validator_id.public(), + candidate_hash, + session, + &statement_3, + &signed_4 + ) + .is_err()); + assert!(check_signature( + &validator_id.public(), + candidate_hash, + session, + &statement_5, + &signed_4 + ) + .is_err()); + + assert!(check_signature( + &validator_id.public(), + candidate_hash, + session, + &statement_5, + &signed_5 + ) + .is_ok()); + assert!(check_signature( + &wrong_validator_id.public(), + candidate_hash, + session, + &statement_5, + &signed_5 + ) + .is_err()); + assert!(check_signature( + &validator_id.public(), + wrong_candidate_hash, + session, + &statement_5, + &signed_5 + ) + .is_err()); + assert!(check_signature( + &validator_id.public(), + candidate_hash, + wrong_session, + &statement_5, + &signed_5 + ) + .is_err()); + assert!(check_signature( + &validator_id.public(), + candidate_hash, + session, + &statement_1, + &signed_5 + ) + .is_err()); + assert!(check_signature( + &validator_id.public(), + candidate_hash, + session, + &statement_2, + &signed_5 + ) + .is_err()); + assert!(check_signature( + &validator_id.public(), + candidate_hash, + session, + &statement_3, + &signed_5 + ) + .is_err()); + assert!(check_signature( + &validator_id.public(), + candidate_hash, + session, + &statement_4, + &signed_5 + ) + .is_err()); +} + +#[test] +fn deduplication_and_sorting_works() { + new_test_ext(Default::default()).execute_with(|| { + let v0 = <ValidatorId as CryptoType>::Pair::generate().0; + let v1 = <ValidatorId as CryptoType>::Pair::generate().0; + let v2 = <ValidatorId as CryptoType>::Pair::generate().0; + let v3 = <ValidatorId as CryptoType>::Pair::generate().0; + + run_to_block(3, |b| { + // a new session at each block + Some(( + true, + b, + vec![(&0, v0.public()), (&1, v1.public()), (&2, v2.public()), (&3, v3.public())], + Some(vec![ + (&0, v0.public()), + (&1, v1.public()), + (&2, v2.public()), + (&3, v3.public()), + ]), + )) + }); + + let candidate_hash_a = CandidateHash(sp_core::H256::repeat_byte(1)); + let candidate_hash_b = CandidateHash(sp_core::H256::repeat_byte(2)); + let candidate_hash_c = CandidateHash(sp_core::H256::repeat_byte(3)); + + let create_explicit_statement = |vidx: ValidatorIndex, + validator: &<ValidatorId as CryptoType>::Pair, + c_hash: &CandidateHash, + valid, + session| { + let payload = + ExplicitDisputeStatement { valid, candidate_hash: c_hash.clone(), session } + .signing_payload(); + let sig = validator.sign(&payload); + (DisputeStatement::Valid(ValidDisputeStatementKind::Explicit), vidx, sig.clone()) + }; + + let explicit_triple_a = + create_explicit_statement(ValidatorIndex(0), &v0, &candidate_hash_a, true, 1); + let explicit_triple_a_bad = + create_explicit_statement(ValidatorIndex(1), &v1, &candidate_hash_a, false, 1); + + let explicit_triple_b = + create_explicit_statement(ValidatorIndex(0), &v0, &candidate_hash_b, true, 2); + let explicit_triple_b_bad = + create_explicit_statement(ValidatorIndex(1), &v1, &candidate_hash_b, false, 2); + + let explicit_triple_c = + create_explicit_statement(ValidatorIndex(0), &v0, &candidate_hash_c, true, 2); + let explicit_triple_c_bad = + create_explicit_statement(ValidatorIndex(1), &v1, &candidate_hash_c, false, 2); + + let mut disputes = vec![ + DisputeStatementSet { + candidate_hash: candidate_hash_b.clone(), + session: 2, + statements: vec![explicit_triple_b.clone(), explicit_triple_b_bad.clone()], + }, + // same session as above + DisputeStatementSet { + candidate_hash: candidate_hash_c.clone(), + session: 2, + statements: vec![explicit_triple_c, explicit_triple_c_bad], + }, + // the duplicate set + DisputeStatementSet { + candidate_hash: candidate_hash_b.clone(), + session: 2, + statements: vec![explicit_triple_b.clone(), explicit_triple_b_bad.clone()], + }, + DisputeStatementSet { + candidate_hash: candidate_hash_a.clone(), + session: 1, + statements: vec![explicit_triple_a, explicit_triple_a_bad], + }, + ]; + + let disputes_orig = disputes.clone(); + + <Pallet<Test> as DisputesHandler< + <Test as frame_system::Config>::BlockNumber, + >>::deduplicate_and_sort_dispute_data(&mut disputes).unwrap_err(); + + // assert ordering of local only disputes, and at the same time, and being free of duplicates + assert_eq!(disputes_orig.len(), disputes.len() + 1); + + let are_these_equal = |a: &DisputeStatementSet, b: &DisputeStatementSet| { + use core::cmp::Ordering; + // we only have local disputes here, so sorting of those adheres to the + // simplified sorting logic + let cmp = + a.session.cmp(&b.session).then_with(|| a.candidate_hash.cmp(&b.candidate_hash)); + assert_ne!(cmp, Ordering::Greater); + cmp == Ordering::Equal + }; + + assert_eq!(false, contains_duplicates_in_sorted_iter(&disputes, are_these_equal)); + }) +} + +fn apply_filter_all<T: Config, I: IntoIterator<Item = DisputeStatementSet>>( + sets: I, +) -> Vec<CheckedDisputeStatementSet> { + let config = <configuration::Pallet<T>>::config(); + let max_spam_slots = config.dispute_max_spam_slots; + let post_conclusion_acceptance_period = config.dispute_post_conclusion_acceptance_period; + + let mut acc = Vec::<CheckedDisputeStatementSet>::new(); + for dispute_statement in sets { + if let Some(checked) = <Pallet<T> as DisputesHandler<<T>::BlockNumber>>::filter_dispute_data( + dispute_statement, + max_spam_slots, + post_conclusion_acceptance_period, + VerifyDisputeSignatures::Yes, + ) { + acc.push(checked); + } + } + acc +} + +#[test] +fn filter_removes_duplicates_within_set() { + new_test_ext(Default::default()).execute_with(|| { + let v0 = <ValidatorId as CryptoType>::Pair::generate().0; + let v1 = <ValidatorId as CryptoType>::Pair::generate().0; + + run_to_block(3, |b| { + // a new session at each block + Some(( + true, + b, + vec![(&0, v0.public()), (&1, v1.public())], + Some(vec![(&0, v0.public()), (&1, v1.public())]), + )) + }); + + let candidate_hash = CandidateHash(sp_core::H256::repeat_byte(1)); + + let payload = ExplicitDisputeStatement { + valid: true, + candidate_hash: candidate_hash.clone(), + session: 1, + } + .signing_payload(); + + let payload_against = ExplicitDisputeStatement { + valid: false, + candidate_hash: candidate_hash.clone(), + session: 1, + } + .signing_payload(); + + let sig_a = v0.sign(&payload); + let sig_b = v0.sign(&payload); + let sig_c = v0.sign(&payload); + let sig_d = v1.sign(&payload_against); + + let statements = DisputeStatementSet { + candidate_hash: candidate_hash.clone(), + session: 1, + statements: vec![ + ( + DisputeStatement::Valid(ValidDisputeStatementKind::Explicit), + ValidatorIndex(0), + sig_a.clone(), + ), + ( + DisputeStatement::Valid(ValidDisputeStatementKind::Explicit), + ValidatorIndex(0), + sig_b, + ), + ( + DisputeStatement::Valid(ValidDisputeStatementKind::Explicit), + ValidatorIndex(0), + sig_c, + ), + ( + DisputeStatement::Invalid(InvalidDisputeStatementKind::Explicit), + ValidatorIndex(1), + sig_d.clone(), + ), + ], + }; + + let max_spam_slots = 10; + let post_conclusion_acceptance_period = 10; + let statements = <Pallet<Test> as DisputesHandler< + <Test as frame_system::Config>::BlockNumber, + >>::filter_dispute_data( + statements, + max_spam_slots, + post_conclusion_acceptance_period, + VerifyDisputeSignatures::Yes, + ); + + assert_eq!( + statements, + Some(CheckedDisputeStatementSet::unchecked_from_unchecked(DisputeStatementSet { + candidate_hash: candidate_hash.clone(), + session: 1, + statements: vec![ + ( + DisputeStatement::Valid(ValidDisputeStatementKind::Explicit), + ValidatorIndex(0), + sig_a, + ), + ( + DisputeStatement::Invalid(InvalidDisputeStatementKind::Explicit), + ValidatorIndex(1), + sig_d, + ), + ] + })) + ); + }) +} + +#[test] +fn filter_bad_signatures_correctly_detects_single_sided() { + new_test_ext(Default::default()).execute_with(|| { + let v0 = <ValidatorId as CryptoType>::Pair::generate().0; + let v1 = <ValidatorId as CryptoType>::Pair::generate().0; + let v2 = <ValidatorId as CryptoType>::Pair::generate().0; + let v3 = <ValidatorId as CryptoType>::Pair::generate().0; + + run_to_block(3, |b| { + // a new session at each block + Some(( + true, + b, + vec![(&0, v0.public()), (&1, v1.public()), (&2, v2.public()), (&3, v3.public())], + Some(vec![ + (&0, v0.public()), + (&1, v1.public()), + (&2, v2.public()), + (&3, v3.public()), + ]), + )) + }); + + let candidate_hash_a = CandidateHash(sp_core::H256::repeat_byte(1)); + + let payload = |c_hash: &CandidateHash, valid| { + ExplicitDisputeStatement { valid, candidate_hash: c_hash.clone(), session: 1 } + .signing_payload() + }; + + let payload_a = payload(&candidate_hash_a, true); + let payload_a_bad = payload(&candidate_hash_a, false); + + let sig_0 = v0.sign(&payload_a); + let sig_1 = v1.sign(&payload_a_bad); + + let statements = vec![DisputeStatementSet { + candidate_hash: candidate_hash_a.clone(), + session: 1, + statements: vec![ + ( + DisputeStatement::Valid(ValidDisputeStatementKind::Explicit), + ValidatorIndex(0), + sig_0.clone(), + ), + ( + DisputeStatement::Invalid(InvalidDisputeStatementKind::Explicit), + ValidatorIndex(2), + sig_1.clone(), + ), + ], + }]; + + let statements = apply_filter_all::<Test, _>(statements); + + assert!(statements.is_empty()); + }) +} + +#[test] +fn filter_correctly_accounts_spam_slots() { + let dispute_max_spam_slots = 2; + + let mock_genesis_config = MockGenesisConfig { + configuration: crate::configuration::GenesisConfig { + config: HostConfiguration { dispute_max_spam_slots, ..Default::default() }, + ..Default::default() + }, + ..Default::default() + }; + + new_test_ext(mock_genesis_config).execute_with(|| { + // We need 7 validators for the byzantine threshold to be 2 + let v0 = <ValidatorId as CryptoType>::Pair::generate().0; + let v1 = <ValidatorId as CryptoType>::Pair::generate().0; + let v2 = <ValidatorId as CryptoType>::Pair::generate().0; + let v3 = <ValidatorId as CryptoType>::Pair::generate().0; + let v4 = <ValidatorId as CryptoType>::Pair::generate().0; + let v5 = <ValidatorId as CryptoType>::Pair::generate().0; + let v6 = <ValidatorId as CryptoType>::Pair::generate().0; + + run_to_block(3, |b| { + // a new session at each block + Some(( + true, + b, + vec![ + (&0, v0.public()), + (&1, v1.public()), + (&2, v2.public()), + (&3, v3.public()), + (&4, v4.public()), + (&5, v5.public()), + (&6, v6.public()), + ], + Some(vec![ + (&0, v0.public()), + (&1, v1.public()), + (&2, v2.public()), + (&3, v3.public()), + (&4, v4.public()), + (&5, v5.public()), + (&6, v6.public()), + ]), + )) + }); + + let candidate_hash_a = CandidateHash(sp_core::H256::repeat_byte(1)); + let candidate_hash_b = CandidateHash(sp_core::H256::repeat_byte(2)); + let candidate_hash_c = CandidateHash(sp_core::H256::repeat_byte(3)); + + let payload = |c_hash: &CandidateHash, valid| { + ExplicitDisputeStatement { valid, candidate_hash: c_hash.clone(), session: 1 } + .signing_payload() + }; + + let payload_a = payload(&candidate_hash_a, true); + let payload_b = payload(&candidate_hash_b, true); + let payload_c = payload(&candidate_hash_c, true); + + let payload_a_bad = payload(&candidate_hash_a, false); + let payload_b_bad = payload(&candidate_hash_b, false); + let payload_c_bad = payload(&candidate_hash_c, false); + + let sig_0a = v0.sign(&payload_a); + let sig_0b = v0.sign(&payload_b); + let sig_0c = v0.sign(&payload_c); + + let sig_1b = v1.sign(&payload_b); + + let sig_2a = v2.sign(&payload_a_bad); + let sig_2b = v2.sign(&payload_b_bad); + let sig_2c = v2.sign(&payload_c_bad); + + let statements = vec![ + // validators 0 and 2 get 1 spam slot from this. + DisputeStatementSet { + candidate_hash: candidate_hash_a.clone(), + session: 1, + statements: vec![ + ( + DisputeStatement::Valid(ValidDisputeStatementKind::Explicit), + ValidatorIndex(0), + sig_0a.clone(), + ), + ( + DisputeStatement::Invalid(InvalidDisputeStatementKind::Explicit), + ValidatorIndex(6), + sig_2a.clone(), + ), + ], + }, + // Validators 0, 2, and 3 get no spam slots for this + DisputeStatementSet { + candidate_hash: candidate_hash_b.clone(), + session: 1, + statements: vec![ + ( + DisputeStatement::Valid(ValidDisputeStatementKind::Explicit), + ValidatorIndex(0), + sig_0b.clone(), + ), + ( + DisputeStatement::Valid(ValidDisputeStatementKind::Explicit), + ValidatorIndex(3), + sig_1b.clone(), + ), + ( + DisputeStatement::Invalid(InvalidDisputeStatementKind::Explicit), + ValidatorIndex(6), + sig_2b.clone(), + ), + ], + }, + // Validators 0 and 2 get an extra spam slot for this. + DisputeStatementSet { + candidate_hash: candidate_hash_c.clone(), + session: 1, + statements: vec![ + ( + DisputeStatement::Valid(ValidDisputeStatementKind::Explicit), + ValidatorIndex(0), + sig_0c.clone(), + ), + ( + DisputeStatement::Invalid(InvalidDisputeStatementKind::Explicit), + ValidatorIndex(6), + sig_2c.clone(), + ), + ], + }, + ]; + + let old_statements = statements + .clone() + .into_iter() + .map(CheckedDisputeStatementSet::unchecked_from_unchecked) + .collect::<Vec<_>>(); + let statements = apply_filter_all::<Test, _>(statements); + + assert_eq!(statements, old_statements); + }) +} + +#[test] +fn filter_removes_session_out_of_bounds() { + new_test_ext(Default::default()).execute_with(|| { + let v0 = <ValidatorId as CryptoType>::Pair::generate().0; + + run_to_block(3, |b| { + // a new session at each block + Some((true, b, vec![(&0, v0.public())], Some(vec![(&0, v0.public())]))) + }); + + let candidate_hash = CandidateHash(sp_core::H256::repeat_byte(1)); + + let payload = ExplicitDisputeStatement { + valid: true, + candidate_hash: candidate_hash.clone(), + session: 1, + } + .signing_payload(); + + let sig_a = v0.sign(&payload); + + let statements = vec![DisputeStatementSet { + candidate_hash: candidate_hash.clone(), + session: 100, + statements: vec![( + DisputeStatement::Valid(ValidDisputeStatementKind::Explicit), + ValidatorIndex(0), + sig_a, + )], + }]; + + let statements = apply_filter_all::<Test, _>(statements); + + assert!(statements.is_empty()); + }) +} + +#[test] +fn filter_removes_concluded_ancient() { + let dispute_post_conclusion_acceptance_period = 2; + + let mock_genesis_config = MockGenesisConfig { + configuration: crate::configuration::GenesisConfig { + config: HostConfiguration { + dispute_post_conclusion_acceptance_period, + ..Default::default() + }, + ..Default::default() + }, + ..Default::default() + }; + + new_test_ext(mock_genesis_config).execute_with(|| { + let v0 = <ValidatorId as CryptoType>::Pair::generate().0; + + run_to_block(3, |b| { + // a new session at each block + Some((true, b, vec![(&0, v0.public())], Some(vec![(&0, v0.public())]))) + }); + + let candidate_hash_a = CandidateHash(sp_core::H256::repeat_byte(1)); + let candidate_hash_b = CandidateHash(sp_core::H256::repeat_byte(2)); + + <Disputes<Test>>::insert( + &1, + &candidate_hash_a, + DisputeState { + validators_for: bitvec![BitOrderLsb0, u8; 0; 4], + validators_against: bitvec![BitOrderLsb0, u8; 1; 4], + start: 0, + concluded_at: Some(0), + }, + ); + + <Disputes<Test>>::insert( + &1, + &candidate_hash_b, + DisputeState { + validators_for: bitvec![BitOrderLsb0, u8; 0; 4], + validators_against: bitvec![BitOrderLsb0, u8; 1; 4], + start: 0, + concluded_at: Some(1), + }, + ); + + let payload_a = ExplicitDisputeStatement { + valid: true, + candidate_hash: candidate_hash_a.clone(), + session: 1, + } + .signing_payload(); + + let payload_b = ExplicitDisputeStatement { + valid: true, + candidate_hash: candidate_hash_b.clone(), + session: 1, + } + .signing_payload(); + + let sig_a = v0.sign(&payload_a); + let sig_b = v0.sign(&payload_b); + + let statements = vec![ + DisputeStatementSet { + candidate_hash: candidate_hash_a.clone(), + session: 1, + statements: vec![( + DisputeStatement::Valid(ValidDisputeStatementKind::Explicit), + ValidatorIndex(0), + sig_a, + )], + }, + DisputeStatementSet { + candidate_hash: candidate_hash_b.clone(), + session: 1, + statements: vec![( + DisputeStatement::Valid(ValidDisputeStatementKind::Explicit), + ValidatorIndex(0), + sig_b.clone(), + )], + }, + ]; + + let statements = apply_filter_all::<Test, _>(statements); + + assert_eq!( + statements, + vec![CheckedDisputeStatementSet::unchecked_from_unchecked(DisputeStatementSet { + candidate_hash: candidate_hash_b.clone(), + session: 1, + statements: vec![( + DisputeStatement::Valid(ValidDisputeStatementKind::Explicit), + ValidatorIndex(0), + sig_b, + ),] + })] + ); + }) +} + +#[test] +fn filter_removes_duplicate_statements_sets() { + new_test_ext(Default::default()).execute_with(|| { + let v0 = <ValidatorId as CryptoType>::Pair::generate().0; + let v1 = <ValidatorId as CryptoType>::Pair::generate().0; + + run_to_block(3, |b| { + // a new session at each block + Some(( + true, + b, + vec![(&0, v0.public()), (&1, v1.public())], + Some(vec![(&0, v0.public()), (&1, v1.public())]), + )) + }); + + let candidate_hash_a = CandidateHash(sp_core::H256::repeat_byte(1)); + + let payload = ExplicitDisputeStatement { + valid: true, + candidate_hash: candidate_hash_a.clone(), + session: 1, + } + .signing_payload(); + + let payload_against = ExplicitDisputeStatement { + valid: false, + candidate_hash: candidate_hash_a.clone(), + session: 1, + } + .signing_payload(); + + let sig_a = v0.sign(&payload); + let sig_a_against = v1.sign(&payload_against); + + let statements = vec![ + ( + DisputeStatement::Valid(ValidDisputeStatementKind::Explicit), + ValidatorIndex(0), + sig_a.clone(), + ), + ( + DisputeStatement::Invalid(InvalidDisputeStatementKind::Explicit), + ValidatorIndex(1), + sig_a_against.clone(), + ), + ]; + + let mut sets = vec![ + DisputeStatementSet { + candidate_hash: candidate_hash_a.clone(), + session: 1, + statements: statements.clone(), + }, + DisputeStatementSet { + candidate_hash: candidate_hash_a.clone(), + session: 1, + statements: statements.clone(), + }, + ]; + + // `Err(())` indicates presence of duplicates + assert!(<Pallet::<Test> as DisputesHandler< + <Test as frame_system::Config>::BlockNumber, + >>::deduplicate_and_sort_dispute_data(&mut sets) + .is_err()); + + assert_eq!( + sets, + vec![DisputeStatementSet { + candidate_hash: candidate_hash_a.clone(), + session: 1, + statements, + }] + ); + }) +} + +#[test] +fn assure_no_duplicate_statements_sets_are_fine() { + new_test_ext(Default::default()).execute_with(|| { + let v0 = <ValidatorId as CryptoType>::Pair::generate().0; + let v1 = <ValidatorId as CryptoType>::Pair::generate().0; + + run_to_block(3, |b| { + // a new session at each block + Some(( + true, + b, + vec![(&0, v0.public()), (&1, v1.public())], + Some(vec![(&0, v0.public()), (&1, v1.public())]), + )) + }); + + let candidate_hash_a = CandidateHash(sp_core::H256::repeat_byte(1)); + + let payload = ExplicitDisputeStatement { + valid: true, + candidate_hash: candidate_hash_a.clone(), + session: 1, + } + .signing_payload(); + + let payload_against = ExplicitDisputeStatement { + valid: false, + candidate_hash: candidate_hash_a.clone(), + session: 1, + } + .signing_payload(); + + let sig_a = v0.sign(&payload); + let sig_a_against = v1.sign(&payload_against); + + let statements = vec![ + ( + DisputeStatement::Valid(ValidDisputeStatementKind::Explicit), + ValidatorIndex(0), + sig_a.clone(), + ), + ( + DisputeStatement::Invalid(InvalidDisputeStatementKind::Explicit), + ValidatorIndex(1), + sig_a_against.clone(), + ), + ]; + + let sets = vec![ + DisputeStatementSet { + candidate_hash: candidate_hash_a.clone(), + session: 1, + statements: statements.clone(), + }, + DisputeStatementSet { + candidate_hash: candidate_hash_a.clone(), + session: 2, + statements: statements.clone(), + }, + ]; + + // `Err(())` indicates presence of duplicates + assert!(<Pallet::<Test> as DisputesHandler< + <Test as frame_system::Config>::BlockNumber, + >>::assure_deduplicated_and_sorted(&sets) + .is_ok()); + }) +} + +#[test] +fn assure_detects_duplicate_statements_sets() { + new_test_ext(Default::default()).execute_with(|| { + let v0 = <ValidatorId as CryptoType>::Pair::generate().0; + let v1 = <ValidatorId as CryptoType>::Pair::generate().0; + + run_to_block(3, |b| { + // a new session at each block + Some(( + true, + b, + vec![(&0, v0.public()), (&1, v1.public())], + Some(vec![(&0, v0.public()), (&1, v1.public())]), + )) + }); + + let candidate_hash_a = CandidateHash(sp_core::H256::repeat_byte(1)); + + let payload = ExplicitDisputeStatement { + valid: true, + candidate_hash: candidate_hash_a.clone(), + session: 1, + } + .signing_payload(); + + let payload_against = ExplicitDisputeStatement { + valid: false, + candidate_hash: candidate_hash_a.clone(), + session: 1, + } + .signing_payload(); + + let sig_a = v0.sign(&payload); + let sig_a_against = v1.sign(&payload_against); + + let statements = vec![ + ( + DisputeStatement::Valid(ValidDisputeStatementKind::Explicit), + ValidatorIndex(0), + sig_a.clone(), + ), + ( + DisputeStatement::Invalid(InvalidDisputeStatementKind::Explicit), + ValidatorIndex(1), + sig_a_against.clone(), + ), + ]; + + let sets = vec![ + DisputeStatementSet { + candidate_hash: candidate_hash_a.clone(), + session: 1, + statements: statements.clone(), + }, + DisputeStatementSet { + candidate_hash: candidate_hash_a.clone(), + session: 1, + statements: statements.clone(), + }, + ]; + + // `Err(())` indicates presence of duplicates + assert!(<Pallet::<Test> as DisputesHandler< + <Test as frame_system::Config>::BlockNumber, + >>::assure_deduplicated_and_sorted(&sets) + .is_err()); + }) +} + +#[test] +fn filter_ignores_single_sided() { + new_test_ext(Default::default()).execute_with(|| { + let v0 = <ValidatorId as CryptoType>::Pair::generate().0; + + run_to_block(3, |b| { + // a new session at each block + Some((true, b, vec![(&0, v0.public())], Some(vec![(&0, v0.public())]))) + }); + + let candidate_hash_a = CandidateHash(sp_core::H256::repeat_byte(1)); + + let payload = ExplicitDisputeStatement { + valid: true, + candidate_hash: candidate_hash_a.clone(), + session: 1, + } + .signing_payload(); + + let sig_a = v0.sign(&payload); + + let statements = vec![DisputeStatementSet { + candidate_hash: candidate_hash_a.clone(), + session: 1, + statements: vec![( + DisputeStatement::Valid(ValidDisputeStatementKind::Explicit), + ValidatorIndex(0), + sig_a.clone(), + )], + }]; + + let statements = apply_filter_all::<Test, _>(statements); + + assert!(statements.is_empty()); + }) +} + +#[test] +fn import_ignores_single_sided() { + new_test_ext(Default::default()).execute_with(|| { + let v0 = <ValidatorId as CryptoType>::Pair::generate().0; + + run_to_block(3, |b| { + // a new session at each block + Some((true, b, vec![(&0, v0.public())], Some(vec![(&0, v0.public())]))) + }); + + let candidate_hash_a = CandidateHash(sp_core::H256::repeat_byte(1)); + + let payload = ExplicitDisputeStatement { + valid: true, + candidate_hash: candidate_hash_a.clone(), + session: 1, + } + .signing_payload(); + + let sig_a = v0.sign(&payload); + + let statements = vec![DisputeStatementSet { + candidate_hash: candidate_hash_a.clone(), + session: 1, + statements: vec![( + DisputeStatement::Valid(ValidDisputeStatementKind::Explicit), + ValidatorIndex(0), + sig_a.clone(), + )], + }]; + + let statements = apply_filter_all::<Test, _>(statements); + assert!(statements.is_empty()); + }) +} diff --git a/polkadot/runtime/parachains/src/dmp.rs b/polkadot/runtime/parachains/src/dmp.rs index c55a07b9f9ec2f0cc7d4129e42b7b49258c14122..4a11796af0ce23af4ddcac412cbf182fc88242d5 100644 --- a/polkadot/runtime/parachains/src/dmp.rs +++ b/polkadot/runtime/parachains/src/dmp.rs @@ -26,6 +26,9 @@ use xcm::latest::SendError; pub use pallet::*; +#[cfg(test)] +mod tests; + /// An error sending a downward message. #[cfg_attr(test, derive(Debug))] pub enum QueueDownwardMessageError { @@ -227,197 +230,3 @@ impl<T: Config> Pallet<T> { <Self as Store>::DownwardMessageQueues::get(&recipient) } } - -#[cfg(test)] -mod tests { - use super::*; - use crate::mock::{new_test_ext, Configuration, Dmp, MockGenesisConfig, Paras, System}; - use hex_literal::hex; - use parity_scale_codec::Encode; - use primitives::v1::BlockNumber; - - pub(crate) fn run_to_block(to: BlockNumber, new_session: Option<Vec<BlockNumber>>) { - while System::block_number() < to { - let b = System::block_number(); - Paras::initializer_finalize(b); - Dmp::initializer_finalize(); - if new_session.as_ref().map_or(false, |v| v.contains(&(b + 1))) { - Dmp::initializer_on_new_session(&Default::default(), &Vec::new()); - } - System::on_finalize(b); - - System::on_initialize(b + 1); - System::set_block_number(b + 1); - - Paras::initializer_finalize(b + 1); - Dmp::initializer_initialize(b + 1); - } - } - - fn default_genesis_config() -> MockGenesisConfig { - MockGenesisConfig { - configuration: crate::configuration::GenesisConfig { - config: crate::configuration::HostConfiguration { - max_downward_message_size: 1024, - ..Default::default() - }, - }, - ..Default::default() - } - } - - fn queue_downward_message( - para_id: ParaId, - msg: DownwardMessage, - ) -> Result<(), QueueDownwardMessageError> { - Dmp::queue_downward_message(&Configuration::config(), para_id, msg) - } - - #[test] - fn clean_dmp_works() { - let a = ParaId::from(1312); - let b = ParaId::from(228); - let c = ParaId::from(123); - - new_test_ext(default_genesis_config()).execute_with(|| { - // enqueue downward messages to A, B and C. - queue_downward_message(a, vec![1, 2, 3]).unwrap(); - queue_downward_message(b, vec![4, 5, 6]).unwrap(); - queue_downward_message(c, vec![7, 8, 9]).unwrap(); - - let notification = crate::initializer::SessionChangeNotification::default(); - let outgoing_paras = vec![a, b]; - Dmp::initializer_on_new_session(¬ification, &outgoing_paras); - - assert!(<Dmp as Store>::DownwardMessageQueues::get(&a).is_empty()); - assert!(<Dmp as Store>::DownwardMessageQueues::get(&b).is_empty()); - assert!(!<Dmp as Store>::DownwardMessageQueues::get(&c).is_empty()); - }); - } - - #[test] - fn dmq_length_and_head_updated_properly() { - let a = ParaId::from(1312); - let b = ParaId::from(228); - - new_test_ext(default_genesis_config()).execute_with(|| { - assert_eq!(Dmp::dmq_length(a), 0); - assert_eq!(Dmp::dmq_length(b), 0); - - queue_downward_message(a, vec![1, 2, 3]).unwrap(); - - assert_eq!(Dmp::dmq_length(a), 1); - assert_eq!(Dmp::dmq_length(b), 0); - assert!(!Dmp::dmq_mqc_head(a).is_zero()); - assert!(Dmp::dmq_mqc_head(b).is_zero()); - }); - } - - #[test] - fn dmp_mqc_head_fixture() { - let a = ParaId::from(2000); - - new_test_ext(default_genesis_config()).execute_with(|| { - run_to_block(2, None); - assert!(Dmp::dmq_mqc_head(a).is_zero()); - queue_downward_message(a, vec![1, 2, 3]).unwrap(); - - run_to_block(3, None); - queue_downward_message(a, vec![4, 5, 6]).unwrap(); - - assert_eq!( - Dmp::dmq_mqc_head(a), - hex!["88dc00db8cc9d22aa62b87807705831f164387dfa49f80a8600ed1cbe1704b6b"].into(), - ); - }); - } - - #[test] - fn check_processed_downward_messages() { - let a = ParaId::from(1312); - - new_test_ext(default_genesis_config()).execute_with(|| { - // processed_downward_messages=0 is allowed when the DMQ is empty. - assert!(Dmp::check_processed_downward_messages(a, 0).is_ok()); - - queue_downward_message(a, vec![1, 2, 3]).unwrap(); - queue_downward_message(a, vec![4, 5, 6]).unwrap(); - queue_downward_message(a, vec![7, 8, 9]).unwrap(); - - // 0 doesn't pass if the DMQ has msgs. - assert!(!Dmp::check_processed_downward_messages(a, 0).is_ok()); - // a candidate can consume up to 3 messages - assert!(Dmp::check_processed_downward_messages(a, 1).is_ok()); - assert!(Dmp::check_processed_downward_messages(a, 2).is_ok()); - assert!(Dmp::check_processed_downward_messages(a, 3).is_ok()); - // there is no 4 messages in the queue - assert!(!Dmp::check_processed_downward_messages(a, 4).is_ok()); - }); - } - - #[test] - fn dmq_pruning() { - let a = ParaId::from(1312); - - new_test_ext(default_genesis_config()).execute_with(|| { - assert_eq!(Dmp::dmq_length(a), 0); - - queue_downward_message(a, vec![1, 2, 3]).unwrap(); - queue_downward_message(a, vec![4, 5, 6]).unwrap(); - queue_downward_message(a, vec![7, 8, 9]).unwrap(); - assert_eq!(Dmp::dmq_length(a), 3); - - // pruning 0 elements shouldn't change anything. - Dmp::prune_dmq(a, 0); - assert_eq!(Dmp::dmq_length(a), 3); - - Dmp::prune_dmq(a, 2); - assert_eq!(Dmp::dmq_length(a), 1); - }); - } - - #[test] - fn queue_downward_message_critical() { - let a = ParaId::from(1312); - - let mut genesis = default_genesis_config(); - genesis.configuration.config.max_downward_message_size = 7; - - new_test_ext(genesis).execute_with(|| { - let smol = [0; 3].to_vec(); - let big = [0; 8].to_vec(); - - // still within limits - assert_eq!(smol.encode().len(), 4); - assert!(queue_downward_message(a, smol).is_ok()); - - // that's too big - assert_eq!(big.encode().len(), 9); - assert!(queue_downward_message(a, big).is_err()); - }); - } - - #[test] - fn verify_dmq_mqc_head_is_externally_accessible() { - use hex_literal::hex; - use primitives::v1::well_known_keys; - - let a = ParaId::from(2020); - - new_test_ext(default_genesis_config()).execute_with(|| { - let head = sp_io::storage::get(&well_known_keys::dmq_mqc_head(a)); - assert_eq!(head, None); - - queue_downward_message(a, vec![1, 2, 3]).unwrap(); - - let head = sp_io::storage::get(&well_known_keys::dmq_mqc_head(a)); - assert_eq!( - head, - Some( - hex!["434f8579a2297dfea851bf6be33093c83a78b655a53ae141a7894494c0010589"] - .to_vec() - ) - ); - }); - } -} diff --git a/polkadot/runtime/parachains/src/dmp/tests.rs b/polkadot/runtime/parachains/src/dmp/tests.rs new file mode 100644 index 0000000000000000000000000000000000000000..46c497dde9044847ade482f190c10378cc7d56a5 --- /dev/null +++ b/polkadot/runtime/parachains/src/dmp/tests.rs @@ -0,0 +1,203 @@ +// Copyright 2020 Parity Technologies (UK) Ltd. +// This file is part of Polkadot. + +// Polkadot is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Polkadot is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Polkadot. If not, see <http://www.gnu.org/licenses/>. + +use super::*; +use crate::mock::{new_test_ext, Configuration, Dmp, MockGenesisConfig, Paras, System}; +use hex_literal::hex; +use parity_scale_codec::Encode; +use primitives::v1::BlockNumber; + +pub(crate) fn run_to_block(to: BlockNumber, new_session: Option<Vec<BlockNumber>>) { + while System::block_number() < to { + let b = System::block_number(); + Paras::initializer_finalize(b); + Dmp::initializer_finalize(); + if new_session.as_ref().map_or(false, |v| v.contains(&(b + 1))) { + Dmp::initializer_on_new_session(&Default::default(), &Vec::new()); + } + System::on_finalize(b); + + System::on_initialize(b + 1); + System::set_block_number(b + 1); + + Paras::initializer_finalize(b + 1); + Dmp::initializer_initialize(b + 1); + } +} + +fn default_genesis_config() -> MockGenesisConfig { + MockGenesisConfig { + configuration: crate::configuration::GenesisConfig { + config: crate::configuration::HostConfiguration { + max_downward_message_size: 1024, + ..Default::default() + }, + }, + ..Default::default() + } +} + +fn queue_downward_message( + para_id: ParaId, + msg: DownwardMessage, +) -> Result<(), QueueDownwardMessageError> { + Dmp::queue_downward_message(&Configuration::config(), para_id, msg) +} + +#[test] +fn clean_dmp_works() { + let a = ParaId::from(1312); + let b = ParaId::from(228); + let c = ParaId::from(123); + + new_test_ext(default_genesis_config()).execute_with(|| { + // enqueue downward messages to A, B and C. + queue_downward_message(a, vec![1, 2, 3]).unwrap(); + queue_downward_message(b, vec![4, 5, 6]).unwrap(); + queue_downward_message(c, vec![7, 8, 9]).unwrap(); + + let notification = crate::initializer::SessionChangeNotification::default(); + let outgoing_paras = vec![a, b]; + Dmp::initializer_on_new_session(¬ification, &outgoing_paras); + + assert!(<Dmp as Store>::DownwardMessageQueues::get(&a).is_empty()); + assert!(<Dmp as Store>::DownwardMessageQueues::get(&b).is_empty()); + assert!(!<Dmp as Store>::DownwardMessageQueues::get(&c).is_empty()); + }); +} + +#[test] +fn dmq_length_and_head_updated_properly() { + let a = ParaId::from(1312); + let b = ParaId::from(228); + + new_test_ext(default_genesis_config()).execute_with(|| { + assert_eq!(Dmp::dmq_length(a), 0); + assert_eq!(Dmp::dmq_length(b), 0); + + queue_downward_message(a, vec![1, 2, 3]).unwrap(); + + assert_eq!(Dmp::dmq_length(a), 1); + assert_eq!(Dmp::dmq_length(b), 0); + assert!(!Dmp::dmq_mqc_head(a).is_zero()); + assert!(Dmp::dmq_mqc_head(b).is_zero()); + }); +} + +#[test] +fn dmp_mqc_head_fixture() { + let a = ParaId::from(2000); + + new_test_ext(default_genesis_config()).execute_with(|| { + run_to_block(2, None); + assert!(Dmp::dmq_mqc_head(a).is_zero()); + queue_downward_message(a, vec![1, 2, 3]).unwrap(); + + run_to_block(3, None); + queue_downward_message(a, vec![4, 5, 6]).unwrap(); + + assert_eq!( + Dmp::dmq_mqc_head(a), + hex!["88dc00db8cc9d22aa62b87807705831f164387dfa49f80a8600ed1cbe1704b6b"].into(), + ); + }); +} + +#[test] +fn check_processed_downward_messages() { + let a = ParaId::from(1312); + + new_test_ext(default_genesis_config()).execute_with(|| { + // processed_downward_messages=0 is allowed when the DMQ is empty. + assert!(Dmp::check_processed_downward_messages(a, 0).is_ok()); + + queue_downward_message(a, vec![1, 2, 3]).unwrap(); + queue_downward_message(a, vec![4, 5, 6]).unwrap(); + queue_downward_message(a, vec![7, 8, 9]).unwrap(); + + // 0 doesn't pass if the DMQ has msgs. + assert!(!Dmp::check_processed_downward_messages(a, 0).is_ok()); + // a candidate can consume up to 3 messages + assert!(Dmp::check_processed_downward_messages(a, 1).is_ok()); + assert!(Dmp::check_processed_downward_messages(a, 2).is_ok()); + assert!(Dmp::check_processed_downward_messages(a, 3).is_ok()); + // there is no 4 messages in the queue + assert!(!Dmp::check_processed_downward_messages(a, 4).is_ok()); + }); +} + +#[test] +fn dmq_pruning() { + let a = ParaId::from(1312); + + new_test_ext(default_genesis_config()).execute_with(|| { + assert_eq!(Dmp::dmq_length(a), 0); + + queue_downward_message(a, vec![1, 2, 3]).unwrap(); + queue_downward_message(a, vec![4, 5, 6]).unwrap(); + queue_downward_message(a, vec![7, 8, 9]).unwrap(); + assert_eq!(Dmp::dmq_length(a), 3); + + // pruning 0 elements shouldn't change anything. + Dmp::prune_dmq(a, 0); + assert_eq!(Dmp::dmq_length(a), 3); + + Dmp::prune_dmq(a, 2); + assert_eq!(Dmp::dmq_length(a), 1); + }); +} + +#[test] +fn queue_downward_message_critical() { + let a = ParaId::from(1312); + + let mut genesis = default_genesis_config(); + genesis.configuration.config.max_downward_message_size = 7; + + new_test_ext(genesis).execute_with(|| { + let smol = [0; 3].to_vec(); + let big = [0; 8].to_vec(); + + // still within limits + assert_eq!(smol.encode().len(), 4); + assert!(queue_downward_message(a, smol).is_ok()); + + // that's too big + assert_eq!(big.encode().len(), 9); + assert!(queue_downward_message(a, big).is_err()); + }); +} + +#[test] +fn verify_dmq_mqc_head_is_externally_accessible() { + use hex_literal::hex; + use primitives::v1::well_known_keys; + + let a = ParaId::from(2020); + + new_test_ext(default_genesis_config()).execute_with(|| { + let head = sp_io::storage::get(&well_known_keys::dmq_mqc_head(a)); + assert_eq!(head, None); + + queue_downward_message(a, vec![1, 2, 3]).unwrap(); + + let head = sp_io::storage::get(&well_known_keys::dmq_mqc_head(a)); + assert_eq!( + head, + Some(hex!["434f8579a2297dfea851bf6be33093c83a78b655a53ae141a7894494c0010589"].to_vec()) + ); + }); +} diff --git a/polkadot/runtime/parachains/src/hrmp.rs b/polkadot/runtime/parachains/src/hrmp.rs index bba182d4c4667d02149863a50f32b4ad817e6a93..025addbc91843a56ed749cdab21e9de6af0654ec 100644 --- a/polkadot/runtime/parachains/src/hrmp.rs +++ b/polkadot/runtime/parachains/src/hrmp.rs @@ -43,6 +43,9 @@ pub const HRMP_MAX_INBOUND_CHANNELS_BOUND: u32 = 128; /// Same as [`HRMP_MAX_INBOUND_CHANNELS_BOUND`], but for outbound channels. pub const HRMP_MAX_OUTBOUND_CHANNELS_BOUND: u32 = 128; +#[cfg(test)] +pub(crate) mod tests; + #[cfg(feature = "runtime-benchmarks")] mod benchmarking; @@ -1543,594 +1546,3 @@ impl<T: Config> Pallet<T> { } } } - -#[cfg(test)] -pub(crate) mod tests { - use super::*; - use crate::mock::{ - new_test_ext, Configuration, Event as MockEvent, Hrmp, MockGenesisConfig, Paras, - ParasShared, System, Test, - }; - use frame_support::{assert_noop, assert_ok, traits::Currency as _}; - use primitives::v1::BlockNumber; - use std::collections::BTreeMap; - - fn run_to_block(to: BlockNumber, new_session: Option<Vec<BlockNumber>>) { - let config = Configuration::config(); - while System::block_number() < to { - let b = System::block_number(); - - // NOTE: this is in reverse initialization order. - Hrmp::initializer_finalize(); - Paras::initializer_finalize(b); - ParasShared::initializer_finalize(); - - if new_session.as_ref().map_or(false, |v| v.contains(&(b + 1))) { - let notification = crate::initializer::SessionChangeNotification { - prev_config: config.clone(), - new_config: config.clone(), - session_index: ParasShared::session_index() + 1, - ..Default::default() - }; - - // NOTE: this is in initialization order. - ParasShared::initializer_on_new_session( - notification.session_index, - notification.random_seed, - ¬ification.new_config, - notification.validators.clone(), - ); - let outgoing_paras = Paras::initializer_on_new_session(¬ification); - Hrmp::initializer_on_new_session(¬ification, &outgoing_paras); - } - - System::on_finalize(b); - - System::on_initialize(b + 1); - System::set_block_number(b + 1); - - // NOTE: this is in initialization order. - ParasShared::initializer_initialize(b + 1); - Paras::initializer_initialize(b + 1); - Hrmp::initializer_initialize(b + 1); - } - } - - #[derive(Debug)] - pub(super) struct GenesisConfigBuilder { - hrmp_channel_max_capacity: u32, - hrmp_channel_max_message_size: u32, - hrmp_max_parathread_outbound_channels: u32, - hrmp_max_parachain_outbound_channels: u32, - hrmp_max_parathread_inbound_channels: u32, - hrmp_max_parachain_inbound_channels: u32, - hrmp_max_message_num_per_candidate: u32, - hrmp_channel_max_total_size: u32, - hrmp_sender_deposit: Balance, - hrmp_recipient_deposit: Balance, - } - - impl Default for GenesisConfigBuilder { - fn default() -> Self { - Self { - hrmp_channel_max_capacity: 2, - hrmp_channel_max_message_size: 8, - hrmp_max_parathread_outbound_channels: 1, - hrmp_max_parachain_outbound_channels: 2, - hrmp_max_parathread_inbound_channels: 1, - hrmp_max_parachain_inbound_channels: 2, - hrmp_max_message_num_per_candidate: 2, - hrmp_channel_max_total_size: 16, - hrmp_sender_deposit: 100, - hrmp_recipient_deposit: 100, - } - } - } - - impl GenesisConfigBuilder { - pub(super) fn build(self) -> crate::mock::MockGenesisConfig { - let mut genesis = default_genesis_config(); - let config = &mut genesis.configuration.config; - config.hrmp_channel_max_capacity = self.hrmp_channel_max_capacity; - config.hrmp_channel_max_message_size = self.hrmp_channel_max_message_size; - config.hrmp_max_parathread_outbound_channels = - self.hrmp_max_parathread_outbound_channels; - config.hrmp_max_parachain_outbound_channels = self.hrmp_max_parachain_outbound_channels; - config.hrmp_max_parathread_inbound_channels = self.hrmp_max_parathread_inbound_channels; - config.hrmp_max_parachain_inbound_channels = self.hrmp_max_parachain_inbound_channels; - config.hrmp_max_message_num_per_candidate = self.hrmp_max_message_num_per_candidate; - config.hrmp_channel_max_total_size = self.hrmp_channel_max_total_size; - config.hrmp_sender_deposit = self.hrmp_sender_deposit; - config.hrmp_recipient_deposit = self.hrmp_recipient_deposit; - genesis - } - } - - fn default_genesis_config() -> MockGenesisConfig { - MockGenesisConfig { - configuration: crate::configuration::GenesisConfig { - config: crate::configuration::HostConfiguration { - max_downward_message_size: 1024, - pvf_checking_enabled: false, - ..Default::default() - }, - }, - ..Default::default() - } - } - - fn register_parachain_with_balance(id: ParaId, balance: Balance) { - assert_ok!(Paras::schedule_para_initialize( - id, - crate::paras::ParaGenesisArgs { - parachain: true, - genesis_head: vec![1].into(), - validation_code: vec![1].into(), - }, - )); - <Test as Config>::Currency::make_free_balance_be(&id.into_account(), balance); - } - - fn register_parachain(id: ParaId) { - register_parachain_with_balance(id, 1000); - } - - fn deregister_parachain(id: ParaId) { - assert_ok!(Paras::schedule_para_cleanup(id)); - } - - fn channel_exists(sender: ParaId, recipient: ParaId) -> bool { - <Hrmp as Store>::HrmpChannels::get(&HrmpChannelId { sender, recipient }).is_some() - } - - #[test] - fn empty_state_consistent_state() { - new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| { - Hrmp::assert_storage_consistency_exhaustive(); - }); - } - - #[test] - fn open_channel_works() { - let para_a = 1.into(); - let para_a_origin: crate::Origin = 1.into(); - let para_b = 3.into(); - let para_b_origin: crate::Origin = 3.into(); - - new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| { - // We need both A & B to be registered and alive parachains. - register_parachain(para_a); - register_parachain(para_b); - - run_to_block(5, Some(vec![4, 5])); - Hrmp::hrmp_init_open_channel(para_a_origin.into(), para_b, 2, 8).unwrap(); - Hrmp::assert_storage_consistency_exhaustive(); - assert!(System::events().iter().any(|record| record.event == - MockEvent::Hrmp(Event::OpenChannelRequested(para_a, para_b, 2, 8)))); - - Hrmp::hrmp_accept_open_channel(para_b_origin.into(), para_a).unwrap(); - Hrmp::assert_storage_consistency_exhaustive(); - assert!(System::events().iter().any(|record| record.event == - MockEvent::Hrmp(Event::OpenChannelAccepted(para_a, para_b)))); - - // Advance to a block 6, but without session change. That means that the channel has - // not been created yet. - run_to_block(6, None); - assert!(!channel_exists(para_a, para_b)); - Hrmp::assert_storage_consistency_exhaustive(); - - // Now let the session change happen and thus open the channel. - run_to_block(8, Some(vec![8])); - assert!(channel_exists(para_a, para_b)); - }); - } - - #[test] - fn close_channel_works() { - let para_a = 5.into(); - let para_b = 2.into(); - let para_b_origin: crate::Origin = 2.into(); - - new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| { - register_parachain(para_a); - register_parachain(para_b); - - run_to_block(5, Some(vec![4, 5])); - Hrmp::init_open_channel(para_a, para_b, 2, 8).unwrap(); - Hrmp::accept_open_channel(para_b, para_a).unwrap(); - - run_to_block(6, Some(vec![6])); - assert!(channel_exists(para_a, para_b)); - - // Close the channel. The effect is not immediate, but rather deferred to the next - // session change. - let channel_id = HrmpChannelId { sender: para_a, recipient: para_b }; - Hrmp::hrmp_close_channel(para_b_origin.into(), channel_id.clone()).unwrap(); - assert!(channel_exists(para_a, para_b)); - Hrmp::assert_storage_consistency_exhaustive(); - - // After the session change the channel should be closed. - run_to_block(8, Some(vec![8])); - assert!(!channel_exists(para_a, para_b)); - Hrmp::assert_storage_consistency_exhaustive(); - assert!(System::events().iter().any(|record| record.event == - MockEvent::Hrmp(Event::ChannelClosed(para_b, channel_id.clone())))); - }); - } - - #[test] - fn send_recv_messages() { - let para_a = 32.into(); - let para_b = 64.into(); - - let mut genesis = GenesisConfigBuilder::default(); - genesis.hrmp_channel_max_message_size = 20; - genesis.hrmp_channel_max_total_size = 20; - new_test_ext(genesis.build()).execute_with(|| { - register_parachain(para_a); - register_parachain(para_b); - - run_to_block(5, Some(vec![4, 5])); - Hrmp::init_open_channel(para_a, para_b, 2, 20).unwrap(); - Hrmp::accept_open_channel(para_b, para_a).unwrap(); - - // On Block 6: - // A sends a message to B - run_to_block(6, Some(vec![6])); - assert!(channel_exists(para_a, para_b)); - let msgs = vec![OutboundHrmpMessage { - recipient: para_b, - data: b"this is an emergency".to_vec(), - }]; - let config = Configuration::config(); - assert!(Hrmp::check_outbound_hrmp(&config, para_a, &msgs).is_ok()); - let _ = Hrmp::queue_outbound_hrmp(para_a, msgs); - Hrmp::assert_storage_consistency_exhaustive(); - - // On Block 7: - // B receives the message sent by A. B sets the watermark to 6. - run_to_block(7, None); - assert!(Hrmp::check_hrmp_watermark(para_b, 7, 6).is_ok()); - let _ = Hrmp::prune_hrmp(para_b, 6); - Hrmp::assert_storage_consistency_exhaustive(); - }); - } - - #[test] - fn hrmp_mqc_head_fixture() { - let para_a = 2000.into(); - let para_b = 2024.into(); - - let mut genesis = GenesisConfigBuilder::default(); - genesis.hrmp_channel_max_message_size = 20; - genesis.hrmp_channel_max_total_size = 20; - new_test_ext(genesis.build()).execute_with(|| { - register_parachain(para_a); - register_parachain(para_b); - - run_to_block(2, Some(vec![1, 2])); - Hrmp::init_open_channel(para_a, para_b, 2, 20).unwrap(); - Hrmp::accept_open_channel(para_b, para_a).unwrap(); - - run_to_block(3, Some(vec![3])); - let _ = Hrmp::queue_outbound_hrmp( - para_a, - vec![OutboundHrmpMessage { recipient: para_b, data: vec![1, 2, 3] }], - ); - - run_to_block(4, None); - let _ = Hrmp::queue_outbound_hrmp( - para_a, - vec![OutboundHrmpMessage { recipient: para_b, data: vec![4, 5, 6] }], - ); - - assert_eq!( - Hrmp::hrmp_mqc_heads(para_b), - vec![( - para_a, - hex_literal::hex![ - "a964fd3b4f3d3ce92a0e25e576b87590d92bb5cb7031909c7f29050e1f04a375" - ] - .into() - ),], - ); - }); - } - - #[test] - fn accept_incoming_request_and_offboard() { - let para_a = 32.into(); - let para_b = 64.into(); - - new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| { - register_parachain(para_a); - register_parachain(para_b); - - run_to_block(5, Some(vec![4, 5])); - Hrmp::init_open_channel(para_a, para_b, 2, 8).unwrap(); - Hrmp::accept_open_channel(para_b, para_a).unwrap(); - deregister_parachain(para_a); - - // On Block 7: 2x session change. The channel should not be created. - run_to_block(7, Some(vec![6, 7])); - assert!(!Paras::is_valid_para(para_a)); - assert!(!channel_exists(para_a, para_b)); - Hrmp::assert_storage_consistency_exhaustive(); - }); - } - - #[test] - fn check_sent_messages() { - let para_a = 32.into(); - let para_b = 64.into(); - let para_c = 97.into(); - - new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| { - register_parachain(para_a); - register_parachain(para_b); - register_parachain(para_c); - - run_to_block(5, Some(vec![4, 5])); - - // Open two channels to the same receiver, b: - // a -> b, c -> b - Hrmp::init_open_channel(para_a, para_b, 2, 8).unwrap(); - Hrmp::accept_open_channel(para_b, para_a).unwrap(); - Hrmp::init_open_channel(para_c, para_b, 2, 8).unwrap(); - Hrmp::accept_open_channel(para_b, para_c).unwrap(); - - // On Block 6: session change. - run_to_block(6, Some(vec![6])); - assert!(Paras::is_valid_para(para_a)); - - let msgs = vec![OutboundHrmpMessage { recipient: para_b, data: b"knock".to_vec() }]; - let config = Configuration::config(); - assert!(Hrmp::check_outbound_hrmp(&config, para_a, &msgs).is_ok()); - let _ = Hrmp::queue_outbound_hrmp(para_a, msgs.clone()); - - // Verify that the sent messages are there and that also the empty channels are present. - let mqc_heads = Hrmp::hrmp_mqc_heads(para_b); - let contents = Hrmp::inbound_hrmp_channels_contents(para_b); - assert_eq!( - contents, - vec![ - (para_a, vec![InboundHrmpMessage { sent_at: 6, data: b"knock".to_vec() }]), - (para_c, vec![]) - ] - .into_iter() - .collect::<BTreeMap::<_, _>>(), - ); - assert_eq!( - mqc_heads, - vec![ - ( - para_a, - hex_literal::hex!( - "3bba6404e59c91f51deb2ae78f1273ebe75896850713e13f8c0eba4b0996c483" - ) - .into() - ), - (para_c, Default::default()) - ], - ); - - Hrmp::assert_storage_consistency_exhaustive(); - }); - } - - #[test] - fn verify_externally_accessible() { - use primitives::v1::{well_known_keys, AbridgedHrmpChannel}; - - let para_a = 20.into(); - let para_b = 21.into(); - - new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| { - // Register two parachains, wait until a session change, then initiate channel open - // request and accept that, and finally wait until the next session. - register_parachain(para_a); - register_parachain(para_b); - run_to_block(5, Some(vec![4, 5])); - Hrmp::init_open_channel(para_a, para_b, 2, 8).unwrap(); - Hrmp::accept_open_channel(para_b, para_a).unwrap(); - run_to_block(8, Some(vec![8])); - - // Here we have a channel a->b opened. - // - // Try to obtain this channel from the storage and - // decode it into the abridged version. - assert!(channel_exists(para_a, para_b)); - let raw_hrmp_channel = - sp_io::storage::get(&well_known_keys::hrmp_channels(HrmpChannelId { - sender: para_a, - recipient: para_b, - })) - .expect("the channel exists and we must be able to get it through well known keys"); - let abridged_hrmp_channel = AbridgedHrmpChannel::decode(&mut &raw_hrmp_channel[..]) - .expect("HrmpChannel should be decodable as AbridgedHrmpChannel"); - - assert_eq!( - abridged_hrmp_channel, - AbridgedHrmpChannel { - max_capacity: 2, - max_total_size: 16, - max_message_size: 8, - msg_count: 0, - total_size: 0, - mqc_head: None, - }, - ); - - let raw_ingress_index = - sp_io::storage::get(&well_known_keys::hrmp_ingress_channel_index(para_b)) - .expect("the ingress index must be present for para_b"); - let ingress_index = <Vec<ParaId>>::decode(&mut &raw_ingress_index[..]) - .expect("ingress indexx should be decodable as a list of para ids"); - assert_eq!(ingress_index, vec![para_a]); - - // Now, verify that we can access and decode the egress index. - let raw_egress_index = - sp_io::storage::get(&well_known_keys::hrmp_egress_channel_index(para_a)) - .expect("the egress index must be present for para_a"); - let egress_index = <Vec<ParaId>>::decode(&mut &raw_egress_index[..]) - .expect("egress index should be decodable as a list of para ids"); - assert_eq!(egress_index, vec![para_b]); - }); - } - - #[test] - fn charging_deposits() { - let para_a = 32.into(); - let para_b = 64.into(); - - new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| { - register_parachain_with_balance(para_a, 0); - register_parachain(para_b); - run_to_block(5, Some(vec![4, 5])); - - assert_noop!( - Hrmp::init_open_channel(para_a, para_b, 2, 8), - pallet_balances::Error::<Test, _>::InsufficientBalance - ); - }); - - new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| { - register_parachain(para_a); - register_parachain_with_balance(para_b, 0); - run_to_block(5, Some(vec![4, 5])); - - Hrmp::init_open_channel(para_a, para_b, 2, 8).unwrap(); - - assert_noop!( - Hrmp::accept_open_channel(para_b, para_a), - pallet_balances::Error::<Test, _>::InsufficientBalance - ); - }); - } - - #[test] - fn refund_deposit_on_normal_closure() { - let para_a = 32.into(); - let para_b = 64.into(); - - let mut genesis = GenesisConfigBuilder::default(); - genesis.hrmp_sender_deposit = 20; - genesis.hrmp_recipient_deposit = 15; - new_test_ext(genesis.build()).execute_with(|| { - // Register two parachains funded with different amounts of funds and arrange a channel. - register_parachain_with_balance(para_a, 100); - register_parachain_with_balance(para_b, 110); - run_to_block(5, Some(vec![4, 5])); - Hrmp::init_open_channel(para_a, para_b, 2, 8).unwrap(); - Hrmp::accept_open_channel(para_b, para_a).unwrap(); - assert_eq!(<Test as Config>::Currency::free_balance(¶_a.into_account()), 80); - assert_eq!(<Test as Config>::Currency::free_balance(¶_b.into_account()), 95); - run_to_block(8, Some(vec![8])); - - // Now, we close the channel and wait until the next session. - Hrmp::close_channel(para_b, HrmpChannelId { sender: para_a, recipient: para_b }) - .unwrap(); - run_to_block(10, Some(vec![10])); - assert_eq!(<Test as Config>::Currency::free_balance(¶_a.into_account()), 100); - assert_eq!(<Test as Config>::Currency::free_balance(¶_b.into_account()), 110); - }); - } - - #[test] - fn refund_deposit_on_offboarding() { - let para_a = 32.into(); - let para_b = 64.into(); - - let mut genesis = GenesisConfigBuilder::default(); - genesis.hrmp_sender_deposit = 20; - genesis.hrmp_recipient_deposit = 15; - new_test_ext(genesis.build()).execute_with(|| { - // Register two parachains and open a channel between them. - register_parachain_with_balance(para_a, 100); - register_parachain_with_balance(para_b, 110); - run_to_block(5, Some(vec![4, 5])); - Hrmp::init_open_channel(para_a, para_b, 2, 8).unwrap(); - Hrmp::accept_open_channel(para_b, para_a).unwrap(); - assert_eq!(<Test as Config>::Currency::free_balance(¶_a.into_account()), 80); - assert_eq!(<Test as Config>::Currency::free_balance(¶_b.into_account()), 95); - run_to_block(8, Some(vec![8])); - assert!(channel_exists(para_a, para_b)); - - // Then deregister one parachain. - deregister_parachain(para_a); - run_to_block(10, Some(vec![9, 10])); - - // The channel should be removed. - assert!(!Paras::is_valid_para(para_a)); - assert!(!channel_exists(para_a, para_b)); - Hrmp::assert_storage_consistency_exhaustive(); - - assert_eq!(<Test as Config>::Currency::free_balance(¶_a.into_account()), 100); - assert_eq!(<Test as Config>::Currency::free_balance(¶_b.into_account()), 110); - }); - } - - #[test] - fn no_dangling_open_requests() { - let para_a = 32.into(); - let para_b = 64.into(); - - let mut genesis = GenesisConfigBuilder::default(); - genesis.hrmp_sender_deposit = 20; - genesis.hrmp_recipient_deposit = 15; - new_test_ext(genesis.build()).execute_with(|| { - // Register two parachains and open a channel between them. - register_parachain_with_balance(para_a, 100); - register_parachain_with_balance(para_b, 110); - run_to_block(5, Some(vec![4, 5])); - - // Start opening a channel a->b - Hrmp::init_open_channel(para_a, para_b, 2, 8).unwrap(); - assert_eq!(<Test as Config>::Currency::free_balance(¶_a.into_account()), 80); - - // Then deregister one parachain, but don't wait two sessions until it takes effect. - // Instead, `para_b` will confirm the request, which will take place the same time - // the offboarding should happen. - deregister_parachain(para_a); - run_to_block(9, Some(vec![9])); - Hrmp::accept_open_channel(para_b, para_a).unwrap(); - assert_eq!(<Test as Config>::Currency::free_balance(¶_b.into_account()), 95); - assert!(!channel_exists(para_a, para_b)); - run_to_block(10, Some(vec![10])); - - // The outcome we expect is `para_b` should receive the refund. - assert_eq!(<Test as Config>::Currency::free_balance(¶_b.into_account()), 110); - assert!(!channel_exists(para_a, para_b)); - Hrmp::assert_storage_consistency_exhaustive(); - }); - } - - #[test] - fn cancel_pending_open_channel_request() { - let para_a = 32.into(); - let para_b = 64.into(); - - let mut genesis = GenesisConfigBuilder::default(); - genesis.hrmp_sender_deposit = 20; - genesis.hrmp_recipient_deposit = 15; - new_test_ext(genesis.build()).execute_with(|| { - // Register two parachains and open a channel between them. - register_parachain_with_balance(para_a, 100); - register_parachain_with_balance(para_b, 110); - run_to_block(5, Some(vec![4, 5])); - - // Start opening a channel a->b - Hrmp::init_open_channel(para_a, para_b, 2, 8).unwrap(); - assert_eq!(<Test as Config>::Currency::free_balance(¶_a.into_account()), 80); - - // Cancel opening the channel - Hrmp::cancel_open_request(para_a, HrmpChannelId { sender: para_a, recipient: para_b }) - .unwrap(); - assert_eq!(<Test as Config>::Currency::free_balance(¶_a.into_account()), 100); - - run_to_block(10, Some(vec![10])); - assert!(!channel_exists(para_a, para_b)); - Hrmp::assert_storage_consistency_exhaustive(); - }); - } -} diff --git a/polkadot/runtime/parachains/src/hrmp/tests.rs b/polkadot/runtime/parachains/src/hrmp/tests.rs new file mode 100644 index 0000000000000000000000000000000000000000..35dd006a44814e1416848ed89c4a01564028a460 --- /dev/null +++ b/polkadot/runtime/parachains/src/hrmp/tests.rs @@ -0,0 +1,601 @@ +// Copyright 2020 Parity Technologies (UK) Ltd. +// This file is part of Polkadot. + +// Polkadot is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Polkadot is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Polkadot. If not, see <http://www.gnu.org/licenses/>. + +use super::*; +use crate::mock::{ + new_test_ext, Configuration, Event as MockEvent, Hrmp, MockGenesisConfig, Paras, ParasShared, + System, Test, +}; +use frame_support::{assert_noop, assert_ok, traits::Currency as _}; +use primitives::v1::BlockNumber; +use std::collections::BTreeMap; + +fn run_to_block(to: BlockNumber, new_session: Option<Vec<BlockNumber>>) { + let config = Configuration::config(); + while System::block_number() < to { + let b = System::block_number(); + + // NOTE: this is in reverse initialization order. + Hrmp::initializer_finalize(); + Paras::initializer_finalize(b); + ParasShared::initializer_finalize(); + + if new_session.as_ref().map_or(false, |v| v.contains(&(b + 1))) { + let notification = crate::initializer::SessionChangeNotification { + prev_config: config.clone(), + new_config: config.clone(), + session_index: ParasShared::session_index() + 1, + ..Default::default() + }; + + // NOTE: this is in initialization order. + ParasShared::initializer_on_new_session( + notification.session_index, + notification.random_seed, + ¬ification.new_config, + notification.validators.clone(), + ); + let outgoing_paras = Paras::initializer_on_new_session(¬ification); + Hrmp::initializer_on_new_session(¬ification, &outgoing_paras); + } + + System::on_finalize(b); + + System::on_initialize(b + 1); + System::set_block_number(b + 1); + + // NOTE: this is in initialization order. + ParasShared::initializer_initialize(b + 1); + Paras::initializer_initialize(b + 1); + Hrmp::initializer_initialize(b + 1); + } +} + +#[derive(Debug)] +pub(super) struct GenesisConfigBuilder { + hrmp_channel_max_capacity: u32, + hrmp_channel_max_message_size: u32, + hrmp_max_parathread_outbound_channels: u32, + hrmp_max_parachain_outbound_channels: u32, + hrmp_max_parathread_inbound_channels: u32, + hrmp_max_parachain_inbound_channels: u32, + hrmp_max_message_num_per_candidate: u32, + hrmp_channel_max_total_size: u32, + hrmp_sender_deposit: Balance, + hrmp_recipient_deposit: Balance, +} + +impl Default for GenesisConfigBuilder { + fn default() -> Self { + Self { + hrmp_channel_max_capacity: 2, + hrmp_channel_max_message_size: 8, + hrmp_max_parathread_outbound_channels: 1, + hrmp_max_parachain_outbound_channels: 2, + hrmp_max_parathread_inbound_channels: 1, + hrmp_max_parachain_inbound_channels: 2, + hrmp_max_message_num_per_candidate: 2, + hrmp_channel_max_total_size: 16, + hrmp_sender_deposit: 100, + hrmp_recipient_deposit: 100, + } + } +} + +impl GenesisConfigBuilder { + pub(super) fn build(self) -> crate::mock::MockGenesisConfig { + let mut genesis = default_genesis_config(); + let config = &mut genesis.configuration.config; + config.hrmp_channel_max_capacity = self.hrmp_channel_max_capacity; + config.hrmp_channel_max_message_size = self.hrmp_channel_max_message_size; + config.hrmp_max_parathread_outbound_channels = self.hrmp_max_parathread_outbound_channels; + config.hrmp_max_parachain_outbound_channels = self.hrmp_max_parachain_outbound_channels; + config.hrmp_max_parathread_inbound_channels = self.hrmp_max_parathread_inbound_channels; + config.hrmp_max_parachain_inbound_channels = self.hrmp_max_parachain_inbound_channels; + config.hrmp_max_message_num_per_candidate = self.hrmp_max_message_num_per_candidate; + config.hrmp_channel_max_total_size = self.hrmp_channel_max_total_size; + config.hrmp_sender_deposit = self.hrmp_sender_deposit; + config.hrmp_recipient_deposit = self.hrmp_recipient_deposit; + genesis + } +} + +fn default_genesis_config() -> MockGenesisConfig { + MockGenesisConfig { + configuration: crate::configuration::GenesisConfig { + config: crate::configuration::HostConfiguration { + max_downward_message_size: 1024, + pvf_checking_enabled: false, + ..Default::default() + }, + }, + ..Default::default() + } +} + +fn register_parachain_with_balance(id: ParaId, balance: Balance) { + assert_ok!(Paras::schedule_para_initialize( + id, + crate::paras::ParaGenesisArgs { + parachain: true, + genesis_head: vec![1].into(), + validation_code: vec![1].into(), + }, + )); + <Test as Config>::Currency::make_free_balance_be(&id.into_account(), balance); +} + +fn register_parachain(id: ParaId) { + register_parachain_with_balance(id, 1000); +} + +fn deregister_parachain(id: ParaId) { + assert_ok!(Paras::schedule_para_cleanup(id)); +} + +fn channel_exists(sender: ParaId, recipient: ParaId) -> bool { + <Hrmp as Store>::HrmpChannels::get(&HrmpChannelId { sender, recipient }).is_some() +} + +#[test] +fn empty_state_consistent_state() { + new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| { + Hrmp::assert_storage_consistency_exhaustive(); + }); +} + +#[test] +fn open_channel_works() { + let para_a = 1.into(); + let para_a_origin: crate::Origin = 1.into(); + let para_b = 3.into(); + let para_b_origin: crate::Origin = 3.into(); + + new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| { + // We need both A & B to be registered and alive parachains. + register_parachain(para_a); + register_parachain(para_b); + + run_to_block(5, Some(vec![4, 5])); + Hrmp::hrmp_init_open_channel(para_a_origin.into(), para_b, 2, 8).unwrap(); + Hrmp::assert_storage_consistency_exhaustive(); + assert!(System::events().iter().any(|record| record.event == + MockEvent::Hrmp(Event::OpenChannelRequested(para_a, para_b, 2, 8)))); + + Hrmp::hrmp_accept_open_channel(para_b_origin.into(), para_a).unwrap(); + Hrmp::assert_storage_consistency_exhaustive(); + assert!(System::events() + .iter() + .any(|record| record.event == + MockEvent::Hrmp(Event::OpenChannelAccepted(para_a, para_b)))); + + // Advance to a block 6, but without session change. That means that the channel has + // not been created yet. + run_to_block(6, None); + assert!(!channel_exists(para_a, para_b)); + Hrmp::assert_storage_consistency_exhaustive(); + + // Now let the session change happen and thus open the channel. + run_to_block(8, Some(vec![8])); + assert!(channel_exists(para_a, para_b)); + }); +} + +#[test] +fn close_channel_works() { + let para_a = 5.into(); + let para_b = 2.into(); + let para_b_origin: crate::Origin = 2.into(); + + new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| { + register_parachain(para_a); + register_parachain(para_b); + + run_to_block(5, Some(vec![4, 5])); + Hrmp::init_open_channel(para_a, para_b, 2, 8).unwrap(); + Hrmp::accept_open_channel(para_b, para_a).unwrap(); + + run_to_block(6, Some(vec![6])); + assert!(channel_exists(para_a, para_b)); + + // Close the channel. The effect is not immediate, but rather deferred to the next + // session change. + let channel_id = HrmpChannelId { sender: para_a, recipient: para_b }; + Hrmp::hrmp_close_channel(para_b_origin.into(), channel_id.clone()).unwrap(); + assert!(channel_exists(para_a, para_b)); + Hrmp::assert_storage_consistency_exhaustive(); + + // After the session change the channel should be closed. + run_to_block(8, Some(vec![8])); + assert!(!channel_exists(para_a, para_b)); + Hrmp::assert_storage_consistency_exhaustive(); + assert!(System::events().iter().any(|record| record.event == + MockEvent::Hrmp(Event::ChannelClosed(para_b, channel_id.clone())))); + }); +} + +#[test] +fn send_recv_messages() { + let para_a = 32.into(); + let para_b = 64.into(); + + let mut genesis = GenesisConfigBuilder::default(); + genesis.hrmp_channel_max_message_size = 20; + genesis.hrmp_channel_max_total_size = 20; + new_test_ext(genesis.build()).execute_with(|| { + register_parachain(para_a); + register_parachain(para_b); + + run_to_block(5, Some(vec![4, 5])); + Hrmp::init_open_channel(para_a, para_b, 2, 20).unwrap(); + Hrmp::accept_open_channel(para_b, para_a).unwrap(); + + // On Block 6: + // A sends a message to B + run_to_block(6, Some(vec![6])); + assert!(channel_exists(para_a, para_b)); + let msgs = + vec![OutboundHrmpMessage { recipient: para_b, data: b"this is an emergency".to_vec() }]; + let config = Configuration::config(); + assert!(Hrmp::check_outbound_hrmp(&config, para_a, &msgs).is_ok()); + let _ = Hrmp::queue_outbound_hrmp(para_a, msgs); + Hrmp::assert_storage_consistency_exhaustive(); + + // On Block 7: + // B receives the message sent by A. B sets the watermark to 6. + run_to_block(7, None); + assert!(Hrmp::check_hrmp_watermark(para_b, 7, 6).is_ok()); + let _ = Hrmp::prune_hrmp(para_b, 6); + Hrmp::assert_storage_consistency_exhaustive(); + }); +} + +#[test] +fn hrmp_mqc_head_fixture() { + let para_a = 2000.into(); + let para_b = 2024.into(); + + let mut genesis = GenesisConfigBuilder::default(); + genesis.hrmp_channel_max_message_size = 20; + genesis.hrmp_channel_max_total_size = 20; + new_test_ext(genesis.build()).execute_with(|| { + register_parachain(para_a); + register_parachain(para_b); + + run_to_block(2, Some(vec![1, 2])); + Hrmp::init_open_channel(para_a, para_b, 2, 20).unwrap(); + Hrmp::accept_open_channel(para_b, para_a).unwrap(); + + run_to_block(3, Some(vec![3])); + let _ = Hrmp::queue_outbound_hrmp( + para_a, + vec![OutboundHrmpMessage { recipient: para_b, data: vec![1, 2, 3] }], + ); + + run_to_block(4, None); + let _ = Hrmp::queue_outbound_hrmp( + para_a, + vec![OutboundHrmpMessage { recipient: para_b, data: vec![4, 5, 6] }], + ); + + assert_eq!( + Hrmp::hrmp_mqc_heads(para_b), + vec![( + para_a, + hex_literal::hex![ + "a964fd3b4f3d3ce92a0e25e576b87590d92bb5cb7031909c7f29050e1f04a375" + ] + .into() + ),], + ); + }); +} + +#[test] +fn accept_incoming_request_and_offboard() { + let para_a = 32.into(); + let para_b = 64.into(); + + new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| { + register_parachain(para_a); + register_parachain(para_b); + + run_to_block(5, Some(vec![4, 5])); + Hrmp::init_open_channel(para_a, para_b, 2, 8).unwrap(); + Hrmp::accept_open_channel(para_b, para_a).unwrap(); + deregister_parachain(para_a); + + // On Block 7: 2x session change. The channel should not be created. + run_to_block(7, Some(vec![6, 7])); + assert!(!Paras::is_valid_para(para_a)); + assert!(!channel_exists(para_a, para_b)); + Hrmp::assert_storage_consistency_exhaustive(); + }); +} + +#[test] +fn check_sent_messages() { + let para_a = 32.into(); + let para_b = 64.into(); + let para_c = 97.into(); + + new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| { + register_parachain(para_a); + register_parachain(para_b); + register_parachain(para_c); + + run_to_block(5, Some(vec![4, 5])); + + // Open two channels to the same receiver, b: + // a -> b, c -> b + Hrmp::init_open_channel(para_a, para_b, 2, 8).unwrap(); + Hrmp::accept_open_channel(para_b, para_a).unwrap(); + Hrmp::init_open_channel(para_c, para_b, 2, 8).unwrap(); + Hrmp::accept_open_channel(para_b, para_c).unwrap(); + + // On Block 6: session change. + run_to_block(6, Some(vec![6])); + assert!(Paras::is_valid_para(para_a)); + + let msgs = vec![OutboundHrmpMessage { recipient: para_b, data: b"knock".to_vec() }]; + let config = Configuration::config(); + assert!(Hrmp::check_outbound_hrmp(&config, para_a, &msgs).is_ok()); + let _ = Hrmp::queue_outbound_hrmp(para_a, msgs.clone()); + + // Verify that the sent messages are there and that also the empty channels are present. + let mqc_heads = Hrmp::hrmp_mqc_heads(para_b); + let contents = Hrmp::inbound_hrmp_channels_contents(para_b); + assert_eq!( + contents, + vec![ + (para_a, vec![InboundHrmpMessage { sent_at: 6, data: b"knock".to_vec() }]), + (para_c, vec![]) + ] + .into_iter() + .collect::<BTreeMap::<_, _>>(), + ); + assert_eq!( + mqc_heads, + vec![ + ( + para_a, + hex_literal::hex!( + "3bba6404e59c91f51deb2ae78f1273ebe75896850713e13f8c0eba4b0996c483" + ) + .into() + ), + (para_c, Default::default()) + ], + ); + + Hrmp::assert_storage_consistency_exhaustive(); + }); +} + +#[test] +fn verify_externally_accessible() { + use primitives::v1::{well_known_keys, AbridgedHrmpChannel}; + + let para_a = 20.into(); + let para_b = 21.into(); + + new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| { + // Register two parachains, wait until a session change, then initiate channel open + // request and accept that, and finally wait until the next session. + register_parachain(para_a); + register_parachain(para_b); + run_to_block(5, Some(vec![4, 5])); + Hrmp::init_open_channel(para_a, para_b, 2, 8).unwrap(); + Hrmp::accept_open_channel(para_b, para_a).unwrap(); + run_to_block(8, Some(vec![8])); + + // Here we have a channel a->b opened. + // + // Try to obtain this channel from the storage and + // decode it into the abridged version. + assert!(channel_exists(para_a, para_b)); + let raw_hrmp_channel = + sp_io::storage::get(&well_known_keys::hrmp_channels(HrmpChannelId { + sender: para_a, + recipient: para_b, + })) + .expect("the channel exists and we must be able to get it through well known keys"); + let abridged_hrmp_channel = AbridgedHrmpChannel::decode(&mut &raw_hrmp_channel[..]) + .expect("HrmpChannel should be decodable as AbridgedHrmpChannel"); + + assert_eq!( + abridged_hrmp_channel, + AbridgedHrmpChannel { + max_capacity: 2, + max_total_size: 16, + max_message_size: 8, + msg_count: 0, + total_size: 0, + mqc_head: None, + }, + ); + + let raw_ingress_index = + sp_io::storage::get(&well_known_keys::hrmp_ingress_channel_index(para_b)) + .expect("the ingress index must be present for para_b"); + let ingress_index = <Vec<ParaId>>::decode(&mut &raw_ingress_index[..]) + .expect("ingress indexx should be decodable as a list of para ids"); + assert_eq!(ingress_index, vec![para_a]); + + // Now, verify that we can access and decode the egress index. + let raw_egress_index = + sp_io::storage::get(&well_known_keys::hrmp_egress_channel_index(para_a)) + .expect("the egress index must be present for para_a"); + let egress_index = <Vec<ParaId>>::decode(&mut &raw_egress_index[..]) + .expect("egress index should be decodable as a list of para ids"); + assert_eq!(egress_index, vec![para_b]); + }); +} + +#[test] +fn charging_deposits() { + let para_a = 32.into(); + let para_b = 64.into(); + + new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| { + register_parachain_with_balance(para_a, 0); + register_parachain(para_b); + run_to_block(5, Some(vec![4, 5])); + + assert_noop!( + Hrmp::init_open_channel(para_a, para_b, 2, 8), + pallet_balances::Error::<Test, _>::InsufficientBalance + ); + }); + + new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| { + register_parachain(para_a); + register_parachain_with_balance(para_b, 0); + run_to_block(5, Some(vec![4, 5])); + + Hrmp::init_open_channel(para_a, para_b, 2, 8).unwrap(); + + assert_noop!( + Hrmp::accept_open_channel(para_b, para_a), + pallet_balances::Error::<Test, _>::InsufficientBalance + ); + }); +} + +#[test] +fn refund_deposit_on_normal_closure() { + let para_a = 32.into(); + let para_b = 64.into(); + + let mut genesis = GenesisConfigBuilder::default(); + genesis.hrmp_sender_deposit = 20; + genesis.hrmp_recipient_deposit = 15; + new_test_ext(genesis.build()).execute_with(|| { + // Register two parachains funded with different amounts of funds and arrange a channel. + register_parachain_with_balance(para_a, 100); + register_parachain_with_balance(para_b, 110); + run_to_block(5, Some(vec![4, 5])); + Hrmp::init_open_channel(para_a, para_b, 2, 8).unwrap(); + Hrmp::accept_open_channel(para_b, para_a).unwrap(); + assert_eq!(<Test as Config>::Currency::free_balance(¶_a.into_account()), 80); + assert_eq!(<Test as Config>::Currency::free_balance(¶_b.into_account()), 95); + run_to_block(8, Some(vec![8])); + + // Now, we close the channel and wait until the next session. + Hrmp::close_channel(para_b, HrmpChannelId { sender: para_a, recipient: para_b }).unwrap(); + run_to_block(10, Some(vec![10])); + assert_eq!(<Test as Config>::Currency::free_balance(¶_a.into_account()), 100); + assert_eq!(<Test as Config>::Currency::free_balance(¶_b.into_account()), 110); + }); +} + +#[test] +fn refund_deposit_on_offboarding() { + let para_a = 32.into(); + let para_b = 64.into(); + + let mut genesis = GenesisConfigBuilder::default(); + genesis.hrmp_sender_deposit = 20; + genesis.hrmp_recipient_deposit = 15; + new_test_ext(genesis.build()).execute_with(|| { + // Register two parachains and open a channel between them. + register_parachain_with_balance(para_a, 100); + register_parachain_with_balance(para_b, 110); + run_to_block(5, Some(vec![4, 5])); + Hrmp::init_open_channel(para_a, para_b, 2, 8).unwrap(); + Hrmp::accept_open_channel(para_b, para_a).unwrap(); + assert_eq!(<Test as Config>::Currency::free_balance(¶_a.into_account()), 80); + assert_eq!(<Test as Config>::Currency::free_balance(¶_b.into_account()), 95); + run_to_block(8, Some(vec![8])); + assert!(channel_exists(para_a, para_b)); + + // Then deregister one parachain. + deregister_parachain(para_a); + run_to_block(10, Some(vec![9, 10])); + + // The channel should be removed. + assert!(!Paras::is_valid_para(para_a)); + assert!(!channel_exists(para_a, para_b)); + Hrmp::assert_storage_consistency_exhaustive(); + + assert_eq!(<Test as Config>::Currency::free_balance(¶_a.into_account()), 100); + assert_eq!(<Test as Config>::Currency::free_balance(¶_b.into_account()), 110); + }); +} + +#[test] +fn no_dangling_open_requests() { + let para_a = 32.into(); + let para_b = 64.into(); + + let mut genesis = GenesisConfigBuilder::default(); + genesis.hrmp_sender_deposit = 20; + genesis.hrmp_recipient_deposit = 15; + new_test_ext(genesis.build()).execute_with(|| { + // Register two parachains and open a channel between them. + register_parachain_with_balance(para_a, 100); + register_parachain_with_balance(para_b, 110); + run_to_block(5, Some(vec![4, 5])); + + // Start opening a channel a->b + Hrmp::init_open_channel(para_a, para_b, 2, 8).unwrap(); + assert_eq!(<Test as Config>::Currency::free_balance(¶_a.into_account()), 80); + + // Then deregister one parachain, but don't wait two sessions until it takes effect. + // Instead, `para_b` will confirm the request, which will take place the same time + // the offboarding should happen. + deregister_parachain(para_a); + run_to_block(9, Some(vec![9])); + Hrmp::accept_open_channel(para_b, para_a).unwrap(); + assert_eq!(<Test as Config>::Currency::free_balance(¶_b.into_account()), 95); + assert!(!channel_exists(para_a, para_b)); + run_to_block(10, Some(vec![10])); + + // The outcome we expect is `para_b` should receive the refund. + assert_eq!(<Test as Config>::Currency::free_balance(¶_b.into_account()), 110); + assert!(!channel_exists(para_a, para_b)); + Hrmp::assert_storage_consistency_exhaustive(); + }); +} + +#[test] +fn cancel_pending_open_channel_request() { + let para_a = 32.into(); + let para_b = 64.into(); + + let mut genesis = GenesisConfigBuilder::default(); + genesis.hrmp_sender_deposit = 20; + genesis.hrmp_recipient_deposit = 15; + new_test_ext(genesis.build()).execute_with(|| { + // Register two parachains and open a channel between them. + register_parachain_with_balance(para_a, 100); + register_parachain_with_balance(para_b, 110); + run_to_block(5, Some(vec![4, 5])); + + // Start opening a channel a->b + Hrmp::init_open_channel(para_a, para_b, 2, 8).unwrap(); + assert_eq!(<Test as Config>::Currency::free_balance(¶_a.into_account()), 80); + + // Cancel opening the channel + Hrmp::cancel_open_request(para_a, HrmpChannelId { sender: para_a, recipient: para_b }) + .unwrap(); + assert_eq!(<Test as Config>::Currency::free_balance(¶_a.into_account()), 100); + + run_to_block(10, Some(vec![10])); + assert!(!channel_exists(para_a, para_b)); + Hrmp::assert_storage_consistency_exhaustive(); + }); +} diff --git a/polkadot/runtime/parachains/src/initializer.rs b/polkadot/runtime/parachains/src/initializer.rs index 123cfad2de0a33244a3839b8158e6bc9594e1cb7..cbfdd84e58a513e69c677278d6fce7b991136064 100644 --- a/polkadot/runtime/parachains/src/initializer.rs +++ b/polkadot/runtime/parachains/src/initializer.rs @@ -34,6 +34,9 @@ use primitives::v1::{BlockNumber, ConsensusLog, SessionIndex, ValidatorId}; use scale_info::TypeInfo; use sp_std::prelude::*; +#[cfg(test)] +mod tests; + #[cfg(feature = "runtime-benchmarks")] mod benchmarking; @@ -328,138 +331,3 @@ impl<T: pallet_session::Config + Config> OneSessionHandler<T::AccountId> for Pal fn on_disabled(_i: u32) {} } - -#[cfg(test)] -mod tests { - use super::*; - use crate::mock::{ - new_test_ext, Configuration, Dmp, Initializer, MockGenesisConfig, Paras, SessionInfo, - System, - }; - use primitives::v1::{HeadData, Id as ParaId}; - use test_helpers::dummy_validation_code; - - use frame_support::{ - assert_ok, - traits::{OnFinalize, OnInitialize}, - }; - - #[test] - fn session_0_is_instantly_applied() { - new_test_ext(Default::default()).execute_with(|| { - Initializer::on_new_session( - false, - 0, - Vec::new().into_iter(), - Some(Vec::new().into_iter()), - ); - - let v = <Initializer as Store>::BufferedSessionChanges::get(); - assert!(v.is_empty()); - - assert_eq!(SessionInfo::earliest_stored_session(), 0); - assert!(SessionInfo::session_info(0).is_some()); - }); - } - - #[test] - fn session_change_before_initialize_is_still_buffered_after() { - new_test_ext(Default::default()).execute_with(|| { - Initializer::on_new_session( - false, - 1, - Vec::new().into_iter(), - Some(Vec::new().into_iter()), - ); - - let now = System::block_number(); - Initializer::on_initialize(now); - - let v = <Initializer as Store>::BufferedSessionChanges::get(); - assert_eq!(v.len(), 1); - }); - } - - #[test] - fn session_change_applied_on_finalize() { - new_test_ext(Default::default()).execute_with(|| { - Initializer::on_initialize(1); - Initializer::on_new_session( - false, - 1, - Vec::new().into_iter(), - Some(Vec::new().into_iter()), - ); - - Initializer::on_finalize(1); - - assert!(<Initializer as Store>::BufferedSessionChanges::get().is_empty()); - }); - } - - #[test] - fn sets_flag_on_initialize() { - new_test_ext(Default::default()).execute_with(|| { - Initializer::on_initialize(1); - - assert!(<Initializer as Store>::HasInitialized::get().is_some()); - }) - } - - #[test] - fn clears_flag_on_finalize() { - new_test_ext(Default::default()).execute_with(|| { - Initializer::on_initialize(1); - Initializer::on_finalize(1); - - assert!(<Initializer as Store>::HasInitialized::get().is_none()); - }) - } - - #[test] - fn scheduled_cleanup_performed() { - let a = ParaId::from(1312); - let b = ParaId::from(228); - let c = ParaId::from(123); - - let mock_genesis = crate::paras::ParaGenesisArgs { - parachain: true, - genesis_head: HeadData(vec![4, 5, 6]), - validation_code: dummy_validation_code(), - }; - - new_test_ext(MockGenesisConfig { - configuration: crate::configuration::GenesisConfig { - config: crate::configuration::HostConfiguration { - max_downward_message_size: 1024, - ..Default::default() - }, - }, - paras: crate::paras::GenesisConfig { - paras: vec![ - (a, mock_genesis.clone()), - (b, mock_genesis.clone()), - (c, mock_genesis.clone()), - ], - ..Default::default() - }, - ..Default::default() - }) - .execute_with(|| { - // enqueue downward messages to A, B and C. - assert_ok!(Dmp::queue_downward_message(&Configuration::config(), a, vec![1, 2, 3])); - assert_ok!(Dmp::queue_downward_message(&Configuration::config(), b, vec![4, 5, 6])); - assert_ok!(Dmp::queue_downward_message(&Configuration::config(), c, vec![7, 8, 9])); - - assert_ok!(Paras::schedule_para_cleanup(a)); - assert_ok!(Paras::schedule_para_cleanup(b)); - - // Apply session 2 in the future - Initializer::apply_new_session(2, vec![], vec![]); - - assert!(Dmp::dmq_contents(a).is_empty()); - assert!(Dmp::dmq_contents(b).is_empty()); - assert!(!Dmp::dmq_contents(c).is_empty()); - }); - } -} diff --git a/polkadot/runtime/parachains/src/initializer/tests.rs b/polkadot/runtime/parachains/src/initializer/tests.rs new file mode 100644 index 0000000000000000000000000000000000000000..6085e5f47168b3822933115bc4f4f3070e590fcb --- /dev/null +++ b/polkadot/runtime/parachains/src/initializer/tests.rs @@ -0,0 +1,131 @@ +// Copyright 2020 Parity Technologies (UK) Ltd. +// This file is part of Polkadot. + +// Polkadot is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Polkadot is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Polkadot. If not, see <http://www.gnu.org/licenses/>. + +use super::*; +use crate::mock::{ + new_test_ext, Configuration, Dmp, Initializer, MockGenesisConfig, Paras, SessionInfo, System, +}; +use primitives::v1::{HeadData, Id as ParaId}; +use test_helpers::dummy_validation_code; + +use frame_support::{ + assert_ok, + traits::{OnFinalize, OnInitialize}, +}; + +#[test] +fn session_0_is_instantly_applied() { + new_test_ext(Default::default()).execute_with(|| { + Initializer::on_new_session(false, 0, Vec::new().into_iter(), Some(Vec::new().into_iter())); + + let v = <Initializer as Store>::BufferedSessionChanges::get(); + assert!(v.is_empty()); + + assert_eq!(SessionInfo::earliest_stored_session(), 0); + assert!(SessionInfo::session_info(0).is_some()); + }); +} + +#[test] +fn session_change_before_initialize_is_still_buffered_after() { + new_test_ext(Default::default()).execute_with(|| { + Initializer::on_new_session(false, 1, Vec::new().into_iter(), Some(Vec::new().into_iter())); + + let now = System::block_number(); + Initializer::on_initialize(now); + + let v = <Initializer as Store>::BufferedSessionChanges::get(); + assert_eq!(v.len(), 1); + }); +} + +#[test] +fn session_change_applied_on_finalize() { + new_test_ext(Default::default()).execute_with(|| { + Initializer::on_initialize(1); + Initializer::on_new_session(false, 1, Vec::new().into_iter(), Some(Vec::new().into_iter())); + + Initializer::on_finalize(1); + + assert!(<Initializer as Store>::BufferedSessionChanges::get().is_empty()); + }); +} + +#[test] +fn sets_flag_on_initialize() { + new_test_ext(Default::default()).execute_with(|| { + Initializer::on_initialize(1); + + assert!(<Initializer as Store>::HasInitialized::get().is_some()); + }) +} + +#[test] +fn clears_flag_on_finalize() { + new_test_ext(Default::default()).execute_with(|| { + Initializer::on_initialize(1); + Initializer::on_finalize(1); + + assert!(<Initializer as Store>::HasInitialized::get().is_none()); + }) +} + +#[test] +fn scheduled_cleanup_performed() { + let a = ParaId::from(1312); + let b = ParaId::from(228); + let c = ParaId::from(123); + + let mock_genesis = crate::paras::ParaGenesisArgs { + parachain: true, + genesis_head: HeadData(vec![4, 5, 6]), + validation_code: dummy_validation_code(), + }; + + new_test_ext(MockGenesisConfig { + configuration: crate::configuration::GenesisConfig { + config: crate::configuration::HostConfiguration { + max_downward_message_size: 1024, + ..Default::default() + }, + }, + paras: crate::paras::GenesisConfig { + paras: vec![ + (a, mock_genesis.clone()), + (b, mock_genesis.clone()), + (c, mock_genesis.clone()), + ], + ..Default::default() + }, + ..Default::default() + }) + .execute_with(|| { + // enqueue downward messages to A, B and C. + assert_ok!(Dmp::queue_downward_message(&Configuration::config(), a, vec![1, 2, 3])); + assert_ok!(Dmp::queue_downward_message(&Configuration::config(), b, vec![4, 5, 6])); + assert_ok!(Dmp::queue_downward_message(&Configuration::config(), c, vec![7, 8, 9])); + + assert_ok!(Paras::schedule_para_cleanup(a)); + assert_ok!(Paras::schedule_para_cleanup(b)); + + // Apply session 2 in the future + Initializer::apply_new_session(2, vec![], vec![]); + + assert!(Dmp::dmq_contents(a).is_empty()); + assert!(Dmp::dmq_contents(b).is_empty()); + assert!(!Dmp::dmq_contents(c).is_empty()); + }); +} diff --git a/polkadot/runtime/parachains/src/scheduler.rs b/polkadot/runtime/parachains/src/scheduler.rs index 7b362322b04ced21ecb5f7aad2bb72eaa017d103..2753fe4e111be1c14e8476867dd32c27f64c064b 100644 --- a/polkadot/runtime/parachains/src/scheduler.rs +++ b/polkadot/runtime/parachains/src/scheduler.rs @@ -48,6 +48,9 @@ use crate::{configuration, initializer::SessionChangeNotification, paras}; pub use pallet::*; +#[cfg(test)] +mod tests; + /// A queued parathread entry, pre-assigned to a core. #[derive(Encode, Decode, TypeInfo)] #[cfg_attr(test, derive(PartialEq, Debug))] @@ -760,1459 +763,3 @@ impl<T: Config> Pallet<T> { }); } } - -#[cfg(test)] -mod tests { - use super::*; - - use frame_support::assert_ok; - use keyring::Sr25519Keyring; - use primitives::v1::{BlockNumber, CollatorId, SessionIndex, ValidatorId}; - - use crate::{ - configuration::HostConfiguration, - initializer::SessionChangeNotification, - mock::{ - new_test_ext, Configuration, MockGenesisConfig, Paras, ParasShared, Scheduler, System, - Test, - }, - paras::ParaGenesisArgs, - }; - - fn schedule_blank_para(id: ParaId, is_chain: bool) { - assert_ok!(Paras::schedule_para_initialize( - id, - ParaGenesisArgs { - genesis_head: Vec::new().into(), - validation_code: vec![1, 2, 3].into(), - parachain: is_chain, - } - )); - } - - fn run_to_block( - to: BlockNumber, - new_session: impl Fn(BlockNumber) -> Option<SessionChangeNotification<BlockNumber>>, - ) { - while System::block_number() < to { - let b = System::block_number(); - - Scheduler::initializer_finalize(); - Paras::initializer_finalize(b); - - if let Some(notification) = new_session(b + 1) { - let mut notification_with_session_index = notification; - // We will make every session change trigger an action queue. Normally this may require 2 or more session changes. - if notification_with_session_index.session_index == SessionIndex::default() { - notification_with_session_index.session_index = - ParasShared::scheduled_session(); - } - Paras::initializer_on_new_session(¬ification_with_session_index); - Scheduler::initializer_on_new_session(¬ification_with_session_index); - } - - System::on_finalize(b); - - System::on_initialize(b + 1); - System::set_block_number(b + 1); - - Paras::initializer_initialize(b + 1); - Scheduler::initializer_initialize(b + 1); - - // In the real runtime this is expected to be called by the `InclusionInherent` pallet. - Scheduler::clear(); - Scheduler::schedule(Vec::new(), b + 1); - } - } - - fn run_to_end_of_block( - to: BlockNumber, - new_session: impl Fn(BlockNumber) -> Option<SessionChangeNotification<BlockNumber>>, - ) { - run_to_block(to, &new_session); - - Scheduler::initializer_finalize(); - Paras::initializer_finalize(to); - - if let Some(notification) = new_session(to + 1) { - Paras::initializer_on_new_session(¬ification); - Scheduler::initializer_on_new_session(¬ification); - } - - System::on_finalize(to); - } - - fn default_config() -> HostConfiguration<BlockNumber> { - HostConfiguration { - parathread_cores: 3, - group_rotation_frequency: 10, - chain_availability_period: 3, - thread_availability_period: 5, - scheduling_lookahead: 2, - parathread_retries: 1, - pvf_checking_enabled: false, - // This field does not affect anything that scheduler does. However, `HostConfiguration` - // is still a subject to consistency test. It requires that `minimum_validation_upgrade_delay` - // is greater than `chain_availability_period` and `thread_availability_period`. - minimum_validation_upgrade_delay: 6, - ..Default::default() - } - } - - #[test] - fn add_parathread_claim_works() { - let genesis_config = MockGenesisConfig { - configuration: crate::configuration::GenesisConfig { - config: default_config(), - ..Default::default() - }, - ..Default::default() - }; - - let thread_id = ParaId::from(10); - let collator = CollatorId::from(Sr25519Keyring::Alice.public()); - - new_test_ext(genesis_config).execute_with(|| { - schedule_blank_para(thread_id, false); - - assert!(!Paras::is_parathread(thread_id)); - - run_to_block(10, |n| if n == 10 { Some(Default::default()) } else { None }); - - assert!(Paras::is_parathread(thread_id)); - - { - Scheduler::add_parathread_claim(ParathreadClaim(thread_id, collator.clone())); - let queue = ParathreadQueue::<Test>::get(); - assert_eq!(queue.next_core_offset, 1); - assert_eq!(queue.queue.len(), 1); - assert_eq!( - queue.queue[0], - QueuedParathread { - claim: ParathreadEntry { - claim: ParathreadClaim(thread_id, collator.clone()), - retries: 0, - }, - core_offset: 0, - } - ); - } - - // due to the index, completing claims are not allowed. - { - let collator2 = CollatorId::from(Sr25519Keyring::Bob.public()); - Scheduler::add_parathread_claim(ParathreadClaim(thread_id, collator2.clone())); - let queue = ParathreadQueue::<Test>::get(); - assert_eq!(queue.next_core_offset, 1); - assert_eq!(queue.queue.len(), 1); - assert_eq!( - queue.queue[0], - QueuedParathread { - claim: ParathreadEntry { - claim: ParathreadClaim(thread_id, collator.clone()), - retries: 0, - }, - core_offset: 0, - } - ); - } - - // claims on non-live parathreads have no effect. - { - let thread_id2 = ParaId::from(11); - Scheduler::add_parathread_claim(ParathreadClaim(thread_id2, collator.clone())); - let queue = ParathreadQueue::<Test>::get(); - assert_eq!(queue.next_core_offset, 1); - assert_eq!(queue.queue.len(), 1); - assert_eq!( - queue.queue[0], - QueuedParathread { - claim: ParathreadEntry { - claim: ParathreadClaim(thread_id, collator.clone()), - retries: 0, - }, - core_offset: 0, - } - ); - } - }) - } - - #[test] - fn cannot_add_claim_when_no_parathread_cores() { - let config = { - let mut config = default_config(); - config.parathread_cores = 0; - config - }; - let genesis_config = MockGenesisConfig { - configuration: crate::configuration::GenesisConfig { config, ..Default::default() }, - ..Default::default() - }; - - let thread_id = ParaId::from(10); - let collator = CollatorId::from(Sr25519Keyring::Alice.public()); - - new_test_ext(genesis_config).execute_with(|| { - schedule_blank_para(thread_id, false); - - assert!(!Paras::is_parathread(thread_id)); - - run_to_block(10, |n| if n == 10 { Some(Default::default()) } else { None }); - - assert!(Paras::is_parathread(thread_id)); - - Scheduler::add_parathread_claim(ParathreadClaim(thread_id, collator.clone())); - assert_eq!(ParathreadQueue::<Test>::get(), Default::default()); - }); - } - - #[test] - fn session_change_prunes_cores_beyond_retries_and_those_from_non_live_parathreads() { - let genesis_config = MockGenesisConfig { - configuration: crate::configuration::GenesisConfig { - config: default_config(), - ..Default::default() - }, - ..Default::default() - }; - let max_parathread_retries = default_config().parathread_retries; - - let thread_a = ParaId::from(1); - let thread_b = ParaId::from(2); - let thread_c = ParaId::from(3); - let thread_d = ParaId::from(4); - - let collator = CollatorId::from(Sr25519Keyring::Alice.public()); - - new_test_ext(genesis_config).execute_with(|| { - assert_eq!(Configuration::config(), default_config()); - - // threads a, b, and c will be live in next session, but not d. - { - schedule_blank_para(thread_a, false); - schedule_blank_para(thread_b, false); - schedule_blank_para(thread_c, false); - } - - // set up a queue as if `n_cores` was 4 and with some with many retries. - ParathreadQueue::<Test>::put({ - let mut queue = ParathreadClaimQueue::default(); - - // Will be pruned: too many retries. - queue.enqueue_entry( - ParathreadEntry { - claim: ParathreadClaim(thread_a, collator.clone()), - retries: max_parathread_retries + 1, - }, - 4, - ); - - // Will not be pruned. - queue.enqueue_entry( - ParathreadEntry { - claim: ParathreadClaim(thread_b, collator.clone()), - retries: max_parathread_retries, - }, - 4, - ); - - // Will not be pruned. - queue.enqueue_entry( - ParathreadEntry { - claim: ParathreadClaim(thread_c, collator.clone()), - retries: 0, - }, - 4, - ); - - // Will be pruned: not a live parathread. - queue.enqueue_entry( - ParathreadEntry { - claim: ParathreadClaim(thread_d, collator.clone()), - retries: 0, - }, - 4, - ); - - queue - }); - - ParathreadClaimIndex::<Test>::put(vec![thread_a, thread_b, thread_c, thread_d]); - - run_to_block(10, |b| match b { - 10 => Some(SessionChangeNotification { - new_config: Configuration::config(), - ..Default::default() - }), - _ => None, - }); - assert_eq!(Configuration::config(), default_config()); - - let queue = ParathreadQueue::<Test>::get(); - assert_eq!( - queue.queue, - vec![ - QueuedParathread { - claim: ParathreadEntry { - claim: ParathreadClaim(thread_b, collator.clone()), - retries: max_parathread_retries, - }, - core_offset: 0, - }, - QueuedParathread { - claim: ParathreadEntry { - claim: ParathreadClaim(thread_c, collator.clone()), - retries: 0, - }, - core_offset: 1, - }, - ] - ); - assert_eq!(queue.next_core_offset, 2); - - assert_eq!(ParathreadClaimIndex::<Test>::get(), vec![thread_b, thread_c]); - }) - } - - #[test] - fn session_change_shuffles_validators() { - let genesis_config = MockGenesisConfig { - configuration: crate::configuration::GenesisConfig { - config: default_config(), - ..Default::default() - }, - ..Default::default() - }; - - assert_eq!(default_config().parathread_cores, 3); - new_test_ext(genesis_config).execute_with(|| { - let chain_a = ParaId::from(1); - let chain_b = ParaId::from(2); - - // ensure that we have 5 groups by registering 2 parachains. - schedule_blank_para(chain_a, true); - schedule_blank_para(chain_b, true); - - run_to_block(1, |number| match number { - 1 => Some(SessionChangeNotification { - new_config: default_config(), - validators: vec![ - ValidatorId::from(Sr25519Keyring::Alice.public()), - ValidatorId::from(Sr25519Keyring::Bob.public()), - ValidatorId::from(Sr25519Keyring::Charlie.public()), - ValidatorId::from(Sr25519Keyring::Dave.public()), - ValidatorId::from(Sr25519Keyring::Eve.public()), - ValidatorId::from(Sr25519Keyring::Ferdie.public()), - ValidatorId::from(Sr25519Keyring::One.public()), - ], - random_seed: [99; 32], - ..Default::default() - }), - _ => None, - }); - - let groups = ValidatorGroups::<Test>::get(); - assert_eq!(groups.len(), 5); - - // first two groups have the overflow. - for i in 0..2 { - assert_eq!(groups[i].len(), 2); - } - - for i in 2..5 { - assert_eq!(groups[i].len(), 1); - } - }); - } - - #[test] - fn session_change_takes_only_max_per_core() { - let config = { - let mut config = default_config(); - config.parathread_cores = 0; - config.max_validators_per_core = Some(1); - config - }; - - let genesis_config = MockGenesisConfig { - configuration: crate::configuration::GenesisConfig { - config: config.clone(), - ..Default::default() - }, - ..Default::default() - }; - - new_test_ext(genesis_config).execute_with(|| { - let chain_a = ParaId::from(1); - let chain_b = ParaId::from(2); - let chain_c = ParaId::from(3); - - // ensure that we have 5 groups by registering 2 parachains. - schedule_blank_para(chain_a, true); - schedule_blank_para(chain_b, true); - schedule_blank_para(chain_c, false); - - run_to_block(1, |number| match number { - 1 => Some(SessionChangeNotification { - new_config: config.clone(), - validators: vec![ - ValidatorId::from(Sr25519Keyring::Alice.public()), - ValidatorId::from(Sr25519Keyring::Bob.public()), - ValidatorId::from(Sr25519Keyring::Charlie.public()), - ValidatorId::from(Sr25519Keyring::Dave.public()), - ValidatorId::from(Sr25519Keyring::Eve.public()), - ValidatorId::from(Sr25519Keyring::Ferdie.public()), - ValidatorId::from(Sr25519Keyring::One.public()), - ], - random_seed: [99; 32], - ..Default::default() - }), - _ => None, - }); - - let groups = ValidatorGroups::<Test>::get(); - assert_eq!(groups.len(), 7); - - // Every validator gets its own group, even though there are 2 paras. - for i in 0..7 { - assert_eq!(groups[i].len(), 1); - } - }); - } - - #[test] - fn schedule_schedules() { - let genesis_config = MockGenesisConfig { - configuration: crate::configuration::GenesisConfig { - config: default_config(), - ..Default::default() - }, - ..Default::default() - }; - - let chain_a = ParaId::from(1); - let chain_b = ParaId::from(2); - - let thread_a = ParaId::from(3); - let thread_b = ParaId::from(4); - let thread_c = ParaId::from(5); - - let collator = CollatorId::from(Sr25519Keyring::Alice.public()); - - new_test_ext(genesis_config).execute_with(|| { - assert_eq!(default_config().parathread_cores, 3); - - // register 2 parachains - schedule_blank_para(chain_a, true); - schedule_blank_para(chain_b, true); - - // and 3 parathreads - schedule_blank_para(thread_a, false); - schedule_blank_para(thread_b, false); - schedule_blank_para(thread_c, false); - - // start a new session to activate, 5 validators for 5 cores. - run_to_block(1, |number| match number { - 1 => Some(SessionChangeNotification { - new_config: default_config(), - validators: vec![ - ValidatorId::from(Sr25519Keyring::Alice.public()), - ValidatorId::from(Sr25519Keyring::Bob.public()), - ValidatorId::from(Sr25519Keyring::Charlie.public()), - ValidatorId::from(Sr25519Keyring::Dave.public()), - ValidatorId::from(Sr25519Keyring::Eve.public()), - ], - ..Default::default() - }), - _ => None, - }); - - { - let scheduled = Scheduler::scheduled(); - assert_eq!(scheduled.len(), 2); - - assert_eq!( - scheduled[0], - CoreAssignment { - core: CoreIndex(0), - para_id: chain_a, - kind: AssignmentKind::Parachain, - group_idx: GroupIndex(0), - } - ); - - assert_eq!( - scheduled[1], - CoreAssignment { - core: CoreIndex(1), - para_id: chain_b, - kind: AssignmentKind::Parachain, - group_idx: GroupIndex(1), - } - ); - } - - // add a couple of parathread claims. - Scheduler::add_parathread_claim(ParathreadClaim(thread_a, collator.clone())); - Scheduler::add_parathread_claim(ParathreadClaim(thread_c, collator.clone())); - - run_to_block(2, |_| None); - - { - let scheduled = Scheduler::scheduled(); - assert_eq!(scheduled.len(), 4); - - assert_eq!( - scheduled[0], - CoreAssignment { - core: CoreIndex(0), - para_id: chain_a, - kind: AssignmentKind::Parachain, - group_idx: GroupIndex(0), - } - ); - - assert_eq!( - scheduled[1], - CoreAssignment { - core: CoreIndex(1), - para_id: chain_b, - kind: AssignmentKind::Parachain, - group_idx: GroupIndex(1), - } - ); - - assert_eq!( - scheduled[2], - CoreAssignment { - core: CoreIndex(2), - para_id: thread_a, - kind: AssignmentKind::Parathread(collator.clone(), 0), - group_idx: GroupIndex(2), - } - ); - - assert_eq!( - scheduled[3], - CoreAssignment { - core: CoreIndex(3), - para_id: thread_c, - kind: AssignmentKind::Parathread(collator.clone(), 0), - group_idx: GroupIndex(3), - } - ); - } - }); - } - - #[test] - fn schedule_schedules_including_just_freed() { - let genesis_config = MockGenesisConfig { - configuration: crate::configuration::GenesisConfig { - config: default_config(), - ..Default::default() - }, - ..Default::default() - }; - - let chain_a = ParaId::from(1); - let chain_b = ParaId::from(2); - - let thread_a = ParaId::from(3); - let thread_b = ParaId::from(4); - let thread_c = ParaId::from(5); - let thread_d = ParaId::from(6); - let thread_e = ParaId::from(7); - - let collator = CollatorId::from(Sr25519Keyring::Alice.public()); - - new_test_ext(genesis_config).execute_with(|| { - assert_eq!(default_config().parathread_cores, 3); - - // register 2 parachains - schedule_blank_para(chain_a, true); - schedule_blank_para(chain_b, true); - - // and 5 parathreads - schedule_blank_para(thread_a, false); - schedule_blank_para(thread_b, false); - schedule_blank_para(thread_c, false); - schedule_blank_para(thread_d, false); - schedule_blank_para(thread_e, false); - - // start a new session to activate, 5 validators for 5 cores. - run_to_block(1, |number| match number { - 1 => Some(SessionChangeNotification { - new_config: default_config(), - validators: vec![ - ValidatorId::from(Sr25519Keyring::Alice.public()), - ValidatorId::from(Sr25519Keyring::Bob.public()), - ValidatorId::from(Sr25519Keyring::Charlie.public()), - ValidatorId::from(Sr25519Keyring::Dave.public()), - ValidatorId::from(Sr25519Keyring::Eve.public()), - ], - ..Default::default() - }), - _ => None, - }); - - // add a couple of parathread claims now that the parathreads are live. - Scheduler::add_parathread_claim(ParathreadClaim(thread_a, collator.clone())); - Scheduler::add_parathread_claim(ParathreadClaim(thread_c, collator.clone())); - - run_to_block(2, |_| None); - - assert_eq!(Scheduler::scheduled().len(), 4); - - // cores 0, 1, 2, and 3 should be occupied. mark them as such. - Scheduler::occupied(&[CoreIndex(0), CoreIndex(1), CoreIndex(2), CoreIndex(3)]); - - { - let cores = AvailabilityCores::<Test>::get(); - - assert!(cores[0].is_some()); - assert!(cores[1].is_some()); - assert!(cores[2].is_some()); - assert!(cores[3].is_some()); - assert!(cores[4].is_none()); - - assert!(Scheduler::scheduled().is_empty()); - } - - // add a couple more parathread claims - the claim on `b` will go to the 3rd parathread core (4) - // and the claim on `d` will go back to the 1st parathread core (2). The claim on `e` then - // will go for core `3`. - Scheduler::add_parathread_claim(ParathreadClaim(thread_b, collator.clone())); - Scheduler::add_parathread_claim(ParathreadClaim(thread_d, collator.clone())); - Scheduler::add_parathread_claim(ParathreadClaim(thread_e, collator.clone())); - - run_to_block(3, |_| None); - - { - let scheduled = Scheduler::scheduled(); - - // cores 0 and 1 are occupied by parachains. cores 2 and 3 are occupied by parathread - // claims. core 4 was free. - assert_eq!(scheduled.len(), 1); - assert_eq!( - scheduled[0], - CoreAssignment { - core: CoreIndex(4), - para_id: thread_b, - kind: AssignmentKind::Parathread(collator.clone(), 0), - group_idx: GroupIndex(4), - } - ); - } - - // now note that cores 0, 2, and 3 were freed. - Scheduler::schedule( - vec![ - (CoreIndex(0), FreedReason::Concluded), - (CoreIndex(2), FreedReason::Concluded), - (CoreIndex(3), FreedReason::TimedOut), // should go back on queue. - ], - 3, - ); - - { - let scheduled = Scheduler::scheduled(); - - // 1 thing scheduled before, + 3 cores freed. - assert_eq!(scheduled.len(), 4); - assert_eq!( - scheduled[0], - CoreAssignment { - core: CoreIndex(0), - para_id: chain_a, - kind: AssignmentKind::Parachain, - group_idx: GroupIndex(0), - } - ); - assert_eq!( - scheduled[1], - CoreAssignment { - core: CoreIndex(2), - para_id: thread_d, - kind: AssignmentKind::Parathread(collator.clone(), 0), - group_idx: GroupIndex(2), - } - ); - assert_eq!( - scheduled[2], - CoreAssignment { - core: CoreIndex(3), - para_id: thread_e, - kind: AssignmentKind::Parathread(collator.clone(), 0), - group_idx: GroupIndex(3), - } - ); - assert_eq!( - scheduled[3], - CoreAssignment { - core: CoreIndex(4), - para_id: thread_b, - kind: AssignmentKind::Parathread(collator.clone(), 0), - group_idx: GroupIndex(4), - } - ); - - // the prior claim on thread A concluded, but the claim on thread C was marked as - // timed out. - let index = ParathreadClaimIndex::<Test>::get(); - let parathread_queue = ParathreadQueue::<Test>::get(); - - // thread A claim should have been wiped, but thread C claim should remain. - assert_eq!(index, vec![thread_b, thread_c, thread_d, thread_e]); - - // Although C was descheduled, the core `4` was occupied so C goes back on the queue. - assert_eq!(parathread_queue.queue.len(), 1); - assert_eq!( - parathread_queue.queue[0], - QueuedParathread { - claim: ParathreadEntry { - claim: ParathreadClaim(thread_c, collator.clone()), - retries: 0, // retries not incremented by timeout - validators' fault. - }, - core_offset: 2, // reassigned to next core. thread_e claim was on offset 1. - } - ); - } - }); - } - - #[test] - fn schedule_clears_availability_cores() { - let genesis_config = MockGenesisConfig { - configuration: crate::configuration::GenesisConfig { - config: default_config(), - ..Default::default() - }, - ..Default::default() - }; - - let chain_a = ParaId::from(1); - let chain_b = ParaId::from(2); - let chain_c = ParaId::from(3); - - new_test_ext(genesis_config).execute_with(|| { - assert_eq!(default_config().parathread_cores, 3); - - // register 3 parachains - schedule_blank_para(chain_a, true); - schedule_blank_para(chain_b, true); - schedule_blank_para(chain_c, true); - - // start a new session to activate, 5 validators for 5 cores. - run_to_block(1, |number| match number { - 1 => Some(SessionChangeNotification { - new_config: default_config(), - validators: vec![ - ValidatorId::from(Sr25519Keyring::Alice.public()), - ValidatorId::from(Sr25519Keyring::Bob.public()), - ValidatorId::from(Sr25519Keyring::Charlie.public()), - ValidatorId::from(Sr25519Keyring::Dave.public()), - ValidatorId::from(Sr25519Keyring::Eve.public()), - ], - ..Default::default() - }), - _ => None, - }); - - run_to_block(2, |_| None); - - assert_eq!(Scheduler::scheduled().len(), 3); - - // cores 0, 1, and 2 should be occupied. mark them as such. - Scheduler::occupied(&[CoreIndex(0), CoreIndex(1), CoreIndex(2)]); - - { - let cores = AvailabilityCores::<Test>::get(); - - assert!(cores[0].is_some()); - assert!(cores[1].is_some()); - assert!(cores[2].is_some()); - - assert!(Scheduler::scheduled().is_empty()); - } - - run_to_block(3, |_| None); - - // now note that cores 0 and 2 were freed. - Scheduler::schedule( - vec![ - (CoreIndex(0), FreedReason::Concluded), - (CoreIndex(2), FreedReason::Concluded), - ], - 3, - ); - - { - let scheduled = Scheduler::scheduled(); - - assert_eq!(scheduled.len(), 2); - assert_eq!( - scheduled[0], - CoreAssignment { - core: CoreIndex(0), - para_id: chain_a, - kind: AssignmentKind::Parachain, - group_idx: GroupIndex(0), - } - ); - assert_eq!( - scheduled[1], - CoreAssignment { - core: CoreIndex(2), - para_id: chain_c, - kind: AssignmentKind::Parachain, - group_idx: GroupIndex(2), - } - ); - - // The freed cores should be `None` in `AvailabilityCores`. - let cores = AvailabilityCores::<Test>::get(); - assert!(cores[0].is_none()); - assert!(cores[2].is_none()); - } - }); - } - - #[test] - fn schedule_rotates_groups() { - let config = { - let mut config = default_config(); - - // make sure parathread requests don't retry-out - config.parathread_retries = config.group_rotation_frequency * 3; - config.parathread_cores = 2; - config - }; - - let rotation_frequency = config.group_rotation_frequency; - let parathread_cores = config.parathread_cores; - - let genesis_config = MockGenesisConfig { - configuration: crate::configuration::GenesisConfig { - config: config.clone(), - ..Default::default() - }, - ..Default::default() - }; - - let thread_a = ParaId::from(1); - let thread_b = ParaId::from(2); - - let collator = CollatorId::from(Sr25519Keyring::Alice.public()); - - new_test_ext(genesis_config).execute_with(|| { - assert_eq!(default_config().parathread_cores, 3); - - schedule_blank_para(thread_a, false); - schedule_blank_para(thread_b, false); - - // start a new session to activate, 5 validators for 5 cores. - run_to_block(1, |number| match number { - 1 => Some(SessionChangeNotification { - new_config: config.clone(), - validators: vec![ - ValidatorId::from(Sr25519Keyring::Alice.public()), - ValidatorId::from(Sr25519Keyring::Eve.public()), - ], - ..Default::default() - }), - _ => None, - }); - - let session_start_block = <Scheduler as Store>::SessionStartBlock::get(); - assert_eq!(session_start_block, 1); - - Scheduler::add_parathread_claim(ParathreadClaim(thread_a, collator.clone())); - Scheduler::add_parathread_claim(ParathreadClaim(thread_b, collator.clone())); - - run_to_block(2, |_| None); - - let assert_groups_rotated = |rotations: u32| { - let scheduled = Scheduler::scheduled(); - assert_eq!(scheduled.len(), 2); - assert_eq!( - scheduled[0].group_idx, - GroupIndex((0u32 + rotations) % parathread_cores) - ); - assert_eq!( - scheduled[1].group_idx, - GroupIndex((1u32 + rotations) % parathread_cores) - ); - }; - - assert_groups_rotated(0); - - // one block before first rotation. - run_to_block(rotation_frequency, |_| None); - - assert_groups_rotated(0); - - // first rotation. - run_to_block(rotation_frequency + 1, |_| None); - assert_groups_rotated(1); - - // one block before second rotation. - run_to_block(rotation_frequency * 2, |_| None); - assert_groups_rotated(1); - - // second rotation. - run_to_block(rotation_frequency * 2 + 1, |_| None); - assert_groups_rotated(2); - }); - } - - #[test] - fn parathread_claims_are_pruned_after_retries() { - let max_retries = default_config().parathread_retries; - - let genesis_config = MockGenesisConfig { - configuration: crate::configuration::GenesisConfig { - config: default_config(), - ..Default::default() - }, - ..Default::default() - }; - - let thread_a = ParaId::from(1); - let thread_b = ParaId::from(2); - - let collator = CollatorId::from(Sr25519Keyring::Alice.public()); - - new_test_ext(genesis_config).execute_with(|| { - assert_eq!(default_config().parathread_cores, 3); - - schedule_blank_para(thread_a, false); - schedule_blank_para(thread_b, false); - - // start a new session to activate, 5 validators for 5 cores. - run_to_block(1, |number| match number { - 1 => Some(SessionChangeNotification { - new_config: default_config(), - validators: vec![ - ValidatorId::from(Sr25519Keyring::Alice.public()), - ValidatorId::from(Sr25519Keyring::Eve.public()), - ], - ..Default::default() - }), - _ => None, - }); - - Scheduler::add_parathread_claim(ParathreadClaim(thread_a, collator.clone())); - Scheduler::add_parathread_claim(ParathreadClaim(thread_b, collator.clone())); - - run_to_block(2, |_| None); - assert_eq!(Scheduler::scheduled().len(), 2); - - run_to_block(2 + max_retries, |_| None); - assert_eq!(Scheduler::scheduled().len(), 2); - - run_to_block(2 + max_retries + 1, |_| None); - assert_eq!(Scheduler::scheduled().len(), 0); - }); - } - - #[test] - fn availability_predicate_works() { - let genesis_config = MockGenesisConfig { - configuration: crate::configuration::GenesisConfig { - config: default_config(), - ..Default::default() - }, - ..Default::default() - }; - - let HostConfiguration { - group_rotation_frequency, - chain_availability_period, - thread_availability_period, - .. - } = default_config(); - let collator = CollatorId::from(Sr25519Keyring::Alice.public()); - - assert!( - chain_availability_period < thread_availability_period && - thread_availability_period < group_rotation_frequency - ); - - let chain_a = ParaId::from(1); - let thread_a = ParaId::from(2); - - new_test_ext(genesis_config).execute_with(|| { - schedule_blank_para(chain_a, true); - schedule_blank_para(thread_a, false); - - // start a new session with our chain & thread registered. - run_to_block(1, |number| match number { - 1 => Some(SessionChangeNotification { - new_config: default_config(), - validators: vec![ - ValidatorId::from(Sr25519Keyring::Alice.public()), - ValidatorId::from(Sr25519Keyring::Bob.public()), - ValidatorId::from(Sr25519Keyring::Charlie.public()), - ValidatorId::from(Sr25519Keyring::Dave.public()), - ValidatorId::from(Sr25519Keyring::Eve.public()), - ], - ..Default::default() - }), - _ => None, - }); - - // assign some availability cores. - { - AvailabilityCores::<Test>::mutate(|cores| { - cores[0] = Some(CoreOccupied::Parachain); - cores[1] = Some(CoreOccupied::Parathread(ParathreadEntry { - claim: ParathreadClaim(thread_a, collator), - retries: 0, - })) - }); - } - - run_to_block(1 + thread_availability_period, |_| None); - assert!(Scheduler::availability_timeout_predicate().is_none()); - - run_to_block(1 + group_rotation_frequency, |_| None); - - { - let pred = Scheduler::availability_timeout_predicate() - .expect("predicate exists recently after rotation"); - - let now = System::block_number(); - let would_be_timed_out = now - thread_availability_period; - for i in 0..AvailabilityCores::<Test>::get().len() { - // returns true for unoccupied cores. - // And can time out both threads and chains at this stage. - assert!(pred(CoreIndex(i as u32), would_be_timed_out)); - } - - assert!(!pred(CoreIndex(0), now)); // assigned: chain - assert!(!pred(CoreIndex(1), now)); // assigned: thread - assert!(pred(CoreIndex(2), now)); - - // check the tighter bound on chains vs threads. - assert!(pred(CoreIndex(0), now - chain_availability_period)); - assert!(!pred(CoreIndex(1), now - chain_availability_period)); - - // check the threshold is exact. - assert!(!pred(CoreIndex(0), now - chain_availability_period + 1)); - assert!(!pred(CoreIndex(1), now - thread_availability_period + 1)); - } - - run_to_block(1 + group_rotation_frequency + chain_availability_period, |_| None); - - { - let pred = Scheduler::availability_timeout_predicate() - .expect("predicate exists recently after rotation"); - - let would_be_timed_out = System::block_number() - thread_availability_period; - - assert!(!pred(CoreIndex(0), would_be_timed_out)); // chains can't be timed out now. - assert!(pred(CoreIndex(1), would_be_timed_out)); // but threads can. - } - - run_to_block(1 + group_rotation_frequency + thread_availability_period, |_| None); - - assert!(Scheduler::availability_timeout_predicate().is_none()); - }); - } - - #[test] - fn next_up_on_available_uses_next_scheduled_or_none_for_thread() { - let mut config = default_config(); - config.parathread_cores = 1; - - let genesis_config = MockGenesisConfig { - configuration: crate::configuration::GenesisConfig { - config: config.clone(), - ..Default::default() - }, - ..Default::default() - }; - - let thread_a = ParaId::from(1); - let thread_b = ParaId::from(2); - - let collator = CollatorId::from(Sr25519Keyring::Alice.public()); - - new_test_ext(genesis_config).execute_with(|| { - schedule_blank_para(thread_a, false); - schedule_blank_para(thread_b, false); - - // start a new session to activate, 5 validators for 5 cores. - run_to_block(1, |number| match number { - 1 => Some(SessionChangeNotification { - new_config: config.clone(), - validators: vec![ - ValidatorId::from(Sr25519Keyring::Alice.public()), - ValidatorId::from(Sr25519Keyring::Eve.public()), - ], - ..Default::default() - }), - _ => None, - }); - - let thread_claim_a = ParathreadClaim(thread_a, collator.clone()); - let thread_claim_b = ParathreadClaim(thread_b, collator.clone()); - - Scheduler::add_parathread_claim(thread_claim_a.clone()); - - run_to_block(2, |_| None); - - { - assert_eq!(Scheduler::scheduled().len(), 1); - assert_eq!(Scheduler::availability_cores().len(), 1); - - Scheduler::occupied(&[CoreIndex(0)]); - - let cores = Scheduler::availability_cores(); - match cores[0].as_ref().unwrap() { - CoreOccupied::Parathread(entry) => assert_eq!(entry.claim, thread_claim_a), - _ => panic!("with no chains, only core should be a thread core"), - } - - assert!(Scheduler::next_up_on_available(CoreIndex(0)).is_none()); - - Scheduler::add_parathread_claim(thread_claim_b); - - let queue = ParathreadQueue::<Test>::get(); - assert_eq!( - queue.get_next_on_core(0).unwrap().claim, - ParathreadClaim(thread_b, collator.clone()), - ); - - assert_eq!( - Scheduler::next_up_on_available(CoreIndex(0)).unwrap(), - ScheduledCore { para_id: thread_b, collator: Some(collator.clone()) } - ); - } - }); - } - - #[test] - fn next_up_on_time_out_reuses_claim_if_nothing_queued() { - let mut config = default_config(); - config.parathread_cores = 1; - - let genesis_config = MockGenesisConfig { - configuration: crate::configuration::GenesisConfig { - config: config.clone(), - ..Default::default() - }, - ..Default::default() - }; - - let thread_a = ParaId::from(1); - let thread_b = ParaId::from(2); - - let collator = CollatorId::from(Sr25519Keyring::Alice.public()); - - new_test_ext(genesis_config).execute_with(|| { - schedule_blank_para(thread_a, false); - schedule_blank_para(thread_b, false); - - // start a new session to activate, 5 validators for 5 cores. - run_to_block(1, |number| match number { - 1 => Some(SessionChangeNotification { - new_config: config.clone(), - validators: vec![ - ValidatorId::from(Sr25519Keyring::Alice.public()), - ValidatorId::from(Sr25519Keyring::Eve.public()), - ], - ..Default::default() - }), - _ => None, - }); - - let thread_claim_a = ParathreadClaim(thread_a, collator.clone()); - let thread_claim_b = ParathreadClaim(thread_b, collator.clone()); - - Scheduler::add_parathread_claim(thread_claim_a.clone()); - - run_to_block(2, |_| None); - - { - assert_eq!(Scheduler::scheduled().len(), 1); - assert_eq!(Scheduler::availability_cores().len(), 1); - - Scheduler::occupied(&[CoreIndex(0)]); - - let cores = Scheduler::availability_cores(); - match cores[0].as_ref().unwrap() { - CoreOccupied::Parathread(entry) => assert_eq!(entry.claim, thread_claim_a), - _ => panic!("with no chains, only core should be a thread core"), - } - - let queue = ParathreadQueue::<Test>::get(); - assert!(queue.get_next_on_core(0).is_none()); - assert_eq!( - Scheduler::next_up_on_time_out(CoreIndex(0)).unwrap(), - ScheduledCore { para_id: thread_a, collator: Some(collator.clone()) } - ); - - Scheduler::add_parathread_claim(thread_claim_b); - - let queue = ParathreadQueue::<Test>::get(); - assert_eq!( - queue.get_next_on_core(0).unwrap().claim, - ParathreadClaim(thread_b, collator.clone()), - ); - - // Now that there is an earlier next-up, we use that. - assert_eq!( - Scheduler::next_up_on_available(CoreIndex(0)).unwrap(), - ScheduledCore { para_id: thread_b, collator: Some(collator.clone()) } - ); - } - }); - } - - #[test] - fn next_up_on_available_is_parachain_always() { - let mut config = default_config(); - config.parathread_cores = 0; - - let genesis_config = MockGenesisConfig { - configuration: crate::configuration::GenesisConfig { - config: config.clone(), - ..Default::default() - }, - ..Default::default() - }; - - let chain_a = ParaId::from(1); - - new_test_ext(genesis_config).execute_with(|| { - schedule_blank_para(chain_a, true); - - // start a new session to activate, 5 validators for 5 cores. - run_to_block(1, |number| match number { - 1 => Some(SessionChangeNotification { - new_config: config.clone(), - validators: vec![ - ValidatorId::from(Sr25519Keyring::Alice.public()), - ValidatorId::from(Sr25519Keyring::Eve.public()), - ], - ..Default::default() - }), - _ => None, - }); - - run_to_block(2, |_| None); - - { - assert_eq!(Scheduler::scheduled().len(), 1); - assert_eq!(Scheduler::availability_cores().len(), 1); - - Scheduler::occupied(&[CoreIndex(0)]); - - let cores = Scheduler::availability_cores(); - match cores[0].as_ref().unwrap() { - CoreOccupied::Parachain => {}, - _ => panic!("with no threads, only core should be a chain core"), - } - - // Now that there is an earlier next-up, we use that. - assert_eq!( - Scheduler::next_up_on_available(CoreIndex(0)).unwrap(), - ScheduledCore { para_id: chain_a, collator: None } - ); - } - }); - } - - #[test] - fn next_up_on_time_out_is_parachain_always() { - let mut config = default_config(); - config.parathread_cores = 0; - - let genesis_config = MockGenesisConfig { - configuration: crate::configuration::GenesisConfig { - config: config.clone(), - ..Default::default() - }, - ..Default::default() - }; - - let chain_a = ParaId::from(1); - - new_test_ext(genesis_config).execute_with(|| { - schedule_blank_para(chain_a, true); - - // start a new session to activate, 5 validators for 5 cores. - run_to_block(1, |number| match number { - 1 => Some(SessionChangeNotification { - new_config: config.clone(), - validators: vec![ - ValidatorId::from(Sr25519Keyring::Alice.public()), - ValidatorId::from(Sr25519Keyring::Eve.public()), - ], - ..Default::default() - }), - _ => None, - }); - - run_to_block(2, |_| None); - - { - assert_eq!(Scheduler::scheduled().len(), 1); - assert_eq!(Scheduler::availability_cores().len(), 1); - - Scheduler::occupied(&[CoreIndex(0)]); - - let cores = Scheduler::availability_cores(); - match cores[0].as_ref().unwrap() { - CoreOccupied::Parachain => {}, - _ => panic!("with no threads, only core should be a chain core"), - } - - // Now that there is an earlier next-up, we use that. - assert_eq!( - Scheduler::next_up_on_available(CoreIndex(0)).unwrap(), - ScheduledCore { para_id: chain_a, collator: None } - ); - } - }); - } - - #[test] - fn session_change_requires_reschedule_dropping_removed_paras() { - let genesis_config = MockGenesisConfig { - configuration: crate::configuration::GenesisConfig { - config: default_config(), - ..Default::default() - }, - ..Default::default() - }; - - assert_eq!(default_config().parathread_cores, 3); - new_test_ext(genesis_config).execute_with(|| { - let chain_a = ParaId::from(1); - let chain_b = ParaId::from(2); - - // ensure that we have 5 groups by registering 2 parachains. - schedule_blank_para(chain_a, true); - schedule_blank_para(chain_b, true); - - run_to_block(1, |number| match number { - 1 => Some(SessionChangeNotification { - new_config: default_config(), - validators: vec![ - ValidatorId::from(Sr25519Keyring::Alice.public()), - ValidatorId::from(Sr25519Keyring::Bob.public()), - ValidatorId::from(Sr25519Keyring::Charlie.public()), - ValidatorId::from(Sr25519Keyring::Dave.public()), - ValidatorId::from(Sr25519Keyring::Eve.public()), - ValidatorId::from(Sr25519Keyring::Ferdie.public()), - ValidatorId::from(Sr25519Keyring::One.public()), - ], - random_seed: [99; 32], - ..Default::default() - }), - _ => None, - }); - - assert_eq!(Scheduler::scheduled().len(), 2); - - let groups = ValidatorGroups::<Test>::get(); - assert_eq!(groups.len(), 5); - - assert_ok!(Paras::schedule_para_cleanup(chain_b)); - - run_to_end_of_block(2, |number| match number { - 2 => Some(SessionChangeNotification { - new_config: default_config(), - validators: vec![ - ValidatorId::from(Sr25519Keyring::Alice.public()), - ValidatorId::from(Sr25519Keyring::Bob.public()), - ValidatorId::from(Sr25519Keyring::Charlie.public()), - ValidatorId::from(Sr25519Keyring::Dave.public()), - ValidatorId::from(Sr25519Keyring::Eve.public()), - ValidatorId::from(Sr25519Keyring::Ferdie.public()), - ValidatorId::from(Sr25519Keyring::One.public()), - ], - random_seed: [99; 32], - ..Default::default() - }), - _ => None, - }); - - Scheduler::clear(); - Scheduler::schedule(Vec::new(), 3); - - assert_eq!( - Scheduler::scheduled(), - vec![CoreAssignment { - core: CoreIndex(0), - para_id: chain_a, - kind: AssignmentKind::Parachain, - group_idx: GroupIndex(0), - }], - ); - }); - } - - #[test] - fn parathread_claims_are_pruned_after_deregistration() { - let genesis_config = MockGenesisConfig { - configuration: crate::configuration::GenesisConfig { - config: default_config(), - ..Default::default() - }, - ..Default::default() - }; - - let thread_a = ParaId::from(1); - let thread_b = ParaId::from(2); - - let collator = CollatorId::from(Sr25519Keyring::Alice.public()); - - new_test_ext(genesis_config).execute_with(|| { - assert_eq!(default_config().parathread_cores, 3); - - schedule_blank_para(thread_a, false); - schedule_blank_para(thread_b, false); - - // start a new session to activate, 5 validators for 5 cores. - run_to_block(1, |number| match number { - 1 => Some(SessionChangeNotification { - new_config: default_config(), - validators: vec![ - ValidatorId::from(Sr25519Keyring::Alice.public()), - ValidatorId::from(Sr25519Keyring::Eve.public()), - ], - ..Default::default() - }), - _ => None, - }); - - Scheduler::add_parathread_claim(ParathreadClaim(thread_a, collator.clone())); - Scheduler::add_parathread_claim(ParathreadClaim(thread_b, collator.clone())); - - run_to_block(2, |_| None); - assert_eq!(Scheduler::scheduled().len(), 2); - - assert_ok!(Paras::schedule_para_cleanup(thread_a)); - - // start a new session to activate, 5 validators for 5 cores. - run_to_block(3, |number| match number { - 3 => Some(SessionChangeNotification { - new_config: default_config(), - validators: vec![ - ValidatorId::from(Sr25519Keyring::Alice.public()), - ValidatorId::from(Sr25519Keyring::Eve.public()), - ], - ..Default::default() - }), - _ => None, - }); - - assert_eq!(Scheduler::scheduled().len(), 1); - }); - } -} diff --git a/polkadot/runtime/parachains/src/scheduler/tests.rs b/polkadot/runtime/parachains/src/scheduler/tests.rs new file mode 100644 index 0000000000000000000000000000000000000000..234c1833d9f0f2d7b3b7042d6252f27d193b293b --- /dev/null +++ b/polkadot/runtime/parachains/src/scheduler/tests.rs @@ -0,0 +1,1451 @@ +// Copyright 2020 Parity Technologies (UK) Ltd. +// This file is part of Polkadot. + +// Polkadot is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Polkadot is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Polkadot. If not, see <http://www.gnu.org/licenses/>. + +use super::*; + +use frame_support::assert_ok; +use keyring::Sr25519Keyring; +use primitives::v1::{BlockNumber, CollatorId, SessionIndex, ValidatorId}; + +use crate::{ + configuration::HostConfiguration, + initializer::SessionChangeNotification, + mock::{ + new_test_ext, Configuration, MockGenesisConfig, Paras, ParasShared, Scheduler, System, Test, + }, + paras::ParaGenesisArgs, +}; + +fn schedule_blank_para(id: ParaId, is_chain: bool) { + assert_ok!(Paras::schedule_para_initialize( + id, + ParaGenesisArgs { + genesis_head: Vec::new().into(), + validation_code: vec![1, 2, 3].into(), + parachain: is_chain, + } + )); +} + +fn run_to_block( + to: BlockNumber, + new_session: impl Fn(BlockNumber) -> Option<SessionChangeNotification<BlockNumber>>, +) { + while System::block_number() < to { + let b = System::block_number(); + + Scheduler::initializer_finalize(); + Paras::initializer_finalize(b); + + if let Some(notification) = new_session(b + 1) { + let mut notification_with_session_index = notification; + // We will make every session change trigger an action queue. Normally this may require 2 or more session changes. + if notification_with_session_index.session_index == SessionIndex::default() { + notification_with_session_index.session_index = ParasShared::scheduled_session(); + } + Paras::initializer_on_new_session(¬ification_with_session_index); + Scheduler::initializer_on_new_session(¬ification_with_session_index); + } + + System::on_finalize(b); + + System::on_initialize(b + 1); + System::set_block_number(b + 1); + + Paras::initializer_initialize(b + 1); + Scheduler::initializer_initialize(b + 1); + + // In the real runtime this is expected to be called by the `InclusionInherent` pallet. + Scheduler::clear(); + Scheduler::schedule(Vec::new(), b + 1); + } +} + +fn run_to_end_of_block( + to: BlockNumber, + new_session: impl Fn(BlockNumber) -> Option<SessionChangeNotification<BlockNumber>>, +) { + run_to_block(to, &new_session); + + Scheduler::initializer_finalize(); + Paras::initializer_finalize(to); + + if let Some(notification) = new_session(to + 1) { + Paras::initializer_on_new_session(¬ification); + Scheduler::initializer_on_new_session(¬ification); + } + + System::on_finalize(to); +} + +fn default_config() -> HostConfiguration<BlockNumber> { + HostConfiguration { + parathread_cores: 3, + group_rotation_frequency: 10, + chain_availability_period: 3, + thread_availability_period: 5, + scheduling_lookahead: 2, + parathread_retries: 1, + pvf_checking_enabled: false, + // This field does not affect anything that scheduler does. However, `HostConfiguration` + // is still a subject to consistency test. It requires that `minimum_validation_upgrade_delay` + // is greater than `chain_availability_period` and `thread_availability_period`. + minimum_validation_upgrade_delay: 6, + ..Default::default() + } +} + +#[test] +fn add_parathread_claim_works() { + let genesis_config = MockGenesisConfig { + configuration: crate::configuration::GenesisConfig { + config: default_config(), + ..Default::default() + }, + ..Default::default() + }; + + let thread_id = ParaId::from(10); + let collator = CollatorId::from(Sr25519Keyring::Alice.public()); + + new_test_ext(genesis_config).execute_with(|| { + schedule_blank_para(thread_id, false); + + assert!(!Paras::is_parathread(thread_id)); + + run_to_block(10, |n| if n == 10 { Some(Default::default()) } else { None }); + + assert!(Paras::is_parathread(thread_id)); + + { + Scheduler::add_parathread_claim(ParathreadClaim(thread_id, collator.clone())); + let queue = ParathreadQueue::<Test>::get(); + assert_eq!(queue.next_core_offset, 1); + assert_eq!(queue.queue.len(), 1); + assert_eq!( + queue.queue[0], + QueuedParathread { + claim: ParathreadEntry { + claim: ParathreadClaim(thread_id, collator.clone()), + retries: 0, + }, + core_offset: 0, + } + ); + } + + // due to the index, completing claims are not allowed. + { + let collator2 = CollatorId::from(Sr25519Keyring::Bob.public()); + Scheduler::add_parathread_claim(ParathreadClaim(thread_id, collator2.clone())); + let queue = ParathreadQueue::<Test>::get(); + assert_eq!(queue.next_core_offset, 1); + assert_eq!(queue.queue.len(), 1); + assert_eq!( + queue.queue[0], + QueuedParathread { + claim: ParathreadEntry { + claim: ParathreadClaim(thread_id, collator.clone()), + retries: 0, + }, + core_offset: 0, + } + ); + } + + // claims on non-live parathreads have no effect. + { + let thread_id2 = ParaId::from(11); + Scheduler::add_parathread_claim(ParathreadClaim(thread_id2, collator.clone())); + let queue = ParathreadQueue::<Test>::get(); + assert_eq!(queue.next_core_offset, 1); + assert_eq!(queue.queue.len(), 1); + assert_eq!( + queue.queue[0], + QueuedParathread { + claim: ParathreadEntry { + claim: ParathreadClaim(thread_id, collator.clone()), + retries: 0, + }, + core_offset: 0, + } + ); + } + }) +} + +#[test] +fn cannot_add_claim_when_no_parathread_cores() { + let config = { + let mut config = default_config(); + config.parathread_cores = 0; + config + }; + let genesis_config = MockGenesisConfig { + configuration: crate::configuration::GenesisConfig { config, ..Default::default() }, + ..Default::default() + }; + + let thread_id = ParaId::from(10); + let collator = CollatorId::from(Sr25519Keyring::Alice.public()); + + new_test_ext(genesis_config).execute_with(|| { + schedule_blank_para(thread_id, false); + + assert!(!Paras::is_parathread(thread_id)); + + run_to_block(10, |n| if n == 10 { Some(Default::default()) } else { None }); + + assert!(Paras::is_parathread(thread_id)); + + Scheduler::add_parathread_claim(ParathreadClaim(thread_id, collator.clone())); + assert_eq!(ParathreadQueue::<Test>::get(), Default::default()); + }); +} + +#[test] +fn session_change_prunes_cores_beyond_retries_and_those_from_non_live_parathreads() { + let genesis_config = MockGenesisConfig { + configuration: crate::configuration::GenesisConfig { + config: default_config(), + ..Default::default() + }, + ..Default::default() + }; + let max_parathread_retries = default_config().parathread_retries; + + let thread_a = ParaId::from(1); + let thread_b = ParaId::from(2); + let thread_c = ParaId::from(3); + let thread_d = ParaId::from(4); + + let collator = CollatorId::from(Sr25519Keyring::Alice.public()); + + new_test_ext(genesis_config).execute_with(|| { + assert_eq!(Configuration::config(), default_config()); + + // threads a, b, and c will be live in next session, but not d. + { + schedule_blank_para(thread_a, false); + schedule_blank_para(thread_b, false); + schedule_blank_para(thread_c, false); + } + + // set up a queue as if `n_cores` was 4 and with some with many retries. + ParathreadQueue::<Test>::put({ + let mut queue = ParathreadClaimQueue::default(); + + // Will be pruned: too many retries. + queue.enqueue_entry( + ParathreadEntry { + claim: ParathreadClaim(thread_a, collator.clone()), + retries: max_parathread_retries + 1, + }, + 4, + ); + + // Will not be pruned. + queue.enqueue_entry( + ParathreadEntry { + claim: ParathreadClaim(thread_b, collator.clone()), + retries: max_parathread_retries, + }, + 4, + ); + + // Will not be pruned. + queue.enqueue_entry( + ParathreadEntry { claim: ParathreadClaim(thread_c, collator.clone()), retries: 0 }, + 4, + ); + + // Will be pruned: not a live parathread. + queue.enqueue_entry( + ParathreadEntry { claim: ParathreadClaim(thread_d, collator.clone()), retries: 0 }, + 4, + ); + + queue + }); + + ParathreadClaimIndex::<Test>::put(vec![thread_a, thread_b, thread_c, thread_d]); + + run_to_block(10, |b| match b { + 10 => Some(SessionChangeNotification { + new_config: Configuration::config(), + ..Default::default() + }), + _ => None, + }); + assert_eq!(Configuration::config(), default_config()); + + let queue = ParathreadQueue::<Test>::get(); + assert_eq!( + queue.queue, + vec![ + QueuedParathread { + claim: ParathreadEntry { + claim: ParathreadClaim(thread_b, collator.clone()), + retries: max_parathread_retries, + }, + core_offset: 0, + }, + QueuedParathread { + claim: ParathreadEntry { + claim: ParathreadClaim(thread_c, collator.clone()), + retries: 0, + }, + core_offset: 1, + }, + ] + ); + assert_eq!(queue.next_core_offset, 2); + + assert_eq!(ParathreadClaimIndex::<Test>::get(), vec![thread_b, thread_c]); + }) +} + +#[test] +fn session_change_shuffles_validators() { + let genesis_config = MockGenesisConfig { + configuration: crate::configuration::GenesisConfig { + config: default_config(), + ..Default::default() + }, + ..Default::default() + }; + + assert_eq!(default_config().parathread_cores, 3); + new_test_ext(genesis_config).execute_with(|| { + let chain_a = ParaId::from(1); + let chain_b = ParaId::from(2); + + // ensure that we have 5 groups by registering 2 parachains. + schedule_blank_para(chain_a, true); + schedule_blank_para(chain_b, true); + + run_to_block(1, |number| match number { + 1 => Some(SessionChangeNotification { + new_config: default_config(), + validators: vec![ + ValidatorId::from(Sr25519Keyring::Alice.public()), + ValidatorId::from(Sr25519Keyring::Bob.public()), + ValidatorId::from(Sr25519Keyring::Charlie.public()), + ValidatorId::from(Sr25519Keyring::Dave.public()), + ValidatorId::from(Sr25519Keyring::Eve.public()), + ValidatorId::from(Sr25519Keyring::Ferdie.public()), + ValidatorId::from(Sr25519Keyring::One.public()), + ], + random_seed: [99; 32], + ..Default::default() + }), + _ => None, + }); + + let groups = ValidatorGroups::<Test>::get(); + assert_eq!(groups.len(), 5); + + // first two groups have the overflow. + for i in 0..2 { + assert_eq!(groups[i].len(), 2); + } + + for i in 2..5 { + assert_eq!(groups[i].len(), 1); + } + }); +} + +#[test] +fn session_change_takes_only_max_per_core() { + let config = { + let mut config = default_config(); + config.parathread_cores = 0; + config.max_validators_per_core = Some(1); + config + }; + + let genesis_config = MockGenesisConfig { + configuration: crate::configuration::GenesisConfig { + config: config.clone(), + ..Default::default() + }, + ..Default::default() + }; + + new_test_ext(genesis_config).execute_with(|| { + let chain_a = ParaId::from(1); + let chain_b = ParaId::from(2); + let chain_c = ParaId::from(3); + + // ensure that we have 5 groups by registering 2 parachains. + schedule_blank_para(chain_a, true); + schedule_blank_para(chain_b, true); + schedule_blank_para(chain_c, false); + + run_to_block(1, |number| match number { + 1 => Some(SessionChangeNotification { + new_config: config.clone(), + validators: vec![ + ValidatorId::from(Sr25519Keyring::Alice.public()), + ValidatorId::from(Sr25519Keyring::Bob.public()), + ValidatorId::from(Sr25519Keyring::Charlie.public()), + ValidatorId::from(Sr25519Keyring::Dave.public()), + ValidatorId::from(Sr25519Keyring::Eve.public()), + ValidatorId::from(Sr25519Keyring::Ferdie.public()), + ValidatorId::from(Sr25519Keyring::One.public()), + ], + random_seed: [99; 32], + ..Default::default() + }), + _ => None, + }); + + let groups = ValidatorGroups::<Test>::get(); + assert_eq!(groups.len(), 7); + + // Every validator gets its own group, even though there are 2 paras. + for i in 0..7 { + assert_eq!(groups[i].len(), 1); + } + }); +} + +#[test] +fn schedule_schedules() { + let genesis_config = MockGenesisConfig { + configuration: crate::configuration::GenesisConfig { + config: default_config(), + ..Default::default() + }, + ..Default::default() + }; + + let chain_a = ParaId::from(1); + let chain_b = ParaId::from(2); + + let thread_a = ParaId::from(3); + let thread_b = ParaId::from(4); + let thread_c = ParaId::from(5); + + let collator = CollatorId::from(Sr25519Keyring::Alice.public()); + + new_test_ext(genesis_config).execute_with(|| { + assert_eq!(default_config().parathread_cores, 3); + + // register 2 parachains + schedule_blank_para(chain_a, true); + schedule_blank_para(chain_b, true); + + // and 3 parathreads + schedule_blank_para(thread_a, false); + schedule_blank_para(thread_b, false); + schedule_blank_para(thread_c, false); + + // start a new session to activate, 5 validators for 5 cores. + run_to_block(1, |number| match number { + 1 => Some(SessionChangeNotification { + new_config: default_config(), + validators: vec![ + ValidatorId::from(Sr25519Keyring::Alice.public()), + ValidatorId::from(Sr25519Keyring::Bob.public()), + ValidatorId::from(Sr25519Keyring::Charlie.public()), + ValidatorId::from(Sr25519Keyring::Dave.public()), + ValidatorId::from(Sr25519Keyring::Eve.public()), + ], + ..Default::default() + }), + _ => None, + }); + + { + let scheduled = Scheduler::scheduled(); + assert_eq!(scheduled.len(), 2); + + assert_eq!( + scheduled[0], + CoreAssignment { + core: CoreIndex(0), + para_id: chain_a, + kind: AssignmentKind::Parachain, + group_idx: GroupIndex(0), + } + ); + + assert_eq!( + scheduled[1], + CoreAssignment { + core: CoreIndex(1), + para_id: chain_b, + kind: AssignmentKind::Parachain, + group_idx: GroupIndex(1), + } + ); + } + + // add a couple of parathread claims. + Scheduler::add_parathread_claim(ParathreadClaim(thread_a, collator.clone())); + Scheduler::add_parathread_claim(ParathreadClaim(thread_c, collator.clone())); + + run_to_block(2, |_| None); + + { + let scheduled = Scheduler::scheduled(); + assert_eq!(scheduled.len(), 4); + + assert_eq!( + scheduled[0], + CoreAssignment { + core: CoreIndex(0), + para_id: chain_a, + kind: AssignmentKind::Parachain, + group_idx: GroupIndex(0), + } + ); + + assert_eq!( + scheduled[1], + CoreAssignment { + core: CoreIndex(1), + para_id: chain_b, + kind: AssignmentKind::Parachain, + group_idx: GroupIndex(1), + } + ); + + assert_eq!( + scheduled[2], + CoreAssignment { + core: CoreIndex(2), + para_id: thread_a, + kind: AssignmentKind::Parathread(collator.clone(), 0), + group_idx: GroupIndex(2), + } + ); + + assert_eq!( + scheduled[3], + CoreAssignment { + core: CoreIndex(3), + para_id: thread_c, + kind: AssignmentKind::Parathread(collator.clone(), 0), + group_idx: GroupIndex(3), + } + ); + } + }); +} + +#[test] +fn schedule_schedules_including_just_freed() { + let genesis_config = MockGenesisConfig { + configuration: crate::configuration::GenesisConfig { + config: default_config(), + ..Default::default() + }, + ..Default::default() + }; + + let chain_a = ParaId::from(1); + let chain_b = ParaId::from(2); + + let thread_a = ParaId::from(3); + let thread_b = ParaId::from(4); + let thread_c = ParaId::from(5); + let thread_d = ParaId::from(6); + let thread_e = ParaId::from(7); + + let collator = CollatorId::from(Sr25519Keyring::Alice.public()); + + new_test_ext(genesis_config).execute_with(|| { + assert_eq!(default_config().parathread_cores, 3); + + // register 2 parachains + schedule_blank_para(chain_a, true); + schedule_blank_para(chain_b, true); + + // and 5 parathreads + schedule_blank_para(thread_a, false); + schedule_blank_para(thread_b, false); + schedule_blank_para(thread_c, false); + schedule_blank_para(thread_d, false); + schedule_blank_para(thread_e, false); + + // start a new session to activate, 5 validators for 5 cores. + run_to_block(1, |number| match number { + 1 => Some(SessionChangeNotification { + new_config: default_config(), + validators: vec![ + ValidatorId::from(Sr25519Keyring::Alice.public()), + ValidatorId::from(Sr25519Keyring::Bob.public()), + ValidatorId::from(Sr25519Keyring::Charlie.public()), + ValidatorId::from(Sr25519Keyring::Dave.public()), + ValidatorId::from(Sr25519Keyring::Eve.public()), + ], + ..Default::default() + }), + _ => None, + }); + + // add a couple of parathread claims now that the parathreads are live. + Scheduler::add_parathread_claim(ParathreadClaim(thread_a, collator.clone())); + Scheduler::add_parathread_claim(ParathreadClaim(thread_c, collator.clone())); + + run_to_block(2, |_| None); + + assert_eq!(Scheduler::scheduled().len(), 4); + + // cores 0, 1, 2, and 3 should be occupied. mark them as such. + Scheduler::occupied(&[CoreIndex(0), CoreIndex(1), CoreIndex(2), CoreIndex(3)]); + + { + let cores = AvailabilityCores::<Test>::get(); + + assert!(cores[0].is_some()); + assert!(cores[1].is_some()); + assert!(cores[2].is_some()); + assert!(cores[3].is_some()); + assert!(cores[4].is_none()); + + assert!(Scheduler::scheduled().is_empty()); + } + + // add a couple more parathread claims - the claim on `b` will go to the 3rd parathread core (4) + // and the claim on `d` will go back to the 1st parathread core (2). The claim on `e` then + // will go for core `3`. + Scheduler::add_parathread_claim(ParathreadClaim(thread_b, collator.clone())); + Scheduler::add_parathread_claim(ParathreadClaim(thread_d, collator.clone())); + Scheduler::add_parathread_claim(ParathreadClaim(thread_e, collator.clone())); + + run_to_block(3, |_| None); + + { + let scheduled = Scheduler::scheduled(); + + // cores 0 and 1 are occupied by parachains. cores 2 and 3 are occupied by parathread + // claims. core 4 was free. + assert_eq!(scheduled.len(), 1); + assert_eq!( + scheduled[0], + CoreAssignment { + core: CoreIndex(4), + para_id: thread_b, + kind: AssignmentKind::Parathread(collator.clone(), 0), + group_idx: GroupIndex(4), + } + ); + } + + // now note that cores 0, 2, and 3 were freed. + Scheduler::schedule( + vec![ + (CoreIndex(0), FreedReason::Concluded), + (CoreIndex(2), FreedReason::Concluded), + (CoreIndex(3), FreedReason::TimedOut), // should go back on queue. + ], + 3, + ); + + { + let scheduled = Scheduler::scheduled(); + + // 1 thing scheduled before, + 3 cores freed. + assert_eq!(scheduled.len(), 4); + assert_eq!( + scheduled[0], + CoreAssignment { + core: CoreIndex(0), + para_id: chain_a, + kind: AssignmentKind::Parachain, + group_idx: GroupIndex(0), + } + ); + assert_eq!( + scheduled[1], + CoreAssignment { + core: CoreIndex(2), + para_id: thread_d, + kind: AssignmentKind::Parathread(collator.clone(), 0), + group_idx: GroupIndex(2), + } + ); + assert_eq!( + scheduled[2], + CoreAssignment { + core: CoreIndex(3), + para_id: thread_e, + kind: AssignmentKind::Parathread(collator.clone(), 0), + group_idx: GroupIndex(3), + } + ); + assert_eq!( + scheduled[3], + CoreAssignment { + core: CoreIndex(4), + para_id: thread_b, + kind: AssignmentKind::Parathread(collator.clone(), 0), + group_idx: GroupIndex(4), + } + ); + + // the prior claim on thread A concluded, but the claim on thread C was marked as + // timed out. + let index = ParathreadClaimIndex::<Test>::get(); + let parathread_queue = ParathreadQueue::<Test>::get(); + + // thread A claim should have been wiped, but thread C claim should remain. + assert_eq!(index, vec![thread_b, thread_c, thread_d, thread_e]); + + // Although C was descheduled, the core `4` was occupied so C goes back on the queue. + assert_eq!(parathread_queue.queue.len(), 1); + assert_eq!( + parathread_queue.queue[0], + QueuedParathread { + claim: ParathreadEntry { + claim: ParathreadClaim(thread_c, collator.clone()), + retries: 0, // retries not incremented by timeout - validators' fault. + }, + core_offset: 2, // reassigned to next core. thread_e claim was on offset 1. + } + ); + } + }); +} + +#[test] +fn schedule_clears_availability_cores() { + let genesis_config = MockGenesisConfig { + configuration: crate::configuration::GenesisConfig { + config: default_config(), + ..Default::default() + }, + ..Default::default() + }; + + let chain_a = ParaId::from(1); + let chain_b = ParaId::from(2); + let chain_c = ParaId::from(3); + + new_test_ext(genesis_config).execute_with(|| { + assert_eq!(default_config().parathread_cores, 3); + + // register 3 parachains + schedule_blank_para(chain_a, true); + schedule_blank_para(chain_b, true); + schedule_blank_para(chain_c, true); + + // start a new session to activate, 5 validators for 5 cores. + run_to_block(1, |number| match number { + 1 => Some(SessionChangeNotification { + new_config: default_config(), + validators: vec![ + ValidatorId::from(Sr25519Keyring::Alice.public()), + ValidatorId::from(Sr25519Keyring::Bob.public()), + ValidatorId::from(Sr25519Keyring::Charlie.public()), + ValidatorId::from(Sr25519Keyring::Dave.public()), + ValidatorId::from(Sr25519Keyring::Eve.public()), + ], + ..Default::default() + }), + _ => None, + }); + + run_to_block(2, |_| None); + + assert_eq!(Scheduler::scheduled().len(), 3); + + // cores 0, 1, and 2 should be occupied. mark them as such. + Scheduler::occupied(&[CoreIndex(0), CoreIndex(1), CoreIndex(2)]); + + { + let cores = AvailabilityCores::<Test>::get(); + + assert!(cores[0].is_some()); + assert!(cores[1].is_some()); + assert!(cores[2].is_some()); + + assert!(Scheduler::scheduled().is_empty()); + } + + run_to_block(3, |_| None); + + // now note that cores 0 and 2 were freed. + Scheduler::schedule( + vec![(CoreIndex(0), FreedReason::Concluded), (CoreIndex(2), FreedReason::Concluded)], + 3, + ); + + { + let scheduled = Scheduler::scheduled(); + + assert_eq!(scheduled.len(), 2); + assert_eq!( + scheduled[0], + CoreAssignment { + core: CoreIndex(0), + para_id: chain_a, + kind: AssignmentKind::Parachain, + group_idx: GroupIndex(0), + } + ); + assert_eq!( + scheduled[1], + CoreAssignment { + core: CoreIndex(2), + para_id: chain_c, + kind: AssignmentKind::Parachain, + group_idx: GroupIndex(2), + } + ); + + // The freed cores should be `None` in `AvailabilityCores`. + let cores = AvailabilityCores::<Test>::get(); + assert!(cores[0].is_none()); + assert!(cores[2].is_none()); + } + }); +} + +#[test] +fn schedule_rotates_groups() { + let config = { + let mut config = default_config(); + + // make sure parathread requests don't retry-out + config.parathread_retries = config.group_rotation_frequency * 3; + config.parathread_cores = 2; + config + }; + + let rotation_frequency = config.group_rotation_frequency; + let parathread_cores = config.parathread_cores; + + let genesis_config = MockGenesisConfig { + configuration: crate::configuration::GenesisConfig { + config: config.clone(), + ..Default::default() + }, + ..Default::default() + }; + + let thread_a = ParaId::from(1); + let thread_b = ParaId::from(2); + + let collator = CollatorId::from(Sr25519Keyring::Alice.public()); + + new_test_ext(genesis_config).execute_with(|| { + assert_eq!(default_config().parathread_cores, 3); + + schedule_blank_para(thread_a, false); + schedule_blank_para(thread_b, false); + + // start a new session to activate, 5 validators for 5 cores. + run_to_block(1, |number| match number { + 1 => Some(SessionChangeNotification { + new_config: config.clone(), + validators: vec![ + ValidatorId::from(Sr25519Keyring::Alice.public()), + ValidatorId::from(Sr25519Keyring::Eve.public()), + ], + ..Default::default() + }), + _ => None, + }); + + let session_start_block = <Scheduler as Store>::SessionStartBlock::get(); + assert_eq!(session_start_block, 1); + + Scheduler::add_parathread_claim(ParathreadClaim(thread_a, collator.clone())); + Scheduler::add_parathread_claim(ParathreadClaim(thread_b, collator.clone())); + + run_to_block(2, |_| None); + + let assert_groups_rotated = |rotations: u32| { + let scheduled = Scheduler::scheduled(); + assert_eq!(scheduled.len(), 2); + assert_eq!(scheduled[0].group_idx, GroupIndex((0u32 + rotations) % parathread_cores)); + assert_eq!(scheduled[1].group_idx, GroupIndex((1u32 + rotations) % parathread_cores)); + }; + + assert_groups_rotated(0); + + // one block before first rotation. + run_to_block(rotation_frequency, |_| None); + + assert_groups_rotated(0); + + // first rotation. + run_to_block(rotation_frequency + 1, |_| None); + assert_groups_rotated(1); + + // one block before second rotation. + run_to_block(rotation_frequency * 2, |_| None); + assert_groups_rotated(1); + + // second rotation. + run_to_block(rotation_frequency * 2 + 1, |_| None); + assert_groups_rotated(2); + }); +} + +#[test] +fn parathread_claims_are_pruned_after_retries() { + let max_retries = default_config().parathread_retries; + + let genesis_config = MockGenesisConfig { + configuration: crate::configuration::GenesisConfig { + config: default_config(), + ..Default::default() + }, + ..Default::default() + }; + + let thread_a = ParaId::from(1); + let thread_b = ParaId::from(2); + + let collator = CollatorId::from(Sr25519Keyring::Alice.public()); + + new_test_ext(genesis_config).execute_with(|| { + assert_eq!(default_config().parathread_cores, 3); + + schedule_blank_para(thread_a, false); + schedule_blank_para(thread_b, false); + + // start a new session to activate, 5 validators for 5 cores. + run_to_block(1, |number| match number { + 1 => Some(SessionChangeNotification { + new_config: default_config(), + validators: vec![ + ValidatorId::from(Sr25519Keyring::Alice.public()), + ValidatorId::from(Sr25519Keyring::Eve.public()), + ], + ..Default::default() + }), + _ => None, + }); + + Scheduler::add_parathread_claim(ParathreadClaim(thread_a, collator.clone())); + Scheduler::add_parathread_claim(ParathreadClaim(thread_b, collator.clone())); + + run_to_block(2, |_| None); + assert_eq!(Scheduler::scheduled().len(), 2); + + run_to_block(2 + max_retries, |_| None); + assert_eq!(Scheduler::scheduled().len(), 2); + + run_to_block(2 + max_retries + 1, |_| None); + assert_eq!(Scheduler::scheduled().len(), 0); + }); +} + +#[test] +fn availability_predicate_works() { + let genesis_config = MockGenesisConfig { + configuration: crate::configuration::GenesisConfig { + config: default_config(), + ..Default::default() + }, + ..Default::default() + }; + + let HostConfiguration { + group_rotation_frequency, + chain_availability_period, + thread_availability_period, + .. + } = default_config(); + let collator = CollatorId::from(Sr25519Keyring::Alice.public()); + + assert!( + chain_availability_period < thread_availability_period && + thread_availability_period < group_rotation_frequency + ); + + let chain_a = ParaId::from(1); + let thread_a = ParaId::from(2); + + new_test_ext(genesis_config).execute_with(|| { + schedule_blank_para(chain_a, true); + schedule_blank_para(thread_a, false); + + // start a new session with our chain & thread registered. + run_to_block(1, |number| match number { + 1 => Some(SessionChangeNotification { + new_config: default_config(), + validators: vec![ + ValidatorId::from(Sr25519Keyring::Alice.public()), + ValidatorId::from(Sr25519Keyring::Bob.public()), + ValidatorId::from(Sr25519Keyring::Charlie.public()), + ValidatorId::from(Sr25519Keyring::Dave.public()), + ValidatorId::from(Sr25519Keyring::Eve.public()), + ], + ..Default::default() + }), + _ => None, + }); + + // assign some availability cores. + { + AvailabilityCores::<Test>::mutate(|cores| { + cores[0] = Some(CoreOccupied::Parachain); + cores[1] = Some(CoreOccupied::Parathread(ParathreadEntry { + claim: ParathreadClaim(thread_a, collator), + retries: 0, + })) + }); + } + + run_to_block(1 + thread_availability_period, |_| None); + assert!(Scheduler::availability_timeout_predicate().is_none()); + + run_to_block(1 + group_rotation_frequency, |_| None); + + { + let pred = Scheduler::availability_timeout_predicate() + .expect("predicate exists recently after rotation"); + + let now = System::block_number(); + let would_be_timed_out = now - thread_availability_period; + for i in 0..AvailabilityCores::<Test>::get().len() { + // returns true for unoccupied cores. + // And can time out both threads and chains at this stage. + assert!(pred(CoreIndex(i as u32), would_be_timed_out)); + } + + assert!(!pred(CoreIndex(0), now)); // assigned: chain + assert!(!pred(CoreIndex(1), now)); // assigned: thread + assert!(pred(CoreIndex(2), now)); + + // check the tighter bound on chains vs threads. + assert!(pred(CoreIndex(0), now - chain_availability_period)); + assert!(!pred(CoreIndex(1), now - chain_availability_period)); + + // check the threshold is exact. + assert!(!pred(CoreIndex(0), now - chain_availability_period + 1)); + assert!(!pred(CoreIndex(1), now - thread_availability_period + 1)); + } + + run_to_block(1 + group_rotation_frequency + chain_availability_period, |_| None); + + { + let pred = Scheduler::availability_timeout_predicate() + .expect("predicate exists recently after rotation"); + + let would_be_timed_out = System::block_number() - thread_availability_period; + + assert!(!pred(CoreIndex(0), would_be_timed_out)); // chains can't be timed out now. + assert!(pred(CoreIndex(1), would_be_timed_out)); // but threads can. + } + + run_to_block(1 + group_rotation_frequency + thread_availability_period, |_| None); + + assert!(Scheduler::availability_timeout_predicate().is_none()); + }); +} + +#[test] +fn next_up_on_available_uses_next_scheduled_or_none_for_thread() { + let mut config = default_config(); + config.parathread_cores = 1; + + let genesis_config = MockGenesisConfig { + configuration: crate::configuration::GenesisConfig { + config: config.clone(), + ..Default::default() + }, + ..Default::default() + }; + + let thread_a = ParaId::from(1); + let thread_b = ParaId::from(2); + + let collator = CollatorId::from(Sr25519Keyring::Alice.public()); + + new_test_ext(genesis_config).execute_with(|| { + schedule_blank_para(thread_a, false); + schedule_blank_para(thread_b, false); + + // start a new session to activate, 5 validators for 5 cores. + run_to_block(1, |number| match number { + 1 => Some(SessionChangeNotification { + new_config: config.clone(), + validators: vec![ + ValidatorId::from(Sr25519Keyring::Alice.public()), + ValidatorId::from(Sr25519Keyring::Eve.public()), + ], + ..Default::default() + }), + _ => None, + }); + + let thread_claim_a = ParathreadClaim(thread_a, collator.clone()); + let thread_claim_b = ParathreadClaim(thread_b, collator.clone()); + + Scheduler::add_parathread_claim(thread_claim_a.clone()); + + run_to_block(2, |_| None); + + { + assert_eq!(Scheduler::scheduled().len(), 1); + assert_eq!(Scheduler::availability_cores().len(), 1); + + Scheduler::occupied(&[CoreIndex(0)]); + + let cores = Scheduler::availability_cores(); + match cores[0].as_ref().unwrap() { + CoreOccupied::Parathread(entry) => assert_eq!(entry.claim, thread_claim_a), + _ => panic!("with no chains, only core should be a thread core"), + } + + assert!(Scheduler::next_up_on_available(CoreIndex(0)).is_none()); + + Scheduler::add_parathread_claim(thread_claim_b); + + let queue = ParathreadQueue::<Test>::get(); + assert_eq!( + queue.get_next_on_core(0).unwrap().claim, + ParathreadClaim(thread_b, collator.clone()), + ); + + assert_eq!( + Scheduler::next_up_on_available(CoreIndex(0)).unwrap(), + ScheduledCore { para_id: thread_b, collator: Some(collator.clone()) } + ); + } + }); +} + +#[test] +fn next_up_on_time_out_reuses_claim_if_nothing_queued() { + let mut config = default_config(); + config.parathread_cores = 1; + + let genesis_config = MockGenesisConfig { + configuration: crate::configuration::GenesisConfig { + config: config.clone(), + ..Default::default() + }, + ..Default::default() + }; + + let thread_a = ParaId::from(1); + let thread_b = ParaId::from(2); + + let collator = CollatorId::from(Sr25519Keyring::Alice.public()); + + new_test_ext(genesis_config).execute_with(|| { + schedule_blank_para(thread_a, false); + schedule_blank_para(thread_b, false); + + // start a new session to activate, 5 validators for 5 cores. + run_to_block(1, |number| match number { + 1 => Some(SessionChangeNotification { + new_config: config.clone(), + validators: vec![ + ValidatorId::from(Sr25519Keyring::Alice.public()), + ValidatorId::from(Sr25519Keyring::Eve.public()), + ], + ..Default::default() + }), + _ => None, + }); + + let thread_claim_a = ParathreadClaim(thread_a, collator.clone()); + let thread_claim_b = ParathreadClaim(thread_b, collator.clone()); + + Scheduler::add_parathread_claim(thread_claim_a.clone()); + + run_to_block(2, |_| None); + + { + assert_eq!(Scheduler::scheduled().len(), 1); + assert_eq!(Scheduler::availability_cores().len(), 1); + + Scheduler::occupied(&[CoreIndex(0)]); + + let cores = Scheduler::availability_cores(); + match cores[0].as_ref().unwrap() { + CoreOccupied::Parathread(entry) => assert_eq!(entry.claim, thread_claim_a), + _ => panic!("with no chains, only core should be a thread core"), + } + + let queue = ParathreadQueue::<Test>::get(); + assert!(queue.get_next_on_core(0).is_none()); + assert_eq!( + Scheduler::next_up_on_time_out(CoreIndex(0)).unwrap(), + ScheduledCore { para_id: thread_a, collator: Some(collator.clone()) } + ); + + Scheduler::add_parathread_claim(thread_claim_b); + + let queue = ParathreadQueue::<Test>::get(); + assert_eq!( + queue.get_next_on_core(0).unwrap().claim, + ParathreadClaim(thread_b, collator.clone()), + ); + + // Now that there is an earlier next-up, we use that. + assert_eq!( + Scheduler::next_up_on_available(CoreIndex(0)).unwrap(), + ScheduledCore { para_id: thread_b, collator: Some(collator.clone()) } + ); + } + }); +} + +#[test] +fn next_up_on_available_is_parachain_always() { + let mut config = default_config(); + config.parathread_cores = 0; + + let genesis_config = MockGenesisConfig { + configuration: crate::configuration::GenesisConfig { + config: config.clone(), + ..Default::default() + }, + ..Default::default() + }; + + let chain_a = ParaId::from(1); + + new_test_ext(genesis_config).execute_with(|| { + schedule_blank_para(chain_a, true); + + // start a new session to activate, 5 validators for 5 cores. + run_to_block(1, |number| match number { + 1 => Some(SessionChangeNotification { + new_config: config.clone(), + validators: vec![ + ValidatorId::from(Sr25519Keyring::Alice.public()), + ValidatorId::from(Sr25519Keyring::Eve.public()), + ], + ..Default::default() + }), + _ => None, + }); + + run_to_block(2, |_| None); + + { + assert_eq!(Scheduler::scheduled().len(), 1); + assert_eq!(Scheduler::availability_cores().len(), 1); + + Scheduler::occupied(&[CoreIndex(0)]); + + let cores = Scheduler::availability_cores(); + match cores[0].as_ref().unwrap() { + CoreOccupied::Parachain => {}, + _ => panic!("with no threads, only core should be a chain core"), + } + + // Now that there is an earlier next-up, we use that. + assert_eq!( + Scheduler::next_up_on_available(CoreIndex(0)).unwrap(), + ScheduledCore { para_id: chain_a, collator: None } + ); + } + }); +} + +#[test] +fn next_up_on_time_out_is_parachain_always() { + let mut config = default_config(); + config.parathread_cores = 0; + + let genesis_config = MockGenesisConfig { + configuration: crate::configuration::GenesisConfig { + config: config.clone(), + ..Default::default() + }, + ..Default::default() + }; + + let chain_a = ParaId::from(1); + + new_test_ext(genesis_config).execute_with(|| { + schedule_blank_para(chain_a, true); + + // start a new session to activate, 5 validators for 5 cores. + run_to_block(1, |number| match number { + 1 => Some(SessionChangeNotification { + new_config: config.clone(), + validators: vec![ + ValidatorId::from(Sr25519Keyring::Alice.public()), + ValidatorId::from(Sr25519Keyring::Eve.public()), + ], + ..Default::default() + }), + _ => None, + }); + + run_to_block(2, |_| None); + + { + assert_eq!(Scheduler::scheduled().len(), 1); + assert_eq!(Scheduler::availability_cores().len(), 1); + + Scheduler::occupied(&[CoreIndex(0)]); + + let cores = Scheduler::availability_cores(); + match cores[0].as_ref().unwrap() { + CoreOccupied::Parachain => {}, + _ => panic!("with no threads, only core should be a chain core"), + } + + // Now that there is an earlier next-up, we use that. + assert_eq!( + Scheduler::next_up_on_available(CoreIndex(0)).unwrap(), + ScheduledCore { para_id: chain_a, collator: None } + ); + } + }); +} + +#[test] +fn session_change_requires_reschedule_dropping_removed_paras() { + let genesis_config = MockGenesisConfig { + configuration: crate::configuration::GenesisConfig { + config: default_config(), + ..Default::default() + }, + ..Default::default() + }; + + assert_eq!(default_config().parathread_cores, 3); + new_test_ext(genesis_config).execute_with(|| { + let chain_a = ParaId::from(1); + let chain_b = ParaId::from(2); + + // ensure that we have 5 groups by registering 2 parachains. + schedule_blank_para(chain_a, true); + schedule_blank_para(chain_b, true); + + run_to_block(1, |number| match number { + 1 => Some(SessionChangeNotification { + new_config: default_config(), + validators: vec![ + ValidatorId::from(Sr25519Keyring::Alice.public()), + ValidatorId::from(Sr25519Keyring::Bob.public()), + ValidatorId::from(Sr25519Keyring::Charlie.public()), + ValidatorId::from(Sr25519Keyring::Dave.public()), + ValidatorId::from(Sr25519Keyring::Eve.public()), + ValidatorId::from(Sr25519Keyring::Ferdie.public()), + ValidatorId::from(Sr25519Keyring::One.public()), + ], + random_seed: [99; 32], + ..Default::default() + }), + _ => None, + }); + + assert_eq!(Scheduler::scheduled().len(), 2); + + let groups = ValidatorGroups::<Test>::get(); + assert_eq!(groups.len(), 5); + + assert_ok!(Paras::schedule_para_cleanup(chain_b)); + + run_to_end_of_block(2, |number| match number { + 2 => Some(SessionChangeNotification { + new_config: default_config(), + validators: vec![ + ValidatorId::from(Sr25519Keyring::Alice.public()), + ValidatorId::from(Sr25519Keyring::Bob.public()), + ValidatorId::from(Sr25519Keyring::Charlie.public()), + ValidatorId::from(Sr25519Keyring::Dave.public()), + ValidatorId::from(Sr25519Keyring::Eve.public()), + ValidatorId::from(Sr25519Keyring::Ferdie.public()), + ValidatorId::from(Sr25519Keyring::One.public()), + ], + random_seed: [99; 32], + ..Default::default() + }), + _ => None, + }); + + Scheduler::clear(); + Scheduler::schedule(Vec::new(), 3); + + assert_eq!( + Scheduler::scheduled(), + vec![CoreAssignment { + core: CoreIndex(0), + para_id: chain_a, + kind: AssignmentKind::Parachain, + group_idx: GroupIndex(0), + }], + ); + }); +} + +#[test] +fn parathread_claims_are_pruned_after_deregistration() { + let genesis_config = MockGenesisConfig { + configuration: crate::configuration::GenesisConfig { + config: default_config(), + ..Default::default() + }, + ..Default::default() + }; + + let thread_a = ParaId::from(1); + let thread_b = ParaId::from(2); + + let collator = CollatorId::from(Sr25519Keyring::Alice.public()); + + new_test_ext(genesis_config).execute_with(|| { + assert_eq!(default_config().parathread_cores, 3); + + schedule_blank_para(thread_a, false); + schedule_blank_para(thread_b, false); + + // start a new session to activate, 5 validators for 5 cores. + run_to_block(1, |number| match number { + 1 => Some(SessionChangeNotification { + new_config: default_config(), + validators: vec![ + ValidatorId::from(Sr25519Keyring::Alice.public()), + ValidatorId::from(Sr25519Keyring::Eve.public()), + ], + ..Default::default() + }), + _ => None, + }); + + Scheduler::add_parathread_claim(ParathreadClaim(thread_a, collator.clone())); + Scheduler::add_parathread_claim(ParathreadClaim(thread_b, collator.clone())); + + run_to_block(2, |_| None); + assert_eq!(Scheduler::scheduled().len(), 2); + + assert_ok!(Paras::schedule_para_cleanup(thread_a)); + + // start a new session to activate, 5 validators for 5 cores. + run_to_block(3, |number| match number { + 3 => Some(SessionChangeNotification { + new_config: default_config(), + validators: vec![ + ValidatorId::from(Sr25519Keyring::Alice.public()), + ValidatorId::from(Sr25519Keyring::Eve.public()), + ], + ..Default::default() + }), + _ => None, + }); + + assert_eq!(Scheduler::scheduled().len(), 1); + }); +} diff --git a/polkadot/runtime/parachains/src/session_info.rs b/polkadot/runtime/parachains/src/session_info.rs index 4e0171b3168bbbeb28634216fe03a9f4059b4525..904d46260b8ddef9d79f5ec0ad093f1a28fdb8ab 100644 --- a/polkadot/runtime/parachains/src/session_info.rs +++ b/polkadot/runtime/parachains/src/session_info.rs @@ -34,6 +34,9 @@ pub use pallet::*; pub mod migration; +#[cfg(test)] +mod tests; + #[frame_support::pallet] pub mod pallet { use super::*; @@ -187,205 +190,3 @@ impl<T: pallet_session::Config + Config> OneSessionHandler<T::AccountId> for Pal fn on_disabled(_i: u32) {} } - -#[cfg(test)] -mod tests { - use super::*; - use crate::{ - configuration::HostConfiguration, - initializer::SessionChangeNotification, - mock::{ - new_test_ext, Configuration, MockGenesisConfig, Origin, ParasShared, SessionInfo, - System, Test, - }, - util::take_active_subset, - }; - use keyring::Sr25519Keyring; - use primitives::v1::{BlockNumber, ValidatorId, ValidatorIndex}; - - fn run_to_block( - to: BlockNumber, - new_session: impl Fn(BlockNumber) -> Option<SessionChangeNotification<BlockNumber>>, - ) { - while System::block_number() < to { - let b = System::block_number(); - - SessionInfo::initializer_finalize(); - ParasShared::initializer_finalize(); - Configuration::initializer_finalize(); - - if let Some(notification) = new_session(b + 1) { - Configuration::initializer_on_new_session(¬ification.session_index); - ParasShared::initializer_on_new_session( - notification.session_index, - notification.random_seed, - ¬ification.new_config, - notification.validators.clone(), - ); - SessionInfo::initializer_on_new_session(¬ification); - } - - System::on_finalize(b); - - System::on_initialize(b + 1); - System::set_block_number(b + 1); - - Configuration::initializer_initialize(b + 1); - ParasShared::initializer_initialize(b + 1); - SessionInfo::initializer_initialize(b + 1); - } - } - - fn default_config() -> HostConfiguration<BlockNumber> { - HostConfiguration { - parathread_cores: 1, - dispute_period: 2, - needed_approvals: 3, - ..Default::default() - } - } - - fn genesis_config() -> MockGenesisConfig { - MockGenesisConfig { - configuration: configuration::GenesisConfig { - config: default_config(), - ..Default::default() - }, - ..Default::default() - } - } - - fn session_changes(n: BlockNumber) -> Option<SessionChangeNotification<BlockNumber>> { - if n % 10 == 0 { - Some(SessionChangeNotification { session_index: n / 10, ..Default::default() }) - } else { - None - } - } - - fn new_session_every_block(n: BlockNumber) -> Option<SessionChangeNotification<BlockNumber>> { - Some(SessionChangeNotification { session_index: n, ..Default::default() }) - } - - #[test] - fn session_pruning_is_based_on_dispute_period() { - new_test_ext(genesis_config()).execute_with(|| { - // Dispute period starts at 2 - let config = Configuration::config(); - assert_eq!(config.dispute_period, 2); - - // Move to session 10 - run_to_block(100, session_changes); - // Earliest stored session is 10 - 2 = 8 - assert_eq!(EarliestStoredSession::<Test>::get(), 8); - // Pruning works as expected - assert!(Sessions::<Test>::get(7).is_none()); - assert!(Sessions::<Test>::get(8).is_some()); - assert!(Sessions::<Test>::get(9).is_some()); - - // changing `dispute_period` works - let dispute_period = 5; - Configuration::set_dispute_period(Origin::root(), dispute_period).unwrap(); - - // Dispute period does not automatically change - let config = Configuration::config(); - assert_eq!(config.dispute_period, 2); - // Two sessions later it will though - run_to_block(120, session_changes); - let config = Configuration::config(); - assert_eq!(config.dispute_period, 5); - - run_to_block(200, session_changes); - assert_eq!(EarliestStoredSession::<Test>::get(), 20 - dispute_period); - - // Increase dispute period even more - let new_dispute_period = 16; - Configuration::set_dispute_period(Origin::root(), new_dispute_period).unwrap(); - - run_to_block(210, session_changes); - assert_eq!(EarliestStoredSession::<Test>::get(), 21 - dispute_period); - - // Two sessions later it kicks in - run_to_block(220, session_changes); - let config = Configuration::config(); - assert_eq!(config.dispute_period, 16); - // Earliest session stays the same - assert_eq!(EarliestStoredSession::<Test>::get(), 21 - dispute_period); - - // We still don't have enough stored sessions to start pruning - run_to_block(300, session_changes); - assert_eq!(EarliestStoredSession::<Test>::get(), 21 - dispute_period); - - // now we do - run_to_block(420, session_changes); - assert_eq!(EarliestStoredSession::<Test>::get(), 42 - new_dispute_period); - }) - } - - #[test] - fn session_info_is_based_on_config() { - new_test_ext(genesis_config()).execute_with(|| { - run_to_block(1, new_session_every_block); - let session = Sessions::<Test>::get(&1).unwrap(); - assert_eq!(session.needed_approvals, 3); - - // change some param - Configuration::set_needed_approvals(Origin::root(), 42).unwrap(); - // 2 sessions later - run_to_block(3, new_session_every_block); - let session = Sessions::<Test>::get(&3).unwrap(); - assert_eq!(session.needed_approvals, 42); - }) - } - - #[test] - fn session_info_active_subsets() { - let unscrambled = vec![ - Sr25519Keyring::Alice, - Sr25519Keyring::Bob, - Sr25519Keyring::Charlie, - Sr25519Keyring::Dave, - Sr25519Keyring::Eve, - ]; - - let active_set = vec![ValidatorIndex(4), ValidatorIndex(0), ValidatorIndex(2)]; - - let unscrambled_validators: Vec<ValidatorId> = - unscrambled.iter().map(|v| v.public().into()).collect(); - let unscrambled_discovery: Vec<AuthorityDiscoveryId> = - unscrambled.iter().map(|v| v.public().into()).collect(); - let unscrambled_assignment: Vec<AssignmentId> = - unscrambled.iter().map(|v| v.public().into()).collect(); - - let validators = take_active_subset(&active_set, &unscrambled_validators); - - new_test_ext(genesis_config()).execute_with(|| { - ParasShared::set_active_validators_with_indices(active_set.clone(), validators.clone()); - - assert_eq!(ParasShared::active_validator_indices(), active_set); - - AssignmentKeysUnsafe::<Test>::set(unscrambled_assignment.clone()); - crate::mock::set_discovery_authorities(unscrambled_discovery.clone()); - assert_eq!(<Test>::authorities(), unscrambled_discovery); - - // invoke directly, because `run_to_block` will invoke `Shared` and clobber our - // values. - SessionInfo::initializer_on_new_session(&SessionChangeNotification { - session_index: 1, - validators: validators.clone(), - ..Default::default() - }); - let session = Sessions::<Test>::get(&1).unwrap(); - - assert_eq!(session.validators, validators); - assert_eq!( - session.discovery_keys, - take_active_subset_and_inactive(&active_set, &unscrambled_discovery), - ); - assert_eq!( - session.assignment_keys, - take_active_subset(&active_set, &unscrambled_assignment), - ); - }) - } -} diff --git a/polkadot/runtime/parachains/src/session_info/tests.rs b/polkadot/runtime/parachains/src/session_info/tests.rs new file mode 100644 index 0000000000000000000000000000000000000000..e350072899589489439b1a41279a4bf956b3c92f --- /dev/null +++ b/polkadot/runtime/parachains/src/session_info/tests.rs @@ -0,0 +1,214 @@ +// Copyright 2020 Parity Technologies (UK) Ltd. +// This file is part of Polkadot. + +// Polkadot is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Polkadot is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Polkadot. If not, see <http://www.gnu.org/licenses/>. + +use super::*; +use crate::{ + configuration::HostConfiguration, + initializer::SessionChangeNotification, + mock::{ + new_test_ext, Configuration, MockGenesisConfig, Origin, ParasShared, SessionInfo, System, + Test, + }, + util::take_active_subset, +}; +use keyring::Sr25519Keyring; +use primitives::v1::{BlockNumber, ValidatorId, ValidatorIndex}; + +fn run_to_block( + to: BlockNumber, + new_session: impl Fn(BlockNumber) -> Option<SessionChangeNotification<BlockNumber>>, +) { + while System::block_number() < to { + let b = System::block_number(); + + SessionInfo::initializer_finalize(); + ParasShared::initializer_finalize(); + Configuration::initializer_finalize(); + + if let Some(notification) = new_session(b + 1) { + Configuration::initializer_on_new_session(¬ification.session_index); + ParasShared::initializer_on_new_session( + notification.session_index, + notification.random_seed, + ¬ification.new_config, + notification.validators.clone(), + ); + SessionInfo::initializer_on_new_session(¬ification); + } + + System::on_finalize(b); + + System::on_initialize(b + 1); + System::set_block_number(b + 1); + + Configuration::initializer_initialize(b + 1); + ParasShared::initializer_initialize(b + 1); + SessionInfo::initializer_initialize(b + 1); + } +} + +fn default_config() -> HostConfiguration<BlockNumber> { + HostConfiguration { + parathread_cores: 1, + dispute_period: 2, + needed_approvals: 3, + ..Default::default() + } +} + +fn genesis_config() -> MockGenesisConfig { + MockGenesisConfig { + configuration: configuration::GenesisConfig { + config: default_config(), + ..Default::default() + }, + ..Default::default() + } +} + +fn session_changes(n: BlockNumber) -> Option<SessionChangeNotification<BlockNumber>> { + if n % 10 == 0 { + Some(SessionChangeNotification { session_index: n / 10, ..Default::default() }) + } else { + None + } +} + +fn new_session_every_block(n: BlockNumber) -> Option<SessionChangeNotification<BlockNumber>> { + Some(SessionChangeNotification { session_index: n, ..Default::default() }) +} + +#[test] +fn session_pruning_is_based_on_dispute_period() { + new_test_ext(genesis_config()).execute_with(|| { + // Dispute period starts at 2 + let config = Configuration::config(); + assert_eq!(config.dispute_period, 2); + + // Move to session 10 + run_to_block(100, session_changes); + // Earliest stored session is 10 - 2 = 8 + assert_eq!(EarliestStoredSession::<Test>::get(), 8); + // Pruning works as expected + assert!(Sessions::<Test>::get(7).is_none()); + assert!(Sessions::<Test>::get(8).is_some()); + assert!(Sessions::<Test>::get(9).is_some()); + + // changing `dispute_period` works + let dispute_period = 5; + Configuration::set_dispute_period(Origin::root(), dispute_period).unwrap(); + + // Dispute period does not automatically change + let config = Configuration::config(); + assert_eq!(config.dispute_period, 2); + // Two sessions later it will though + run_to_block(120, session_changes); + let config = Configuration::config(); + assert_eq!(config.dispute_period, 5); + + run_to_block(200, session_changes); + assert_eq!(EarliestStoredSession::<Test>::get(), 20 - dispute_period); + + // Increase dispute period even more + let new_dispute_period = 16; + Configuration::set_dispute_period(Origin::root(), new_dispute_period).unwrap(); + + run_to_block(210, session_changes); + assert_eq!(EarliestStoredSession::<Test>::get(), 21 - dispute_period); + + // Two sessions later it kicks in + run_to_block(220, session_changes); + let config = Configuration::config(); + assert_eq!(config.dispute_period, 16); + // Earliest session stays the same + assert_eq!(EarliestStoredSession::<Test>::get(), 21 - dispute_period); + + // We still don't have enough stored sessions to start pruning + run_to_block(300, session_changes); + assert_eq!(EarliestStoredSession::<Test>::get(), 21 - dispute_period); + + // now we do + run_to_block(420, session_changes); + assert_eq!(EarliestStoredSession::<Test>::get(), 42 - new_dispute_period); + }) +} + +#[test] +fn session_info_is_based_on_config() { + new_test_ext(genesis_config()).execute_with(|| { + run_to_block(1, new_session_every_block); + let session = Sessions::<Test>::get(&1).unwrap(); + assert_eq!(session.needed_approvals, 3); + + // change some param + Configuration::set_needed_approvals(Origin::root(), 42).unwrap(); + // 2 sessions later + run_to_block(3, new_session_every_block); + let session = Sessions::<Test>::get(&3).unwrap(); + assert_eq!(session.needed_approvals, 42); + }) +} + +#[test] +fn session_info_active_subsets() { + let unscrambled = vec![ + Sr25519Keyring::Alice, + Sr25519Keyring::Bob, + Sr25519Keyring::Charlie, + Sr25519Keyring::Dave, + Sr25519Keyring::Eve, + ]; + + let active_set = vec![ValidatorIndex(4), ValidatorIndex(0), ValidatorIndex(2)]; + + let unscrambled_validators: Vec<ValidatorId> = + unscrambled.iter().map(|v| v.public().into()).collect(); + let unscrambled_discovery: Vec<AuthorityDiscoveryId> = + unscrambled.iter().map(|v| v.public().into()).collect(); + let unscrambled_assignment: Vec<AssignmentId> = + unscrambled.iter().map(|v| v.public().into()).collect(); + + let validators = take_active_subset(&active_set, &unscrambled_validators); + + new_test_ext(genesis_config()).execute_with(|| { + ParasShared::set_active_validators_with_indices(active_set.clone(), validators.clone()); + + assert_eq!(ParasShared::active_validator_indices(), active_set); + + AssignmentKeysUnsafe::<Test>::set(unscrambled_assignment.clone()); + crate::mock::set_discovery_authorities(unscrambled_discovery.clone()); + assert_eq!(<Test>::authorities(), unscrambled_discovery); + + // invoke directly, because `run_to_block` will invoke `Shared` and clobber our + // values. + SessionInfo::initializer_on_new_session(&SessionChangeNotification { + session_index: 1, + validators: validators.clone(), + ..Default::default() + }); + let session = Sessions::<Test>::get(&1).unwrap(); + + assert_eq!(session.validators, validators); + assert_eq!( + session.discovery_keys, + take_active_subset_and_inactive(&active_set, &unscrambled_discovery), + ); + assert_eq!( + session.assignment_keys, + take_active_subset(&active_set, &unscrambled_assignment), + ); + }) +} diff --git a/polkadot/runtime/parachains/src/shared.rs b/polkadot/runtime/parachains/src/shared.rs index f0586604332742e69aa75d95440af440baa95a07..7bd33c503c630b4de3db46f08cdc711144c7b839 100644 --- a/polkadot/runtime/parachains/src/shared.rs +++ b/polkadot/runtime/parachains/src/shared.rs @@ -35,6 +35,9 @@ pub use pallet::*; // which guarantees that at least one full session has passed before any changes are applied. pub(crate) const SESSION_DELAY: SessionIndex = 2; +#[cfg(test)] +mod tests; + #[frame_support::pallet] pub mod pallet { use super::*; @@ -139,93 +142,3 @@ impl<T: Config> Pallet<T> { ActiveValidatorKeys::<T>::set(keys); } } - -#[cfg(test)] -mod tests { - use super::*; - use crate::{ - configuration::HostConfiguration, - mock::{new_test_ext, MockGenesisConfig, ParasShared}, - }; - use keyring::Sr25519Keyring; - - fn validator_pubkeys(val_ids: &[Sr25519Keyring]) -> Vec<ValidatorId> { - val_ids.iter().map(|v| v.public().into()).collect() - } - - #[test] - fn sets_and_shuffles_validators() { - let validators = vec![ - Sr25519Keyring::Alice, - Sr25519Keyring::Bob, - Sr25519Keyring::Charlie, - Sr25519Keyring::Dave, - Sr25519Keyring::Ferdie, - ]; - - let mut config = HostConfiguration::default(); - config.max_validators = None; - - let pubkeys = validator_pubkeys(&validators); - - new_test_ext(MockGenesisConfig::default()).execute_with(|| { - let validators = ParasShared::initializer_on_new_session(1, [1; 32], &config, pubkeys); - - assert_eq!( - validators, - validator_pubkeys(&[ - Sr25519Keyring::Ferdie, - Sr25519Keyring::Bob, - Sr25519Keyring::Charlie, - Sr25519Keyring::Dave, - Sr25519Keyring::Alice, - ]) - ); - - assert_eq!(ParasShared::active_validator_keys(), validators); - - assert_eq!( - ParasShared::active_validator_indices(), - vec![ - ValidatorIndex(4), - ValidatorIndex(1), - ValidatorIndex(2), - ValidatorIndex(3), - ValidatorIndex(0), - ] - ); - }); - } - - #[test] - fn sets_truncates_and_shuffles_validators() { - let validators = vec![ - Sr25519Keyring::Alice, - Sr25519Keyring::Bob, - Sr25519Keyring::Charlie, - Sr25519Keyring::Dave, - Sr25519Keyring::Ferdie, - ]; - - let mut config = HostConfiguration::default(); - config.max_validators = Some(2); - - let pubkeys = validator_pubkeys(&validators); - - new_test_ext(MockGenesisConfig::default()).execute_with(|| { - let validators = ParasShared::initializer_on_new_session(1, [1; 32], &config, pubkeys); - - assert_eq!( - validators, - validator_pubkeys(&[Sr25519Keyring::Ferdie, Sr25519Keyring::Bob,]) - ); - - assert_eq!(ParasShared::active_validator_keys(), validators); - - assert_eq!( - ParasShared::active_validator_indices(), - vec![ValidatorIndex(4), ValidatorIndex(1),] - ); - }); - } -} diff --git a/polkadot/runtime/parachains/src/shared/tests.rs b/polkadot/runtime/parachains/src/shared/tests.rs new file mode 100644 index 0000000000000000000000000000000000000000..0113c3539d9a8a22fe2c3be68160c6540f99c44b --- /dev/null +++ b/polkadot/runtime/parachains/src/shared/tests.rs @@ -0,0 +1,99 @@ +// Copyright 2020 Parity Technologies (UK) Ltd. +// This file is part of Polkadot. + +// Polkadot is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Polkadot is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Polkadot. If not, see <http://www.gnu.org/licenses/>. + +use super::*; +use crate::{ + configuration::HostConfiguration, + mock::{new_test_ext, MockGenesisConfig, ParasShared}, +}; +use keyring::Sr25519Keyring; + +fn validator_pubkeys(val_ids: &[Sr25519Keyring]) -> Vec<ValidatorId> { + val_ids.iter().map(|v| v.public().into()).collect() +} + +#[test] +fn sets_and_shuffles_validators() { + let validators = vec![ + Sr25519Keyring::Alice, + Sr25519Keyring::Bob, + Sr25519Keyring::Charlie, + Sr25519Keyring::Dave, + Sr25519Keyring::Ferdie, + ]; + + let mut config = HostConfiguration::default(); + config.max_validators = None; + + let pubkeys = validator_pubkeys(&validators); + + new_test_ext(MockGenesisConfig::default()).execute_with(|| { + let validators = ParasShared::initializer_on_new_session(1, [1; 32], &config, pubkeys); + + assert_eq!( + validators, + validator_pubkeys(&[ + Sr25519Keyring::Ferdie, + Sr25519Keyring::Bob, + Sr25519Keyring::Charlie, + Sr25519Keyring::Dave, + Sr25519Keyring::Alice, + ]) + ); + + assert_eq!(ParasShared::active_validator_keys(), validators); + + assert_eq!( + ParasShared::active_validator_indices(), + vec![ + ValidatorIndex(4), + ValidatorIndex(1), + ValidatorIndex(2), + ValidatorIndex(3), + ValidatorIndex(0), + ] + ); + }); +} + +#[test] +fn sets_truncates_and_shuffles_validators() { + let validators = vec![ + Sr25519Keyring::Alice, + Sr25519Keyring::Bob, + Sr25519Keyring::Charlie, + Sr25519Keyring::Dave, + Sr25519Keyring::Ferdie, + ]; + + let mut config = HostConfiguration::default(); + config.max_validators = Some(2); + + let pubkeys = validator_pubkeys(&validators); + + new_test_ext(MockGenesisConfig::default()).execute_with(|| { + let validators = ParasShared::initializer_on_new_session(1, [1; 32], &config, pubkeys); + + assert_eq!(validators, validator_pubkeys(&[Sr25519Keyring::Ferdie, Sr25519Keyring::Bob,])); + + assert_eq!(ParasShared::active_validator_keys(), validators); + + assert_eq!( + ParasShared::active_validator_indices(), + vec![ValidatorIndex(4), ValidatorIndex(1),] + ); + }); +} diff --git a/polkadot/runtime/parachains/src/ump.rs b/polkadot/runtime/parachains/src/ump.rs index 302937af4a3ddbad78531d7008f071a84a9b1236..344b0f7d439f26859d7ba6214a1d91053b7bc8b4 100644 --- a/polkadot/runtime/parachains/src/ump.rs +++ b/polkadot/runtime/parachains/src/ump.rs @@ -28,6 +28,9 @@ use xcm::latest::Outcome; pub use pallet::*; +#[cfg(test)] +pub(crate) mod tests; + /// All upward messages coming from parachains will be funneled into an implementation of this trait. /// /// The message is opaque from the perspective of UMP. The message size can range from 0 to @@ -703,342 +706,3 @@ impl NeedsDispatchCursor { <Pallet<T> as Store>::NeedsDispatch::put(self.needs_dispatch); } } - -#[cfg(test)] -pub(crate) mod tests { - use super::*; - use crate::mock::{ - assert_last_event, new_test_ext, take_processed, Configuration, MockGenesisConfig, Origin, - System, Test, Ump, - }; - use frame_support::{assert_noop, assert_ok, weights::Weight}; - use std::collections::HashSet; - - struct GenesisConfigBuilder { - max_upward_message_size: u32, - max_upward_message_num_per_candidate: u32, - max_upward_queue_count: u32, - max_upward_queue_size: u32, - ump_service_total_weight: Weight, - ump_max_individual_weight: Weight, - } - - impl Default for GenesisConfigBuilder { - fn default() -> Self { - Self { - max_upward_message_size: 16, - max_upward_message_num_per_candidate: 2, - max_upward_queue_count: 4, - max_upward_queue_size: 64, - ump_service_total_weight: 1000, - ump_max_individual_weight: 100, - } - } - } - - impl GenesisConfigBuilder { - fn build(self) -> crate::mock::MockGenesisConfig { - let mut genesis = default_genesis_config(); - let config = &mut genesis.configuration.config; - - config.max_upward_message_size = self.max_upward_message_size; - config.max_upward_message_num_per_candidate = self.max_upward_message_num_per_candidate; - config.max_upward_queue_count = self.max_upward_queue_count; - config.max_upward_queue_size = self.max_upward_queue_size; - config.ump_service_total_weight = self.ump_service_total_weight; - config.ump_max_individual_weight = self.ump_max_individual_weight; - genesis - } - } - - fn default_genesis_config() -> MockGenesisConfig { - MockGenesisConfig { - configuration: crate::configuration::GenesisConfig { - config: crate::configuration::HostConfiguration { - max_downward_message_size: 1024, - ..Default::default() - }, - }, - ..Default::default() - } - } - - fn queue_upward_msg(para: ParaId, msg: UpwardMessage) { - let msgs = vec![msg]; - assert!(Ump::check_upward_messages(&Configuration::config(), para, &msgs).is_ok()); - let _ = Ump::receive_upward_messages(para, msgs); - } - - fn assert_storage_consistency_exhaustive() { - // check that empty queues don't clutter the storage. - for (_para, queue) in <Ump as Store>::RelayDispatchQueues::iter() { - assert!(!queue.is_empty()); - } - - // actually count the counts and sizes in queues and compare them to the bookkept version. - for (para, queue) in <Ump as Store>::RelayDispatchQueues::iter() { - let (expected_count, expected_size) = <Ump as Store>::RelayDispatchQueueSize::get(para); - let (actual_count, actual_size) = - queue.into_iter().fold((0, 0), |(acc_count, acc_size), x| { - (acc_count + 1, acc_size + x.len() as u32) - }); - - assert_eq!(expected_count, actual_count); - assert_eq!(expected_size, actual_size); - } - - // since we wipe the empty queues the sets of paras in queue contents, queue sizes and - // need dispatch set should all be equal. - let queue_contents_set = <Ump as Store>::RelayDispatchQueues::iter() - .map(|(k, _)| k) - .collect::<HashSet<ParaId>>(); - let queue_sizes_set = <Ump as Store>::RelayDispatchQueueSize::iter() - .map(|(k, _)| k) - .collect::<HashSet<ParaId>>(); - let needs_dispatch_set = - <Ump as Store>::NeedsDispatch::get().into_iter().collect::<HashSet<ParaId>>(); - assert_eq!(queue_contents_set, queue_sizes_set); - assert_eq!(queue_contents_set, needs_dispatch_set); - - // `NextDispatchRoundStartWith` should point into a para that is tracked. - if let Some(para) = <Ump as Store>::NextDispatchRoundStartWith::get() { - assert!(queue_contents_set.contains(¶)); - } - - // `NeedsDispatch` is always sorted. - assert!(<Ump as Store>::NeedsDispatch::get().windows(2).all(|xs| xs[0] <= xs[1])); - } - - #[test] - fn dispatch_empty() { - new_test_ext(default_genesis_config()).execute_with(|| { - assert_storage_consistency_exhaustive(); - - // make sure that the case with empty queues is handled properly - Ump::process_pending_upward_messages(); - - assert_storage_consistency_exhaustive(); - }); - } - - #[test] - fn dispatch_single_message() { - let a = ParaId::from(228); - let msg = 1000u32.encode(); - - new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| { - queue_upward_msg(a, msg.clone()); - Ump::process_pending_upward_messages(); - assert_eq!(take_processed(), vec![(a, msg)]); - - assert_storage_consistency_exhaustive(); - }); - } - - #[test] - fn dispatch_resume_after_exceeding_dispatch_stage_weight() { - let a = ParaId::from(128); - let c = ParaId::from(228); - let q = ParaId::from(911); - - let a_msg_1 = (200u32, "a_msg_1").encode(); - let a_msg_2 = (100u32, "a_msg_2").encode(); - let c_msg_1 = (300u32, "c_msg_1").encode(); - let c_msg_2 = (100u32, "c_msg_2").encode(); - let q_msg = (500u32, "q_msg").encode(); - - new_test_ext( - GenesisConfigBuilder { ump_service_total_weight: 500, ..Default::default() }.build(), - ) - .execute_with(|| { - queue_upward_msg(q, q_msg.clone()); - queue_upward_msg(c, c_msg_1.clone()); - queue_upward_msg(a, a_msg_1.clone()); - queue_upward_msg(a, a_msg_2.clone()); - - assert_storage_consistency_exhaustive(); - - // we expect only two first messages to fit in the first iteration. - Ump::process_pending_upward_messages(); - assert_eq!(take_processed(), vec![(a, a_msg_1), (c, c_msg_1)]); - assert_storage_consistency_exhaustive(); - - queue_upward_msg(c, c_msg_2.clone()); - assert_storage_consistency_exhaustive(); - - // second iteration should process the second message. - Ump::process_pending_upward_messages(); - assert_eq!(take_processed(), vec![(q, q_msg)]); - assert_storage_consistency_exhaustive(); - - // 3rd iteration. - Ump::process_pending_upward_messages(); - assert_eq!(take_processed(), vec![(a, a_msg_2), (c, c_msg_2)]); - assert_storage_consistency_exhaustive(); - - // finally, make sure that the queue is empty. - Ump::process_pending_upward_messages(); - assert_eq!(take_processed(), vec![]); - assert_storage_consistency_exhaustive(); - }); - } - - #[test] - fn dispatch_keeps_message_after_weight_exhausted() { - let a = ParaId::from(128); - - let a_msg_1 = (300u32, "a_msg_1").encode(); - let a_msg_2 = (300u32, "a_msg_2").encode(); - - new_test_ext( - GenesisConfigBuilder { - ump_service_total_weight: 500, - ump_max_individual_weight: 300, - ..Default::default() - } - .build(), - ) - .execute_with(|| { - queue_upward_msg(a, a_msg_1.clone()); - queue_upward_msg(a, a_msg_2.clone()); - - assert_storage_consistency_exhaustive(); - - // we expect only one message to fit in the first iteration. - Ump::process_pending_upward_messages(); - assert_eq!(take_processed(), vec![(a, a_msg_1)]); - assert_storage_consistency_exhaustive(); - - // second iteration should process the remaining message. - Ump::process_pending_upward_messages(); - assert_eq!(take_processed(), vec![(a, a_msg_2)]); - assert_storage_consistency_exhaustive(); - - // finally, make sure that the queue is empty. - Ump::process_pending_upward_messages(); - assert_eq!(take_processed(), vec![]); - assert_storage_consistency_exhaustive(); - }); - } - - #[test] - fn dispatch_correctly_handle_remove_of_latest() { - let a = ParaId::from(1991); - let b = ParaId::from(1999); - - let a_msg_1 = (300u32, "a_msg_1").encode(); - let a_msg_2 = (300u32, "a_msg_2").encode(); - let b_msg_1 = (300u32, "b_msg_1").encode(); - - new_test_ext( - GenesisConfigBuilder { ump_service_total_weight: 900, ..Default::default() }.build(), - ) - .execute_with(|| { - // We want to test here an edge case, where we remove the queue with the highest - // para id (i.e. last in the `needs_dispatch` order). - // - // If the last entry was removed we should proceed execution, assuming we still have - // weight available. - - queue_upward_msg(a, a_msg_1.clone()); - queue_upward_msg(a, a_msg_2.clone()); - queue_upward_msg(b, b_msg_1.clone()); - Ump::process_pending_upward_messages(); - assert_eq!(take_processed(), vec![(a, a_msg_1), (b, b_msg_1), (a, a_msg_2)]); - }); - } - - #[test] - fn verify_relay_dispatch_queue_size_is_externally_accessible() { - // Make sure that the relay dispatch queue size storage entry is accessible via well known - // keys and is decodable into a (u32, u32). - - use parity_scale_codec::Decode as _; - use primitives::v1::well_known_keys; - - let a = ParaId::from(228); - let msg = vec![1, 2, 3]; - - new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| { - queue_upward_msg(a, msg); - - let raw_queue_size = - sp_io::storage::get(&well_known_keys::relay_dispatch_queue_size(a)).expect( - "enqueing a message should create the dispatch queue\ - and it should be accessible via the well known keys", - ); - let (cnt, size) = <(u32, u32)>::decode(&mut &raw_queue_size[..]) - .expect("the dispatch queue size should be decodable into (u32, u32)"); - - assert_eq!(cnt, 1); - assert_eq!(size, 3); - }); - } - - #[test] - fn service_overweight_unknown() { - // This test just makes sure that 0 is not a valid index and we can use it not worrying in - // the next test. - new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| { - assert_noop!( - Ump::service_overweight(Origin::root(), 0, 1000), - Error::<Test>::UnknownMessageIndex - ); - }); - } - - #[test] - fn overweight_queue_works() { - let para_a = ParaId::from(2021); - - let a_msg_1 = (301u32, "a_msg_1").encode(); - let a_msg_2 = (500u32, "a_msg_2").encode(); - let a_msg_3 = (500u32, "a_msg_3").encode(); - - new_test_ext( - GenesisConfigBuilder { - ump_service_total_weight: 900, - ump_max_individual_weight: 300, - ..Default::default() - } - .build(), - ) - .execute_with(|| { - // HACK: Start with the block number 1. This is needed because should an event be - // emitted during the genesis block they will be implicitly wiped. - System::set_block_number(1); - - // This one is overweight. However, the weight is plenty and we can afford to execute - // this message, thus expect it. - queue_upward_msg(para_a, a_msg_1.clone()); - Ump::process_pending_upward_messages(); - assert_eq!(take_processed(), vec![(para_a, a_msg_1)]); - - // This is overweight and this message cannot fit into the total weight budget. - queue_upward_msg(para_a, a_msg_2.clone()); - queue_upward_msg(para_a, a_msg_3.clone()); - Ump::process_pending_upward_messages(); - assert_last_event( - Event::OverweightEnqueued(para_a, upward_message_id(&a_msg_3[..]), 0, 500).into(), - ); - - // Now verify that if we wanted to service this overweight message with less than enough - // weight it will fail. - assert_noop!( - Ump::service_overweight(Origin::root(), 0, 499), - Error::<Test>::WeightOverLimit - ); - - // ... and if we try to service it with just enough weight it will succeed as well. - assert_ok!(Ump::service_overweight(Origin::root(), 0, 500)); - assert_last_event(Event::OverweightServiced(0, 500).into()); - - // ... and if we try to service a message with index that doesn't exist it will error - // out. - assert_noop!( - Ump::service_overweight(Origin::root(), 1, 1000), - Error::<Test>::UnknownMessageIndex - ); - }); - } -} diff --git a/polkadot/runtime/parachains/src/ump/tests.rs b/polkadot/runtime/parachains/src/ump/tests.rs new file mode 100644 index 0000000000000000000000000000000000000000..8a45e7bbbb448382fafed1e6c68283dd9c84f8ad --- /dev/null +++ b/polkadot/runtime/parachains/src/ump/tests.rs @@ -0,0 +1,350 @@ +// Copyright 2020 Parity Technologies (UK) Ltd. +// This file is part of Polkadot. + +// Polkadot is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Polkadot is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Polkadot. If not, see <http://www.gnu.org/licenses/>. + +use super::*; +use crate::mock::{ + assert_last_event, new_test_ext, take_processed, Configuration, MockGenesisConfig, Origin, + System, Test, Ump, +}; +use frame_support::{assert_noop, assert_ok, weights::Weight}; +use std::collections::HashSet; + +struct GenesisConfigBuilder { + max_upward_message_size: u32, + max_upward_message_num_per_candidate: u32, + max_upward_queue_count: u32, + max_upward_queue_size: u32, + ump_service_total_weight: Weight, + ump_max_individual_weight: Weight, +} + +impl Default for GenesisConfigBuilder { + fn default() -> Self { + Self { + max_upward_message_size: 16, + max_upward_message_num_per_candidate: 2, + max_upward_queue_count: 4, + max_upward_queue_size: 64, + ump_service_total_weight: 1000, + ump_max_individual_weight: 100, + } + } +} + +impl GenesisConfigBuilder { + fn build(self) -> crate::mock::MockGenesisConfig { + let mut genesis = default_genesis_config(); + let config = &mut genesis.configuration.config; + + config.max_upward_message_size = self.max_upward_message_size; + config.max_upward_message_num_per_candidate = self.max_upward_message_num_per_candidate; + config.max_upward_queue_count = self.max_upward_queue_count; + config.max_upward_queue_size = self.max_upward_queue_size; + config.ump_service_total_weight = self.ump_service_total_weight; + config.ump_max_individual_weight = self.ump_max_individual_weight; + genesis + } +} + +fn default_genesis_config() -> MockGenesisConfig { + MockGenesisConfig { + configuration: crate::configuration::GenesisConfig { + config: crate::configuration::HostConfiguration { + max_downward_message_size: 1024, + ..Default::default() + }, + }, + ..Default::default() + } +} + +fn queue_upward_msg(para: ParaId, msg: UpwardMessage) { + let msgs = vec![msg]; + assert!(Ump::check_upward_messages(&Configuration::config(), para, &msgs).is_ok()); + let _ = Ump::receive_upward_messages(para, msgs); +} + +fn assert_storage_consistency_exhaustive() { + // check that empty queues don't clutter the storage. + for (_para, queue) in <Ump as Store>::RelayDispatchQueues::iter() { + assert!(!queue.is_empty()); + } + + // actually count the counts and sizes in queues and compare them to the bookkept version. + for (para, queue) in <Ump as Store>::RelayDispatchQueues::iter() { + let (expected_count, expected_size) = <Ump as Store>::RelayDispatchQueueSize::get(para); + let (actual_count, actual_size) = queue + .into_iter() + .fold((0, 0), |(acc_count, acc_size), x| (acc_count + 1, acc_size + x.len() as u32)); + + assert_eq!(expected_count, actual_count); + assert_eq!(expected_size, actual_size); + } + + // since we wipe the empty queues the sets of paras in queue contents, queue sizes and + // need dispatch set should all be equal. + let queue_contents_set = <Ump as Store>::RelayDispatchQueues::iter() + .map(|(k, _)| k) + .collect::<HashSet<ParaId>>(); + let queue_sizes_set = <Ump as Store>::RelayDispatchQueueSize::iter() + .map(|(k, _)| k) + .collect::<HashSet<ParaId>>(); + let needs_dispatch_set = + <Ump as Store>::NeedsDispatch::get().into_iter().collect::<HashSet<ParaId>>(); + assert_eq!(queue_contents_set, queue_sizes_set); + assert_eq!(queue_contents_set, needs_dispatch_set); + + // `NextDispatchRoundStartWith` should point into a para that is tracked. + if let Some(para) = <Ump as Store>::NextDispatchRoundStartWith::get() { + assert!(queue_contents_set.contains(¶)); + } + + // `NeedsDispatch` is always sorted. + assert!(<Ump as Store>::NeedsDispatch::get().windows(2).all(|xs| xs[0] <= xs[1])); +} + +#[test] +fn dispatch_empty() { + new_test_ext(default_genesis_config()).execute_with(|| { + assert_storage_consistency_exhaustive(); + + // make sure that the case with empty queues is handled properly + Ump::process_pending_upward_messages(); + + assert_storage_consistency_exhaustive(); + }); +} + +#[test] +fn dispatch_single_message() { + let a = ParaId::from(228); + let msg = 1000u32.encode(); + + new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| { + queue_upward_msg(a, msg.clone()); + Ump::process_pending_upward_messages(); + assert_eq!(take_processed(), vec![(a, msg)]); + + assert_storage_consistency_exhaustive(); + }); +} + +#[test] +fn dispatch_resume_after_exceeding_dispatch_stage_weight() { + let a = ParaId::from(128); + let c = ParaId::from(228); + let q = ParaId::from(911); + + let a_msg_1 = (200u32, "a_msg_1").encode(); + let a_msg_2 = (100u32, "a_msg_2").encode(); + let c_msg_1 = (300u32, "c_msg_1").encode(); + let c_msg_2 = (100u32, "c_msg_2").encode(); + let q_msg = (500u32, "q_msg").encode(); + + new_test_ext( + GenesisConfigBuilder { ump_service_total_weight: 500, ..Default::default() }.build(), + ) + .execute_with(|| { + queue_upward_msg(q, q_msg.clone()); + queue_upward_msg(c, c_msg_1.clone()); + queue_upward_msg(a, a_msg_1.clone()); + queue_upward_msg(a, a_msg_2.clone()); + + assert_storage_consistency_exhaustive(); + + // we expect only two first messages to fit in the first iteration. + Ump::process_pending_upward_messages(); + assert_eq!(take_processed(), vec![(a, a_msg_1), (c, c_msg_1)]); + assert_storage_consistency_exhaustive(); + + queue_upward_msg(c, c_msg_2.clone()); + assert_storage_consistency_exhaustive(); + + // second iteration should process the second message. + Ump::process_pending_upward_messages(); + assert_eq!(take_processed(), vec![(q, q_msg)]); + assert_storage_consistency_exhaustive(); + + // 3rd iteration. + Ump::process_pending_upward_messages(); + assert_eq!(take_processed(), vec![(a, a_msg_2), (c, c_msg_2)]); + assert_storage_consistency_exhaustive(); + + // finally, make sure that the queue is empty. + Ump::process_pending_upward_messages(); + assert_eq!(take_processed(), vec![]); + assert_storage_consistency_exhaustive(); + }); +} + +#[test] +fn dispatch_keeps_message_after_weight_exhausted() { + let a = ParaId::from(128); + + let a_msg_1 = (300u32, "a_msg_1").encode(); + let a_msg_2 = (300u32, "a_msg_2").encode(); + + new_test_ext( + GenesisConfigBuilder { + ump_service_total_weight: 500, + ump_max_individual_weight: 300, + ..Default::default() + } + .build(), + ) + .execute_with(|| { + queue_upward_msg(a, a_msg_1.clone()); + queue_upward_msg(a, a_msg_2.clone()); + + assert_storage_consistency_exhaustive(); + + // we expect only one message to fit in the first iteration. + Ump::process_pending_upward_messages(); + assert_eq!(take_processed(), vec![(a, a_msg_1)]); + assert_storage_consistency_exhaustive(); + + // second iteration should process the remaining message. + Ump::process_pending_upward_messages(); + assert_eq!(take_processed(), vec![(a, a_msg_2)]); + assert_storage_consistency_exhaustive(); + + // finally, make sure that the queue is empty. + Ump::process_pending_upward_messages(); + assert_eq!(take_processed(), vec![]); + assert_storage_consistency_exhaustive(); + }); +} + +#[test] +fn dispatch_correctly_handle_remove_of_latest() { + let a = ParaId::from(1991); + let b = ParaId::from(1999); + + let a_msg_1 = (300u32, "a_msg_1").encode(); + let a_msg_2 = (300u32, "a_msg_2").encode(); + let b_msg_1 = (300u32, "b_msg_1").encode(); + + new_test_ext( + GenesisConfigBuilder { ump_service_total_weight: 900, ..Default::default() }.build(), + ) + .execute_with(|| { + // We want to test here an edge case, where we remove the queue with the highest + // para id (i.e. last in the `needs_dispatch` order). + // + // If the last entry was removed we should proceed execution, assuming we still have + // weight available. + + queue_upward_msg(a, a_msg_1.clone()); + queue_upward_msg(a, a_msg_2.clone()); + queue_upward_msg(b, b_msg_1.clone()); + Ump::process_pending_upward_messages(); + assert_eq!(take_processed(), vec![(a, a_msg_1), (b, b_msg_1), (a, a_msg_2)]); + }); +} + +#[test] +fn verify_relay_dispatch_queue_size_is_externally_accessible() { + // Make sure that the relay dispatch queue size storage entry is accessible via well known + // keys and is decodable into a (u32, u32). + + use parity_scale_codec::Decode as _; + use primitives::v1::well_known_keys; + + let a = ParaId::from(228); + let msg = vec![1, 2, 3]; + + new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| { + queue_upward_msg(a, msg); + + let raw_queue_size = sp_io::storage::get(&well_known_keys::relay_dispatch_queue_size(a)) + .expect( + "enqueing a message should create the dispatch queue\ + and it should be accessible via the well known keys", + ); + let (cnt, size) = <(u32, u32)>::decode(&mut &raw_queue_size[..]) + .expect("the dispatch queue size should be decodable into (u32, u32)"); + + assert_eq!(cnt, 1); + assert_eq!(size, 3); + }); +} + +#[test] +fn service_overweight_unknown() { + // This test just makes sure that 0 is not a valid index and we can use it not worrying in + // the next test. + new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| { + assert_noop!( + Ump::service_overweight(Origin::root(), 0, 1000), + Error::<Test>::UnknownMessageIndex + ); + }); +} + +#[test] +fn overweight_queue_works() { + let para_a = ParaId::from(2021); + + let a_msg_1 = (301u32, "a_msg_1").encode(); + let a_msg_2 = (500u32, "a_msg_2").encode(); + let a_msg_3 = (500u32, "a_msg_3").encode(); + + new_test_ext( + GenesisConfigBuilder { + ump_service_total_weight: 900, + ump_max_individual_weight: 300, + ..Default::default() + } + .build(), + ) + .execute_with(|| { + // HACK: Start with the block number 1. This is needed because should an event be + // emitted during the genesis block they will be implicitly wiped. + System::set_block_number(1); + + // This one is overweight. However, the weight is plenty and we can afford to execute + // this message, thus expect it. + queue_upward_msg(para_a, a_msg_1.clone()); + Ump::process_pending_upward_messages(); + assert_eq!(take_processed(), vec![(para_a, a_msg_1)]); + + // This is overweight and this message cannot fit into the total weight budget. + queue_upward_msg(para_a, a_msg_2.clone()); + queue_upward_msg(para_a, a_msg_3.clone()); + Ump::process_pending_upward_messages(); + assert_last_event( + Event::OverweightEnqueued(para_a, upward_message_id(&a_msg_3[..]), 0, 500).into(), + ); + + // Now verify that if we wanted to service this overweight message with less than enough + // weight it will fail. + assert_noop!( + Ump::service_overweight(Origin::root(), 0, 499), + Error::<Test>::WeightOverLimit + ); + + // ... and if we try to service it with just enough weight it will succeed as well. + assert_ok!(Ump::service_overweight(Origin::root(), 0, 500)); + assert_last_event(Event::OverweightServiced(0, 500).into()); + + // ... and if we try to service a message with index that doesn't exist it will error + // out. + assert_noop!( + Ump::service_overweight(Origin::root(), 1, 1000), + Error::<Test>::UnknownMessageIndex + ); + }); +}