diff --git a/Cargo.lock b/Cargo.lock index 217a89d37b1b..8c774d7e67ca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7126,6 +7126,7 @@ dependencies = [ "scale-info", "serde", "sp-api", + "sp-application-crypto", "sp-core", "sp-inherents", "sp-io", diff --git a/runtime/kusama/src/weights/runtime_parachains_paras.rs b/runtime/kusama/src/weights/runtime_parachains_paras.rs index 36ec52fcf5ba..8586105c546e 100644 --- a/runtime/kusama/src/weights/runtime_parachains_paras.rs +++ b/runtime/kusama/src/weights/runtime_parachains_paras.rs @@ -110,4 +110,10 @@ impl runtime_parachains::paras::WeightInfo for WeightIn .saturating_add(T::DbWeight::get().reads(1 as Weight)) .saturating_add(T::DbWeight::get().writes(1 as Weight)) } + + fn include_pvf_check_statement_finalize_upgrade_accept() -> u64 { Weight::MAX } + fn include_pvf_check_statement_finalize_upgrade_reject() -> u64 { Weight::MAX } + fn include_pvf_check_statement_finalize_onboarding_accept() -> u64 { Weight::MAX } + fn include_pvf_check_statement_finalize_onboarding_reject() -> u64 { Weight::MAX } + fn include_pvf_check_statement() -> u64 { Weight::MAX } } diff --git a/runtime/parachains/Cargo.toml b/runtime/parachains/Cargo.toml index 028b749452d9..565cc48608fd 100644 --- a/runtime/parachains/Cargo.toml +++ b/runtime/parachains/Cargo.toml @@ -24,6 +24,7 @@ sp-staking = { git = "https://github.com/paritytech/substrate", branch = "master sp-core = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false } sp-keystore = { git = "https://github.com/paritytech/substrate", branch = "master", optional = true } sp-tracing = { version = "4.0.0-dev", branch = "master", git = "https://github.com/paritytech/substrate", default-features = false, optional = true } +sp-application-crypto = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false, optional = true } pallet-authority-discovery = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false } pallet-authorship = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false } @@ -96,6 +97,7 @@ runtime-benchmarks = [ "frame-system/runtime-benchmarks", "primitives/runtime-benchmarks", "static_assertions", + "sp-application-crypto", ] try-runtime = [ "frame-support/try-runtime", diff --git a/runtime/parachains/src/paras/benchmarking.rs b/runtime/parachains/src/paras/benchmarking.rs index bd9106422d90..9af049372b15 100644 --- a/runtime/parachains/src/paras/benchmarking.rs +++ b/runtime/parachains/src/paras/benchmarking.rs @@ -15,12 +15,16 @@ // along with Polkadot. If not, see . use super::*; -use crate::{configuration::HostConfiguration, shared}; +use crate::configuration::HostConfiguration; use frame_benchmarking::benchmarks; use frame_system::RawOrigin; use primitives::v1::{HeadData, Id as ParaId, ValidationCode, MAX_CODE_SIZE, MAX_HEAD_DATA_SIZE}; use sp_runtime::traits::{One, Saturating}; +mod pvf_check; + +use self::pvf_check::{VoteCause, VoteOutcome}; + // 2 ^ 10, because binary search time complexity is O(log(2, n)) and n = 1024 gives us a big and // round number. // Due to the limited number of parachains, the number of pruning, upcoming upgrades and cooldowns @@ -139,6 +143,48 @@ benchmarks! { let code_hash = [0; 32].into(); }: _(RawOrigin::Root, code_hash) + include_pvf_check_statement { + let (stmt, signature) = pvf_check::prepare_inclusion_bench::(); + }: { + let _ = Pallet::::include_pvf_check_statement(RawOrigin::None.into(), stmt, signature); + } + + include_pvf_check_statement_finalize_upgrade_accept { + let (stmt, signature) = pvf_check::prepare_finalization_bench::( + VoteCause::Upgrade, + VoteOutcome::Accept, + ); + }: { + let _ = Pallet::::include_pvf_check_statement(RawOrigin::None.into(), stmt, signature); + } + + include_pvf_check_statement_finalize_upgrade_reject { + let (stmt, signature) = pvf_check::prepare_finalization_bench::( + VoteCause::Upgrade, + VoteOutcome::Reject, + ); + }: { + let _ = Pallet::::include_pvf_check_statement(RawOrigin::None.into(), stmt, signature); + } + + include_pvf_check_statement_finalize_onboarding_accept { + let (stmt, signature) = pvf_check::prepare_finalization_bench::( + VoteCause::Onboarding, + VoteOutcome::Accept, + ); + }: { + let _ = Pallet::::include_pvf_check_statement(RawOrigin::None.into(), stmt, signature); + } + + include_pvf_check_statement_finalize_onboarding_reject { + let (stmt, signature) = pvf_check::prepare_finalization_bench::( + VoteCause::Onboarding, + VoteOutcome::Reject, + ); + }: { + let _ = Pallet::::include_pvf_check_statement(RawOrigin::None.into(), stmt, signature); + } + impl_benchmark_test_suite!( Pallet, crate::mock::new_test_ext(Default::default()), diff --git a/runtime/parachains/src/paras/benchmarking/pvf_check.rs b/runtime/parachains/src/paras/benchmarking/pvf_check.rs new file mode 100644 index 000000000000..0e1fdd7189ff --- /dev/null +++ b/runtime/parachains/src/paras/benchmarking/pvf_check.rs @@ -0,0 +1,195 @@ +// Copyright 2022 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 . + +//! This module focuses on the benchmarking of the `include_pvf_check_statement` dispatchable. + +use crate::{configuration, paras::*, shared::Pallet as ParasShared}; +use frame_system::RawOrigin; +use primitives::v1::{HeadData, Id as ParaId, ValidationCode, ValidatorId, ValidatorIndex}; +use sp_application_crypto::RuntimeAppPublic; + +// Constants for the benchmarking +const SESSION_INDEX: SessionIndex = 1; +const VALIDATOR_NUM: usize = 800; +const CAUSES_NUM: usize = 100; +fn validation_code() -> ValidationCode { + ValidationCode(vec![0]) +} +fn old_validation_code() -> ValidationCode { + ValidationCode(vec![1]) +} + +/// Prepares the PVF check statement and the validator signature to pass into +/// `include_pvf_check_statement` during benchmarking phase. +/// +/// It won't trigger finalization, so we expect the benchmarking will only measure the performance +/// of only vote accounting. +pub fn prepare_inclusion_bench() -> (PvfCheckStatement, ValidatorSignature) +where + T: Config + shared::Config, +{ + initialize::(); + // we do not plan to trigger finalization, thus the cause is inconsequential. + initialize_pvf_active_vote::(VoteCause::Onboarding); + + // `unwrap` cannot panic here since the `initialize` function should initialize validators count + // to be more than 0. + // + // VoteDirection doesn't matter here as well. + let stmt_n_sig = generate_statements::(VoteOutcome::Accept).next().unwrap(); + + stmt_n_sig +} + +/// Prepares conditions for benchmarking of the finalization part of `include_pvf_check_statement`. +/// +/// This function will initialize a PVF pre-check vote, then submit a number of PVF pre-checking +/// statements so that to achieve the quorum only one statement is left. This statement is returned +/// from this function and is expected to be passed into `include_pvf_check_statement` during the +/// benchmarking phase. +pub fn prepare_finalization_bench( + cause: VoteCause, + outcome: VoteOutcome, +) -> (PvfCheckStatement, ValidatorSignature) +where + T: Config + shared::Config, +{ + initialize::(); + initialize_pvf_active_vote::(cause); + + let mut stmts = generate_statements::(outcome).collect::>(); + // this should be ensured by the `initialize` function. + assert!(stmts.len() > 2); + + // stash the last statement to be used in the benchmarking phase. + let stmt_n_sig = stmts.pop().unwrap(); + + for (stmt, sig) in stmts { + let r = Pallet::::include_pvf_check_statement(RawOrigin::None.into(), stmt, sig); + assert!(r.is_ok()); + } + + stmt_n_sig +} + +/// What caused the PVF pre-checking vote? +#[derive(PartialEq, Eq, Copy, Clone, Debug)] +pub enum VoteCause { + Onboarding, + Upgrade, +} + +/// The outcome of the PVF pre-checking vote. +#[derive(PartialEq, Eq, Copy, Clone, Debug)] +pub enum VoteOutcome { + Accept, + Reject, +} + +fn initialize() +where + T: Config + shared::Config, +{ + // 0. generate a list of validators + let validators = (0..VALIDATOR_NUM) + .map(|_| ::generate_pair(None)) + .collect::>(); + + // 1. Make sure PVF pre-checking is enabled in the config. + let mut config = configuration::Pallet::::config(); + config.pvf_checking_enabled = true; + configuration::Pallet::::force_set_active_config(config.clone()); + + // 2. initialize a new session with deterministic validator set. + ParasShared::::set_active_validators_ascending(validators.clone()); + ParasShared::::set_session_index(SESSION_INDEX); +} + +/// Creates a new PVF pre-checking active vote. +/// +/// The subject of the vote (i.e. validation code) and the cause (upgrade/onboarding) is specified +/// by the test setup. +fn initialize_pvf_active_vote(vote_cause: VoteCause) +where + T: Config + shared::Config, +{ + for i in 0..CAUSES_NUM { + let id = ParaId::from(i as u32); + + if vote_cause == VoteCause::Upgrade { + // we do care about validation code being actually different, since there is a check + // that prevents upgrading to the same code. + let old_validation_code = old_validation_code(); + let validation_code = validation_code(); + + let mut parachains = ParachainsCache::new(); + Pallet::::initialize_para_now( + &mut parachains, + id, + &ParaGenesisArgs { + parachain: true, + genesis_head: HeadData(vec![1, 2, 3, 4]), + validation_code: old_validation_code, + }, + ); + // don't care about performance here, but we do care about robustness. So dump the cache + // asap. + drop(parachains); + + Pallet::::schedule_code_upgrade( + id, + validation_code, + /* relay_parent_number */ 1u32.into(), + &configuration::Pallet::::config(), + ); + } else { + let r = Pallet::::schedule_para_initialize( + id, + ParaGenesisArgs { + parachain: true, + genesis_head: HeadData(vec![1, 2, 3, 4]), + validation_code: validation_code(), + }, + ); + assert!(r.is_ok()); + } + } +} + +/// Generates a list of votes combined with signatures for the active validator set. The number of +/// votes is equal to the minimum number of votes required to reach the supermajority. +fn generate_statements( + vote_outcome: VoteOutcome, +) -> impl Iterator +where + T: Config + shared::Config, +{ + let validators = ParasShared::::active_validator_keys(); + + let required_votes = primitives::v1::supermajority_threshold(validators.len()); + (0..required_votes).map(move |validator_index| { + let stmt = PvfCheckStatement { + accept: vote_outcome == VoteOutcome::Accept, + subject: validation_code().hash(), + session_index: SESSION_INDEX, + + validator_index: ValidatorIndex(validator_index as u32), + }; + let signature = validators[validator_index].sign(&stmt.signing_payload()).unwrap(); + + (stmt, signature) + }) +} diff --git a/runtime/parachains/src/paras/mod.rs b/runtime/parachains/src/paras/mod.rs index 26b70b42bdbf..7e68e0cbaf94 100644 --- a/runtime/parachains/src/paras/mod.rs +++ b/runtime/parachains/src/paras/mod.rs @@ -407,6 +407,12 @@ pub trait WeightInfo { fn force_queue_action() -> Weight; fn add_trusted_validation_code(c: u32) -> Weight; fn poke_unused_validation_code() -> Weight; + + fn include_pvf_check_statement_finalize_upgrade_accept() -> Weight; + fn include_pvf_check_statement_finalize_upgrade_reject() -> Weight; + fn include_pvf_check_statement_finalize_onboarding_accept() -> Weight; + fn include_pvf_check_statement_finalize_onboarding_reject() -> Weight; + fn include_pvf_check_statement() -> Weight; } pub struct TestWeightInfo; @@ -432,6 +438,22 @@ impl WeightInfo for TestWeightInfo { fn poke_unused_validation_code() -> Weight { Weight::MAX } + fn include_pvf_check_statement_finalize_upgrade_accept() -> Weight { + Weight::MAX + } + fn include_pvf_check_statement_finalize_upgrade_reject() -> Weight { + Weight::MAX + } + fn include_pvf_check_statement_finalize_onboarding_accept() -> Weight { + Weight::MAX + } + fn include_pvf_check_statement_finalize_onboarding_reject() -> Weight { + Weight::MAX + } + fn include_pvf_check_statement() -> Weight { + // This special value is to distinguish from the finalizing variants above in tests. + Weight::MAX - 1 + } } #[frame_support::pallet] @@ -858,12 +880,23 @@ pub mod pallet { /// Includes a statement for a PVF pre-checking vote. Potentially, finalizes the vote and /// enacts the results if that was the last vote before achieving the supermajority. - #[pallet::weight(Weight::MAX)] + #[pallet::weight( + sp_std::cmp::max( + sp_std::cmp::max( + ::WeightInfo::include_pvf_check_statement_finalize_upgrade_accept(), + ::WeightInfo::include_pvf_check_statement_finalize_upgrade_reject(), + ), + sp_std::cmp::max( + ::WeightInfo::include_pvf_check_statement_finalize_onboarding_accept(), + ::WeightInfo::include_pvf_check_statement_finalize_onboarding_reject(), + ) + ) + )] pub fn include_pvf_check_statement( origin: OriginFor, stmt: PvfCheckStatement, signature: ValidatorSignature, - ) -> DispatchResult { + ) -> DispatchResultWithPostInfo { ensure_none(origin)?; // Make sure that PVF pre-checking is enabled. @@ -934,13 +967,17 @@ pub mod pallet { Self::enact_pvf_rejected(&stmt.subject, active_vote.causes); }, } + + // No weight refund since this statement was the last one and lead to finalization. + Ok(().into()) } else { - // No quorum has been achieved. So just store the updated state back into the - // storage. + // No quorum has been achieved. + // + // - So just store the updated state back into the storage. + // - Only charge weight for simple vote inclusion. PvfActiveVoteMap::::insert(&stmt.subject, active_vote); + Ok(Some(::WeightInfo::include_pvf_check_statement()).into()) } - - Ok(()) } } diff --git a/runtime/parachains/src/paras/tests.rs b/runtime/parachains/src/paras/tests.rs index eb0bd6008399..6ff62842abd3 100644 --- a/runtime/parachains/src/paras/tests.rs +++ b/runtime/parachains/src/paras/tests.rs @@ -1278,7 +1278,8 @@ fn pvf_check_submit_vote() { ::validate_unsigned(TransactionSource::InBlock, &call) .map(|_| ()); let dispatch_result = - Paras::include_pvf_check_statement(None.into(), stmt.clone(), signature.clone()); + Paras::include_pvf_check_statement(None.into(), stmt.clone(), signature.clone()) + .map(|_| ()); (validate_unsigned, dispatch_result) }; @@ -1376,6 +1377,68 @@ fn pvf_check_submit_vote() { }); } +#[test] +fn include_pvf_check_statement_refunds_weight() { + let a = ParaId::from(111); + let new_code: ValidationCode = vec![3, 2, 1].into(); + + let paras = vec![( + a, + ParaGenesisArgs { + parachain: false, + genesis_head: Default::default(), + validation_code: ValidationCode(vec![]), // valid since in genesis + }, + )]; + + let genesis_config = MockGenesisConfig { + paras: GenesisConfig { paras, ..Default::default() }, + configuration: crate::configuration::GenesisConfig { + config: HostConfiguration { pvf_checking_enabled: true, ..Default::default() }, + ..Default::default() + }, + ..Default::default() + }; + + new_test_ext(genesis_config).execute_with(|| { + // At this point `a` is already onboarded. Run to block 1 performing session change at + // the end of block #0. + run_to_block(2, Some(vec![1])); + + // Relay parent of the block that schedules the upgrade. + const RELAY_PARENT: BlockNumber = 1; + // Expected current session index. + const EXPECTED_SESSION: SessionIndex = 1; + + Paras::schedule_code_upgrade(a, new_code.clone(), RELAY_PARENT, &Configuration::config()); + + let mut stmts = IntoIterator::into_iter([0, 1, 2, 3]) + .map(|i| { + let stmt = PvfCheckStatement { + accept: true, + subject: new_code.hash(), + session_index: EXPECTED_SESSION, + validator_index: (i as u32).into(), + }; + let sig = VALIDATORS[i].sign(&stmt.signing_payload()); + (stmt, sig) + }) + .collect::>(); + let last_one = stmts.pop().unwrap(); + + // Verify that just vote submission is priced accordingly. + for (stmt, sig) in stmts { + let r = Paras::include_pvf_check_statement(None.into(), stmt, sig.into()).unwrap(); + assert_eq!(r.actual_weight, Some(TestWeightInfo::include_pvf_check_statement())); + } + + // Verify that the last statement is priced maximally. + let (stmt, sig) = last_one; + let r = Paras::include_pvf_check_statement(None.into(), stmt, sig.into()).unwrap(); + assert_eq!(r.actual_weight, None); + }); +} + #[test] fn add_trusted_validation_code_inserts_with_no_users() { // This test is to ensure that trusted validation code is inserted into the storage diff --git a/runtime/parachains/src/shared.rs b/runtime/parachains/src/shared.rs index 7bd33c503c63..1315ebc01f4b 100644 --- a/runtime/parachains/src/shared.rs +++ b/runtime/parachains/src/shared.rs @@ -124,7 +124,7 @@ impl Pallet { CurrentSessionIndex::::set(index); } - #[cfg(test)] + #[cfg(any(feature = "runtime-benchmarks", test))] pub(crate) fn set_active_validators_ascending(active: Vec) { ActiveValidatorIndices::::set( (0..active.len()).map(|i| ValidatorIndex(i as _)).collect(), diff --git a/runtime/polkadot/src/weights/runtime_parachains_paras.rs b/runtime/polkadot/src/weights/runtime_parachains_paras.rs index 1e67b0e9359c..f1f6f30a236d 100644 --- a/runtime/polkadot/src/weights/runtime_parachains_paras.rs +++ b/runtime/polkadot/src/weights/runtime_parachains_paras.rs @@ -110,4 +110,9 @@ impl runtime_parachains::paras::WeightInfo for WeightIn .saturating_add(T::DbWeight::get().reads(1 as Weight)) .saturating_add(T::DbWeight::get().writes(1 as Weight)) } + fn include_pvf_check_statement_finalize_upgrade_accept() -> u64 { Weight::MAX } + fn include_pvf_check_statement_finalize_upgrade_reject() -> u64 { Weight::MAX } + fn include_pvf_check_statement_finalize_onboarding_accept() -> u64 { Weight::MAX } + fn include_pvf_check_statement_finalize_onboarding_reject() -> u64 { Weight::MAX } + fn include_pvf_check_statement() -> u64 { Weight::MAX } } diff --git a/runtime/rococo/src/weights/runtime_parachains_paras.rs b/runtime/rococo/src/weights/runtime_parachains_paras.rs index 10d0cd013021..82d1ebce79a4 100644 --- a/runtime/rococo/src/weights/runtime_parachains_paras.rs +++ b/runtime/rococo/src/weights/runtime_parachains_paras.rs @@ -110,4 +110,9 @@ impl runtime_parachains::paras::WeightInfo for WeightIn .saturating_add(T::DbWeight::get().reads(1 as Weight)) .saturating_add(T::DbWeight::get().writes(1 as Weight)) } + fn include_pvf_check_statement_finalize_upgrade_accept() -> u64 { Weight::MAX } + fn include_pvf_check_statement_finalize_upgrade_reject() -> u64 { Weight::MAX } + fn include_pvf_check_statement_finalize_onboarding_accept() -> u64 { Weight::MAX } + fn include_pvf_check_statement_finalize_onboarding_reject() -> u64 { Weight::MAX } + fn include_pvf_check_statement() -> u64 { Weight::MAX } } diff --git a/runtime/westend/src/weights/runtime_parachains_paras.rs b/runtime/westend/src/weights/runtime_parachains_paras.rs index 630f51edc4e5..4224a776c3b1 100644 --- a/runtime/westend/src/weights/runtime_parachains_paras.rs +++ b/runtime/westend/src/weights/runtime_parachains_paras.rs @@ -113,4 +113,9 @@ impl runtime_parachains::paras::WeightInfo for WeightIn .saturating_add(T::DbWeight::get().reads(1 as Weight)) .saturating_add(T::DbWeight::get().writes(1 as Weight)) } + fn include_pvf_check_statement_finalize_upgrade_accept() -> u64 { Weight::MAX } + fn include_pvf_check_statement_finalize_upgrade_reject() -> u64 { Weight::MAX } + fn include_pvf_check_statement_finalize_onboarding_accept() -> u64 { Weight::MAX } + fn include_pvf_check_statement_finalize_onboarding_reject() -> u64 { Weight::MAX } + fn include_pvf_check_statement() -> u64 { Weight::MAX } }