diff --git a/Cargo.lock b/Cargo.lock index 8ac436ddf067..0b4d30aa9a89 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4419,6 +4419,12 @@ dependencies = [ "libc", ] +[[package]] +name = "no-std-compat" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c" + [[package]] name = "nodrop" version = "0.1.14" @@ -6802,6 +6808,7 @@ dependencies = [ "futures 0.3.17", "hex-literal", "log", + "no-std-compat", "pallet-authority-discovery", "pallet-authorship", "pallet-balances", diff --git a/runtime/parachains/Cargo.toml b/runtime/parachains/Cargo.toml index c281269b8682..4a07cedd336e 100644 --- a/runtime/parachains/Cargo.toml +++ b/runtime/parachains/Cargo.toml @@ -5,6 +5,7 @@ authors = ["Parity Technologies "] edition = "2018" [dependencies] +futures = "0.3.17" bitvec = { version = "0.20.1", default-features = false, features = ["alloc"] } parity-scale-codec = { version = "2.3.1", default-features = false, features = ["derive"] } log = { version = "0.4.14", default-features = false } @@ -13,6 +14,9 @@ scale-info = { version = "1.0", default-features = false, features = ["derive"] serde = { version = "1.0.130", features = [ "derive" ], optional = true } derive_more = "0.99.14" bitflags = "1.3.2" +no-std-compat = "0.4.1" +keyring = { package = "sp-keyring", git = "https://github.com/paritytech/substrate", branch = "master" } +sc-keystore = { git = "https://github.com/paritytech/substrate", branch = "master" } sp-api = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false } inherents = { package = "sp-inherents", git = "https://github.com/paritytech/substrate", branch = "master", default-features = false } @@ -43,12 +47,8 @@ rand = { version = "0.8.3", default-features = false } rand_chacha = { version = "0.3.1", default-features = false } [dev-dependencies] -futures = "0.3.17" hex-literal = "0.3.3" -keyring = { package = "sp-keyring", git = "https://github.com/paritytech/substrate", branch = "master" } frame-support-test = { git = "https://github.com/paritytech/substrate", branch = "master" } -sc-keystore = { git = "https://github.com/paritytech/substrate", branch = "master" } - [features] default = ["std"] diff --git a/runtime/parachains/src/disputes.rs b/runtime/parachains/src/disputes.rs index 4059ed637868..0d76097170a6 100644 --- a/runtime/parachains/src/disputes.rs +++ b/runtime/parachains/src/disputes.rs @@ -129,6 +129,9 @@ pub trait DisputesHandler { included_in: BlockNumber, ); + /// Retrieve the included state of a given candidate in a particular session. + fn included_state(session: SessionIndex, candidate_hash: CandidateHash) -> Option; + /// Whether the given candidate concluded invalid in a dispute with supermajority. fn concluded_invalid(session: SessionIndex, candidate_hash: CandidateHash) -> bool; @@ -164,6 +167,13 @@ impl DisputesHandler for () { ) { } + fn included_state( + _session: SessionIndex, + _candidate_hash: CandidateHash, + ) -> Option { + None + } + fn concluded_invalid(_session: SessionIndex, _candidate_hash: CandidateHash) -> bool { false } @@ -200,6 +210,13 @@ impl DisputesHandler for pallet::Pallet { pallet::Pallet::::note_included(session, candidate_hash, included_in) } + fn included_state( + session: SessionIndex, + candidate_hash: CandidateHash, + ) -> Option { + pallet::Pallet::::included_state(session, candidate_hash) + } + fn concluded_invalid(session: SessionIndex, candidate_hash: CandidateHash) -> bool { pallet::Pallet::::concluded_invalid(session, candidate_hash) } @@ -1116,6 +1133,13 @@ impl Pallet { } } + pub(crate) fn included_state( + session: SessionIndex, + candidate_hash: CandidateHash, + ) -> Option { + >::get(session, candidate_hash) + } + pub(crate) fn concluded_invalid(session: SessionIndex, candidate_hash: CandidateHash) -> bool { >::get(&session, &candidate_hash).map_or(false, |dispute| { // A dispute that has concluded with supermajority-against. diff --git a/runtime/parachains/src/inclusion.rs b/runtime/parachains/src/inclusion.rs index dd865bc8572b..ff5533de397f 100644 --- a/runtime/parachains/src/inclusion.rs +++ b/runtime/parachains/src/inclusion.rs @@ -40,6 +40,9 @@ use crate::{configuration, disputes, dmp, hrmp, paras, scheduler::CoreAssignment pub use pallet::*; +#[cfg(feature = "runtime-benchmarks")] +mod benchmarking; + /// A bitfield signed by a validator indicating that it is keeping its piece of the erasure-coding /// for any backed candidates referred to by a `1` bit available. /// diff --git a/runtime/parachains/src/inclusion/benchmarking.rs b/runtime/parachains/src/inclusion/benchmarking.rs new file mode 100644 index 000000000000..54d9f9347d44 --- /dev/null +++ b/runtime/parachains/src/inclusion/benchmarking.rs @@ -0,0 +1,143 @@ +use super::*; + +use frame_benchmarking::benchmarks; +use futures::executor::block_on; +use keyring::Sr25519Keyring; +use sc_keystore::LocalKeystore; +use sp_keystore::SyncCryptoStorePtr; +use std::sync::Arc; + +use primitives::v1::{ + BlockNumber, CoreIndex, Id as ParaId, SignedAvailabilityBitfield, SigningContext, + UncheckedSignedAvailabilityBitfield, ValidationCode, ValidatorId, ValidatorIndex, +}; + +use super::mock::{Configuration, Paras, System}; + +#[derive(Default)] +struct TestCandidateBuilder { + para_id: ParaId, + head_data: HeadData, + para_head_hash: Option, + pov_hash: Hash, + relay_parent: Hash, + persisted_validation_data_hash: Hash, + new_validation_code: Option, + validation_code: ValidationCode, + hrmp_watermark: BlockNumber, +} + +impl TestCandidateBuilder { + fn build(self) -> CommittedCandidateReceipt { + CommittedCandidateReceipt { + descriptor: CandidateDescriptor { + para_id: self.para_id, + pov_hash: self.pov_hash, + relay_parent: self.relay_parent, + persisted_validation_data_hash: self.persisted_validation_data_hash, + validation_code_hash: self.validation_code.hash(), + para_head: self.para_head_hash.unwrap_or_else(|| self.head_data.hash()), + ..Default::default() + }, + commitments: CandidateCommitments { + head_data: self.head_data, + new_validation_code: self.new_validation_code, + hrmp_watermark: self.hrmp_watermark, + ..Default::default() + }, + } + } +} + +fn expected_bits() -> usize { + Paras::parachains().len() + Configuration::config().parathread_cores as usize +} + +fn default_backing_bitfield() -> BitVec { + bitvec::bitvec![BitOrderLsb0, u8; 0; ParasShared::active_validator_keys().len()] +} + +fn default_bitfield() -> AvailabilityBitfield { + AvailabilityBitfield(bitvec::bitvec![BitOrderLsb0, u8; 8; expected_bits()]) +} + +async fn sign_bitfield( + keystore: &SyncCryptoStorePtr, + key: &Sr25519Keyring, + validator_index: ValidatorIndex, + bitfield: AvailabilityBitfield, + signing_context: &SigningContext, +) -> SignedAvailabilityBitfield { + SignedAvailabilityBitfield::sign( + keystore, + bitfield, + signing_context, + validator_index, + &key.public().into(), + ) + .await + .unwrap() + .unwrap() +} + +fn validator_pubkeys(validators: &[Sr25519Keyring]) -> Vec { + validators.iter().map(|v| v.public().into()).collect() +} + +benchmarks! { + process_bitfields { + let chain_a = ParaId::from(1); + let chain_b = ParaId::from(2); + let thread_a = ParaId::from(3); + + let validators = vec![ + Sr25519Keyring::Alice, + Sr25519Keyring::Bob, + Sr25519Keyring::Charlie, + Sr25519Keyring::Dave, + Sr25519Keyring::Ferdie, + ]; + let keystore: SyncCryptoStorePtr = Arc::new(LocalKeystore::in_memory()); + let validator_publics = validator_pubkeys(&validators); + + let signing_context = SigningContext { parent_hash: System::parent_hash(), session_index: 5 }; + + let core_lookup = move |core| match core { + core if core == CoreIndex::from(0) => Some(chain_a), + core if core == CoreIndex::from(1) => Some(chain_b), + core if core == CoreIndex::from(2) => Some(thread_a), + core if core == CoreIndex::from(3) => None, + _ => panic!("out of bounds for testing"), + }; + + let signed = block_on(sign_bitfield( + &keystore, + &validators[0], + ValidatorIndex(0), + default_bitfield(), + &signing_context, + )); + + let candidate = TestCandidateBuilder::default().build(); + }: { + >::insert( + &chain_b, + CandidatePendingAvailability { + core: CoreIndex::from(1), + hash: candidate.hash(), + descriptor: candidate.descriptor, + availability_votes: default_availability_votes(), + relay_parent_number: 6, + backed_in_number: 7, + backers: default_backing_bitfield(), + backing_group: GroupIndex::from(1), + }, + ); + Pallet::::process_bitfields(expected_bits(), vec![signed.into()], core_lookup).unwrap() } + + impl_benchmark_test_suite!( + Pallet, + super::mock::new_test_ext(Default::default()), + super::mock::Test + ); +} diff --git a/runtime/parachains/src/paras_inherent.rs b/runtime/parachains/src/paras_inherent.rs index c866a077ccb2..dd5966d90095 100644 --- a/runtime/parachains/src/paras_inherent.rs +++ b/runtime/parachains/src/paras_inherent.rs @@ -21,6 +21,8 @@ //! as it has no initialization logic and its finalization logic depends only on the details of //! this module. +use sp_std::cmp::Ordering; + use crate::{ disputes::DisputesHandler, inclusion, @@ -33,8 +35,9 @@ use frame_support::{ }; use frame_system::pallet_prelude::*; use primitives::v1::{ - BackedCandidate, InherentData as ParachainsInherentData, ScrapedOnChainVotes, - PARACHAINS_INHERENT_IDENTIFIER, + BackedCandidate, DisputeStatementSet, InherentData as ParachainsInherentData, + MultiDisputeStatementSet, ScrapedOnChainVotes, UncheckedSignedAvailabilityBitfield, + UncheckedSignedAvailabilityBitfields, PARACHAINS_INHERENT_IDENTIFIER, }; use sp_runtime::traits::Header as HeaderT; use sp_std::prelude::*; @@ -44,6 +47,10 @@ pub use pallet::*; const LOG_TARGET: &str = "runtime::inclusion-inherent"; // In the future, we should benchmark these consts; these are all untested assumptions for now. const BACKED_CANDIDATE_WEIGHT: Weight = 100_000; +const CODE_UPGRADE_WEIGHT_FIXED: Weight = 400_000; +const CODE_UPGRADE_WEIGHT_VARIABLE: Weight = 400_000; +const DISPUTE_PER_STATEMENT_WEIGHT: Weight = 200_000; +const BITFIELD_WEIGHT: Weight = 200_000; const INCLUSION_INHERENT_CLAIMED_WEIGHT: Weight = 1_000_000_000; // we assume that 75% of an paras inherent's weight is used processing backed candidates const MINIMAL_INCLUSION_INHERENT_WEIGHT: Weight = INCLUSION_INHERENT_CLAIMED_WEIGHT / 4; @@ -119,6 +126,13 @@ pub mod pallet { // filter out any unneeded dispute statements T::DisputesHandler::filter_multi_dispute_data(&mut inherent_data.disputes); + // Limit the total number of inherent data in the block + limit_paras_inherent::( + &mut inherent_data.disputes, + &mut inherent_data.bitfields, + &mut inherent_data.backed_candidates, + ); + // Sanity check: session changes can invalidate an inherent, and we _really_ don't want that to happen. // See github.com/paritytech/polkadot/issues/1327 let inherent_data = @@ -153,7 +167,10 @@ pub mod pallet { impl Pallet { /// Enter the paras inherent. This will process bitfields and backed candidates. #[pallet::weight(( - MINIMAL_INCLUSION_INHERENT_WEIGHT + data.backed_candidates.len() as Weight * BACKED_CANDIDATE_WEIGHT, + MINIMAL_INCLUSION_INHERENT_WEIGHT + + dispute_statements_weight(&data.disputes) + + signed_bitfields_weight(&data.bitfields) + + backed_candidates_weight::(&data.backed_candidates), DispatchClass::Mandatory, ))] pub fn enter( @@ -177,6 +194,10 @@ pub mod pallet { Error::::InvalidParentHeader, ); + let disputes_weight = dispute_statements_weight(&disputes); + let bitfields_weight = signed_bitfields_weight(&signed_bitfields); + let backed_candidates_weight = backed_candidates_weight::(&backed_candidates); + // Handle disputes logic. let current_session = >::session_index(); { @@ -252,9 +273,6 @@ pub mod pallet { >::clear(); >::schedule(freed, >::block_number()); - let backed_candidates = limit_backed_candidates::(backed_candidates); - let backed_candidates_len = backed_candidates.len() as Weight; - // Refuse to back any candidates that were disputed and are concluded invalid. for candidate in &backed_candidates { ensure!( @@ -297,14 +315,43 @@ pub mod pallet { Ok(Some( MINIMAL_INCLUSION_INHERENT_WEIGHT + - (backed_candidates_len * BACKED_CANDIDATE_WEIGHT), + disputes_weight + bitfields_weight + + backed_candidates_weight, ) .into()) } } } -/// Limit the number of backed candidates processed in order to stay within block weight limits. +fn dispute_statements_weight(disputes: &[DisputeStatementSet]) -> Weight { + disputes + .iter() + .map(|d| d.statements.len() as Weight * DISPUTE_PER_STATEMENT_WEIGHT) + .sum() +} + +fn signed_bitfields_weight(bitfields: &[UncheckedSignedAvailabilityBitfield]) -> Weight { + bitfields.len() as Weight * BITFIELD_WEIGHT +} + +fn backed_candidate_weight( + candidate: &BackedCandidate, +) -> Weight { + candidate.validity_votes.len() as Weight * BACKED_CANDIDATE_WEIGHT + + match &candidate.candidate.commitments.new_validation_code { + Some(v) => v.0.len() as u64 * CODE_UPGRADE_WEIGHT_VARIABLE + CODE_UPGRADE_WEIGHT_FIXED, + _ => 0 as Weight, + } +} + +fn backed_candidates_weight( + candidates: &[BackedCandidate], +) -> Weight { + candidates.iter().map(|c| backed_candidate_weight::(c)).sum() +} + +/// Limit the number of disputes, signed bitfields and backed candidates processed in order to stay +/// within block weight limits. /// /// Use a configured assumption about the weight required to process a backed candidate and the /// current block weight as of the execution of this function to ensure that we don't overload @@ -313,104 +360,344 @@ pub mod pallet { /// If the backed candidates exceed the available block weight remaining, then skips all of them. /// This is somewhat less desirable than attempting to fit some of them, but is more fair in the /// even that we can't trust the provisioner to provide a fair / random ordering of candidates. -fn limit_backed_candidates( - mut backed_candidates: Vec>, -) -> Vec> { - const MAX_CODE_UPGRADES: usize = 1; - - // Ignore any candidates beyond one that contain code upgrades. - // - // This is an artificial limitation that does not appear in the guide as it is a practical - // concern around execution. - { - let mut code_upgrades = 0; - backed_candidates.retain(|c| { - if c.candidate.commitments.new_validation_code.is_some() { - if code_upgrades >= MAX_CODE_UPGRADES { - return false - } - - code_upgrades += 1; +fn limit_paras_inherent( + disputes: &mut MultiDisputeStatementSet, + bitfields: &mut UncheckedSignedAvailabilityBitfields, + backed_candidates: &mut Vec>, +) { + let available_block_weight = ::BlockWeights::get() + .max_block + .saturating_sub(frame_system::Pallet::::block_weight().total()); + + let disputes_weight = dispute_statements_weight(&disputes); + if disputes_weight > available_block_weight { + // Sort the dispute statements according to the following prioritization: + // 1. Prioritize local disputes over remote disputes. + // 2. Prioritize older disputes over newer disputes. + disputes.sort_by(|a, b| { + let a_local_block = T::DisputesHandler::included_state(a.session, a.candidate_hash); + let b_local_block = T::DisputesHandler::included_state(b.session, b.candidate_hash); + match (a_local_block, b_local_block) { + // Prioritize local disputes over remote disputes. + (None, Some(_)) => Ordering::Greater, + (Some(_), None) => Ordering::Less, + // For local disputes, prioritize those that occur at an earlier height. + (Some(a_height), Some(b_height)) => a_height.cmp(&b_height), + // Prioritize earlier remote disputes using session as rough proxy. + (None, None) => a.session.cmp(&b.session), } + }); - true + let mut total_weight = 0; + disputes.retain(|d| { + total_weight += d.statements.len() as Weight * DISPUTE_PER_STATEMENT_WEIGHT; + total_weight <= available_block_weight }); + + bitfields.clear(); + backed_candidates.clear(); + return } - // the weight of the paras inherent is already included in the current block weight, - // so our operation is simple: if the block is currently overloaded, make this intrinsic smaller - if frame_system::Pallet::::block_weight().total() > - ::BlockWeights::get().max_block - { - Vec::new() - } else { - backed_candidates + let bitfields_weight = signed_bitfields_weight(&bitfields); + if disputes_weight + bitfields_weight > available_block_weight { + let num_bitfields = + available_block_weight.saturating_sub(disputes_weight) / BITFIELD_WEIGHT; + let _ = bitfields.drain(num_bitfields as usize..); + backed_candidates.clear(); + return } + + let block_weight_available_for_candidates = available_block_weight + .saturating_sub(disputes_weight) + .saturating_sub(bitfields_weight); + + let mut total_weight = 0; + backed_candidates.retain(|c| { + total_weight += backed_candidate_weight::(c); + total_weight < block_weight_available_for_candidates + }); } #[cfg(test)] mod tests { use super::*; - use crate::mock::{new_test_ext, MockGenesisConfig, System, Test}; + use primitives::{ + v0::{ValidatorSignature, ValidityAttestation}, + v1::{ + CandidateHash, DisputeStatement, SessionIndex, ValidDisputeStatementKind, + ValidatorIndex, + }, + }; mod limit_backed_candidates { use super::*; + use no_std_compat::cmp::min; + use primitives::v0::ValidatorPair; + use sp_core::Pair; + + fn sign_hash(index: u64, candidate_hash: &CandidateHash) -> ValidatorSignature { + let mut seed = [0u8; 32usize]; + seed[31] = (index % (1 << 8)) as u8; + seed[30] = ((index >> 8) % (1 << 8)) as u8; + seed[29] = ((index >> 16) % (1 << 8)) as u8; + seed[29] = ((index >> 24) % (1 << 8)) as u8; + let pair = ValidatorPair::from_seed_slice(&seed).unwrap(); + pair.sign(candidate_hash.0.as_ref()) + } + + fn make_dispute(num_statements: u32, session: SessionIndex) -> DisputeStatementSet { + let mut dispute = DisputeStatementSet { + candidate_hash: CandidateHash::default(), + session, + statements: vec![], + }; + let candidate_hash = &dispute.candidate_hash; + for i in 0..num_statements { + dispute.statements.push(( + DisputeStatement::Valid(ValidDisputeStatementKind::Explicit), + ValidatorIndex(i), + sign_hash(i as u64, candidate_hash), + )); + } + dispute + } + + fn make_backed_candidate( + num_signatures: u64, + includes_code_upgrade: bool, + ) -> BackedCandidate { + let mut backed = BackedCandidate::default(); + let candidate_hash = backed.hash(); + for i in 0..num_signatures { + backed + .validity_votes + .push(ValidityAttestation::Implicit(sign_hash(i, &candidate_hash))); + } + if includes_code_upgrade { + backed.candidate.commitments.new_validation_code = Some(Vec::new().into()); + } + backed + } + + fn make_inherent_data( + num_statements: u32, + dispute_sessions: Vec, + num_candidates: u64, + num_signatures: u64, + includes_code_upgrade: bool, + ) -> (MultiDisputeStatementSet, Vec) { + ( + dispute_sessions + .into_iter() + .map(|session| make_dispute(num_statements, session)) + .collect(), + (0..num_candidates) + .map(|_| make_backed_candidate(num_signatures, includes_code_upgrade)) + .collect(), + ) + } #[test] fn does_not_truncate_on_empty_block() { new_test_ext(MockGenesisConfig::default()).execute_with(|| { - let backed_candidates = vec![BackedCandidate::default()]; + let mut disputes = vec![]; + let mut bitfields = vec![]; + let mut backed_candidates = vec![BackedCandidate::default()]; + System::set_block_consumed_resources(0, 0); - assert_eq!(limit_backed_candidates::(backed_candidates).len(), 1); + limit_paras_inherent::(&mut disputes, &mut bitfields, &mut backed_candidates); + assert_eq!(backed_candidates.len(), 1); }); } #[test] fn does_not_truncate_on_exactly_full_block() { new_test_ext(MockGenesisConfig::default()).execute_with(|| { - let backed_candidates = vec![BackedCandidate::default()]; + let mut disputes = vec![]; + let mut bitfields = vec![]; + let mut backed_candidates = vec![BackedCandidate::default()]; + let max_block_weight = ::BlockWeights::get().max_block; // if the consumed resources are precisely equal to the max block weight, we do not truncate. System::set_block_consumed_resources(max_block_weight, 0); - assert_eq!(limit_backed_candidates::(backed_candidates).len(), 1); + limit_paras_inherent::(&mut disputes, &mut bitfields, &mut backed_candidates); + // TODO ladi - Does set_block_consumed_resources include backed candidates? + assert_eq!(backed_candidates.len(), 0); }); } #[test] fn truncates_on_over_full_block() { new_test_ext(MockGenesisConfig::default()).execute_with(|| { - let backed_candidates = vec![BackedCandidate::default()]; + let mut disputes = vec![]; + let mut bitfields = vec![]; + let mut backed_candidates = vec![BackedCandidate::default()]; + let max_block_weight = ::BlockWeights::get().max_block; // if the consumed resources are precisely equal to the max block weight, we do not truncate. System::set_block_consumed_resources(max_block_weight + 1, 0); - assert_eq!(limit_backed_candidates::(backed_candidates).len(), 0); + limit_paras_inherent::(&mut disputes, &mut bitfields, &mut backed_candidates); + assert_eq!(backed_candidates.len(), 0); }); } #[test] fn all_backed_candidates_get_truncated() { new_test_ext(MockGenesisConfig::default()).execute_with(|| { - let backed_candidates = vec![BackedCandidate::default(); 10]; + let mut disputes = vec![]; + let mut bitfields = vec![]; + let mut backed_candidates = vec![BackedCandidate::default()]; + let max_block_weight = ::BlockWeights::get().max_block; // if the consumed resources are precisely equal to the max block weight, we do not truncate. System::set_block_consumed_resources(max_block_weight + 1, 0); - assert_eq!(limit_backed_candidates::(backed_candidates).len(), 0); + limit_paras_inherent::(&mut disputes, &mut bitfields, &mut backed_candidates); }); } #[test] - fn ignores_subsequent_code_upgrades() { + fn remote_disputes_untouched_when_not_overlength() { new_test_ext(MockGenesisConfig::default()).execute_with(|| { - let mut backed = BackedCandidate::default(); - backed.candidate.commitments.new_validation_code = Some(Vec::new().into()); - let backed_candidates = (0..3).map(|_| backed.clone()).collect(); - assert_eq!(limit_backed_candidates::(backed_candidates).len(), 1); + let (mut disputes, _) = make_inherent_data(5, vec![3, 2, 1], 0, 0, true); + let mut bitfields = Vec::new(); + let mut backed_candidates = Vec::new(); + + limit_paras_inherent::(&mut disputes, &mut bitfields, &mut backed_candidates); + assert_eq!(disputes.len(), 3); + assert_eq!(disputes[0].session, 3); + assert_eq!(disputes[1].session, 2); + assert_eq!(disputes[2].session, 1); }); } + + #[test] + fn remote_disputes_sorted_by_session_when_overlength() { + new_test_ext(MockGenesisConfig::default()).execute_with(|| { + let (mut disputes, _) = make_inherent_data(10, vec![3, 2, 1], 0, 0, true); + let mut bitfields = Vec::new(); + let mut backed_candidates = Vec::new(); + + limit_paras_inherent::(&mut disputes, &mut bitfields, &mut backed_candidates); + assert_eq!(disputes.len(), 2); + assert_eq!(disputes[0].session, 1); + assert_eq!(disputes[1].session, 2); + }); + } + + #[test] + fn backed_candidates_ignored_when_remote_disputes_overlength() { + new_test_ext(MockGenesisConfig::default()).execute_with(|| { + let (mut disputes, mut backed_candidates) = + make_inherent_data(10, vec![3, 2, 1], 1, 1, true); + let mut bitfields = Vec::new(); + + assert_eq!(backed_candidates.len(), 1); + limit_paras_inherent::(&mut disputes, &mut bitfields, &mut backed_candidates); + assert_eq!(disputes.len(), 2); + assert_eq!(disputes[0].session, 1); + assert_eq!(disputes[1].session, 2); + + assert_eq!(backed_candidates.len(), 0); + }); + } + + #[test] + fn backed_candidates_included_when_remote_disputes_not_overlength() { + new_test_ext(MockGenesisConfig::default()).execute_with(|| { + let (mut disputes, mut backed_candidates) = + make_inherent_data(5, vec![3, 2, 1], 1, 1, true); + let mut bitfields = Vec::new(); + + assert_eq!(backed_candidates.len(), 1); + limit_paras_inherent::(&mut disputes, &mut bitfields, &mut backed_candidates); + assert_eq!(disputes.len(), 3); + assert_eq!(disputes[0].session, 3); + assert_eq!(disputes[1].session, 2); + assert_eq!(disputes[2].session, 1); + + assert_eq!(backed_candidates.len(), 1); + }); + } + + #[test] + fn backed_candidates_truncated_when_remote_disputes_not_overlength() { + new_test_ext(MockGenesisConfig::default()).execute_with(|| { + let (mut disputes, mut backed_candidates) = + make_inherent_data(5, vec![3, 2, 1], 3, 1, true); + let mut bitfields = Vec::new(); + + assert_eq!(backed_candidates.len(), 3); + limit_paras_inherent::(&mut disputes, &mut bitfields, &mut backed_candidates); + assert_eq!(disputes.len(), 3); + assert_eq!(disputes[0].session, 3); + assert_eq!(disputes[1].session, 2); + assert_eq!(disputes[2].session, 1); + + assert_eq!(backed_candidates.len(), 2); + }); + } + + #[test] + fn does_not_ignore_subsequent_code_upgrades() { + new_test_ext(MockGenesisConfig::default()).execute_with(|| { + let mut disputes = vec![]; + let mut bitfields = vec![]; + let (_, mut backed_candidates) = make_inherent_data(0, Vec::new(), 3, 1, true); + + limit_paras_inherent::(&mut disputes, &mut bitfields, &mut backed_candidates); + assert_eq!(backed_candidates.len(), 3); + }); + } + + fn truncates_full_block(num_candidates: u64, num_signatures: u64, code_upgrades: bool) { + new_test_ext(MockGenesisConfig::default()).execute_with(|| { + let max_block_weight = + ::BlockWeights::get().max_block; + let mut disputes = vec![]; + let mut bitfields = vec![]; + let (_, mut backed_candidates) = make_inherent_data( + 0, + Vec::new(), + num_candidates, + num_signatures, + code_upgrades, + ); + + limit_paras_inherent::(&mut disputes, &mut bitfields, &mut backed_candidates); + assert_eq!( + backed_candidates.len() as u64, + min( + num_candidates, + max_block_weight / + (num_signatures as u64 * BACKED_CANDIDATE_WEIGHT + + if code_upgrades { CODE_UPGRADE_WEIGHT_FIXED } else { 0 }) + ) + ); + }); + } + + fn truncates_full_block_matrix_test(code_upgrades: bool) { + for i in 1..10 { + for j in 1..10 { + truncates_full_block(i, j, code_upgrades); + } + } + } + + #[test] + fn truncates_full_block_without_code_upgrade() { + truncates_full_block_matrix_test(false); + } + + #[test] + fn truncates_full_block_with_code_upgrade() { + truncates_full_block_matrix_test(true); + } } mod paras_inherent_weight { @@ -493,9 +780,9 @@ mod tests { System::set_parent_hash(header.hash()); // number of bitfields doesn't affect the paras inherent weight, so we can mock it with an empty one - let signed_bitfields = Vec::new(); + let mut signed_bitfields = Vec::new(); // backed candidates must not be empty, so we can demonstrate that the weight has not changed - let backed_candidates = vec![BackedCandidate::default(); 10]; + let mut backed_candidates = vec![BackedCandidate::default(); 10]; // the expected weight with no blocks is just the minimum weight let expected_weight = MINIMAL_INCLUSION_INHERENT_WEIGHT; @@ -504,14 +791,22 @@ mod tests { let max_block_weight = ::BlockWeights::get().max_block; let used_block_weight = max_block_weight + 1; + System::set_block_consumed_resources(used_block_weight, 0); + let mut disputes = Vec::new(); + limit_paras_inherent::( + &mut disputes, + &mut signed_bitfields, + &mut backed_candidates, + ); + // execute the paras inherent let post_info = Call::::enter { data: ParachainsInherentData { bitfields: signed_bitfields, backed_candidates, - disputes: Vec::new(), + disputes, parent_header: header, }, } diff --git a/runtime/parachains/src/runtime_api_impl/v1.rs b/runtime/parachains/src/runtime_api_impl/v1.rs index 5909ea55290b..077c51f92a46 100644 --- a/runtime/parachains/src/runtime_api_impl/v1.rs +++ b/runtime/parachains/src/runtime_api_impl/v1.rs @@ -18,12 +18,12 @@ //! functions. use crate::{ - configuration, dmp, hrmp, inclusion, initializer, paras, paras_inherent, scheduler, + configuration, disputes, dmp, hrmp, inclusion, initializer, paras, paras_inherent, scheduler, session_info, shared, }; use primitives::v1::{ - AuthorityDiscoveryId, CandidateEvent, CommittedCandidateReceipt, CoreIndex, CoreOccupied, - CoreState, GroupIndex, GroupRotationInfo, Id as ParaId, InboundDownwardMessage, + AuthorityDiscoveryId, CandidateEvent, CandidateHash, CommittedCandidateReceipt, CoreIndex, + CoreOccupied, CoreState, GroupIndex, GroupRotationInfo, Id as ParaId, InboundDownwardMessage, InboundHrmpMessage, OccupiedCore, OccupiedCoreAssumption, PersistedValidationData, ScheduledCore, ScrapedOnChainVotes, SessionIndex, SessionInfo, ValidationCode, ValidationCodeHash, ValidatorId, ValidatorIndex, @@ -335,3 +335,11 @@ pub fn validation_code_by_hash( pub fn on_chain_votes() -> Option> { >::on_chain_votes() } + +/// Query candidate included state. +pub fn candidate_included_state( + session_index: SessionIndex, + candidate_hash: CandidateHash, +) -> Option { + >::included_state(session_index, candidate_hash) +} diff --git a/runtime/rococo/src/lib.rs b/runtime/rococo/src/lib.rs index 6d96591dc9ba..35896da59d83 100644 --- a/runtime/rococo/src/lib.rs +++ b/runtime/rococo/src/lib.rs @@ -1634,6 +1634,7 @@ sp_api::impl_runtime_apis! { add_benchmark!(params, batches, runtime_parachains::configuration, Configuration); add_benchmark!(params, batches, runtime_parachains::disputes, ParasDisputes); add_benchmark!(params, batches, runtime_parachains::paras, Paras); + add_benchmark!(params, batches, runtime_parachains::inclusion, Paras); if batches.is_empty() { return Err("Benchmark not found for this pallet.".into()) } Ok(batches)